diff -Nru django-tastypie-0.12.0/AUTHORS django-tastypie-0.13.3/AUTHORS --- django-tastypie-0.12.0/AUTHORS 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/AUTHORS 2016-02-14 09:15:23.000000000 +0000 @@ -84,7 +84,21 @@ * Revolution Systems & The Python Software Foundation for funding a significant portion of the port to Python 3! * tunix for a patch related to Tastypie's timezone-aware dates. * Steven Davidson (damycra) for a documentation patch. - +* Harish Srinivas for a minor bug fix that raises an exception if a given related resource is none. +* Renjith Thankachan for a patch that fixes `django.utils.importlib` error in Django 1.9+ release. +* Sean Hayes (SeanHayes) for test suite and CI enhancements, various patches +* Michael Thornhill (mthornhill) for strict dict checking bugfix +* Fedor Baart (siggyf) for an update on the serializer +* Alexey Kotlyarov (koterpillar) - fixing DateField/DateTimeField. +* Yuri Govorushchenko (metametadata) for documentation fixes. +* Guilhem Saurel (Nim65s) for a minor issue with Django 1.9 +* Jack Cushman (jcushman) for converting ResourceTestCase to ResourceTestCaseMixin. +* John Lucas (jlucas91) for an improvement to the response for requests with invalid JSON. +* Judit Novak (juditnovak) for related schema updates +* Mike Bryant (mikebryant) for unit tests +* Maxim Filipenko (prokaktus) for changing repr value to str in Python 2 +* Thomas Yip (thomasyip) PR #1409 +* Sam Thompson(georgedorn) for fixing some docs and tests. Thanks to Tav for providing validate_jsonp.py, placed in public domain. diff -Nru django-tastypie-0.12.0/BACKWARDS-INCOMPATIBLE.txt django-tastypie-0.13.3/BACKWARDS-INCOMPATIBLE.txt --- django-tastypie-0.12.0/BACKWARDS-INCOMPATIBLE.txt 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/BACKWARDS-INCOMPATIBLE.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -Master (v0.9.16) -================ - -[2012-12-11] abc0bef - Changed response code of PUT with always_return_data=True from 202 to 200 - - -v0.9.13 -======= - -[2013-03-15] 2dff249 - Authorization methods that were previously expected to raise ``Unauthorized`` must now always return ``True`` if the authorization succeeds - - -v0.9.12 -======= - -[2013-02-14] 76c4f15 - JSONP is disabled by default -[2013-02-14] 3ee47fb - Tastypie no longer ever returns HTML errors -[2013-02-06] d850758 - Heavy rewrite of ``Authorization`` which adds a variety of new methods -[2012-08-04] 651f964 - The addition of ``SessionAuthentication`` requires Django 1.3+ -[2012-06-26] a57d85b - ``Authentication`` classes now require ``User.is_active = True`` -[2012-05-04] 84821d7 - AutoFields will now serialize and deserialize as Integers - - -Backward-incompatible commits were not recorded prior to the release of v0.9.11 -but may be found using ``git grep --oneline ...v0.9.11 | grep "BACKWARD-"``. \ No newline at end of file diff -Nru django-tastypie-0.12.0/CONTRIBUTING django-tastypie-0.13.3/CONTRIBUTING --- django-tastypie-0.12.0/CONTRIBUTING 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/CONTRIBUTING 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -============ -Contributing -============ - -Tastypie is open-source and, as such, grows (or shrinks) & improves in part -due to the community. Below are some guidelines on how to help with the project. - - -Philosophy -========== - -* Tastypie is BSD-licensed. All contributed code must be either - - * the original work of the author, contributed under the BSD, or... - * work taken from another project released under a BSD-compatible license. - -* GPL'd (or similar) works are not eligible for inclusion. -* Tastypie's git master branch should always be stable, production-ready & - passing all tests. -* Major releases (1.x.x) are commitments to backward-compatibility of the public APIs. - Any documented API should ideally not change between major releases. - The exclusion to this rule is in the event of either a security issue - or to accommodate changes in Django itself. -* Minor releases (x.3.x) are for the addition of substantial features or major - bugfixes. -* Patch releases (x.x.4) are for minor features or bugfixes. - - -Guidelines For Reporting An Issue/Feature -========================================= - -So you've found a bug or have a great idea for a feature. Here's the steps you -should take to help get it added/fixed in Tastypie: - -* First, check to see if there's an existing issue/pull request for the - bug/feature. All issues are at https://github.com/toastdriven/django-tastypie/issues - and pull reqs are at https://github.com/toastdriven/django-tastypie/pulls. -* If there isn't one there, please file an issue. The ideal report includes: - - * A description of the problem/suggestion. - * How to recreate the bug. - * If relevant, including the versions of your: - - * Python interpreter - * Django - * Tastypie - * Optionally of the other dependencies involved - - * Ideally, creating a pull request with a (failing) test case demonstrating - what's wrong. This makes it easy for us to reproduce & fix the problem. - Instructions for running the tests are at :doc:`index` - -You might also hop into the IRC channel (``#tastypie`` on ``irc.freenode.net``) -& raise your question there, as there may be someone who can help you with a -work-around. - - -Guidelines For Contributing Code -================================ - -If you're ready to take the plunge & contribute back some code/docs, the -process should look like: - -* Fork the project on GitHub into your own account. -* Clone your copy of Tastypie. -* Make a new branch in git & commit your changes there. -* Push your new branch up to GitHub. -* Again, ensure there isn't already an issue or pull request out there on it. - If there is & you feel you have a better fix, please take note of the issue - number & mention it in your pull request. -* Create a new pull request (based on your branch), including what the - problem/feature is, versions of your software & referencing any related - issues/pull requests. - -In order to be merged into Tastypie, contributions must have the following: - -* A solid patch that: - - * is clear. - * works across all supported versions of Python/Django. - * follows the existing style of the code base (mostly PEP-8). - * comments included as needed. - -* A test case that demonstrates the previous flaw that now passes - with the included patch. -* If it adds/changes a public API, it must also include documentation - for those changes. -* Must be appropriately licensed (see "Philosophy"). -* Adds yourself to the AUTHORS file. - -If your contribution lacks any of these things, they will have to be added -by a core contributor before being merged into Tastypie proper, which may take -substantial time for the all-volunteer team to get to. - - -Guidelines For Core Contributors -================================ - -If you've been granted the commit bit, here's how to shepherd the changes in: - -* Any time you go to work on Tastypie, please use ``git pull --rebase`` to fetch - the latest changes. -* Any new features/bug fixes must meet the above guidelines for contributing - code (solid patch/tests passing/docs included). -* Commits are typically cherry-picked onto a branch off master. - - * This is done so as not to include extraneous commits, as some people submit - pull reqs based on their git master that has other things applied to it. - -* A set of commits should be squashed down to a single commit. - - * ``git merge --squash`` is a good tool for performing this, as is - ``git rebase -i HEAD~N``. - * This is done to prevent anyone using the git repo from accidently pulling - work-in-progress commits. - -* Commit messages should use past tense, describe what changed & thank anyone - involved. Examples:: - - """Added a new way to do per-object authorization.""" - """Fixed a bug in ``Serializer.to_xml``. Thanks to joeschmoe for the report!""" - """BACKWARD-INCOMPATIBLE: Altered the arguments passed to ``Bundle.__init__``. - - Further description appears here if the change warrants an explanation - as to why it was done.""" - -* For any patches applied from a contributor, please ensure their name appears - in the AUTHORS file. -* When closing issues or pull requests, please reference the SHA in the closing - message (i.e. ``Thanks! Fixed in SHA: 6b93f6``). GitHub will automatically - link to it. diff -Nru django-tastypie-0.12.0/debian/changelog django-tastypie-0.13.3/debian/changelog --- django-tastypie-0.12.0/debian/changelog 2015-07-22 06:39:42.000000000 +0000 +++ django-tastypie-0.13.3/debian/changelog 2016-06-02 14:37:09.000000000 +0000 @@ -1,3 +1,9 @@ +django-tastypie (0.13.3-1ubuntu1) xenial; urgency=medium + + * new upstream release with support for Django 1.8 + + -- Jason Hobbs Thu, 02 Jun 2016 09:36:01 -0500 + django-tastypie (0.12.0-1build1) wily; urgency=medium * No-change rebuild for python3.5 transition diff -Nru django-tastypie-0.12.0/debian/patches/fixups.patch django-tastypie-0.13.3/debian/patches/fixups.patch --- django-tastypie-0.12.0/debian/patches/fixups.patch 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/debian/patches/fixups.patch 2016-06-02 14:43:12.000000000 +0000 @@ -0,0 +1,57 @@ +Description: + TODO: Put a short summary on the line above and replace this paragraph + with a longer explanation of this change. Complete the meta-information + with other relevant fields (see below for details). To make it easier, the + information below has been extracted from the changelog. Adjust it or drop + it. + . + django-tastypie (0.13.3-1ubuntu1) xenial; urgency=medium + . + * new upstream release with support for Django 1.8 +Author: Jason Hobbs + +--- +The information above should follow the Patch Tagging Guidelines, please +checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here +are templates for supplementary fields that you might want to add: + +Origin: , +Bug: +Bug-Debian: https://bugs.debian.org/ +Bug-Ubuntu: https://launchpad.net/bugs/ +Forwarded: +Reviewed-By: +Last-Update: + +--- django-tastypie-0.13.3.orig/django_tastypie.egg-info/SOURCES.txt ++++ django-tastypie-0.13.3/django_tastypie.egg-info/SOURCES.txt +@@ -42,22 +42,14 @@ docs/tutorial.rst + docs/validation.rst + docs/who_uses.rst + docs/code/authentication.py +-docs/code/authentication.pyc + docs/code/manage.py + docs/code/myapp/__init__.py +-docs/code/myapp/__init__.pyc + docs/code/myapp/models.py +-docs/code/myapp/models.pyc + docs/code/myapp/urls.py +-docs/code/myapp/urls.pyc + docs/code/myapp/api/__init__.py +-docs/code/myapp/api/__init__.pyc + docs/code/myapp/api/resources.py +-docs/code/myapp/api/resources.pyc + docs/code/myproject/__init__.py +-docs/code/myproject/__init__.pyc + docs/code/myproject/settings.py +-docs/code/myproject/settings.pyc + docs/code/myproject/urls.py + docs/code/myproject/wsgi.py + docs/release_notes/dev.rst +--- django-tastypie-0.13.3.orig/django_tastypie.egg-info/requires.txt ++++ django-tastypie-0.13.3/django_tastypie.egg-info/requires.txt +@@ -1,2 +1,2 @@ + python-mimeparse >= 0.1.4, != 1.5 +-python-dateutil >= 1.5, != 2.0 +\ No newline at end of file ++python-dateutil >= 1.5, != 2.0 diff -Nru django-tastypie-0.12.0/debian/patches/series django-tastypie-0.13.3/debian/patches/series --- django-tastypie-0.12.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/debian/patches/series 2016-06-02 14:43:12.000000000 +0000 @@ -0,0 +1 @@ +fixups.patch diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/dependency_links.txt django-tastypie-0.13.3/django_tastypie.egg-info/dependency_links.txt --- django-tastypie-0.12.0/django_tastypie.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/dependency_links.txt 2016-02-17 13:10:52.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/not-zip-safe django-tastypie-0.13.3/django_tastypie.egg-info/not-zip-safe --- django-tastypie-0.12.0/django_tastypie.egg-info/not-zip-safe 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/not-zip-safe 2016-02-06 11:47:21.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/pbr.json django-tastypie-0.13.3/django_tastypie.egg-info/pbr.json --- django-tastypie-0.12.0/django_tastypie.egg-info/pbr.json 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/pbr.json 2016-02-12 05:26:31.000000000 +0000 @@ -0,0 +1 @@ +{"is_release": false, "git_version": "3d946f6"} \ No newline at end of file diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/PKG-INFO django-tastypie-0.13.3/django_tastypie.egg-info/PKG-INFO --- django-tastypie-0.12.0/django_tastypie.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/PKG-INFO 2016-02-17 13:10:52.000000000 +0000 @@ -0,0 +1,170 @@ +Metadata-Version: 1.1 +Name: django-tastypie +Version: 0.13.3 +Summary: A flexible & capable API layer for Django. +Home-page: https://github.com/django-tastypie/django-tastypie +Author: Daniel Lindsley +Author-email: daniel@toastdriven.com +License: UNKNOWN +Description: =============== + django-tastypie + =============== + + .. image:: https://readthedocs.org/projects/django-tastypie/badge/ + :target: https://django-tastypie.readthedocs.org/ + :alt: Docs + + .. image:: https://travis-ci.org/django-tastypie/django-tastypie.svg + :target: https://travis-ci.org/django-tastypie/django-tastypie + :alt: CI + + .. image:: https://coveralls.io/repos/django-tastypie/django-tastypie/badge.svg?service=github + :target: https://coveralls.io/github/django-tastypie/django-tastypie + :alt: Code Coverage + + .. image:: https://img.shields.io/pypi/v/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Version + + .. image:: https://img.shields.io/pypi/dm/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Downloads + + Creating delicious APIs for Django apps since 2010. + + Currently in beta (v0.13.3) but being used actively in production on several + sites. + + + Requirements + ============ + + Core + ---- + + * Python 2.7+ or Python 3.4+ + * Django 1.7 through Django 1.9 + * dateutil (http://labix.org/python-dateutil) >= 2.1 + + Format Support + -------------- + + * XML: lxml 3 (http://lxml.de/) and defusedxml (https://pypi.python.org/pypi/defusedxml) + * YAML: pyyaml (http://pyyaml.org/) + * binary plist: biplist (http://explorapp.com/biplist/) + + Optional + -------- + + * HTTP Digest authentication: python3-digest (https://bitbucket.org/akoha/python-digest/) + + + What's It Look Like? + ==================== + + A basic example looks like: + + .. code:: python + + # myapp/api.py + # ============ + from tastypie.resources import ModelResource + from myapp.models import Entry + + + class EntryResource(ModelResource): + class Meta: + queryset = Entry.objects.all() + + + # urls.py + # ======= + from django.conf.urls import url, include + from tastypie.api import Api + from myapp.api import EntryResource + + v1_api = Api(api_name='v1') + v1_api.register(EntryResource()) + + urlpatterns = [ + # The normal jazz here then... + url(r'^api/', include(v1_api.urls)), + ] + + That gets you a fully working, read-write API for the ``Entry`` model that + supports all CRUD operations in a RESTful way. JSON/XML/YAML support is already + there, and it's easy to add related data/authentication/caching. + + You can find more in the documentation at + http://django-tastypie.readthedocs.org/. + + + Why Tastypie? + ============= + + There are other API frameworks out there for Django. You need to + assess the options available and decide for yourself. That said, here are some + common reasons for tastypie. + + * You need an API that is RESTful and uses HTTP well. + * You want to support deep relations. + * You DON'T want to have to write your own serializer to make the output right. + * You want an API framework that has little magic, very flexible and maps well to + the problem domain. + * You want/need XML serialization that is treated equally to JSON (and YAML is + there too). + + + Reference Material + ================== + + * https://django-tastypie.readthedocs.org/en/latest/ + * https://github.com/django-tastypie/django-tastypie/tree/master/tests/basic shows + basic usage of tastypie + * http://en.wikipedia.org/wiki/REST + * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes + * http://www.ietf.org/rfc/rfc2616.txt + * http://jacobian.org/writing/rest-worst-practices/ + + + Getting Help + ============ + + There are two primary ways of getting help. + + 1. Go to `StackOverflow`_ and post a question with the ``tastypie`` tag. + 2. We have an IRC channel (`#tastypie on irc.freenode.net`_) to get help, + bounce an idea by us, or generally shoot the breeze. + + .. _`StackOverflow`: https://stackoverflow.com/questions/tagged/tastypie + .. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie + + + Security + ======== + + Tastypie is committed to providing a flexible and secure API, and was designed + with many security features and options in mind. Due to the complex nature of + APIs and the constant discovery of new attack vectors and vulnerabilities, + no software is immune to security holes. We rely on our community to report + and help us investigate security issues. + + If you come across a security hole **please do not open a Github issue**. + Instead, **drop us an email** at ``tastypie-security@googlegroups.com`` + + We'll then work together to investigate and resolve the problem so we can + announce a solution along with the vulnerability. + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Utilities +Requires: python_mimeparse(>=0.1.4, !=1.5) +Requires: dateutil(>=1.5, !=2.0) diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/requires.txt django-tastypie-0.13.3/django_tastypie.egg-info/requires.txt --- django-tastypie-0.12.0/django_tastypie.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/requires.txt 2016-02-17 13:10:52.000000000 +0000 @@ -0,0 +1,2 @@ +python-mimeparse >= 0.1.4, != 1.5 +python-dateutil >= 1.5, != 2.0 \ No newline at end of file diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/SOURCES.txt django-tastypie-0.13.3/django_tastypie.egg-info/SOURCES.txt --- django-tastypie-0.12.0/django_tastypie.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/SOURCES.txt 2016-02-17 13:12:45.000000000 +0000 @@ -0,0 +1,118 @@ +AUTHORS +LICENSE +MANIFEST.in +README.rst +setup.cfg +setup.py +django_tastypie.egg-info/PKG-INFO +django_tastypie.egg-info/SOURCES.txt +django_tastypie.egg-info/dependency_links.txt +django_tastypie.egg-info/not-zip-safe +django_tastypie.egg-info/pbr.json +django_tastypie.egg-info/requires.txt +django_tastypie.egg-info/top_level.txt +docs/Makefile +docs/api.rst +docs/authentication.rst +docs/authorization.rst +docs/bundles.rst +docs/caching.rst +docs/compatibility_notes.rst +docs/conf.py +docs/content_types.rst +docs/contributing.rst +docs/cookbook.rst +docs/debugging.rst +docs/fields.rst +docs/geodjango.rst +docs/index.rst +docs/interacting.rst +docs/namespaces.rst +docs/non_orm_data_sources.rst +docs/paginator.rst +docs/python3.rst +docs/resources.rst +docs/serialization.rst +docs/settings.rst +docs/testing.rst +docs/throttling.rst +docs/toc.rst +docs/tools.rst +docs/tutorial.rst +docs/validation.rst +docs/who_uses.rst +docs/code/authentication.py +docs/code/authentication.pyc +docs/code/manage.py +docs/code/myapp/__init__.py +docs/code/myapp/__init__.pyc +docs/code/myapp/models.py +docs/code/myapp/models.pyc +docs/code/myapp/urls.py +docs/code/myapp/urls.pyc +docs/code/myapp/api/__init__.py +docs/code/myapp/api/__init__.pyc +docs/code/myapp/api/resources.py +docs/code/myapp/api/resources.pyc +docs/code/myproject/__init__.py +docs/code/myproject/__init__.pyc +docs/code/myproject/settings.py +docs/code/myproject/settings.pyc +docs/code/myproject/urls.py +docs/code/myproject/wsgi.py +docs/release_notes/dev.rst +docs/release_notes/index.rst +docs/release_notes/v0.10.0.rst +docs/release_notes/v0.11.0.rst +docs/release_notes/v0.11.1.rst +docs/release_notes/v0.12.0.rst +docs/release_notes/v0.12.1.rst +docs/release_notes/v0.12.2.rst +docs/release_notes/v0.13.0.rst +docs/release_notes/v0.13.1.rst +docs/release_notes/v0.13.2.rst +docs/release_notes/v0.13.3.rst +docs/release_notes/v0.9.13.rst +docs/release_notes/v0.9.14.rst +docs/release_notes/v0.9.15.rst +docs/release_notes/v0.9.16.rst +tastypie/__init__.py +tastypie/admin.py +tastypie/api.py +tastypie/authentication.py +tastypie/authorization.py +tastypie/bundle.py +tastypie/cache.py +tastypie/compat.py +tastypie/constants.py +tastypie/exceptions.py +tastypie/fields.py +tastypie/http.py +tastypie/models.py +tastypie/paginator.py +tastypie/resources.py +tastypie/serializers.py +tastypie/test.py +tastypie/throttle.py +tastypie/validation.py +tastypie/contrib/__init__.py +tastypie/contrib/contenttypes/__init__.py +tastypie/contrib/contenttypes/fields.py +tastypie/contrib/contenttypes/resources.py +tastypie/contrib/gis/__init__.py +tastypie/contrib/gis/resources.py +tastypie/management/__init__.py +tastypie/management/commands/__init__.py +tastypie/management/commands/backfill_api_keys.py +tastypie/migrations/0001_initial.py +tastypie/migrations/__init__.py +tastypie/templates/tastypie/basic.html +tastypie/templates/tastypie/detail.html +tastypie/templates/tastypie/list.html +tastypie/utils/__init__.py +tastypie/utils/dict.py +tastypie/utils/formatting.py +tastypie/utils/mime.py +tastypie/utils/timezone.py +tastypie/utils/urls.py +tastypie/utils/validate_jsonp.py \ No newline at end of file diff -Nru django-tastypie-0.12.0/django_tastypie.egg-info/top_level.txt django-tastypie-0.13.3/django_tastypie.egg-info/top_level.txt --- django-tastypie-0.12.0/django_tastypie.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/django_tastypie.egg-info/top_level.txt 2016-02-17 13:10:52.000000000 +0000 @@ -0,0 +1 @@ +tastypie diff -Nru django-tastypie-0.12.0/docs/api.rst django-tastypie-0.13.3/docs/api.rst --- django-tastypie-0.12.0/docs/api.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/api.rst 2016-02-14 09:14:12.000000000 +0000 @@ -17,6 +17,7 @@ A sample api definition might look something like (usually located in a URLconf):: + from django.conf.urls import url, include from tastypie.api import Api from myapp.api.resources import UserResource, EntryResource @@ -25,10 +26,11 @@ v1_api.register(EntryResource()) # Standard bits... - urlpatterns = patterns('', - (r'^api/', include(v1_api.urls)), - ) + urlpatterns = [ + url(r'^api/', include(v1_api.urls)), + ] +For namespaced urls see :ref:`namespaces` ``Api`` Methods =============== @@ -83,8 +85,7 @@ A hook for adding your own URLs or matching before the default URLs. Useful for adding custom endpoints or overriding the built-in ones. -Should return a list of individual URLconf lines (**NOT** wrapped in -``patterns``). +Should return a list of individual URLconf lines. ``urls`` ~~~~~~~~ @@ -103,4 +104,3 @@ A view that returns a serialized list of all resources registers to the ``Api``. Useful for discovery. - diff -Nru django-tastypie-0.12.0/docs/authentication.rst django-tastypie-0.13.3/docs/authentication.rst --- django-tastypie-0.12.0/docs/authentication.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/authentication.rst 2016-02-14 09:10:26.000000000 +0000 @@ -94,10 +94,10 @@ objects. Hooking it up looks like:: from django.contrib.auth.models import User - from django.db import models + from django.db.models import signals from tastypie.models import create_api_key - models.signals.post_save.connect(create_api_key, sender=User) + signals.post_save.connect(create_api_key, sender=User) .. warning:: diff -Nru django-tastypie-0.12.0/docs/code/authentication.py django-tastypie-0.13.3/docs/code/authentication.py --- django-tastypie-0.12.0/docs/code/authentication.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/authentication.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,4 @@ +import mock + + +OAuth20Authentication = mock.Mock() Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/authentication.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/authentication.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/manage.py django-tastypie-0.13.3/docs/code/manage.py --- django-tastypie-0.12.0/docs/code/manage.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/manage.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myapp/api/__init__.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myapp/api/__init__.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/myapp/api/resources.py django-tastypie-0.13.3/docs/code/myapp/api/resources.py --- django-tastypie-0.12.0/docs/code/myapp/api/resources.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myapp/api/resources.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,8 @@ +from tastypie.resources import ModelResource + +from ..models import User + + +class UserResource(ModelResource): + class Meta: + object_class = User.objects.all() Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myapp/api/resources.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myapp/api/resources.pyc differ Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myapp/__init__.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myapp/__init__.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/myapp/models.py django-tastypie-0.13.3/docs/code/myapp/models.py --- django-tastypie-0.12.0/docs/code/myapp/models.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myapp/models.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,8 @@ +import mock + +from django.contrib.auth.models import User # flake8: noqa + + +Choice = mock.MagicMock() +Poll = mock.MagicMock() +MyModel = mock.MagicMock() Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myapp/models.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myapp/models.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/myapp/urls.py django-tastypie-0.13.3/docs/code/myapp/urls.py --- django-tastypie-0.12.0/docs/code/myapp/urls.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myapp/urls.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,4 @@ +from tastypie.api import Api + + +my_api = Api() Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myapp/urls.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myapp/urls.pyc differ Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myproject/__init__.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myproject/__init__.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/myproject/settings.py django-tastypie-0.13.3/docs/code/myproject/settings.py --- django-tastypie-0.12.0/docs/code/myproject/settings.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myproject/settings.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,85 @@ +""" +Django settings for myproject project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '%ovvje%lh&k-%0v!@_c1gygt#aq-!o3*t$(hpee7@aj&35cr3a' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myapp', + 'tastypie', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'myproject.urls' + +WSGI_APPLICATION = 'myproject.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' Binary files /tmp/tmpNxMVKq/mHTeSNYPtA/django-tastypie-0.12.0/docs/code/myproject/settings.pyc and /tmp/tmpNxMVKq/_9_cxiui0G/django-tastypie-0.13.3/docs/code/myproject/settings.pyc differ diff -Nru django-tastypie-0.12.0/docs/code/myproject/urls.py django-tastypie-0.13.3/docs/code/myproject/urls.py --- django-tastypie-0.12.0/docs/code/myproject/urls.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myproject/urls.py 2016-02-14 09:15:23.000000000 +0000 @@ -0,0 +1,10 @@ +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + # Examples: + # url(r'^$', 'myproject.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + + url(r'^admin/', include(admin.site.urls)), +] diff -Nru django-tastypie-0.12.0/docs/code/myproject/wsgi.py django-tastypie-0.13.3/docs/code/myproject/wsgi.py --- django-tastypie-0.12.0/docs/code/myproject/wsgi.py 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/code/myproject/wsgi.py 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,14 @@ +""" +WSGI config for myproject project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") + +from django.core.wsgi import get_wsgi_application # flake8: noqa +application = get_wsgi_application() diff -Nru django-tastypie-0.12.0/docs/compatibility_notes.rst django-tastypie-0.13.3/docs/compatibility_notes.rst --- django-tastypie-0.12.0/docs/compatibility_notes.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/compatibility_notes.rst 2016-02-14 09:14:12.000000000 +0000 @@ -19,7 +19,7 @@ to the way MySQL works & the way Django generates index names, this migration would fail miserably on many MySQL installs. -If you are using MySQL, South & the ``ApiKey`` authentication class, you should +If you are using MySQL & the ``ApiKey`` authentication class, you may need to manually add an index for the the ``ApiKey.key`` field. Something to the effect of:: diff -Nru django-tastypie-0.12.0/docs/conf.py django-tastypie-0.13.3/docs/conf.py --- django-tastypie-0.12.0/docs/conf.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/conf.py 2016-02-14 09:15:23.000000000 +0000 @@ -3,7 +3,8 @@ # Tastypie documentation build configuration file, created by # sphinx-quickstart on Sat May 22 21:44:34 2010. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its containing +# dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,18 +12,29 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +# sys.path.append(os.path.abspath('.')) + +import datetime +import os +import sys + +sys.path.append('..') + +from tastypie import __short_version__, __version__ # flake8: noqa -# -- General configuration ----------------------------------------------------- +docs_path = os.path.dirname(__file__) +doctest_path = [os.path.join(docs_path, 'code'), os.path.join(docs_path, '..')] -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.doctest', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -31,91 +43,101 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. master_doc = 'toc' # General information about the project. project = u'Tastypie' -copyright = u'2010-2014, Daniel Lindsley & the Tastypie core team' +copyright = u'2010-%s, Daniel Lindsley & the Tastypie core team' %\ + datetime.date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.12.0' +version = __short_version__ # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' +try: + import sphinx_rtd_theme +except ImportError: + pass +else: + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -124,81 +146,81 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Tastypiedoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, +# author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Tastypie.tex', u'Tastypie Documentation', - u'Daniel Lindsley \\& the Tastypie core team', 'manual'), + ('index', 'Tastypie.tex', u'Tastypie Documentation', + u'Daniel Lindsley \\& the Tastypie core team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True -# -- Options for Texinfo output ------------------------------------------------ +# -- Options for Texinfo output ----------------------------------------------- # List of tuples (startdocname, targetname, title, author, dir_entry, # description, category, toctree_only) -texinfo_documents=[( +texinfo_documents = [( master_doc, "django-tastypie", "", "", "Tastypie", "Documentation of the Tastypie framework", "Web development", False )] - diff -Nru django-tastypie-0.12.0/docs/contributing.rst django-tastypie-0.13.3/docs/contributing.rst --- django-tastypie-0.12.0/docs/contributing.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/contributing.rst 2016-02-14 09:14:12.000000000 +0000 @@ -33,8 +33,8 @@ should take to help get it added/fixed in Tastypie: * First, check to see if there's an existing issue/pull request for the - bug/feature. All issues are at https://github.com/toastdriven/django-tastypie/issues - and pull reqs are at https://github.com/toastdriven/django-tastypie/pulls. + bug/feature. All issues are at https://github.com/django-tastypie/django-tastypie/issues + and pull reqs are at https://github.com/django-tastypie/django-tastypie/pulls. * If there isn't one there, please file an issue. The ideal report includes: * A description of the problem/suggestion. @@ -88,6 +88,15 @@ * Must be appropriately licensed (see "Philosophy"). * Adds yourself to the AUTHORS file. +Please also: + +* Unless your change only modifies the documentation, add the issue you're + solving to the list in docs/release_notes/dev.rst, include issue and PR + numbers. +* Squash your changes down to a single commit, or down to one commit containing + your failing tests and one more commit containing the fix that makes those + tests pass. + If your contribution lacks any of these things, they will have to be added by a core contributor before being merged into Tastypie proper, which may take substantial time for the all-volunteer team to get to. diff -Nru django-tastypie-0.12.0/docs/cookbook.rst django-tastypie-0.13.3/docs/cookbook.rst --- django-tastypie-0.12.0/docs/cookbook.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/cookbook.rst 2016-02-14 09:15:23.000000000 +0000 @@ -9,15 +9,28 @@ It is common to use django to provision OAuth 2.0 tokens for users and then have Tasty Pie use these tokens to authenticate users to the API. `Follow this tutorial `_ and `use this custom authentication class `_ to enable -OAuth 2.0 authentication with Tasty Pie.:: +OAuth 2.0 authentication with Tasty Pie. + +.. testsetup:: + + import os + import django + from django.core.management import call_command + + os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings' + django.setup() + call_command('migrate', verbosity=0) + +.. testcode:: # api.py - from tastypie.resources import ModelResource - from tastypie.authorization import DjangoAuthorization - from polls.models import Poll, Choice from tastypie import fields + from tastypie.authorization import DjangoAuthorization + from tastypie.resources import ModelResource, Resource + from myapp.models import Poll, Choice from authentication import OAuth20Authentication + class ChoiceResource(ModelResource): class Meta: queryset = Choice.objects.all() @@ -25,14 +38,21 @@ authorization = DjangoAuthorization() authentication = OAuth20Authentication() + class PollResource(ModelResource): choices = fields.ToManyField(ChoiceResource, 'choice_set', full=True) + class Meta: queryset = Poll.objects.all() resource_name = 'poll' authorization = DjangoAuthorization() authentication = OAuth20Authentication() - + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Adding Custom Values -------------------- @@ -40,32 +60,53 @@ You might encounter cases where you wish to include additional data in a response which is not obtained from a field or method on your model. You can easily extend the :meth:`~tastypie.resources.Resource.dehydrate` method to -provide additional values:: +provide additional values: + +.. testcode:: + + from myapp.models import MyModel + class MyModelResource(Resource): class Meta: - qs = MyModel.objects.all() + queryset = MyModel.objects.all() def dehydrate(self, bundle): bundle.data['custom_field'] = "Whatever you want" return bundle +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Per-Request Alterations To The Queryset --------------------------------------- A common pattern is needing to limit a queryset by something that changes per-request, for instance the date/time. You can accomplish this by lightly -modifying ``get_object_list``:: +modifying ``get_object_list``: + +.. testcode:: + + from django.utils import timezone + from myapp.models import MyModel - from tastypie.utils import now - class MyResource(ModelResource): + class MyModelResource(ModelResource): class Meta: - queryset = MyObject.objects.all() + queryset = MyModel.objects.all() def get_object_list(self, request): - return super(MyResource, self).get_object_list(request).filter(start_date__gte=now) + return super(MyModelResource, self).get_object_list(request).filter(start_date__gte=timezone.now()) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Using Your ``Resource`` In Regular Views @@ -74,7 +115,10 @@ In addition to using your resource classes to power the API, you can also use them to write other parts of your application, such as your views. For instance, if you wanted to encode user information in the page for some -Javascript's use, you could do the following:: +Javascript's use, you could do the following. In this case, ``user_json`` will +not include a valid ``resource_uri``: + +.. testcode:: # views.py from django.shortcuts import render_to_response @@ -82,18 +126,76 @@ def user_detail(request, username): - ur = UserResource() - user = ur.obj_get(username=username) + res = UserResource() + request_bundle = res.build_bundle(request=request) + user = res.obj_get(request_bundle, username=username) # Other things get prepped to go into the context then... - ur_bundle = ur.build_bundle(obj=user, request=request) - return render_to_response('myapp/user_detail.html', { + user_bundle = res.build_bundle(request=request, obj=user) + user_json = res.serialize(None, res.full_dehydrate(user_bundle), "application/json") + + return render_to_response("myapp/user_detail.html", { # Other things here. - "user_json": ur.serialize(None, ur.full_dehydrate(ur_bundle), 'application/json'), + "user_json": user_json, }) -Example of getting a list of users:: +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + +To include a valid ``resource_uri``, the resource must be associated +with an ``tastypie.Api`` instance, as below: + +.. testcode:: + + # urls.py + from tastypie.api import Api + from myapp.api.resources import UserResource + + + my_api = Api(api_name='v1') + my_api.register(UserResource()) + + # views.py + from myapp.urls import my_api + + + def user_detail(request, username): + res = my_api.canonical_resource_for('user') + # continue as above... + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + +Alternatively, to get a valid ``resource_uri`` you may pass in the ``api_name`` +parameter directly to the Resource: + +.. testcode:: + + # views.py + from django.shortcuts import render_to_response + from myapp.api.resources import UserResource + + + def user_detail(request, username): + res = UserResource(api_name='v1') + # continue as above... + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + +Example of getting a list of users: + +.. testcode:: def user_list(request): res = UserResource() @@ -112,10 +214,16 @@ "list_json": list_json, }) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Then in template you could convert JSON into JavaScript object:: @@ -129,9 +237,15 @@ for the lookup. For example, if you want to expose ``User`` resources by username, you can do -something like the following:: +something like the following: + +.. testcode:: # myapp/api/resources.py + from django.conf.urls import url + from django.contrib.auth.models import User + + class UserResource(ModelResource): class Meta: queryset = User.objects.all() @@ -142,34 +256,49 @@ url(r"^(?P%s)/(?P[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), ] +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + The added URLconf matches before the standard URLconf included by default & matches on the username provided in the URL. -Another alternative approach is to override the ``dispatch`` method:: +Another alternative approach is to override the ``dispatch`` method: + +.. testcode:: # myapp/api/resources.py - class EntryResource(ModelResource): + from myapp.models import MyModel + + class MyModelResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: - queryset = Entry.objects.all() - resource_name = 'entry' + queryset = MyModel.objects.all() + resource_name = 'mymodel' def dispatch(self, request_type, request, **kwargs): username = kwargs.pop('username') kwargs['user'] = get_object_or_404(User, username=username) - return super(EntryResource, self).dispatch(request_type, request, **kwargs) + return super(MyModelResource, self).dispatch(request_type, request, **kwargs) # urls.py - from django.conf.urls.defaults import * - from myapp.api import EntryResource + from django.conf.urls import url, include - entry_resource = EntryResource() + mymodel_resource = MyModelResource() - urlpatterns = patterns('', + urlpatterns = [ # The normal jazz here, then... - (r'^api/(?P\w+)/', include(entry_resource.urls)), - ) + url(r'^api/(?P\w+)/', include(mymodel_resource.urls)), + ] + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Nested Resources @@ -177,7 +306,14 @@ You can also do "nested resources" (resources within another related resource) by lightly overriding the ``prepend_urls`` method & adding on a new method to -handle the children:: +handle the children: + +.. testcode:: + + class ChildResource(ModelResource): + pass + + from tastypie.utils import trailing_slash class ParentResource(ModelResource): children = fields.ToManyField(ChildResource, 'children') @@ -197,7 +333,13 @@ return HttpMultipleChoices("More than one resource is found at this URI.") child_resource = ChildResource() - return child_resource.get_detail(request, parent_id=obj.pk) + return child_resource.get_list(request, parent_id=obj.pk) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Adding Search Functionality @@ -208,7 +350,7 @@ We leave the CRUD methods of the resource alone, choosing to add a new endpoint at ``/api/v1/notes/search/``:: - from django.conf.urls.defaults import * + from django.conf.urls import url, include from django.core.paginator import Paginator, InvalidPage from django.http import Http404 from haystack.query import SearchQuerySet @@ -266,26 +408,38 @@ how to implement it for two basic operations: listing and creation of an object. For listing we want to list only objects for which 'user' field matches -'request.user'. This could be done by applying a filter in the ``apply_authorization_limits`` -method of your resource. +'request.user'. This could be done by applying a filter in the +``authorized_read_list`` method of your resource. For creating we'd have to wrap ``obj_create`` method of ``ModelResource``. Then the -resulting code will look something like:: +resulting code will look something like: + +.. testcode:: # myapp/api/resources.py - class EnvironmentResource(ModelResource): + from tastypie.authentication import ApiKeyAuthentication + from tastypie.authorization import Authorization + + + class MyModelResource(ModelResource): class Meta: - queryset = Environment.objects.all() - resource_name = 'environment' + queryset = MyModel.objects.all() + resource_name = 'mymodel' list_allowed_methods = ['get', 'post'] authentication = ApiKeyAuthentication() authorization = Authorization() def obj_create(self, bundle, **kwargs): - return super(EnvironmentResource, self).obj_create(bundle, user=bundle.request.user) + return super(MyModelResource, self).obj_create(bundle, user=bundle.request.user) - def apply_authorization_limits(self, request, object_list): - return object_list.filter(user=request.user) + def authorized_read_list(self, object_list, bundle): + return object_list.filter(user=bundle.request.user) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... camelCase JSON Serialization ---------------------------- @@ -293,12 +447,14 @@ The convention in the world of Javascript has standardized on camelCase, where Tastypie uses underscore syntax, which can lead to "ugly" looking code in Javascript. You can create a custom serializer that emits -values in camelCase instead:: +values in camelCase instead: - from tastypie.serializers import Serializer +.. testcode:: import re import json + from tastypie.serializers import Serializer + class CamelCaseJSONSerializer(Serializer): formats = ['json'] @@ -354,12 +510,20 @@ return underscored_data +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Pretty-printed JSON Serialization --------------------------------- By default, Tastypie outputs JSON with no indentation or newlines (equivalent to calling :py:func:`json.dumps` with *indent* set to ``None``). You can override this -behavior in a custom serializer:: +behavior in a custom serializer: + +.. testcode:: import json from django.core.serializers.json import DjangoJSONEncoder @@ -374,16 +538,25 @@ return json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True, ensure_ascii=False, indent=self.json_indent) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Determining format via URL -------------------------- Sometimes it's required to allow selecting the response format by specifying it in the API URL, for example ``/api/v1/users.json`` instead of ``/api/v1/users/?format=json``. The following snippet allows that kind -of syntax additional to the default URL scheme:: +of syntax additional to the default URL scheme: + +.. testcode:: # myapp/api/resources.py + from django.contrib.auth.models import User # Piggy-back on internal csrf_exempt existence handling from tastypie.resources import csrf_exempt @@ -421,6 +594,12 @@ return wrapped_view(request, *args, **kwargs) return wrapper +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Adding to the Django Admin -------------------------- @@ -428,21 +607,29 @@ or edit ApiKeys next to users. To do this, you need to unregister the built-in UserAdmin, alter the inlines, and re-register it. This could go in any of your admin.py files. You may also want to register ApiAccess and ApiKey models on -their own.:: +their own.: - from tastypie.admin import ApiKeyInline - from tastypie.models import ApiAccess, ApiKey +.. testcode:: + + from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User - admin.site.register(ApiKey) - admin.site.register(ApiAccess) + from tastypie.admin import ApiKeyInline + class UserModelAdmin(UserAdmin): inlines = UserAdmin.inlines + [ApiKeyInline] + admin.site.unregister(User) - admin.site.register(User,UserModelAdmin) + admin.site.register(User, UserModelAdmin) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Using ``SessionAuthentication`` @@ -450,7 +637,9 @@ If your users are logged into the site & you want Javascript to be able to access the API (assuming jQuery), the first thing to do is setup -``SessionAuthentication``:: +``SessionAuthentication``: + +.. testcode:: from django.contrib.auth.models import User from tastypie.authentication import SessionAuthentication @@ -463,6 +652,12 @@ queryset = User.objects.all() authentication = SessionAuthentication() +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Then you'd build a template like:: diff -Nru django-tastypie-0.12.0/docs/debugging.rst django-tastypie-0.13.3/docs/debugging.rst --- django-tastypie-0.12.0/docs/debugging.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/debugging.rst 2016-02-14 09:14:12.000000000 +0000 @@ -15,10 +15,10 @@ This issue is that Tastypie respects the ``Accept`` header your browser sends. Most browsers send something like:: - Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 -Note that ``application/xml`` comes first, which is a format that Tastypie -handles by default, hence why you receive XML. +Note that ``application/xml`` is the first format that Tastypie +handles, hence you receive XML. If you use ``curl`` from the command line, you should receive JSON by default:: @@ -70,18 +70,3 @@ curl -H 'Content-Type: application/json' -X PUT --data @- "http://localhost:8000/api/v1/entry/" .. _Requests: http://python-requests.org - - -"Why is my syncdb with superuser failing with a DatabaseError?" -=============================================================== - -More specifically, this specific ``DatabaseError``:: - - django.db.utils.DatabaseError: no such table: tastypie_apikey - -This is a side effect of the (disabled by default) ``create_api_key`` signal -as described in the :ref:`authentication` section of the -documentation when used in conjunction with South. - -To work around this issue, you can disable the ``create_api_key`` signal -until you have completed running ``syncdb --migrate`` for the first time. diff -Nru django-tastypie-0.12.0/docs/geodjango.rst django-tastypie-0.13.3/docs/geodjango.rst --- django-tastypie-0.12.0/docs/geodjango.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/geodjango.rst 2016-02-14 09:14:12.000000000 +0000 @@ -6,7 +6,7 @@ Tastypie features support for GeoDjango! Resources return and accept `GeoJSON `_ (or similarly-formatted -analogs for other formats) and all `spatial lookup `_ filters are supported. Distance lookups are not yet supported. +analogs for other formats) and all `spatial lookup `_ filters are supported. Distance lookups are not yet supported. Usage ===== @@ -61,7 +61,7 @@ Filtering --------- -We can filter using any standard GeoDjango `spatial lookup `_ filter. Simply provide a GeoJSON (or the analog) as a ``GET`` parameter value. +We can filter using any standard GeoDjango `spatial lookup `_ filter. Simply provide a GeoJSON (or the analog) as a ``GET`` parameter value. Let's find all of our ``GeoNote`` resources that contain a point inside of `Golden Gate Park `_:: diff -Nru django-tastypie-0.12.0/docs/index.rst django-tastypie-0.13.3/docs/index.rst --- django-tastypie-0.12.0/docs/index.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/index.rst 2016-02-14 09:14:12.000000000 +0000 @@ -30,6 +30,7 @@ paginator geodjango content_types + namespaces cookbook debugging @@ -42,18 +43,6 @@ release_notes/index -Getting Help -============ - -There are two primary ways of getting help. We have a `mailing list`_ hosted at -Google (http://groups.google.com/group/django-tastypie/) and an IRC channel -(`#tastypie on irc.freenode.net`_) to get help, want to bounce idea or -generally shoot the breeze. - -.. _`mailing list`: http://groups.google.com/group/django-tastypie/ -.. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie - - Quick Start =========== @@ -72,17 +61,18 @@ 4. In your root URLconf, add the following code (around where the admin code might be):: + from django.conf.urls import url, include from tastypie.api import Api from my_app.api.resources import MyModelResource v1_api = Api(api_name='v1') v1_api.register(MyModelResource()) - urlpatterns = patterns('', + urlpatterns = [ # ...more URLconf bits here... # Then add: - (r'^api/', include(v1_api.urls)), - ) + url(r'^api/', include(v1_api.urls)), + ] 5. Hit http://localhost:8000/api/v1/?format=json in your browser! @@ -90,31 +80,30 @@ Requirements ============ -Tastypie requires the following modules. If you use Pip_, you can install -the necessary bits via the included ``requirements.txt``: +Core +---- -Required --------- - -* Python 2.6+ or Python 3.3+ -* Django 1.5+ +* Python 2.7+ or Python 3.4+ +* Django 1.7 through Django 1.9 * dateutil (http://labix.org/python-dateutil) >= 2.1 +Format Support +-------------- + +* XML: lxml 3 (http://lxml.de/) and defusedxml (https://pypi.python.org/pypi/defusedxml) +* YAML: pyyaml (http://pyyaml.org/) +* binary plist: biplist (http://explorapp.com/biplist/) + Optional -------- -* python_digest (https://bitbucket.org/akoha/python-digest/) -* lxml (http://lxml.de/) and defusedxml (https://bitbucket.org/tiran/defusedxml) if using the XML serializer -* pyyaml (http://pyyaml.org/) if using the YAML serializer -* biplist (https://pypi.python.org/pypi/biplist) if using the binary plist serializer - -.. _Pip: http://pip.openplans.org/ +* HTTP Digest authentication: python3-digest (https://bitbucket.org/akoha/python-digest/) Why Tastypie? ============= -There are other, better known API frameworks out there for Django. You need to +There are other API frameworks out there for Django. You need to assess the options available and decide for yourself. That said, here are some common reasons for tastypie. @@ -125,14 +114,13 @@ the problem domain. * You want/need XML serialization that is treated equally to JSON (and YAML is there too). -* You want to support my perceived NIH syndrome, which is less about NIH and more - about trying to help out friends/coworkers. Reference Material ================== -* http://github.com/toastdriven/django-tastypie/tree/master/tests/basic shows +* https://django-tastypie.readthedocs.org/en/latest/ +* https://github.com/django-tastypie/django-tastypie/tree/master/tests/basic shows basic usage of tastypie * http://en.wikipedia.org/wiki/REST * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes @@ -140,12 +128,25 @@ * http://jacobian.org/writing/rest-worst-practices/ +Getting Help +============ + +There are two primary ways of getting help. + +1. Go to `StackOverflow`_ and post a question with the ``tastypie`` tag. +2. We have an IRC channel (`#tastypie on irc.freenode.net`_) to get help, + bounce an idea by us, or generally shoot the breeze. + +.. _`StackOverflow`: https://stackoverflow.com/questions/tagged/tastypie +.. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie + + Running The Tests ================= The easiest way to get setup to run Tastypie's tests looks like:: - $ git clone https://github.com/toastdriven/django-tastypie.git + $ git clone https://github.com/django-tastypie/django-tastypie.git $ cd django-tastypie $ virtualenv env $ . env/bin/activate @@ -164,4 +165,4 @@ find a failure, please `report it`_ along with the versions of the installed software. -.. _`report it`: https://github.com/toastdriven/django-tastypie/issues +.. _`report it`: https://github.com/django-tastypie/django-tastypie/issues diff -Nru django-tastypie-0.12.0/docs/interacting.rst django-tastypie-0.13.3/docs/interacting.rst --- django-tastypie-0.12.0/docs/interacting.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/interacting.rst 2016-02-14 09:14:12.000000000 +0000 @@ -45,7 +45,7 @@ # urls.py - from django.conf.urls.defaults import * + from django.conf.urls import url, include from tastypie.api import Api from myapp.api.resources import EntryResource, UserResource @@ -53,11 +53,11 @@ v1_api.register(UserResource()) v1_api.register(EntryResource()) - urlpatterns = patterns('', + urlpatterns = [ # The normal jazz here... - (r'^blog/', include('myapp.urls')), - (r'^api/', include(v1_api.urls)), - ) + url(r'^blog/', include('myapp.urls')), + url(r'^api/', include(v1_api.urls)), + ] Let's fire up a shell & start exploring the API! @@ -139,6 +139,34 @@ We'll stick to JSON for the rest of this document, but using XML should be OK to do at any time. +It's also possible to get all schemas (`Inspecting The Resource's Schema`_) in a single request:: + + curl http://localhost:8000/api/v1/?fullschema=true + +You'll get back something like:: + + { + "entry": { + "list_endpoint": "/api/v1/entry/", + "schema": { + "default_format": "application/json", + "fields": { + "body": { + "help_text": "Unicode string data. Ex: \"Hello World\"", + "nullable": false, + "readonly": false, + "type": "string" + }, + ... + }, + "filtering": { + "pub_date": ["exact", "lt", "lte", "gte", "gt"], + "user": 2 + } + } + }, + } + .. _schema-inspection: @@ -196,6 +224,8 @@ "nullable": false, "readonly": false, "type": "related" + "related_type": "to_one" + "related_schema": "/api/v1/user/schema/" } }, "filtering": { @@ -528,7 +558,7 @@ .. note:: - A ``PUT`` request requires that the entire resource representation be enclosed. Missing fields may cause errors, or be filled in by default values. + A ``PUT`` request requires that the entire resource representation be enclosed. Missing fields may cause errors, or be filled in by default values. Partially Updating An Existing Resource (PATCH) @@ -666,7 +696,7 @@ Content-Length: 0 Content-Type: text/html; charset=utf-8 -The Accepted response means the server has accepted the request, but gives no details on the result. In order to see any created resources, we would need to do a get ``GET`` on the list endpoint. +The Accepted response means the server has accepted the request, but gives no details on the result. In order to see any created resources, we would need to do a get ``GET`` on the list endpoint. For detailed information on the format of a bulk request, see :ref:`patch-list`. diff -Nru django-tastypie-0.12.0/docs/namespaces.rst django-tastypie-0.13.3/docs/namespaces.rst --- django-tastypie-0.12.0/docs/namespaces.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/namespaces.rst 2016-02-14 09:15:23.000000000 +0000 @@ -0,0 +1,32 @@ +.. _namespaces: + +========== +Namespaces +========== + +For various reasons you might want to deploy your API under a namespaced URL path. To support that tastypie includes ``NamespacedApi`` and ``NamespacedModelResource``. + +A sample definition of your API in this case would be something like:: + + from django.conf.urls import url, include + from tastypie.api import NamespacedApi + from my_application.api.resources import NamespacedUserResource + + api = NamespacedApi(api_name='v1', urlconf_namespace='special') + api.register(NamespacedUserResource()) + + urlpatterns = [ + url(r'^api/', include(api.urls, namespace='special')), + ] + +And your model resource:: + + from django.contrib.auth.models import User + from tastypie.resources import NamespacedModelResource + from tastypie.authorization import Authorization + + class NamespacedUserResource(NamespacedModelResource): + class Meta: + resource_name = 'users' + queryset = User.objects.all() + authorization = Authorization() diff -Nru django-tastypie-0.12.0/docs/non_orm_data_sources.rst django-tastypie-0.13.3/docs/non_orm_data_sources.rst --- django-tastypie-0.12.0/docs/non_orm_data_sources.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/non_orm_data_sources.rst 2016-02-14 09:15:23.000000000 +0000 @@ -47,6 +47,12 @@ since it has both a simple API and demonstrate what hooking up to a non-relational datastore looks like:: + import riak + + from tastypie import fields + from tastypie.authorization import Authorization + from tastypie.resources import Resource + # We need a generic object to shove data in/get data from. # Riak generally just tosses around dictionaries, so we'll lightly # wrap that. @@ -157,4 +163,4 @@ All said and done, just nine methods needed overriding, eight of which were highly specific to how data access is done. -.. _Riak: http://www.basho.com/products_riak_overview.php +.. _Riak: https://pypi.python.org/pypi/riak diff -Nru django-tastypie-0.12.0/docs/python3.rst django-tastypie-0.13.3/docs/python3.rst --- django-tastypie-0.12.0/docs/python3.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/python3.rst 2016-02-14 09:14:12.000000000 +0000 @@ -7,14 +7,14 @@ As of Tastypie v0.10.0, it has been ported to support both Python 2 & Python 3 within the same codebase. This builds on top of what `six`_ & `Django`_ provide. -No changes are required for anyone running an existing Tastpie +No changes are required for anyone running an existing Tastypie installation. The API is completely backward-compatible, so you should be able to run your existing software without modification. All tests pass under both Python 2 & 3. .. _`six`: http://pythonhosted.org/six/ -.. _`Django`: https://docs.djangoproject.com/en/1.5/topics/python3/#str-and-unicode-methods +.. _`Django`: https://docs.djangoproject.com/en/dev/topics/python3/#str-and-unicode-methods Incompatibilities diff -Nru django-tastypie-0.12.0/docs/release_notes/dev.rst django-tastypie-0.13.3/docs/release_notes/dev.rst --- django-tastypie-0.12.0/docs/release_notes/dev.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/dev.rst 2016-02-17 13:08:30.000000000 +0000 @@ -0,0 +1,10 @@ +dev +=== + +The current in-progress version. Put your notes here so they can be easily +copied to the release notes for the next release. + +Bugfixes +-------- + +* list of changes diff -Nru django-tastypie-0.12.0/docs/release_notes/index.rst django-tastypie-0.13.3/docs/release_notes/index.rst --- django-tastypie-0.12.0/docs/release_notes/index.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/index.rst 2016-02-17 13:08:47.000000000 +0000 @@ -4,6 +4,13 @@ .. toctree:: :maxdepth: 1 + dev + v0.13.3 + v0.13.2 + v0.13.1 + v0.13.0 + v0.12.2 + v0.12.1 v0.12.0 v0.11.1 v0.11.0 diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.12.1.rst django-tastypie-0.13.3/docs/release_notes/v0.12.1.rst --- django-tastypie-0.12.0/docs/release_notes/v0.12.1.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.12.1.rst 2016-02-14 09:10:26.000000000 +0000 @@ -0,0 +1,7 @@ +v0.12.1 +======= + +:date: 2014-10-22 + +This release is a small bugfix release, specifically to remove accidentally +added files in the Wheel release. diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.12.2.rst django-tastypie-0.13.3/docs/release_notes/v0.12.2.rst --- django-tastypie-0.12.0/docs/release_notes/v0.12.2.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.12.2.rst 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,14 @@ +v0.12.1 +======= + +:date: 2015-07-16 + +Dropped Python 2.6 support, added Django 1.8. + + +Bugfixes +-------- + +* Dropped support for Python 2.6 +* Added support for Django 1.8 +* Fix stale data caused by prefetch_related cache (SHA: b78661d) diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.13.0.rst django-tastypie-0.13.3/docs/release_notes/v0.13.0.rst --- django-tastypie-0.12.0/docs/release_notes/v0.13.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.13.0.rst 2016-02-14 09:14:12.000000000 +0000 @@ -0,0 +1,41 @@ +v0.13.0 +======= + +:date: 2016-01-12 + +Dropped Django 1.5-1.6 support, added Django 1.9. + + +Bugfixes +-------- + +* Various performance improvements (#1330, #1335, #1337, #1363) +* More descriptive error messages (#1201) +* Throttled requests now include Retry-After header. (#1204) +* In DecimalField.hydrate, catch decimal.InvalidOperation and raise ApiFieldError (#862) +* Add 'primary_key' Field To Schema (#1141) +* ContentTypes: Remove 'return' in __init__; remove redundant parentheses (#1090) +* Allow callable strings for ToOneField.attribute (#1193) +* Ensure Tastypie doesn't return extra data it received (#1169) +* In DecimalField.hydrate, catch decimal.InvalidOperation and raise ApiFieldError (#862) +* Fixed tastypie's losing received microseconds. (#1126) +* Data leakage fix (#1203) +* Ignore extra related data (#1336) +* Suppress Content-Type header on HTTP 204 (see #111) (#1054) +* Allow creation of related resources that have an 'items' related_name (supercedes #1000) (#1340) +* Serializers: remove unimplemented to_html/from_html (#1343) +* If GEOS is not installed then exclude geos related calls. (#1348) +* Fixed Resource.deserialize() to honor format parameter (#1354 #1356, #1358) +* Raise ValueError when trying to register a Resource class instead of a Resource instance. (#1361) +* Fix hydrating/saving of related resources. (#1363) +* Use Tastypie DateField for DateField on the model. (SHA: b248e7f) +* ApiFieldError on empty non-null field (#1208) +* Full schema (all schemas in a single request) (#1207) +* Added verbose_name to API schema. (#1370) +* Fixes Reverse One to One Relationships (Replaces #568) (#1378) +* Fixed "GIS importerror vs improperlyconfigured" (#1384) +* Fixed bug which occurs when detail_uri_name field has a default value (Issue #1323) (#1387) +* Fixed disabling cache using timeout=0, fixes #1213, #1212 (#1399) +* Removed Django 1.5-1.6 support, added 1.9 support. (#1400) +* stop using django.conf.urls.patterns (#1402) +* Fix for saving related items when resource_uri is provided but other unique data is not. (#1394) (#1410) diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.13.1.rst django-tastypie-0.13.3/docs/release_notes/v0.13.1.rst --- django-tastypie-0.12.0/docs/release_notes/v0.13.1.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.13.1.rst 2016-02-14 09:15:23.000000000 +0000 @@ -0,0 +1,13 @@ +v0.13.1 +======= + +:date: 2016-01-25 + +Bugfixes +-------- + +* Prevent muting non-tastypie's exceptions (#1297, PR #1404) +* Gracefully handle UnsupportFormat exception (#1154, PR #1417) +* Add related schema urls (#782, PR #1309) +* Repr value must be str in Py2 (#1421, PR #1422) +* Fixed assertHttpAccepted (PR #1416) diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.13.2.rst django-tastypie-0.13.3/docs/release_notes/v0.13.2.rst --- django-tastypie-0.12.0/docs/release_notes/v0.13.2.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.13.2.rst 2016-02-15 04:54:12.000000000 +0000 @@ -0,0 +1,27 @@ +v0.13.2 +======= + +:date: 2016-02-14 + +Bugfixes +-------- + +* Fix in `Resource.save_related`: `related_obj` can be empty in patch requests (introduced in #1378). (Fixes #1436) +* Fixed bug that prevented fitlering on related resources. `apply_filters` hook now used in obj_get. (Fixes #1435, Fixes #1443) +* Use `build_filters` in `obj_get`. (Fixes #1444) +* Updated DjangoAuthorization to disallow read unless a user has `change` permission. (#1407, PR #1409) +* Authorization classes now handle usernames containing spaces. Closes #966. +* Cleaned up old, unneeded code. (closes PR #1433) + * Reuse Django test `Client.patch()`. (@SeanHayes, closes #1442) + * Just a typo fix in the testing docs (by @bezidejni, closes #810) + * Removed references to patterns() (by @SeanHayes, closes #1437) + * Removed deprecated methods `Resource.apply_authorization_limits` and `Authorization.apply_limits` from code and documentation. (by @SeanHayes, closes #1383, #1045, #1284, #837) + * Updates docs/cookbook.rst to make sure it's clear which `url` to import. (by @yuvadm, closes #716) + * Updated docs/tutorial.rst. Without "null=True, blank=True" parameters in Slugfield, expecting "automatic slug generation" in save method is pointless. (by @orges, closes #753) + * Cleaned up Riak docs. (by @SeanHayes, closes #275) + * Include import statement for trailing_slash. (by @ljosa, closes #770) + * Fix docs: `Meta.filtering` is actually a dict. (by @georgedorn, closes #807) + * Fix load data command. (by @blite, closes #357, #358) +* Related schemas no longer raise error when not URL accessible. (Fixes PR #1439) +* Avoid modifying Field instances during request/response cycle. (closes #1415) +* Removing the Manager dependency in ToManyField.dehydrate(). (Closes #537) diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.13.3.rst django-tastypie-0.13.3/docs/release_notes/v0.13.3.rst --- django-tastypie-0.12.0/docs/release_notes/v0.13.3.rst 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.13.3.rst 2016-02-17 13:08:13.000000000 +0000 @@ -0,0 +1,9 @@ +v0.13.3 +======= + +:date: 2016-02-17 + +Bugfixes +-------- + +* Permit changing existing value on a ToOneField to None. (Closes #1449) diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.9.13.rst django-tastypie-0.13.3/docs/release_notes/v0.9.13.rst --- django-tastypie-0.12.0/docs/release_notes/v0.9.13.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.9.13.rst 2016-02-14 09:14:12.000000000 +0000 @@ -18,11 +18,11 @@ * Don't even use XML and want to disable it? There's a simple :ref:`TASTYPIE_DEFAULT_FORMATS setting ` setting to globally restrict the set of supported formats - (closes `#833 `_): + (closes `#833 `_): http://django-tastypie.readthedocs.org/en/v0.9.14/settings.html#tastypie-default-formats -* Content negotiation will return an error for malformed accept headers (closes `#832 `_) -* The Api class itself now allows a custom serializer (closes `#817 `_) +* Content negotiation will return an error for malformed accept headers (closes `#832 `_) +* The Api class itself now allows a custom serializer (closes `#817 `_) * The serialization documentation has been upgraded with security advice: http://django-tastypie.readthedocs.org/en/v0.9.14/serialization.html#serialization-security @@ -36,4 +36,4 @@ ``pip install defusedxml "lxml>=3"`` * Python 2.5 is no longer officially supported because defusedxml requires Python 2.6 or later. If you cannot - upgrade to a newer version of Python please consider disabling XML support entirely. \ No newline at end of file + upgrade to a newer version of Python please consider disabling XML support entirely. diff -Nru django-tastypie-0.12.0/docs/release_notes/v0.9.16.rst django-tastypie-0.13.3/docs/release_notes/v0.9.16.rst --- django-tastypie-0.12.0/docs/release_notes/v0.9.16.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/release_notes/v0.9.16.rst 2016-02-14 09:10:26.000000000 +0000 @@ -25,7 +25,7 @@ * Fixed test case to no longer require a defined object class. (SHA: d8e250f) * Fixed the signature on ``dehydrate`` in the GIS resource class. (SHA: f724919) * Fixed a bug involving updating foreign relations on create. (SHA: 50a6741) -* Changed the PUT response code (with ``always_return_data = True``) to 202. +* Changed the PUT response code (with ``always_return_data = True``) from 202 to 200. (SHA: abc0bef) * Documentation updates: diff -Nru django-tastypie-0.12.0/docs/resources.rst django-tastypie-0.13.3/docs/resources.rst --- django-tastypie-0.12.0/docs/resources.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/resources.rst 2016-02-14 09:15:23.000000000 +0000 @@ -95,7 +95,6 @@ * the requested HTTP method is in ``allowed_methods`` (``method_check``), * the class has a method that can handle the request (``get_list``), * the user is authenticated (``is_authenticated``), - * the user is authorized (``is_authorized``), * & the user has not exceeded their throttle (``throttle_check``). At this point, ``dispatch`` actually calls the requested method (``get_list``). @@ -106,7 +105,7 @@ of ``ModelResource``, this builds the ORM filters to apply (``ModelResource.build_filters``). It then gets the ``QuerySet`` via ``ModelResource.get_object_list`` (which performs - ``Resource.apply_authorization_limits`` to possibly limit the set the user + ``Resource.authorized_read_list`` to possibly limit the set the user can work with) and applies the built filters to it. * It then sorts the objects based on user input (``ModelResource.apply_sorting``). @@ -587,8 +586,8 @@ ``filtering`` ------------- - Provides a list of fields that the ``Resource`` will accept client - filtering on. Default is ``{}``. + Specifies the fields that the ``Resource`` will accept client filtering on. + Default is ``{}``. Keys should be the fieldnames as strings while values should be a list of accepted filter types. @@ -777,8 +776,7 @@ The standard URLs this ``Resource`` should respond to. These include the list, detail, schema & multiple endpoints by default. -Should return a list of individual URLconf lines (**NOT** wrapped in -``patterns``). +Should return a list of individual URLconf lines. ``override_urls`` ----------------- @@ -796,8 +794,7 @@ A hook for adding your own URLs or matching before the default URLs. Useful for adding custom endpoints or overriding the built-in ones (from ``base_urls``). -Should return a list of individual URLconf lines (**NOT** wrapped in -``patterns``). +Should return a list of individual URLconf lines. ``urls`` -------- @@ -952,16 +949,6 @@ # GET. self.method_check(request, ['get']) -``is_authorized`` ------------------ - -.. method:: Resource.is_authorized(self, request, object=None) - -Handles checking of permissions to see if the user has authorization -to GET, POST, PUT, or DELETE this resource. If ``object`` is provided, -the authorization backend can apply additional row-level permissions -checking. - ``is_authenticated`` -------------------- @@ -1102,10 +1089,24 @@ .. method:: Resource.full_dehydrate(self, bundle, for_list=False) -Given a bundle with an object instance, extract the information from it to -populate the resource. +Populate the bundle's :attr:`data` attribute. + +The ``bundle`` parameter will have the data that needs dehydrating in its +:attr:`obj` attribute. + +The ``for_list`` parameter indicates the style of response being prepared: + - ``True`` indicates a list of items. Note that :meth:`full_dehydrate` will + be called once for each object requested. + - ``False`` indicates a response showing the details for an item + +This method is responsible for invoking the the :meth:`dehydrate` method of +all the fields in the resource. Additionally, it calls +:meth:`Resource.dehydrate`. + +Must return a :class:`Bundle` with the desired dehydrated :attr:`data` +(usually a :class:`dict`). Typically one should modify the bundle passed in +and return it, but you may also return a completely new bundle. -The for_list flag is used to control which fields are excluded by the ``use_in`` attribute. ``dehydrate`` ------------- @@ -1189,16 +1190,6 @@ ``ModelResource`` includes a full working version specific to Django's ``Models``. -``apply_authorization_limits`` ------------------------------- - -.. method:: Resource.apply_authorization_limits(self, request, object_list) - -Allows the ``Authorization`` class to further limit the object list. -Also a hook to customize per ``Resource``. - -Calls ``Authorization.apply_limits`` if available. - ``can_create`` -------------- @@ -1418,7 +1409,7 @@ Return ``HttpNoContent`` (204 No Content) if ``Meta.always_return_data = False`` (default). -Return ``HttpAccepted`` (202 Accepted) if +Return ``HttpAccepted`` (200 OK) if ``Meta.always_return_data = True``. ``put_detail`` @@ -1440,8 +1431,8 @@ ``Meta.always_return_data = False`` (default), return ``HttpNoContent`` (204 No Content). If an existing resource is modified and -``Meta.always_return_data = True``, return ``HttpAccepted`` (202 -Accepted). +``Meta.always_return_data = True``, return ``HttpAccepted`` (200 +OK). ``post_list`` ------------- diff -Nru django-tastypie-0.12.0/docs/serialization.rst django-tastypie-0.13.3/docs/serialization.rst --- django-tastypie-0.12.0/docs/serialization.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/serialization.rst 2016-02-14 09:10:26.000000000 +0000 @@ -41,11 +41,10 @@ * jsonp (Disabled by default) * xml * yaml -* html * plist (see http://explorapp.com/biplist/) Not everyone wants to install or support all the serialization options. If you -would list to customize the list of supported formats for your entire site +would like to customize the list of supported formats for your entire site the :ref:`TASTYPIE_DEFAULT_FORMATS setting ` allows you to set the default format list site-wide. @@ -77,7 +76,7 @@ queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] - serializer = Serializer(formats=['json', 'jsonp', 'xml', 'yaml', 'html', 'plist']) + serializer = Serializer(formats=['json', 'jsonp', 'xml', 'yaml', 'plist']) Serialization Security @@ -157,26 +156,27 @@ class CSVSerializer(Serializer): - formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'plist', 'csv'] - content_types = { - 'json': 'application/json', - 'jsonp': 'text/javascript', - 'xml': 'application/xml', - 'yaml': 'text/yaml', - 'html': 'text/html', - 'plist': 'application/x-plist', - 'csv': 'text/csv', - } + formats = Serializer.formats + ['csv'] + + content_types = dict( + Serializer.content_types.items() + + [('csv', 'text/csv')]) def to_csv(self, data, options=None): options = options or {} data = self.to_simple(data, options) raw_data = StringIO.StringIO() - # Untested, so this might not work exactly right. - for item in data: - writer = csv.DictWriter(raw_data, item.keys(), extrasaction='ignore') - writer.write(item) - return raw_data + if data['objects']: + fields = data['objects'][0].keys() + writer = csv.DictWriter(raw_data, fields, + dialect="excel", + extrasaction='ignore') + header = dict(zip(fields, fields)) + writer.writerow(header) # In Python 2.7: `writer.writeheader()` + for item in data['objects']: + writer.writerow(item) + + return raw_data.getvalue() def from_csv(self, content): raw_data = StringIO.StringIO(content) @@ -198,7 +198,6 @@ * jsonp * xml * yaml - * html * plist It was designed to make changing behavior easy, either by overridding the @@ -360,24 +359,3 @@ Given some binary plist data, returns a Python dictionary of the decoded data. -``to_html`` -~~~~~~~~~~~ - -.. method:: Serializer.to_html(self, data, options=None): - -Reserved for future usage. - -The desire is to provide HTML output of a resource, making an API -available to a browser. This is on the TODO list but not currently -implemented. - -``from_html`` -~~~~~~~~~~~~~ - -.. method:: Serializer.from_html(self, content): - -Reserved for future usage. - -The desire is to handle form-based (maybe Javascript?) input, making an -API available to a browser. This is on the TODO list but not currently -implemented. diff -Nru django-tastypie-0.12.0/docs/settings.rst django-tastypie-0.13.3/docs/settings.rst --- django-tastypie-0.12.0/docs/settings.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/settings.rst 2016-02-14 09:10:26.000000000 +0000 @@ -113,7 +113,7 @@ TASTYPIE_DEFAULT_FORMATS = ['json', 'xml'] -Defaults to ``['json', 'xml', 'yaml', 'html', 'plist']``. +Defaults to ``['json', 'xml', 'yaml', 'plist']``. ``TASTYPIE_ABSTRACT_APIKEY`` diff -Nru django-tastypie-0.12.0/docs/testing.rst django-tastypie-0.13.3/docs/testing.rst --- django-tastypie-0.12.0/docs/testing.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/testing.rst 2016-02-14 09:15:23.000000000 +0000 @@ -9,24 +9,23 @@ correctly with the rest of your application. Tastypie provides some basic facilities that build on top of `Django's testing`_ -support, in the form of a specialized ``TestApiClient`` & ``ResourceTestCase``. +support, in the form of a specialized ``TestApiClient`` & ``ResourceTestCaseMixin``. .. _`Django's testing`: https://docs.djangoproject.com/en/dev/topics/testing/ -The ``ResourceTestCase`` builds on top of Django's ``TestCase``. It provides quite -a few extra assertion methods that are specific to APIs. Under the hood, it -uses the ``TestApiClient`` to perform requests properly. +The ``ResourceTestCaseMixin`` can be used along with Django's ``TestCase`` or other +Django test classes. It provides quite a few extra assertion methods that are specific +to APIs. Under the hood, it uses the ``TestApiClient`` to perform requests properly. The ``TestApiClient`` builds on & exposes an interface similar to that of Django's ``Client``. However, under the hood, it hands all the setup needed to construct a proper request. - Example Usage ============= -The typical use case will primarily consist of subclassing the -``ResourceTestCase`` class & using the built-in assertions to ensure your +The typical use case will primarily consist of adding the ``ResourceTestCaseMixin`` +class to an ordinary Django test class & using the built-in assertions to ensure your API is behaving correctly. For the purposes of this example, we'll assume the resource in question looks like:: @@ -45,11 +44,12 @@ import datetime from django.contrib.auth.models import User - from tastypie.test import ResourceTestCase + from django.test import TestCase + from tastypie.test import ResourceTestCaseMixin from entries.models import Entry - class EntryResourceTest(ResourceTestCase): + class EntryResourceTest(ResourceTestCaseMixin, TestCase): # Use ``fixtures`` & ``urls`` as normal. See Django's ``TestCase`` # documentation for the gory details. fixtures = ['test_entries.json'] @@ -83,7 +83,7 @@ def get_credentials(self): return self.create_basic(username=self.username, password=self.password) - def test_get_list_unauthorzied(self): + def test_get_list_unauthenticated(self): self.assertHttpUnauthorized(self.api_client.get('/api/v1/entries/', format='json')) def test_get_list_json(self): @@ -160,17 +160,17 @@ a list endpoint, ``DELETE`` to a list endpoint, ``PATCH`` support, etc. -``ResourceTestCase`` API Reference ----------------------------------- +``ResourceTestCaseMixin`` API Reference +--------------------------------------- -The ``ResourceTestCase`` exposes the following methods for use. Most are +The ``ResourceTestCaseMixin`` exposes the following methods for use. Most are enhanced assertions or provide API-specific behaviors. ``get_credentials`` ~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.get_credentials(self) +.. method:: ResourceTestCaseMixin.get_credentials(self) A convenience method for the user as a way to shorten up the often repetitious calls to create the same authentication. @@ -179,7 +179,7 @@ Usage:: - class MyResourceTestCase(ResourceTestCase): + class MyResourceTestCase(ResourceTestCaseMixin, TestCase): def get_credentials(self): return self.create_basic('daniel', 'pass') @@ -188,147 +188,147 @@ ``create_basic`` ~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.create_basic(self, username, password) +.. method:: ResourceTestCaseMixin.create_basic(self, username, password) Creates & returns the HTTP ``Authorization`` header for use with BASIC Auth. ``create_apikey`` ~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.create_apikey(self, username, api_key) +.. method:: ResourceTestCaseMixin.create_apikey(self, username, api_key) Creates & returns the HTTP ``Authorization`` header for use with ``ApiKeyAuthentication``. ``create_digest`` ~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.create_digest(self, username, api_key, method, uri) +.. method:: ResourceTestCaseMixin.create_digest(self, username, api_key, method, uri) Creates & returns the HTTP ``Authorization`` header for use with Digest Auth. ``create_oauth`` ~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.create_oauth(self, user) +.. method:: ResourceTestCaseMixin.create_oauth(self, user) Creates & returns the HTTP ``Authorization`` header for use with Oauth. ``assertHttpOK`` ~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpOK(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpOK(self, resp) Ensures the response is returning a HTTP 200. ``assertHttpCreated`` ~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpCreated(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpCreated(self, resp) Ensures the response is returning a HTTP 201. ``assertHttpAccepted`` ~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpAccepted(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpAccepted(self, resp) Ensures the response is returning either a HTTP 202 or a HTTP 204. ``assertHttpMultipleChoices`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpMultipleChoices(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpMultipleChoices(self, resp) Ensures the response is returning a HTTP 300. ``assertHttpSeeOther`` ~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpSeeOther(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpSeeOther(self, resp) Ensures the response is returning a HTTP 303. ``assertHttpNotModified`` ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpNotModified(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpNotModified(self, resp) Ensures the response is returning a HTTP 304. ``assertHttpBadRequest`` ~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpBadRequest(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpBadRequest(self, resp) Ensures the response is returning a HTTP 400. ``assertHttpUnauthorized`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpUnauthorized(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpUnauthorized(self, resp) Ensures the response is returning a HTTP 401. ``assertHttpForbidden`` ~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpForbidden(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpForbidden(self, resp) Ensures the response is returning a HTTP 403. ``assertHttpNotFound`` ~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpNotFound(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpNotFound(self, resp) Ensures the response is returning a HTTP 404. ``assertHttpMethodNotAllowed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpMethodNotAllowed(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpMethodNotAllowed(self, resp) Ensures the response is returning a HTTP 405. ``assertHttpConflict`` ~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpConflict(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpConflict(self, resp) Ensures the response is returning a HTTP 409. ``assertHttpGone`` ~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpGone(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpGone(self, resp) Ensures the response is returning a HTTP 410. ``assertHttpTooManyRequests`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpTooManyRequests(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpTooManyRequests(self, resp) Ensures the response is returning a HTTP 429. ``assertHttpApplicationError`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpApplicationError(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpApplicationError(self, resp) Ensures the response is returning a HTTP 500. ``assertHttpNotImplemented`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertHttpNotImplemented(self, resp) +.. method:: ResourceTestCaseMixin.assertHttpNotImplemented(self, resp) Ensures the response is returning a HTTP 501. ``assertValidJSON`` ~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidJSON(self, data) +.. method:: ResourceTestCaseMixin.assertValidJSON(self, data) Given the provided ``data`` as a string, ensures that it is valid JSON & can be loaded properly. @@ -336,7 +336,7 @@ ``assertValidXML`` ~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidXML(self, data) +.. method:: ResourceTestCaseMixin.assertValidXML(self, data) Given the provided ``data`` as a string, ensures that it is valid XML & can be loaded properly. @@ -344,7 +344,7 @@ ``assertValidYAML`` ~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidYAML(self, data) +.. method:: ResourceTestCaseMixin.assertValidYAML(self, data) Given the provided ``data`` as a string, ensures that it is valid YAML & can be loaded properly. @@ -352,7 +352,7 @@ ``assertValidPlist`` ~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidPlist(self, data) +.. method:: ResourceTestCaseMixin.assertValidPlist(self, data) Given the provided ``data`` as a string, ensures that it is valid binary plist & can be loaded properly. @@ -360,7 +360,7 @@ ``assertValidJSONResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidJSONResponse(self, resp) +.. method:: ResourceTestCaseMixin.assertValidJSONResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: @@ -372,7 +372,7 @@ ``assertValidXMLResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidXMLResponse(self, resp) +.. method:: ResourceTestCaseMixin.assertValidXMLResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: @@ -384,7 +384,7 @@ ``assertValidYAMLResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidYAMLResponse(self, resp) +.. method:: ResourceTestCaseMixin.assertValidYAMLResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: @@ -396,7 +396,7 @@ ``assertValidPlistResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertValidPlistResponse(self, resp) +.. method:: ResourceTestCaseMixin.assertValidPlistResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: @@ -408,7 +408,7 @@ ``deserialize`` ~~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.deserialize(self, resp) +.. method:: ResourceTestCaseMixin.deserialize(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, this method checks the ``Content-Type`` header & attempts to deserialize the data based on @@ -419,7 +419,7 @@ ``serialize`` ~~~~~~~~~~~~~ -.. method:: ResourceTestCase.serialize(self, data, format='application/json') +.. method:: ResourceTestCaseMixin.serialize(self, data, format='application/json') Given a Python datastructure (typically a ``dict``) & a desired content-type, this method will return a serialized string of that data. @@ -427,7 +427,7 @@ ``assertKeys`` ~~~~~~~~~~~~~~ -.. method:: ResourceTestCase.assertKeys(self, data, expected) +.. method:: ResourceTestCaseMixin.assertKeys(self, data, expected) This method ensures that the keys of the ``data`` match up to the keys of ``expected``. @@ -437,6 +437,15 @@ testing the full structure, which can be prone to data changes. +``ResourceTestCase`` API Reference +---------------------------------- + +``ResourceTestCase`` is deprecated and will be removed by v1.0.0. + +``class MyTest(ResourceTestCase)`` is equivalent to +``class MyTest(ResourceTestCaseMixin, TestCase)``. + + ``TestApiClient`` API Reference ------------------------------- diff -Nru django-tastypie-0.12.0/docs/tutorial.rst django-tastypie-0.13.3/docs/tutorial.rst --- django-tastypie-0.12.0/docs/tutorial.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/docs/tutorial.rst 2016-02-14 09:15:23.000000000 +0000 @@ -35,7 +35,7 @@ user = models.ForeignKey(User) pub_date = models.DateTimeField(default=now) title = models.CharField(max_length=200) - slug = models.SlugField() + slug = models.SlugField(null=True, blank=True) body = models.TextField() def __unicode__(self): @@ -59,8 +59,8 @@ 1. Download the dependencies: - * Python 2.6+ or Python 3.3+ - * Django 1.5+ + * Python 2.7+ or Python 3.4+ + * Django 1.7+ * ``python-mimeparse`` 0.1.4+ (http://pypi.python.org/pypi/python-mimeparse) * ``dateutil`` (http://labix.org/python-dateutil) * **OPTIONAL** - ``lxml`` (http://lxml.de/) and ``defusedxml`` (https://pypi.python.org/pypi/defusedxml) if using the XML serializer @@ -72,7 +72,7 @@ 3. Either symlink the ``tastypie`` directory into your project or copy the directory in. What ever works best for you. -.. _GitHub: http://github.com/toastdriven/django-tastypie +.. _GitHub: https://github.com/django-tastypie/django-tastypie .. _PyPI: http://pypi.python.org/pypi/django-tastypie @@ -131,16 +131,16 @@ ``urls``:: # urls.py - from django.conf.urls.defaults import * + from django.conf.urls import url, include from myapp.api import EntryResource entry_resource = EntryResource() - urlpatterns = patterns('', + urlpatterns = [ # The normal jazz here... - (r'^blog/', include('myapp.urls')), - (r'^api/', include(entry_resource.urls)), - ) + url(r'^blog/', include('myapp.urls')), + url(r'^api/', include(entry_resource.urls)), + ] Now it's just a matter of firing up server (``./manage.py runserver``) and going to http://127.0.0.1:8000/api/entry/?format=json. You should get back a @@ -182,7 +182,7 @@ class Meta: queryset = Entry.objects.all() resource_name = 'entry' - authorization= Authorization() + authorization = Authorization() .. warning:: @@ -253,7 +253,7 @@ following:: # urls.py - from django.conf.urls.defaults import * + from django.conf.urls import url, include from tastypie.api import Api from myapp.api import EntryResource, UserResource @@ -261,11 +261,11 @@ v1_api.register(UserResource()) v1_api.register(EntryResource()) - urlpatterns = patterns('', + urlpatterns = [ # The normal jazz here... - (r'^blog/', include('myapp.urls')), - (r'^api/', include(v1_api.urls)), - ) + url(r'^blog/', include('myapp.urls')), + url(r'^api/', include(v1_api.urls)), + ] Note that we're now creating an :class:`~tastypie.api.Api` instance, registering our ``EntryResource`` and ``UserResource`` instances with it and diff -Nru django-tastypie-0.12.0/.gitignore django-tastypie-0.13.3/.gitignore --- django-tastypie-0.12.0/.gitignore 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/.gitignore 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -*.pyc -.DS_Store -_build -.*.sw[po] -*.egg-info -dist -build -MANIFEST -tests/tastypie.db -.buildinfo -.tox -env -env3 diff -Nru django-tastypie-0.12.0/PKG-INFO django-tastypie-0.13.3/PKG-INFO --- django-tastypie-0.12.0/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 +++ django-tastypie-0.13.3/PKG-INFO 2016-02-17 13:12:45.000000000 +0000 @@ -0,0 +1,170 @@ +Metadata-Version: 1.1 +Name: django-tastypie +Version: 0.13.3 +Summary: A flexible & capable API layer for Django. +Home-page: https://github.com/django-tastypie/django-tastypie +Author: Daniel Lindsley +Author-email: daniel@toastdriven.com +License: UNKNOWN +Description: =============== + django-tastypie + =============== + + .. image:: https://readthedocs.org/projects/django-tastypie/badge/ + :target: https://django-tastypie.readthedocs.org/ + :alt: Docs + + .. image:: https://travis-ci.org/django-tastypie/django-tastypie.svg + :target: https://travis-ci.org/django-tastypie/django-tastypie + :alt: CI + + .. image:: https://coveralls.io/repos/django-tastypie/django-tastypie/badge.svg?service=github + :target: https://coveralls.io/github/django-tastypie/django-tastypie + :alt: Code Coverage + + .. image:: https://img.shields.io/pypi/v/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Version + + .. image:: https://img.shields.io/pypi/dm/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Downloads + + Creating delicious APIs for Django apps since 2010. + + Currently in beta (v0.13.3) but being used actively in production on several + sites. + + + Requirements + ============ + + Core + ---- + + * Python 2.7+ or Python 3.4+ + * Django 1.7 through Django 1.9 + * dateutil (http://labix.org/python-dateutil) >= 2.1 + + Format Support + -------------- + + * XML: lxml 3 (http://lxml.de/) and defusedxml (https://pypi.python.org/pypi/defusedxml) + * YAML: pyyaml (http://pyyaml.org/) + * binary plist: biplist (http://explorapp.com/biplist/) + + Optional + -------- + + * HTTP Digest authentication: python3-digest (https://bitbucket.org/akoha/python-digest/) + + + What's It Look Like? + ==================== + + A basic example looks like: + + .. code:: python + + # myapp/api.py + # ============ + from tastypie.resources import ModelResource + from myapp.models import Entry + + + class EntryResource(ModelResource): + class Meta: + queryset = Entry.objects.all() + + + # urls.py + # ======= + from django.conf.urls import url, include + from tastypie.api import Api + from myapp.api import EntryResource + + v1_api = Api(api_name='v1') + v1_api.register(EntryResource()) + + urlpatterns = [ + # The normal jazz here then... + url(r'^api/', include(v1_api.urls)), + ] + + That gets you a fully working, read-write API for the ``Entry`` model that + supports all CRUD operations in a RESTful way. JSON/XML/YAML support is already + there, and it's easy to add related data/authentication/caching. + + You can find more in the documentation at + http://django-tastypie.readthedocs.org/. + + + Why Tastypie? + ============= + + There are other API frameworks out there for Django. You need to + assess the options available and decide for yourself. That said, here are some + common reasons for tastypie. + + * You need an API that is RESTful and uses HTTP well. + * You want to support deep relations. + * You DON'T want to have to write your own serializer to make the output right. + * You want an API framework that has little magic, very flexible and maps well to + the problem domain. + * You want/need XML serialization that is treated equally to JSON (and YAML is + there too). + + + Reference Material + ================== + + * https://django-tastypie.readthedocs.org/en/latest/ + * https://github.com/django-tastypie/django-tastypie/tree/master/tests/basic shows + basic usage of tastypie + * http://en.wikipedia.org/wiki/REST + * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes + * http://www.ietf.org/rfc/rfc2616.txt + * http://jacobian.org/writing/rest-worst-practices/ + + + Getting Help + ============ + + There are two primary ways of getting help. + + 1. Go to `StackOverflow`_ and post a question with the ``tastypie`` tag. + 2. We have an IRC channel (`#tastypie on irc.freenode.net`_) to get help, + bounce an idea by us, or generally shoot the breeze. + + .. _`StackOverflow`: https://stackoverflow.com/questions/tagged/tastypie + .. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie + + + Security + ======== + + Tastypie is committed to providing a flexible and secure API, and was designed + with many security features and options in mind. Due to the complex nature of + APIs and the constant discovery of new attack vectors and vulnerabilities, + no software is immune to security holes. We rely on our community to report + and help us investigate security issues. + + If you come across a security hole **please do not open a Github issue**. + Instead, **drop us an email** at ``tastypie-security@googlegroups.com`` + + We'll then work together to investigate and resolve the problem so we can + announce a solution along with the vulnerability. + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Utilities +Requires: python_mimeparse(>=0.1.4, !=1.5) +Requires: dateutil(>=1.5, !=2.0) diff -Nru django-tastypie-0.12.0/README.rst django-tastypie-0.13.3/README.rst --- django-tastypie-0.12.0/README.rst 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/README.rst 2016-02-17 13:09:06.000000000 +0000 @@ -2,9 +2,29 @@ django-tastypie =============== +.. image:: https://readthedocs.org/projects/django-tastypie/badge/ + :target: https://django-tastypie.readthedocs.org/ + :alt: Docs + +.. image:: https://travis-ci.org/django-tastypie/django-tastypie.svg + :target: https://travis-ci.org/django-tastypie/django-tastypie + :alt: CI + +.. image:: https://coveralls.io/repos/django-tastypie/django-tastypie/badge.svg?service=github + :target: https://coveralls.io/github/django-tastypie/django-tastypie + :alt: Code Coverage + +.. image:: https://img.shields.io/pypi/v/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Version + +.. image:: https://img.shields.io/pypi/dm/django-tastypie.svg + :target: https://pypi.python.org/pypi/django-tastypie + :alt: Downloads + Creating delicious APIs for Django apps since 2010. -Currently in beta (v0.11.2-dev) but being used actively in production on several +Currently in beta (v0.13.3) but being used actively in production on several sites. @@ -14,8 +34,8 @@ Core ---- -* Python 2.6+ or Python 3.3+ -* Django 1.5+ +* Python 2.7+ or Python 3.4+ +* Django 1.7 through Django 1.9 * dateutil (http://labix.org/python-dateutil) >= 2.1 Format Support @@ -34,7 +54,9 @@ What's It Look Like? ==================== -A basic example looks like:: +A basic example looks like: + +.. code:: python # myapp/api.py # ============ @@ -49,17 +71,17 @@ # urls.py # ======= - from django.conf.urls.defaults import * + from django.conf.urls import url, include from tastypie.api import Api from myapp.api import EntryResource v1_api = Api(api_name='v1') v1_api.register(EntryResource()) - urlpatterns = patterns('', + urlpatterns = [ # The normal jazz here then... - (r'^api/', include(v1_api.urls)), - ) + url(r'^api/', include(v1_api.urls)), + ] That gets you a fully working, read-write API for the ``Entry`` model that supports all CRUD operations in a RESTful way. JSON/XML/YAML support is already @@ -72,7 +94,7 @@ Why Tastypie? ============= -There are other, better known API frameworks out there for Django. You need to +There are other API frameworks out there for Django. You need to assess the options available and decide for yourself. That said, here are some common reasons for tastypie. @@ -83,14 +105,13 @@ the problem domain. * You want/need XML serialization that is treated equally to JSON (and YAML is there too). -* You want to support my perceived NIH syndrome, which is less about NIH and more - about trying to help out friends/coworkers. Reference Material ================== -* http://github.com/toastdriven/django-tastypie/tree/master/tests/basic shows +* https://django-tastypie.readthedocs.org/en/latest/ +* https://github.com/django-tastypie/django-tastypie/tree/master/tests/basic shows basic usage of tastypie * http://en.wikipedia.org/wiki/REST * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes @@ -98,6 +119,19 @@ * http://jacobian.org/writing/rest-worst-practices/ +Getting Help +============ + +There are two primary ways of getting help. + +1. Go to `StackOverflow`_ and post a question with the ``tastypie`` tag. +2. We have an IRC channel (`#tastypie on irc.freenode.net`_) to get help, + bounce an idea by us, or generally shoot the breeze. + +.. _`StackOverflow`: https://stackoverflow.com/questions/tagged/tastypie +.. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie + + Security ======== diff -Nru django-tastypie-0.12.0/requirements.txt django-tastypie-0.13.3/requirements.txt --- django-tastypie-0.12.0/requirements.txt 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -python-dateutil>=2.1 - -# Because the official 0.1.4 release (that added Py3 support) HAS NO FILES. -# mimeparse>=0.1.3 -python-mimeparse>=0.1.4 diff -Nru django-tastypie-0.12.0/setup.cfg django-tastypie-0.13.3/setup.cfg --- django-tastypie-0.12.0/setup.cfg 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/setup.cfg 2016-02-17 13:12:45.000000000 +0000 @@ -1,2 +1,8 @@ [wheel] universal = 1 + +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff -Nru django-tastypie-0.12.0/setup.py django-tastypie-0.13.3/setup.py --- django-tastypie-0.12.0/setup.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/setup.py 2016-02-14 09:17:32.000000000 +0000 @@ -8,20 +8,22 @@ from setuptools import setup +from tastypie import __version__ + + setup( name='django-tastypie', - version='0.12.0', + version=__version__, description='A flexible & capable API layer for Django.', author='Daniel Lindsley', author_email='daniel@toastdriven.com', - url='http://github.com/toastdriven/django-tastypie/', + url='https://github.com/django-tastypie/django-tastypie', long_description=open('README.rst', 'r').read(), packages=[ 'tastypie', 'tastypie.utils', 'tastypie.management', 'tastypie.management.commands', - 'tastypie.south_migrations', 'tastypie.migrations', 'tastypie.contrib', 'tastypie.contrib.gis', @@ -32,11 +34,11 @@ }, zip_safe=False, requires=[ - 'python_mimeparse(>=0.1.4)', + 'python_mimeparse(>=0.1.4, !=1.5)', 'dateutil(>=1.5, !=2.0)', ], install_requires=[ - 'python-mimeparse >= 0.1.4', + 'python-mimeparse >= 0.1.4, != 1.5', 'python-dateutil >= 1.5, != 2.0', ], tests_require=['mock', 'PyYAML', 'lxml', 'defusedxml'], diff -Nru django-tastypie-0.12.0/tastypie/api.py django-tastypie-0.13.3/tastypie/api.py --- django-tastypie-0.12.0/tastypie/api.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/api.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,13 +1,14 @@ from __future__ import unicode_literals import warnings -from django.conf.urls import url, patterns, include +from django.conf.urls import url, include from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseBadRequest from tastypie.exceptions import NotRegistered, BadRequest from tastypie.serializers import Serializer -from tastypie.utils import trailing_slash, is_valid_jsonp_callback_value +from tastypie.utils import is_valid_jsonp_callback_value, string_to_python, trailing_slash from tastypie.utils.mime import determine_format, build_content_type +from tastypie.resources import Resource class Api(object): @@ -38,6 +39,9 @@ """ resource_name = getattr(resource._meta, 'resource_name', None) + if not isinstance(resource, Resource): + raise ValueError("An instance of ``Resource`` subclass should be passed in for %s" % resource_name) + if resource_name is None: raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource) @@ -99,12 +103,12 @@ ``Resources`` beneath it. """ pattern_list = [ - url(r"^(?P%s)%s$" % (self.api_name, trailing_slash()), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name), + url(r"^(?P%s)%s$" % (self.api_name, trailing_slash), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name), ] for name in sorted(self._registry.keys()): self._registry[name].api_name = self.api_name - pattern_list.append((r"^(?P%s)/" % self.api_name, include(self._registry[name].urls))) + pattern_list.append(url(r"^(?P%s)/" % self.api_name, include(self._registry[name].urls))) urlpatterns = self.prepend_urls() @@ -113,9 +117,7 @@ warnings.warn("'override_urls' is a deprecated method & will be removed by v1.0.0. Please rename your method to ``prepend_urls``.") urlpatterns += overridden_urls - urlpatterns += patterns('', - *pattern_list - ) + urlpatterns += pattern_list return urlpatterns def top_level(self, request, api_name=None): @@ -123,21 +125,29 @@ A view that returns a serialized list of all resources registers to the ``Api``. Useful for discovery. """ + fullschema = request.GET.get('fullschema', False) + fullschema = string_to_python(fullschema) + available_resources = {} if api_name is None: api_name = self.api_name - for name in sorted(self._registry.keys()): - available_resources[name] = { - 'list_endpoint': self._build_reverse_url("api_dispatch_list", kwargs={ + for name, resource in self._registry.items(): + if not fullschema: + schema = self._build_reverse_url("api_get_schema", kwargs={ 'api_name': api_name, 'resource_name': name, - }), - 'schema': self._build_reverse_url("api_get_schema", kwargs={ + }) + else: + schema = resource.build_schema() + + available_resources[name] = { + 'list_endpoint': self._build_reverse_url("api_dispatch_list", kwargs={ 'api_name': api_name, 'resource_name': name, }), + 'schema': schema, } desired_format = determine_format(request, self.serializer) diff -Nru django-tastypie-0.12.0/tastypie/authentication.py django-tastypie-0.13.3/tastypie/authentication.py --- django-tastypie-0.12.0/tastypie/authentication.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/authentication.py 2016-02-14 09:15:23.000000000 +0000 @@ -1,5 +1,6 @@ from __future__ import unicode_literals import base64 +from hashlib import sha1 import hmac import time import uuid @@ -8,16 +9,11 @@ from django.contrib.auth import authenticate from django.core.exceptions import ImproperlyConfigured from django.middleware.csrf import _sanitize_token, constant_time_compare -from django.utils.http import same_origin +from django.utils.six.moves.urllib.parse import urlparse from django.utils.translation import ugettext as _ -from tastypie.http import HttpUnauthorized -from tastypie.compat import get_user_model, get_username_field -try: - from hashlib import sha1 -except ImportError: - import sha - sha1 = sha.sha +from tastypie.compat import get_user_model, get_username_field +from tastypie.http import HttpUnauthorized try: import python_digest @@ -35,15 +31,56 @@ oauth_provider = None +def same_origin(url1, url2): + """ + Checks if two URLs are 'same-origin' + """ + PROTOCOL_TO_PORT = { + 'http': 80, + 'https': 443, + } + p1, p2 = urlparse(url1), urlparse(url2) + try: + o1 = (p1.scheme, p1.hostname, p1.port or PROTOCOL_TO_PORT[p1.scheme]) + o2 = (p2.scheme, p2.hostname, p2.port or PROTOCOL_TO_PORT[p2.scheme]) + return o1 == o2 + except (ValueError, KeyError): + return False + + class Authentication(object): """ A simple base class to establish the protocol for auth. By default, this indicates the user is always authenticated. """ + auth_type = 'none' + def __init__(self, require_active=True): self.require_active = require_active + def get_authorization_data(self, request): + """ + Verifies that the HTTP Authorization header has the right auth type + (matches self.auth_type) and returns the auth data. + + Raises ValueError when data could not be extracted. + """ + authorization = request.META.get('HTTP_AUTHORIZATION', '') + + if not authorization: + raise ValueError('Authorization header missing or empty.') + + try: + auth_type, data = authorization.split(' ', 1) + except: + raise ValueError('Authorization header must have a space separating auth_type and data.') + + if auth_type.lower() != self.auth_type: + raise ValueError('auth_type is not "%s".' % self.auth_type) + + return data + def is_authenticated(self, request, **kwargs): """ Identifies if the user is authenticated to continue or not. @@ -90,6 +127,8 @@ The realm to use in the ``HttpUnauthorized`` response. Default: ``django-tastypie``. """ + auth_type = 'basic' + def __init__(self, backend=None, realm='django-tastypie', **kwargs): super(BasicAuthentication, self).__init__(**kwargs) self.backend = backend @@ -101,6 +140,13 @@ response['WWW-Authenticate'] = 'Basic Realm="%s"' % self.realm return response + def extract_credentials(self, request): + data = self.get_authorization_data(request) + data = base64.b64decode(data).decode('utf-8') + username, password = data.split(':', 1) + + return username, password + def is_authenticated(self, request, **kwargs): """ Checks a user's basic auth credentials against the current @@ -109,26 +155,21 @@ Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ - if not request.META.get('HTTP_AUTHORIZATION'): - return self._unauthorized() - try: - (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split() - if auth_type.lower() != 'basic': - return self._unauthorized() - user_pass = base64.b64decode(data).decode('utf-8') - except: + username, password = self.extract_credentials(request) + except ValueError: return self._unauthorized() - bits = user_pass.split(':', 1) - - if len(bits) != 2: + if not username or not password: return self._unauthorized() if self.backend: - user = self.backend.authenticate(username=bits[0], password=bits[1]) + user = self.backend.authenticate( + username=username, + password=password + ) else: - user = authenticate(username=bits[0], password=bits[1]) + user = authenticate(username=username, password=password) if user is None: return self._unauthorized() @@ -145,7 +186,11 @@ This implementation returns the user's basic auth username. """ - return request.META.get('REMOTE_USER', 'nouser') + try: + username = self.extract_credentials(request)[0] + except ValueError: + username = '' + return username or 'nouser' class ApiKeyAuthentication(Authentication): @@ -156,20 +201,19 @@ a different model, override the ``get_key`` method to perform the key check as suits your needs. """ + auth_type = 'apikey' + def _unauthorized(self): return HttpUnauthorized() def extract_credentials(self, request): - if request.META.get('HTTP_AUTHORIZATION') and request.META['HTTP_AUTHORIZATION'].lower().startswith('apikey '): - (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split() - - if auth_type.lower() != 'apikey': - raise ValueError("Incorrect authorization header.") - - username, api_key = data.split(':', 1) - else: + try: + data = self.get_authorization_data(request) + except ValueError: username = request.GET.get('username') or request.POST.get('username') api_key = request.GET.get('api_key') or request.POST.get('api_key') + else: + username, api_key = data.split(':', 1) return username, api_key @@ -180,7 +224,6 @@ Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ - try: username, api_key = self.extract_credentials(request) except ValueError: @@ -192,9 +235,9 @@ username_field = get_username_field() User = get_user_model() + lookup_kwargs = {username_field: username} try: - lookup_kwargs = {username_field: username} - user = User.objects.get(**lookup_kwargs) + user = User.objects.select_related('api_key').get(**lookup_kwargs) except (User.DoesNotExist, User.MultipleObjectsReturned): return self._unauthorized() @@ -215,7 +258,8 @@ from tastypie.models import ApiKey try: - ApiKey.objects.get(user=user, key=api_key) + if user.api_key.key != api_key: + return self._unauthorized() except ApiKey.DoesNotExist: return self._unauthorized() @@ -227,7 +271,10 @@ This implementation returns the user's username. """ - username, api_key = self.extract_credentials(request) + try: + username = self.extract_credentials(request)[0] + except ValueError: + username = '' return username or 'nouser' @@ -303,6 +350,8 @@ The realm to use in the ``HttpUnauthorized`` response. Default: ``django-tastypie``. """ + auth_type = 'digest' + def __init__(self, backend=None, realm='django-tastypie', **kwargs): super(DigestAuthentication, self).__init__(**kwargs) self.backend = backend @@ -317,7 +366,7 @@ opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest() response['WWW-Authenticate'] = python_digest.build_digest_challenge( timestamp=time.time(), - secret=getattr(settings, 'SECRET_KEY', ''), + secret=settings.SECRET_KEY, realm=self.realm, opaque=opaque, stale=False @@ -331,21 +380,15 @@ Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ - if not request.META.get('HTTP_AUTHORIZATION'): - return self._unauthorized() - try: - (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split(' ', 1) - - if auth_type.lower() != 'digest': - return self._unauthorized() - except: + self.get_authorization_data(request) + except ValueError: return self._unauthorized() digest_response = python_digest.parse_digest_credentials(request.META['HTTP_AUTHORIZATION']) # FIXME: Should the nonce be per-user? - if not python_digest.validate_nonce(digest_response.nonce, getattr(settings, 'SECRET_KEY', '')): + if not python_digest.validate_nonce(digest_response.nonce, settings.SECRET_KEY): return self._unauthorized() user = self.get_user(digest_response.username) @@ -428,7 +471,7 @@ raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") def is_authenticated(self, request, **kwargs): - from oauth_provider.store import store, InvalidTokenError + from oauth_provider.store import store if self.is_valid_request(request): oauth_request = oauth_provider.utils.get_oauth_request(request) diff -Nru django-tastypie-0.12.0/tastypie/authorization.py django-tastypie-0.13.3/tastypie/authorization.py --- django-tastypie-0.12.0/tastypie/authorization.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/authorization.py 2016-02-14 09:15:23.000000000 +0000 @@ -1,5 +1,7 @@ from __future__ import unicode_literals -from tastypie.exceptions import TastypieError, Unauthorized + +from tastypie.exceptions import Unauthorized +from tastypie.compat import get_module_name class Authorization(object): @@ -15,14 +17,6 @@ self.resource_meta = instance return self - def apply_limits(self, request, object_list): - """ - Deprecated. - - FIXME: REMOVE BEFORE 1.0 - """ - raise TastypieError("Authorization classes no longer support `apply_limits`. Please update to using `read_list`.") - def read_list(self, object_list, bundle): """ Returns a list of all the objects a user is allowed to read. @@ -137,6 +131,12 @@ Both the list & detail variants simply check the model they're based on, as that's all the more granular Django's permission setup gets. """ + + # By default, following `ModelAdmin` "convention", `app.change_model` is used + # `django.contrib.auth.models.Permission` as perm code for viewing and updating. + # https://docs.djangoproject.com/es/1.9/topics/auth/default/#permissions-and-authorization + READ_PERM_CODE = 'change' + def base_checks(self, request, model_klass): # If it doesn't look like a model, we can't check permissions. if not model_klass or not getattr(model_klass, '_meta', None): @@ -148,98 +148,61 @@ return model_klass - def read_list(self, object_list, bundle): - klass = self.base_checks(bundle.request, object_list.model) + def check_user_perm(self, user, permission, obj_or_list): + return user.has_perm(permission) + def perm_list_checks(self, request, code, obj_list): + klass = self.base_checks(request, obj_list.model) if klass is False: return [] - # GET-style methods are always allowed. - return object_list + permission = '%s.%s_%s' % ( + klass._meta.app_label, + code, + get_module_name(klass._meta) + ) - def read_detail(self, object_list, bundle): - klass = self.base_checks(bundle.request, bundle.obj.__class__) + if self.check_user_perm(request.user, permission, obj_list): + return obj_list + return obj_list.none() + + def perm_obj_checks(self, request, code, obj): + klass = self.base_checks(request, obj.__class__) if klass is False: raise Unauthorized("You are not allowed to access that resource.") - # GET-style methods are always allowed. - return True + permission = '%s.%s_%s' % ( + klass._meta.app_label, + code, + get_module_name(klass._meta) + ) - def create_list(self, object_list, bundle): - klass = self.base_checks(bundle.request, object_list.model) + if self.check_user_perm(request.user, permission, obj): + return True - if klass is False: - return [] + raise Unauthorized("You are not allowed to access that resource.") - permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name) + def read_list(self, object_list, bundle): + return self.perm_list_checks(bundle.request, self.READ_PERM_CODE, object_list) - if not bundle.request.user.has_perm(permission): - return [] + def read_detail(self, object_list, bundle): + return self.perm_obj_checks(bundle.request, self.READ_PERM_CODE, bundle.obj) - return object_list + def create_list(self, object_list, bundle): + return self.perm_list_checks(bundle.request, 'add', object_list) def create_detail(self, object_list, bundle): - klass = self.base_checks(bundle.request, bundle.obj.__class__) - - if klass is False: - raise Unauthorized("You are not allowed to access that resource.") - - permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name) - - if not bundle.request.user.has_perm(permission): - raise Unauthorized("You are not allowed to access that resource.") - - return True + return self.perm_obj_checks(bundle.request, 'add', bundle.obj) def update_list(self, object_list, bundle): - klass = self.base_checks(bundle.request, object_list.model) - - if klass is False: - return [] - - permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) - - if not bundle.request.user.has_perm(permission): - return [] - - return object_list + return self.perm_list_checks(bundle.request, 'change', object_list) def update_detail(self, object_list, bundle): - klass = self.base_checks(bundle.request, bundle.obj.__class__) - - if klass is False: - raise Unauthorized("You are not allowed to access that resource.") - - permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) - - if not bundle.request.user.has_perm(permission): - raise Unauthorized("You are not allowed to access that resource.") - - return True + return self.perm_obj_checks(bundle.request, 'change', bundle.obj) def delete_list(self, object_list, bundle): - klass = self.base_checks(bundle.request, object_list.model) - - if klass is False: - return [] - - permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name) - - if not bundle.request.user.has_perm(permission): - return [] - - return object_list + return self.perm_list_checks(bundle.request, 'delete', object_list) def delete_detail(self, object_list, bundle): - klass = self.base_checks(bundle.request, bundle.obj.__class__) - - if klass is False: - raise Unauthorized("You are not allowed to access that resource.") - - permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name) - - if not bundle.request.user.has_perm(permission): - raise Unauthorized("You are not allowed to access that resource.") - - return True + return self.perm_obj_checks(bundle.request, 'delete', bundle.obj) diff -Nru django-tastypie-0.12.0/tastypie/bundle.py django-tastypie-0.13.3/tastypie/bundle.py --- django-tastypie-0.12.0/tastypie/bundle.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/bundle.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,5 +1,6 @@ from __future__ import unicode_literals from django.http import HttpRequest +from django.utils import six # In a separate file to avoid circular imports... @@ -19,6 +20,7 @@ related_name=None, objects_saved=None, related_objects_to_save=None, + via_uri=False, ): self.obj = obj self.data = data or {} @@ -28,6 +30,10 @@ self.errors = {} self.objects_saved = objects_saved or set() self.related_objects_to_save = related_objects_to_save or {} + self.via_uri = via_uri def __repr__(self): - return "" % (self.obj, self.data) + repr_string = "" + if six.PY2: + repr_string = repr_string.encode('utf-8') + return repr_string % (self.obj, self.data) diff -Nru django-tastypie-0.12.0/tastypie/cache.py django-tastypie-0.13.3/tastypie/cache.py --- django-tastypie-0.12.0/tastypie/cache.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/cache.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.core.cache import get_cache +from django.core.cache import caches class NoCache(object): @@ -59,8 +59,8 @@ Defaults to ``60`` seconds. """ super(SimpleCache, self).__init__(*args, **kwargs) - self.cache = get_cache(cache_name) - self.timeout = timeout or self.cache.default_timeout + self.cache = caches[cache_name] + self.timeout = timeout if timeout is not None else self.cache.default_timeout self.public = public self.private = private diff -Nru django-tastypie-0.12.0/tastypie/compat.py django-tastypie-0.13.3/tastypie/compat.py --- django-tastypie-0.12.0/tastypie/compat.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/compat.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,25 +1,22 @@ from __future__ import unicode_literals -from django.conf import settings + import django +from django.conf import settings +from django.contrib.auth import get_user_model # flake8: noqa + __all__ = ['get_user_model', 'get_username_field', 'AUTH_USER_MODEL'] -AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') -# Django 1.5+ compatibility -if django.VERSION >= (1, 5): - def get_user_model(): - from django.contrib.auth import get_user_model as django_get_user_model +AUTH_USER_MODEL = settings.AUTH_USER_MODEL + + +def get_username_field(): + return get_user_model().USERNAME_FIELD - return django_get_user_model() - def get_username_field(): - return get_user_model().USERNAME_FIELD -else: - def get_user_model(): - from django.contrib.auth.models import User +def get_module_name(meta): + return meta.model_name - return User - def get_username_field(): - return 'username' +atomic_decorator = django.db.transaction.atomic diff -Nru django-tastypie-0.12.0/tastypie/contrib/contenttypes/resources.py django-tastypie-0.13.3/tastypie/contrib/contenttypes/resources.py --- django-tastypie-0.12.0/tastypie/contrib/contenttypes/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/contrib/contenttypes/resources.py 2016-02-14 09:14:12.000000000 +0000 @@ -10,8 +10,8 @@ Provides a stand-in resource for GFK relations. """ def __init__(self, resources, *args, **kwargs): - self.resource_mapping = dict((r._meta.resource_name, r) for r in resources) - return super(GenericResource, self).__init__(*args, **kwargs) + self.resource_mapping = {r._meta.resource_name: r for r in resources} + super(GenericResource, self).__init__(*args, **kwargs) def get_via_uri(self, uri, request=None): """ @@ -27,7 +27,7 @@ chomped_uri = uri if prefix and chomped_uri.startswith(prefix): - chomped_uri = chomped_uri[len(prefix)-1:] + chomped_uri = chomped_uri[len(prefix) - 1:] try: view, args, kwargs = resolve(chomped_uri) diff -Nru django-tastypie-0.12.0/tastypie/contrib/gis/COPYING django-tastypie-0.13.3/tastypie/contrib/gis/COPYING --- django-tastypie-0.12.0/tastypie/contrib/gis/COPYING 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/contrib/gis/COPYING 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -resources.py contains code from -https://github.com/newsapps/django-boundaryservice/blob/master/boundaryservice/tastyhacks.py: - -Copyright (c) 2011 Chicago Tribune, Christopher Groskopf, Ryan Nagle - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff -Nru django-tastypie-0.12.0/tastypie/fields.py django-tastypie-0.13.3/tastypie/fields.py --- django-tastypie-0.12.0/tastypie/fields.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/fields.py 2016-02-15 04:48:11.000000000 +0000 @@ -1,11 +1,21 @@ from __future__ import unicode_literals + import datetime from dateutil.parser import parse +import decimal from decimal import Decimal -import re +import importlib + from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned -from django.utils import datetime_safe, importlib -from django.utils import six +from django.db import models +try: + from django.db.models.fields.related import\ + SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor +except ImportError: + from django.db.models.fields.related_descriptors import\ + ReverseOneToOneDescriptor +from django.utils import datetime_safe, six + from tastypie.bundle import Bundle from tastypie.exceptions import ApiFieldError, NotFound from tastypie.utils import dict_strip_unicode_keys, make_aware @@ -16,18 +26,16 @@ return 'No default provided.' -DATE_REGEX = re.compile('^(?P\d{4})-(?P\d{2})-(?P\d{2}).*?$') -DATETIME_REGEX = re.compile('^(?P\d{4})-(?P\d{2})-(?P\d{2})(T|\s+)(?P\d{2}):(?P\d{2}):(?P\d{2}).*?$') - - # All the ApiField variants. class ApiField(object): - """The base implementation of a field used by the resources.""" + "The base implementation of a field used by the resources." + is_m2m = False + is_related = False dehydrated_type = 'string' help_text = '' - def __init__(self, attribute=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, unique=False, help_text=None, use_in='all'): + def __init__(self, attribute=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, unique=False, help_text=None, use_in='all', verbose_name=None): """ Sets up the field. This is generally called when the containing ``Resource`` is initialized. @@ -64,22 +72,28 @@ is a callable, and returns ``True``, the field will be included during dehydration. Defaults to ``all``. + + Optionally accepts ``verbose_name``, which lets you provide a + more verbose name of the field exposed at the schema level. """ # Track what the index thinks this field is called. self.instance_name = None self._resource = None self.attribute = attribute + # Check for `__` in the field for looking through the relation. + self._attrs = attribute.split('__') if attribute is not None and isinstance(attribute, six.string_types) else [] self._default = default self.null = null self.blank = blank self.readonly = readonly - self.value = None self.unique = unique self.use_in = 'all' if use_in in ['all', 'detail', 'list'] or callable(use_in): self.use_in = use_in + self.verbose_name = verbose_name + if help_text: self.help_text = help_text @@ -107,11 +121,9 @@ resource. """ if self.attribute is not None: - # Check for `__` in the field for looking through the relation. - attrs = self.attribute.split('__') current_object = bundle.obj - for attr in attrs: + for attr in self._attrs: previous_object = current_object current_object = getattr(current_object, attr, None) @@ -155,27 +167,37 @@ """ if self.readonly: return None - if not self.instance_name in bundle.data: - if getattr(self, 'is_related', False) and not getattr(self, 'is_m2m', False): + if self.instance_name not in bundle.data: + if self.is_related and not self.is_m2m: # We've got an FK (or alike field) & a possible parent object. # Check for it. if bundle.related_obj and bundle.related_name in (self.attribute, self.instance_name): return bundle.related_obj if self.blank: return None - elif self.attribute and getattr(bundle.obj, self.attribute, None): - return getattr(bundle.obj, self.attribute) - elif self.instance_name and hasattr(bundle.obj, self.instance_name): - return getattr(bundle.obj, self.instance_name) - elif self.has_default(): + if self.attribute: + try: + val = getattr(bundle.obj, self.attribute, None) + + if val is not None: + return val + except ObjectDoesNotExist: + pass + if self.instance_name: + try: + if hasattr(bundle.obj, self.instance_name): + return getattr(bundle.obj, self.instance_name) + except ObjectDoesNotExist: + pass + if self.has_default(): if callable(self._default): return self._default() return self._default - elif self.null: + if self.null: return None - else: - raise ApiFieldError("The '%s' field has no data and doesn't allow a default or null value." % self.instance_name) + + raise ApiFieldError("The '%s' field has no data and doesn't allow a default or null value." % self.instance_name) return bundle.data[self.instance_name] @@ -265,7 +287,10 @@ value = super(DecimalField, self).hydrate(bundle) if value and not isinstance(value, Decimal): - value = Decimal(value) + try: + value = Decimal(value) + except decimal.InvalidOperation: + raise ApiFieldError("Invalid decimal string for '%s' field: '%s'" % (self.instance_name, value)) return value @@ -326,12 +351,11 @@ return None if isinstance(value, six.string_types): - match = DATE_REGEX.search(value) + try: + year, month, day = value[:10].split('-') - if match: - data = match.groupdict() - return datetime_safe.date(int(data['year']), int(data['month']), int(data['day'])) - else: + return datetime_safe.date(int(year), int(month), int(day)) + except ValueError: raise ApiFieldError("Date provided to '%s' field doesn't appear to be a valid date string: '%s'" % (self.instance_name, value)) return value @@ -364,12 +388,12 @@ return None if isinstance(value, six.string_types): - match = DATETIME_REGEX.search(value) + try: + year, month, day = value[:10].split('-') + hour, minute, second = value[10:18].split(':') - if match: - data = match.groupdict() - return make_aware(datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second']))) - else: + return make_aware(datetime_safe.datetime(int(year), int(month), int(day), int(hour), int(minute), int(second))) + except ValueError: raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value)) return value @@ -409,10 +433,10 @@ """ dehydrated_type = 'related' is_related = True - self_referential = False help_text = 'A related resource. Can be either a URI or set of nested resource data.' - def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, use_in='all', full_list=True, full_detail=True): + def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, use_in='all', verbose_name=None, full_list=True, full_detail=True): + """ Builds the field and prepares it to access to related data. @@ -456,6 +480,9 @@ dehydration. Defaults to ``all``. + Optionally accepts ``verbose_name``, which lets you provide a + more verbose name of the field exposed at the schema level. + Optionally accepts a ``full_list``, which indicated whether or not data should be fully dehydrated when the request is for a list of resources. Accepts ``True``, ``False`` or a callable that accepts @@ -468,56 +495,35 @@ bundle and returns ``True`` or ``False``.Depends on ``full`` being ``True``. Defaults to ``True``. """ - self.instance_name = None - self._resource = None - self.to = to - self.attribute = attribute + super(RelatedField, self).__init__(attribute=attribute, default=default, null=null, blank=blank, readonly=readonly, unique=unique, help_text=help_text, use_in=use_in, verbose_name=verbose_name) self.related_name = related_name - self._default = default - self.null = null - self.blank = blank - self.readonly = readonly + self.to = to + self._to_class = None + self._rel_resources = {} self.full = full + self.full_list = full_list if callable(full_list) else lambda bundle: full_list + self.full_detail = full_detail if callable(full_detail) else lambda bundle: full_detail + self.api_name = None self.resource_name = None - self.unique = unique - self._to_class = None - self.use_in = 'all' - self.full_list = full_list - self.full_detail = full_detail - - if use_in in ['all', 'detail', 'list'] or callable(use_in): - self.use_in = use_in - - if self.to == 'self': - self.self_referential = True - self._to_class = self.__class__ - - if help_text: - self.help_text = help_text - - def contribute_to_class(self, cls, name): - super(RelatedField, self).contribute_to_class(cls, name) - - # Check if we're self-referential and hook it up. - # We can't do this quite like Django because there's no ``AppCache`` - # here (which I think we should avoid as long as possible). - if self.self_referential or self.to == 'self': - self._to_class = cls def get_related_resource(self, related_instance): """ Instaniates the related resource. """ + related_class = type(related_instance) + if related_class in self._rel_resources: + return self._rel_resources[related_class] + related_resource = self.to_class() # Fix the ``api_name`` if it's not present. if related_resource._meta.api_name is None: - if self._resource and not self._resource._meta.api_name is None: + if self._resource and self._resource._meta.api_name is not None: related_resource._meta.api_name = self._resource._meta.api_name - # Try to be efficient about DB queries. - related_resource.instance = related_instance + self._rel_resources[related_class] = related_resource + return related_resource @property @@ -532,6 +538,13 @@ self._to_class = self.to return self._to_class + # Check if we're self-referential and hook it up. + # We can't do this quite like Django because there's no ``AppCache`` + # here (which I think we should avoid as long as possible). + if self.to == 'self': + self._to_class = self._resource + return self._to_class + # It's a string. Let's figure it out. if '.' in self.to: # Try to import. @@ -563,7 +576,7 @@ else: # ZOMG extra data and big payloads. bundle = related_resource.build_bundle( - obj=related_resource.instance, + obj=bundle.obj, request=bundle.request, objects_saved=bundle.objects_saved ) @@ -574,15 +587,21 @@ Given a URI is provided, the related resource is attempted to be loaded based on the identifiers in the URI. """ + err_msg = "Could not find the provided %s object via resource URI '%s'." % (fk_resource._meta.resource_name, uri,) + + if not uri: + raise ApiFieldError(err_msg) + try: obj = fk_resource.get_via_uri(uri, request=request) bundle = fk_resource.build_bundle( obj=obj, - request=request + request=request, + via_uri=True ) return fk_resource.full_dehydrate(bundle) except ObjectDoesNotExist: - raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri) + raise ApiFieldError(err_msg) def resource_from_data(self, fk_resource, data, request=None, related_obj=None, related_name=None): """ @@ -591,8 +610,18 @@ """ # Try to hydrate the data provided. data = dict_strip_unicode_keys(data) + obj = None + if getattr(fk_resource._meta, 'include_resource_uri', True) and 'resource_uri' in data: + uri = data['resource_uri'] + err_msg = "Could not find the provided %s object via resource URI '%s'." % (fk_resource._meta.resource_name, uri,) + try: + obj = fk_resource.get_via_uri(uri, request=request) + except ObjectDoesNotExist: + raise ApiFieldError(err_msg) + fk_bundle = fk_resource.build_bundle( data=data, + obj=obj, request=request ) @@ -600,20 +629,24 @@ fk_bundle.related_obj = related_obj fk_bundle.related_name = related_name - unique_keys = dict((k, v) for k, v in data.items() if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique)) + unique_keys = { + k: v + for k, v in data.items() + if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique) + } # If we have no unique keys, we shouldn't go look for some resource that # happens to match other kwargs. In the case of a create, it might be the # completely wrong resource. # We also need to check to see if updates are allowed on the FK resource. - if unique_keys and fk_resource.can_update(): + if not obj and unique_keys: try: - return fk_resource.obj_update(fk_bundle, skip_errors=True, **data) - except (NotFound, TypeError): + fk_resource.obj_get(fk_bundle, skip_errors=True, **data) + except (ObjectDoesNotExist, NotFound, TypeError): try: # Attempt lookup by primary key - return fk_resource.obj_update(fk_bundle, skip_errors=True, **unique_keys) - except NotFound: + fk_resource.obj_get(fk_bundle, skip_errors=True, **unique_keys) + except (ObjectDoesNotExist, NotFound): pass except MultipleObjectsReturned: pass @@ -644,7 +677,7 @@ Accepts either a URI, a data dictionary (or dictionary-like structure) or an object with a ``pk``. """ - self.fk_resource = self.to_class() + fk_resource = self.to_class() kwargs = { 'request': request, 'related_obj': related_obj, @@ -656,15 +689,15 @@ return value elif isinstance(value, six.string_types): # We got a URI. Load the object and assign it. - return self.resource_from_uri(self.fk_resource, value, **kwargs) - elif hasattr(value, 'items'): + return self.resource_from_uri(fk_resource, value, **kwargs) + elif isinstance(value, dict): # We've got a data dictionary. # Since this leads to creation, this is the only one of these # methods that might care about "parent" data. - return self.resource_from_data(self.fk_resource, value, **kwargs) + return self.resource_from_data(fk_resource, value, **kwargs) elif hasattr(value, 'pk'): # We've got an object with a primary key. - return self.resource_from_pk(self.fk_resource, value, **kwargs) + return self.resource_from_pk(fk_resource, value, **kwargs) else: raise ApiFieldError("The '%s' field was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: %s." % (self.instance_name, value)) @@ -677,10 +710,10 @@ if self.full: is_details_view = not for_list if is_details_view: - if (not callable(self.full_detail) and self.full_detail) or (callable(self.full_detail) and self.full_detail(bundle)): + if self.full_detail(bundle): should_dehydrate_full_resource = True else: - if (not callable(self.full_list) and self.full_list) or (callable(self.full_list) and self.full_list(bundle)): + if self.full_list(bundle): should_dehydrate_full_resource = True return should_dehydrate_full_resource @@ -696,40 +729,56 @@ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, - unique=False, help_text=None, use_in='all', full_list=True, full_detail=True): + unique=False, help_text=None, use_in='all', verbose_name=None, + full_list=True, full_detail=True): super(ToOneField, self).__init__( to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text, use_in=use_in, - full_list=full_list, full_detail=full_detail + verbose_name=verbose_name, full_list=full_list, + full_detail=full_detail ) - self.fk_resource = None + + def contribute_to_class(self, cls, name): + super(ToOneField, self).contribute_to_class(cls, name) + if not self.related_name: + related_field = getattr(self._resource._meta.object_class, self.attribute, None) + if isinstance(related_field, ReverseOneToOneDescriptor): + # This is the case when we are writing to a reverse one to one field. + # Enable related name to make this work fantastically. + # see https://code.djangoproject.com/ticket/18638 (bug; closed; worksforme) + # and https://github.com/django-tastypie/django-tastypie/issues/566 + + # this gets the related_name of the one to one field of our model + self.related_name = related_field.related.field.name def dehydrate(self, bundle, for_list=True): foreign_obj = None - if isinstance(self.attribute, six.string_types): - attrs = self.attribute.split('__') + if callable(self.attribute): + previous_obj = bundle.obj + foreign_obj = self.attribute(bundle) + elif isinstance(self.attribute, six.string_types): foreign_obj = bundle.obj - for attr in attrs: + for attr in self._attrs: previous_obj = foreign_obj try: foreign_obj = getattr(foreign_obj, attr, None) except ObjectDoesNotExist: foreign_obj = None - elif callable(self.attribute): - foreign_obj = self.attribute(bundle) if not foreign_obj: if not self.null: - raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr)) - + if callable(self.attribute): + raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj)) + else: + raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr)) return None - self.fk_resource = self.get_related_resource(foreign_obj) + fk_resource = self.get_related_resource(foreign_obj) fk_bundle = Bundle(obj=foreign_obj, request=bundle.request) - return self.dehydrate_related(fk_bundle, self.fk_resource, for_list=for_list) + return self.dehydrate_related(fk_bundle, fk_resource, for_list=for_list) def hydrate(self, bundle): value = super(ToOneField, self).hydrate(bundle) @@ -739,6 +788,7 @@ return self.build_related_resource(value, request=bundle.request) + class ForeignKey(ToOneField): """ A convenience subclass for those who prefer to mirror ``django.db.models``. @@ -768,14 +818,15 @@ def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, - unique=False, help_text=None, use_in='all', full_list=True, full_detail=True): + unique=False, help_text=None, use_in='all', verbose_name=None, + full_list=True, full_detail=True): super(ToManyField, self).__init__( to, attribute, related_name=related_name, default=default, null=null, blank=blank, readonly=readonly, full=full, unique=unique, help_text=help_text, use_in=use_in, - full_list=full_list, full_detail=full_detail + verbose_name=verbose_name, full_list=full_list, + full_detail=full_detail ) - self.m2m_bundles = [] def dehydrate(self, bundle, for_list=True): if not bundle.obj or not bundle.obj.pk: @@ -788,11 +839,12 @@ previous_obj = bundle.obj attr = self.attribute - if isinstance(self.attribute, six.string_types): - attrs = self.attribute.split('__') + if callable(self.attribute): + the_m2ms = self.attribute(bundle) + elif isinstance(self.attribute, six.string_types): the_m2ms = bundle.obj - for attr in attrs: + for attr in self._attrs: previous_obj = the_m2ms try: the_m2ms = getattr(the_m2ms, attr, None) @@ -802,25 +854,23 @@ if not the_m2ms: break - elif callable(self.attribute): - the_m2ms = self.attribute(bundle) - if not the_m2ms: if not self.null: raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr)) return [] - self.m2m_resources = [] - m2m_dehydrated = [] + if isinstance(the_m2ms, models.Manager): + the_m2ms = the_m2ms.all() - # TODO: Also model-specific and leaky. Relies on there being a - # ``Manager`` there. - for m2m in the_m2ms.all(): - m2m_resource = self.get_related_resource(m2m) - m2m_bundle = Bundle(obj=m2m, request=bundle.request) - self.m2m_resources.append(m2m_resource) - m2m_dehydrated.append(self.dehydrate_related(m2m_bundle, m2m_resource, for_list=for_list)) + m2m_dehydrated = [ + self.dehydrate_related( + Bundle(obj=m2m, request=bundle.request), + self.get_related_resource(m2m), + for_list=for_list + ) + for m2m in the_m2ms + ] return m2m_dehydrated @@ -834,28 +884,23 @@ if bundle.data.get(self.instance_name) is None: if self.blank: return [] - elif self.null: + if self.null: return [] - else: - raise ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name) - - m2m_hydrated = [] + raise ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name) - for value in bundle.data.get(self.instance_name): - if value is None: - continue - - kwargs = { - 'request': bundle.request, - } - - if self.related_name: - kwargs['related_obj'] = bundle.obj - kwargs['related_name'] = self.related_name - - m2m_hydrated.append(self.build_related_resource(value, **kwargs)) + kwargs = { + 'request': bundle.request, + } - return m2m_hydrated + if self.related_name: + kwargs['related_obj'] = bundle.obj + kwargs['related_name'] = self.related_name + + return [ + self.build_related_resource(value, **kwargs) + for value in bundle.data.get(self.instance_name) + if value is not None + ] class ManyToManyField(ToManyField): @@ -890,7 +935,7 @@ except (ValueError, TypeError) as e: raise ApiFieldError(str(e)) else: - return datetime.time(dt.hour, dt.minute, dt.second) + return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond) def hydrate(self, bundle): value = super(TimeField, self).hydrate(bundle) diff -Nru django-tastypie-0.12.0/tastypie/http.py django-tastypie-0.13.3/tastypie/http.py --- django-tastypie-0.12.0/tastypie/http.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/http.py 2016-02-14 09:10:26.000000000 +0000 @@ -22,6 +22,10 @@ class HttpNoContent(HttpResponse): status_code = 204 + def __init__(self, *args, **kwargs): + super(HttpNoContent, self).__init__(*args, **kwargs) + del self['Content-Type'] + class HttpMultipleChoices(HttpResponse): status_code = 300 @@ -77,4 +81,3 @@ class HttpNotImplemented(HttpResponse): status_code = 501 - diff -Nru django-tastypie-0.12.0/tastypie/__init__.py django-tastypie-0.13.3/tastypie/__init__.py --- django-tastypie-0.12.0/tastypie/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/__init__.py 2016-02-17 13:09:17.000000000 +0000 @@ -2,4 +2,8 @@ __author__ = 'Daniel Lindsley & the Tastypie core team' -__version__ = (0, 12, 0) + +VERSION = (0, 13, 3) + +__short_version__ = '.'.join(map(str, VERSION[0:2])) +__version__ = ''.join(['.'.join(map(str, VERSION[0:3])), ''.join(VERSION[3:])]) diff -Nru django-tastypie-0.12.0/tastypie/management/commands/backfill_api_keys.py django-tastypie-0.13.3/tastypie/management/commands/backfill_api_keys.py --- django-tastypie-0.12.0/tastypie/management/commands/backfill_api_keys.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/management/commands/backfill_api_keys.py 2016-02-14 09:10:26.000000000 +0000 @@ -9,7 +9,7 @@ help = "Goes through all users and adds API keys for any that don't have one." def handle_noargs(self, **options): - """Goes through all users and adds API keys for any that don't have one.""" + "Goes through all users and adds API keys for any that don't have one." self.verbosity = int(options.get('verbosity', 1)) User = get_user_model() diff -Nru django-tastypie-0.12.0/tastypie/models.py django-tastypie-0.13.3/tastypie/models.py --- django-tastypie-0.12.0/tastypie/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/models.py 2016-02-14 09:15:23.000000000 +0000 @@ -1,16 +1,15 @@ from __future__ import unicode_literals + +from hashlib import sha1 import hmac import time + from django.conf import settings from django.db import models from django.utils.encoding import python_2_unicode_compatible + from tastypie.utils import now -try: - from hashlib import sha1 -except ImportError: - import sha - sha1 = sha.sha @python_2_unicode_compatible class ApiAccess(models.Model): @@ -31,6 +30,7 @@ if 'django.contrib.auth' in settings.INSTALLED_APPS: import uuid from tastypie.compat import AUTH_USER_MODEL + class ApiKey(models.Model): user = models.OneToOneField(AUTH_USER_MODEL, related_name='api_key') key = models.CharField(max_length=128, blank=True, default='', db_index=True) @@ -54,10 +54,9 @@ class Meta: abstract = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False) - - def create_api_key(sender, **kwargs): + def create_api_key(sender, instance, created, **kwargs): """ A signal for hooking up automatic ``ApiKey`` creation. """ - if kwargs.get('created') is True: - ApiKey.objects.create(user=kwargs.get('instance')) + if kwargs.get('raw', False) is False and created is True: + ApiKey.objects.create(user=instance) diff -Nru django-tastypie-0.12.0/tastypie/paginator.py django-tastypie-0.13.3/tastypie/paginator.py --- django-tastypie-0.12.0/tastypie/paginator.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/paginator.py 2016-02-14 09:10:26.000000000 +0000 @@ -136,7 +136,7 @@ if offset - limit < 0: return None - return self._generate_uri(limit, offset-limit) + return self._generate_uri(limit, offset - limit) def get_next(self, limit, offset, count): """ @@ -146,7 +146,7 @@ if offset + limit >= count: return None - return self._generate_uri(limit, offset+limit) + return self._generate_uri(limit, offset + limit) def _generate_uri(self, limit, offset): if self.resource_uri is None: diff -Nru django-tastypie-0.12.0/tastypie/resources.py django-tastypie-0.13.3/tastypie/resources.py --- django-tastypie-0.12.0/tastypie/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/resources.py 2016-02-17 13:07:02.000000000 +0000 @@ -1,43 +1,64 @@ from __future__ import unicode_literals -from __future__ import with_statement + from copy import deepcopy +from datetime import datetime import logging +import sys +from time import mktime +import traceback import warnings +from wsgiref.handlers import format_date_time +import django from django.conf import settings -from django.conf.urls import patterns, url -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError -from django.core.urlresolvers import NoReverseMatch, reverse, resolve, Resolver404, get_script_prefix +from django.conf.urls import url +from django.core.exceptions import ( + ObjectDoesNotExist, MultipleObjectsReturned, ValidationError, +) +from django.core.urlresolvers import ( + NoReverseMatch, reverse, Resolver404, get_script_prefix +) from django.core.signals import got_request_exception -from django.db import transaction +from django.core.exceptions import ImproperlyConfigured +try: + from django.contrib.gis.db.models.fields import GeometryField +except (ImproperlyConfigured, ImportError): + GeometryField = None from django.db.models.constants import LOOKUP_SEP +try: + from django.db.models.fields.related import\ + SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor +except ImportError: + from django.db.models.fields.related_descriptors import\ + ReverseOneToOneDescriptor from django.db.models.sql.constants import QUERY_TERMS from django.http import HttpResponse, HttpResponseNotFound, Http404 +from django.utils import six from django.utils.cache import patch_cache_control, patch_vary_headers from django.utils.html import escape -from django.utils import six +from django.views.decorators.csrf import csrf_exempt from tastypie.authentication import Authentication from tastypie.authorization import ReadOnlyAuthorization from tastypie.bundle import Bundle from tastypie.cache import NoCache from tastypie.constants import ALL, ALL_WITH_RELATIONS -from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse, Unauthorized +from tastypie.exceptions import ( + NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, + ImmediateHttpResponse, Unauthorized, UnsupportedFormat, +) from tastypie import fields from tastypie import http from tastypie.paginator import Paginator from tastypie.serializers import Serializer from tastypie.throttle import BaseThrottle -from tastypie.utils import is_valid_jsonp_callback_value, dict_strip_unicode_keys, trailing_slash +from tastypie.utils import ( + dict_strip_unicode_keys, is_valid_jsonp_callback_value, string_to_python, + trailing_slash, +) from tastypie.utils.mime import determine_format, build_content_type from tastypie.validation import Validation - -# If ``csrf_exempt`` isn't present, stub it. -try: - from django.views.decorators.csrf import csrf_exempt -except ImportError: - def csrf_exempt(func): - return func +from tastypie.compat import get_module_name, atomic_decorator def sanitize(text): @@ -46,11 +67,6 @@ return escape(text).replace(''', "'").replace('"', '"') -class NOT_AVAILABLE: - def __str__(self): - return 'No such data is available.' - - class ResourceOptions(object): """ A configuration class for ``Resource``. @@ -116,18 +132,15 @@ declared_fields = {} # Inherit any fields from parent(s). - try: - parents = [b for b in bases if issubclass(b, Resource)] - # Simulate the MRO. - parents.reverse() - - for p in parents: - parent_fields = getattr(p, 'base_fields', {}) - - for field_name, field_object in parent_fields.items(): - attrs['base_fields'][field_name] = deepcopy(field_object) - except NameError: - pass + parents = [b for b in bases if issubclass(b, Resource)] + # Simulate the MRO. + parents.reverse() + + for p in parents: + parent_fields = getattr(p, 'base_fields', {}) + + for field_name, field_object in parent_fields.items(): + attrs['base_fields'][field_name] = deepcopy(field_object) for field_name, obj in attrs.copy().items(): # Look for ``dehydrated_type`` instead of doing ``isinstance``, @@ -151,9 +164,9 @@ new_class._meta.resource_name = resource_name if getattr(new_class._meta, 'include_resource_uri', True): - if not 'resource_uri' in new_class.base_fields: - new_class.base_fields['resource_uri'] = fields.CharField(readonly=True) - elif 'resource_uri' in new_class.base_fields and not 'resource_uri' in attrs: + if 'resource_uri' not in new_class.base_fields: + new_class.base_fields['resource_uri'] = fields.CharField(readonly=True, verbose_name="resource uri") + elif 'resource_uri' in new_class.base_fields and 'resource_uri' not in attrs: del(new_class.base_fields['resource_uri']) for field_name, field_object in new_class.base_fields.items(): @@ -175,15 +188,20 @@ data sources, such as search results, files, other data, etc. """ def __init__(self, api_name=None): + # this can cause: + # TypeError: object.__new__(method-wrapper) is not safe, use method-wrapper.__new__() + # when trying to copy a generator used as a default. Wrap call to + # generator in lambda to get around this error. self.fields = deepcopy(self.base_fields) - if not api_name is None: + if api_name is not None: self._meta.api_name = api_name def __getattr__(self, name): - if name in self.fields: + try: return self.fields[name] - raise AttributeError(name) + except KeyError: + raise AttributeError(name) def wrap_view(self, view): """ @@ -228,7 +246,9 @@ data = {"error": sanitize(e.messages)} return self.error_response(request, data, response_class=http.HttpBadRequest) except Exception as e: - if hasattr(e, 'response'): + # Prevent muting non-django's exceptions + # i.e. RequestException from 'requests' library + if hasattr(e, 'response') and isinstance(e.response, HttpResponse): return e.response # A real, non-expected exception. @@ -250,8 +270,6 @@ return wrapper def _handle_500(self, request, exception): - import traceback - import sys the_trace = '\n'.join(traceback.format_exception(*(sys.exc_info()))) response_class = http.HttpApplicationError response_code = 500 @@ -262,6 +280,10 @@ response_class = HttpResponseNotFound response_code = 404 + elif isinstance(exception, UnsupportedFormat): + response_class = http.HttpBadRequest + response_code = 400 + if settings.DEBUG: data = { "error_message": sanitize(six.text_type(exception)), @@ -271,9 +293,8 @@ # When DEBUG is False, send an error message to the admins (unless it's # a 404, in which case we check the setting). - send_broken_links = getattr(settings, 'SEND_BROKEN_LINK_EMAILS', False) - if not response_code == 404 or send_broken_links: + if not response_code == 404: log = logging.getLogger('django.request.tastypie') log.error('Internal Server Error: %s' % request.path, exc_info=True, extra={'status_code': response_code, 'request': request}) @@ -300,10 +321,10 @@ The standard URLs this ``Resource`` should respond to. """ return [ - url(r"^(?P%s)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_list'), name="api_dispatch_list"), - url(r"^(?P%s)/schema%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_schema'), name="api_get_schema"), - url(r"^(?P%s)/set/(?P<%s_list>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash()), self.wrap_view('get_multiple'), name="api_get_multiple"), - url(r"^(?P%s)/(?P<%s>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash()), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), + url(r"^(?P%s)%s$" % (self._meta.resource_name, trailing_slash), self.wrap_view('dispatch_list'), name="api_dispatch_list"), + url(r"^(?P%s)/schema%s$" % (self._meta.resource_name, trailing_slash), self.wrap_view('get_schema'), name="api_get_schema"), + url(r"^(?P%s)/set/(?P<%s_list>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash), self.wrap_view('get_multiple'), name="api_get_multiple"), + url(r"^(?P%s)/(?P<%s>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), ] def override_urls(self): @@ -335,10 +356,7 @@ urls += overridden_urls urls += self.base_urls() - urlpatterns = patterns('', - *urls - ) - return urlpatterns + return urls def determine_format(self, request): """ @@ -378,7 +396,7 @@ Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. """ - deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', 'application/json')) + deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', format)) return deserialized def alter_list_data_to_serialize(self, request, data): @@ -523,7 +541,7 @@ response['Allow'] = allows raise ImmediateHttpResponse(response=response) - if not request_method in allowed: + if request_method not in allowed: response = http.HttpMethodNotAllowed(allows) response['Allow'] = allows raise ImmediateHttpResponse(response=response) @@ -544,7 +562,7 @@ if isinstance(auth_result, HttpResponse): raise ImmediateHttpResponse(response=auth_result) - if not auth_result is True: + if auth_result is not True: raise ImmediateHttpResponse(response=http.HttpUnauthorized()) def throttle_check(self, request): @@ -557,9 +575,19 @@ identifier = self._meta.authentication.get_identifier(request) # Check to see if they should be throttled. - if self._meta.throttle.should_be_throttled(identifier): + throttle = self._meta.throttle.should_be_throttled(identifier) + + if throttle: # Throttle limit exceeded. - raise ImmediateHttpResponse(response=http.HttpTooManyRequests()) + + response = http.HttpTooManyRequests() + + if isinstance(throttle, int) and not isinstance(throttle, bool): + response['Retry-After'] = throttle + elif isinstance(throttle, datetime): + response['Retry-After'] = format_date_time(mktime(throttle.timetuple())) + + raise ImmediateHttpResponse(response=response) def log_throttled_access(self, request): """ @@ -593,7 +621,7 @@ """ try: auth_result = self._meta.authorization.read_detail(object_list, bundle) - if not auth_result is True: + if auth_result is not True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) @@ -619,7 +647,7 @@ """ try: auth_result = self._meta.authorization.create_detail(object_list, bundle) - if not auth_result is True: + if auth_result is not True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) @@ -645,7 +673,7 @@ """ try: auth_result = self._meta.authorization.update_detail(object_list, bundle) - if not auth_result is True: + if auth_result is not True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) @@ -678,7 +706,7 @@ return auth_result - def build_bundle(self, obj=None, data=None, request=None, objects_saved=None): + def build_bundle(self, obj=None, data=None, request=None, objects_saved=None, via_uri=None): """ Given either an object, a data dictionary or both, builds a ``Bundle`` for use throughout the ``dehydrate/hydrate`` cycle. @@ -694,10 +722,11 @@ obj=obj, data=data, request=request, - objects_saved=objects_saved + objects_saved=objects_saved, + via_uri=via_uri ) - def build_filters(self, filters=None): + def build_filters(self, filters=None, ignore_bad_filters=False): """ Allows for the filtering of applicable objects. @@ -726,21 +755,26 @@ Usually just accesses ``bundle.obj.pk`` by default. """ - return getattr(bundle.obj, self._meta.detail_uri_name) + return getattr(bundle.obj, self._meta.detail_uri_name, None) # URL-related methods. def detail_uri_kwargs(self, bundle_or_obj): """ - This needs to be implemented at the user level. - - Given a ``Bundle`` or an object, it returns the extra kwargs needed to - generate a detail URI. + Given a ``Bundle`` or an object (typically a ``Model`` instance), + it returns the extra kwargs needed to generate a detail URI. - ``ModelResource`` includes a full working version specific to Django's - ``Models``. + By default, it uses this resource's ``detail_uri_name`` in order to + create the URI. """ - raise NotImplementedError() + kwargs = {} + + if isinstance(bundle_or_obj, Bundle): + bundle_or_obj = bundle_or_obj.obj + + kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj, self._meta.detail_uri_name) + + return kwargs def resource_uri_kwargs(self, bundle_or_obj=None): """ @@ -800,7 +834,7 @@ chomped_uri = uri if prefix and chomped_uri.startswith(prefix): - chomped_uri = chomped_uri[len(prefix)-1:] + chomped_uri = chomped_uri[len(prefix) - 1:] # We mangle the path a bit further & run URL resolution against *only* # the current class. This ought to prevent bad URLs from resolving to @@ -831,31 +865,34 @@ Given a bundle with an object instance, extract the information from it to populate the resource. """ - use_in = ['all', 'list' if for_list else 'detail'] + data = bundle.data + + api_name = self._meta.api_name + resource_name = self._meta.resource_name # Dehydrate each field. for field_name, field_object in self.fields.items(): # If it's not for use in this mode, skip - field_use_in = getattr(field_object, 'use_in', 'all') + field_use_in = field_object.use_in if callable(field_use_in): if not field_use_in(bundle): continue else: - if field_use_in not in use_in: + if field_use_in not in ['all', 'list' if for_list else 'detail']: continue # A touch leaky but it makes URI resolution work. - if getattr(field_object, 'dehydrated_type', None) == 'related': - field_object.api_name = self._meta.api_name - field_object.resource_name = self._meta.resource_name + if field_object.dehydrated_type == 'related': + field_object.api_name = api_name + field_object.resource_name = resource_name - bundle.data[field_name] = field_object.dehydrate(bundle, for_list=for_list) + data[field_name] = field_object.dehydrate(bundle, for_list=for_list) # Check for an optional method to do further dehydration. method = getattr(self, "dehydrate_%s" % field_name, None) if method: - bundle.data[field_name] = method(bundle) + data[field_name] = method(bundle) bundle = self.dehydrate(bundle) return bundle @@ -902,9 +939,9 @@ if value is not None or field_object.null: # We need to avoid populating M2M data here as that will # cause things to blow up. - if not getattr(field_object, 'is_related', False): + if not field_object.is_related: setattr(bundle.obj, field_object.attribute, value) - elif not getattr(field_object, 'is_m2m', False): + elif not field_object.is_m2m: if value is not None: # NOTE: A bug fix in Django (ticket #18153) fixes incorrect behavior # which Tastypie was relying on. To fix this, we store value.obj to @@ -913,10 +950,12 @@ setattr(bundle.obj, field_object.attribute, value.obj) except (ValueError, ObjectDoesNotExist): bundle.related_objects_to_save[field_object.attribute] = value.obj + elif field_object.null: + if not isinstance(getattr(bundle.obj.__class__, field_object.attribute, None), ReverseOneToOneDescriptor): + # only update if not a reverse one to one field + setattr(bundle.obj, field_object.attribute, value) elif field_object.blank: continue - elif field_object.null: - setattr(bundle.obj, field_object.attribute, value) return bundle @@ -940,7 +979,7 @@ raise HydrationError("You must call 'full_hydrate' before attempting to run 'hydrate_m2m' on %r." % self) for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): + if not field_object.is_m2m: continue if field_object.attribute: @@ -951,7 +990,7 @@ bundle.data[field_name] = field_object.hydrate_m2m(bundle) for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): + if not field_object.is_m2m: continue method = getattr(self, "hydrate_%s" % field_name, None) @@ -982,6 +1021,12 @@ if self._meta.filtering: data['filtering'] = self._meta.filtering + # Skip assigning pk_field_name for non-model resources + try: + pk_field_name = self._meta.queryset.model._meta.pk.name + except AttributeError: + pk_field_name = None + for field_name, field_object in self.fields.items(): data['fields'][field_name] = { 'default': field_object.default, @@ -991,13 +1036,24 @@ 'readonly': field_object.readonly, 'help_text': field_object.help_text, 'unique': field_object.unique, + 'primary_key': True if field_name == pk_field_name else False, + 'verbose_name': field_object.verbose_name or field_name.replace("_", " "), } + if field_object.dehydrated_type == 'related': - if getattr(field_object, 'is_m2m', False): + if field_object.is_m2m: related_type = 'to_many' else: related_type = 'to_one' data['fields'][field_name]['related_type'] = related_type + try: + uri = reverse('api_get_schema', kwargs={ + 'api_name': self._meta.api_name, + 'resource_name': field_object.to_class()._meta.resource_name + }) + except NoReverseMatch: + uri = '' + data['fields'][field_name]['related_schema'] = uri return data @@ -1021,10 +1077,7 @@ This is based off the current api_name/resource_name/args/kwargs. """ - smooshed = [] - - for key, value in kwargs.items(): - smooshed.append("%s=%s" % (key, value)) + smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()] # Use a list plus a ``.join()`` because it's faster than concatenation. return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), ':'.join(sorted(smooshed))) @@ -1042,14 +1095,6 @@ """ raise NotImplementedError() - def apply_authorization_limits(self, request, object_list): - """ - Deprecated. - - FIXME: REMOVE BEFORE 1.0 - """ - return self._meta.authorization.apply_limits(request, object_list) - def can_create(self): """ Checks to ensure ``post`` is within ``allowed_methods``. @@ -1290,11 +1335,10 @@ to_be_serialized = paginator.page() # Dehydrate the bundles in preparation for serialization. - bundles = [] - - for obj in to_be_serialized[self._meta.collection_name]: - bundle = self.build_bundle(obj=obj, request=request) - bundles.append(self.full_dehydrate(bundle, for_list=True)) + bundles = [ + self.full_dehydrate(self.build_bundle(obj=obj, request=request), for_list=True) + for obj in to_be_serialized[self._meta.collection_name] + ] to_be_serialized[self._meta.collection_name] = bundles to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) @@ -1374,8 +1418,8 @@ deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) deserialized = self.alter_deserialized_list_data(request, deserialized) - if not self._meta.collection_name in deserialized: - raise BadRequest("Invalid data sent.") + if self._meta.collection_name not in deserialized: + raise BadRequest("Invalid data sent: missing '%s'" % self._meta.collection_name) basic_bundle = self.build_bundle(request=request) self.obj_delete_list_for_update(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) @@ -1396,8 +1440,12 @@ if not self._meta.always_return_data: return http.HttpNoContent() else: - to_be_serialized = {} - to_be_serialized[self._meta.collection_name] = [self.full_dehydrate(bundle, for_list=True) for bundle in bundles_seen] + to_be_serialized = { + self._meta.collection_name: [ + self.full_dehydrate(b, for_list=True) + for b in bundles_seen + ] + } to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) return self.create_response(request, to_be_serialized) @@ -1430,6 +1478,9 @@ if not self._meta.always_return_data: return http.HttpNoContent() else: + # Invalidate prefetched_objects_cache for bundled object + # because we might have changed a prefetched field + updated_bundle.obj._prefetched_objects_cache = {} updated_bundle = self.full_dehydrate(updated_bundle) updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) return self.create_response(request, updated_bundle) @@ -1584,8 +1635,12 @@ if not self._meta.always_return_data: return http.HttpAccepted() else: - to_be_serialized = {} - to_be_serialized['objects'] = [self.full_dehydrate(bundle, for_list=True) for bundle in bundles_seen] + to_be_serialized = { + 'objects': [ + self.full_dehydrate(b, for_list=True) + for b in bundles_seen + ] + } to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) return self.create_response(request, to_be_serialized, response_class=http.HttpAccepted) @@ -1625,6 +1680,9 @@ if not self._meta.always_return_data: return http.HttpAccepted() else: + # Invalidate prefetched_objects_cache for bundled object + # because we might have changed a prefetched field + bundle.obj._prefetched_objects_cache = {} bundle = self.full_dehydrate(bundle) bundle = self.alter_detail_data_to_serialize(request, bundle) return self.create_response(request, bundle, response_class=http.HttpAccepted) @@ -1720,7 +1778,7 @@ continue if field_name in new_class.declared_fields: continue - if len(include_fields) and not field_name in include_fields: + if len(include_fields) and field_name not in include_fields: del(new_class.base_fields[field_name]) if len(excludes) and field_name in excludes: del(new_class.base_fields[field_name]) @@ -1729,9 +1787,9 @@ new_class.base_fields.update(new_class.get_fields(include_fields, excludes)) if getattr(new_class._meta, 'include_absolute_url', True): - if not 'absolute_url' in new_class.base_fields: + if 'absolute_url' not in new_class.base_fields: new_class.base_fields['absolute_url'] = fields.CharField(attribute='get_absolute_url', readonly=True) - elif 'absolute_url' in new_class.base_fields and not 'absolute_url' in attrs: + elif 'absolute_url' in new_class.base_fields and 'absolute_url' not in attrs: del(new_class.base_fields['absolute_url']) return new_class @@ -1768,7 +1826,9 @@ result = default internal_type = f.get_internal_type() - if internal_type in ('DateField', 'DateTimeField'): + if internal_type == 'DateField': + result = fields.DateField + elif internal_type == 'DateTimeField': result = fields.DateTimeField elif internal_type in ('BooleanField', 'NullBooleanField'): result = fields.BooleanField @@ -1826,6 +1886,7 @@ kwargs = { 'attribute': f.name, 'help_text': f.help_text, + 'verbose_name': f.verbose_name, } if f.null is True: @@ -1868,13 +1929,13 @@ if filter_bits is None: filter_bits = [] - if not field_name in self._meta.filtering: + if field_name not in self._meta.filtering: raise InvalidFilterError("The '%s' field does not allow filtering." % field_name) # Check to see if it's an allowed lookup type. - if not self._meta.filtering[field_name] in (ALL, ALL_WITH_RELATIONS): + if self._meta.filtering[field_name] not in (ALL, ALL_WITH_RELATIONS): # Must be an explicit whitelist. - if not filter_type in self._meta.filtering[field_name]: + if filter_type not in self._meta.filtering[field_name]: raise InvalidFilterError("'%s' is not an allowed filter on the '%s' field." % (filter_type, field_name)) if self.fields[field_name].attribute is None: @@ -1902,12 +1963,7 @@ Turn the string ``value`` into a python object. """ # Simple values - if value in ['true', 'True', True]: - value = True - elif value in ['false', 'False', False]: - value = False - elif value in ('nil', 'none', 'None', None): - value = None + value = string_to_python(value) # Split on ',' if not empty string and either an in or range filter. if filter_type in ('in', 'range') and len(value): @@ -1921,7 +1977,7 @@ return value - def build_filters(self, filters=None): + def build_filters(self, filters=None, ignore_bad_filters=False): """ Given a dictionary of filters, create the necessary ORM-level filters. @@ -1950,20 +2006,28 @@ query_terms = self._meta.queryset.query.query_terms else: query_terms = QUERY_TERMS + if django.VERSION >= (1, 8) and GeometryField: + query_terms = query_terms | set(GeometryField.class_lookups.keys()) for filter_expr, value in filters.items(): filter_bits = filter_expr.split(LOOKUP_SEP) field_name = filter_bits.pop(0) filter_type = 'exact' - if not field_name in self.fields: + if field_name not in self.fields: # It's not a field we know about. Move along citizen. continue if len(filter_bits) and filter_bits[-1] in query_terms: filter_type = filter_bits.pop() - lookup_bits = self.check_filtering(field_name, filter_type, filter_bits) + try: + lookup_bits = self.check_filtering(field_name, filter_type, filter_bits) + except InvalidFilterError: + if ignore_bad_filters: + continue + else: + raise value = self.filter_value_to_python(value, field_name, filters, filter_expr, filter_type) db_field_name = LOOKUP_SEP.join(lookup_bits) @@ -1987,8 +2051,8 @@ parameter_name = 'order_by' - if not 'order_by' in options: - if not 'sort_by' in options: + if 'order_by' not in options: + if 'sort_by' not in options: # Nothing to alter the order. Return what we've got. return obj_list else: @@ -2015,11 +2079,11 @@ field_name = order_by_bits[0][1:] order = '-' - if not field_name in self.fields: + if field_name not in self.fields: # It's not a field we know about. Move along citizen. raise InvalidSortError("No matching '%s' field for ordering on." % field_name) - if not field_name in self._meta.ordering: + if field_name not in self._meta.ordering: raise InvalidSortError("The '%s' field does not allow ordering." % field_name) if self.fields[field_name].attribute is None: @@ -2050,8 +2114,7 @@ """ A ORM-specific implementation of ``obj_get_list``. - Takes an optional ``request`` object, whose ``GET`` dictionary can be - used to narrow the query. + ``GET`` dictionary of bundle.request can be used to narrow the query. """ filters = {} @@ -2076,9 +2139,17 @@ Takes optional ``kwargs``, which are used to narrow the query to find the instance. """ + # Use ignore_bad_filters=True. `obj_get_list` filters based on + # request.GET, but `obj_get` usually filters based on `detail_uri_name` + # or data from a related field, so we don't want to raise errors if + # something doesn't explicitly match a configured filter. + applicable_filters = self.build_filters(filters=kwargs, ignore_bad_filters=True) + if self._meta.detail_uri_name in kwargs: + applicable_filters[self._meta.detail_uri_name] = kwargs[self._meta.detail_uri_name] + try: - object_list = self.get_object_list(bundle.request).filter(**kwargs) - stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in kwargs.items()]) + object_list = self.apply_filters(bundle.request, applicable_filters) + stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in applicable_filters.items()]) if len(object_list) <= 0: raise self._meta.object_class.DoesNotExist("Couldn't find an instance of '%s' which matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) @@ -2126,7 +2197,8 @@ field_object = self.fields[identifier] # Skip readonly or related fields. - if field_object.readonly is True or getattr(field_object, 'is_related', False): + if field_object.readonly or field_object.is_related or\ + not field_object.attribute: continue # Check for an optional method to do further hydration. @@ -2135,10 +2207,7 @@ if method: bundle = method(bundle) - if field_object.attribute: - value = field_object.hydrate(bundle) - - lookup_kwargs[identifier] = value + lookup_kwargs[identifier] = field_object.hydrate(bundle) return lookup_kwargs @@ -2146,7 +2215,10 @@ """ A ORM-specific implementation of ``obj_update``. """ - if not bundle.obj or not self.get_bundle_detail_data(bundle): + bundle_detail_data = self.get_bundle_detail_data(bundle) + arg_detail_data = kwargs.get(self._meta.detail_uri_name) + + if bundle_detail_data is None or (arg_detail_data is not None and str(bundle_detail_data) != str(arg_detail_data)): try: lookup_kwargs = self.lookup_kwargs_with_identifiers(bundle, kwargs) except: @@ -2207,7 +2279,7 @@ self.authorized_delete_detail(self.get_object_list(bundle.request), bundle) bundle.obj.delete() - @transaction.commit_on_success() + @atomic_decorator() def patch_list(self, request, **kwargs): """ An ORM-specific implementation of ``patch_list``. @@ -2229,9 +2301,12 @@ bundle.obj.delete() def create_identifier(self, obj): - return u"%s.%s.%s" % (obj._meta.app_label, obj._meta.module_name, obj.pk) + return u"%s.%s.%s" % (obj._meta.app_label, get_module_name(obj._meta), obj.pk) def save(self, bundle, skip_errors=False): + if bundle.via_uri: + return bundle + self.is_valid(bundle) if bundle.errors and not skip_errors: @@ -2247,8 +2322,11 @@ self.save_related(bundle) # Save the main object. - bundle.obj.save() - bundle.objects_saved.add(self.create_identifier(bundle.obj)) + obj_id = self.create_identifier(bundle.obj) + + if obj_id not in bundle.objects_saved or bundle.obj._state.adding: + bundle.obj.save() + bundle.objects_saved.add(obj_id) # Now pick up the M2M bits. m2m_bundle = self.hydrate_m2m(bundle) @@ -2268,10 +2346,10 @@ M2M data is handled by the ``ModelResource.save_m2m`` method. """ for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_related', False): + if not field_object.is_related: continue - if getattr(field_object, 'is_m2m', False): + if field_object.is_m2m: continue if not field_object.attribute: @@ -2280,34 +2358,42 @@ if field_object.readonly: continue - if field_object.blank and not field_name in bundle.data: + if field_object.blank and field_name not in bundle.data: continue # Get the object. try: related_obj = getattr(bundle.obj, field_object.attribute) except ObjectDoesNotExist: + # Django 1.8: unset related objects default to None, no error + related_obj = None + + # We didn't get it, so maybe we created it but haven't saved it + if related_obj is None: related_obj = bundle.related_objects_to_save.get(field_object.attribute, None) - # Because sometimes it's ``None`` & that's OK. - if related_obj: - if field_object.related_name: - if not self.get_bundle_detail_data(bundle): - bundle.obj.save() + if related_obj and field_object.related_name: + # this might be a reverse relation, so we need to save this + # model, attach it to the related object, and save the related + # object. + if not self.get_bundle_detail_data(bundle): + bundle.obj.save() - setattr(related_obj, field_object.related_name, bundle.obj) + setattr(related_obj, field_object.related_name, bundle.obj) - related_resource = field_object.get_related_resource(related_obj) + related_resource = field_object.get_related_resource(related_obj) - # Before we build the bundle & try saving it, let's make sure we - # haven't already saved it. + # Before we build the bundle & try saving it, let's make sure we + # haven't already saved it. + if related_obj: obj_id = self.create_identifier(related_obj) if obj_id in bundle.objects_saved: # It's already been saved. We're done here. continue - if bundle.data.get(field_name) and hasattr(bundle.data[field_name], 'keys'): + if bundle.data.get(field_name): + if hasattr(bundle.data[field_name], 'keys'): # Only build & save if there's data, not just a URI. related_bundle = related_resource.build_bundle( obj=related_obj, @@ -2315,8 +2401,21 @@ request=bundle.request, objects_saved=bundle.objects_saved ) + related_resource.full_hydrate(related_bundle) + related_resource.save(related_bundle) + related_obj = related_bundle.obj + elif field_object.related_name: + # This condition probably means a URI for a reverse + # relation was provided. + related_bundle = related_resource.build_bundle( + obj=related_obj, + request=bundle.request, + objects_saved=bundle.objects_saved + ) related_resource.save(related_bundle) + related_obj = related_bundle.obj + if related_obj: setattr(bundle.obj, field_object.attribute, related_obj) def save_m2m(self, bundle): @@ -2330,7 +2429,7 @@ relation and recreate the related data as needed. """ for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): + if not field_object.is_m2m: continue if not field_object.attribute: @@ -2362,45 +2461,20 @@ for related_bundle in bundle.data[field_name]: related_resource = field_object.get_related_resource(bundle.obj) - # Before we build the bundle & try saving it, let's make sure we - # haven't already saved it. - obj_id = self.create_identifier(related_bundle.obj) - - if obj_id in bundle.objects_saved: - # It's already been saved. We're done here. - continue - # Only build & save if there's data, not just a URI. updated_related_bundle = related_resource.build_bundle( obj=related_bundle.obj, data=related_bundle.data, request=bundle.request, - objects_saved=bundle.objects_saved + objects_saved=bundle.objects_saved, + via_uri=related_bundle.via_uri, ) - #Only save related models if they're newly added. - if updated_related_bundle.obj._state.adding: - related_resource.save(updated_related_bundle) + related_resource.save(updated_related_bundle) related_objs.append(updated_related_bundle.obj) related_mngr.add(*related_objs) - def detail_uri_kwargs(self, bundle_or_obj): - """ - Given a ``Bundle`` or an object (typically a ``Model`` instance), - it returns the extra kwargs needed to generate a detail URI. - - By default, it uses the model's ``pk`` in order to create the URI. - """ - kwargs = {} - - if isinstance(bundle_or_obj, Bundle): - kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj.obj, self._meta.detail_uri_name) - else: - kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj, self._meta.detail_uri_name) - - return kwargs - class ModelResource(six.with_metaclass(ModelDeclarativeMetaclass, BaseModelResource)): pass @@ -2423,8 +2497,8 @@ """ if request.method == verb: if hasattr(request, '_post'): - del(request._post) - del(request._files) + del request._post + del request._files try: request.method = "POST" diff -Nru django-tastypie-0.12.0/tastypie/serializers.py django-tastypie-0.13.3/tastypie/serializers.py --- django-tastypie-0.12.0/tastypie/serializers.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/serializers.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,7 +1,9 @@ from __future__ import unicode_literals + import datetime +import json import re -import django + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils import six @@ -10,19 +12,19 @@ from tastypie.bundle import Bundle from tastypie.exceptions import BadRequest, UnsupportedFormat -from tastypie.utils import format_datetime, format_date, format_time, make_naive +from tastypie.utils import format_datetime, format_date, format_time,\ + make_naive try: import defusedxml.lxml as lxml from defusedxml.common import DefusedXmlException from defusedxml.lxml import parse as parse_xml - from lxml.etree import Element, tostring, LxmlError, XMLParser + from lxml.etree import Element, tostring, LxmlError except ImportError: lxml = None try: import yaml - from django.core.serializers import pyyaml except ImportError: yaml = None @@ -31,8 +33,6 @@ except ImportError: biplist = None -import json - XML_ENCODING = re.compile('<\?xml.*?\?>', re.IGNORECASE) @@ -53,9 +53,13 @@ except UnicodeEncodeError: return value - TastypieConstructor.add_constructor(u'tag:yaml.org,2002:python/unicode', TastypieConstructor.construct_yaml_unicode_dammit) + TastypieConstructor.add_constructor( + u'tag:yaml.org,2002:python/unicode', + TastypieConstructor.construct_yaml_unicode_dammit + ) - class TastypieLoader(Reader, Scanner, Parser, Composer, TastypieConstructor, Resolver): + class TastypieLoader(Reader, Scanner, Parser, Composer, + TastypieConstructor, Resolver): def __init__(self, stream): Reader.__init__(self, stream) Scanner.__init__(self) @@ -65,6 +69,34 @@ Resolver.__init__(self) +_NUM = 0 +_DICT = 1 +_LIST = 2 +_STR = 3 +_BUNDLE = 4 +_DATETIME = 5 +_DATE = 6 +_TIME = 7 + +_SIMPLETYPES = { + float: _NUM, + bool: _NUM, + dict: _DICT, + list: _LIST, + tuple: _LIST, + Bundle: _BUNDLE, + datetime.datetime: _DATETIME, + datetime.date: _DATE, + datetime.time: _TIME, +} + +for integer_type in six.integer_types: + _SIMPLETYPES[integer_type] = _NUM + +for string_type in six.string_types: + _SIMPLETYPES[string_type] = _STR + + class Serializer(object): """ A swappable class for serialization. @@ -75,7 +107,6 @@ * jsonp (Disabled by default) * xml * yaml - * html * plist (see http://explorapp.com/biplist/) It was designed to make changing behavior easy, either by overridding the @@ -83,20 +114,22 @@ ``formats/content_types`` options or by altering the other hook methods. """ - formats = ['json', 'xml', 'yaml', 'html', 'plist'] + formats = ['json', 'xml', 'yaml', 'plist'] - content_types = {'json': 'application/json', - 'jsonp': 'text/javascript', - 'xml': 'application/xml', - 'yaml': 'text/yaml', - 'html': 'text/html', - 'plist': 'application/x-plist'} + content_types = { + 'json': 'application/json', + 'jsonp': 'text/javascript', + 'xml': 'application/xml', + 'yaml': 'text/yaml', + 'plist': 'application/x-plist' + } def __init__(self, formats=None, content_types=None, datetime_formatting=None): if datetime_formatting is not None: self.datetime_formatting = datetime_formatting else: - self.datetime_formatting = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601') + self.datetime_formatting = getattr(settings, + 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601') self.supported_formats = [] @@ -107,12 +140,13 @@ self.formats = formats if self.formats is Serializer.formats and hasattr(settings, 'TASTYPIE_DEFAULT_FORMATS'): - # We want TASTYPIE_DEFAULT_FORMATS to override unmodified defaults but not intentational changes - # on Serializer subclasses: + # We want TASTYPIE_DEFAULT_FORMATS to override unmodified defaults + # but not intentational changes on Serializer subclasses: self.formats = settings.TASTYPIE_DEFAULT_FORMATS if not isinstance(self.formats, (list, tuple)): - raise ImproperlyConfigured('Formats should be a list or tuple, not %r' % self.formats) + raise ImproperlyConfigured( + 'Formats should be a list or tuple, not %r' % self.formats) for format in self.formats: try: @@ -120,6 +154,24 @@ except KeyError: raise ImproperlyConfigured("Content type for specified type '%s' not found. Please provide it at either the class level or via the arguments." % format) + # Reverse the list, because mimeparse is weird like that. See also + # https://github.com/django-tastypie/django-tastypie/issues#issue/12 for + # more information. + self.supported_formats_reversed = list(self.supported_formats) + self.supported_formats_reversed.reverse() + + self._from_methods = {} + self._to_methods = {} + + for short_format, long_format in self.content_types.items(): + method = getattr(self, "from_%s" % short_format, None) + + self._from_methods[long_format] = method + + method = getattr(self, "to_%s" % short_format, None) + + self._to_methods[long_format] = method + def get_mime_for_format(self, format): """ Given a format, attempts to determine the correct MIME type. @@ -146,7 +198,7 @@ return format_datetime(data) if self.datetime_formatting == 'iso-8601-strict': # Remove microseconds to strictly adhere to iso-8601 - data = data - datetime.timedelta(microseconds = data.microsecond) + data = data - datetime.timedelta(microseconds=data.microsecond) return data.isoformat() @@ -177,7 +229,10 @@ return format_time(data) if self.datetime_formatting == 'iso-8601-strict': # Remove microseconds to strictly adhere to iso-8601 - data = (datetime.datetime.combine(datetime.date(1,1,1),data) - datetime.timedelta(microseconds = data.microsecond)).time() + data = ( + datetime.datetime.combine(datetime.date(1, 1, 1), data) - + datetime.timedelta(microseconds=data.microsecond) + ).time() return data.isoformat() @@ -186,45 +241,35 @@ Given some data and a format, calls the correct method to serialize the data and returns the result. """ - desired_format = None + method = None if options is None: options = {} - for short_format, long_format in self.content_types.items(): - if format == long_format: - if hasattr(self, "to_%s" % short_format): - desired_format = short_format - break + method = self._to_methods.get(format) - if desired_format is None: + if method is None: raise UnsupportedFormat("The format indicated '%s' had no available serialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format) - serialized = getattr(self, "to_%s" % desired_format)(bundle, options) - return serialized + return method(bundle, options) def deserialize(self, content, format='application/json'): """ Given some data and a format, calls the correct method to deserialize the data and returns the result. """ - desired_format = None + method = None format = format.split(';')[0] - for short_format, long_format in self.content_types.items(): - if format == long_format: - if hasattr(self, "from_%s" % short_format): - desired_format = short_format - break + method = self._from_methods.get(format) - if desired_format is None: + if method is None: raise UnsupportedFormat("The format indicated '%s' had no available deserialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format) if isinstance(content, six.binary_type): content = force_text(content) - deserialized = getattr(self, "from_%s" % desired_format)(content) - return deserialized + return method(content) def to_simple(self, data, options): """ @@ -234,39 +279,39 @@ This brings complex Python data structures down to native types of the serialization format(s). """ - if isinstance(data, (list, tuple)): - return [self.to_simple(item, options) for item in data] - if isinstance(data, dict): - return dict((key, self.to_simple(val, options)) for (key, val) in data.items()) - elif isinstance(data, Bundle): - return dict((key, self.to_simple(val, options)) for (key, val) in data.data.items()) - elif hasattr(data, 'dehydrated_type'): - if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False: - if data.full: - return self.to_simple(data.fk_resource, options) - else: - return self.to_simple(data.value, options) - elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True: - if data.full: - return [self.to_simple(bundle, options) for bundle in data.m2m_bundles] - else: - return [self.to_simple(val, options) for val in data.value] - else: - return self.to_simple(data.value, options) - elif isinstance(data, datetime.datetime): + if data is None: + return None + + data_type = type(data) + + stype = _STR + + for dt in data_type.__mro__: + try: + stype = _SIMPLETYPES[dt] + break + except KeyError: + pass + + if stype == _NUM: + return data + if stype == _DICT: + to_simple = self.to_simple + return {key: to_simple(val, options) for key, val in six.iteritems(data)} + if stype == _STR: + return force_text(data) + if stype == _LIST: + to_simple = self.to_simple + return [to_simple(item, options) for item in data] + if stype == _BUNDLE: + to_simple = self.to_simple + return {key: to_simple(val, options) for key, val in six.iteritems(data.data)} + if stype == _DATETIME: return self.format_datetime(data) - elif isinstance(data, datetime.date): + if stype == _DATE: return self.format_date(data) - elif isinstance(data, datetime.time): + if stype == _TIME: return self.format_time(data) - elif isinstance(data, bool): - return data - elif isinstance(data, (six.integer_types, float)): - return data - elif data is None: - return None - else: - return force_text(data) def to_etree(self, data, options=None, name=None, depth=0): """ @@ -281,7 +326,7 @@ else: element = Element('objects') for item in data: - element.append(self.to_etree(item, options, depth=depth+1)) + element.append(self.to_etree(item, options, depth=depth + 1)) element[:] = sorted(element, key=lambda x: x.tag) elif isinstance(data, dict): if depth == 0: @@ -290,30 +335,15 @@ element = Element(name or 'object') element.set('type', 'hash') for (key, value) in data.items(): - element.append(self.to_etree(value, options, name=key, depth=depth+1)) + element.append(self.to_etree( + value, options, name=key, depth=depth + 1)) element[:] = sorted(element, key=lambda x: x.tag) elif isinstance(data, Bundle): element = Element(name or 'object') for field_name, field_object in data.data.items(): - element.append(self.to_etree(field_object, options, name=field_name, depth=depth+1)) + element.append(self.to_etree( + field_object, options, name=field_name, depth=depth + 1)) element[:] = sorted(element, key=lambda x: x.tag) - elif hasattr(data, 'dehydrated_type'): - if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False: - if data.full: - return self.to_etree(data.fk_resource, options, name, depth+1) - else: - return self.to_etree(data.value, options, name, depth+1) - elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True: - if data.full: - element = Element(name or 'objects') - for bundle in data.m2m_bundles: - element.append(self.to_etree(bundle, options, bundle.resource_name, depth+1)) - else: - element = Element(name or 'objects') - for value in data.value: - element.append(self.to_etree(value, options, name, depth=depth+1)) - else: - return self.to_etree(data.value, options, name) else: element = Element(name or 'value') simple_data = self.to_simple(data, options) @@ -343,9 +373,15 @@ for element in elements: if element.tag in ('object', 'objects'): return self.from_etree(element) - return dict((element.tag, self.from_etree(element)) for element in elements) + return { + element.tag: self.from_etree(element) + for element in elements + } elif data.tag == 'object' or data.get('type') == 'hash': - return dict((element.tag, self.from_etree(element)) for element in data.getchildren()) + return { + element.tag: self.from_etree(element) + for element in data.getchildren() + } elif data.tag == 'objects' or data.get('type') == 'list': return [self.from_etree(element) for element in data.getchildren()] else: @@ -371,7 +407,8 @@ options = options or {} data = self.to_simple(data, options) - return djangojson.json.dumps(data, cls=djangojson.DjangoJSONEncoder, sort_keys=True, ensure_ascii=False) + return djangojson.json.dumps(data, cls=djangojson.DjangoJSONEncoder, + sort_keys=True, ensure_ascii=False) def from_json(self, content): """ @@ -380,7 +417,7 @@ try: return json.loads(content) except ValueError: - raise BadRequest + raise BadRequest('Request is not valid JSON.') def to_jsonp(self, data, options=None): """ @@ -393,9 +430,9 @@ details. """ options = options or {} - json = self.to_json(data, options) - json = json.replace(u'\u2028', u'\\u2028').replace(u'\u2029', u'\\u2029') - return u'%s(%s)' % (options['callback'], json) + jsonstr = self.to_json(data, options).replace( + u'\u2028', u'\\u2028').replace(u'\u2029', u'\\u2029') + return u'%s(%s)' % (options['callback'], jsonstr) def to_xml(self, data, options=None): """ @@ -404,9 +441,11 @@ options = options or {} if lxml is None: - raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.") + raise ImproperlyConfigured( + "Usage of the XML aspects requires lxml and defusedxml.") - return tostring(self.to_etree(data, options), xml_declaration=True, encoding='utf-8') + return tostring(self.to_etree(data, options), xml_declaration=True, + encoding='utf-8') def from_xml(self, content, forbid_dtd=True, forbid_entities=True): """ @@ -417,7 +456,8 @@ necessary. """ if lxml is None: - raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.") + raise ImproperlyConfigured( + "Usage of the XML aspects requires lxml and defusedxml.") try: # Stripping the encoding declaration. Because lxml. @@ -440,7 +480,8 @@ options = options or {} if yaml is None: - raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.") + raise ImproperlyConfigured( + "Usage of the YAML aspects requires yaml.") return yaml.dump(self.to_simple(data, options)) @@ -449,7 +490,8 @@ Given some YAML data, returns a Python dictionary of the decoded data. """ if yaml is None: - raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.") + raise ImproperlyConfigured( + "Usage of the YAML aspects requires yaml.") return yaml.load(content, Loader=TastypieLoader) @@ -460,42 +502,25 @@ options = options or {} if biplist is None: - raise ImproperlyConfigured("Usage of the plist aspects requires biplist.") + raise ImproperlyConfigured( + "Usage of the plist aspects requires biplist.") return biplist.writePlistToString(self.to_simple(data, options)) def from_plist(self, content): """ - Given some binary plist data, returns a Python dictionary of the decoded data. + Given some binary plist data, returns a Python dictionary of the + decoded data. """ if biplist is None: - raise ImproperlyConfigured("Usage of the plist aspects requires biplist.") + raise ImproperlyConfigured( + "Usage of the plist aspects requires biplist.") if isinstance(content, six.text_type): content = smart_bytes(content) return biplist.readPlistFromString(content) - def to_html(self, data, options=None): - """ - Reserved for future usage. - - The desire is to provide HTML output of a resource, making an API - available to a browser. This is on the TODO list but not currently - implemented. - """ - options = options or {} - return 'Sorry, not implemented yet. Please append "?format=json" to your URL.' - - def from_html(self, content): - """ - Reserved for future usage. - - The desire is to handle form-based (maybe Javascript?) input, making an - API available to a browser. This is on the TODO list but not currently - implemented. - """ - pass def get_type_string(data): """ diff -Nru django-tastypie-0.12.0/tastypie/south_migrations/0001_initial.py django-tastypie-0.13.3/tastypie/south_migrations/0001_initial.py --- django-tastypie-0.12.0/tastypie/south_migrations/0001_initial.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/south_migrations/0001_initial.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -# encoding: utf-8 -from __future__ import unicode_literals -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models -from tastypie.compat import AUTH_USER_MODEL - - -class Migration(SchemaMigration): - - def forwards(self, orm): - - # Adding model 'ApiAccess' - db.create_table('tastypie_apiaccess', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('identifier', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('url', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)), - ('request_method', self.gf('django.db.models.fields.CharField')(default='', max_length=10, blank=True)), - ('accessed', self.gf('django.db.models.fields.PositiveIntegerField')()), - )) - db.send_create_signal('tastypie', ['ApiAccess']) - - # Adding model 'ApiKey' - db.create_table('tastypie_apikey', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='api_key', unique=True, to=orm[AUTH_USER_MODEL])), - ('key', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)), - ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), - )) - db.send_create_signal('tastypie', ['ApiKey']) - - - def backwards(self, orm): - - # Deleting model 'ApiAccess' - db.delete_table('tastypie_apiaccess') - - # Deleting model 'ApiKey' - db.delete_table('tastypie_apikey') - - - 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': {'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'}) - }, - AUTH_USER_MODEL: { - 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'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'}) - }, - 'tastypie.apiaccess': { - 'Meta': {'object_name': 'ApiAccess'}, - 'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}), - 'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}) - }, - 'tastypie.apikey': { - 'Meta': {'object_name': 'ApiKey'}, - 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL}) - } - } - - complete_apps = ['tastypie'] diff -Nru django-tastypie-0.12.0/tastypie/south_migrations/0002_add_apikey_index.py django-tastypie-0.13.3/tastypie/south_migrations/0002_add_apikey_index.py --- django-tastypie-0.12.0/tastypie/south_migrations/0002_add_apikey_index.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/south_migrations/0002_add_apikey_index.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models -from tastypie.compat import AUTH_USER_MODEL - - -class Migration(SchemaMigration): - - def forwards(self, orm): - if not db.backend_name in ('mysql', 'sqlite'): - # Adding index on 'ApiKey', fields ['key'] - db.create_index('tastypie_apikey', ['key']) - - def backwards(self, orm): - if not db.backend_name in ('mysql', 'sqlite'): - # Removing index on 'ApiKey', fields ['key'] - db.delete_index('tastypie_apikey', ['key']) - - 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'}) - }, - AUTH_USER_MODEL: { - 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - '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'}) - }, - 'tastypie.apiaccess': { - 'Meta': {'object_name': 'ApiAccess'}, - 'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}), - 'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}) - }, - 'tastypie.apikey': { - 'Meta': {'object_name': 'ApiKey'}, - 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 5, 0, 0)'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'db_index': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL}) - } - } - - complete_apps = ['tastypie'] \ No newline at end of file diff -Nru django-tastypie-0.12.0/tastypie/test.py django-tastypie-0.13.3/tastypie/test.py --- django-tastypie-0.12.0/tastypie/test.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/test.py 2016-02-14 09:15:23.000000000 +0000 @@ -1,18 +1,14 @@ from __future__ import unicode_literals import time +import warnings from django.conf import settings from django.test import TestCase -from django.test.client import FakePayload, Client +from django.test.client import Client from django.utils.encoding import force_text from tastypie.serializers import Serializer -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse - class TestApiClient(object): def __init__(self, serializer=None): @@ -30,29 +26,37 @@ def get_content_type(self, short_format): """ - Given a short name (such as ``json`` or ``xml``), returns the full content-type - for it (``application/json`` or ``application/xml`` in this case). + Given a short name (such as ``json`` or ``xml``), returns the full + content-type for it (``application/json`` or ``application/xml`` in + this case). """ return self.serializer.content_types.get(short_format, 'json') - def get(self, uri, format='json', data=None, authentication=None, **kwargs): + def get(self, uri, format='json', data=None, authentication=None, + **kwargs): """ Performs a simulated ``GET`` request to the provided URI. - Optionally accepts a ``data`` kwarg, which in the case of ``GET``, lets you - send along ``GET`` parameters. This is useful when testing filtering or other - things that read off the ``GET`` params. Example:: + Optionally accepts a ``data`` kwarg, which in the case of ``GET``, lets + you send along ``GET`` parameters. This is useful when testing + filtering or other things that read off the ``GET`` params. Example:: from tastypie.test import TestApiClient client = TestApiClient() - response = client.get('/api/v1/entry/1/', data={'format': 'json', 'title__startswith': 'a', 'limit': 20, 'offset': 60}) + response = client.get('/api/v1/entry/1/', data={ + 'format': 'json', + 'title__startswith': 'a', + 'limit': 20, + 'offset': 60 + }) - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. + Optionally accepts an ``authentication`` kwarg, which should be an HTTP + header with the correct authentication data already setup. All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client + ``TestClient``. See +https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client for details. """ content_type = self.get_content_type(format) @@ -67,12 +71,14 @@ return self.client.get(uri, **kwargs) - def post(self, uri, format='json', data=None, authentication=None, **kwargs): + def post(self, uri, format='json', data=None, authentication=None, + **kwargs): """ Performs a simulated ``POST`` request to the provided URI. - Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``POST`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. + Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``POST`` + the ``data`` gets serialized & sent as the body instead of becoming + part of the URI. Example:: from tastypie.test import TestApiClient @@ -85,30 +91,34 @@ 'user': '/api/v1/user/1/', }) - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. + Optionally accepts an ``authentication`` kwarg, which should be an HTTP + header with the correct authentication data already setup. All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client + ``TestClient``. See +https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client for details. """ content_type = self.get_content_type(format) kwargs['content_type'] = content_type if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) + kwargs['data'] = self.serializer.serialize( + data, format=content_type) if authentication is not None: kwargs['HTTP_AUTHORIZATION'] = authentication return self.client.post(uri, **kwargs) - def put(self, uri, format='json', data=None, authentication=None, **kwargs): + def put(self, uri, format='json', data=None, authentication=None, + **kwargs): """ Performs a simulated ``PUT`` request to the provided URI. Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PUT`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. + ``data`` gets serialized & sent as the body instead of becoming part of + the URI. Example:: from tastypie.test import TestApiClient @@ -121,30 +131,34 @@ 'user': '/api/v1/user/1/', }) - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. + Optionally accepts an ``authentication`` kwarg, which should be an HTTP + header with the correct authentication data already setup. All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client + ``TestClient``. See +https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client for details. """ content_type = self.get_content_type(format) kwargs['content_type'] = content_type if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) + kwargs['data'] = self.serializer.serialize( + data, format=content_type) if authentication is not None: kwargs['HTTP_AUTHORIZATION'] = authentication return self.client.put(uri, **kwargs) - def patch(self, uri, format='json', data=None, authentication=None, **kwargs): + def patch(self, uri, format='json', data=None, authentication=None, + **kwargs): """ Performs a simulated ``PATCH`` request to the provided URI. - Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PATCH`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. + Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PATCH`` + the ``data`` gets serialized & sent as the body instead of becoming + part of the URI. Example:: from tastypie.test import TestApiClient @@ -157,53 +171,48 @@ 'user': '/api/v1/user/1/', }) - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. + Optionally accepts an ``authentication`` kwarg, which should be an HTTP + header with the correct authentication data already setup. All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client + ``TestClient``. See +https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client for details. """ content_type = self.get_content_type(format) kwargs['content_type'] = content_type if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) + kwargs['data'] = self.serializer.serialize( + data, format=content_type) if authentication is not None: kwargs['HTTP_AUTHORIZATION'] = authentication - # This hurts because Django doesn't support PATCH natively. - parsed = urlparse(uri) - r = { - 'CONTENT_LENGTH': len(kwargs['data']), - 'CONTENT_TYPE': content_type, - 'PATH_INFO': self.client._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'PATCH', - 'wsgi.input': FakePayload(kwargs['data']), - } - r.update(kwargs) - return self.client.request(**r) + return self.client.patch(uri, **kwargs) - def delete(self, uri, format='json', data=None, authentication=None, **kwargs): + def delete(self, uri, format='json', data=None, authentication=None, + **kwargs): """ Performs a simulated ``DELETE`` request to the provided URI. - Optionally accepts a ``data`` kwarg, which in the case of ``DELETE``, lets you - send along ``DELETE`` parameters. This is useful when testing filtering or other - things that read off the ``DELETE`` params. Example:: + Optionally accepts a ``data`` kwarg, which in the case of ``DELETE``, + lets you send along ``DELETE`` parameters. This is useful when testing + filtering or other things that read off the ``DELETE`` params. + Example:: from tastypie.test import TestApiClient client = TestApiClient() - response = client.delete('/api/v1/entry/1/', data={'format': 'json'}) + response = client.delete('/api/v1/entry/1/', + data={'format': 'json'}) - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. + Optionally accepts an ``authentication`` kwarg, which should be an HTTP + header with the correct authentication data already setup. All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client + ``TestClient``. See +https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client for details. """ content_type = self.get_content_type(format) @@ -219,12 +228,13 @@ return self.client.delete(uri, **kwargs) -class ResourceTestCase(TestCase): +class ResourceTestCaseMixin(object): """ - A useful base class for the start of testing Tastypie APIs. + A mixin of useful methods for testing Tastypie APIs. + Below we use this to subclass Django's TestCase and TransactionTestCase classes. """ def setUp(self): - super(ResourceTestCase, self).setUp() + super(ResourceTestCaseMixin, self).setUp() self.serializer = Serializer() self.api_client = TestApiClient() @@ -244,7 +254,8 @@ # Then the usual tests... """ - raise NotImplementedError("You must return the class for your Resource to test.") + raise NotImplementedError( + "You must return the class for your Resource to test.") def create_basic(self, username, password): """ @@ -252,7 +263,8 @@ Auth. """ import base64 - return 'Basic %s' % base64.b64encode(':'.join([username, password]).encode('utf-8')).decode('utf-8') + return 'Basic %s' % base64.b64encode( + ':'.join([username, password]).encode('utf-8')).decode('utf-8') def create_apikey(self, username, api_key): """ @@ -269,13 +281,21 @@ from tastypie.authentication import hmac, sha1, uuid, python_digest new_uuid = uuid.uuid4() - opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest().decode('utf-8') + opaque = hmac.new( + str(new_uuid).encode('utf-8'), digestmod=sha1 + ).hexdigest().decode('utf-8') return python_digest.build_authorization_request( username, method.upper(), uri, - 1, # nonce_count - digest_challenge=python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), 'django-tastypie', opaque, False), + 1, # nonce_count + digest_challenge=python_digest.build_digest_challenge( + time.time(), + settings.SECRET_KEY, + 'django-tastypie', + opaque, + False + ), password=api_key ) @@ -293,12 +313,16 @@ 'name': 'Test', 'description': 'Testing...' }) - token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={ - 'consumer': consumer, - 'resource': resource, - 'secret': '', - 'user': user, - }) + token, _ = Token.objects.get_or_create( + key='foo', + token_type=Token.ACCESS, + defaults={ + 'consumer': consumer, + 'resource': resource, + 'secret': '', + 'user': user, + } + ) # Then generate the header. oauth_data = { @@ -309,7 +333,9 @@ 'oauth_timestamp': str(int(time.time())), 'oauth_token': 'foo', } - return 'OAuth %s' % ','.join([key+'='+value for key, value in oauth_data.items()]) + return 'OAuth %s' % ','.join([ + key + '=' + value for key, value in oauth_data.items() + ]) def assertHttpOK(self, resp): """ @@ -327,7 +353,7 @@ """ Ensures the response is returning either a HTTP 202 or a HTTP 204. """ - return self.assertIn(resp.status_code, [202, 204]) + self.assertIn(resp.status_code, [202, 204]) def assertHttpMultipleChoices(self, resp): """ @@ -415,10 +441,11 @@ def assertValidJSON(self, data): """ - Given the provided ``data`` as a string, ensures that it is valid JSON & - can be loaded properly. + Given the provided ``data`` as a string, ensures that it is valid JSON + & can be loaded properly. """ - # Just try the load. If it throws an exception, the test case will fail. + # Just try the load. If it throws an exception, the test case will + # fail. self.serializer.from_json(data) def assertValidXML(self, data): @@ -426,15 +453,17 @@ Given the provided ``data`` as a string, ensures that it is valid XML & can be loaded properly. """ - # Just try the load. If it throws an exception, the test case will fail. + # Just try the load. If it throws an exception, the test case will + # fail. self.serializer.from_xml(data) def assertValidYAML(self, data): """ - Given the provided ``data`` as a string, ensures that it is valid YAML & - can be loaded properly. + Given the provided ``data`` as a string, ensures that it is valid YAML + & can be loaded properly. """ - # Just try the load. If it throws an exception, the test case will fail. + # Just try the load. If it throws an exception, the test case will + # fail. self.serializer.from_yaml(data) def assertValidPlist(self, data): @@ -442,13 +471,14 @@ Given the provided ``data`` as a string, ensures that it is valid binary plist & can be loaded properly. """ - # Just try the load. If it throws an exception, the test case will fail. + # Just try the load. If it throws an exception, the test case will + # fail. self.serializer.from_plist(data) def assertValidJSONResponse(self, resp): """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: + Given a ``HttpResponse`` coming back from using the ``client``, assert + that you get back: * An HTTP 200 * The correct content-type (``application/json``) @@ -460,8 +490,8 @@ def assertValidXMLResponse(self, resp): """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: + Given a ``HttpResponse`` coming back from using the ``client``, assert + that you get back: * An HTTP 200 * The correct content-type (``application/xml``) @@ -473,8 +503,8 @@ def assertValidYAMLResponse(self, resp): """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: + Given a ``HttpResponse`` coming back from using the ``client``, assert + that you get back: * An HTTP 200 * The correct content-type (``text/yaml``) @@ -486,8 +516,8 @@ def assertValidPlistResponse(self, resp): """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: + Given a ``HttpResponse`` coming back from using the ``client``, assert + that you get back: * An HTTP 200 * The correct content-type (``application/x-plist``) @@ -499,28 +529,44 @@ def deserialize(self, resp): """ - Given a ``HttpResponse`` coming back from using the ``client``, this method - checks the ``Content-Type`` header & attempts to deserialize the data based on - that. + Given a ``HttpResponse`` coming back from using the ``client``, this + method checks the ``Content-Type`` header & attempts to deserialize the + data based on that. - It returns a Python datastructure (typically a ``dict``) of the serialized data. + It returns a Python datastructure (typically a ``dict``) of the + serialized data. """ - return self.serializer.deserialize(resp.content, format=resp['Content-Type']) + return self.serializer.deserialize( + resp.content, format=resp['Content-Type']) def serialize(self, data, format='application/json'): """ - Given a Python datastructure (typically a ``dict``) & a desired content-type, - this method will return a serialized string of that data. + Given a Python datastructure (typically a ``dict``) & a desired + content-type, this method will return a serialized string of that data. """ return self.serializer.serialize(data, format=format) def assertKeys(self, data, expected): """ - This method ensures that the keys of the ``data`` match up to the keys of - ``expected``. + This method ensures that the keys of the ``data`` match up to the keys + of ``expected``. - It covers the (extremely) common case where you want to make sure the keys of - a response match up to what is expected. This is typically less fragile than - testing the full structure, which can be prone to data changes. + It covers the (extremely) common case where you want to make sure the + keys of a response match up to what is expected. This is typically less + fragile than testing the full structure, which can be prone to data + changes. """ self.assertEqual(sorted(data.keys()), sorted(expected)) + + +class ResourceTestCase(ResourceTestCaseMixin, TestCase): + """ + This class exists for backwards compatibility, from before ResourceTestCaseMixin was added. + It throws a deprecation warning. + """ + def __init__(self, *args, **kwargs): + warnings.warn( + "'ResourceTestCase' is deprecated & will be removed by v1.0.0. " + "Please use ``ResourceTestCaseMixin`` instead. " + "For example: ``class MyTest(ResourceTestCaseMixin, django.test.TestCase):``.") + super(ResourceTestCase, self).__init__(*args, **kwargs) diff -Nru django-tastypie-0.12.0/tastypie/throttle.py django-tastypie-0.13.3/tastypie/throttle.py --- django-tastypie-0.12.0/tastypie/throttle.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/throttle.py 2016-02-14 09:10:26.000000000 +0000 @@ -1,8 +1,12 @@ from __future__ import unicode_literals import time + from django.core.cache import cache +_other_allowed_chars = frozenset(['_', '.', '-']) + + class BaseThrottle(object): """ A simplified, swappable base class for throttling. @@ -36,18 +40,21 @@ Takes an identifier (like a username or IP address) and converts it into a key usable by the cache system. """ - bits = [] + bits = [ + char + for char in identifier + if char.isalnum() or char in _other_allowed_chars + ] + bits.append('_accesses') - for char in identifier: - if char.isalnum() or char in ['_', '.', '-']: - bits.append(char) - - safe_string = ''.join(bits) - return "%s_accesses" % safe_string + return ''.join(bits) def should_be_throttled(self, identifier, **kwargs): """ - Returns whether or not the user has exceeded their throttle limit. + Returns whether or not the user has exceeded their throttle limit. If + throttled, can return either True, and int specifying the number of + seconds to wait, or a datetime object specifying when to retry the + request. Always returns ``False``, as this implementation does not actually throttle the user. @@ -69,7 +76,10 @@ """ def should_be_throttled(self, identifier, **kwargs): """ - Returns whether or not the user has exceeded their throttle limit. + Returns whether or not the user has exceeded their throttle limit. If + throttled, can return either True, and int specifying the number of + seconds to wait, or a datetime object specifying when to retry the + request. Maintains a list of timestamps when the user accessed the api within the cache. @@ -80,13 +90,17 @@ key = self.convert_identifier_to_key(identifier) # Weed out anything older than the timeframe. - minimum_time = int(time.time()) - int(self.timeframe) + now = int(time.time()) + timeframe = int(self.timeframe) + throttle_at = int(self.throttle_at) + + minimum_time = now - timeframe times_accessed = [access for access in cache.get(key, []) if access >= minimum_time] cache.set(key, times_accessed, self.expiration) - if len(times_accessed) >= int(self.throttle_at): + if len(times_accessed) >= throttle_at: # Throttle them. - return True + return timeframe - (now - times_accessed[-throttle_at]) # Let them through. return False diff -Nru django-tastypie-0.12.0/tastypie/utils/dict.py django-tastypie-0.13.3/tastypie/utils/dict.py --- django-tastypie-0.12.0/tastypie/utils/dict.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/dict.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,5 +1,5 @@ -from django.utils.encoding import smart_bytes from django.utils import six +from django.utils.encoding import smart_bytes def dict_strip_unicode_keys(uni_dict): @@ -11,9 +11,4 @@ if six.PY3: return uni_dict - data = {} - - for key, value in uni_dict.items(): - data[smart_bytes(key)] = value - - return data + return {smart_bytes(key): value for key, value in uni_dict.items()} diff -Nru django-tastypie-0.12.0/tastypie/utils/formatting.py django-tastypie-0.13.3/tastypie/utils/formatting.py --- django-tastypie-0.12.0/tastypie/utils/formatting.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/formatting.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,17 +1,11 @@ from __future__ import unicode_literals -import email -import datetime -import time + from django.utils import dateformat -from tastypie.utils.timezone import make_aware, make_naive, aware_datetime -# Try to use dateutil for maximum date-parsing niceness. Fall back to -# hard-coded RFC2822 parsing if that's not possible. -try: - from dateutil.parser import parse as mk_datetime -except ImportError: - def mk_datetime(string): - return make_aware(datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string)))) +from tastypie.utils.timezone import make_naive, aware_datetime + +from dateutil.parser import parse as mk_datetime # flake8: noqa + def format_datetime(dt): """ @@ -19,6 +13,7 @@ """ return dateformat.format(make_naive(dt), 'r') + def format_date(d): """ RFC 2822 date formatter @@ -28,6 +23,7 @@ dt = aware_datetime(d.year, d.month, d.day, 0, 0, 0) return dateformat.format(dt, 'j M Y') + def format_time(t): """ RFC 2822 time formatter diff -Nru django-tastypie-0.12.0/tastypie/utils/__init__.py django-tastypie-0.13.3/tastypie/utils/__init__.py --- django-tastypie-0.12.0/tastypie/utils/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/__init__.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,5 +1,15 @@ -from tastypie.utils.dict import dict_strip_unicode_keys -from tastypie.utils.formatting import mk_datetime, format_datetime, format_date, format_time -from tastypie.utils.urls import trailing_slash -from tastypie.utils.validate_jsonp import is_valid_jsonp_callback_value -from tastypie.utils.timezone import now, make_aware, make_naive, aware_date, aware_datetime +from tastypie.utils.dict import dict_strip_unicode_keys # flake8: noqa +from tastypie.utils.formatting import mk_datetime, format_datetime, format_date, format_time # flake8: noqa +from tastypie.utils.urls import trailing_slash # flake8: noqa +from tastypie.utils.validate_jsonp import is_valid_jsonp_callback_value # flake8: noqa +from tastypie.utils.timezone import now, make_aware, make_naive, aware_date, aware_datetime # flake8: noqa + + +def string_to_python(value): + if value in ('true', 'True', True): + value = True + elif value in ('false', 'False', False): + value = False + elif value in ('nil', 'none', 'None', None): + value = None + return value diff -Nru django-tastypie-0.12.0/tastypie/utils/mime.py django-tastypie-0.13.3/tastypie/utils/mime.py --- django-tastypie-0.12.0/tastypie/utils/mime.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/mime.py 2016-02-14 09:10:26.000000000 +0000 @@ -22,24 +22,20 @@ malformed HTTP request headers! """ # First, check if they forced the format. - if request.GET.get('format'): - if request.GET['format'] in serializer.formats: - return serializer.get_mime_for_format(request.GET['format']) + format = request.GET.get('format') + if format: + if format in serializer.formats: + return serializer.get_mime_for_format(format) # If callback parameter is present, use JSONP. if 'callback' in request.GET: return serializer.get_mime_for_format('jsonp') # Try to fallback on the Accepts header. - if request.META.get('HTTP_ACCEPT', '*/*') != '*/*': - formats = list(serializer.supported_formats) or [] - # Reverse the list, because mimeparse is weird like that. See also - # https://github.com/toastdriven/django-tastypie/issues#issue/12 for - # more information. - formats.reverse() - + accept = request.META.get('HTTP_ACCEPT', '*/*') + if accept != '*/*': try: - best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT']) + best_format = mimeparse.best_match(serializer.supported_formats_reversed, accept) except ValueError: raise BadRequest('Invalid Accept header') diff -Nru django-tastypie-0.12.0/tastypie/utils/timezone.py django-tastypie-0.13.3/tastypie/utils/timezone.py --- django-tastypie-0.12.0/tastypie/utils/timezone.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/timezone.py 2016-02-14 09:14:12.000000000 +0000 @@ -1,32 +1,31 @@ from __future__ import unicode_literals + import datetime from django.conf import settings +from django.utils import timezone + + +def make_aware(value): + if settings.USE_TZ and timezone.is_naive(value): + default_tz = timezone.get_default_timezone() + value = timezone.make_aware(value, default_tz) + return value + + +def make_naive(value): + if settings.USE_TZ and timezone.is_aware(value): + default_tz = timezone.get_default_timezone() + value = timezone.make_naive(value, default_tz) + return value + + +def now(): + d = timezone.now() -try: - from django.utils import timezone + if d.tzinfo: + return timezone.localtime(d) - def make_aware(value): - if getattr(settings, "USE_TZ", False) and timezone.is_naive(value): - default_tz = timezone.get_default_timezone() - value = timezone.make_aware(value, default_tz) - return value - - def make_naive(value): - if getattr(settings, "USE_TZ", False) and timezone.is_aware(value): - default_tz = timezone.get_default_timezone() - value = timezone.make_naive(value, default_tz) - return value - - def now(): - d = timezone.now() - - if d.tzinfo: - return timezone.localtime(timezone.now()) - - return d -except ImportError: - now = datetime.datetime.now - make_aware = make_naive = lambda x: x + return d def aware_date(*args, **kwargs): diff -Nru django-tastypie-0.12.0/tastypie/utils/urls.py django-tastypie-0.13.3/tastypie/utils/urls.py --- django-tastypie-0.12.0/tastypie/utils/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/urls.py 2016-02-14 09:10:26.000000000 +0000 @@ -1,9 +1,15 @@ from __future__ import unicode_literals + from django.conf import settings +from django.utils import six + + +_trailing_slash = '/?' if getattr(settings, 'TASTYPIE_ALLOW_MISSING_SLASH', False) else '/' -def trailing_slash(): - if getattr(settings, 'TASTYPIE_ALLOW_MISSING_SLASH', False): - return '/?' +# for backwards compatibility where 3rd parties still call this like a function. +class CallableUnicode(six.text_type): + def __call__(self): + return self - return '/' +trailing_slash = CallableUnicode(_trailing_slash) diff -Nru django-tastypie-0.12.0/tastypie/utils/validate_jsonp.py django-tastypie-0.13.3/tastypie/utils/validate_jsonp.py --- django-tastypie-0.12.0/tastypie/utils/validate_jsonp.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/utils/validate_jsonp.py 2016-02-14 09:10:26.000000000 +0000 @@ -3,7 +3,7 @@ # Placed into the Public Domain by tav # Modified for Python 3 compatibility. -"""Validate Javascript Identifiers for use as JSON-P callback parameters.""" +"Validate Javascript Identifiers for use as JSON-P callback parameters." from __future__ import unicode_literals import re @@ -11,54 +11,55 @@ from django.utils import six -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # javascript identifier unicode categories and "exceptional" chars -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- valid_jsid_categories_start = frozenset([ 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl' - ]) +]) valid_jsid_categories = frozenset([ 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc' - ]) +]) valid_jsid_chars = ('$', '_') -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # regex to find array[index] patterns -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- array_index_regex = re.compile(r'\[[0-9]+\]$') has_valid_array_index = array_index_regex.search replace_array_index = array_index_regex.sub -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # javascript reserved words -- including keywords and null/boolean literals -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- is_reserved_js_word = frozenset([ - 'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float', - 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', - 'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private', - 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', - 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', - 'typeof', 'var', 'void', 'volatile', 'while', 'with', + 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', + 'instanceof', 'int', 'interface', 'long', 'native', 'new', 'null', + 'package', 'private', 'protected', 'public', 'return', 'short', 'static', + 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', + 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', # potentially reserved in a future version of the ES5 standard # 'let', 'yield' - ]).__contains__ +]).__contains__ -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # the core validation functions -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- + -def is_valid_javascript_identifier(identifier, escape=r'\\u', ucd_cat=category): +def is_valid_javascript_identifier(identifier, escape=r'\\u', + ucd_cat=category): """Return whether the given ``id`` is a valid Javascript identifier.""" if not identifier: @@ -71,8 +72,8 @@ return False if escape in identifier: - - new = []; add_char = new.append + new = [] + add_char = new.append split_id = identifier.split(escape) add_char(split_id.pop(0)) @@ -105,7 +106,7 @@ def is_valid_jsonp_callback_value(value): - """Return whether the given ``value`` can be used as a JSON-P callback.""" + "Return whether the given ``value`` can be used as a JSON-P callback." for identifier in value.split(u'.'): while '[' in identifier: @@ -117,14 +118,15 @@ return True -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # test -# ------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- + def test(): """ - The function ``is_valid_javascript_identifier`` validates a given identifier - according to the latest draft of the ECMAScript 5 Specification: + The function ``is_valid_javascript_identifier`` validates a given + identifier according to the latest draft of the ECMAScript 5 Specification: >>> is_valid_javascript_identifier('hello') True diff -Nru django-tastypie-0.12.0/tastypie/validation.py django-tastypie-0.13.3/tastypie/validation.py --- django-tastypie-0.12.0/tastypie/validation.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tastypie/validation.py 2016-02-14 09:10:26.000000000 +0000 @@ -37,8 +37,9 @@ This form will be used to validate the data in ``bundle.data``. """ def __init__(self, **kwargs): - if not 'form_class' in kwargs: - raise ImproperlyConfigured("You must provide a 'form_class' to 'FormValidation' classes.") + if 'form_class' not in kwargs: + raise ImproperlyConfigured( + "You must provide a 'form_class' to 'FormValidation' classes.") self.form_class = kwargs.pop('form_class') super(FormValidation, self).__init__(**kwargs) diff -Nru django-tastypie-0.12.0/tests/alphanumeric/api/resources.py django-tastypie-0.13.3/tests/alphanumeric/api/resources.py --- django-tastypie-0.12.0/tests/alphanumeric/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -from tastypie.authorization import Authorization -from tastypie.fields import CharField -from tastypie.resources import ModelResource -from alphanumeric.models import Product - - -class ProductResource(ModelResource): - class Meta: - resource_name = 'products' - queryset = Product.objects.all() - authorization = Authorization() diff -Nru django-tastypie-0.12.0/tests/alphanumeric/api/urls.py django-tastypie-0.13.3/tests/alphanumeric/api/urls.py --- django-tastypie-0.12.0/tests/alphanumeric/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * -from tastypie.api import Api -from alphanumeric.api.resources import ProductResource - -api = Api(api_name='v1') -api.register(ProductResource(), canonical=True) - -urlpatterns = api.urls diff -Nru django-tastypie-0.12.0/tests/alphanumeric/fixtures/test_data.json django-tastypie-0.13.3/tests/alphanumeric/fixtures/test_data.json --- django-tastypie-0.12.0/tests/alphanumeric/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -[ - { - "fields": { - "name": "Skateboardrampe", - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "11111" - }, - { - "fields": { - "name": "Bigwheel", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "76123" - }, - { - "fields": { - "name": "Trampolin", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "WS65150-01" - }, - { - "fields": { - "name": "Laufrad", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "65100A-01" - }, - { - "fields": { - "name": "Bigwheel", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "76123/01" - }, - { - "fields": { - "name": "Human Hamsterball", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "WS65150/01-01" - }, - { - "fields": { - "name": "Ant Farm", - "created": "2010-05-04 20:05:00", - "updated": "2010-05-04 20:05:00" - }, - "model": "alphanumeric.product", - "pk": "WS77.86" - } -] diff -Nru django-tastypie-0.12.0/tests/alphanumeric/models.py django-tastypie-0.13.3/tests/alphanumeric/models.py --- django-tastypie-0.12.0/tests/alphanumeric/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -import datetime -from django.db import models -from tastypie.utils import now - - -class Product(models.Model): - artnr = models.CharField(max_length=8, primary_key=True) - name = models.CharField(max_length=32, null=False, blank=True, default='') - created = models.DateTimeField(default=now) - updated = models.DateTimeField(default=now) - - def __unicode__(self): - return "%s - %s" % (self.artnr, self.name) - - def save(self, *args, **kwargs): - self.updated = now() - return super(Product, self).save(*args, **kwargs) diff -Nru django-tastypie-0.12.0/tests/alphanumeric/tests/http.py django-tastypie-0.13.3/tests/alphanumeric/tests/http.py --- django-tastypie-0.12.0/tests/alphanumeric/tests/http.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/tests/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,143 +0,0 @@ -import json - -from django.test.utils import override_settings -from django.utils import six - -from testcases import TestServerTestCase - -try: - from http.client import HTTPConnection -except ImportError: - from httplib import HTTPConnection - - -def header_name(name): - if six.PY3: - return name - else: - return name.lower() - -@override_settings(DEBUG=True) -class HTTPTestCase(TestServerTestCase): - def setUp(self): - self.start_test_server(address='localhost', port=8001) - - def tearDown(self): - self.stop_test_server() - - def get_connection(self): - return HTTPConnection('localhost', 8001) - - def test_get_apis_json(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200, data) - self.assertEqual(data, '{"products": {"list_endpoint": "/api/v1/products/", "schema": "/api/v1/products/schema/"}}') - - def test_get_apis_xml(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/xml'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200, data) - self.assertEqual(data, '\n/api/v1/products//api/v1/products/schema/') - - def test_get_list(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/products/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200, data) - expected = { - 'meta': { - 'previous': None, - 'total_count': 7, - 'offset': 0, - 'limit': 20, - 'next': None - }, - 'objects': [ - { - 'updated': '2010-03-30T20:05:00', - 'resource_uri': '/api/v1/products/11111/', - 'name': 'Skateboardrampe', - 'artnr': '11111', - 'created': '2010-03-30T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/76123/', - 'name': 'Bigwheel', - 'artnr': '76123', - 'created': '2010-05-04T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/WS65150-01/', - 'name': 'Trampolin', - 'artnr': 'WS65150-01', - 'created': '2010-05-04T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/65100A-01/', - 'name': 'Laufrad', - 'artnr': '65100A-01', - 'created': '2010-05-04T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/76123/01/', - 'name': 'Bigwheel', - 'artnr': '76123/01', - 'created': '2010-05-04T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/WS65150/01-01/', - 'name': 'Human Hamsterball', - 'artnr': 'WS65150/01-01', - 'created': '2010-05-04T20:05:00' - }, - { - 'updated': '2010-05-04T20:05:00', - 'resource_uri': '/api/v1/products/WS77.86/', - 'name': 'Ant Farm', - 'artnr': 'WS77.86', - 'created': '2010-05-04T20:05:00' - } - ] - } - self.maxDiff = None - resp = json.loads(data) - - #testing separately to help locate issues - self.assertEqual(resp['meta'], expected['meta']) - self.assertEqual(resp['objects'], expected['objects']) - - def test_post_object(self): - connection = self.get_connection() - post_data = '{"artnr": "A76124/03", "name": "Bigwheel XXL"}' - connection.request('POST', '/api/v1/products/', body=post_data, headers={'Accept': 'application/json', 'Content-type': 'application/json'}) - response = connection.getresponse() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 201, data) - self.assertEqual(dict(response.getheaders())[header_name('Location')], 'http://localhost:8001/api/v1/products/A76124/03/') - - # make sure posted object exists - connection.request('GET', '/api/v1/products/A76124/03/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200, data) - - obj = json.loads(data) - - self.assertEqual(obj['name'], 'Bigwheel XXL') - self.assertEqual(obj['artnr'], 'A76124/03') diff -Nru django-tastypie-0.12.0/tests/alphanumeric/tests/__init__.py django-tastypie-0.13.3/tests/alphanumeric/tests/__init__.py --- django-tastypie-0.12.0/tests/alphanumeric/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -from alphanumeric.tests.views import * -from alphanumeric.tests.http import * diff -Nru django-tastypie-0.12.0/tests/alphanumeric/tests/views.py django-tastypie-0.13.3/tests/alphanumeric/tests/views.py --- django-tastypie-0.12.0/tests/alphanumeric/tests/views.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/tests/views.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,179 +0,0 @@ -import django -from django.http import HttpRequest -import json -from testcases import TestCaseWithFixture - - -class ViewsTestCase(TestCaseWithFixture): - def setUp(self): - if django.VERSION >= (1, 4): - self.body_attr = "body" - else: - self.body_attr = "raw_post_data" - super(ViewsTestCase, self).setUp() - - def test_gets(self): - resp = self.client.get('/api/v1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(deserialized['products'], {'list_endpoint': '/api/v1/products/', 'schema': '/api/v1/products/schema/'}) - - resp = self.client.get('/api/v1/products/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['meta']['limit'], 20) - self.assertEqual(len(deserialized['objects']), 7) - self.assertEqual([obj['name'] for obj in deserialized['objects']], - [u'Skateboardrampe', u'Bigwheel', u'Trampolin', u'Laufrad', u'Bigwheel', u'Human Hamsterball', u'Ant Farm']) - - resp = self.client.get('/api/v1/products/11111/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], u'Skateboardrampe') - - resp = self.client.get('/api/v1/products/set/11111;76123/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['name'] for obj in deserialized['objects']], [u'Skateboardrampe', u'Bigwheel']) - - # Same tests with \w+ instead of \d+ for primary key regexp: - resp = self.client.get('/api/v1/products/WS65150-01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], u'Trampolin') - - resp = self.client.get('/api/v1/products/set/WS65150-01;65100A-01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['name'] for obj in deserialized['objects']], [u'Trampolin', u'Laufrad']) - - # And now Slashes - resp = self.client.get('/api/v1/products/76123/01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], u'Bigwheel') - - resp = self.client.get('/api/v1/products/set/76123/01;65100A-01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['name'] for obj in deserialized['objects']], [u'Bigwheel', u'Laufrad']) - - resp = self.client.get('/api/v1/products/WS65150/01-01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], u'Human Hamsterball') - - resp = self.client.get('/api/v1/products/set/76123/01;WS65150/01-01/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['name'] for obj in deserialized['objects']], [u'Bigwheel', u'Human Hamsterball']) - - # And now dots - resp = self.client.get('/api/v1/products/WS77.86/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], u'Ant Farm') - - #slashes, and more dots - resp = self.client.get('/api/v1/products/set/76123/01;WS77.86/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['name'] for obj in deserialized['objects']], [u'Bigwheel', u'Ant Farm']) - - - - def test_posts(self): - request = HttpRequest() - post_data = '{"name": "Ball", "artnr": "12345"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/products/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/products/12345/') - - # make sure posted object exists - resp = self.client.get('/api/v1/products/12345/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['name'], 'Ball') - self.assertEqual(obj['artnr'], '12345') - - # With appended characters - request = HttpRequest() - post_data = '{"name": "Ball 2", "artnr": "12345ABC"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/products/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/products/12345ABC/') - - # make sure posted object exists - resp = self.client.get('/api/v1/products/12345ABC/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['name'], 'Ball 2') - self.assertEqual(obj['artnr'], '12345ABC') - - # With prepended characters - request = HttpRequest() - post_data = '{"name": "Ball 3", "artnr": "WK12345"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/products/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/products/WK12345/') - - # make sure posted object exists - resp = self.client.get('/api/v1/products/WK12345/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['name'], 'Ball 3') - self.assertEqual(obj['artnr'], 'WK12345') - - # Now Primary Keys with Slashes - request = HttpRequest() - post_data = '{"name": "Bigwheel", "artnr": "76123/03"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/products/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/products/76123/03/') - - # make sure posted object exists - resp = self.client.get('/api/v1/products/76123/03/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['name'], 'Bigwheel') - self.assertEqual(obj['artnr'], '76123/03') - - request = HttpRequest() - post_data = '{"name": "Trampolin", "artnr": "WS65150/02"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/products/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/products/WS65150/02/') - - # make sure posted object exists - resp = self.client.get('/api/v1/products/WS65150/02/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['name'], 'Trampolin') - self.assertEqual(obj['artnr'], 'WS65150/02') diff -Nru django-tastypie-0.12.0/tests/alphanumeric/urls.py django-tastypie-0.13.3/tests/alphanumeric/urls.py --- django-tastypie-0.12.0/tests/alphanumeric/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/alphanumeric/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -urlpatterns = patterns('', - (r'^api/', include('alphanumeric.api.urls')), -) diff -Nru django-tastypie-0.12.0/tests/authorization/api/resources.py django-tastypie-0.13.3/tests/authorization/api/resources.py --- django-tastypie-0.12.0/tests/authorization/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/authorization/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,92 +0,0 @@ -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.core.exceptions import ObjectDoesNotExist -from tastypie.authentication import BasicAuthentication -from tastypie.authorization import Authorization -from tastypie.exceptions import Unauthorized -from tastypie import fields -from tastypie.resources import ModelResource -from ..models import AuthorProfile, Article - - -class PerUserAuthorization(Authorization): - def create_list(self, object_list, bundle): - return object_list - - def create_detail(self, object_list, bundle): - return True - - def update_list(self, object_list, bundle): - revised_list = [] - - for article in object_list: - for profile in article.authors.all(): - if bundle.request.user.pk == profile.user.pk: - revised_list.append(article) - - return revised_list - - def update_detail(self, object_list, bundle): - if getattr(bundle.obj, 'pk', None): - try: - obj = object_list.get(pk=bundle.obj.pk) - - for profile in bundle.obj.authors.all(): - if bundle.request.user.pk == profile.user.pk: - return True - except ObjectDoesNotExist: - pass - - # Fallback on the data sent. - for profile in bundle.data['authors']: - if hasattr(profile, 'keys'): - if bundle.request.user.pk == profile['user'].get('id'): - return True - else: - # Ghetto. - if bundle.request.user.pk == profile.strip('/').split('/')[-1]: - return True - - raise Unauthorized("Nope.") - - def delete_list(self, object_list, bundle): - # Disallow deletes completely. - raise Unauthorized("Nope.") - - def delete_detail(self, object_list, bundle): - # Disallow deletes completely. - raise Unauthorized("Nope.") - - -class UserResource(ModelResource): - class Meta: - queryset = User.objects.all() - authentication = BasicAuthentication() - authorization = Authorization() - excludes = ['email', 'password', 'is_staff', 'is_superuser'] - - -class SiteResource(ModelResource): - class Meta: - queryset = Site.objects.all() - authentication = BasicAuthentication() - authorization = Authorization() - - -class AuthorProfileResource(ModelResource): - user = fields.ToOneField(UserResource, 'user', full=True) - sites = fields.ToManyField(SiteResource, 'sites', related_name='author_profiles', full=True) - - class Meta: - queryset = AuthorProfile.objects.all() - authentication = BasicAuthentication() - authorization = Authorization() - - -class ArticleResource(ModelResource): - authors = fields.ToManyField(AuthorProfileResource, 'authors', related_name='articles', full=True) - - class Meta: - queryset = Article.objects.all() - authentication = BasicAuthentication() - authorization = PerUserAuthorization() diff -Nru django-tastypie-0.12.0/tests/authorization/models.py django-tastypie-0.13.3/tests/authorization/models.py --- django-tastypie-0.12.0/tests/authorization/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/authorization/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.db import models -from django.template.defaultfilters import slugify -from tastypie.utils.timezone import now - - -class AuthorProfile(models.Model): - user = models.OneToOneField(User, related_name='author_profile') - short_bio = models.CharField(max_length=255, blank=True, default='') - bio = models.TextField(blank=True, default='') - # We'll use the ``sites`` the author is assigned to as a way to control - # the permissions. - sites = models.ManyToManyField(Site, related_name='author_profiles') - - def __unicode__(self): - return u"Profile: {0}".format(self.user.username) - - -class Article(models.Model): - # We'll also use the ``authors`` to control perms. - authors = models.ManyToManyField(AuthorProfile, related_name='articles') - title = models.CharField(max_length=255) - slug = models.SlugField(blank=True) - content = models.TextField(blank=True, default='') - added_on = models.DateTimeField(default=now) - - def __unicode__(self): - return u"{0} - {1}".format(self.title, self.slug) - - def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify(self.title) - - return super(Article, self).save(*args, **kwargs) diff -Nru django-tastypie-0.12.0/tests/authorization/tests.py django-tastypie-0.13.3/tests/authorization/tests.py --- django-tastypie-0.12.0/tests/authorization/tests.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/authorization/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,248 +0,0 @@ -import mock -import json - -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from tastypie.test import ResourceTestCase -from .models import AuthorProfile, Article -from .api.resources import PerUserAuthorization - - -# If `./run_all_tests.sh authorization` is run, ret_false might never get called -# and some tests will fail, but if `./run_all_tests.sh authorization.tests` is -# run they'll pass. -def ret_false(*args): - return False - -class PerUserAuthorizationTestCase(ResourceTestCase): - def setUp(self): - super(PerUserAuthorizationTestCase, self).setUp() - - # Make some data. - self.site_1 = Site.objects.create( - domain='superawesomenewssite.com', - name='Super Awesome News Site' - ) - self.site_2 = Site.objects.create( - domain='snarkynewssite.com', - name='Snarky News Site' - ) - - self.user_1 = User.objects.create_user('mr_author', 'mister_author@example.com', 'pass') - self.user_2 = User.objects.create_user('mrs_author', 'missus_author@example.com', 'pass') - self.user_3 = User.objects.create_user('ms_editor', 'miss_editor@example.com', 'pass') - - self.author_profile_1 = AuthorProfile.objects.create( - user=self.user_1, - short_bio="Just a dude writing stories for Super Awesome.", - bio="Just a dude writing stories for Super Awesome. Life is good." - ) - self.author_profile_2 = AuthorProfile.objects.create( - user=self.user_2, - short_bio="A highly professional woman writing for Snarky.", - bio="Way better educated than that schmuck writing for Super Awesome. " - ) - self.author_profile_3 = AuthorProfile.objects.create( - user=self.user_3, - short_bio="I wish my writers used spellcheck.", - bio="Whatever." - ) - self.author_profile_1.sites.add(self.site_1) - self.author_profile_2.sites.add(self.site_2) - self.author_profile_3.sites.add(self.site_1, self.site_2) - - self.article_1 = Article.objects.create( - title='New Stuff Announced Today!', - content="Some big tech company announced new stuffs! Go get your consumerism on!" - ) - self.article_1.authors.add(self.author_profile_1, self.author_profile_3) - - self.article_2 = Article.objects.create( - title='Editorial: Why stuff is great', - content="Because you can buy buy buy & fill the gaping voids in your life." - ) - self.article_2.authors.add(self.author_profile_3) - - self.article_3 = Article.objects.create( - title='Ugh, Who Cares About New Stuff?', - content="Obviously stuff by other by other company is way better." - ) - self.article_3.authors.add(self.author_profile_2, self.author_profile_3) - - # Auth bits. - self.author_auth_1 = self.create_basic('mr_author', 'pass') - self.author_auth_2 = self.create_basic('mrs_author', 'pass') - self.author_auth_3 = self.create_basic('ms_editor', 'pass') - - # URIs. - self.article_uri_1 = '/api/v1/article/{0}/'.format(self.article_1.pk) - self.article_uri_2 = '/api/v1/article/{0}/'.format(self.article_2.pk) - self.article_uri_3 = '/api/v1/article/{0}/'.format(self.article_3.pk) - self.author_uri_1 = '/api/v1/authorprofile/{0}/'.format(self.author_profile_1.pk) - self.author_uri_2 = '/api/v1/authorprofile/{0}/'.format(self.author_profile_2.pk) - self.author_uri_3 = '/api/v1/authorprofile/{0}/'.format(self.author_profile_3.pk) - - def test_get_list(self): - # Should be all articles. - resp = self.api_client.get('/api/v1/article/', format='json', authentication=self.author_auth_1) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)['objects']), 3) - first_article = self.deserialize(resp)['objects'][0] - self.assertEqual(first_article['id'], self.article_1.pk) - self.assertEqual(len(first_article['authors']), 2) - - # Should ALSO be all articles. - resp = self.api_client.get('/api/v1/article/', format='json', authentication=self.author_auth_2) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)['objects']), 3) - first_article = self.deserialize(resp)['objects'][0] - self.assertEqual(first_article['id'], self.article_1.pk) - self.assertEqual(len(first_article['authors']), 2) - - def test_get_detail(self): - # Should be all articles. - resp = self.api_client.get(self.article_uri_1, format='json', authentication=self.author_auth_1) - self.assertValidJSONResponse(resp) - first_article = self.deserialize(resp) - self.assertKeys(first_article, ['added_on', 'authors', 'content', 'id', 'resource_uri', 'slug', 'title']) - self.assertEqual(first_article['id'], self.article_1.pk) - - # Should ALSO be all articles, even though it's not our article. - resp = self.api_client.get(self.article_uri_1, format='json', authentication=self.author_auth_2) - self.assertValidJSONResponse(resp) - first_article = self.deserialize(resp) - self.assertKeys(first_article, ['added_on', 'authors', 'content', 'id', 'resource_uri', 'slug', 'title']) - self.assertEqual(first_article['id'], self.article_1.pk) - - # Should ALSO ALSO be all articles, even though it's not our article. - resp = self.api_client.get(self.article_uri_2, format='json', authentication=self.author_auth_1) - self.assertValidJSONResponse(resp) - second_article = self.deserialize(resp) - self.assertKeys(second_article, ['added_on', 'authors', 'content', 'id', 'resource_uri', 'slug', 'title']) - self.assertEqual(second_article['id'], self.article_2.pk) - - @mock.patch.object(PerUserAuthorization, "read_detail", ret_false) - def test_get_unauthorized_detail(self): - resp = self.api_client.get(self.article_uri_1, format='json', authentication=self.author_auth_1) - self.assertHttpUnauthorized(resp) - - def test_post_list(self): - # Should be able to create with reckless abandon. - self.assertEqual(Article.objects.count(), 3) - self.assertHttpCreated(self.api_client.post('/api/v1/article/', format='json', data={ - 'title': 'Yet Another Story', - 'content': 'Stuff.', - 'authors': [self.author_uri_1], - }, authentication=self.author_auth_1)) - # Verify a new one has been added. - self.assertEqual(Article.objects.count(), 4) - - # Should ALSO be able to create with reckless abandon. - self.assertHttpCreated(self.api_client.post('/api/v1/article/', format='json', data={ - 'title': 'Even Another Story', - 'content': 'This time, with competent words.', - 'authors': [self.author_uri_2], - }, authentication=self.author_auth_2)) - # Verify a new one has been added. - self.assertEqual(Article.objects.count(), 5) - - @mock.patch.object(PerUserAuthorization, "create_detail", ret_false) - def test_post_unauthorized_detail(self): - resp = self.api_client.post('/api/v1/article/', format='json', data={ - 'title': 'Yet Another Story', - 'content': 'Stuff.', - 'authors': [self.author_uri_1], - }, authentication=self.author_auth_1) - self.assertHttpUnauthorized(resp) - - def test_put_list(self): - resp = self.api_client.get('/api/v1/article/', format='json', authentication=self.author_auth_2) - self.assertHttpOK(resp) - the_data = json.loads(resp.content.decode('utf-8')) - - # Tweak the data. - the_data['objects'][0]['title'] = 'This is edited.' - the_data['objects'][2]['title'] = 'Updated: {0}'.format(the_data['objects'][2]['title']) - - # Editor can edit whatever, since they're on all the articles. - self.assertEqual(Article.objects.count(), 3) - self.assertHttpAccepted(self.api_client.put('/api/v1/article/', format='json', data=the_data, authentication=self.author_auth_3)) - # Verify no change in count. - self.assertEqual(Article.objects.count(), 3) - self.assertEqual(Article.objects.get(pk=self.article_1.pk).title, 'This is edited.') - self.assertEqual(Article.objects.get(pk=self.article_1.pk).content, 'Some big tech company announced new stuffs! Go get your consumerism on!') - self.assertEqual(Article.objects.get(pk=self.article_3.pk).title, 'Updated: Ugh, Who Cares About New Stuff?') - self.assertEqual(Article.objects.get(pk=self.article_3.pk).content, 'Obviously stuff by other by other company is way better.') - - # But a regular author can't update the whole list. - the_data['objects'][2]['title'] = "Your Story Is Bad And You Should Feel Bad" - del the_data['objects'][0] - self.assertHttpUnauthorized(self.api_client.put('/api/v1/article/', format='json', data=the_data, authentication=self.author_auth_1)) - # Verify count goes down. - self.assertEqual(Article.objects.count(), 2) - # Verify he couldn't edit that title. - self.assertEqual(Article.objects.get(pk=self.article_3.pk).title, 'Updated: Ugh, Who Cares About New Stuff?') - - def test_put_detail(self): - # Should be able to update our story. - self.assertEqual(Article.objects.count(), 3) - self.assertHttpAccepted(self.api_client.put(self.article_uri_1, format='json', data={ - 'title': 'Revised Story', - 'content': "We didn't like the previous version.", - 'authors': [self.author_uri_1], - }, authentication=self.author_auth_1)) - # Verify no change in count. - self.assertEqual(Article.objects.count(), 3) - self.assertEqual(Article.objects.get(pk=self.article_1.pk).title, 'Revised Story') - self.assertEqual(Article.objects.get(pk=self.article_1.pk).content, "We didn't like the previous version.") - - # But CAN'T update one we don't have authorship of. - self.assertHttpUnauthorized(self.api_client.put(self.article_uri_2, format='json', data={ - 'title': 'Ha, Her Story Was Bad', - 'content': "And she didn't share a bagel with me this morning.", - 'authors': [self.author_uri_1], - }, authentication=self.author_auth_2)) - # Verify no change in count. - self.assertEqual(Article.objects.count(), 3) - # Verify no change in content - self.assertEqual(Article.objects.get(pk=self.article_2.pk).title, 'Editorial: Why stuff is great') - self.assertEqual(Article.objects.get(pk=self.article_2.pk).content, 'Because you can buy buy buy & fill the gaping voids in your life.') - - @mock.patch.object(PerUserAuthorization, "update_detail", ret_false) - def test_put_unauthorized_detail(self): - resp = self.api_client.put(self.article_uri_1, format='json', data={ - 'title': 'Revised Story', - 'content': "We didn't like the previous version.", - 'authors': [self.author_uri_1], - }, authentication=self.author_auth_1) - self.assertHttpUnauthorized(resp) - - def test_delete_list(self): - # Never a delete, not even once. - self.assertEqual(Article.objects.count(), 3) - self.assertHttpUnauthorized(self.api_client.delete('/api/v1/article/', format='json', authentication=self.author_auth_1)) - self.assertEqual(Article.objects.count(), 3) - - self.assertHttpUnauthorized(self.api_client.delete('/api/v1/article/', format='json', authentication=self.author_auth_2)) - self.assertEqual(Article.objects.count(), 3) - - self.assertHttpUnauthorized(self.api_client.delete('/api/v1/article/', format='json', authentication=self.author_auth_3)) - self.assertEqual(Article.objects.count(), 3) - - def test_delete_detail(self): - # Never a delete, not even once. - self.assertEqual(Article.objects.count(), 3) - self.assertHttpUnauthorized(self.api_client.delete(self.article_uri_1, format='json', authentication=self.author_auth_1)) - self.assertEqual(Article.objects.count(), 3) - - self.assertHttpUnauthorized(self.api_client.delete(self.article_uri_1, format='json', authentication=self.author_auth_2)) - self.assertEqual(Article.objects.count(), 3) - - self.assertHttpUnauthorized(self.api_client.delete(self.article_uri_1, format='json', authentication=self.author_auth_3)) - self.assertEqual(Article.objects.count(), 3) - - @mock.patch.object(PerUserAuthorization, "delete_detail", ret_false) - def test_delete_unauthorized_detail(self): - self.assertEqual(Article.objects.count(), 3) - self.assertHttpUnauthorized(self.api_client.delete(self.article_uri_1, format='json', authentication=self.author_auth_1)) - self.assertEqual(Article.objects.count(), 3) diff -Nru django-tastypie-0.12.0/tests/authorization/urls.py django-tastypie-0.13.3/tests/authorization/urls.py --- django-tastypie-0.12.0/tests/authorization/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/authorization/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -try: - from django.conf.urls import patterns, url, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, url, include - -from tastypie.api import Api -from .api.resources import ArticleResource, AuthorProfileResource, SiteResource, UserResource -v1_api = Api() -v1_api.register(ArticleResource()) -v1_api.register(AuthorProfileResource()) -v1_api.register(SiteResource()) -v1_api.register(UserResource()) - - -urlpatterns = patterns('', - url(r'^api/', include(v1_api.urls)), -) diff -Nru django-tastypie-0.12.0/tests/basic/api/resources.py django-tastypie-0.13.3/tests/basic/api/resources.py --- django-tastypie-0.12.0/tests/basic/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +0,0 @@ -from django.contrib.auth.models import User -from tastypie.cache import SimpleCache -from tastypie import fields -from tastypie.resources import ModelResource -from tastypie.authentication import SessionAuthentication -from tastypie.authorization import Authorization -from basic.models import Note, AnnotatedNote, SlugBasedNote - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - authorization = Authorization() - - -class CachedUserResource(ModelResource): - class Meta: - allowed_methods = ('get', ) - queryset = User.objects.all() - resource_name = 'cached_users' - cache = SimpleCache(timeout=3600) - - -class PublicCachedUserResource(ModelResource): - class Meta: - allowed_methods = ('get', ) - queryset = User.objects.all() - resource_name = 'public_cached_users' - cache = SimpleCache(timeout=3600, public=True) - - -class PrivateCachedUserResource(ModelResource): - class Meta: - allowed_methods = ('get', ) - queryset = User.objects.all() - resource_name = 'private_cached_users' - cache = SimpleCache(timeout=3600, private=True) - - -class NoteResource(ModelResource): - user = fields.ForeignKey(UserResource, 'user') - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - authorization = Authorization() - - -class BustedResource(ModelResource): - class Meta: - queryset = AnnotatedNote.objects.all() - resource_name = 'busted' - - def get_list(self, *args, **kwargs): - raise Exception("It's broke.") - - -class SlugBasedNoteResource(ModelResource): - class Meta: - queryset = SlugBasedNote.objects.all() - resource_name = 'slugbased' - detail_uri_name = 'slug' - authorization = Authorization() - - -class SessionUserResource(ModelResource): - class Meta: - resource_name = 'sessionusers' - queryset = User.objects.all() - authentication = SessionAuthentication() - authorization = Authorization() diff -Nru django-tastypie-0.12.0/tests/basic/api/urls.py django-tastypie-0.13.3/tests/basic/api/urls.py --- django-tastypie-0.12.0/tests/basic/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * -from tastypie.api import Api -from basic.api.resources import NoteResource, UserResource, BustedResource, CachedUserResource, PublicCachedUserResource, PrivateCachedUserResource, SlugBasedNoteResource, SessionUserResource - -api = Api(api_name='v1') -api.register(NoteResource(), canonical=True) -api.register(UserResource(), canonical=True) -api.register(CachedUserResource(), canonical=True) -api.register(PublicCachedUserResource(), canonical=True) -api.register(PrivateCachedUserResource(), canonical=True) - -v2_api = Api(api_name='v2') -v2_api.register(BustedResource(), canonical=True) -v2_api.register(SlugBasedNoteResource()) -v2_api.register(SessionUserResource()) - -urlpatterns = v2_api.urls + api.urls diff -Nru django-tastypie-0.12.0/tests/basic/fixtures/test_data.json django-tastypie-0.13.3/tests/basic/fixtures/test_data.json --- django-tastypie-0.12.0/tests/basic/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "this_is_not_a_valid_password_string" - }, - "model": "auth.user", - "pk": 1 - }, - - { - "fields": { - "user": 1, - "title": "First Post!", - "slug": "first-post", - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "is_active": true, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "basic.note", - "pk": 1 - }, - { - "fields": { - "user": 1, - "title": "Another Post", - "slug": "another-post", - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "is_active": true, - "created": "2010-03-31 20:05:00", - "updated": "2010-03-31 20:05:00" - }, - "model": "basic.note", - "pk": 2 - }, - { - "fields": { - "title": "First Post", - "content": "But not before the cat ate the hamster.", - "is_active": true - }, - "model": "basic.slugbasednote", - "pk": "first-post" - }, - { - "fields": { - "title": "Another First Post", - "content": "So it's a hamster-in-a-cat-in-a-dog kinda situation.", - "is_active": true - }, - "model": "basic.slugbasednote", - "pk": "another-first-post" - } -] diff -Nru django-tastypie-0.12.0/tests/basic/models.py django-tastypie-0.13.3/tests/basic/models.py --- django-tastypie-0.12.0/tests/basic/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,51 +0,0 @@ -import datetime -from django.contrib.auth.models import User -from django.db import models -from django import forms -from tastypie.utils import now - - -class Note(models.Model): - user = models.ForeignKey(User, related_name='notes') - title = models.CharField(max_length=255) - slug = models.SlugField() - content = models.TextField() - is_active = models.BooleanField(default=True) - created = models.DateTimeField(default=now) - updated = models.DateTimeField(default=now) - - def __unicode__(self): - return self.title - - def save(self, *args, **kwargs): - self.updated = now() - return super(Note, self).save(*args, **kwargs) - - -class AnnotatedNote(models.Model): - note = models.OneToOneField(Note, related_name='annotated') - annotations = models.TextField() - - def __unicode__(self): - title = None - try: - title = self.note.title - except Note.DoesNotExist: - pass - - return u"Annotated %s" % title - - -class SlugBasedNote(models.Model): - slug = models.SlugField(primary_key=True) - title = models.CharField(max_length=255) - content = models.TextField() - is_active = models.BooleanField(default=True) - - def __unicode__(self): - return u"SlugBased %s" % self.title - - -class UserForm(forms.ModelForm): - class Meta: - model = User diff -Nru django-tastypie-0.12.0/tests/basic/tests/http.py django-tastypie-0.13.3/tests/basic/tests/http.py --- django-tastypie-0.12.0/tests/basic/tests/http.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/tests/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,151 +0,0 @@ -import json - -from django.test.utils import override_settings -from django.utils import six - -from testcases import TestServerTestCase - -try: - from http.client import HTTPConnection -except ImportError: - from httplib import HTTPConnection - - -def header_name(name): - if six.PY3: - return name - else: - return name.lower() - -@override_settings(DEBUG=True) -class HTTPTestCase(TestServerTestCase): - def setUp(self): - self.start_test_server(address='localhost', port=8001) - - def tearDown(self): - self.stop_test_server() - - def get_connection(self): - return HTTPConnection('localhost', 8001) - - def test_get_apis_json(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200) - self.assertEqual(data, '{"cached_users": {"list_endpoint": "/api/v1/cached_users/", "schema": "/api/v1/cached_users/schema/"}, "notes": {"list_endpoint": "/api/v1/notes/", "schema": "/api/v1/notes/schema/"}, "private_cached_users": {"list_endpoint": "/api/v1/private_cached_users/", "schema": "/api/v1/private_cached_users/schema/"}, "public_cached_users": {"list_endpoint": "/api/v1/public_cached_users/", "schema": "/api/v1/public_cached_users/schema/"}, "users": {"list_endpoint": "/api/v1/users/", "schema": "/api/v1/users/schema/"}}') - - def test_get_apis_invalid_accept(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'invalid'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 400, "Invalid HTTP Accept headers should return HTTP 400") - - def test_get_resource_invalid_accept(self): - """Invalid HTTP Accept headers should return HTTP 400""" - # We need to test this twice as there's a separate dispatch path for resources: - - connection = self.get_connection() - connection.request('GET', '/api/v1/notes/', headers={'Accept': 'invalid'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 400, "Invalid HTTP Accept headers should return HTTP 400") - - def test_get_apis_xml(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/xml'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200) - self.assertEqual(data, '\n/api/v1/cached_users//api/v1/cached_users/schema//api/v1/notes//api/v1/notes/schema//api/v1/private_cached_users//api/v1/private_cached_users/schema//api/v1/public_cached_users//api/v1/public_cached_users/schema//api/v1/users//api/v1/users/schema/') - - def test_get_list(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/notes/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - self.assertEqual(response.read().decode('utf-8'), '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 2}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00", "user": "/api/v1/users/1/"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00", "user": "/api/v1/users/1/"}]}') - - def test_post_object(self): - connection = self.get_connection() - post_data = '{"content": "A new post.", "is_active": true, "title": "New Title", "slug": "new-title", "user": "/api/v1/users/1/"}' - connection.request('POST', '/api/v1/notes/', body=post_data, headers={'Accept': 'application/json', 'Content-type': 'application/json'}) - response = connection.getresponse() - self.assertEqual(response.status, 201) - self.assertEqual(dict(response.getheaders())[header_name('Location')], 'http://localhost:8001/api/v1/notes/3/') - - # make sure posted object exists - connection.request('GET', '/api/v1/notes/3/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - - data = response.read().decode('utf-8') - obj = json.loads(data) - - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_vary_accept(self): - """ - Ensure that resources return the Vary: Accept header. - """ - connection = self.get_connection() - connection.request('GET', '/api/v1/cached_users/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - - headers = dict(response.getheaders()) - vary = headers.get(header_name('Vary'), "") - vary_types = [x.strip().lower() for x in vary.split(",") if x.strip()] - self.assertIn("accept", vary_types) - - def test_cache_control(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/cached_users/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - headers = dict(response.getheaders()) - cache_control = set([x.strip().lower() for x in headers[header_name("Cache-Control")].split(",") if x.strip()]) - - self.assertEqual(cache_control, set(["s-maxage=3600", "max-age=3600"])) - self.assertTrue('"johndoe"' in response.read().decode('utf-8')) - - def test_public_cache_control(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/public_cached_users/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - headers = dict(response.getheaders()) - cache_control = set([x.strip().lower() for x in headers[header_name("Cache-Control")].split(",") if x.strip()]) - - self.assertEqual(cache_control, set(["s-maxage=3600", "max-age=3600", "public"])) - self.assertTrue('"johndoe"' in response.read().decode('utf-8')) - - def test_private_cache_control(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/private_cached_users/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - headers = dict(response.getheaders()) - cache_control = set([x.strip().lower() for x in headers[header_name("Cache-Control")].split(",") if x.strip()]) - - self.assertEqual(cache_control, set(["s-maxage=3600", "max-age=3600", "private"])) - self.assertTrue('"johndoe"' in response.read().decode('utf-8')) diff -Nru django-tastypie-0.12.0/tests/basic/tests/__init__.py django-tastypie-0.13.3/tests/basic/tests/__init__.py --- django-tastypie-0.12.0/tests/basic/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -from basic.tests.http import * -from basic.tests.resources import * -from basic.tests.views import * \ No newline at end of file diff -Nru django-tastypie-0.12.0/tests/basic/tests/resources.py django-tastypie-0.13.3/tests/basic/tests/resources.py --- django-tastypie-0.12.0/tests/basic/tests/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/tests/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,188 +0,0 @@ -from django.contrib.auth.models import User -from django.http import HttpRequest -from tastypie.bundle import Bundle -from tastypie.fields import ToOneField, ToManyField -from tastypie.resources import ModelResource -from basic.api.resources import SlugBasedNoteResource -from basic.models import Note, AnnotatedNote, SlugBasedNote -from testcases import TestCaseWithFixture - - -class InvalidLazyUserResource(ModelResource): - notes = ToManyField('basic.api.resources.FooResource', 'notes') - - class Meta: - queryset = User.objects.all() - - -class NoPathLazyUserResource(ModelResource): - notes = ToManyField('FooResource', 'notes') - - class Meta: - queryset = User.objects.all() - - -class LazyUserResource(ModelResource): - notes = ToManyField('basic.tests.resources.NoteResource', 'notes') - - class Meta: - queryset = User.objects.all() - api_name = 'foo' - - -class NoteResource(ModelResource): - class Meta: - queryset = Note.objects.all() - - -class AnnotatedNoteResource(ModelResource): - class Meta: - queryset = AnnotatedNote.objects.all() - - -class NoteWithAnnotationsResource(ModelResource): - annotated = ToOneField(AnnotatedNoteResource, 'annotated', null=True) - - class Meta: - queryset = Note.objects.all() - - -class NoteModelResourceTestCase(TestCaseWithFixture): - def test_init(self): - resource_1 = NoteResource() - self.assertEqual(len(resource_1.fields), 8) - self.assertNotEqual(resource_1._meta.queryset, None) - self.assertEqual(resource_1._meta.resource_name, 'note') - - # TextFields should have ``default=''`` to match Django's behavior, - # even though that's not what is on the field proper. - self.assertEqual(resource_1.fields['content'].default, '') - - def test_lazy_relations(self): - ilur = InvalidLazyUserResource() - nplur = NoPathLazyUserResource() - lur = LazyUserResource() - - self.assertEqual(ilur.notes.to, 'basic.api.resources.FooResource') - self.assertEqual(nplur.notes.to, 'FooResource') - self.assertEqual(lur.notes.to, 'basic.tests.resources.NoteResource') - - try: - ilur.notes.to_class() - self.fail("to_class on InvalidLazyUserResource should fail!") - except ImportError: - pass - - try: - nplur.notes.to_class() - self.fail("to_class on NoPathLazyUserResource should fail!") - except ImportError: - pass - - to_class = lur.notes.to_class() - self.assertTrue(isinstance(to_class, NoteResource)) - # This is important, as without passing on the ``api_name``, URL - # reversals will fail. Fakes the instance as ``None``, since for - # testing purposes, we don't care. - related = lur.notes.get_related_resource(None) - self.assertEqual(related._meta.api_name, 'foo') - - -class AnnotatedNoteModelResourceTestCase(TestCaseWithFixture): - def test_one_to_one_regression(self): - # Make sure bits don't completely blow up if the related model - # is gone. - n1 = Note.objects.get(pk=1) - - resource_1 = NoteWithAnnotationsResource() - n1_bundle = resource_1.build_bundle(obj=n1) - dehydrated = resource_1.full_dehydrate(n1_bundle) - - -class DetailURIKwargsResourceTestCase(TestCaseWithFixture): - def test_correct_detail_uri_model(self): - n1 = Note.objects.get(pk=1) - - resource = NoteWithAnnotationsResource() - self.assertEqual(resource.detail_uri_kwargs(n1), { - 'pk': 1, - }) - - def test_correct_detail_uri_bundle(self): - n1 = Note.objects.get(pk=1) - - resource = NoteWithAnnotationsResource() - n1_bundle = resource.build_bundle(obj=n1) - self.assertEqual(resource.detail_uri_kwargs(n1_bundle), { - 'pk': 1, - }) - - def test_correct_slug_detail_uri_model(self): - n1 = SlugBasedNote.objects.get(pk='first-post') - - resource = SlugBasedNoteResource() - self.assertEqual(resource.detail_uri_kwargs(n1), { - 'slug': 'first-post', - }) - - def test_correct_slug_detail_uri_bundle(self): - n1 = SlugBasedNote.objects.get(pk='first-post') - - resource = SlugBasedNoteResource() - n1_bundle = resource.build_bundle(obj=n1) - self.assertEqual(resource.detail_uri_kwargs(n1_bundle), { - 'slug': 'first-post', - }) - - -class SlugBasedResourceTestCase(TestCaseWithFixture): - def setUp(self): - super(SlugBasedResourceTestCase, self).setUp() - self.n1 = SlugBasedNote.objects.get(pk='first-post') - self.request = HttpRequest() - self.request.method = 'PUT' - self.resource = SlugBasedNoteResource() - self.n1_bundle = self.resource.build_bundle(obj=self.n1) - - def test_bundle_unique_field(self): - self.assertEqual(self.resource.get_bundle_detail_data(self.n1_bundle), u'first-post') - - def test_obj_update(self): - bundle = self.resource.build_bundle(obj=self.n1, data={ - 'title': 'Foo!', - }) - updated_bundle = self.resource.obj_update(bundle, slug='first-post') - self.assertEqual(updated_bundle.obj.slug, 'first-post') - self.assertEqual(updated_bundle.obj.title, 'Foo!') - - # Again, without the PK this time. - self.n1.slug = None - bundle = self.resource.build_bundle(obj=self.n1, data={ - 'title': 'Bar!', - }) - updated_bundle_2 = self.resource.obj_update(bundle, slug='first-post') - self.assertEqual(updated_bundle_2.obj.slug, 'first-post') - self.assertEqual(updated_bundle_2.obj.title, 'Bar!') - - def test_update_in_place(self): - new_data = { - 'slug': u'foo', - 'title': u'Foo!', - } - new_bundle = self.resource.update_in_place(self.request, self.n1_bundle, new_data) - # Check for updated data. - self.assertEqual(new_bundle.obj.title, u'Foo!') - self.assertEqual(new_bundle.obj.slug, u'foo') - # Make sure it looked up the right instance, even though we didn't - # hand it a PK... - self.assertEqual(new_bundle.obj.pk, self.n1_bundle.obj.pk) - - def test_rollback(self): - bundles = [ - self.n1_bundle - ] - self.resource.rollback(bundles) - - # Make sure it's gone. - self.assertRaises(SlugBasedNote.DoesNotExist, SlugBasedNote.objects.get, pk='first-post') - diff -Nru django-tastypie-0.12.0/tests/basic/tests/views.py django-tastypie-0.13.3/tests/basic/tests/views.py --- django-tastypie-0.12.0/tests/basic/tests/views.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/tests/views.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,160 +0,0 @@ -import django -from django.contrib.auth.models import User -from django.http import HttpRequest -from django.test import Client -import json -from testcases import TestCaseWithFixture - - -class ViewsTestCase(TestCaseWithFixture): - def setUp(self): - if django.VERSION >= (1, 4): - self.body_attr = "body" - else: - self.body_attr = "raw_post_data" - super(ViewsTestCase, self).setUp() - - def test_gets(self): - resp = self.client.get('/api/v1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['notes'], {'list_endpoint': '/api/v1/notes/', 'schema': '/api/v1/notes/schema/'}) - - resp = self.client.get('/api/v1/notes/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['meta']['limit'], 20) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'First Post!', u'Another Post']) - - resp = self.client.get('/api/v1/notes/1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 9) - self.assertEqual(deserialized['title'], u'First Post!') - - resp = self.client.get('/api/v1/notes/set/2;1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'Another Post', u'First Post!']) - - def test_get_test_client_error(self): - # The test server should re-raise exceptions to make debugging easier. - self.assertRaises(Exception, self.client.get, '/api/v2/busted/', data={'format': 'json'}) - - def test_posts(self): - request = HttpRequest() - post_data = b'{"content": "A new post.", "is_active": true, "title": "New Title", "slug": "new-title", "user": "/api/v1/users/1/"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/notes/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/notes/3/') - - # make sure posted object exists - resp = self.client.get('/api/v1/notes/3/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_puts(self): - request = HttpRequest() - post_data = '{"content": "Another new post.", "is_active": true, "title": "Another New Title", "slug": "new-title", "user": "/api/v1/users/1/"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.put('/api/v1/notes/1/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 204) - - # make sure posted object exists - resp = self.client.get('/api/v1/notes/1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['content'], 'Another new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_api_field_error(self): - # When a field error is encountered, we should be presenting the message - # back to the user. - request = HttpRequest() - post_data = '{"content": "More internet memes.", "is_active": true, "title": "IT\'S OVER 9000!", "slug": "its-over", "user": "/api/v1/users/9001/"}' - setattr(request, "_" + self.body_attr, post_data) - - resp = self.client.post('/api/v1/notes/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content.decode('utf-8')), - { - "error": "Could not find the provided object via resource URI \'/api/v1/users/9001/\'." - } - ) - - def test_options(self): - resp = self.client.options('/api/v1/notes/') - self.assertEqual(resp.status_code, 200) - allows = 'GET,POST,PUT,DELETE,PATCH' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/notes/1/') - self.assertEqual(resp.status_code, 200) - allows = 'GET,POST,PUT,DELETE,PATCH' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/notes/schema/') - self.assertEqual(resp.status_code, 200) - allows = 'GET' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/notes/set/2;1/') - self.assertEqual(resp.status_code, 200) - allows = 'GET' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - def test_slugbased(self): - resp = self.client.get('/api/v2/slugbased/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['meta']['limit'], 20) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'First Post', u'Another First Post']) - - resp = self.client.get('/api/v2/slugbased/first-post/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['title'], u'First Post') - - resp = self.client.get('/api/v2/slugbased/set/another-first-post;first-post/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'Another First Post', u'First Post']) - - def test_session_auth(self): - csrf_client = Client(enforce_csrf_checks=True) - super_duper = User.objects.create_superuser('daniel', 'daniel@example.com', 'pass') - - # Unauthenticated. - resp = csrf_client.get('/api/v2/sessionusers/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 401) - - # Now log in. - self.assertTrue(csrf_client.login(username='daniel', password='pass')) - # Fake the cookie the login didn't create. :( - csrf_client.cookies['csrftoken'] = 'o9nXqnrypI9ydKoiWGCjDDcxXI7qRymH' - - resp = csrf_client.get('/api/v2/sessionusers/', data={'format': 'json'}, HTTP_X_CSRFTOKEN='o9nXqnrypI9ydKoiWGCjDDcxXI7qRymH') - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) diff -Nru django-tastypie-0.12.0/tests/basic/urls.py django-tastypie-0.13.3/tests/basic/urls.py --- django-tastypie-0.12.0/tests/basic/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/basic/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -urlpatterns = patterns('', - (r'^api/', include('basic.api.urls')), -) diff -Nru django-tastypie-0.12.0/tests/complex/api/resources.py django-tastypie-0.13.3/tests/complex/api/resources.py --- django-tastypie-0.12.0/tests/complex/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/complex/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -from django.contrib.auth.models import User, Group -from django.contrib.comments.models import Comment -from tastypie.fields import CharField, ForeignKey, ManyToManyField, OneToOneField, OneToManyField -from tastypie.resources import ModelResource -from complex.models import Post, Profile - - -class ProfileResource(ModelResource): - class Meta: - queryset = Profile.objects.all() - resource_name = 'profiles' - - -class CommentResource(ModelResource): - class Meta: - queryset = Comment.objects.all() - resource_name = 'comments' - - -class GroupResource(ModelResource): - class Meta: - queryset = Group.objects.all() - resource_name = 'groups' - - -class UserResource(ModelResource): - groups = ManyToManyField(GroupResource, 'groups', full=True) - profile = OneToOneField(ProfileResource, 'profile', full=True) - - class Meta: - queryset = User.objects.all() - resource_name = 'users' - - -class PostResource(ModelResource): - user = ForeignKey(UserResource, 'user') - comments = OneToManyField(CommentResource, 'comments', full=False) - - class Meta: - queryset = Post.objects.all() - resource_name = 'posts' diff -Nru django-tastypie-0.12.0/tests/complex/api/urls.py django-tastypie-0.13.3/tests/complex/api/urls.py --- django-tastypie-0.12.0/tests/complex/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/complex/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * -from tastypie.api import Api -from complex.api.resources import PostResource, ProfileResource, CommentResource, UserResource, GroupResource - - -api = Api(api_name='v1') -api.register(PostResource(), canonical=True) -api.register(ProfileResource(), canonical=True) -api.register(CommentResource(), canonical=True) -api.register(UserResource(), canonical=True) -api.register(GroupResource(), canonical=True) - -urlpatterns = api.urls diff -Nru django-tastypie-0.12.0/tests/complex/fixtures/test_data.json django-tastypie-0.13.3/tests/complex/fixtures/test_data.json --- django-tastypie-0.12.0/tests/complex/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/complex/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,180 +0,0 @@ -[ - { - "fields": { - "content": "This is the first post!", - "created": "2010-04-21 13:38:35", - "is_active": true, - "slug": "first-post", - "title": "The First Post", - "updated": "2010-04-21 13:39:12", - "user": 1 - }, - "model": "complex.post", - "pk": 1 - }, - { - "fields": { - "content": "This is another post!", - "created": "2010-04-21 14:38:35", - "is_active": true, - "slug": "another-post", - "title": "Another Post", - "updated": "2010-04-21 14:39:12", - "user": 2 - }, - "model": "complex.post", - "pk": 2 - }, - { - "fields": { - "active": true, - "age": 29, - "avatar": "avatars/avatar1.png", - "balance": "5123.44", - "bio": "The incredible house-rockin Daniel Lindsley.", - "date_joined": "2010-04-20", - "datetime_joined": "2010-04-20 13:27:24", - "document": "documents/document1.txt", - "email": "daniel@example.com", - "favorite_color": "blue", - "favorite_number": 3003, - "favorite_numbers": "3,23", - "favorite_small_number": 3, - "file_path": "afile.txt", - "height": 5, - "homepage": "http://daniel.example.com/", - "ip": "321.321.321.321", - "name_slug": "daniel-lindsley", - "rocks_da_house": true, - "time_joined": "13:27:24", - "user": 1, - "weight": 180.0 - }, - "model": "complex.profile", - "pk": 1 - }, - { - "fields": { - "active": true, - "age": 79, - "avatar": "avatars/avatar2.png", - "balance": "1342.12", - "bio": "I'm the scatman.", - "date_joined": "2010-04-21", - "datetime_joined": "2010-04-21 13:27:24", - "document": "documents/document2.txt", - "email": "scatman@example.com", - "favorite_color": "brown", - "favorite_number": 4004, - "favorite_numbers": "1,2,3", - "favorite_small_number": 3, - "file_path": "anewfile.txt", - "height": 6, - "homepage": "http://scatman.example.com/", - "ip": "123.123.123.123", - "name_slug": "scatman", - "rocks_da_house": false, - "time_joined": "13:27:22", - "user": 2, - "weight": 150.0 - }, - "model": "complex.profile", - "pk": 2 - }, - { - "fields": { - "comment": "Nice post", - "content_type": ["complex", "post"], - "ip_address": "", - "is_public": true, - "is_removed": false, - "object_pk": "1", - "site": 1, - "submit_date": "2010-04-21 14:56:36", - "user": 1, - "user_email": "", - "user_name": "", - "user_url": "" - }, - "model": "comments.comment", - "pk": 1 - }, - { - "fields": { - "comment": "Even better than the last!", - "content_type": ["complex", "post"], - "ip_address": "", - "is_public": true, - "is_removed": false, - "object_pk": "2", - "site": 1, - "submit_date": "2010-04-21 14:56:36", - "user": 2, - "user_email": "", - "user_name": "", - "user_url": "" - }, - "model": "comments.comment", - "pk": 2 - }, - { - "fields": { - "date_joined": "2010-04-21 15:02:31", - "email": "daniel@example.com", - "first_name": "", - "groups": [1, 2], - "is_active": true, - "is_staff": true, - "is_superuser": true, - "last_login": "2010-04-21 15:02:31", - "last_name": "", - "password": "sha1$e9521$b36b113c225647b49a58a2c7e106c8921260e7d5", - "user_permissions": [], - "username": "daniel" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "date_joined": "2010-04-21 15:04:53", - "email": "scatman@example.com", - "first_name": "", - "groups": [1], - "is_active": true, - "is_staff": false, - "is_superuser": false, - "last_login": "2010-04-21 15:04:53", - "last_name": "", - "password": "sha1$16d5f$98895372d746f796f38c7ec04b74bea01a921cbb", - "user_permissions": [], - "username": "scatman" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "name": "Ninjas", - "permissions": [] - }, - "model": "auth.group", - "pk": 1 - }, - { - "fields": { - "name": "Pirates", - "permissions": [] - }, - "model": "auth.group", - "pk": 2 - }, - { - "fields": { - "domain": "example.com", - "name": "example.com" - }, - "model": "sites.site", - "pk": 1 - } -] diff -Nru django-tastypie-0.12.0/tests/complex/models.py django-tastypie-0.13.3/tests/complex/models.py --- django-tastypie-0.12.0/tests/complex/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/complex/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,66 +0,0 @@ -import datetime -import os -from django.contrib.auth.models import User -from django.contrib.comments.models import Comment -from django.contrib.contenttypes import generic -from django.contrib.contenttypes.models import ContentType -from django.core.management import call_command -from django.db import models -from django.db.models import signals, get_models -from django.conf import settings - -from tastypie.utils import now - -try: - import PIL -except: - PIL = None - -class Post(models.Model): - user = models.ForeignKey(User, related_name='notes') - title = models.CharField(max_length=255) - slug = models.SlugField() - content = models.TextField() - is_active = models.BooleanField(default=True) - created = models.DateTimeField(default=now) - updated = models.DateTimeField(default=now) - comments = generic.GenericRelation(Comment, content_type_field="content_type", object_id_field="object_pk") - - def __unicode__(self): - return self.title - - def save(self, *args, **kwargs): - self.updated = now() - return super(Post, self).save(*args, **kwargs) - - -class Profile(models.Model): - user = models.OneToOneField(User) - email = models.EmailField() - active = models.BooleanField() - favorite_color = models.CharField(max_length=255) - favorite_numbers = models.CommaSeparatedIntegerField(max_length=5) - favorite_number = models.IntegerField() - age = models.PositiveSmallIntegerField() - favorite_small_number = models.SmallIntegerField() - height = models.PositiveIntegerField() - weight = models.FloatField() - balance = models.DecimalField(decimal_places=2, max_digits=6) - date_joined = models.DateField() - time_joined = models.TimeField() - datetime_joined = models.DateTimeField() - document = models.FileField(upload_to='documents') - file_path = models.FilePathField(path=settings.MEDIA_ROOT) - ip = models.IPAddressField() - rocks_da_house = models.NullBooleanField() - name_slug = models.SlugField() - bio = models.TextField() - homepage = models.URLField() - if PIL: - avatar = models.ImageField(upload_to='avatars') - else: - avatar = models.FileField(upload_to='avatars') - - def __unicode__(self): - return "%s's profile" % self.user - diff -Nru django-tastypie-0.12.0/tests/complex/urls.py django-tastypie-0.13.3/tests/complex/urls.py --- django-tastypie-0.12.0/tests/complex/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/complex/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -urlpatterns = patterns('', - (r'^api/', include('complex.api.urls')), -) diff -Nru django-tastypie-0.12.0/tests/content_gfk/api/resources.py django-tastypie-0.13.3/tests/content_gfk/api/resources.py --- django-tastypie-0.12.0/tests/content_gfk/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -from tastypie.authorization import Authorization -from tastypie.contrib.contenttypes.fields import GenericForeignKeyField -from tastypie.resources import ModelResource -from content_gfk.models import Note, Quote, Definition, Rating - - -class DefinitionResource(ModelResource): - - class Meta: - resource_name = 'definitions' - queryset = Definition.objects.all() - -class NoteResource(ModelResource): - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - - -class QuoteResource(ModelResource): - - class Meta: - resource_name = 'quotes' - queryset = Quote.objects.all() - -class RatingResource(ModelResource): - content_object = GenericForeignKeyField({ - Note: NoteResource, - Quote: QuoteResource - }, 'content_object') - - class Meta: - resource_name = 'ratings' - queryset = Rating.objects.all() - authorization = Authorization() diff -Nru django-tastypie-0.12.0/tests/content_gfk/api/urls.py django-tastypie-0.13.3/tests/content_gfk/api/urls.py --- django-tastypie-0.12.0/tests/content_gfk/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * -from tastypie.api import Api -from content_gfk.api.resources import NoteResource, QuoteResource, \ - RatingResource, DefinitionResource - - -api = Api(api_name='v1') -api.register(NoteResource()) -api.register(QuoteResource()) -api.register(RatingResource()) -api.register(DefinitionResource()) - -urlpatterns = api.urls diff -Nru django-tastypie-0.12.0/tests/content_gfk/models.py django-tastypie-0.13.3/tests/content_gfk/models.py --- django-tastypie-0.12.0/tests/content_gfk/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic - - -class Definition(models.Model): - word = models.CharField(max_length=255) - content = models.TextField() - -class Note(models.Model): - title = models.CharField(max_length=255) - content = models.TextField() - - -class Quote(models.Model): - byline = models.CharField(max_length=255) - content = models.TextField() - - -class Rating(models.Model): - RATINGS = [ (x, x) for x in range(1, 6) ] - - rating = models.PositiveIntegerField(choices=RATINGS, default=3) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') diff -Nru django-tastypie-0.12.0/tests/content_gfk/tests/fields.py django-tastypie-0.13.3/tests/content_gfk/tests/fields.py --- django-tastypie-0.12.0/tests/content_gfk/tests/fields.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/tests/fields.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -from __future__ import with_statement - -from django.test import TestCase -from tastypie.contrib.contenttypes.fields import GenericForeignKeyField -from tastypie.bundle import Bundle -from content_gfk.models import Note, Quote, Rating, Definition -from content_gfk.api.resources import NoteResource, DefinitionResource, \ - QuoteResource, RatingResource - - -class ContentTypeFieldTestCase(TestCase): - - def test_init(self): - # Test that you have to use a dict some other resources - with self.assertRaises(ValueError): - GenericForeignKeyField(((Note, NoteResource)), 'nofield') - - # Test that you must register some other resources - with self.assertRaises(ValueError): - GenericForeignKeyField({}, 'nofield') - - # Test that the resources you raise must be models - with self.assertRaises(ValueError): - GenericForeignKeyField({NoteResource: Note}, 'nofield') - - def test_get_related_resource(self): - gfk_field = GenericForeignKeyField({ - Note: NoteResource, - Quote: QuoteResource - }, 'nofield') - - definition_1 = Definition.objects.create( - word='toast', - content="Cook or brown (food, esp. bread or cheese)" - ) - - # Test that you can not link to a model that does not have a resource - with self.assertRaises(TypeError): - gfk_field.get_related_resource(definition_1) - - note_1 = Note.objects.create( - title='All aboard the rest train', - content='Sometimes it is just better to lorem ipsum' - ) - - self.assertTrue(isinstance(gfk_field.get_related_resource(note_1), NoteResource)) - - def test_resource_from_uri(self): - note_2 = Note.objects.create( - title='Generic and such', - content='Sometimes it is to lorem ipsum' - ) - - gfk_field = GenericForeignKeyField({ - Note: NoteResource, - Quote: QuoteResource - }, 'nofield') - - self.assertEqual( - gfk_field.resource_from_uri( - gfk_field.to_class(), - '/api/v1/notes/%s/' % note_2.pk - ).obj, - note_2 - ) - - def test_build_related_resource(self): - gfk_field = GenericForeignKeyField({ - Note: NoteResource, - Quote: QuoteResource - }, 'nofield') - - quote_1 = Quote.objects.create( - byline='Issac Kelly', - content='To ipsum or not to ipsum, that is the cliche' - ) - qr = QuoteResource() - qr.build_bundle(obj=quote_1) - - bundle = gfk_field.build_related_resource( - '/api/v1/quotes/%s/' % quote_1.pk - ) - - # Test that the GFK field builds the same as the QuoteResource - self.assertEqual(bundle.obj, quote_1) diff -Nru django-tastypie-0.12.0/tests/content_gfk/tests/__init__.py django-tastypie-0.13.3/tests/content_gfk/tests/__init__.py --- django-tastypie-0.12.0/tests/content_gfk/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -import warnings -warnings.simplefilter('ignore', Warning) - -from content_gfk.tests.fields import * -from content_gfk.tests.resources import * diff -Nru django-tastypie-0.12.0/tests/content_gfk/tests/resources.py django-tastypie-0.13.3/tests/content_gfk/tests/resources.py --- django-tastypie-0.12.0/tests/content_gfk/tests/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/tests/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -from django.test import TestCase -from tastypie.exceptions import NotFound -from tastypie.contrib.contenttypes.resources import GenericResource - -from content_gfk.api.resources import NoteResource, DefinitionResource - - -class GenericResourceTestCase(TestCase): - def setUp(self): - self.resource = GenericResource([NoteResource, DefinitionResource]) - - - def test_bad_uri(self): - bad_uri = '/bad_uri/' - self.assertRaises(NotFound, self.resource.get_via_uri, bad_uri) - - - def test_resource_not_registered(self): - bad_uri = '/api/v1/quotes/1/' - self.assertRaises(NotFound, self.resource.get_via_uri, bad_uri) diff -Nru django-tastypie-0.12.0/tests/content_gfk/urls.py django-tastypie-0.13.3/tests/content_gfk/urls.py --- django-tastypie-0.12.0/tests/content_gfk/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/content_gfk/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -urlpatterns = patterns('', - (r'^api/', include('content_gfk.api.urls')), -) diff -Nru django-tastypie-0.12.0/tests/core/fixtures/note_testdata.json django-tastypie-0.13.3/tests/core/fixtures/note_testdata.json --- django-tastypie-0.12.0/tests/core/fixtures/note_testdata.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/fixtures/note_testdata.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,148 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "abc123" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "janedoe", - "email": "jane@doe.com", - "password": "def456" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "username": "bobdoe", - "email": "bob@doe.com", - "password": "ghi789", - "is_active": false - }, - "model": "auth.user", - "pk": 3 - }, - - { - "fields": { - "date": "2012-09-07", - "username": "MARAUJOP", - "message": "hello" - }, - "model": "core.daterecord", - "pk": 1 - }, - - { - "fields": { - "author": 1, - "title": "First Post!", - "slug": "first-post", - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "is_active": true, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "core.note", - "pk": 1 - }, - { - "fields": { - "author": 1, - "title": "Another Post", - "slug": "another-post", - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "is_active": true, - "created": "2010-03-31 20:05:00", - "updated": "2010-03-31 20:05:00" - }, - "model": "core.note", - "pk": 2 - }, - { - "fields": { - "author": 2, - "title": "Hello World!", - "slug": "hello-world", - "content": "On second though, not sure if I'm ready to share this with the world.", - "is_active": false, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "core.note", - "pk": 3 - }, - { - "fields": { - "author": 2, - "title": "Recent Volcanic Activity.", - "slug": "recent-volcanic-activity", - "content": "My neighborhood's been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "is_active": true, - "created": "2010-04-01 20:05:00", - "updated": "2010-04-01 20:05:00" - }, - "model": "core.note", - "pk": 4 - }, - { - "fields": { - "author": 1, - "title": "My favorite new show", - "slug": "my-favorite-new-show", - "content": "I found an awesome new show on TV. It's about vampires and pancakes and the strong love between them that the wagon is trying to break up.", - "is_active": false, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "core.note", - "pk": 5 - }, - { - "fields": { - "author": 2, - "title": "Granny's Gone", - "slug": "grannys-gone", - "content": "Man, the second eruption came on fast. Granny didn't have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "is_active": true, - "created": "2010-04-02 10:05:00", - "updated": "2010-04-02 10:05:00" - }, - "model": "core.note", - "pk": 6 - }, - - { - "fields": { - "note": 1, - "title": "Funny Cat Picture", - "image": "lulz/catz.gif" - }, - "model": "core.mediabit", - "pk": 1 - }, - - { - "fields": { - "name": "Signups", - "slug": "signups", - "count": 1 - }, - "model": "core.counter", - "pk": 1 - }, - { - "fields": { - "name": "Logins", - "slug": "logins", - "count": 4 - }, - "model": "core.counter", - "pk": 2 - } -] diff -Nru django-tastypie-0.12.0/tests/core/forms.py django-tastypie-0.13.3/tests/core/forms.py --- django-tastypie-0.12.0/tests/core/forms.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/forms.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -from django import forms -from core.models import Note - - -class NoteForm(forms.ModelForm): - foobaz = forms.CharField() - - class Meta: - model = Note - - -class VeryCustomNoteForm(NoteForm): - class Meta: - model = Note - fields = ['title', 'content', 'created', 'is_active', 'foobaz'] - - -# Notes: -# * VeryCustomNoteForm will ONLY have the four listed fields. -# * VeryCustomNoteForm does NOT inherit the ``foobaz`` field from it's -# parent class (unless manually specified). diff -Nru django-tastypie-0.12.0/tests/core/models.py django-tastypie-0.13.3/tests/core/models.py --- django-tastypie-0.12.0/tests/core/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,81 +0,0 @@ -import datetime -from django.contrib.auth.models import User -from django.db import models -from tastypie.utils import now, aware_datetime - - -class DateRecord(models.Model): - date = models.DateField() - username = models.CharField(max_length=20) - message = models.CharField(max_length=20) - - -class Note(models.Model): - author = models.ForeignKey(User, related_name='notes', blank=True, null=True) - title = models.CharField(max_length=100) - slug = models.SlugField() - content = models.TextField(blank=True) - is_active = models.BooleanField(default=True) - created = models.DateTimeField(default=now) - updated = models.DateTimeField(default=now) - - def __unicode__(self): - return self.title - - def save(self, *args, **kwargs): - self.updated = now() - return super(Note, self).save(*args, **kwargs) - - def what_time_is_it(self): - return aware_datetime(2010, 4, 1, 0, 48) - - def get_absolute_url(self): - return '/some/fake/path/%s/' % self.pk - - @property - def my_property(self): - return 'my_property' - -class NoteWithEditor(Note): - editor = models.ForeignKey(User, related_name='notes_edited') - -class Subject(models.Model): - notes = models.ManyToManyField(Note, related_name='subjects') - name = models.CharField(max_length=255) - url = models.URLField() - created = models.DateTimeField(default=now) - - def __unicode__(self): - return self.name - - -class MediaBit(models.Model): - note = models.ForeignKey(Note, related_name='media_bits') - title = models.CharField(max_length=32) - image = models.FileField(blank=True, null=True, upload_to='bits/') - - def __unicode__(self): - return self.title - - -class AutoNowNote(models.Model): - # Purposely a bit more complex to test correct introspection. - title = models.CharField(max_length=100) - slug = models.SlugField(unique=True) - content = models.TextField(blank=True) - is_active = models.BooleanField(default=True) - created = models.DateTimeField(auto_now_add=now, null=True) - updated = models.DateTimeField(auto_now=now) - - def __unicode__(self): - return self.title - - -class Counter(models.Model): - name = models.CharField(max_length=30) - slug = models.SlugField(unique=True) - count = models.PositiveIntegerField(default=0) - - def __unicode__(self): - return self.name - diff -Nru django-tastypie-0.12.0/tests/core/tests/api.py django-tastypie-0.13.3/tests/core/tests/api.py --- django-tastypie-0.12.0/tests/core/tests/api.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/api.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,184 +0,0 @@ -from django.contrib.auth.models import User -from django.http import HttpRequest -from django.test import TestCase -from tastypie.api import Api -from tastypie.exceptions import NotRegistered, BadRequest -from tastypie.resources import Resource, ModelResource -from tastypie.serializers import Serializer -from core.models import Note - - -class NoteResource(ModelResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - - -class ApiTestCase(TestCase): - urls = 'core.tests.api_urls' - - def test_register(self): - # NOTE: these have all been registered in core.tests.api_urls - api = Api() - self.assertEqual(len(api._registry), 0) - - api.register(NoteResource()) - self.assertEqual(len(api._registry), 1) - self.assertEqual(sorted(api._registry.keys()), ['notes']) - - api.register(UserResource()) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - - api.register(UserResource()) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - - self.assertEqual(len(api._canonicals), 2) - api.register(UserResource(), canonical=False) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - self.assertEqual(len(api._canonicals), 2) - - def test_global_registry(self): - api = Api() - self.assertEqual(len(api._registry), 0) - - api.register(NoteResource()) - self.assertEqual(len(api._registry), 1) - self.assertEqual(sorted(api._registry.keys()), ['notes']) - - api.register(UserResource()) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - - api.register(UserResource()) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - - self.assertEqual(len(api._canonicals), 2) - api.register(UserResource(), canonical=False) - self.assertEqual(len(api._registry), 2) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - self.assertEqual(len(api._canonicals), 2) - - def test_unregister(self): - api = Api() - api.register(NoteResource()) - api.register(UserResource(), canonical=False) - self.assertEqual(sorted(api._registry.keys()), ['notes', 'users']) - - self.assertEqual(len(api._canonicals), 1) - api.unregister('users') - self.assertEqual(len(api._registry), 1) - self.assertEqual(sorted(api._registry.keys()), ['notes']) - self.assertEqual(len(api._canonicals), 1) - - api.unregister('notes') - self.assertEqual(len(api._registry), 0) - self.assertEqual(sorted(api._registry.keys()), []) - - api.unregister('users') - self.assertEqual(len(api._registry), 0) - self.assertEqual(sorted(api._registry.keys()), []) - - def test_canonical_resource_for(self): - api = Api() - note_resource = NoteResource() - user_resource = UserResource() - api.register(note_resource) - api.register(user_resource) - self.assertEqual(len(api._canonicals), 2) - - self.assertEqual(isinstance(api.canonical_resource_for('notes'), NoteResource), True) - - api_2 = Api() - api.unregister(user_resource._meta.resource_name) - self.assertRaises(NotRegistered, api.canonical_resource_for, 'users') - - def test_urls(self): - api = Api() - api.register(NoteResource()) - api.register(UserResource()) - - patterns = api.urls - self.assertEqual(len(patterns), 3) - self.assertEqual(sorted([pattern.name for pattern in patterns if hasattr(pattern, 'name')]), ['api_v1_top_level']) - self.assertEqual([[pattern.name for pattern in include.url_patterns if hasattr(pattern, 'name')] for include in patterns if hasattr(include, 'reverse_dict')], [['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail'], ['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail']]) - - api = Api(api_name='v2') - api.register(NoteResource()) - api.register(UserResource()) - - patterns = api.urls - self.assertEqual(len(patterns), 3) - self.assertEqual(sorted([pattern.name for pattern in patterns if hasattr(pattern, 'name')]), ['api_v2_top_level']) - self.assertEqual([[pattern.name for pattern in include.url_patterns if hasattr(pattern, 'name')] for include in patterns if hasattr(include, 'reverse_dict')], [['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail'], ['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail']]) - - def test_top_level(self): - api = Api() - api.register(NoteResource()) - api.register(UserResource()) - request = HttpRequest() - - resp = api.top_level(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"notes": {"list_endpoint": "/api/v1/notes/", "schema": "/api/v1/notes/schema/"}, "users": {"list_endpoint": "/api/v1/users/", "schema": "/api/v1/users/schema/"}}') - - def test_top_level_jsonp(self): - api = Api() - api.register(NoteResource()) - api.register(UserResource()) - request = HttpRequest() - request.META = {'HTTP_ACCEPT': 'text/javascript'} - request.GET = {'callback': 'foo'} - - resp = api.top_level(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp['content-type'].split(';')[0], 'text/javascript') - self.assertEqual(resp.content.decode('utf-8'), 'foo({"notes": {"list_endpoint": "/api/v1/notes/", "schema": "/api/v1/notes/schema/"}, "users": {"list_endpoint": "/api/v1/users/", "schema": "/api/v1/users/schema/"}})') - - request = HttpRequest() - request.META = {'HTTP_ACCEPT': 'text/javascript'} - request.GET = {'callback': ''} - - try: - resp = api.top_level(request) - self.fail("Broken callback didn't fail!") - except BadRequest: - # Regression: We expect this, which is fine, but this used to - # be an import error. - pass - - def test_custom_api_serializer(self): - """Confirm that an Api can use a custom serializer""" - - # Origin: https://github.com/toastdriven/django-tastypie/pull/817 - - class JSONSerializer(Serializer): - formats = ('json', ) - - api = Api(serializer_class=JSONSerializer) - api.register(NoteResource()) - - request = HttpRequest() - request.META = {'HTTP_ACCEPT': 'text/javascript'} - - resp = api.top_level(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp['content-type'], 'application/json', - msg="Expected application/json response but received %s" % resp['content-type']) - - request = HttpRequest() - request.META = {'HTTP_ACCEPT': 'application/xml'} - - resp = api.top_level(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp['content-type'], 'application/json', - msg="Expected application/json response but received %s" % resp['content-type']) diff -Nru django-tastypie-0.12.0/tests/core/tests/api_urls.py django-tastypie-0.13.3/tests/core/tests/api_urls.py --- django-tastypie-0.12.0/tests/core/tests/api_urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/api_urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -from core.tests.api import Api, NoteResource, UserResource - - -api = Api() -api.register(NoteResource()) -api.register(UserResource()) - -urlpatterns = patterns('', - (r'^api/', include(api.urls)), -) diff -Nru django-tastypie-0.12.0/tests/core/tests/authentication.py django-tastypie-0.13.3/tests/core/tests/authentication.py --- django-tastypie-0.12.0/tests/core/tests/authentication.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/authentication.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,587 +0,0 @@ -import base64 -import os -import time -import unittest -import warnings -from django.conf import settings -from django.contrib.auth.models import AnonymousUser, User -from django.http import HttpRequest -from django.test import TestCase -from django.test.testcases import skipIf -from tastypie.authentication import Authentication, BasicAuthentication, ApiKeyAuthentication, SessionAuthentication, DigestAuthentication, OAuthAuthentication, MultiAuthentication -from tastypie.http import HttpUnauthorized -from tastypie.models import ApiKey, create_api_key - - -# Be tricky. -from tastypie.authentication import python_digest, oauth2, oauth_provider - -if python_digest is None: - warnings.warn("Running tests without python_digest! Bad news!") -if oauth2 is None: - warnings.warn("Running tests without oauth2! Bad news!") -if oauth_provider is None: - warnings.warn("Running tests without oauth_provider! Bad news!") - - -class AuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_is_authenticated(self): - auth = Authentication() - request = HttpRequest() - # Doesn't matter. Always true. - self.assertTrue(auth.is_authenticated(None)) - self.assertTrue(auth.is_authenticated(request)) - - def test_get_identifier(self): - auth = Authentication() - request = HttpRequest() - self.assertEqual(auth.get_identifier(request), 'noaddr_nohost') - - request = HttpRequest() - request.META['REMOTE_ADDR'] = '127.0.0.1' - request.META['REMOTE_HOST'] = 'nebula.local' - self.assertEqual(auth.get_identifier(request), '127.0.0.1_nebula.local') - - def test_check_active_false(self): - auth = Authentication(require_active=False) - user = User.objects.get(username='johndoe') - self.assertTrue(auth.check_active(user)) - - auth = Authentication(require_active=False) - user = User.objects.get(username='bobdoe') - self.assertTrue(auth.check_active(user)) - - def test_check_active_true(self): - auth = Authentication(require_active=True) - user = User.objects.get(username='johndoe') - self.assertTrue(auth.check_active(user)) - - auth = Authentication(require_active=True) - user = User.objects.get(username='bobdoe') - self.assertFalse(auth.check_active(user)) - - # Check the default. - auth = Authentication() - user = User.objects.get(username='bobdoe') - self.assertFalse(auth.check_active(user)) - - -class BasicAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_is_authenticated(self): - auth = BasicAuthentication() - request = HttpRequest() - - # No HTTP Basic auth details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # HttpUnauthorized with auth type and realm - self.assertEqual(auth.is_authenticated(request)['WWW-Authenticate'], 'Basic Realm="django-tastypie"') - - # Wrong basic auth details. - request.META['HTTP_AUTHORIZATION'] = 'abcdefg' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # No password. - request.META['HTTP_AUTHORIZATION'] = base64.b64encode('daniel'.encode('utf-8')).decode('utf-8') - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong user/password. - request.META['HTTP_AUTHORIZATION'] = base64.b64encode('daniel:pass'.encode('utf-8')).decode('utf-8') - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Correct user/password. - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass'.encode('utf-8')).decode('utf-8') - self.assertEqual(auth.is_authenticated(request), True) - - # Regression: Password with colon. - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass:word') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass:word'.encode('utf-8')).decode('utf-8') - self.assertEqual(auth.is_authenticated(request), True) - - # Capitalization shouldn't matter. - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass:word') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'bAsIc %s' % base64.b64encode('johndoe:pass:word'.encode('utf-8')).decode('utf-8') - self.assertEqual(auth.is_authenticated(request), True) - - def test_check_active_true(self): - auth = BasicAuthentication() - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - bob_doe.set_password('pass') - bob_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('bobdoe:pass'.encode('utf-8')).decode('utf-8') - self.assertFalse(auth.is_authenticated(request)) - - def test_check_active_false(self): - auth = BasicAuthentication(require_active=False) - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - bob_doe.set_password('pass') - bob_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('bobdoe:pass'.encode('utf-8')).decode('utf-8') - self.assertTrue(auth.is_authenticated(request)) - - -class ApiKeyAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def setUp(self): - super(ApiKeyAuthenticationTestCase, self).setUp() - ApiKey.objects.all().delete() - - def test_is_authenticated_get_params(self): - auth = ApiKeyAuthentication() - request = HttpRequest() - - # Simulate sending the signal. - john_doe = User.objects.get(username='johndoe') - create_api_key(User, instance=john_doe, created=True) - - # No username/api_key details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong username details. - request.GET['username'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # No api_key. - request.GET['username'] = 'daniel' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong user/api_key. - request.GET['username'] = 'daniel' - request.GET['api_key'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Correct user/api_key. - john_doe = User.objects.get(username='johndoe') - request.GET['username'] = 'johndoe' - request.GET['api_key'] = john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'johndoe') - - def test_is_authenticated_header(self): - auth = ApiKeyAuthentication() - request = HttpRequest() - - # Simulate sending the signal. - john_doe = User.objects.get(username='johndoe') - create_api_key(User, instance=john_doe, created=True) - - # No username/api_key details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong username details. - request.META['HTTP_AUTHORIZATION'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # No api_key. - request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong user/api_key. - request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel:pass' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Correct user/api_key. - john_doe = User.objects.get(username='johndoe') - request.META['HTTP_AUTHORIZATION'] = 'ApiKey johndoe:%s' % john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - - # Capitalization shouldn't matter. - john_doe = User.objects.get(username='johndoe') - request.META['HTTP_AUTHORIZATION'] = 'aPiKeY johndoe:%s' % john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - - def test_check_active_true(self): - auth = ApiKeyAuthentication() - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - create_api_key(User, instance=bob_doe, created=True) - request.META['HTTP_AUTHORIZATION'] = 'ApiKey bobdoe:%s' % bob_doe.api_key.key - self.assertFalse(auth.is_authenticated(request)) - - def test_check_active_false(self): - auth = BasicAuthentication(require_active=False) - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - create_api_key(User, instance=bob_doe, created=True) - request.META['HTTP_AUTHORIZATION'] = 'ApiKey bobdoe:%s' % bob_doe.api_key.key - self.assertTrue(auth.is_authenticated(request)) - - -class SessionAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_is_authenticated(self): - auth = SessionAuthentication() - request = HttpRequest() - request.method = 'POST' - request.COOKIES = { - settings.CSRF_COOKIE_NAME: 'abcdef1234567890abcdef1234567890' - } - - # No CSRF token. - request.META = {} - self.assertFalse(auth.is_authenticated(request)) - - # Invalid CSRF token. - request.META = { - 'HTTP_X_CSRFTOKEN': 'abc123' - } - self.assertFalse(auth.is_authenticated(request)) - - # Not logged in. - request.META = { - 'HTTP_X_CSRFTOKEN': 'abcdef1234567890abcdef1234567890' - } - request.user = AnonymousUser() - self.assertFalse(auth.is_authenticated(request)) - - # Logged in. - request.user = User.objects.get(username='johndoe') - self.assertTrue(auth.is_authenticated(request)) - - # Logged in (with GET & no token). - request.method = 'GET' - request.META = {} - request.user = User.objects.get(username='johndoe') - self.assertTrue(auth.is_authenticated(request)) - - # Secure & wrong referrer. - os.environ["HTTPS"] = "on" - request.method = 'POST' - request.META = { - 'HTTP_X_CSRFTOKEN': 'abcdef1234567890abcdef1234567890' - } - request.META['HTTP_HOST'] = 'example.com' - request.META['HTTP_REFERER'] = '' - self.assertFalse(auth.is_authenticated(request)) - - # Secure & correct referrer. - request.META['HTTP_REFERER'] = 'https://example.com/' - self.assertTrue(auth.is_authenticated(request)) - - os.environ["HTTPS"] = "off" - - def test_get_identifier(self): - auth = SessionAuthentication() - request = HttpRequest() - - # Not logged in. - request.user = AnonymousUser() - self.assertEqual(auth.get_identifier(request), '') - - # Logged in. - request.user = User.objects.get(username='johndoe') - self.assertEqual(auth.get_identifier(request), 'johndoe') - -@skipIf(python_digest is None, "python-digest is not installed") -class DigestAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def setUp(self): - super(DigestAuthenticationTestCase, self).setUp() - ApiKey.objects.all().delete() - - def test_is_authenticated(self): - auth = DigestAuthentication() - request = HttpRequest() - - # Simulate sending the signal. - john_doe = User.objects.get(username='johndoe') - create_api_key(User, instance=john_doe, created=True) - - # No HTTP Basic auth details should fail. - auth_request = auth.is_authenticated(request) - self.assertEqual(isinstance(auth_request, HttpUnauthorized), True) - - # HttpUnauthorized with auth type and realm - self.assertEqual(auth_request['WWW-Authenticate'].find('Digest'), 0) - self.assertEqual(auth_request['WWW-Authenticate'].find(' realm="django-tastypie"') > 0, True) - self.assertEqual(auth_request['WWW-Authenticate'].find(' opaque=') > 0, True) - self.assertEqual(auth_request['WWW-Authenticate'].find('nonce=') > 0, True) - - # Wrong basic auth details. - request.META['HTTP_AUTHORIZATION'] = 'abcdefg' - auth_request = auth.is_authenticated(request) - self.assertEqual(isinstance(auth_request, HttpUnauthorized), True) - - # No password. - request.META['HTTP_AUTHORIZATION'] = base64.b64encode('daniel'.encode('utf-8')).decode('utf-8') - auth_request = auth.is_authenticated(request) - self.assertEqual(isinstance(auth_request, HttpUnauthorized), True) - - # Wrong user/password. - request.META['HTTP_AUTHORIZATION'] = base64.b64encode('daniel:pass'.encode('utf-8')).decode('utf-8') - auth_request = auth.is_authenticated(request) - self.assertEqual(isinstance(auth_request, HttpUnauthorized), True) - - # Correct user/password. - john_doe = User.objects.get(username='johndoe') - request.META['HTTP_AUTHORIZATION'] = python_digest.build_authorization_request( - username=john_doe.username, - method=request.method, - uri='/', - nonce_count=1, - digest_challenge=python_digest.parse_digest_challenge(auth_request['WWW-Authenticate']), - password=john_doe.api_key.key - ) - auth_request = auth.is_authenticated(request) - self.assertEqual(auth_request, True) - - def test_check_active_true(self): - auth = DigestAuthentication() - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - create_api_key(User, instance=bob_doe, created=True) - auth_request = auth.is_authenticated(request) - request.META['HTTP_AUTHORIZATION'] = python_digest.build_authorization_request( - username=bob_doe.username, - method=request.method, - uri='/', - nonce_count=1, - digest_challenge=python_digest.parse_digest_challenge(auth_request['WWW-Authenticate']), - password=bob_doe.api_key.key - ) - auth_request = auth.is_authenticated(request) - self.assertFalse(auth_request) - - def test_check_active_false(self): - auth = DigestAuthentication(require_active=False) - request = HttpRequest() - - bob_doe = User.objects.get(username='bobdoe') - create_api_key(User, instance=bob_doe, created=True) - auth_request = auth.is_authenticated(request) - request.META['HTTP_AUTHORIZATION'] = python_digest.build_authorization_request( - username=bob_doe.username, - method=request.method, - uri='/', - nonce_count=1, - digest_challenge=python_digest.parse_digest_challenge(auth_request['WWW-Authenticate']), - password=bob_doe.api_key.key - ) - auth_request = auth.is_authenticated(request) - self.assertTrue(auth_request, True) - - -@skipIf(not oauth2 or not oauth_provider, "oauth provider not installed") -class OAuthAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def setUp(self): - super(OAuthAuthenticationTestCase, self).setUp() - - self.request = HttpRequest() - self.request.META['SERVER_NAME'] = 'testsuite' - self.request.META['SERVER_PORT'] = '8080' - self.request.REQUEST = self.request.GET = {} - self.request.method = "GET" - - from oauth_provider.models import Consumer, Token, Resource - self.user = User.objects.create_user('daniel', 'test@example.com', 'password') - self.user_inactive = User.objects.get(username='bobdoe') - self.resource, _ = Resource.objects.get_or_create(url='test', defaults={ - 'name': 'Test Resource' - }) - self.consumer, _ = Consumer.objects.get_or_create(key='123', defaults={ - 'name': 'Test', - 'description': 'Testing...' - }) - self.token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={ - 'consumer': self.consumer, - 'resource': self.resource, - 'secret': '', - 'user': self.user, - }) - self.token_inactive, _ = Token.objects.get_or_create(key='bar', token_type=Token.ACCESS, defaults={ - 'consumer': self.consumer, - 'resource': self.resource, - 'secret': '', - 'user': self.user_inactive, - }) - - def test_is_authenticated(self): - auth = OAuthAuthentication() - - # Invalid request. - resp = auth.is_authenticated(self.request) - self.assertEqual(resp.status_code, 401) - - # No username/api_key details should fail. - self.request.REQUEST = self.request.GET = { - 'oauth_consumer_key': '123', - 'oauth_nonce': 'abc', - 'oauth_signature': '&', - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_timestamp': str(int(time.time())), - 'oauth_token': 'foo', - } - self.request.META['Authorization'] = 'OAuth ' + ','.join([key+'='+value for key, value in self.request.REQUEST.items()]) - resp = auth.is_authenticated(self.request) - self.assertEqual(resp, True) - self.assertEqual(self.request.user.pk, self.user.pk) - - def test_check_active_true(self): - auth = OAuthAuthentication() - - # No username/api_key details should fail. - self.request.REQUEST = self.request.GET = { - 'oauth_consumer_key': '123', - 'oauth_nonce': 'abc', - 'oauth_signature': '&', - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_timestamp': str(int(time.time())), - 'oauth_token': 'bar', - } - self.request.META['Authorization'] = 'OAuth ' + ','.join([key+'='+value for key, value in self.request.REQUEST.items()]) - resp = auth.is_authenticated(self.request) - self.assertFalse(resp) - - def test_check_active_false(self): - auth = OAuthAuthentication(require_active=False) - - # No username/api_key details should fail. - self.request.REQUEST = self.request.GET = { - 'oauth_consumer_key': '123', - 'oauth_nonce': 'abc', - 'oauth_signature': '&', - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_timestamp': str(int(time.time())), - 'oauth_token': 'bar', - } - self.request.META['Authorization'] = 'OAuth ' + ','.join([key+'='+value for key, value in self.request.REQUEST.items()]) - resp = auth.is_authenticated(self.request) - self.assertTrue(resp) - self.assertEqual(self.request.user.pk, self.user_inactive.pk) - - -class MultiAuthenticationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_apikey_and_authentication_enforce_user(self): - session_auth = SessionAuthentication() - api_key_auth = ApiKeyAuthentication() - auth = MultiAuthentication(api_key_auth, session_auth) - john_doe = User.objects.get(username='johndoe') - request1 = HttpRequest() - request2 = HttpRequest() - request3 = HttpRequest() - - request1.method = 'POST' - request1.META = { - 'HTTP_X_CSRFTOKEN': 'abcdef1234567890abcdef1234567890' - } - request1.COOKIES = { - settings.CSRF_COOKIE_NAME: 'abcdef1234567890abcdef1234567890' - } - request1.user = john_doe - - request2.POST['username'] = 'janedoe' - request2.POST['api_key'] = 'invalid key' - - request3.method = 'POST' - request3.META = { - 'HTTP_X_CSRFTOKEN': 'abcdef1234567890abcdef1234567890' - } - request3.COOKIES = { - settings.CSRF_COOKIE_NAME: 'abcdef1234567890abcdef1234567890' - } - request3.user = john_doe - request3.POST['username'] = 'janedoe' - request3.POST['api_key'] = 'invalid key' - - #session auth should pass if since john_doe is logged in - self.assertTrue(session_auth.is_authenticated(request1)) - #api key auth should fail because of invalid api key - self.assertEqual(isinstance(api_key_auth.is_authenticated(request2), HttpUnauthorized), True) - - #multi auth shouldn't change users if api key auth fails - #multi auth passes since session auth is valid - self.assertEqual(request3.user.username, 'johndoe') - self.assertTrue(auth.is_authenticated(request3)) - self.assertEqual(request3.user.username, 'johndoe') - - def test_apikey_and_authentication(self): - auth = MultiAuthentication(ApiKeyAuthentication(), Authentication()) - request = HttpRequest() - - john_doe = User.objects.get(username='johndoe') - - # No username/api_key details should pass. - self.assertEqual(auth.is_authenticated(request), True) - - # The identifier should be the basic auth stock. - self.assertEqual(auth.get_identifier(request), 'noaddr_nohost') - - # Wrong username details. - request = HttpRequest() - request.GET['username'] = 'foo' - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'noaddr_nohost') - - # No api_key. - request = HttpRequest() - request.GET['username'] = 'daniel' - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'noaddr_nohost') - - # Wrong user/api_key. - request = HttpRequest() - request.GET['username'] = 'daniel' - request.GET['api_key'] = 'foo' - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'noaddr_nohost') - - request = HttpRequest() - request.GET['username'] = 'johndoe' - request.GET['api_key'] = john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'johndoe') - - - def test_apikey_and_basic_auth(self): - auth = MultiAuthentication(BasicAuthentication(), ApiKeyAuthentication()) - request = HttpRequest() - john_doe = User.objects.get(username='johndoe') - - # No API Key or HTTP Basic auth details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Basic Auth still returns appropriately. - self.assertEqual(auth.is_authenticated(request)['WWW-Authenticate'], 'Basic Realm="django-tastypie"') - - # API Key Auth works. - request = HttpRequest() - request.GET['username'] = 'johndoe' - request.GET['api_key'] = john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), 'johndoe') - - - # Basic Auth works. - request = HttpRequest() - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass'.encode('utf-8')).decode('utf-8') - self.assertEqual(auth.is_authenticated(request), True) - - diff -Nru django-tastypie-0.12.0/tests/core/tests/authorization.py django-tastypie-0.13.3/tests/core/tests/authorization.py --- django-tastypie-0.12.0/tests/core/tests/authorization.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/authorization.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,240 +0,0 @@ -from django.test import TestCase -from django.http import HttpRequest -from django.contrib.auth.models import User, Permission -from core.models import Note -from tastypie.authorization import Authorization, ReadOnlyAuthorization, DjangoAuthorization -from tastypie.exceptions import Unauthorized -from tastypie import fields -from tastypie.resources import Resource, ModelResource - - -class NoRulesNoteResource(ModelResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - authorization = Authorization() - - -class ReadOnlyNoteResource(ModelResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - authorization = ReadOnlyAuthorization() - - -class DjangoNoteResource(ModelResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - authorization = DjangoAuthorization() - - -class NotAModel(object): - name = 'Foo' - - -class NotAModelResource(Resource): - name = fields.CharField(attribute='name') - - class Meta: - resource_name = 'notamodel' - object_class = NotAModel - authorization = DjangoAuthorization() - - -class AuthorizationTestCase(TestCase): - fixtures = ['note_testdata'] - - def test_no_rules(self): - request = HttpRequest() - resource = NoRulesNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertRaises(NotImplementedError, auth.create_list, resource.get_object_list(bundle.request), bundle) - self.assertTrue(auth.create_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.update_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.delete_detail(resource.get_object_list(bundle.request)[0], bundle)) - - def test_read_only(self): - request = HttpRequest() - resource = ReadOnlyNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.create_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.update_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.delete_detail, resource.get_object_list(bundle.request)[0], bundle) - - -class DjangoAuthorizationTestCase(TestCase): - fixtures = ['note_testdata'] - - def setUp(self): - super(DjangoAuthorizationTestCase, self).setUp() - self.add = Permission.objects.get_by_natural_key('add_note', 'core', 'note') - self.change = Permission.objects.get_by_natural_key('change_note', 'core', 'note') - self.delete = Permission.objects.get_by_natural_key('delete_note', 'core', 'note') - self.user = User.objects.all()[0] - self.user.user_permissions.clear() - - def test_no_perms(self): - # sanity check: user has no permissions - self.assertFalse(self.user.get_all_permissions()) - - request = HttpRequest() - request.user = self.user - # with no permissions, api is read-only - resource = DjangoNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.create_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.update_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.delete_detail, resource.get_object_list(bundle.request)[0], bundle) - - def test_add_perm(self): - request = HttpRequest() - request.user = self.user - - # give add permission - request.user.user_permissions.add(self.add) - - request = HttpRequest() - request.user = self.user - resource = DjangoNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.create_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.update_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.delete_detail, resource.get_object_list(bundle.request)[0], bundle) - - def test_change_perm(self): - request = HttpRequest() - request.user = self.user - - # give change permission - request.user.user_permissions.add(self.change) - - resource = DjangoNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.create_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.update_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.delete_detail, resource.get_object_list(bundle.request)[0], bundle) - - def test_delete_perm(self): - request = HttpRequest() - request.user = self.user - - # give delete permission - request.user.user_permissions.add(self.delete) - - resource = DjangoNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.create_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 0) - self.assertRaises(Unauthorized, auth.update_detail, resource.get_object_list(bundle.request)[0], bundle) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.delete_detail(resource.get_object_list(bundle.request)[0], bundle)) - - def test_all(self): - request = HttpRequest() - request.user = self.user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - resource = DjangoNoteResource() - auth = resource._meta.authorization - bundle = resource.build_bundle(request=request) - - bundle.request.method = 'GET' - self.assertEqual(len(auth.read_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.read_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'POST' - self.assertEqual(len(auth.create_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.create_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'PUT' - self.assertEqual(len(auth.update_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.update_detail(resource.get_object_list(bundle.request)[0], bundle)) - - bundle.request.method = 'DELETE' - self.assertEqual(len(auth.delete_list(resource.get_object_list(bundle.request), bundle)), 4) - self.assertTrue(auth.delete_detail(resource.get_object_list(bundle.request)[0], bundle)) diff -Nru django-tastypie-0.12.0/tests/core/tests/cache.py django-tastypie-0.13.3/tests/core/tests/cache.py --- django-tastypie-0.12.0/tests/core/tests/cache.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/cache.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -import mock -import time - -from django.core.cache import cache -from django.test import TestCase - -from tastypie.cache import NoCache, SimpleCache - - -class NoCacheTestCase(TestCase): - def tearDown(self): - cache.delete('foo') - cache.delete('moof') - super(NoCacheTestCase, self).tearDown() - - def test_get(self): - cache.set('foo', 'bar', 60) - cache.set('moof', 'baz', 1) - - no_cache = NoCache() - self.assertEqual(no_cache.get('foo'), None) - self.assertEqual(no_cache.get('moof'), None) - self.assertEqual(no_cache.get(''), None) - - def test_set(self): - no_cache = NoCache() - no_cache.set('foo', 'bar') - no_cache.set('moof', 'baz', timeout=1) - - # Use the underlying cache system to verify. - self.assertEqual(cache.get('foo'), None) - self.assertEqual(cache.get('moof'), None) - - -class SimpleCacheTestCase(TestCase): - def tearDown(self): - cache.delete('foo') - cache.delete('moof') - super(SimpleCacheTestCase, self).tearDown() - - def test_get(self): - cache.set('foo', 'bar', 60) - cache.set('moof', 'baz', 1) - - simple_cache = SimpleCache() - self.assertEqual(simple_cache.get('foo'), 'bar') - self.assertEqual(simple_cache.get('moof'), 'baz') - self.assertEqual(simple_cache.get(''), None) - - def test_set(self): - simple_cache = SimpleCache(timeout=1) - - with mock.patch.object(simple_cache, 'cache', mock.Mock(wraps=simple_cache.cache)) as mocked_cache: - simple_cache.set('foo', 'bar', timeout=10) - simple_cache.set('moof', 'baz') - - # Use the underlying cache system to verify. - self.assertEqual(cache.get('foo'), 'bar') - self.assertEqual(cache.get('moof'), 'baz') - - # make sure cache was called with correct timeouts. - self.assertEqual(mocked_cache.set.call_args_list[0][0][2], 10) - self.assertEqual(mocked_cache.set.call_args_list[1][0][2], 1) diff -Nru django-tastypie-0.12.0/tests/core/tests/commands.py django-tastypie-0.13.3/tests/core/tests/commands.py --- django-tastypie-0.12.0/tests/core/tests/commands.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/commands.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -from tastypie.compat import get_user_model -from django.core.management import call_command -from django.db import models -from django.test import TestCase -from tastypie.models import ApiKey, create_api_key - - -class BackfillApiKeysTestCase(TestCase): - def setUp(self): - super(BackfillApiKeysTestCase, self).setUp() - self.User = get_user_model() - - # Disconnect the signal to prevent automatic key generation. - models.signals.post_save.disconnect(create_api_key, sender=self.User) - - def tearDown(self): - # Reconnect the signal. - models.signals.post_save.connect(create_api_key, sender=self.User) - super(BackfillApiKeysTestCase, self).tearDown() - - def test_command(self): - self.assertEqual(ApiKey.objects.count(), 0) - - # Create a new User that ought not to have an API key. - new_user = self.User.objects.create_user(username='mr_pants', password='password', email='mister@pants.com') - - self.assertEqual(ApiKey.objects.count(), 0) - - try: - ApiKey.objects.get(user=new_user) - self.fail('Wha? The user mysteriously has a key? WTF?') - except ApiKey.DoesNotExist: - pass - - call_command('backfill_api_keys', verbosity=0) - self.assertEqual(ApiKey.objects.count(), 1) - - try: - api_key = ApiKey.objects.get(user=new_user) - except ApiKey.DoesNotExist: - self.fail("No key means the command didn't work.") diff -Nru django-tastypie-0.12.0/tests/core/tests/fields.py django-tastypie-0.13.3/tests/core/tests/fields.py --- django-tastypie-0.12.0/tests/core/tests/fields.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/fields.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,1341 +0,0 @@ -import datetime -from dateutil.tz import * -from django.db import models -from django.contrib.auth.models import User -from django.test import TestCase -from django.http import HttpRequest -from tastypie.bundle import Bundle -from tastypie.exceptions import ApiFieldError, NotFound -from tastypie.fields import * -from tastypie.resources import ModelResource -from core.models import Note, Subject, MediaBit -from core.tests.mocks import MockRequest - -from tastypie.utils import aware_datetime, aware_date - - -class ApiFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = ApiField() - self.assertEqual(field_1.instance_name, None) - self.assertEqual(field_1.attribute, None) - self.assertEqual(field_1._default, NOT_PROVIDED) - self.assertEqual(field_1.null, False) - self.assertEqual(field_1.value, None) - self.assertEqual(field_1.help_text, '') - self.assertEqual(field_1.use_in, 'all') - - field_2 = ApiField(attribute='foo', default=True, null=True, readonly=True, help_text='Foo.', use_in="foo") - self.assertEqual(field_2.instance_name, None) - self.assertEqual(field_2.attribute, 'foo') - self.assertEqual(field_2._default, True) - self.assertEqual(field_2.null, True) - self.assertEqual(field_2.value, None) - self.assertEqual(field_2.readonly, True) - self.assertEqual(field_2.help_text, 'Foo.') - self.assertEqual(field_1.use_in, 'all') - - field_3 = ApiField(use_in="list") - self.assertEqual(field_3.use_in, 'list') - - field_4 = ApiField(use_in="detail") - self.assertEqual(field_4.use_in, 'detail') - - use_in_callable = lambda x: True - field_5 = ApiField(use_in=use_in_callable) - self.assertTrue(field_5.use_in is use_in_callable) - - def test_dehydrated_type(self): - field_1 = ApiField() - self.assertEqual(field_1.dehydrated_type, 'string') - - def test_has_default(self): - field_1 = ApiField() - self.assertEqual(field_1.has_default(), False) - - field_2 = ApiField(default=True) - self.assertEqual(field_2.has_default(), True) - - def test_default(self): - field_1 = ApiField() - self.assertEqual(isinstance(field_1.default, NOT_PROVIDED), True) - - field_2 = ApiField(default=True) - self.assertEqual(field_2.default, True) - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - # With no attribute or default, we should get ``None``. - field_1 = ApiField() - self.assertEqual(field_1.dehydrate(bundle), None) - - # Still no attribute, so we should pick up the default - field_2 = ApiField(default=True) - self.assertEqual(field_2.dehydrate(bundle), True) - - # Wrong attribute should yield default. - field_3 = ApiField(attribute='foo', default=True) - self.assertEqual(field_3.dehydrate(bundle), True) - - # Wrong attribute should yield null. - field_4 = ApiField(attribute='foo', null=True) - self.assertEqual(field_4.dehydrate(bundle), None) - - # Correct attribute. - field_5 = ApiField(attribute='title', default=True) - self.assertEqual(field_5.dehydrate(bundle), u'First Post!') - - # Correct callable attribute. - field_6 = ApiField(attribute='what_time_is_it', default=True) - self.assertEqual(field_6.dehydrate(bundle), aware_datetime(2010, 4, 1, 0, 48)) - - def test_convert(self): - field_1 = ApiField() - self.assertEqual(field_1.convert('foo'), 'foo') - self.assertEqual(field_1.convert(True), True) - - def test_hydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - # With no value, default or nullable, we should get an ``ApiFieldError``. - field_1 = ApiField() - field_1.instance_name = 'api' - self.assertRaises(ApiFieldError, field_1.hydrate, bundle) - - # The default. - field_2 = ApiField(default='foo') - field_2.instance_name = 'api' - self.assertEqual(field_2.hydrate(bundle), 'foo') - - # The callable default. - def foo(): - return 'bar' - - field_3 = ApiField(default=foo) - field_3.instance_name = 'api' - self.assertEqual(field_3.hydrate(bundle), 'bar') - - # The nullable case. - field_4 = ApiField(null=True) - field_4.instance_name = 'api' - self.assertEqual(field_4.hydrate(bundle), None) - - # The readonly case. - field_5 = ApiField(readonly=True) - field_5.instance_name = 'api' - bundle.data['api'] = 'abcdef' - self.assertEqual(field_5.hydrate(bundle), None) - - # A real, live attribute! - field_6 = ApiField(attribute='title') - field_6.instance_name = 'api' - bundle.data['api'] = note.title - self.assertEqual(field_6.hydrate(bundle), u'First Post!') - - # Make sure it uses attribute when there's no data - field_7 = ApiField(attribute='title') - field_7.instance_name = 'notinbundle' - self.assertEqual(field_7.hydrate(bundle), u'First Post!') - - # Make sure it falls back to instance name if there is no attribute - field_8 = ApiField() - field_8.instance_name = 'title' - self.assertEqual(field_8.hydrate(bundle), u'First Post!') - - # Attribute & null regression test. - # First, simulate data missing from the bundle & ``null=True``. - field_9 = ApiField(attribute='notinbundle', null=True) - field_9.instance_name = 'notinbundle' - self.assertEqual(field_9.hydrate(bundle), None) - # The do something in the bundle also with ``null=True``. - field_10 = ApiField(attribute='title', null=True) - field_10.instance_name = 'title' - self.assertEqual(field_10.hydrate(bundle), u'First Post!') - - # The blank case. - field_11 = ApiField(attribute='notinbundle', blank=True) - field_11.instance_name = 'notinbundle' - self.assertEqual(field_11.hydrate(bundle), None) - - bundle.data['title'] = note.title - field_12 = ApiField(attribute='title', blank=True) - field_12.instance_name = 'title' - self.assertEqual(field_12.hydrate(bundle), u'First Post!') - - -class CharFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = CharField() - self.assertEqual(field_1.help_text, 'Unicode string data. Ex: "Hello World"') - - field_2 = CharField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = CharField() - self.assertEqual(field_1.dehydrated_type, 'string') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = CharField(attribute='title', default=True) - self.assertEqual(field_1.dehydrate(bundle), u'First Post!') - - field_2 = CharField(default=20) - self.assertEqual(field_2.dehydrate(bundle), u'20') - - -class FileFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = FileField() - self.assertEqual(field_1.help_text, 'A file URL as a string. Ex: "http://media.example.com/media/photos/my_photo.jpg"') - - field_2 = FileField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = FileField() - self.assertEqual(field_1.dehydrated_type, 'string') - - def test_dehydrate(self): - bit = MediaBit.objects.get(pk=1) - bundle = Bundle(obj=bit) - - field_1 = FileField(attribute='image', default=True) - self.assertEqual(field_1.dehydrate(bundle), u'http://localhost:8080/media/lulz/catz.gif') - - field_2 = FileField(default='http://media.example.com/img/default_avatar.jpg') - self.assertEqual(field_2.dehydrate(bundle), u'http://media.example.com/img/default_avatar.jpg') - - bit = MediaBit.objects.get(pk=1) - bit.image = '' - bundle = Bundle(obj=bit) - - field_3 = FileField(attribute='image', default=True) - self.assertEqual(field_3.dehydrate(bundle), None) - - bit.image = None - bundle = Bundle(obj=bit) - - field_4 = FileField(attribute='image', null=True) - self.assertEqual(field_4.dehydrate(bundle), None) - - -class IntegerFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = IntegerField() - self.assertEqual(field_1.help_text, 'Integer data. Ex: 2673') - - field_2 = IntegerField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = IntegerField() - self.assertEqual(field_1.dehydrated_type, 'integer') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = IntegerField(default=25) - self.assertEqual(field_1.dehydrate(bundle), 25) - - field_2 = IntegerField(default='20') - self.assertEqual(field_2.dehydrate(bundle), 20) - - field_3 = IntegerField(default=18.5) - self.assertEqual(field_3.dehydrate(bundle), 18) - - -class FloatFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = FloatField() - self.assertEqual(field_1.help_text, 'Floating point numeric data. Ex: 26.73') - - field_2 = FloatField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = FloatField() - self.assertEqual(field_1.dehydrated_type, 'float') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = FloatField(default=20) - self.assertEqual(field_1.dehydrate(bundle), 20.0) - - field_2 = IntegerField(default=18.5) - self.assertEqual(field_2.dehydrate(bundle), 18) - - -class DecimalFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = DecimalField() - self.assertEqual(field_1.help_text, 'Fixed precision numeric data. Ex: 26.73') - - field_2 = DecimalField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = DecimalField() - self.assertEqual(field_1.dehydrated_type, 'decimal') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = DecimalField(default='20') - self.assertEqual(field_1.dehydrate(bundle), Decimal('20.0')) - - field_2 = DecimalField(default='18.5') - self.assertEqual(field_2.dehydrate(bundle), Decimal('18.5')) - - def test_hydrate(self): - bundle = Bundle(data={ - 'decimal-y': '18.50', - }) - - field_1 = DecimalField(default='20') - self.assertEqual(field_1.hydrate(bundle), Decimal('20.0')) - - field_2 = DecimalField(default='18.5') - self.assertEqual(field_2.hydrate(bundle), Decimal('18.5')) - - def test_model_resource_correct_association(self): - api_field = ModelResource.api_field_from_django_field(models.DecimalField()) - self.assertEqual(api_field, DecimalField) - - -class ListFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = ListField() - self.assertEqual(field_1.help_text, "A list of data. Ex: ['abc', 26.73, 8]") - - field_2 = ListField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = ListField() - self.assertEqual(field_1.dehydrated_type, 'list') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = ListField(default=[1, 2, 3]) - self.assertEqual(field_1.dehydrate(bundle), [1, 2, 3]) - - field_2 = ListField(default=['abc']) - self.assertEqual(field_2.dehydrate(bundle), ['abc']) - - -class DictFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = DictField() - self.assertEqual(field_1.help_text, "A dictionary of data. Ex: {'price': 26.73, 'name': 'Daniel'}") - - field_2 = DictField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = DictField() - self.assertEqual(field_1.dehydrated_type, 'dict') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = DictField(default={'price': 12.34, 'name': 'Daniel'}) - self.assertEqual(field_1.dehydrate(bundle), {'price': 12.34, 'name': 'Daniel'}) - - field_2 = DictField(default={'name': 'Daniel'}) - self.assertEqual(field_2.dehydrate(bundle), {'name': 'Daniel'}) - - -class BooleanFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = BooleanField() - self.assertEqual(field_1.help_text, 'Boolean data. Ex: True') - - field_2 = BooleanField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = BooleanField() - self.assertEqual(field_1.dehydrated_type, 'boolean') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = BooleanField(attribute='is_active', default=False) - self.assertEqual(field_1.dehydrate(bundle), True) - - field_2 = BooleanField(default=True) - self.assertEqual(field_2.dehydrate(bundle), True) - - -class TimeFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = TimeField() - self.assertEqual(field_1.help_text, 'A time as string. Ex: "20:05:23"') - field_2 = TimeField(help_text='Custom.') - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = TimeField() - self.assertEqual(field_1.dehydrated_type, 'time') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = TimeField(attribute='created') - self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5)) - - field_2 = TimeField(default=datetime.time(23, 5, 58)) - self.assertEqual(field_2.dehydrate(bundle), datetime.time(23, 5, 58)) - - field_3 = TimeField(attribute='created_string') - - note.created_string = '13:06:00' - self.assertEqual(field_3.dehydrate(bundle), datetime.time(13, 6)) - - note.created_string = '13:37:44' - self.assertEqual(field_3.dehydrate(bundle), datetime.time(13, 37, 44)) - - note.created_string = 'hello' - self.assertRaises(ApiFieldError, field_3.dehydrate, bundle) - - def test_hydrate(self): - bundle_1 = Bundle(data={'time': '03:49'}) - field_1 = TimeField(attribute='created') - field_1.instance_name = 'time' - self.assertEqual(field_1.hydrate(bundle_1), datetime.time(3, 49)) - - bundle_2 = Bundle() - field_2 = TimeField(default=datetime.time(17, 40)) - field_2.instance_name = 'doesnotmatter' # Wont find in bundle data - self.assertEqual(field_2.hydrate(bundle_2), datetime.time(17, 40)) - - bundle_3 = Bundle(data={'time': '22:08:11'}) - field_3 = TimeField(attribute='created_string') - field_3.instance_name = 'time' - self.assertEqual(field_3.hydrate(bundle_3), datetime.time(22, 8, 11)) - - bundle_4 = Bundle(data={'time': '07:45'}) - field_4 = TimeField(attribute='created') - field_4.instance_name = 'time' - self.assertEqual(field_4.hydrate(bundle_4), datetime.time(7, 45)) - - bundle_5 = Bundle(data={'time': None}) - field_5 = TimeField(attribute='created', null=True) - field_5.instance_name = 'time' - self.assertEqual(field_5.hydrate(bundle_5), None) - - -class DateFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = CharField() - self.assertEqual(field_1.help_text, 'Unicode string data. Ex: "Hello World"') - - field_2 = CharField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = DateField() - self.assertEqual(field_1.dehydrated_type, 'date') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = DateField(attribute='created') - self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5)) - - field_2 = DateField(default=datetime.date(2010, 4, 1)) - self.assertEqual(field_2.dehydrate(bundle), datetime.date(2010, 4, 1)) - - note.created_string = '2010-04-02' - field_3 = DateField(attribute='created_string') - self.assertEqual(field_3.dehydrate(bundle), datetime.date(2010, 4, 2)) - - def test_hydrate(self): - note = Note.objects.get(pk=1) - - bundle_1 = Bundle(data={ - 'date': '2010-05-12', - }) - field_1 = DateField(attribute='created') - field_1.instance_name = 'date' - self.assertEqual(field_1.hydrate(bundle_1), datetime.date(2010, 5, 12)) - - bundle_2 = Bundle() - field_2 = DateField(default=datetime.date(2010, 4, 1)) - field_2.instance_name = 'date' - self.assertEqual(field_2.hydrate(bundle_2), datetime.date(2010, 4, 1)) - - bundle_3 = Bundle(data={ - 'date': 'Wednesday, May 12, 2010', - }) - field_3 = DateField(attribute='created_string') - field_3.instance_name = 'date' - self.assertEqual(field_3.hydrate(bundle_3), datetime.date(2010, 5, 12)) - - bundle_4 = Bundle(data={ - 'date': '5 Apr 2010', - }) - field_4 = DateField(attribute='created') - field_4.instance_name = 'date' - self.assertEqual(field_4.hydrate(bundle_4), datetime.date(2010, 4, 5)) - - bundle_5 = Bundle(data={ - 'date': None, - }) - field_5 = DateField(attribute='created', null=True) - field_5.instance_name = 'date' - self.assertEqual(field_5.hydrate(bundle_5), None) - - -class DateTimeFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = CharField() - self.assertEqual(field_1.help_text, 'Unicode string data. Ex: "Hello World"') - - field_2 = CharField(help_text="Custom.") - self.assertEqual(field_2.help_text, 'Custom.') - - def test_dehydrated_type(self): - field_1 = DateTimeField() - self.assertEqual(field_1.dehydrated_type, 'datetime') - - def test_dehydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = DateTimeField(attribute='created') - self.assertEqual(field_1.dehydrate(bundle), aware_datetime(2010, 3, 30, 20, 5)) - - field_2 = DateTimeField(default=aware_datetime(2010, 4, 1, 1, 7)) - self.assertEqual(field_2.dehydrate(bundle), aware_datetime(2010, 4, 1, 1, 7)) - - note.created_string = '2010-04-02 01:11:00' - field_3 = DateTimeField(attribute='created_string') - self.assertEqual(field_3.dehydrate(bundle), aware_datetime(2010, 4, 2, 1, 11)) - - def test_hydrate(self): - note = Note.objects.get(pk=1) - - bundle_1 = Bundle(data={ - 'datetime': '2010-05-12 10:36:28', - }) - field_1 = DateTimeField(attribute='created') - field_1.instance_name = 'datetime' - self.assertEqual(field_1.hydrate(bundle_1), aware_datetime(2010, 5, 12, 10, 36, 28)) - - bundle_2 = Bundle() - field_2 = DateTimeField(default=aware_datetime(2010, 4, 1, 2, 0)) - field_2.instance_name = 'datetime' - self.assertEqual(field_2.hydrate(bundle_2), aware_datetime(2010, 4, 1, 2, 0)) - - bundle_3 = Bundle(data={ - 'datetime': 'Tue, 30 Mar 2010 20:05:00 -0500', - }) - field_3 = DateTimeField(attribute='created_string') - field_3.instance_name = 'datetime' - self.assertEqual(field_3.hydrate(bundle_3), aware_datetime(2010, 3, 30, 20, 5, tzinfo=tzoffset(None, -18000))) - - bundle_4 = Bundle(data={ - 'datetime': None, - }) - field_4 = DateField(attribute='created', null=True) - field_4.instance_name = 'datetime' - self.assertEqual(field_4.hydrate(bundle_4), None) - - bundle_5 = Bundle(data={'datetime': 'foo'}) - field_5 = DateTimeField() - field_5.instance_name = 'datetime' - self.assertRaises(ApiFieldError, field_5.hydrate, bundle_5) - - bundle_6 = Bundle(data={'datetime': ['a', 'list', 'used', 'to', 'crash']}) - field_6 = DateTimeField() - field_6.instance_name = 'datetime' - self.assertRaises(ApiFieldError, field_6.hydrate, bundle_6) - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/users/' - - return '/api/v1/users/%s/' % bundle_or_obj.obj.id - - -class ToOneFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_init(self): - field_1 = ToOneField(UserResource, 'author') - self.assertEqual(field_1.instance_name, None) - self.assertEqual(issubclass(field_1.to, UserResource), True) - self.assertEqual(field_1.attribute, 'author') - self.assertEqual(field_1.related_name, None) - self.assertEqual(field_1.null, False) - self.assertEqual(field_1.full, False) - self.assertEqual(field_1.readonly, False) - self.assertEqual(field_1.help_text, 'A single related resource. Can be either a URI or set of nested resource data.') - - field_2 = ToOneField(UserResource, 'author', null=True, help_text="Points to a User.") - self.assertEqual(field_2.instance_name, None) - self.assertEqual(issubclass(field_2.to, UserResource), True) - self.assertEqual(field_2.attribute, 'author') - self.assertEqual(field_2.related_name, None) - self.assertEqual(field_2.null, True) - self.assertEqual(field_2.full, False) - self.assertEqual(field_2.readonly, False) - self.assertEqual(field_2.help_text, 'Points to a User.') - - field_3 = ToOneField(UserResource, 'author', default=1, null=True, help_text="Points to a User.") - self.assertEqual(field_3.instance_name, None) - self.assertEqual(issubclass(field_3.to, UserResource), True) - self.assertEqual(field_3.attribute, 'author') - self.assertEqual(field_3.related_name, None) - self.assertEqual(field_3.null, True) - self.assertEqual(field_3.default, 1) - self.assertEqual(field_3.full, False) - self.assertEqual(field_3.readonly, False) - self.assertEqual(field_3.help_text, 'Points to a User.') - - field_4 = ToOneField(UserResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.") - self.assertEqual(field_4.instance_name, None) - self.assertEqual(issubclass(field_4.to, UserResource), True) - self.assertEqual(field_4.attribute, 'author') - self.assertEqual(field_4.related_name, None) - self.assertEqual(field_4.null, True) - self.assertEqual(field_4.default, 1) - self.assertEqual(field_4.full, False) - self.assertEqual(field_4.readonly, True) - self.assertEqual(field_4.help_text, 'Points to a User.') - - field_5 = ToOneField(UserResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="list") - self.assertEqual(field_5.use_in, 'list') - - field_6 = ToOneField(UserResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="detail") - self.assertEqual(field_6.use_in, 'detail') - - use_in_callable = lambda x: True - field_7 = ToOneField(UserResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in=use_in_callable) - self.assertTrue(field_7.use_in is use_in_callable) - - field_8 = ToOneField(UserResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="foo") - self.assertEqual(field_8.use_in, 'all') - - def test_dehydrated_type(self): - field_1 = ToOneField(UserResource, 'author') - self.assertEqual(field_1.dehydrated_type, 'related') - - def test_has_default(self): - field_1 = ToOneField(UserResource, 'author') - self.assertEqual(field_1.has_default(), False) - - field_1 = ToOneField(UserResource, 'author', default=1) - self.assertEqual(field_1.has_default(), True) - - def test_default(self): - field_1 = ToOneField(UserResource, 'author') - self.assertTrue(isinstance(field_1.default, NOT_PROVIDED)) - - field_2 = ToOneField(UserResource, 'author', default=1) - self.assertEqual(field_2.default, 1) - - def test_dehydrate(self): - note = Note() - bundle = Bundle(obj=note) - - field_1 = ToOneField(UserResource, 'author') - self.assertRaises(ApiFieldError, field_1.dehydrate, bundle) - - field_2 = ToOneField(UserResource, 'author', null=True) - self.assertEqual(field_2.dehydrate(bundle), None) - - note = Note.objects.get(pk=1) - request = MockRequest() - request.path = "/api/v1/notes/1/" - bundle = Bundle(obj=note, request=request) - - field_3 = ToOneField(UserResource, 'author') - self.assertEqual(field_3.dehydrate(bundle), '/api/v1/users/1/') - - field_4 = ToOneField(UserResource, 'author', full=True) - user_bundle = field_4.dehydrate(bundle) - self.assertEqual(isinstance(user_bundle, Bundle), True) - self.assertEqual(user_bundle.data['username'], u'johndoe') - self.assertEqual(user_bundle.data['email'], u'john@doe.com') - - def test_dehydrate_with_callable(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - field_1 = ToOneField(UserResource, lambda bundle: User.objects.get(pk=1)) - self.assertEqual(field_1.dehydrate(bundle), '/api/v1/users/1/') - - field_2 = ToManyField(UserResource, lambda bundle: User.objects.filter(pk=1)) - self.assertEqual(field_2.dehydrate(bundle), ['/api/v1/users/1/']) - - def test_dehydrate_full_detail_list(self): - note = Note.objects.get(pk=1) - request = MockRequest() - bundle = Bundle(obj=note, request=request) - - #details path with full_list=False - request.path = "/api/v1/notes/" - field_1 = ToOneField(UserResource, 'author', full=True, full_list=False) - self.assertEqual(field_1.dehydrate(bundle), '/api/v1/users/1/') - - #list path with full_detail=False - request.path = "/api/v1/notes/1/" - field_1 = ToOneField(UserResource, 'author', full=True, full_detail=False) - self.assertEqual(field_1.dehydrate(bundle, for_list=False), '/api/v1/users/1/') - - def test_hydrate(self): - note = Note() - bundle = Bundle(obj=note) - - # With no value or nullable, we should get an ``ApiFieldError``. - field_1 = ToOneField(UserResource, 'author') - self.assertRaises(ApiFieldError, field_1.hydrate, bundle) - - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - # The nullable case. - field_2 = ToOneField(UserResource, 'author', null=True) - field_2.instance_name = 'fk' - bundle.data['fk'] = None - self.assertEqual(field_2.hydrate(bundle), None) - - # Wrong resource URI. - field_3 = ToOneField(UserResource, 'author') - field_3.instance_name = 'fk' - bundle.data['fk'] = '/api/v1/users/abc/' - self.assertRaises(NotFound, field_3.hydrate, bundle) - - # A real, live attribute! - field_4 = ToOneField(UserResource, 'author') - field_4.instance_name = 'fk' - bundle.data['fk'] = '/api/v1/users/1/' - fk_bundle = field_4.hydrate(bundle) - self.assertEqual(fk_bundle.data['username'], u'johndoe') - self.assertEqual(fk_bundle.data['email'], u'john@doe.com') - self.assertEqual(fk_bundle.obj.username, u'johndoe') - self.assertEqual(fk_bundle.obj.email, u'john@doe.com') - - field_5 = ToOneField(UserResource, 'author') - field_5.instance_name = 'fk' - bundle.data['fk'] = { - 'username': u'mistersmith', - 'email': u'smith@example.com', - 'password': u'foobar', - } - fk_bundle = field_5.hydrate(bundle) - self.assertEqual(fk_bundle.data['username'], u'mistersmith') - self.assertEqual(fk_bundle.data['email'], u'smith@example.com') - self.assertEqual(fk_bundle.obj.username, u'mistersmith') - self.assertEqual(fk_bundle.obj.email, u'smith@example.com') - - # Regression - Make sure Unicode keys get converted to regular strings - # so that we can **kwargs them. - field_6 = ToOneField(UserResource, 'author') - field_6.instance_name = 'fk' - bundle.data['fk'] = { - u'username': u'mistersmith', - u'email': u'smith@example.com', - u'password': u'foobar', - } - fk_bundle = field_6.hydrate(bundle) - self.assertEqual(fk_bundle.data['username'], u'mistersmith') - self.assertEqual(fk_bundle.data['email'], u'smith@example.com') - self.assertEqual(fk_bundle.obj.username, u'mistersmith') - self.assertEqual(fk_bundle.obj.email, u'smith@example.com') - - # Attribute & null regression test. - # First, simulate data missing from the bundle & ``null=True``. - # Use a Note with NO author, so that the lookup for the related - # author fails. - note = Note.objects.create( - title='Biplanes for all!', - slug='biplanes-for-all', - content='Somewhere, east of Manhattan, will lie the mythical land of planes with more one wing...' - ) - bundle = Bundle(obj=note) - field_7 = ToOneField(UserResource, 'notinbundle', null=True) - field_7.instance_name = 'notinbundle' - self.assertEqual(field_7.hydrate(bundle), None) - # Then do something in the bundle also with ``null=True``. - field_8 = ToOneField(UserResource, 'author', null=True) - field_8.instance_name = 'author' - fk_bundle = field_8.hydrate(bundle) - self.assertEqual(field_8.hydrate(bundle), None) - # Then use an unsaved object in the bundle also with ``null=True``. - new_note = Note( - title='Biplanes for all!', - slug='biplanes-for-all', - content='Somewhere, east of Manhattan, will lie the mythical land of planes with more one wing...' - ) - new_bundle = Bundle(obj=new_note) - field_9 = ToOneField(UserResource, 'author', null=True) - field_9.instance_name = 'author' - self.assertEqual(field_9.hydrate(bundle), None) - - # The blank case. - field_10 = ToOneField(UserResource, 'fk', blank=True) - field_10.instance_name = 'fk' - self.assertEqual(field_10.hydrate(bundle), None) - - bundle.data['author'] = '/api/v1/users/1/' - field_11 = ToOneField(UserResource, 'author', blank=True) - field_11.instance_name = 'author' - fk_bundle = field_11.hydrate(bundle) - self.assertEqual(fk_bundle.obj.username, 'johndoe') - - # The readonly case. - field_12 = ToOneField(UserResource, 'author', readonly=True) - field_12.instance_name = 'author' - self.assertEqual(field_12.hydrate(bundle), None) - - # A related object. - field_13 = ToOneField(UserResource, 'author') - field_13.instance_name = 'fk' - bundle.related_obj = User.objects.get(pk=1) - bundle.related_name = 'author' - fk_bundle = field_13.hydrate(bundle) - self.assertEqual(fk_bundle.obj.username, u'johndoe') - self.assertEqual(fk_bundle.obj.email, u'john@doe.com') - - def test_resource_from_uri(self): - ur = UserResource() - field_1 = ToOneField(UserResource, 'author') - fk_bundle = field_1.resource_from_uri(ur, '/api/v1/users/1/') - self.assertEqual(fk_bundle.data['username'], u'johndoe') - self.assertEqual(fk_bundle.data['email'], u'john@doe.com') - self.assertEqual(fk_bundle.obj.username, u'johndoe') - self.assertEqual(fk_bundle.obj.email, u'john@doe.com') - - fk_bundle = field_1.resource_from_uri(ur, '/api/v1/users/1/', related_obj='Foo', related_name='Bar') - self.assertEqual(fk_bundle.related_obj, None) - self.assertEqual(fk_bundle.related_name, None) - - def test_resource_from_data(self): - ur = UserResource() - field_1 = ToOneField(UserResource, 'author') - fk_bundle = field_1.resource_from_data(ur, { - 'username': u'mistersmith', - 'email': u'smith@example.com', - 'password': u'foobar', - }) - self.assertEqual(fk_bundle.data['username'], u'mistersmith') - self.assertEqual(fk_bundle.data['email'], u'smith@example.com') - self.assertEqual(fk_bundle.obj.username, u'mistersmith') - self.assertEqual(fk_bundle.obj.email, u'smith@example.com') - - fk_bundle = field_1.resource_from_data(ur, { - 'username': u'mistersmith', - 'email': u'smith@example.com', - 'password': u'foobar', - }, related_obj='Foo', related_name='Bar') - self.assertEqual(fk_bundle.related_obj, 'Foo') - self.assertEqual(fk_bundle.related_name, 'Bar') - - def test_resource_from_pk(self): - user = User.objects.get(pk=1) - ur = UserResource() - field_1 = ToOneField(UserResource, 'author') - fk_bundle = field_1.resource_from_pk(ur, user) - self.assertEqual(fk_bundle.data['username'], u'johndoe') - self.assertEqual(fk_bundle.data['email'], u'john@doe.com') - self.assertEqual(fk_bundle.obj.username, u'johndoe') - self.assertEqual(fk_bundle.obj.email, u'john@doe.com') - - fk_bundle = field_1.resource_from_pk(ur, user, related_obj='Foo', related_name='Bar') - self.assertEqual(fk_bundle.related_obj, None) - self.assertEqual(fk_bundle.related_name, None) - - def test_traversed_attribute_dehydrate(self): - user = User.objects.get(pk=1) - mediabit = MediaBit(note=Note(author=user)) - bundle = Bundle(obj=mediabit) - - field_1 = ToOneField(UserResource, 'note__author') - field_1.instance_name = 'fk' - self.assertEqual(field_1.dehydrate(bundle), '/api/v1/users/1/') - - field_2 = ToOneField(UserResource, 'fakefield__author') - field_2.instance_name = 'fk' - self.assertRaises(ApiFieldError, field_2.hydrate, bundle) - - -class SubjectResource(ModelResource): - class Meta: - resource_name = 'subjects' - queryset = Subject.objects.all() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/subjects/' - - return '/api/v1/subjects/%s/' % bundle_or_obj.obj.id - - -class MediaBitResource(ModelResource): - class Meta: - resource_name = 'mediabits' - queryset = MediaBit.objects.all() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/mediabits/' - - return '/api/v1/mediabits/%s/' % bundle_or_obj.obj.id - - -class ToManyFieldTestCase(TestCase): - fixtures = ['note_testdata.json'] - urls = 'core.tests.field_urls' - - def setUp(self): - self.note_1 = Note.objects.get(pk=1) - self.note_2 = Note.objects.get(pk=2) - self.note_3 = Note.objects.get(pk=3) - - self.subject_1 = Subject.objects.create( - name='News', - url='/news/' - ) - self.subject_2 = Subject.objects.create( - name='Photos', - url='/photos/' - ) - self.subject_3 = Subject.objects.create( - name='Personal Interest', - url='/news/personal-interest/' - ) - - self.note_1.subjects.add(self.subject_1) - self.note_1.subjects.add(self.subject_2) - self.note_2.subjects.add(self.subject_1) - self.note_2.subjects.add(self.subject_3) - - def test_init(self): - field_1 = ToManyField(SubjectResource, 'subjects') - self.assertEqual(field_1.instance_name, None) - self.assertEqual(issubclass(field_1.to, SubjectResource), True) - self.assertEqual(field_1.attribute, 'subjects') - self.assertEqual(field_1.related_name, None) - self.assertEqual(field_1.null, False) - self.assertEqual(field_1.full, False) - self.assertEqual(field_1.readonly, False) - self.assertEqual(field_1.help_text, 'Many related resources. Can be either a list of URIs or list of individually nested resource data.') - - field_2 = ToManyField(SubjectResource, 'subjects', null=True, help_text='Points to many Subjects.') - self.assertEqual(field_2.instance_name, None) - self.assertEqual(issubclass(field_2.to, SubjectResource), True) - self.assertEqual(field_2.attribute, 'subjects') - self.assertEqual(field_2.related_name, None) - self.assertEqual(field_2.null, True) - self.assertEqual(field_2.full, False) - self.assertEqual(field_2.readonly, False) - self.assertEqual(field_2.help_text, 'Points to many Subjects.') - - field_3 = ToManyField(SubjectResource, 'subjects', default=1, null=True, help_text='Points to many Subjects.') - self.assertEqual(field_3.instance_name, None) - self.assertEqual(issubclass(field_3.to, SubjectResource), True) - self.assertEqual(field_3.attribute, 'subjects') - self.assertEqual(field_3.related_name, None) - self.assertEqual(field_3.null, True) - self.assertEqual(field_3.default, 1) - self.assertEqual(field_3.full, False) - self.assertEqual(field_3.readonly, False) - self.assertEqual(field_3.help_text, 'Points to many Subjects.') - - field_4 = ToManyField(SubjectResource, 'subjects', default=1, null=True, readonly=True, help_text='Points to many Subjects.') - self.assertEqual(field_4.instance_name, None) - self.assertEqual(issubclass(field_4.to, SubjectResource), True) - self.assertEqual(field_4.attribute, 'subjects') - self.assertEqual(field_4.related_name, None) - self.assertEqual(field_4.null, True) - self.assertEqual(field_4.default, 1) - self.assertEqual(field_4.full, False) - self.assertEqual(field_4.readonly, True) - self.assertEqual(field_4.help_text, 'Points to many Subjects.') - - field_5 = ToManyField(SubjectResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="list") - self.assertEqual(field_5.use_in, 'list') - - field_6 = ToManyField(SubjectResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="detail") - self.assertEqual(field_6.use_in, 'detail') - - use_in_callable = lambda x: True - field_7 = ToManyField(SubjectResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in=use_in_callable) - self.assertTrue(field_7.use_in is use_in_callable) - - field_8 = ToManyField(SubjectResource, 'author', default=1, null=True, readonly=True, help_text="Points to a User.", use_in="foo") - self.assertEqual(field_8.use_in, 'all') - - def test_dehydrated_type(self): - field_1 = ToManyField(SubjectResource, 'subjects') - self.assertEqual(field_1.dehydrated_type, 'related') - - def test_has_default(self): - field_1 = ToManyField(SubjectResource, 'subjects') - self.assertEqual(field_1.has_default(), False) - - field_2 = ToManyField(SubjectResource, 'subjects', default=1) - self.assertEqual(field_2.has_default(), True) - - def test_default(self): - field_1 = ToManyField(SubjectResource, 'subjects') - self.assertTrue(isinstance(field_1.default, NOT_PROVIDED)) - - field_2 = ToManyField(SubjectResource, 'subjects', default=1) - self.assertEqual(field_2.default, 1) - - def test_dehydrate(self): - note = Note() - bundle_1 = Bundle(obj=note) - field_1 = ToManyField(SubjectResource, 'subjects') - field_1.instance_name = 'm2m' - - try: - # self.assertRaises isn't cooperating here. Do it the hard way. - field_1.dehydrate(bundle_1) - self.fail() - except ApiFieldError: - pass - - field_2 = ToManyField(SubjectResource, 'subjects', null=True) - field_2.instance_name = 'm2m' - self.assertEqual(field_2.dehydrate(bundle_1), []) - - field_3 = ToManyField(SubjectResource, 'subjects') - field_3.instance_name = 'm2m' - bundle_3 = Bundle(obj=self.note_1) - self.assertEqual(field_3.dehydrate(bundle_3), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - field_4 = ToManyField(SubjectResource, 'subjects', full=True) - field_4.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_4 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_4.dehydrate(bundle_4) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - field_5 = ToManyField(SubjectResource, 'subjects') - field_5.instance_name = 'm2m' - bundle_5 = Bundle(obj=self.note_2) - self.assertEqual(field_5.dehydrate(bundle_5), ['/api/v1/subjects/1/', '/api/v1/subjects/3/']) - - field_6 = ToManyField(SubjectResource, 'subjects') - field_6.instance_name = 'm2m' - bundle_6 = Bundle(obj=self.note_3) - self.assertEqual(field_6.dehydrate(bundle_6), []) - - try: - # Regression for missing variable initialization. - field_7 = ToManyField(SubjectResource, None) - field_7.instance_name = 'm2m' - bundle_7 = Bundle(obj=self.note_3) - field_7.dehydrate(bundle_7) - self.fail('ToManyField requires an attribute of some type.') - except ApiFieldError: - pass - - def test_dehydrate_full_detail_list(self): - #details path with full_detail=False - field_1 = ToManyField(SubjectResource, 'subjects', full=True, full_detail=False) - field_1.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_1 = Bundle(obj=self.note_1, request=request) - self.assertEqual(field_1.dehydrate(bundle_1, for_list=False), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - #list path with full_detail=False - field_2 = ToManyField(SubjectResource, 'subjects', full=True, full_detail=False) - field_2.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/" - bundle_2 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_2.dehydrate(bundle_2) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - #list path with full_list=False - field_3 = ToManyField(SubjectResource, 'subjects', full=True, full_list=False) - field_3.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/" - bundle_3 = Bundle(obj=self.note_1, request=request) - self.assertEqual(field_3.dehydrate(bundle_3), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - #detail path with full_list=False - field_4 = ToManyField(SubjectResource, 'subjects', full=True, full_list=False) - field_4.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_4 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_4.dehydrate(bundle_4, for_list=False) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - #list url with callable returning True - field_5 = ToManyField(SubjectResource, 'subjects', full=True, full_list=lambda x: True) - field_5.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/" - bundle_5 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_5.dehydrate(bundle_5) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - #list url with callable returning False - field_6 = ToManyField(SubjectResource, 'subjects', full=True, full_list=lambda x: False) - field_6.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/" - bundle_6 = Bundle(obj=self.note_1, request=request) - self.assertEqual(field_6.dehydrate(bundle_6), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - #detail url with callable returning True - field_7 = ToManyField(SubjectResource, 'subjects', full=True, full_detail=lambda x: True) - field_7.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_7 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_7.dehydrate(bundle_7) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - #detail url with callable returning False - field_8 = ToManyField(SubjectResource, 'subjects', full=True, full_detail=lambda x: False) - field_8.instance_name = 'm2m' - request = MockRequest() - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_8 = Bundle(obj=self.note_1, request=request) - self.assertEqual(field_8.dehydrate(bundle_8, for_list=False), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - #detail url with full_detail=True and get parameters - field_9 = ToManyField(SubjectResource, 'subjects', full=True, full_detail=True) - field_9.instance_name = 'm2m' - request = HttpRequest() - request.method = "GET" - request.GET = {"foo": "bar"} - request.META["QUERY_STRING"] = "foo=bar" - request.path = "/api/v1/subjects/%(pk)s/" % {'pk': self.note_1.pk} - bundle_9 = Bundle(obj=self.note_1, request=request) - subject_bundle_list = field_9.dehydrate(bundle_9) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(isinstance(subject_bundle_list[0], Bundle), True) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - self.assertEqual(isinstance(subject_bundle_list[1], Bundle), True) - self.assertEqual(subject_bundle_list[1].data['name'], u'Photos') - self.assertEqual(subject_bundle_list[1].data['url'], u'/photos/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Photos') - self.assertEqual(subject_bundle_list[1].obj.url, u'/photos/') - - def test_dehydrate_with_callable(self): - note = Note() - bundle_1 = Bundle(obj=self.note_2) - field_1 = ToManyField(SubjectResource, attribute=lambda bundle: Subject.objects.filter(notes=bundle.obj, name__startswith='Personal')) - field_1.instance_name = 'm2m' - - self.assertEqual(field_1.dehydrate(bundle_1), ['/api/v1/subjects/3/']) - - def test_hydrate(self): - note = Note.objects.get(pk=1) - bundle = Bundle(obj=note) - - # With no value or nullable, we should get an ``ApiFieldError``. - field_1 = ToManyField(SubjectResource, 'subjects') - field_1.instance_name = 'm2m' - self.assertRaises(ApiFieldError, field_1.hydrate_m2m, bundle) - - # The nullable case. - field_2 = ToManyField(SubjectResource, 'subjects', null=True) - field_2.instance_name = 'm2m' - empty_bundle = Bundle() - self.assertEqual(field_2.hydrate_m2m(empty_bundle), []) - - field_3 = ToManyField(SubjectResource, 'subjects', null=True) - field_3.instance_name = 'm2m' - bundle_3 = Bundle(data={'m2m': []}) - self.assertEqual(field_3.hydrate_m2m(bundle_3), []) - - # Wrong resource URI. - field_4 = ToManyField(SubjectResource, 'subjects') - field_4.instance_name = 'm2m' - bundle_4 = Bundle(data={'m2m': ['/api/v1/subjects/abc/']}) - self.assertRaises(NotFound, field_4.hydrate_m2m, bundle_4) - - # A real, live attribute! - field_5 = ToManyField(SubjectResource, 'subjects') - field_5.instance_name = 'm2m' - bundle_5 = Bundle(data={'m2m': ['/api/v1/subjects/1/']}) - subject_bundle_list = field_5.hydrate_m2m(bundle_5) - self.assertEqual(len(subject_bundle_list), 1) - self.assertEqual(subject_bundle_list[0].data['name'], u'News') - self.assertEqual(subject_bundle_list[0].data['url'], u'/news/') - self.assertEqual(subject_bundle_list[0].obj.name, u'News') - self.assertEqual(subject_bundle_list[0].obj.url, u'/news/') - - field_6 = ToManyField(SubjectResource, 'subjects') - field_6.instance_name = 'm2m' - bundle_6 = Bundle(data={'m2m': [ - { - 'name': u'Foo', - 'url': u'/foo/', - }, - { - 'name': u'Bar', - 'url': u'/bar/', - }, - ]}) - subject_bundle_list = field_6.hydrate_m2m(bundle_6) - self.assertEqual(len(subject_bundle_list), 2) - self.assertEqual(subject_bundle_list[0].data['name'], u'Foo') - self.assertEqual(subject_bundle_list[0].data['url'], u'/foo/') - self.assertEqual(subject_bundle_list[0].obj.name, u'Foo') - self.assertEqual(subject_bundle_list[0].obj.url, u'/foo/') - self.assertEqual(subject_bundle_list[1].data['name'], u'Bar') - self.assertEqual(subject_bundle_list[1].data['url'], u'/bar/') - self.assertEqual(subject_bundle_list[1].obj.name, u'Bar') - self.assertEqual(subject_bundle_list[1].obj.url, u'/bar/') - - # The blank case. - field_7 = ToManyField(SubjectResource, 'fk', blank=True) - field_7.instance_name = 'fk' - self.assertEqual(field_7.hydrate(bundle_6), None) - - field_8 = ToManyField(SubjectResource, 'm2m', blank=True) - field_8.instance_name = 'm2m' - subject_bundle_list_2 = field_8.hydrate_m2m(bundle_6) - self.assertEqual(len(subject_bundle_list_2), 2) - self.assertEqual(subject_bundle_list_2[0].data['name'], u'Foo') - self.assertEqual(subject_bundle_list_2[0].data['url'], u'/foo/') - self.assertEqual(subject_bundle_list_2[0].obj.name, u'Foo') - self.assertEqual(subject_bundle_list_2[0].obj.url, u'/foo/') - self.assertEqual(subject_bundle_list_2[1].data['name'], u'Bar') - self.assertEqual(subject_bundle_list_2[1].data['url'], u'/bar/') - self.assertEqual(subject_bundle_list_2[1].obj.name, u'Bar') - self.assertEqual(subject_bundle_list_2[1].obj.url, u'/bar/') - - # The readonly case. - field_9 = ToManyField(SubjectResource, 'subjects', readonly=True) - field_9.instance_name = 'm2m' - self.assertEqual(field_9.hydrate(bundle_6), None) - - # A related object. - field_10 = ToManyField(MediaBitResource, 'media_bits', related_name='note') - field_10.instance_name = 'mbs' - note_1 = Note.objects.get(pk=1) - bundle_10 = Bundle(obj=note_1, data={'mbs': [ - { - 'title': 'Foo!', - }, - ]}) - media_bundle_list = field_10.hydrate_m2m(bundle_10) - self.assertEqual(len(media_bundle_list), 1) - self.assertEqual(media_bundle_list[0].obj.title, u'Foo!') - - def test_traversed_attribute_dehydrate(self): - mediabit = MediaBit(id=1, note=self.note_1) - bundle = Bundle(obj=mediabit) - - field_1 = ToManyField(SubjectResource, 'note__subjects') - field_1.instance_name = 'm2m' - self.assertEqual(field_1.dehydrate(bundle), ['/api/v1/subjects/1/', '/api/v1/subjects/2/']) - - field_2 = ToOneField(SubjectResource, 'fakefield__subjects') - field_2.instance_name = 'm2m' - self.assertRaises(ApiFieldError, field_2.hydrate, bundle) diff -Nru django-tastypie-0.12.0/tests/core/tests/field_urls.py django-tastypie-0.13.3/tests/core/tests/field_urls.py --- django-tastypie-0.12.0/tests/core/tests/field_urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/field_urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include -from tastypie import fields -from tastypie.resources import ModelResource -from core.models import Note, Subject -from core.tests.api import Api, UserResource - - -class SubjectResource(ModelResource): - class Meta: - resource_name = 'subjects' - queryset = Subject.objects.all() - - -class CustomNoteResource(ModelResource): - author = fields.ForeignKey(UserResource, 'author') - subjects = fields.ManyToManyField(SubjectResource, 'subjects') - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - - -api = Api(api_name='v1') -api.register(CustomNoteResource()) -api.register(UserResource()) -api.register(SubjectResource()) - -urlpatterns = patterns('', - (r'^api/', include(api.urls)), -) diff -Nru django-tastypie-0.12.0/tests/core/tests/http.py django-tastypie-0.13.3/tests/core/tests/http.py --- django-tastypie-0.12.0/tests/core/tests/http.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -# Basically just a sanity check to make sure things don't change from underneath us. -from django.test import TestCase -from tastypie.http import * - - -class HttpTestCase(TestCase): - def test_various_statuses(self): - created = HttpCreated(location='http://example.com/thingy/1/') - self.assertEqual(created.status_code, 201) - self.assertEqual(created['Location'], 'http://example.com/thingy/1/') - # Regression. - created_2 = HttpCreated() - self.assertEqual(created_2.status_code, 201) - self.assertEqual(created_2['Location'], '') - accepted = HttpAccepted() - self.assertEqual(accepted.status_code, 202) - no_content = HttpNoContent() - self.assertEqual(no_content.status_code, 204) - see_other = HttpSeeOther() - self.assertEqual(see_other.status_code, 303) - not_modified = HttpNotModified() - self.assertEqual(not_modified.status_code, 304) - bad_request = HttpBadRequest() - self.assertEqual(bad_request.status_code, 400) - unauthorized = HttpUnauthorized() - self.assertEqual(unauthorized.status_code, 401) - not_found = HttpNotFound() - self.assertEqual(not_found.status_code, 404) - not_allowed = HttpMethodNotAllowed() - self.assertEqual(not_allowed.status_code, 405) - conflict = HttpConflict() - self.assertEqual(conflict.status_code, 409) - gone = HttpGone() - self.assertEqual(gone.status_code, 410) - toomanyrequests = HttpTooManyRequests() - self.assertEqual(toomanyrequests.status_code, 429) - not_implemented = HttpNotImplemented() - self.assertEqual(not_implemented.status_code, 501) diff -Nru django-tastypie-0.12.0/tests/core/tests/__init__.py django-tastypie-0.13.3/tests/core/tests/__init__.py --- django-tastypie-0.12.0/tests/core/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -import warnings -warnings.simplefilter('ignore', Warning) - -from core.tests.api import * -from core.tests.authentication import * -from core.tests.authorization import * -from core.tests.cache import * -from core.tests.commands import * -from core.tests.fields import * -from core.tests.http import * -from core.tests.paginator import * -from core.tests.resources import * -from core.tests.serializers import * -from core.tests.throttle import * -from core.tests.utils import * -from core.tests.validation import * diff -Nru django-tastypie-0.12.0/tests/core/tests/manual_urls.py django-tastypie-0.13.3/tests/core/tests/manual_urls.py --- django-tastypie-0.12.0/tests/core/tests/manual_urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/manual_urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include -from core.tests.resources import NoteResource - - -note_resource = NoteResource() - -urlpatterns = patterns('', - (r'^', include(note_resource.urls)), -) diff -Nru django-tastypie-0.12.0/tests/core/tests/mocks.py django-tastypie-0.13.3/tests/core/tests/mocks.py --- django-tastypie-0.12.0/tests/core/tests/mocks.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/mocks.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -import django - -class MockRequest(object): - def __init__(self): - self.GET = {} - self.POST = {} - self.PUT = {} - self.DELETE = {} - self.META = {} - self.path = '' - self.method = 'GET' - - def get_full_path(self, *args, **kwargs): - return self.path - - def is_ajax(self): - return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' - - def set_body(self, content): - if django.VERSION >= (1, 4): - body_attr = "body" - else: - body_attr = "raw_post_data" - - setattr(self, body_attr, content) \ No newline at end of file diff -Nru django-tastypie-0.12.0/tests/core/tests/paginator.py django-tastypie-0.13.3/tests/core/tests/paginator.py --- django-tastypie-0.12.0/tests/core/tests/paginator.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/paginator.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,283 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf import settings -from django.test import TestCase -from tastypie.exceptions import BadRequest -from tastypie.paginator import Paginator -from core.models import Note -from core.tests.resources import NoteResource -from django.db import reset_queries -from django.http import QueryDict - - -class PaginatorTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def setUp(self): - super(PaginatorTestCase, self).setUp() - self.data_set = Note.objects.all() - self.old_debug = settings.DEBUG - settings.DEBUG = True - - def tearDown(self): - settings.DEBUG = self.old_debug - super(PaginatorTestCase, self).tearDown() - - def _get_query_count(self): - try: - from django.db import connections - return connections['default'].queries - except ImportError: - from django.db import connection - return connection.queries - - def test_page1(self): - reset_queries() - self.assertEqual(len(self._get_query_count()), 0) - - paginator = Paginator({}, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=0) - - # REGRESSION: Check to make sure only part of the cache is full. - # We used to run ``len()`` on the ``QuerySet``, which would populate - # the entire result set. Owwie. - paginator.get_count() - self.assertEqual(len(self._get_query_count()), 1) - # Should be nothing in the cache. - self.assertEqual(paginator.objects._result_cache, None) - - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 0) - self.assertEqual(meta['previous'], None) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=2' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - def test_page2(self): - paginator = Paginator({}, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - def test_page3(self): - paginator = Paginator({}, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=4) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 4) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=2' in meta['previous']) - self.assertEqual(meta['next'], None) - self.assertEqual(meta['total_count'], 6) - - def test_page2_with_request(self): - for req in [{'offset' : '2', 'limit' : '2'}, QueryDict('offset=2&limit=2')]: - paginator = Paginator(req, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - def test_page3_with_request(self): - for req in [{'offset' : '4', 'limit' : '2'}, QueryDict('offset=4&limit=2')]: - paginator = Paginator(req, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=4) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 4) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=2' in meta['previous']) - self.assertEqual(meta['next'], None) - self.assertEqual(meta['total_count'], 6) - - def test_large_limit(self): - paginator = Paginator({}, self.data_set, resource_uri='/api/v1/notes/', limit=20, offset=0) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 20) - self.assertEqual(meta['offset'], 0) - self.assertEqual(meta['previous'], None) - self.assertEqual(meta['next'], None) - self.assertEqual(meta['total_count'], 6) - - def test_all(self): - paginator = Paginator({'limit': 0}, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=0) - page = paginator.page() - meta = page['meta'] - self.assertEqual(meta['limit'], 1000) - self.assertEqual(meta['offset'], 0) - self.assertEqual(meta['total_count'], 6) - self.assertEqual(len(page['objects']), 6) - - def test_complex_get(self): - request = { - 'slug__startswith': 'food', - 'format': 'json', - } - paginator = Paginator(request, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('slug__startswith=food' in meta['previous']) - self.assertTrue('format=json' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertTrue('slug__startswith=food' in meta['next']) - self.assertTrue('format=json' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - def test_limit(self): - paginator = Paginator({}, self.data_set, limit=20, offset=0) - - paginator.limit = '10' - self.assertEqual(paginator.get_limit(), 10) - - paginator.limit = None - self.assertEqual(paginator.get_limit(), 20) - - paginator.limit = 10 - self.assertEqual(paginator.get_limit(), 10) - - paginator.limit = -10 - raised = False - try: - paginator.get_limit() - except BadRequest as e: - raised = e - self.assertTrue(raised) - self.assertEqual(str(raised), "Invalid limit '-10' provided. Please provide a positive integer >= 0.") - - paginator.limit = 'hAI!' - raised = False - try: - paginator.get_limit() - except BadRequest as e: - raised = e - self.assertTrue(raised) - self.assertEqual(str(raised), "Invalid limit 'hAI!' provided. Please provide a positive integer.") - - # Test the max_limit. - paginator.limit = 1000 - self.assertEqual(paginator.get_limit(), 1000) - - paginator.limit = 1001 - self.assertEqual(paginator.get_limit(), 1000) - - paginator = Paginator({}, self.data_set, limit=20, offset=0, max_limit=10) - self.assertEqual(paginator.get_limit(), 10) - - def test_offset(self): - paginator = Paginator({}, self.data_set, limit=20, offset=0) - - paginator.offset = '10' - self.assertEqual(paginator.get_offset(), 10) - - paginator.offset = 0 - self.assertEqual(paginator.get_offset(), 0) - - paginator.offset = 10 - self.assertEqual(paginator.get_offset(), 10) - - paginator.offset= -10 - raised = False - try: - paginator.get_offset() - except BadRequest as e: - raised = e - self.assertTrue(raised) - self.assertEqual(str(raised), "Invalid offset '-10' provided. Please provide a positive integer >= 0.") - - paginator.offset = 'hAI!' - raised = False - try: - paginator.get_offset() - except BadRequest as e: - raised = e - self.assertTrue(raised) - self.assertEqual(str(raised), "Invalid offset 'hAI!' provided. Please provide an integer.") - - def test_regression_nonqueryset(self): - paginator = Paginator({}, ['foo', 'bar', 'baz'], limit=2, offset=0) - # This would fail due to ``count`` being present on ``list`` but called - # differently. - page = paginator.page() - self.assertEqual(page['objects'], ['foo', 'bar']) - - def test_unicode_request(self): - request = { - 'slug__startswith': u'☃', - 'format': 'json', - } - paginator = Paginator(request, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('slug__startswith=%E2%98%83' in meta['previous']) - self.assertTrue('format=json' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertTrue('slug__startswith=%E2%98%83' in meta['next']) - self.assertTrue('format=json' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - request = QueryDict('slug__startswith=☃&format=json') - paginator = Paginator(request, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('slug__startswith=%E2%98%83' in meta['previous']) - self.assertTrue('format=json' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertTrue('slug__startswith=%E2%98%83' in meta['next']) - self.assertTrue('format=json' in meta['next']) - self.assertEqual(meta['total_count'], 6) - - def test_custom_collection_name(self): - paginator = Paginator({}, self.data_set, resource_uri='/api/v1/notes/', limit=20, offset=0, collection_name='notes') - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 20) - self.assertEqual(meta['offset'], 0) - self.assertEqual(meta['previous'], None) - self.assertEqual(meta['next'], None) - self.assertEqual(meta['total_count'], 6) - self.assertEqual(len(paginator.page()['notes']), 6) - - def test_multiple(self): - request = QueryDict('a=1&a=2') - paginator = Paginator(request, self.data_set, resource_uri='/api/v1/notes/', limit=2, offset=2) - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 2) - self.assertEqual(meta['offset'], 2) - self.assertTrue('limit=2' in meta['previous']) - self.assertTrue('offset=0' in meta['previous']) - self.assertTrue('a=1' in meta['previous']) - self.assertTrue('a=2' in meta['previous']) - self.assertTrue('limit=2' in meta['next']) - self.assertTrue('offset=4' in meta['next']) - self.assertTrue('a=1' in meta['next']) - self.assertTrue('a=2' in meta['next']) - - def test_max_limit(self): - paginator = Paginator({'limit': 0}, self.data_set, max_limit=10, - resource_uri='/api/v1/notes/') - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 10) - - def test_max_limit_none(self): - paginator = Paginator({'limit': 0}, self.data_set, max_limit=None, - resource_uri='/api/v1/notes/') - meta = paginator.page()['meta'] - self.assertEqual(meta['limit'], 0) - diff -Nru django-tastypie-0.12.0/tests/core/tests/resources.py django-tastypie-0.13.3/tests/core/tests/resources.py --- django-tastypie-0.12.0/tests/core/tests/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,4226 +0,0 @@ -import base64 -import copy -import datetime -from decimal import Decimal -import django -import json -from mock import patch - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.cache import cache -from django.core.exceptions import FieldError, MultipleObjectsReturned -from django.core import mail -from django.core.urlresolvers import reverse -from django import forms -from django.http import HttpRequest, QueryDict, Http404 -from django.test import TestCase -from django.utils.encoding import force_text -from django.utils import six - -from tastypie.authentication import BasicAuthentication -from tastypie.authorization import Authorization -from tastypie.bundle import Bundle -from tastypie.exceptions import InvalidFilterError, InvalidSortError, ImmediateHttpResponse, BadRequest, NotFound -from tastypie import fields -from tastypie.paginator import Paginator -from tastypie.resources import Resource, ModelResource, ALL, ALL_WITH_RELATIONS, convert_post_to_put, convert_post_to_patch -from tastypie.serializers import Serializer -from tastypie.throttle import CacheThrottle -from tastypie.utils import aware_datetime, make_naive -from tastypie.validation import FormValidation -from core.models import Note, NoteWithEditor, Subject, MediaBit, AutoNowNote, DateRecord, Counter -from core.tests.mocks import MockRequest -from core.utils import SimpleHandler - - -class CustomSerializer(Serializer): - pass - - -class TestObject(object): - name = None - view_count = None - date_joined = None - - -class BasicResourceWithDifferentListAndDetailFields(Resource): - name = fields.CharField(attribute='name', use_in="all") - view_count = fields.IntegerField(attribute='view_count', default=0, use_in="detail") - date_joined = fields.DateTimeField(null=True, use_in="list") - - def dehydrate_date_joined(self, bundle): - if getattr(bundle.obj, 'date_joined', None) is not None: - return bundle.obj.date_joined - - if bundle.data.get('date_joined') is not None: - return bundle.data.get('date_joined') - - return aware_datetime(2010, 3, 27, 22, 30, 0) - - def hydrate_date_joined(self, bundle): - bundle.obj.date_joined = bundle.data['date_joined'] - return bundle - - def obj_get_list(self, bundle, **kwargs): - test_object_1 = TestObject() - test_object_1.name = 'Daniel' - test_object_1.view_count = 12 - test_object_1.date_joined = aware_datetime(2010, 3, 30, 9, 0, 0) - return [test_object_1] - - class Meta: - object_class = TestObject - resource_name = 'basic' - - -class BasicResourceWithDifferentListAndDetailFieldsCallable(Resource): - name = fields.CharField(attribute='name', use_in="all") - view_count = fields.IntegerField(attribute='view_count', default=0, use_in=lambda x: True) - date_joined = fields.DateTimeField(null=True, use_in=lambda x: False) - - class Meta: - object_class = TestObject - resource_name = 'basic' - - -class BasicResource(Resource): - name = fields.CharField(attribute='name') - view_count = fields.IntegerField(attribute='view_count', default=0) - date_joined = fields.DateTimeField(null=True) - - class Meta: - object_class = TestObject - resource_name = 'basic' - authorization = Authorization() - - def dehydrate_date_joined(self, bundle): - if getattr(bundle.obj, 'date_joined', None) is not None: - return bundle.obj.date_joined - - if bundle.data.get('date_joined') is not None: - return bundle.data.get('date_joined') - - return aware_datetime(2010, 3, 27, 22, 30, 0) - - def hydrate_date_joined(self, bundle): - bundle.obj.date_joined = bundle.data['date_joined'] - return bundle - - def get_list(self, request, **kwargs): - raise NotImplementedError - - -class AnotherBasicResource(BasicResource): - name = fields.CharField(attribute='name') - view_count = fields.IntegerField(attribute='view_count', default=0) - date_joined = fields.DateField(attribute='created') - is_active = fields.BooleanField(attribute='is_active', default=True) - aliases = fields.ListField(attribute='aliases', null=True) - meta = fields.DictField(attribute='metadata', null=True) - owed = fields.DecimalField(attribute='money_owed', null=True) - - class Meta: - object_class = TestObject - resource_name = 'anotherbasic' - authorization = Authorization() - - def dehydrate(self, bundle): - if hasattr(bundle.obj, 'bar'): - bundle.data['bar'] = bundle.obj.bar - - bundle.data['aliases'] = ['Mr. Smith', 'John Doe'] - bundle.data['meta'] = {'threat': 'high'} - bundle.data['owed'] = Decimal('102.57') - return bundle - - def hydrate(self, bundle): - if 'bar' in bundle.data: - bundle.obj.bar = 'O HAI BAR!' - - return bundle - - -class NoUriBasicResource(BasicResource): - name = fields.CharField(attribute='name') - view_count = fields.IntegerField(attribute='view_count', default=0) - date_joined = fields.DateTimeField(null=True) - - class Meta: - object_class = TestObject - include_resource_uri = False - authorization = Authorization() - - -class NullableNameResource(Resource): - name = fields.CharField(attribute='name', null=True) - - class Meta: - object_class = TestObject - resource_name = 'nullable_name' - authorization = Authorization() - - -class MangledBasicResource(BasicResource): - class Meta: - object_class = TestObject - resource_name = 'mangledbasic' - authorization = Authorization() - - def alter_list_data_to_serialize(self, request, data_dict): - if isinstance(data_dict, dict): - if 'meta' in data_dict: - # Get rid of the "meta". - del(data_dict['meta']) - # Rename the objects. - data_dict['testobjects'] = copy.copy(data_dict['objects']) - del(data_dict['objects']) - - return data_dict - - def alter_deserialized_detail_data(self, request, bundle_or_list): - # Automatically shove in the user. - if isinstance(bundle_or_list, Bundle): - # Handle the detail. - bundle_or_list.data['user'] = request.user - elif isinstance(bundle_or_list, list): - # Handle the list. - for obj_data in bundle_or_list: - obj_data['user'] = request.user - - return bundle_or_list - - -class MROBaseFieldResourceA(Resource): - test = fields.CharField(default='test_a') - - -class MROBaseFieldResourceB(Resource): - test = fields.CharField(default='test_b') - name = fields.CharField(default='Mr. Field') - - -class MROFieldResource(MROBaseFieldResourceA, MROBaseFieldResourceB): - pass - - -class ConvertTestCase(TestCase): - def test_to_put(self): - request = HttpRequest() - request.method = 'PUT' - # Obviously not the right data, but we just need to make sure it gets - # removed. - request._post = 'foo' - request._files = 'bar' - request.POST = { - 'test': 'thing' - } - # Make Django happy. - request._read_started = False - request._raw_post_data = request._body = '' - - modified = convert_post_to_put(request) - self.assertEqual(modified.method, 'PUT') - self.assertEqual(len(modified._post), 0) - self.assertEqual(len(modified._files), 0) - self.assertEqual(modified.POST, {'test': 'thing'}) - self.assertEqual(modified.PUT, {'test': 'thing'}) - - def test_to_patch(self): - request = HttpRequest() - request.method = 'PATCH' - # Obviously not the right data, but we just need to make sure it gets - # removed. - request._post = 'foo' - request._files = 'bar' - request.POST = { - 'test': 'thing' - } - # Make Django happy. - request._read_started = False - request._raw_post_data = request._body = '' - - modified = convert_post_to_patch(request) - self.assertEqual(modified.method, 'PATCH') - self.assertEqual(len(modified._post), 0) - self.assertEqual(len(modified._files), 0) - self.assertEqual(modified.POST, {'test': 'thing'}) - self.assertEqual(modified.PATCH, {'test': 'thing'}) - - -class ResourceTestCase(TestCase): - def test_fields(self): - basic = BasicResource() - self.assertEqual(len(basic.fields), 4) - self.assert_('name' in basic.fields) - self.assertEqual(isinstance(basic.fields['name'], fields.CharField), True) - self.assertEqual(basic.fields['name']._resource, basic.__class__) - self.assertEqual(basic.fields['name'].instance_name, 'name') - self.assert_('view_count' in basic.fields) - self.assertEqual(isinstance(basic.fields['view_count'], fields.IntegerField), True) - self.assertEqual(basic.fields['view_count']._resource, basic.__class__) - self.assertEqual(basic.fields['view_count'].instance_name, 'view_count') - self.assert_('date_joined' in basic.fields) - self.assertEqual(isinstance(basic.fields['date_joined'], fields.DateTimeField), True) - self.assertEqual(basic.fields['date_joined']._resource, basic.__class__) - self.assertEqual(basic.fields['date_joined'].instance_name, 'date_joined') - self.assert_('resource_uri' in basic.fields) - self.assertEqual(isinstance(basic.fields['resource_uri'], fields.CharField), True) - self.assertEqual(basic.fields['resource_uri']._resource, basic.__class__) - self.assertEqual(basic.fields['resource_uri'].instance_name, 'resource_uri') - self.assertEqual(basic._meta.resource_name, 'basic') - - another = AnotherBasicResource() - self.assertEqual(len(another.fields), 8) - self.assert_('name' in another.fields) - self.assertEqual(isinstance(another.name, fields.CharField), True) - self.assertEqual(another.fields['name']._resource, another.__class__) - self.assertEqual(another.fields['name'].instance_name, 'name') - self.assert_('view_count' in another.fields) - self.assertEqual(isinstance(another.view_count, fields.IntegerField), True) - self.assertEqual(another.fields['view_count']._resource, another.__class__) - self.assertEqual(another.fields['view_count'].instance_name, 'view_count') - self.assert_('date_joined' in another.fields) - self.assertEqual(isinstance(another.date_joined, fields.DateField), True) - self.assertEqual(another.fields['date_joined']._resource, another.__class__) - self.assertEqual(another.fields['date_joined'].instance_name, 'date_joined') - self.assert_('is_active' in another.fields) - self.assertEqual(isinstance(another.is_active, fields.BooleanField), True) - self.assertEqual(another.fields['is_active']._resource, another.__class__) - self.assertEqual(another.fields['is_active'].instance_name, 'is_active') - self.assert_('aliases' in another.fields) - self.assertEqual(isinstance(another.aliases, fields.ListField), True) - self.assertEqual(another.fields['aliases']._resource, another.__class__) - self.assertEqual(another.fields['aliases'].instance_name, 'aliases') - self.assert_('meta' in another.fields) - self.assertEqual(isinstance(another.meta, fields.DictField), True) - self.assertEqual(another.fields['meta']._resource, another.__class__) - self.assertEqual(another.fields['meta'].instance_name, 'meta') - self.assert_('owed' in another.fields) - self.assertEqual(isinstance(another.owed, fields.DecimalField), True) - self.assertEqual(another.fields['owed']._resource, another.__class__) - self.assertEqual(another.fields['owed'].instance_name, 'owed') - self.assert_('resource_uri' in another.fields) - self.assertEqual(isinstance(another.resource_uri, fields.CharField), True) - self.assertEqual(another.fields['resource_uri']._resource, another.__class__) - self.assertEqual(another.fields['resource_uri'].instance_name, 'resource_uri') - self.assertEqual(another._meta.resource_name, 'anotherbasic') - - nouri = NoUriBasicResource() - self.assertEqual(len(nouri.fields), 3) - self.assert_('name' in nouri.fields) - self.assertEqual(isinstance(nouri.name, fields.CharField), True) - self.assertEqual(nouri.fields['name']._resource, nouri.__class__) - self.assertEqual(nouri.fields['name'].instance_name, 'name') - self.assert_('view_count' in nouri.fields) - self.assertEqual(isinstance(nouri.view_count, fields.IntegerField), True) - self.assertEqual(nouri.fields['view_count']._resource, nouri.__class__) - self.assertEqual(nouri.fields['view_count'].instance_name, 'view_count') - self.assert_('date_joined' in nouri.fields) - self.assertEqual(isinstance(nouri.date_joined, fields.DateTimeField), True) - self.assertEqual(nouri.fields['date_joined']._resource, nouri.__class__) - self.assertEqual(nouri.fields['date_joined'].instance_name, 'date_joined') - # Note - automatic resource naming. - self.assertEqual(nouri._meta.resource_name, 'nouribasic') - - def test_inheritance(self): - mrofr = MROFieldResource() - self.assertEqual(len(mrofr.fields), 3) - self.assertEqual(mrofr.fields['test'].default, 'test_a') - self.assertEqual(mrofr.fields['name'].default, 'Mr. Field') - - def test_full_dehydrate_with_use_in(self): - test_object_1 = TestObject() - test_object_1.name = 'Daniel' - test_object_1.view_count = 12 - test_object_1.date_joined = aware_datetime(2010, 3, 30, 9, 0, 0) - - basic = BasicResourceWithDifferentListAndDetailFields() - test_bundle_1 = basic.build_bundle(obj=test_object_1) - - # Sanity check. - self.assertEqual(basic.name.value, None) - self.assertEqual(basic.view_count.value, None) - self.assertEqual(basic.date_joined.value, None) - - #check hydration with details - bundle_1 = basic.full_dehydrate(test_bundle_1) - self.assertEqual(bundle_1.data['name'], 'Daniel') - self.assertEqual(bundle_1.data['view_count'], 12) - self.assertEqual(bundle_1.data.get('date_joined'), None) - - #now check dehydration with lists - test_bundle_2 = basic.build_bundle(obj=test_object_1) - - bundle_2 = basic.full_dehydrate(test_bundle_2, for_list=True) - self.assertEqual(bundle_2.data['name'], 'Daniel') - self.assertEqual(bundle_2.data.get('view_count'), None) - self.assertEqual(bundle_2.data['date_joined'].year, 2010) - self.assertEqual(bundle_2.data['date_joined'].day, 30) - - def test_full_dehydrate_with_use_in_callable(self): - test_object_1 = TestObject() - test_object_1.name = 'Daniel' - test_object_1.view_count = 12 - test_object_1.date_joined = aware_datetime(2010, 3, 30, 9, 0, 0) - - basic = BasicResourceWithDifferentListAndDetailFieldsCallable() - test_bundle_1 = basic.build_bundle(obj=test_object_1) - - # Sanity check. - self.assertEqual(basic.name.value, None) - self.assertEqual(basic.view_count.value, None) - self.assertEqual(basic.date_joined.value, None) - - #check hydration with details - bundle_1 = basic.full_dehydrate(test_bundle_1) - self.assertEqual(bundle_1.data['name'], 'Daniel') - self.assertEqual(bundle_1.data['view_count'], 12) - self.assertEqual(bundle_1.data.get('date_joined'), None) - - #now check dehydration with lists. Should be the same as details since - #we are using callables for the use_in - test_bundle_2 = basic.build_bundle(obj=test_object_1) - - bundle_2 = basic.full_dehydrate(test_bundle_2, for_list=True) - self.assertEqual(bundle_2.data['name'], 'Daniel') - self.assertEqual(bundle_2.data['view_count'], 12) - self.assertEqual(bundle_2.data.get('date_joined'), None) - - def test_full_dehydrate(self): - test_object_1 = TestObject() - test_object_1.name = 'Daniel' - test_object_1.view_count = 12 - test_object_1.date_joined = aware_datetime(2010, 3, 30, 9, 0, 0) - test_object_1.foo = "Hi, I'm ignored." - - basic = BasicResource() - test_bundle_1 = basic.build_bundle(obj=test_object_1) - - # Sanity check. - self.assertEqual(basic.name.value, None) - self.assertEqual(basic.view_count.value, None) - self.assertEqual(basic.date_joined.value, None) - - bundle_1 = basic.full_dehydrate(test_bundle_1) - self.assertEqual(bundle_1.data['name'], 'Daniel') - self.assertEqual(bundle_1.data['view_count'], 12) - self.assertEqual(bundle_1.data['date_joined'].year, 2010) - self.assertEqual(bundle_1.data['date_joined'].day, 30) - self.assertEqual(bundle_1.data.get('bar'), None) - - # Now check the fallback behaviors. - test_object_2 = TestObject() - test_object_2.name = 'Daniel' - basic_2 = BasicResource() - test_bundle_2 = basic_2.build_bundle(obj=test_object_2) - - bundle_2 = basic_2.full_dehydrate(test_bundle_2) - self.assertEqual(bundle_2.data['name'], 'Daniel') - self.assertEqual(bundle_2.data['view_count'], 0) - self.assertEqual(bundle_2.data['date_joined'].year, 2010) - self.assertEqual(bundle_2.data['date_joined'].day, 27) - - test_object_3 = TestObject() - test_object_3.name = 'Joe' - test_object_3.view_count = 5 - test_object_3.created = aware_datetime(2010, 3, 29, 11, 0, 0) - test_object_3.is_active = False - test_object_3.bar = "But sometimes I'm not ignored!" - another_1 = AnotherBasicResource() - test_bundle_3 = another_1.build_bundle(obj=test_object_3) - - another_bundle_1 = another_1.full_dehydrate(test_bundle_3) - self.assertEqual(another_bundle_1.data['name'], 'Joe') - self.assertEqual(another_bundle_1.data['view_count'], 5) - self.assertEqual(another_bundle_1.data['date_joined'].year, 2010) - self.assertEqual(another_bundle_1.data['date_joined'].day, 29) - self.assertEqual(another_bundle_1.data['is_active'], False) - self.assertEqual(another_bundle_1.data['aliases'], ['Mr. Smith', 'John Doe']) - self.assertEqual(another_bundle_1.data['meta'], {'threat': 'high'}) - self.assertEqual(another_bundle_1.data['owed'], Decimal('102.57')) - self.assertEqual(another_bundle_1.data['bar'], "But sometimes I'm not ignored!") - - def test_full_hydrate(self): - basic = BasicResource() - basic_bundle_1 = Bundle(data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0) - }) - - # Now load up the data. - hydrated = basic.full_hydrate(basic_bundle_1) - - self.assertEqual(hydrated.data['name'], 'Daniel') - self.assertEqual(hydrated.data['view_count'], 6) - self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0)) - self.assertEqual(hydrated.obj.name, 'Daniel') - self.assertEqual(hydrated.obj.view_count, 6) - self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0)) - - another = AnotherBasicResource() - another_bundle_1 = Bundle(data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0), - 'aliases': ['test', 'test1'], - 'meta': {'foo': 'bar'}, - 'owed': '12.53', - }) - - # Now load up the data (without the ``bar`` key). - hydrated = another.full_hydrate(another_bundle_1) - - self.assertEqual(hydrated.data['name'], 'Daniel') - self.assertEqual(hydrated.data['view_count'], 6) - self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0)) - self.assertEqual(hydrated.data['aliases'], ['test', 'test1']) - self.assertEqual(hydrated.data['meta'], {'foo': 'bar'}) - self.assertEqual(hydrated.data['owed'], '12.53') - self.assertEqual(hydrated.obj.name, 'Daniel') - self.assertEqual(hydrated.obj.view_count, 6) - self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0)) - self.assertEqual(hasattr(hydrated.obj, 'bar'), False) - - another_bundle_2 = Bundle(data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0), - 'bar': True, - }) - - # Now load up the data (this time with the ``bar`` key). - hydrated = another.full_hydrate(another_bundle_2) - - self.assertEqual(hydrated.data['name'], 'Daniel') - self.assertEqual(hydrated.data['view_count'], 6) - self.assertEqual(hydrated.data['date_joined'], aware_datetime(2010, 2, 15, 12, 0, 0)) - self.assertEqual(hydrated.obj.name, 'Daniel') - self.assertEqual(hydrated.obj.view_count, 6) - self.assertEqual(hydrated.obj.date_joined, aware_datetime(2010, 2, 15, 12, 0, 0)) - self.assertEqual(hydrated.obj.bar, 'O HAI BAR!') - - # Test that a nullable value with a previous non-null value - # can be set to None. - nullable = NullableNameResource() - obj = nullable._meta.object_class() - obj.name = "Daniel" - null_bundle = Bundle(obj=obj, data={'name': None}) - hydrated = nullable.full_hydrate(null_bundle) - - self.assertTrue(hydrated.obj.name is None) - - # Test that a nullable value with a previous non-null value - # is not overridden if no value was given - obj = nullable._meta.object_class() - obj.name = "Daniel" - empty_null_bundle = Bundle(obj=obj, data={}) - hydrated = nullable.full_hydrate(empty_null_bundle) - - self.assertEquals(hydrated.obj.name, "Daniel") - - def test_obj_get_list(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_get_list, bundle) - - def test_obj_delete_list(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_delete_list, bundle) - - def test_obj_get(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_get, bundle, pk=1) - - def test_obj_create(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_create, bundle) - - def test_obj_update(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_update, bundle) - - def test_obj_delete(self): - basic = BasicResource() - bundle = Bundle() - self.assertRaises(NotImplementedError, basic.obj_delete, bundle) - - def test_rollback(self): - basic = BasicResource() - bundles_seen = [] - self.assertRaises(NotImplementedError, basic.rollback, bundles_seen) - - def adjust_schema(self, schema_dict): - for field, field_info in schema_dict['fields'].items(): - if isinstance(field_info['default'], fields.NOT_PROVIDED): - schema_dict['fields'][field]['default'] = 'No default provided.' - - return schema_dict - - def test_build_schema(self): - basic = BasicResource() - schema = self.adjust_schema(basic.build_schema()) - self.assertEqual(schema, { - 'allowed_detail_http_methods': ['get', 'post', 'put', 'delete', 'patch'], - 'allowed_list_http_methods': ['get', 'post', 'put', 'delete', 'patch'], - 'default_format': 'application/json', - 'default_limit': 20, - 'fields': { - 'date_joined': { - 'blank': False, - 'default': 'No default provided.', - 'help_text': 'A date & time as a string. Ex: "2010-11-10T03:07:43"', - 'nullable': True, - 'readonly': False, - 'type': 'datetime', - 'unique': False - }, - 'name': { - 'blank': False, - 'default': 'No default provided.', - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'nullable': False, - 'readonly': False, - 'type': 'string', - 'unique': False - }, - 'resource_uri': { - 'blank': False, - 'default': 'No default provided.', - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'nullable': False, - 'readonly': True, - 'type': 'string', - 'unique': False - }, - 'view_count': { - 'blank': False, - 'default': 0, - 'help_text': 'Integer data. Ex: 2673', - 'nullable': False, - 'readonly': False, - 'type': 'integer', - 'unique': False - } - } - }) - - basic = BasicResource() - basic._meta.ordering = ['date_joined', 'name'] - basic._meta.filtering = {'date_joined': ['gt', 'gte'], 'name': ALL} - schema = self.adjust_schema(basic.build_schema()) - self.assertEqual(schema, { - 'filtering': { - 'name': 1, - 'date_joined': ['gt', 'gte'] - }, - 'allowed_detail_http_methods': ['get', 'post', 'put', 'delete', 'patch'], - 'ordering': ['date_joined', 'name'], - 'fields': { - 'view_count': { - 'nullable': False, - 'default': 0, - 'readonly': False, - 'blank': False, - 'help_text': 'Integer data. Ex: 2673', - 'unique': False, - 'type': 'integer' - }, - 'date_joined': { - 'nullable': True, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'A date & time as a string. Ex: "2010-11-10T03:07:43"', - 'unique': False, - 'type': 'datetime' - }, - 'name': { - 'nullable': False, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - }, - 'resource_uri': { - 'nullable': False, - 'default': 'No default provided.', - 'readonly': True, - 'blank': False, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - } - }, - 'default_format': 'application/json', - 'default_limit': 20, - 'allowed_list_http_methods': ['get', 'post', 'put', 'delete', 'patch'] - }) - - def test_subclassing(self): - class CommonMeta: - default_format = 'application/xml' - - class MiniResource(Resource): - abcd = fields.CharField(default='abcd') - efgh = fields.IntegerField(default=1234) - - class Meta: - resource_name = 'mini' - - mini = MiniResource() - self.assertEqual(len(mini.fields), 3) - - class AnotherMiniResource(MiniResource): - ijkl = fields.BooleanField(default=True) - - class Meta(CommonMeta): - resource_name = 'anothermini' - - another = AnotherMiniResource() - self.assertEqual(len(another.fields), 4) - self.assertEqual(another._meta.default_format, 'application/xml') - - def test_method_check(self): - basic = BasicResource() - request = HttpRequest() - request.method = 'GET' - request.GET = {'format': 'json'} - - # No allowed methods. Kaboom. - self.assertRaises(ImmediateHttpResponse, basic.method_check, request) - - try: - basic.method_check(request) - self.fail("Should have thrown an exception.") - except ImmediateHttpResponse as e: - self.assertEqual(e.response['Allow'], '') - - # Not an allowed request. - self.assertRaises(ImmediateHttpResponse, basic.method_check, request, allowed=['post']) - - try: - basic.method_check(request, allowed=['post']) - self.fail("Should have thrown an exception.") - except ImmediateHttpResponse as e: - self.assertEqual(e.response['Allow'], 'POST') - - # Allowed (single). - request_method = basic.method_check(request, allowed=['get']) - self.assertEqual(request_method, 'get') - - # Allowed (unicode, for Python 2.* with `from __future__ import unicode_literals`) - request_method = basic.method_check(request, allowed=[u'get']) - - # Allowed (multiple). - request_method = basic.method_check(request, allowed=['post', 'get', 'put']) - self.assertEqual(request_method, 'get') - - request = HttpRequest() - request.method = 'POST' - request.POST = {'format': 'json'} - - # Not an allowed request. - self.assertRaises(ImmediateHttpResponse, basic.method_check, request, allowed=['get']) - - try: - basic.method_check(request, allowed=['get', 'put', 'delete', 'patch']) - self.fail("Should have thrown an exception.") - except ImmediateHttpResponse as e: - self.assertEqual(e.response['Allow'], 'GET,PUT,DELETE,PATCH') - - # Allowed (multiple). - request_method = basic.method_check(request, allowed=['post', 'get', 'put']) - self.assertEqual(request_method, 'post') - - def test_auth_check(self): - basic = BasicResource() - request = HttpRequest() - request.GET = {'format': 'json'} - - # Allowed (single). - try: - basic.is_authenticated(request) - except: - self.fail() - - def test_create_response(self): - basic = BasicResource() - request = HttpRequest() - request.GET = {'format': 'json'} - - data = {'hello': 'world'} - output = basic.create_response(request, data) - self.assertEqual(output.status_code, 200) - self.assertEqual(force_text(output.content), '{"hello": "world"}') - - request.GET = {'format': 'xml'} - data = {'objects': [{'hello': 'world', 'abc': 123}], 'meta': {'page': 1}} - output = basic.create_response(request, data) - self.assertEqual(output.status_code, 200) - self.assertEqual(force_text(output.content), '\n1123world') - - def test_mangled(self): - mangled = MangledBasicResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.user = 'mr_authed' - - data = Bundle(data={'hello': 'world'}) - output = mangled.alter_deserialized_detail_data(request, data) - self.assertEqual(output.data, {'hello': 'world', 'user': 'mr_authed'}) - - request.GET = {'format': 'xml'} - data = {'objects': [{'hello': 'world', 'abc': 123}], 'meta': {'page': 1}} - output = mangled.alter_list_data_to_serialize(request, data) - self.assertEqual(output, {'testobjects': [{'abc': 123, 'hello': 'world'}]}) - - def test_get_list_with_use_in(self): - basic = BasicResourceWithDifferentListAndDetailFields() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - basic_resource_list = json.loads(force_text(basic.get_list(request).content))['objects'] - self.assertEquals(basic_resource_list[0]['name'], 'Daniel') - self.assertEquals(basic_resource_list[0]['date_joined'], u'2010-03-30T09:00:00') - - self.assertNotIn('view_count', basic_resource_list[0]) - - -# ==================== -# Model-based tests... -# ==================== - -class DateRecordResource(ModelResource): - class Meta: - queryset = DateRecord.objects.all() - always_return_data = True - authorization = Authorization() - - def hydrate(self, bundle): - bundle.data['message'] = bundle.data['message'].lower() - return bundle - - def hydrate_username(self, bundle): - bundle.data['username'] = bundle.data['username'].upper() - return bundle - - -class NoteResource(ModelResource): - class Meta: - resource_name = 'notes' - authorization = Authorization() - filtering = { - 'content': ['startswith', 'exact'], - 'title': ALL, - 'slug': ['exact'], - } - ordering = ['title', 'slug', 'resource_uri'] - queryset = Note.objects.filter(is_active=True) - serializer = Serializer(formats=['json', 'jsonp', 'xml', 'yaml', 'html', 'plist']) - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/notes/' - - return '/api/v1/notes/%s/' % bundle_or_obj.obj.id - -class NoQuerysetNoteResource(ModelResource): - class Meta: - resource_name = 'noqsnotes' - authorization = Authorization() - filtering = { - 'name': ALL, - } - object_class = Note - - -class LightlyCustomNoteResource(NoteResource): - class Meta: - resource_name = 'noteish' - authorization = Authorization() - allowed_methods = ['get'] - queryset = Note.objects.filter(is_active=True) - - -class TinyLimitNoteResource(NoteResource): - class Meta: - limit = 3 - resource_name = 'littlenote' - authorization = Authorization() - allowed_methods = ['get'] - queryset = Note.objects.filter(is_active=True) - - -class AlwaysDataNoteResource(NoteResource): - class Meta: - resource_name = 'alwaysdatanote' - queryset = Note.objects.filter(is_active=True) - always_return_data = True - authorization = Authorization() - - -class AlwaysDataNoteResourceUseIn(NoteResource): - author = fields.CharField(attribute='author__username', use_in="detail") - constant = fields.IntegerField(default=20, use_in="list") - - class Meta: - resource_name = 'alwaysdatanote' - queryset = Note.objects.filter(is_active=True) - always_return_data = True - authorization = Authorization() - - -class VeryCustomNoteResource(NoteResource): - author = fields.CharField(attribute='author__username') - constant = fields.IntegerField(default=20) - - class Meta: - authorization = Authorization() - limit = 50 - resource_name = 'notey' - serializer = CustomSerializer() - list_allowed_methods = ['get'] - detail_allowed_methods = ['get', 'post', 'put'] - queryset = Note.objects.all() - fields = ['title', 'content', 'created', 'is_active'] - - -class AutoNowNoteResource(ModelResource): - class Meta: - resource_name = 'autonownotes' - queryset = AutoNowNote.objects.filter(is_active=True) - authorization = Authorization() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/autonownotes/' - - return '/api/v1/autonownotes/%s/' % bundle_or_obj.obj.id - - -class CustomPaginator(Paginator): - def page(self): - data = super(CustomPaginator, self).page() - data['extra'] = 'Some extra stuff here.' - return data - - -class CustomPageNoteResource(NoteResource): - class Meta: - limit = 10 - resource_name = 'pagey' - paginator_class = CustomPaginator - queryset = Note.objects.all() - authorization = Authorization() - - -class AlwaysUserNoteResource(NoteResource): - class Meta: - resource_name = 'noteish' - queryset = Note.objects.filter(is_active=True) - authorization = Authorization() - - def get_object_list(self, request): - return super(AlwaysUserNoteResource, self).get_object_list(request).filter(author=request.user) - -class UseInNoteResource(NoteResource): - - content = fields.CharField(attribute='content', use_in='detail') - title = fields.CharField(attribute='title', use_in='list') - - class Meta: - queryset = Note.objects.all() - authorization = Authorization() - -class UserResource(ModelResource): - class Meta: - queryset = User.objects.all() - authorization = Authorization() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/users/' - - return '/api/v1/users/%s/' % bundle_or_obj.obj.id - - -class DetailedNoteResource(ModelResource): - user = fields.ForeignKey(UserResource, 'author') - hello_world = fields.CharField(default='world') - - class Meta: - resource_name = 'detailednotes' - filtering = { - 'content': ['startswith', 'exact'], - 'title': ALL, - 'slug': ['exact'], - 'user': ALL, - 'hello_world': ['exact'], # Note this is invalid for filtering. - } - ordering = ['title', 'slug', 'user'] - queryset = Note.objects.filter(is_active=True) - authorization = Authorization() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/notes/' - - return '/api/v1/notes/%s/' % bundle_or_obj.obj.id - -class DetailedNoteResourceWithHydrate(DetailedNoteResource): - def hydrate(self, bundle): - bundle.data['user'] = bundle.request.user # This should fail using TastyPie 0.9.11 if triggered in patch_list - return bundle - -class RequiredFKNoteResource(ModelResource): - editor = fields.ForeignKey(UserResource, 'editor') - - class Meta: - resource_name = 'requiredfknotes' - queryset = NoteWithEditor.objects.all() - authorization = Authorization() - - -class ThrottledNoteResource(NoteResource): - class Meta: - resource_name = 'throttlednotes' - queryset = Note.objects.filter(is_active=True) - throttle = CacheThrottle(throttle_at=2, timeframe=5, expiration=5) - authorization = Authorization() - - -class BasicAuthNoteResource(NoteResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - authentication = BasicAuthentication() - authorization = Authorization() - - -class NoUriNoteResource(ModelResource): - class Meta: - queryset = Note.objects.filter(is_active=True) - include_resource_uri = False - authorization = Authorization() - - -class WithAbsoluteURLNoteResource(ModelResource): - class Meta: - queryset = Note.objects.filter(is_active=True) - include_absolute_url = True - resource_name = 'withabsoluteurlnote' - authorization = Authorization() - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - if bundle_or_obj is None: - return '/api/v1/withabsoluteurlnote/' - - return '/api/v1/withabsoluteurlnote/%s/' % bundle_or_obj.obj.id - - -class AlternativeCollectionNameNoteResource(ModelResource): - class Meta: - queryset = Note.objects.filter(is_active=True) - collection_name = 'alt_objects' - authorization = Authorization() - - -class SubjectResource(ModelResource): - class Meta: - queryset = Subject.objects.all() - resource_name = 'subjects' - filtering = { - 'name': ALL, - } - authorization = Authorization() - - -class RelatedNoteResource(ModelResource): - author = fields.ForeignKey(UserResource, 'author') - subjects = fields.ManyToManyField(SubjectResource, 'subjects') - - class Meta: - queryset = Note.objects.all() - resource_name = 'relatednotes' - filtering = { - 'author': ALL, - 'subjects': ALL_WITH_RELATIONS, - } - fields = ['title', 'slug', 'content', 'created', 'is_active'] - authorization = Authorization() - - -class AnotherSubjectResource(ModelResource): - notes = fields.ToManyField(DetailedNoteResource, 'notes') - - class Meta: - queryset = Subject.objects.all() - resource_name = 'anothersubjects' - excludes = ['notes'] - filtering = { - 'notes': ALL_WITH_RELATIONS, - } - authorization = Authorization() - - -class AnotherRelatedNoteResource(ModelResource): - author = fields.ForeignKey(UserResource, 'author') - subjects = fields.ManyToManyField(SubjectResource, 'subjects', full=True) - - class Meta: - queryset = Note.objects.all() - resource_name = 'relatednotes' - filtering = { - 'author': ALL, - 'subjects': ALL_WITH_RELATIONS, - } - fields = ['title', 'slug', 'content', 'created', 'is_active'] - authorization = Authorization() - - -class YetAnotherRelatedNoteResource(ModelResource): - author = fields.ForeignKey(UserResource, 'author', full=True) - subjects = fields.ManyToManyField(SubjectResource, 'subjects') - - class Meta: - queryset = Note.objects.all() - resource_name = 'relatednotes' - filtering = { - 'author': ALL, - 'subjects': ALL_WITH_RELATIONS, - } - fields = ['title', 'slug', 'content', 'created', 'is_active'] - authorization = Authorization() - - -class NullableRelatedNoteResource(AnotherRelatedNoteResource): - author = fields.ForeignKey(UserResource, 'author', null=True) - subjects = fields.ManyToManyField(SubjectResource, 'subjects', null=True) - - -class NullableMediaBitResource(ModelResource): - # The old (broke) way to allow ``note`` to be omitted, even though it's a required field. - note = fields.ToOneField(NoteResource, 'note', null=True) - - class Meta: - queryset = MediaBit.objects.all() - resource_name = 'nullablemediabit' - authorization = Authorization() - - -class ReadOnlyRelatedNoteResource(ModelResource): - author = fields.ToOneField(UserResource, 'author', readonly=True) - my_property = fields.CharField(attribute='my_property', null=True, readonly=True) - - class Meta: - queryset = Note.objects.all() - authorization = Authorization() - - -class BlankMediaBitResource(ModelResource): - # Allow ``note`` to be omitted, even though it's a required field. - note = fields.ToOneField(NoteResource, 'note', blank=True) - - class Meta: - queryset = MediaBit.objects.all() - resource_name = 'blankmediabit' - authorization = Authorization() - - # We'll custom populate the note here if it's not present. - # Doesn't make a ton of sense in this context, but for things - # like ``user`` or ``site`` that you can autopopulate based - # on the request. - def hydrate_note(self, bundle): - if not bundle.data.get('note'): - bundle.obj.note = Note.objects.get(pk=1) - - return bundle - - -class TestOptionsResource(ModelResource): - class Meta: - queryset = Note.objects.all() - allowed_methods = ['post'] - list_allowed_methods = ['post', 'put'] - authorization = Authorization() - - -# Per user authorization bits. -class PerUserAuthorization(Authorization): - def read_list(self, object_list, bundle): - if bundle.request and hasattr(bundle.request, 'user'): - if bundle.request.user.is_authenticated(): - object_list = object_list.filter(author=bundle.request.user) - else: - object_list = object_list.none() - - return object_list - - -class PerUserNoteResource(NoteResource): - class Meta: - resource_name = 'perusernotes' - queryset = Note.objects.all() - authorization = PerUserAuthorization() - - def authorized_read_list(self, object_list, bundle): - if object_list._result_cache is not None: - self._pre_limits = len(object_list._result_cache) - else: - self._pre_limits = 0 - - # Just to demonstrate the per-resource hooks. - new_object_list = super(PerUserNoteResource, self).authorized_read_list(object_list, bundle) - - if object_list._result_cache is not None: - self._post_limits = len(object_list._result_cache) - else: - self._post_limits = 0 - - return new_object_list.filter(is_active=True) -# End per user authorization bits. - - -# Per object authorization bits. -class PerObjectAuthorization(Authorization): - def read_list(self, object_list, bundle): - # Does a per-object check that "can't" be expressed as part of a - # ``QuerySet``. This helps test that all objects in the ``QuerySet`` - # aren't loaded & evaluated, only results that match the request. - final_list = [] - - for obj in object_list: - # Only match ``Note`` objects with 'post' in the title. - if 'post' in obj.title.lower(): - final_list.append(obj) - - return final_list - - -class PerObjectNoteResource(NoteResource): - class Meta: - resource_name = 'perobjectnotes' - queryset = Note.objects.all() - authorization = PerObjectAuthorization() - filtering = { - 'is_active': ALL, - } - - def authorized_read_list(self, object_list, bundle): - if object_list._result_cache is not None: - self._pre_limits = len(object_list._result_cache) - else: - self._pre_limits = 0 - - # Check the QuerySet cache to make sure we haven't populated everything. - new_object_list = super(PerObjectNoteResource, self).authorized_read_list(object_list, bundle) - - self._post_limits = len(object_list._result_cache) - return new_object_list -# End per object authorization bits. - - -class CounterResource(ModelResource): - count = fields.IntegerField('count', default=0, null=True) - - class Meta: - queryset = Counter.objects.all() - authorization = Authorization() - - def full_hydrate(self, bundle): - bundle.times_hydrated = getattr(bundle, 'times_hydrated', 0) + 1 - new_shiny = super(CounterResource, self).full_hydrate(bundle) - new_shiny.obj.count = new_shiny.times_hydrated - return new_shiny - - -class CounterAuthorization(Authorization): - def create_detail(self, object_list, bundle, *args, **kwargs): - bundle._create_auth_call_count = getattr(bundle, '_create_auth_call_count', 0) + 1 - return True - - def update_detail(self, object_list, bundle, *args, **kwargs): - bundle._update_auth_call_count = getattr(bundle, '_update_auth_call_count', 0) + 1 - return True - - -class CounterCreateDetailResource(ModelResource): - count = fields.IntegerField('count', default=0, null=True) - - class Meta: - queryset = Counter.objects.all() - authorization = CounterAuthorization() - - -class CounterUpdateDetailResource(ModelResource): - count = fields.IntegerField('count', default=0, null=True) - - class Meta: - queryset = Counter.objects.all() - authorization = CounterAuthorization() - - -class ModelResourceTestCase(TestCase): - fixtures = ['note_testdata.json'] - urls = 'core.tests.field_urls' - - def setUp(self): - super(ModelResourceTestCase, self).setUp() - self.note_1 = Note.objects.get(pk=1) - self.subject_1 = Subject.objects.create( - name='News', - url='/news/' - ) - self.subject_2 = Subject.objects.create( - name='Photos', - url='/photos/' - ) - self.note_1.subjects.add(self.subject_1) - self.note_1.subjects.add(self.subject_2) - - if django.VERSION >= (1, 4): - self.body_attr = "body" - else: - self.body_attr = "raw_post_data" - - @patch('django.core.signals.got_request_exception.send') - @patch('tastypie.resources.ModelResource.obj_get_list', side_effect=IOError) - def test_exception_handling(self, obj_get_list_mock, send_signal_mock): - request = HttpRequest() - request.method = 'GET' - resource = NoteResource() - res = resource.wrap_view('dispatch_list')(request) - self.assertTrue(obj_get_list_mock.called, msg="Test invalid: obj_get_list should have been dispatched") - self.assertTrue(send_signal_mock.called, msg="got_request_exception was not called after an error") - - def test_escaping(self): - request = HttpRequest() - request.method = 'GET' - request.GET = { - 'limit': '', - } - resource = NoteResource() - res = resource.wrap_view('dispatch_list')(request) - self.assertEqual(res.status_code, 400) - err_data = json.loads(res.content.decode('utf-8')) - self.assertTrue('<script>alert(1)</script>' in err_data['error']) - - def test_init(self): - # Very minimal & stock. - resource_1 = NoteResource() - self.assertEqual(len(resource_1.fields), 8) - self.assertNotEqual(resource_1._meta.queryset, None) - self.assertEqual(resource_1._meta.resource_name, 'notes') - self.assertEqual(resource_1._meta.limit, 20) - self.assertEqual(resource_1._meta.list_allowed_methods, ['get', 'post', 'put', 'delete', 'patch']) - self.assertEqual(resource_1._meta.detail_allowed_methods, ['get', 'post', 'put', 'delete', 'patch']) - self.assertEqual(isinstance(resource_1._meta.serializer, Serializer), True) - - # Lightly custom. - resource_2 = LightlyCustomNoteResource() - self.assertEqual(len(resource_2.fields), 8) - self.assertNotEqual(resource_2._meta.queryset, None) - self.assertEqual(resource_2._meta.resource_name, 'noteish') - self.assertEqual(resource_2._meta.limit, 20) - self.assertEqual(resource_2._meta.list_allowed_methods, ['get']) - self.assertEqual(resource_2._meta.detail_allowed_methods, ['get']) - self.assertEqual(isinstance(resource_2._meta.serializer, Serializer), True) - - # Highly custom. - resource_3 = VeryCustomNoteResource() - self.assertEqual(len(resource_3.fields), 7) - self.assertNotEqual(resource_3._meta.queryset, None) - self.assertEqual(resource_3._meta.resource_name, 'notey') - self.assertEqual(resource_3._meta.limit, 50) - self.assertEqual(resource_3._meta.list_allowed_methods, ['get']) - self.assertEqual(resource_3._meta.detail_allowed_methods, ['get', 'post', 'put']) - self.assertEqual(isinstance(resource_3._meta.serializer, CustomSerializer), True) - - # Note - automatic resource naming. - resource_4 = NoUriNoteResource() - self.assertEqual(resource_4._meta.resource_name, 'nourinote') - - # Test to make sure that, even with a mix of basic & advanced - # configuration, options are set right. - resource_5 = TestOptionsResource() - self.assertEqual(resource_5._meta.allowed_methods, ['post']) - # Should be the overridden values. - self.assertEqual(resource_5._meta.list_allowed_methods, ['post', 'put']) - # Should inherit from the basic configuration. - self.assertEqual(resource_5._meta.detail_allowed_methods, ['post']) - - resource_6 = CustomPageNoteResource() - self.assertEqual(resource_6._meta.paginator_class, CustomPaginator) - - def test_can_create(self): - resource_1 = NoteResource() - self.assertEqual(resource_1.can_create(), True) - - resource_2 = LightlyCustomNoteResource() - self.assertEqual(resource_2.can_create(), False) - - def test_can_update(self): - resource_1 = NoteResource() - self.assertEqual(resource_1.can_update(), True) - - resource_2 = LightlyCustomNoteResource() - self.assertEqual(resource_2.can_update(), False) - - resource_3 = TestOptionsResource() - self.assertEqual(resource_3.can_update(), True) - - def test_can_delete(self): - resource_1 = NoteResource() - self.assertEqual(resource_1.can_delete(), True) - - resource_2 = LightlyCustomNoteResource() - self.assertEqual(resource_2.can_delete(), False) - - def test_fields(self): - # Different from the ``ResourceTestCase.test_fields``, we're checking - # some related bits here & self-referential bits later on. - resource_1 = RelatedNoteResource() - self.assertEqual(len(resource_1.fields), 8) - self.assert_('author' in resource_1.fields) - self.assertTrue(isinstance(resource_1.fields['author'], fields.ToOneField)) - self.assertEqual(resource_1.fields['author']._resource, resource_1.__class__) - self.assertEqual(resource_1.fields['author'].instance_name, 'author') - self.assertTrue('subjects' in resource_1.fields) - self.assertTrue(isinstance(resource_1.fields['subjects'], fields.ToManyField)) - self.assertEqual(resource_1.fields['subjects']._resource, resource_1.__class__) - self.assertEqual(resource_1.fields['subjects'].instance_name, 'subjects') - - # Sanity check the other introspected fields. - annr = AutoNowNoteResource() - self.assertEqual(len(annr.fields), 8) - self.assertEqual(sorted(annr.fields.keys()), ['content', 'created', 'id', 'is_active', 'resource_uri', 'slug', 'title', 'updated']) - - self.assertTrue(isinstance(annr.fields['content'], fields.CharField)) - self.assertEqual(annr.fields['content'].attribute, 'content') - self.assertEqual(annr.fields['content'].blank, True) - self.assertEqual(annr.fields['content']._default, '') - self.assertEqual(annr.fields['content'].instance_name, 'content') - self.assertEqual(annr.fields['content'].null, False) - self.assertEqual(annr.fields['content'].readonly, False) - self.assertEqual(annr.fields['content'].unique, False) - self.assertEqual(annr.fields['content'].value, None) - - self.assertTrue(isinstance(annr.fields['created'], fields.DateTimeField)) - self.assertEqual(annr.fields['created'].attribute, 'created') - self.assertEqual(annr.fields['created'].blank, False) - self.assertTrue(isinstance(annr.fields['created']._default(), datetime.datetime)) - self.assertEqual(annr.fields['created'].instance_name, 'created') - self.assertEqual(annr.fields['created'].null, True) - self.assertEqual(annr.fields['created'].readonly, False) - self.assertEqual(annr.fields['created'].unique, False) - self.assertEqual(annr.fields['created'].value, None) - - self.assertTrue(isinstance(annr.fields['id'], fields.IntegerField)) - self.assertEqual(annr.fields['id'].attribute, 'id') - self.assertEqual(annr.fields['id'].blank, True) - self.assertEqual(annr.fields['id']._default, '') - self.assertEqual(annr.fields['id'].instance_name, 'id') - self.assertEqual(annr.fields['id'].null, False) - self.assertEqual(annr.fields['id'].readonly, False) - self.assertEqual(annr.fields['id'].unique, True) - self.assertEqual(annr.fields['id'].value, None) - - self.assertTrue(isinstance(annr.fields['is_active'], fields.BooleanField)) - self.assertEqual(annr.fields['is_active'].attribute, 'is_active') - self.assertEqual(annr.fields['is_active'].blank, True) - self.assertEqual(annr.fields['is_active']._default, True) - self.assertEqual(annr.fields['is_active'].instance_name, 'is_active') - self.assertEqual(annr.fields['is_active'].null, False) - self.assertEqual(annr.fields['is_active'].readonly, False) - self.assertEqual(annr.fields['is_active'].unique, False) - self.assertEqual(annr.fields['is_active'].value, None) - - self.assertTrue(isinstance(annr.fields['resource_uri'], fields.CharField)) - self.assertEqual(annr.fields['resource_uri'].attribute, None) - self.assertEqual(annr.fields['resource_uri'].blank, False) - self.assertEqual(annr.fields['resource_uri']._default, fields.NOT_PROVIDED) - self.assertEqual(annr.fields['resource_uri'].instance_name, 'resource_uri') - self.assertEqual(annr.fields['resource_uri'].null, False) - self.assertEqual(annr.fields['resource_uri'].readonly, True) - self.assertEqual(annr.fields['resource_uri'].unique, False) - self.assertEqual(annr.fields['resource_uri'].value, None) - - self.assertTrue(isinstance(annr.fields['slug'], fields.CharField)) - self.assertEqual(annr.fields['slug'].attribute, 'slug') - self.assertEqual(annr.fields['slug'].blank, False) - self.assertEqual(annr.fields['slug']._default, fields.NOT_PROVIDED) - self.assertEqual(annr.fields['slug'].instance_name, 'slug') - self.assertEqual(annr.fields['slug'].null, False) - self.assertEqual(annr.fields['slug'].readonly, False) - self.assertEqual(annr.fields['slug'].unique, True) - self.assertEqual(annr.fields['slug'].value, None) - - self.assertTrue(isinstance(annr.fields['title'], fields.CharField)) - self.assertEqual(annr.fields['title'].attribute, 'title') - self.assertEqual(annr.fields['title'].blank, False) - self.assertEqual(annr.fields['title']._default, fields.NOT_PROVIDED) - self.assertEqual(annr.fields['title'].instance_name, 'title') - self.assertEqual(annr.fields['title'].null, False) - self.assertEqual(annr.fields['title'].readonly, False) - self.assertEqual(annr.fields['title'].unique, False) - self.assertEqual(annr.fields['title'].value, None) - - self.assertTrue(isinstance(annr.fields['updated'], fields.DateTimeField)) - self.assertEqual(annr.fields['updated'].attribute, 'updated') - self.assertEqual(annr.fields['updated'].blank, True) - self.assertTrue(isinstance(annr.fields['updated']._default(), datetime.datetime)) - self.assertEqual(annr.fields['updated'].instance_name, 'updated') - self.assertEqual(annr.fields['updated'].null, False) - self.assertEqual(annr.fields['updated'].readonly, False) - self.assertEqual(annr.fields['updated'].unique, False) - self.assertEqual(annr.fields['updated'].value, None) - - def test_urls(self): - # The common case, where the ``Api`` specifies the name. - resource = NoteResource(api_name='v1') - patterns = resource.urls - self.assertEqual(len(patterns), 4) - self.assertEqual([pattern.name for pattern in patterns], ['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail']) - self.assertEqual(reverse('api_dispatch_list', kwargs={ - 'api_name': 'v1', - 'resource_name': 'notes', - }), '/api/v1/notes/') - self.assertEqual(reverse('api_dispatch_detail', kwargs={ - 'api_name': 'v1', - 'resource_name': 'notes', - 'pk': 1, - }), '/api/v1/notes/1/') - - # Start over. - resource = NoteResource() - patterns = resource.urls - self.assertEqual(len(patterns), 4) - self.assertEqual([pattern.name for pattern in patterns], ['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail']) - self.assertEqual(reverse('api_dispatch_list', urlconf='core.tests.manual_urls', kwargs={ - 'resource_name': 'notes', - }), '/notes/') - self.assertEqual(reverse('api_dispatch_detail', urlconf='core.tests.manual_urls', kwargs={ - 'resource_name': 'notes', - 'pk': 1, - }), '/notes/1/') - - def test_get_via_uri(self): - resource = NoteResource(api_name='v1') - note_1 = resource.get_via_uri('/api/v1/notes/1/') - self.assertEqual(note_1.pk, 1) - # Should work even if app name is the same as resource - note_1 = resource.get_via_uri('/notes/api/v1/notes/1/') - self.assertEqual(note_1.pk, 1) - - try: - should_fail = resource.get_via_uri('http://example.com/') - self.fail("'get_via_uri' should fail miserably with something that isn't an object URI.") - except NotFound: - pass - - try: - should_also_fail = resource.get_via_uri('/api/v1/notes/') - self.fail("'get_via_uri' should fail miserably with something that isn't an object URI.") - except MultipleObjectsReturned: - pass - - # Check with the request. - request = HttpRequest() - note_1 = resource.get_via_uri('/api/v1/notes/1/', request=request) - self.assertEqual(note_1.pk, 1) - - def test_create_identifier(self): - resource = NoteResource() - new_note = Note.objects.get(pk=1) - self.assertEqual(resource.create_identifier(new_note), 'core.note.1') - - def test_determine_format(self): - resource = NoteResource() - request = HttpRequest() - - # Default. - self.assertEqual(resource.determine_format(request), 'application/json') - - # Test forcing the ``format`` parameter. - request.GET = {'format': 'json'} - self.assertEqual(resource.determine_format(request), 'application/json') - - request.GET = {'format': 'jsonp'} - self.assertEqual(resource.determine_format(request), 'text/javascript') - - request.GET = {'format': 'xml'} - self.assertEqual(resource.determine_format(request), 'application/xml') - - request.GET = {'format': 'yaml'} - self.assertEqual(resource.determine_format(request), 'text/yaml') - - request.GET = {'format': 'foo'} - self.assertEqual(resource.determine_format(request), 'application/json') - - # Test the ``Accept`` header. - request.META = {'HTTP_ACCEPT': 'application/json'} - self.assertEqual(resource.determine_format(request), 'application/json') - - request.META = {'HTTP_ACCEPT': 'text/javascript'} - self.assertEqual(resource.determine_format(request), 'text/javascript') - - request.META = {'HTTP_ACCEPT': 'application/xml'} - self.assertEqual(resource.determine_format(request), 'application/xml') - - request.META = {'HTTP_ACCEPT': 'text/yaml'} - self.assertEqual(resource.determine_format(request), 'text/yaml') - - request.META = {'HTTP_ACCEPT': 'text/html'} - self.assertEqual(resource.determine_format(request), 'text/html') - - request.META = {'HTTP_ACCEPT': 'application/json,application/xml;q=0.9,*/*;q=0.8'} - self.assertEqual(resource.determine_format(request), 'application/json') - - request.META = {'HTTP_ACCEPT': 'text/plain,application/xml,application/json;q=0.9,*/*;q=0.8'} - self.assertEqual(resource.determine_format(request), 'application/xml') - - def adjust_schema(self, schema_dict): - for field, field_info in schema_dict['fields'].items(): - if isinstance(field_info['default'], fields.NOT_PROVIDED): - schema_dict['fields'][field]['default'] = 'No default provided.' - if isinstance(field_info['default'], (datetime.datetime, datetime.date)): - schema_dict['fields'][field]['default'] = 'The current date.' - - return schema_dict - - def test_build_schema(self): - related = RelatedNoteResource() - schema = self.adjust_schema(related.build_schema()) - self.assertEqual(schema, { - 'filtering': { - 'subjects': 2, - 'author': 1 - }, - 'allowed_detail_http_methods': ['get', 'post', 'put', 'delete', 'patch'], - 'fields': { - 'author': { - 'related_type': 'to_one', - 'nullable': False, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'A single related resource. Can be either a URI or set of nested resource data.', - 'unique': False, - 'type': 'related' - }, - 'title': { - 'nullable': False, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - }, - 'created': { - 'nullable': False, - 'default': 'The current date.', - 'readonly': False, - 'blank': False, - 'help_text': 'A date & time as a string. Ex: "2010-11-10T03:07:43"', - 'unique': False, - 'type': 'datetime' - }, - 'is_active': { - 'nullable': False, - 'default': True, - 'readonly': False, - 'blank': True, - 'help_text': 'Boolean data. Ex: True', - 'unique': False, - 'type': 'boolean' - }, - 'content': { - 'nullable': False, - 'default': '', - 'readonly': False, - 'blank': True, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - }, - 'subjects': { - 'related_type': 'to_many', - 'nullable': False, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'Many related resources. Can be either a list of URIs or list of individually nested resource data.', - 'unique': False, - 'type': 'related' - }, - 'slug': { - 'nullable': False, - 'default': 'No default provided.', - 'readonly': False, - 'blank': False, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - }, - 'resource_uri': { - 'nullable': False, - 'default': 'No default provided.', - 'readonly': True, - 'blank': False, - 'help_text': 'Unicode string data. Ex: "Hello World"', - 'unique': False, - 'type': 'string' - } - }, - 'default_format': 'application/json', - 'default_limit': 20, - 'allowed_list_http_methods': ['get', 'post', 'put', 'delete', 'patch'] - }) - - def test_build_filters(self): - resource = NoteResource() - - # Valid none. - self.assertEqual(resource.build_filters(), {}) - self.assertEqual(resource.build_filters(filters=None), {}) - - # Not in the filtering dict. - self.assertEqual(resource.build_filters(filters={'resource_url__exact': '/foo/bar/'}), {}) - - # Filter valid but disallowed. - self.assertRaises(InvalidFilterError, resource.build_filters, filters={'slug__startswith': 'whee'}) - - # Skipped due to not being recognized. - self.assertEqual(resource.build_filters(filters={'moof__exact': 'baz'}), {}) - - # Invalid simple (implicit ``__exact``). - self.assertEqual(resource.build_filters(filters={'title': 'Hello world.'}), {'title__exact': 'Hello world.'}) - - # Valid simple (explicit ``__exact``). - self.assertEqual(resource.build_filters(filters={'title__exact': 'Hello world.'}), {'title__exact': 'Hello world.'}) - - # Valid in (using ``,``). - self.assertEqual(resource.build_filters(filters={'title__in': ''}), {'title__in': ''}) - self.assertEqual(resource.build_filters(filters={'title__in': 'foo'}), {'title__in': ['foo']}) - self.assertEqual(resource.build_filters(filters={'title__in': 'foo,bar'}), {'title__in': ['foo', 'bar']}) - - # Valid in (using multiple params). - self.assertEqual(resource.build_filters(filters=QueryDict('title__in=foo&title__in=bar')), {'title__in': ['foo', 'bar']}) - self.assertEqual(resource.build_filters(filters=QueryDict('title__in=foo,bar')), {'title__in': ['foo', 'bar']}) - - # Valid simple (non-``__exact``). - self.assertEqual(resource.build_filters(filters={'content__startswith': 'Hello'}), {'content__startswith': 'Hello'}) - - # Valid boolean. - self.assertEqual(resource.build_filters(filters={'title': 'true'}), {'title__exact': True}) - self.assertEqual(resource.build_filters(filters={'title': 'True'}), {'title__exact': True}) - self.assertEqual(resource.build_filters(filters={'title': True}), {'title__exact': True}) - self.assertEqual(resource.build_filters(filters={'title': 'false'}), {'title__exact': False}) - self.assertEqual(resource.build_filters(filters={'title': 'False'}), {'title__exact': False}) - self.assertEqual(resource.build_filters(filters={'title': False}), {'title__exact': False}) - self.assertEqual(resource.build_filters(filters={'title': 'nil'}), {'title__exact': None}) - self.assertEqual(resource.build_filters(filters={'title': 'none'}), {'title__exact': None}) - self.assertEqual(resource.build_filters(filters={'title': 'None'}), {'title__exact': None}) - self.assertEqual(resource.build_filters(filters={'title': None}), {'title__exact': None}) - - # Valid multiple. - self.assertEqual(resource.build_filters(filters={ - 'slug__exact': 'Hello', - 'title__exact': 'RAGE', - 'content__startswith': 'A thing here.' - }), {'slug__exact': 'Hello', 'content__startswith': 'A thing here.', 'title__exact': 'RAGE'}) - - # Valid multiple (model attribute differs from field name). - resource_2 = DetailedNoteResource() - filters_1 = { - 'slug__exact': 'Hello', - 'title__exact': 'RAGE', - 'content__startswith': 'A thing here.', - 'user__gt': 2, - } - self.assertEqual(resource_2.build_filters(filters=filters_1), {'title__exact': 'RAGE', 'slug__exact': 'Hello', 'author__gt': 2, 'content__startswith': 'A thing here.'}) - - # No relationship traversal to the filter, please. - resource_3 = RelatedNoteResource() - self.assertRaises(InvalidFilterError, resource_3.build_filters, filters={'author__username__startswith': 'j'}) - - # Allow relationship traversal. - self.assertEqual(resource_3.build_filters(filters={'subjects__name__startswith': 'News'}), {'subjects__name__startswith': 'News'}) - - # Ensure related fields that do not have filtering throw an exception. - self.assertRaises(InvalidFilterError, resource_3.build_filters, filters={'subjects__url__startswith': 'News'}) - - # Ensure related fields that do not exist throw an exception. - self.assertRaises(InvalidFilterError, resource_3.build_filters, filters={'subjects__foo__startswith': 'News'}) - - # Check where the field name doesn't match the database relation. - resource_4 = AnotherSubjectResource() - self.assertEqual(resource_4.build_filters(filters={'notes__user__startswith': 'Daniel'}), {'notes__author__startswith': 'Daniel'}) - - # Make sure that fields that don't have attributes can't be filtered on. - self.assertRaises(InvalidFilterError, resource_4.build_filters, filters={'notes__hello_world': 'News'}) - - # Make sure build_filters works even on resources without queryset - resource = NoQuerysetNoteResource() - self.assertEqual(resource.build_filters(), {}) - - def test_xss_regressions(self): - # Make sure the body is JSON & the content-type is right. - resource = RelatedNoteResource() - request = HttpRequest() - request.method = 'GET' - - request.GET = { - 'format': 'xml', - 'author__username__startswith': 'j', - } - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp['content-type'], 'application/xml; charset=utf-8') - self.assertEqual(resp.content.decode('utf-8'), "\nLookups are not allowed more than one level deep on the 'author' field.") - - request.GET = { - 'format': 'json', - 'author__': 'j', - } - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp['content-type'], 'application/json') - self.assertEqual(resp.content.decode('utf-8'), '{"error": "Lookups are not allowed more than one level deep on the \'author\' field."}') - - request.GET = { - 'format': 'json', - 'limit': '', - } - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp['content-type'], 'application/json') - self.assertEqual(resp.content.decode('utf-8'), '{"error": "Invalid limit \'<img%20src=\\"http://ycombinator.com/images/y18.gif\\">\' provided. Please provide a positive integer."}') - - request.GET = { - 'format': 'json', - 'limit': '', - } - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp['content-type'], 'application/json') - self.assertEqual(resp.content.decode('utf-8'), '{"error": "Invalid limit \'<img%20src=\\"http://ycombinator.com/images/y18.gif\\">\' provided. Please provide a positive integer."}') - - request.GET = { - 'format': 'json', - 'offset': '', - } - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp['content-type'], 'application/json') - self.assertEqual(resp.content.decode('utf-8'), '{"error": "Invalid offset \'<script>alert(\\"XSS\\")</script>\' provided. Please provide an integer."}') - - def test_apply_sorting(self): - resource = NoteResource() - base_bundle = Bundle() - - # Valid none. - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list) - self.assertEqual([obj.id for obj in ordered_list], [1, 2, 4, 6]) - - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options=None) - self.assertEqual([obj.id for obj in ordered_list], [1, 2, 4, 6]) - - # Not a valid field. - object_list = resource.obj_get_list(base_bundle) - self.assertRaises(InvalidSortError, resource.apply_sorting, object_list, options={'order_by': 'foobar'}) - - # Not in the ordering dict. - object_list = resource.obj_get_list(base_bundle) - self.assertRaises(InvalidSortError, resource.apply_sorting, object_list, options={'order_by': 'content'}) - - # No attribute to sort by. - object_list = resource.obj_get_list(base_bundle) - self.assertRaises(InvalidSortError, resource.apply_sorting, object_list, options={'order_by': 'resource_uri'}) - - # Valid ascending. - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'order_by': 'title'}) - self.assertEqual([obj.id for obj in ordered_list], [2, 1, 6, 4]) - - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'order_by': 'slug'}) - self.assertEqual([obj.id for obj in ordered_list], [2, 1, 6, 4]) - - # Valid descending. - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'order_by': '-title'}) - self.assertEqual([obj.id for obj in ordered_list], [4, 6, 1, 2]) - - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'order_by': '-slug'}) - self.assertEqual([obj.id for obj in ordered_list], [4, 6, 1, 2]) - - # Ensure the deprecated parameter still works. - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'sort_by': '-title'}) - self.assertEqual([obj.id for obj in ordered_list], [4, 6, 1, 2]) - - # Valid combination. - object_list = resource.obj_get_list(base_bundle) - ordered_list = resource.apply_sorting(object_list, options={'order_by': ['title', '-slug']}) - self.assertEqual([obj.id for obj in ordered_list], [2, 1, 6, 4]) - - # Valid (model attribute differs from field name). - resource_2 = DetailedNoteResource(base_bundle) - object_list = resource_2.obj_get_list(base_bundle) - ordered_list = resource_2.apply_sorting(object_list, options={'order_by': '-user'}) - self.assertEqual([obj.id for obj in ordered_list], [6, 4, 2, 1]) - - # Invalid relation. - resource_2 = DetailedNoteResource() - object_list = resource_2.obj_get_list(base_bundle) - ordered_list = resource_2.apply_sorting(object_list, options={'order_by': '-user__baz'}) - - try: - [obj.id for obj in ordered_list] - self.fail() - except FieldError: - pass - - # Valid relation. - resource_2 = DetailedNoteResource() - object_list = resource_2.obj_get_list(base_bundle) - ordered_list = resource_2.apply_sorting(object_list, options={'order_by': 'user__id'}) - self.assertEqual([obj.id for obj in ordered_list], [1, 2, 4, 6]) - - resource_2 = DetailedNoteResource() - object_list = resource_2.obj_get_list(base_bundle) - ordered_list = resource_2.apply_sorting(object_list, options={'order_by': '-user__id'}) - self.assertEqual([obj.id for obj in ordered_list], [6, 4, 2, 1]) - - # Valid relational combination. - resource_2 = DetailedNoteResource() - object_list = resource_2.obj_get_list(base_bundle) - ordered_list = resource_2.apply_sorting(object_list, options={'order_by': ['-user__username', 'title']}) - self.assertEqual([obj.id for obj in ordered_list], [2, 1, 6, 4]) - - def test_get_list(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 4}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": 4, "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": 6, "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}]}') - - # Test slicing. - # First an invalid offset. - request.GET = {'format': 'json', 'offset': 'abc', 'limit': 1} - try: - resp = resource.get_list(request) - self.fail() - except BadRequest as e: - pass - - # Try again with ``wrap_view`` for sanity. - resp = resource.wrap_view('get_list')(request) - self.assertEqual(resp.status_code, 400) - - # Then an out of range offset. - request.GET = {'format': 'json', 'offset': -1, 'limit': 1} - try: - resp = resource.get_list(request) - self.fail() - except BadRequest as e: - pass - - # Then an out of range limit. - request.GET = {'format': 'json', 'offset': 0, 'limit': -1} - try: - resp = resource.get_list(request) - self.fail() - except BadRequest as e: - pass - - # Valid slice. - request.GET = {'format': 'json', 'offset': 0, 'limit': 2} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 2) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertTrue('/api/v1/notes/?' in list_data['meta']['next']) - self.assertTrue('format=json' in list_data['meta']['next']) - self.assertTrue('limit=2' in list_data['meta']['next']) - self.assertTrue('offset=2' in list_data['meta']['next']) - self.assertEqual(list_data['objects'], [ - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - } - ]) - - # Valid, slightly overlapping slice. - request.GET = {'format': 'json', 'offset': 1, 'limit': 2} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 2) - self.assertEqual(list_data['meta']['offset'], 1) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertTrue('/api/v1/notes/?' in list_data['meta']['next']) - self.assertTrue('format=json' in list_data['meta']['next']) - self.assertTrue('limit=2' in list_data['meta']['next']) - self.assertTrue('offset=3' in list_data['meta']['next']) - self.assertEqual(list_data['objects'], [ - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - } - ]) - - # Valid, non-overlapping slice. - request.GET = {'format': 'json', 'offset': 3, 'limit': 2} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 2) - self.assertEqual(list_data['meta']['offset'], 3) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['next'], None) - self.assertTrue('/api/v1/notes/?' in list_data['meta']['previous']) - self.assertTrue('format=json' in list_data['meta']['previous']) - self.assertTrue('limit=2' in list_data['meta']['previous']) - self.assertTrue('offset=1' in list_data['meta']['previous']) - self.assertEqual(list_data['objects'], [ - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - } - ]) - - # Valid, but beyond the bounds slice. - request.GET = {'format': 'json', 'offset': 100, 'limit': 2} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 2) - self.assertEqual(list_data['meta']['offset'], 100) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['next'], None) - self.assertTrue('/api/v1/notes/?' in list_data['meta']['previous']) - self.assertTrue('format=json' in list_data['meta']['previous']) - self.assertTrue('limit=2' in list_data['meta']['previous']) - self.assertTrue('offset=98' in list_data['meta']['previous']) - self.assertEqual(list_data['objects'], []) - - # Valid slice, fetch all results. - request.GET = {'format': 'json', 'offset': 0, 'limit': 0} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 1000) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertEqual(list_data['meta']['next'], None) - self.assertEqual(list_data['objects'], [ - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - }, - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - } - ]) - - # Valid sorting. - request.GET = {'format': 'json', 'order_by': 'title'} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 20) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertEqual(list_data['meta']['next'], None) - self.assertEqual(list_data['objects'], [ - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - } - ]) - - request.GET = {'format': 'json', 'order_by': '-title'} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 20) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertEqual(list_data['meta']['next'], None) - self.assertEqual(list_data['objects'], [ - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - }, - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - }, - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - } - ]) - - #invalid sorting - request.GET = {'format': 'json', 'order_by': 'monkey'} - resp = resource.wrap_view('get_list')(request) - self.assertEqual(resp.status_code, 400) - res = json.loads(resp.content.decode('utf-8')) - self.assertTrue('error' in res.keys()) - self.assertTrue('monkey' in res['error']) #Error looks like "No matching \'monkey\' field for ordering on. - - # Test to make sure we're not inadvertently caching the QuerySet. - request.GET = {'format': 'json'} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 20) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 4) - self.assertEqual(list_data['meta']['previous'], None) - self.assertEqual(list_data['meta']['next'], None) - self.assertEqual(list_data['objects'], [ - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - }, - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - } - ]) - new_note = Note.objects.create( - title='Another fresh note.', - slug='another-fresh-note', - content='Whee!', - created=aware_datetime(2010, 7, 21, 11, 23), - updated=aware_datetime(2010, 7, 21, 11, 23), - ) - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 20) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 5) - self.assertEqual(list_data['meta']['previous'], None) - self.assertEqual(list_data['meta']['next'], None) - self.assertEqual(list_data['objects'], [ - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - }, - { - "content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", - "created": "2010-04-02T10:05:00", - "id": 6, - "is_active": True, - "resource_uri": "/api/v1/notes/6/", - "slug": "grannys-gone", - "title": "Granny\'s Gone", - "updated": "2010-04-02T10:05:00" - }, - { - "content": "Whee!", - "created": "2010-07-21T11:23:00", - "id": 7, - "is_active": True, - "resource_uri": "/api/v1/notes/7/", - "slug": "another-fresh-note", - "title": "Another fresh note.", - "updated": make_naive(new_note.updated).isoformat() - } - ]) - - # Regression - Ensure that the limit on the Resource gets used if - # no other limit is requested. - resource = TinyLimitNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - list_data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(list_data['meta']['limit'], 3) - self.assertEqual(list_data['meta']['offset'], 0) - self.assertEqual(list_data['meta']['total_count'], 5) - self.assertEqual(list_data['meta']['previous'], None) - self.assertTrue('/api/v1/notes/?' in list_data['meta']['next']) - self.assertTrue('format=json' in list_data['meta']['next']) - self.assertTrue('limit=3' in list_data['meta']['next']) - self.assertTrue('offset=3' in list_data['meta']['next']) - self.assertEqual(list_data['objects'], [ - { - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "created": "2010-03-30T20:05:00", - "id": 1, - "is_active": True, - "resource_uri": "/api/v1/notes/1/", - "slug": "first-post", - "title": "First Post!", - "updated": "2010-03-30T20:05:00" - }, - { - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "created": "2010-03-31T20:05:00", - "id": 2, - "is_active": True, - "resource_uri": "/api/v1/notes/2/", - "slug": "another-post", - "title": "Another Post", - "updated": "2010-03-31T20:05:00" - }, - { - "content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", - "created": "2010-04-01T20:05:00", - "id": 4, - "is_active": True, - "resource_uri": "/api/v1/notes/4/", - "slug": "recent-volcanic-activity", - "title": "Recent Volcanic Activity.", - "updated": "2010-04-01T20:05:00" - } - ]) - - def test_get_list_use_in(self): - resource = UseInNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - resp = resource.get_list(request) - self.assertEqual(resp.status_code, 200) - resp = json.loads(resp.content.decode('utf-8')) - for note in resp['objects']: - self.assertNotIn('content', note) - - - def test_get_detail(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - - resp = resource.get_detail(request, pk=1) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - - resp = resource.get_detail(request, pk=2) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}') - - resp = resource.get_detail(request, pk=300) - self.assertEqual(resp.status_code, 404) - - def test_get_detail_use_in(self): - resource = UseInNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - resp = resource.get_detail(request, pk=1) - self.assertEqual(resp.status_code, 200) - resp = json.loads(resp.content.decode('utf-8')) - self.assertNotIn('title', resp) - - def test_put_list(self): - resource = NoteResource() - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - - self.assertEqual(Note.objects.count(), 6) - setattr(request, self.body_attr, '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}]}') - - resp = resource.put_list(request) - self.assertEqual(resp.status_code, 204) - self.assertEqual(resp.content.decode('utf-8'), '') - self.assertEqual(Note.objects.count(), 3) - self.assertEqual(Note.objects.filter(is_active=True).count(), 1) - new_note = Note.objects.get(slug='cat-is-back-again') - self.assertEqual(new_note.content, "The cat is back. The dog coughed him up out back.") - - always_resource = AlwaysDataNoteResource() - resp = always_resource.put_list(request) - self.assertEqual(resp.status_code, 200) - self.assertTrue(resp.content.decode('utf-8').startswith('{"objects": [')) - - def test_put_list_with_use_in(self): - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - - self.assertEqual(Note.objects.count(), 6) - setattr(request, self.body_attr, '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}]}') - - always_resource = AlwaysDataNoteResourceUseIn() - resp = always_resource.put_list(request) - self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content.decode('utf-8')) - self.assertTrue(len(content['objects']) == 1) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - def test_put_detail(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}') - - resp = resource.put_detail(request, pk=10) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Note.objects.count(), 7) - new_note = Note.objects.get(slug='cat-is-back') - self.assertEqual(new_note.content, "The cat is back. The dog coughed him up out back.") - - setattr(request, self.body_attr, '{"content": "The cat is gone again. I think it was the rabbits that ate him this time.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Gone", "updated": "2010-04-03 20:05:00"}') - - resp = resource.put_detail(request, pk=10) - self.assertEqual(resp.status_code, 204) - self.assertEqual(Note.objects.count(), 7) - new_note = Note.objects.get(slug='cat-is-back') - self.assertEqual(new_note.content, u'The cat is gone again. I think it was the rabbits that ate him this time.') - - always_resource = AlwaysDataNoteResource() - resp = always_resource.put_detail(request, pk=10) - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertTrue("id" in data) - self.assertEqual(data["id"], 10) - self.assertTrue("content" in data) - self.assertEqual(data["content"], "The cat is gone again. I think it was the rabbits that ate him this time.") - self.assertTrue("resource_uri" in data) - self.assertTrue("title" in data) - self.assertTrue("is_active" in data) - - # Now make sure we can null-out a relation. - # Associate some data first. - new_note = Note.objects.get(slug='cat-is-back') - new_note.author = User.objects.get(username='johndoe') - new_note.save() - nullable_resource = NullableRelatedNoteResource() - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00", "author": null}') - - resp = nullable_resource.put_detail(request, pk=10) - self.assertEqual(resp.status_code, 204) - self.assertEqual(Note.objects.count(), 7) - new_note = Note.objects.get(slug='cat-is-back') - self.assertEqual(new_note.author, None) - - def test_put_detail_with_use_in(self): - new_note = Note.objects.get(slug='another-post') - new_note.author = User.objects.get(username='johndoe') - new_note.save() - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}') - - always_resource = AlwaysDataNoteResourceUseIn() - resp = always_resource.put_detail(request, pk=new_note.pk) - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertTrue("id" in data) - self.assertEqual(data["id"], new_note.pk) - self.assertTrue("author" in data) - self.assertFalse("constant" in data) - self.assertTrue("resource_uri" in data) - self.assertTrue("title" in data) - self.assertTrue("is_active" in data) - - def test_put_detail_with_identifiers(self): - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"date": "2012-09-07", "username": "WAT", "message": "hello"}') - - date_record_resource = DateRecordResource() - resp = date_record_resource.put_detail(request, username="maraujop") - - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(data['username'], "MARAUJOP") - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"date": "WAT", "username": "maraujop", "message": "hello"}') - - date_record_resource = DateRecordResource() - resp = date_record_resource.put_detail(request, date="2012-09-07") - - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(data['date'], "2012-09-07T00:00:00") - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - setattr(request, self.body_attr, '{"date": "2012-09-07", "username": "maraujop", "message": "WAT"}') - date_record_resource = DateRecordResource() - resp = date_record_resource.put_detail(request, message="HELLO") - - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(data['message'], "hello") - - def test_post_list(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - setattr(request, self.body_attr, '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}') - - resp = resource.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Note.objects.count(), 7) - new_note = Note.objects.get(slug='cat-is-back') - self.assertEqual(new_note.content, "The cat is back. The dog coughed him up out back.") - - always_resource = AlwaysDataNoteResource() - resp = always_resource.post_list(request) - self.assertEqual(resp.status_code, 201) - data = json.loads(resp.content.decode('utf-8')) - self.assertTrue("id" in data) - self.assertEqual(data["id"], 8) - self.assertTrue("content" in data) - self.assertEqual(data["content"], "The cat is back. The dog coughed him up out back.") - self.assertTrue("resource_uri" in data) - self.assertTrue("title" in data) - self.assertTrue("is_active" in data) - - def test_post_detail(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - - resp = resource.post_detail(request, pk=2) - self.assertEqual(resp.status_code, 501) - - def test_delete_list(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'DELETE' - - resp = resource.delete_list(request) - self.assertEqual(resp.status_code, 204) - # Only the non-actives are left alive. - self.assertEqual(Note.objects.count(), 2) - - def test_delete_detail(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'DELETE' - - resp = resource.delete_detail(request, pk=2) - self.assertEqual(resp.status_code, 204) - self.assertEqual(Note.objects.count(), 5) - - def test_patch_list(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - - self.assertEqual(Note.objects.count(), 6) - request._raw_post_data = request._body = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}, {"resource_uri": "/api/v1/notes/2/", "content": "This is note 2."}], "deleted_objects": ["/api/v1/notes/1/"]}' - - resp = resource.patch_list(request) - self.assertEqual(resp.status_code, 202) - self.assertEqual(resp.content.decode('utf-8'), '') - self.assertEqual(Note.objects.count(), 6) - self.assertEqual(Note.objects.filter(is_active=True).count(), 4) - new_note = Note.objects.get(slug='cat-is-back-again') - self.assertEqual(new_note.content, "The cat is back. The dog coughed him up out back.") - updated_note = Note.objects.get(pk=2) - self.assertEqual(updated_note.content, "This is note 2.") - - def test_patch_list_return_data(self): - always_resource = AlwaysDataNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - - self.assertEqual(Note.objects.count(), 6) - request._raw_post_data = request._body = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}, {"resource_uri": "/api/v1/alwaysdatanote/2/", "content": "This is note 2."}], "deleted_objects": ["/api/v1/alwaysdatanote/1/"]}' - - resp = always_resource.patch_list(request) - self.assertEqual(resp.status_code, 202) - self.assertTrue(resp.content.decode('utf-8').startswith('{"objects": [')) - - def test_patch_list_return_data_use_in(self): - always_resource = AlwaysDataNoteResourceUseIn() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - - self.assertEqual(Note.objects.count(), 6) - request._raw_post_data = request._body = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}, {"resource_uri": "/api/v1/alwaysdatanote/2/", "content": "This is note 2."}], "deleted_objects": ["/api/v1/alwaysdatanote/1/"]}' - - resp = always_resource.patch_list(request) - self.assertEqual(resp.status_code, 202) - - content = json.loads(resp.content.decode('utf-8')) - - self.assertEqual(len(content['objects']), 2) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - def test_patch_list_bad_resource_uri(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - - self.assertEqual(Note.objects.count(), 6) - request._raw_post_data = request._body = '{"objects": [{"resource_uri": "/api/v1/notes/99999/", "content": "This is an invalid resource_uri", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "invalid-uri", "title": "Invalid URI", "updated": "2010-04-03 20:05:00"}]}' - - resp = resource.patch_list(request) - self.assertEqual(resp.status_code, 202) - self.assertEqual(resp.content.decode('utf-8'), '') - self.assertEqual(Note.objects.count(), 7) - new_note = Note.objects.get(slug='invalid-uri') - self.assertEqual(new_note.content, "This is an invalid resource_uri") - - def test_patch_list_with_request_data(self): - """ - Verify that request data is accessible in a Resource's hydrate method after patch_list. - """ - resource = DetailedNoteResourceWithHydrate() - request = HttpRequest() - request.user = User.objects.get(username='johndoe') - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False # Not sure what this line does, copied from above - request._raw_post_data = request._body = '{"objects": [{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-again-again", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00"}]}' - - resp = resource.patch_list(request) - self.assertEqual(resp.status_code, 202) - self.assertEqual(resp.content.decode('utf-8'), '') - self.assertEqual(Note.objects.filter(author=request.user, slug="cat-again-again").count(), 1) # Validate that request.user was successfully passed in - - def test_patch_detail(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - request._raw_post_data = request._body = '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00"}' - - resp = resource.patch_detail(request, pk=10) - self.assertEqual(resp.status_code, 404) - - resp = resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - self.assertEqual(Note.objects.count(), 6) - note = Note.objects.get(pk=1) - self.assertEqual(note.content, "The cat is back. The dog coughed him up out back.") - self.assertEqual(note.created, aware_datetime(2010, 4, 3, 20, 5)) - - request._raw_post_data = request._body = '{"content": "The cat is gone again. I think it was the rabbits that ate him this time."}' - - resp = resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - self.assertEqual(Note.objects.count(), 6) - new_note = Note.objects.get(pk=1) - self.assertEqual(new_note.content, u'The cat is gone again. I think it was the rabbits that ate him this time.') - - always_resource = AlwaysDataNoteResource() - request._raw_post_data = request._body = '{"content": "Wait, now the cat is back."}' - resp = always_resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - data = json.loads(resp.content.decode('utf-8')) - self.assertTrue("id" in data) - self.assertEqual(data["id"], 1) - self.assertTrue("content" in data) - self.assertEqual(data["content"], u'Wait, now the cat is back.') - self.assertTrue("resource_uri" in data) - self.assertTrue("title" in data) - self.assertTrue("is_active" in data) - - def test_patch_detail_use_in(self): - self.assertEqual(Note.objects.count(), 6) - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request._read_started = False - request._raw_post_data = request._body = '{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00"}' - - resp = resource.patch_detail(request, pk=10) - self.assertEqual(resp.status_code, 404) - - resp = resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - self.assertEqual(Note.objects.count(), 6) - note = Note.objects.get(pk=1) - self.assertEqual(note.content, "The cat is back. The dog coughed him up out back.") - self.assertEqual(note.created, aware_datetime(2010, 4, 3, 20, 5)) - - request._raw_post_data = request._body = '{"content": "The cat is gone again. I think it was the rabbits that ate him this time."}' - - resp = resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - self.assertEqual(Note.objects.count(), 6) - new_note = Note.objects.get(pk=1) - self.assertEqual(new_note.content, u'The cat is gone again. I think it was the rabbits that ate him this time.') - - always_resource = AlwaysDataNoteResourceUseIn() - request._raw_post_data = request._body = '{"content": "Wait, now the cat is back."}' - resp = always_resource.patch_detail(request, pk=1) - self.assertEqual(resp.status_code, 202) - data = json.loads(resp.content.decode('utf-8')) - self.assertTrue("id" in data) - self.assertEqual(data["id"], 1) - self.assertTrue("author" in data) - self.assertFalse("constant" in data) - self.assertTrue("content" in data) - self.assertEqual(data["content"], u'Wait, now the cat is back.') - self.assertTrue("resource_uri" in data) - self.assertTrue("title" in data) - self.assertTrue("is_active" in data) - - def test_dispatch_list(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - resp = resource.dispatch_list(request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 4}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": 4, "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": 6, "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}]}') - - def test_dispatch_detail(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - resp = resource.dispatch_detail(request, pk=1) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - - def test_dispatch(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - resp = resource.dispatch('list', request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"meta": {"limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 4}, "objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": 4, "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": 6, "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}]}') - - resp = resource.dispatch('detail', request, pk=1) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - - # Check for an override. - request.META = { - 'HTTP_X_HTTP_METHOD_OVERRIDE': 'PATCH', - } - request._read_started = False - request._raw_post_data = request._body = '{"title": "Super-duper override ACTIVATE!"}' - resp = resource.dispatch('detail', request, pk=1) - self.assertEqual(resp.status_code, 202) - self.assertEqual(resp.content.decode('utf-8'), '') - self.assertEqual(Note.objects.get(pk=1).title, u'Super-duper override ACTIVATE!') - - def test_build_bundle(self): - resource = NoteResource() - - unpopulated_bundle = resource.build_bundle() - self.assertTrue(isinstance(unpopulated_bundle, Bundle)) - self.assertEqual(unpopulated_bundle.data, {}) - - populated_bundle = resource.build_bundle(data={'title': 'Foo'}) - self.assertTrue(isinstance(populated_bundle, Bundle)) - self.assertEqual(populated_bundle.data, {'title': 'Foo'}) - - req = HttpRequest() - req.GET = {'foo': 'bar'} - populated_bundle_with_request = resource.build_bundle(data={'title': 'Foo'}, request=req) - self.assertTrue(isinstance(populated_bundle_with_request, Bundle)) - self.assertEqual(populated_bundle_with_request.data, {'title': 'Foo'}) - self.assertEqual(populated_bundle_with_request.request.GET['foo'], 'bar') - - def test_obj_get_list(self): - resource = NoteResource() - base_bundle = Bundle() - - object_list = resource.obj_get_list(base_bundle) - self.assertEqual(len(object_list), 4) - self.assertEqual(object_list[0].title, u'First Post!') - - notes = NoteResource().obj_get_list(base_bundle) - self.assertEqual(len(notes), 4) - self.assertEqual(notes[0].is_active, True) - self.assertEqual(notes[0].title, u'First Post!') - self.assertEqual(notes[1].is_active, True) - self.assertEqual(notes[1].title, u'Another Post') - self.assertEqual(notes[2].is_active, True) - self.assertEqual(notes[2].title, u'Recent Volcanic Activity.') - self.assertEqual(notes[3].is_active, True) - self.assertEqual(notes[3].title, u"Granny's Gone") - - customs = VeryCustomNoteResource().obj_get_list(base_bundle) - self.assertEqual(len(customs), 6) - self.assertEqual(customs[0].is_active, True) - self.assertEqual(customs[0].title, u'First Post!') - self.assertEqual(customs[0].author.username, u'johndoe') - self.assertEqual(customs[1].is_active, True) - self.assertEqual(customs[1].title, u'Another Post') - self.assertEqual(customs[1].author.username, u'johndoe') - self.assertEqual(customs[2].is_active, False) - self.assertEqual(customs[2].title, u'Hello World!') - self.assertEqual(customs[2].author.username, u'janedoe') - self.assertEqual(customs[3].is_active, True) - self.assertEqual(customs[3].title, u'Recent Volcanic Activity.') - self.assertEqual(customs[3].author.username, u'janedoe') - self.assertEqual(customs[4].is_active, False) - self.assertEqual(customs[4].title, u'My favorite new show') - self.assertEqual(customs[4].author.username, u'johndoe') - self.assertEqual(customs[5].is_active, True) - self.assertEqual(customs[5].title, u"Granny's Gone") - self.assertEqual(customs[5].author.username, u'janedoe') - - # Ensure filtering by request params works. - mock_request = MockRequest() - mock_request.GET['title'] = u"Granny's Gone" - base_bundle.request = mock_request - notes = NoteResource().obj_get_list(bundle=base_bundle) - self.assertEqual(len(notes), 1) - self.assertEqual(notes[0].title, u"Granny's Gone") - - # Ensure kwargs override request params. - mock_request = MockRequest() - mock_request.GET['title'] = u"Granny's Gone" - base_bundle.request = mock_request - notes = NoteResource().obj_get_list(bundle=base_bundle, title='Recent Volcanic Activity.') - self.assertEqual(len(notes), 1) - self.assertEqual(notes[0].title, u'Recent Volcanic Activity.') - - def test_apply_filters(self): - nr = NoteResource() - mock_request = MockRequest() - - # No filters. - notes = nr.apply_filters(mock_request, {}) - self.assertEqual(len(notes), 4) - - filters = { - 'title': u"Granny's Gone" - } - notes = nr.apply_filters(mock_request, filters) - self.assertEqual(len(notes), 1) - self.assertEqual(notes[0].title, u"Granny's Gone") - - filters = { - 'title__icontains': u"post", - 'created__lte': datetime.date(2010, 6, 30), - } - notes = nr.apply_filters(mock_request, filters) - self.assertEqual(len(notes), 2) - self.assertEqual(notes[0].title, u'First Post!') - self.assertEqual(notes[1].title, u'Another Post') - - def test_obj_get(self): - resource = NoteResource() - base_bundle = Bundle() - - obj = resource.obj_get(base_bundle, pk=1) - self.assertTrue(isinstance(obj, Note)) - self.assertEqual(obj.title, u'First Post!') - - # Test non-pk gets. - obj = resource.obj_get(base_bundle, slug='another-post') - self.assertTrue(isinstance(obj, Note)) - self.assertEqual(obj.title, u'Another Post') - - note = NoteResource() - note_obj = note.obj_get(base_bundle, pk=1) - self.assertEqual(note_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?') - self.assertEqual(note_obj.created, aware_datetime(2010, 3, 30, 20, 5)) - self.assertEqual(note_obj.is_active, True) - self.assertEqual(note_obj.slug, u'first-post') - self.assertEqual(note_obj.title, u'First Post!') - self.assertEqual(note_obj.updated, aware_datetime(2010, 3, 30, 20, 5)) - - custom = VeryCustomNoteResource() - custom_obj = custom.obj_get(base_bundle, pk=1) - self.assertEqual(custom_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?') - self.assertEqual(custom_obj.created, aware_datetime(2010, 3, 30, 20, 5)) - self.assertEqual(custom_obj.is_active, True) - self.assertEqual(custom_obj.author.username, u'johndoe') - self.assertEqual(custom_obj.title, u'First Post!') - - related = RelatedNoteResource() - related_obj = related.obj_get(base_bundle, pk=1) - self.assertEqual(related_obj.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?') - self.assertEqual(related_obj.created, aware_datetime(2010, 3, 30, 20, 5)) - self.assertEqual(related_obj.is_active, True) - self.assertEqual(related_obj.author.username, u'johndoe') - self.assertEqual(related_obj.title, u'First Post!') - self.assertEqual(list(related_obj.subjects.values_list('id', flat=True)), [1, 2]) - - def test_uri_fields(self): - with_abs_url = WithAbsoluteURLNoteResource() - base_bundle = Bundle() - with_abs_url_obj = with_abs_url.obj_get(base_bundle, pk=1) - - with_abs_url_bundle = with_abs_url.build_bundle(obj=with_abs_url_obj) - abs_bundle = with_abs_url.full_dehydrate(with_abs_url_bundle) - self.assertEqual(abs_bundle.data['resource_uri'], '/api/v1/withabsoluteurlnote/1/') - self.assertEqual(abs_bundle.data['absolute_url'], u'/some/fake/path/1/') - - def test_jsonp_validation(self): - resource = NoteResource() - - # invalid JSONP callback should return Http400 - request = HttpRequest() - request.GET = {'format': 'jsonp', 'callback': '()'} - request.method = 'GET' - try: - resp = resource.dispatch_detail(request, pk=1) - self.fail() - except BadRequest as e: - pass - - # Try again with ``wrap_view`` for sanity. - resp = resource.wrap_view('dispatch_detail')(request, pk=1) - self.assertEqual(resp.status_code, 400) - self.assertEqual(force_text(resp.content), '{"error": "JSONP callback name is invalid."}') - self.assertEqual(resp['content-type'], 'application/json') - - # valid JSONP callback should work - request = HttpRequest() - request.GET = {'format': 'jsonp', 'callback': 'myCallback'} - request.method = 'GET' - resp = resource.dispatch_detail(request, pk=1) - self.assertEqual(resp.status_code, 200) - - def test_get_schema(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - # Patch the ``created/updated`` defaults for testability. - old_created = resource.fields['created']._default - old_updated = resource.fields['updated']._default - resource.fields['created']._default = aware_datetime(2011, 9, 24, 0, 2) - resource.fields['updated']._default = aware_datetime(2011, 9, 24, 0, 2) - - resp = resource.get_schema(request) - self.assertEqual(resp.status_code, 200) - schema = { - "allowed_detail_http_methods": ["get", "post", "put", "delete", "patch"], - "allowed_list_http_methods": ["get", "post", "put", "delete", "patch"], - "default_format": "application/json", - "default_limit": 20, - "fields": { - "content": { - "blank": True, - "default": "", - "help_text": "Unicode string data. Ex: \"Hello World\"", - "nullable": False, - "readonly": False, - "type": "string", - "unique": False - }, - "created": { - "blank": False, - "default": "2011-09-24T00:02:00", - "help_text": "A date & time as a string. Ex: \"2010-11-10T03:07:43\"", - "nullable": False, - "readonly": False, - "type": "datetime", - "unique": False - }, - "id": { - "blank": True, - "default": "", - "help_text": "Integer data. Ex: 2673", - "nullable": False, - "readonly": False, - "type": "integer", - "unique": True - }, - "is_active": { - "blank": True, - "default": True, - "help_text": "Boolean data. Ex: True", - "nullable": False, - "readonly": False, - "type": "boolean", - "unique": False - }, - "resource_uri": { - "blank": False, - "default": "No default provided.", - "help_text": "Unicode string data. Ex: \"Hello World\"", - "nullable": False, - "readonly": True, - "type": "string", - "unique": False - }, - "slug": { - "blank": False, - "default": "No default provided.", - "help_text": "Unicode string data. Ex: \"Hello World\"", - "nullable": False, - "readonly": False, - "type": "string", - "unique": False - }, - "title": { - "blank": False, - "default": "No default provided.", - "help_text": "Unicode string data. Ex: \"Hello World\"", - "nullable": False, - "readonly": False, - "type": "string", - "unique": False - }, - "updated": { - "blank": False, - "default": "2011-09-24T00:02:00", - "help_text": "A date & time as a string. Ex: \"2010-11-10T03:07:43\"", - "nullable": False, - "readonly": False, - "type": "datetime", - "unique": False - } - }, - "filtering": { - "content": ["startswith", "exact"], - "slug": ["exact"], - "title": 1 - }, - "ordering": ["title", "slug", "resource_uri"], - } - self.assertEqual(json.loads(resp.content.decode('utf-8')), schema) - - # Unpatch. - resource.fields['created']._default = old_created - resource.fields['updated']._default = old_updated - - def test_get_multiple(self): - resource = NoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - resp = resource.get_multiple(request, pk_list='1') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}]}') - - resp = resource.get_multiple(request, pk_list='1;2') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}]}') - - resp = resource.get_multiple(request, pk_list='2;3') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"not_found": ["3"], "objects": [{"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}]}') - - resp = resource.get_multiple(request, pk_list='1;2;4;6') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"objects": [{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "/api/v1/notes/2/", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": 4, "is_active": true, "resource_uri": "/api/v1/notes/4/", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": 6, "is_active": true, "resource_uri": "/api/v1/notes/6/", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}]}') - - def test_get_multiple_use_in(self): - resource = AlwaysDataNoteResourceUseIn() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - resp = resource.get_multiple(request, pk_list='1') - self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content.decode('utf-8')) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - resp = resource.get_multiple(request, pk_list='1;2') - self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content.decode('utf-8')) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - resp = resource.get_multiple(request, pk_list='2;3') - self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content.decode('utf-8')) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - resp = resource.get_multiple(request, pk_list='1;2;4;6') - self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content.decode('utf-8')) - for note in content['objects']: - self.assertIn('constant', note) - self.assertNotIn('author', note) - - def test_check_throttling(self): - # Stow. - old_debug = settings.DEBUG - settings.DEBUG = False - - resource = ThrottledNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - # Not throttled. - resp = resource.dispatch('list', request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(cache.get('noaddr_nohost_accesses')), 1) - - # Not throttled. - resp = resource.dispatch('list', request) - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(cache.get('noaddr_nohost_accesses')), 2) - - # Throttled. - try: - resp = resource.dispatch('list', request) - self.fail() - except ImmediateHttpResponse as e: - self.assertEqual(e.response.status_code, 429) - self.assertEqual(len(cache.get('noaddr_nohost_accesses')), 2) - - # Throttled. - try: - resp = resource.dispatch('list', request) - self.fail() - except ImmediateHttpResponse as e: - self.assertEqual(e.response.status_code, 429) - self.assertEqual(len(cache.get('noaddr_nohost_accesses')), 2) - - # Check the ``wrap_view``. - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp.status_code, 429) - self.assertEqual(len(cache.get('noaddr_nohost_accesses')), 2) - - # Restore. - settings.DEBUG = old_debug - - def test_generate_cache_key(self): - resource = NoteResource() - self.assertEqual(resource.generate_cache_key(), 'None:notes::') - self.assertEqual(resource.generate_cache_key('abc', '123'), 'None:notes:abc:123:') - self.assertEqual(resource.generate_cache_key(foo='bar', moof='baz'), 'None:notes::foo=bar:moof=baz') - self.assertEqual(resource.generate_cache_key('abc', '123', foo='bar', moof='baz'), 'None:notes:abc:123:foo=bar:moof=baz') - - def test_cached_fetch_list(self): - resource = NoteResource() - base_bundle = Bundle() - - object_list = resource.cached_obj_get_list(base_bundle) - self.assertEqual(len(object_list), 4) - self.assertEqual(object_list[0].title, u'First Post!') - - def test_cached_fetch_detail(self): - resource = NoteResource() - base_bundle = Bundle() - - obj = resource.cached_obj_get(base_bundle, pk=1) - self.assertTrue(isinstance(obj, Note)) - self.assertEqual(obj.title, u'First Post!') - - def test_configuration(self): - note = NoteResource() - self.assertEqual(len(note.fields), 8) - self.assertEqual(sorted(note.fields.keys()), ['content', 'created', 'id', 'is_active', 'resource_uri', 'slug', 'title', 'updated']) - self.assertEqual(note.fields['content'].default, '') - - custom = VeryCustomNoteResource() - self.assertEqual(len(custom.fields), 7) - self.assertEqual(sorted(custom.fields.keys()), ['author', 'constant', 'content', 'created', 'is_active', 'resource_uri', 'title']) - - no_uri = NoUriNoteResource() - self.assertEqual(len(no_uri.fields), 7) - self.assertEqual(sorted(no_uri.fields.keys()), ['content', 'created', 'id', 'is_active', 'slug', 'title', 'updated']) - - with_abs_url = WithAbsoluteURLNoteResource() - self.assertEqual(len(with_abs_url.fields), 9) - self.assertEqual(sorted(with_abs_url.fields.keys()), ['absolute_url', 'content', 'created', 'id', 'is_active', 'resource_uri', 'slug', 'title', 'updated']) - - def test_obj_delete_list_custom_qs(self): - self.assertEqual(len(Note.objects.all()), 6) - base_bundle = Bundle() - notes = NoteResource().obj_delete_list(base_bundle) - self.assertEqual(len(Note.objects.all()), 2) - - def test_obj_delete_list_basic_qs(self): - self.assertEqual(len(Note.objects.all()), 6) - base_bundle = Bundle() - customs = VeryCustomNoteResource().obj_delete_list(base_bundle) - self.assertEqual(len(Note.objects.all()), 0) - - def test_obj_delete_list_non_queryset(self): - class NonQuerysetNoteResource(ModelResource): - class Meta: - queryset = Note.objects.all() - - def authorized_delete_list(self, object_list, bundle): - return tuple(object_list[:2]) - - request = HttpRequest() - request.method = 'DELETE' - self.assertEqual(len(Note.objects.all()), 6) - # This is a regression. Used to fail miserably. - notes = NonQuerysetNoteResource().delete_list(request=request) - self.assertEqual(len(Note.objects.all()), 4) - - def test_obj_delete_list_filtered(self): - self.assertEqual(Note.objects.all().count(), 6) - - note_to_delete = Note.objects.filter(is_active=True)[0] - - request = HttpRequest() - request.method = 'DELETE' - request.GET = {'slug':str(note_to_delete.slug)} - NoteResource().delete_list(request=request) - self.assertEqual(len(Note.objects.all()), 5) - - def test_obj_create(self): - self.assertEqual(Note.objects.all().count(), 6) - note = NoteResource() - bundle = Bundle(data={ - 'title': "A new post!", - 'slug': "a-new-post", - 'content': "Testing, 1, 2, 3!", - 'is_active': True - }) - note.obj_create(bundle) - self.assertEqual(Note.objects.all().count(), 7) - latest = Note.objects.get(slug='a-new-post') - self.assertEqual(latest.title, u"A new post!") - self.assertEqual(latest.slug, u'a-new-post') - self.assertEqual(latest.content, u'Testing, 1, 2, 3!') - self.assertEqual(latest.is_active, True) - - self.assertEqual(Note.objects.all().count(), 7) - note = RelatedNoteResource() - related_bundle = Bundle(data={ - 'title': "Yet another new post!", - 'slug': "yet-another-new-post", - 'content': "WHEEEEEE!", - 'is_active': True, - 'author': '/api/v1/user/1/', - 'subjects': ['/api/v1/subjects/2/'], - }) - note.obj_create(related_bundle) - self.assertEqual(Note.objects.all().count(), 8) - latest = Note.objects.get(slug='yet-another-new-post') - self.assertEqual(latest.title, u"Yet another new post!") - self.assertEqual(latest.slug, u'yet-another-new-post') - self.assertEqual(latest.content, u'WHEEEEEE!') - self.assertEqual(latest.is_active, True) - self.assertEqual(latest.author.username, u'johndoe') - self.assertEqual(latest.subjects.all().count(), 1) - self.assertEqual([sub.id for sub in latest.subjects.all()], [2]) - - self.assertEqual(Note.objects.all().count(), 8) - note = AnotherRelatedNoteResource() - related_bundle = Bundle(data={ - 'title': "Yet another another new post!", - 'slug': "yet-another-another-new-post", - 'content': "WHEEEEEE!", - 'is_active': True, - 'author': '/api/v1/user/1/', - 'subjects': [{ - 'name': 'helloworld', - 'url': 'http://example.com', - 'created': '2010-05-20 14:22:00', - }], - }) - note.obj_create(related_bundle) - self.assertEqual(Note.objects.all().count(), 9) - latest = Note.objects.get(slug='yet-another-another-new-post') - self.assertEqual(latest.title, u"Yet another another new post!") - self.assertEqual(latest.slug, u'yet-another-another-new-post') - self.assertEqual(latest.content, u'WHEEEEEE!') - self.assertEqual(latest.is_active, True) - self.assertEqual(latest.author.username, u'johndoe') - self.assertEqual(latest.subjects.all().count(), 1) - self.assertEqual([sub.id for sub in latest.subjects.all()], [3]) - - self.assertEqual(Note.objects.all().count(), 9) - self.assertEqual(User.objects.filter(username='snerble').count(), 0) - note = YetAnotherRelatedNoteResource() - related_bundle = Bundle(data={ - 'title': "Yet yet another another new post!", - 'slug': "yet-yet-another-another-new-post", - 'content': "WHOA!!!", - 'is_active': True, - 'author': { - 'username': 'snerble', - 'password': 'hunter42', - }, - 'subjects': [], - }) - note.obj_create(related_bundle) - self.assertEqual(Note.objects.all().count(), 10) - latest = Note.objects.get(slug='yet-yet-another-another-new-post') - self.assertEqual(latest.title, u"Yet yet another another new post!") - self.assertEqual(latest.slug, u'yet-yet-another-another-new-post') - self.assertEqual(latest.content, u'WHOA!!!') - self.assertEqual(latest.is_active, True) - self.assertEqual(latest.author.username, u'snerble') - self.assertEqual(latest.subjects.all().count(), 0) - - note = RequiredFKNoteResource() - related_bundle = Bundle(data={ - 'slug': 'note-with-editor', - 'editor': { - 'username': 'zeus', - 'password': 'apollo', - }, - }) - note.obj_create(related_bundle) - latest = NoteWithEditor.objects.get(slug='note-with-editor') - self.assertEqual(latest.editor.username, u'zeus') - - def test_obj_create_full_hydrate_on_create_authorization(self): - cr = CounterCreateDetailResource() - counter_bundle = cr.build_bundle(data={ - "name": "About", - "slug": "about", - }, obj=Counter()) - cr.obj_create(counter_bundle) - - self.assertEquals(counter_bundle._create_auth_call_count, 1) - self.assertEquals(counter_bundle.obj.name, "About") - self.assertEquals(counter_bundle.obj.slug, "about") - - def test_obj_update(self): - self.assertEqual(Note.objects.all().count(), 6) - note = NoteResource() - base_bundle = Bundle() - note_obj = note.obj_get(base_bundle, pk=1) - note_bundle = note.build_bundle(obj=note_obj) - note_bundle = note.full_dehydrate(note_bundle) - note_bundle.data['title'] = 'Whee!' - note.obj_update(note_bundle, pk=1) - self.assertEqual(Note.objects.all().count(), 6) - numero_uno = Note.objects.get(pk=1) - self.assertEqual(numero_uno.title, u'Whee!') - self.assertEqual(numero_uno.slug, u'first-post') - self.assertEqual(numero_uno.content, u'This is my very first post using my shiny new API. Pretty sweet, huh?') - self.assertEqual(numero_uno.is_active, True) - - self.assertEqual(Note.objects.all().count(), 6) - note = RelatedNoteResource() - related_obj = note.obj_get(base_bundle, pk=1) - related_bundle = Bundle(obj=related_obj, data={ - 'title': "Yet another new post!", - 'slug': "yet-another-new-post", - 'content': "WHEEEEEE!", - 'is_active': True, - 'author': '/api/v1/user/2/', - 'subjects': ['/api/v1/subjects/2/', '/api/v1/subjects/1/'], - }) - note.obj_update(related_bundle, pk=1) - self.assertEqual(Note.objects.all().count(), 6) - latest = Note.objects.get(slug='yet-another-new-post') - self.assertEqual(latest.title, u"Yet another new post!") - self.assertEqual(latest.slug, u'yet-another-new-post') - self.assertEqual(latest.content, u'WHEEEEEE!') - self.assertEqual(latest.is_active, True) - self.assertEqual(latest.author.username, u'janedoe') - self.assertEqual(latest.subjects.all().count(), 2) - self.assertEqual([sub.id for sub in latest.subjects.all()], [1, 2]) - - self.assertEqual(Note.objects.all().count(), 6) - note = AnotherRelatedNoteResource() - related_obj = note.obj_get(base_bundle, pk=1) - related_bundle = Bundle(data={ - 'title': "Yet another another new post!", - 'slug': "yet-another-another-new-post", - 'content': "WHEEEEEE!", - 'is_active': True, - 'author': '/api/v1/user/1/', - 'subjects': [{ - 'name': 'helloworld', - 'url': 'http://example.com', - 'created': '2010-05-20 14:22:00', - }], - }) - note.obj_update(related_bundle, pk=1) - self.assertEqual(Note.objects.all().count(), 6) - latest = Note.objects.get(slug='yet-another-another-new-post') - self.assertEqual(latest.title, u"Yet another another new post!") - self.assertEqual(latest.slug, u'yet-another-another-new-post') - self.assertEqual(latest.content, u'WHEEEEEE!') - self.assertEqual(latest.is_active, True) - self.assertEqual(latest.author.username, u'johndoe') - self.assertEqual(latest.subjects.all().count(), 1) - self.assertEqual([sub.id for sub in latest.subjects.all()], [3]) - - # Fix non-native types (like datetimes) during attempted hydration. - # This ensures that handing the wrong type should get coerced to the - # right thing. - self.assertEqual(Note.objects.all().count(), 6) - note = NoteResource() - note_obj = note.obj_get(base_bundle, pk=1) - self.assertEqual(note_obj.title, u'Yet another another new post!') - self.assertEqual(note_obj.created, aware_datetime(2010, 3, 30, 20, 5)) - note_bundle = note.build_bundle(obj=note_obj) - note_bundle = note.full_dehydrate(note_bundle) - note_bundle.data['title'] = 'OMGOMGOMGOMG!' - note_bundle.data['created'] = aware_datetime(2011, 11, 23, 1, 0, 0) - note.obj_update(note_bundle, pk=1, created='2010-03-30T20:05:00') - self.assertEqual(Note.objects.all().count(), 6) - numero_uno = Note.objects.get(pk=1) - self.assertEqual(numero_uno.title, u'OMGOMGOMGOMG!') - self.assertEqual(numero_uno.slug, u'yet-another-another-new-post') - self.assertEqual(numero_uno.content, u'WHEEEEEE!') - self.assertEqual(numero_uno.created, aware_datetime(2011, 11, 23, 1, 0)) - - # Now try a lookup that should fail. - note = NoteResource() - note_bundle = note.build_bundle(data={ - "author": "/api/v1/user/1/", - "title": "Something something Post!", - "slug": "something-something-post", - "content": "Stock post content.", - "is_active": True, - "created": "2011-03-30 20:05:00", - "updated": "2011-03-30 20:05:00" - }) - self.assertRaises(NotFound, note.obj_update, note_bundle, pk=1, created='2010-03-31T20:05:00') - self.assertEqual(Note.objects.all().count(), 6) - - # Assign based on the ``request.user``, which helps ensure that - # the correct ``request`` is being passed along. - request = HttpRequest() - request.user = User.objects.get(username='johndoe') - base_bundle.request = request - self.assertEqual(AlwaysUserNoteResource().get_object_list(request).count(), 2) - note = AlwaysUserNoteResource() - note_obj = note.obj_get(base_bundle, pk=1) - note_bundle = note.build_bundle(obj=note_obj) - note_bundle = note.full_dehydrate(note_bundle) - note_bundle.data['title'] = 'Whee!' - note_bundle.request = request - note.obj_update(note_bundle, pk=1) - self.assertEqual(Note.objects.all().count(), 6) - numero_uno = Note.objects.get(pk=1) - self.assertEqual(numero_uno.title, u'Whee!') - self.assertEqual(numero_uno.slug, u'yet-another-another-new-post') - self.assertEqual(numero_uno.content, u'WHEEEEEE!') - self.assertEqual(numero_uno.is_active, True) - self.assertEqual(numero_uno.author.pk, request.user.pk) - - def test_obj_update_single_hydrate(self): - counter = Counter.objects.get(pk=1) - self.assertEqual(counter.count, 1) - cr = CounterResource() - counter_bundle = cr.build_bundle(data={ - "pk": counter.pk, - "name": "Signups", - "slug": "signups", - }) - cr.obj_update(counter_bundle, pk=1) - self.assertEqual(Counter.objects.all().count(), 2) - counter = Counter.objects.get(pk=1) - self.assertEqual(counter.count, 1) - - def test_obj_update_full_hydrate_on_update_authorization(self): - counter = Counter.objects.get(pk=1) - - cr = CounterUpdateDetailResource() - counter_bundle = cr.build_bundle(data={ - "pk": counter.pk, - "name": "Signups", - "slug": "signups", - }, obj=Counter()) - cr.obj_update(counter_bundle, pk=1) - - counter = Counter.objects.get(pk=1) - self.assertEquals(counter_bundle._update_auth_call_count, 1) - self.assertEquals(counter_bundle.obj.name, "Signups") - self.assertEquals(counter_bundle.obj.slug, "signups") - - def test_obj_delete(self): - self.assertEqual(Note.objects.all().count(), 6) - note = NoteResource() - base_bundle = Bundle() - note.obj_delete(base_bundle, pk=1) - self.assertEqual(Note.objects.all().count(), 5) - self.assertRaises(Note.DoesNotExist, Note.objects.get, pk=1) - - # Test non-pk deletes. - base_bundle = Bundle() - note.obj_delete(base_bundle, slug='another-post') - self.assertEqual(Note.objects.all().count(), 4) - self.assertRaises(Note.DoesNotExist, Note.objects.get, slug='another-post') - - def test_rollback(self): - self.assertEqual(Note.objects.all().count(), 6) - note = NoteResource() - - bundles_seen = [] - note.rollback(bundles_seen) - self.assertEqual(Note.objects.all().count(), 6) - - # The one that exists should be deleted, the others ignored. - bundles_seen = [Bundle(obj=Note.objects.get(pk=1)), Bundle(obj=Note()), Bundle()] - note.rollback(bundles_seen) - self.assertEqual(Note.objects.all().count(), 5) - - def test_is_valid(self): - # Using the plug. - note = NoteResource() - bundle = Bundle(data={}) - - try: - note.is_valid(bundle) - except: - self.fail("Stock 'is_valid' should pass without exception.") - - # An actual form. - class NoteForm(forms.Form): - title = forms.CharField(max_length=100) - slug = forms.CharField(max_length=50) - content = forms.CharField(required=False, widget=forms.Textarea) - is_active = forms.BooleanField() - - # Define a custom clean to make sure non-field errors are making it - # through. - def clean(self): - if not self.cleaned_data.get('content', ''): - raise forms.ValidationError('Having no content makes for a very boring note.') - - return self.cleaned_data - - class ValidatedNoteResource(ModelResource): - class Meta: - queryset = Note.objects.all() - resource_name = 'validated' - validation = FormValidation(form_class=NoteForm) - - class ValidatedXMLNoteResource(ModelResource): - class Meta: - queryset = Note.objects.all() - resource_name = 'validated' - validation = FormValidation(form_class=NoteForm) - default_format = 'application/xml' - - validated = ValidatedNoteResource() - validated_xml = ValidatedXMLNoteResource() - - # Test empty data. - bundle = Bundle(data={}) - self.assertFalse(validated.is_valid(bundle)) - self.assertEqual(bundle.errors, {'validated': {'is_active': [u'This field is required.'], 'slug': [u'This field is required.'], '__all__': [u'Having no content makes for a very boring note.'], 'title': [u'This field is required.']}}) - - # Test something that fails validation. - bundle = Bundle(data={ - 'title': 123, - 'slug': '123456789012345678901234567890123456789012345678901234567890', - 'content': '', - 'is_active': True, - }) - self.assertFalse(validated.is_valid(bundle)) - self.assertEqual(bundle.errors, {'validated': {'slug': [u'Ensure this value has at most 50 characters (it has 60).'], '__all__': [u'Having no content makes for a very boring note.']}}) - - # Test something that passes validation. - bundle = Bundle(data={ - 'title': 'Test Content', - 'slug': 'test-content', - 'content': "It doesn't get any more awesome than this.", - 'is_active': True, - }) - - self.assertTrue(validated.is_valid(bundle)) - - def test_self_referential(self): - class SelfResource(ModelResource): - me_baby_me = fields.ToOneField('self', 'parent', null=True) - - class Meta: - queryset = Note.objects.all() - resource_name = 'me_baby_me' - - me_baby_me = SelfResource() - self.assertEqual(len(me_baby_me.fields), 9) - self.assertEqual(me_baby_me._meta.resource_name, 'me_baby_me') - self.assertEqual(me_baby_me.fields['me_baby_me'].to, 'self') - self.assertEqual(me_baby_me.fields['me_baby_me'].to_class, SelfResource) - - class AnotherSelfResource(SelfResource): - class Meta: - queryset = Note.objects.all() - resource_name = 'another_me_baby_me' - - another_me_baby_me = AnotherSelfResource() - self.assertEqual(len(another_me_baby_me.fields), 9) - self.assertEqual(another_me_baby_me._meta.resource_name, 'another_me_baby_me') - self.assertEqual(another_me_baby_me.fields['me_baby_me'].to, 'self') - self.assertEqual(another_me_baby_me.fields['me_baby_me'].to_class, AnotherSelfResource) - - def test_subclassing(self): - class MiniResource(ModelResource): - abcd = fields.CharField(default='abcd') - efgh = fields.IntegerField(default=1234) - - class Meta: - queryset = Note.objects.all() - resource_name = 'mini' - - mini = MiniResource() - self.assertEqual(len(mini.fields), 10) - self.assertEqual(len(mini._meta.queryset.all()), 6) - self.assertEqual(mini._meta.resource_name, 'mini') - - class AnotherMiniResource(MiniResource): - ijkl = fields.BooleanField(default=True) - - class Meta: - queryset = Note.objects.all() - resource_name = 'anothermini' - - another = AnotherMiniResource() - self.assertEqual(len(another.fields), 11) - self.assertEqual(len(another._meta.queryset.all()), 6) - self.assertEqual(another._meta.resource_name, 'anothermini') - - class YetAnotherMiniResource(MiniResource): - mnop = fields.FloatField(default=True) - - class Meta: - queryset = Note.objects.all() - resource_name = 'yetanothermini' - fields = ['title', 'abcd', 'mnop'] - include_absolute_url = True - - yetanother = YetAnotherMiniResource() - self.assertEqual(len(yetanother.fields), 5) - self.assertEqual(sorted(yetanother.fields.keys()), ['abcd', 'absolute_url', 'mnop', 'resource_uri', 'title']) - self.assertEqual(len(yetanother._meta.queryset.all()), 6) - self.assertEqual(yetanother._meta.resource_name, 'yetanothermini') - - def test_nullable_toone_full_hydrate(self): - nrrnr = NullableRelatedNoteResource() - - # Regression: not specifying the ToOneField should still work if - # it is nullable. - bundle_1 = Bundle(data={ - 'subjects': [], - }) - - hydrated1 = nrrnr.full_hydrate(bundle_1) - - self.assertEqual(hydrated1.data.get('author'), None) - self.assertEqual(hydrated1.data['subjects'], []) - - def test_optional_required_data(self): - # Regression: You have a FK field that's required on the model - # but you want to optionally allow the user to omit it and use - # custom ``hydrate_*`` method to populate it if it's not - # present. - nmbr = NullableMediaBitResource() - - bundle_1 = Bundle(data={ - 'title': "Foo", - }) - - try: - # This is where things blow up, because you can't assign - # ``None`` to a required FK. - hydrated1 = nmbr.full_hydrate(bundle_1) - self.fail() - except (Note.DoesNotExist, ValueError): - pass - - # So we introduced ``blank=True``. - bmbr = BlankMediaBitResource() - hydrated1 = bmbr.full_hydrate(bundle_1) - self.assertEqual(hydrated1.obj.title, "Foo") - self.assertEqual(hydrated1.obj.note.pk, 1) - - def test_nullable_tomany_full_hydrate(self): - nrrnr = NullableRelatedNoteResource() - bundle_1 = Bundle(data={ - 'author': '/api/v1/user/1/', - 'subjects': [], - }) - - # Now load up the data. - hydrated = nrrnr.full_hydrate(bundle_1) - hydrated = nrrnr.hydrate_m2m(hydrated) - - self.assertEqual(hydrated.data['author'], '/api/v1/user/1/') - self.assertEqual(hydrated.data['subjects'], []) - - # Regression: not specifying the tomany field should still work if - # it is nullable. - bundle_2 = Bundle(data={ - 'author': '/api/v1/user/1/', - }) - - hydrated2 = nrrnr.full_hydrate(bundle_2) - hydrated2 = nrrnr.hydrate_m2m(hydrated2) - - self.assertEqual(hydrated2.data['author'], '/api/v1/user/1/') - self.assertEqual(hydrated2.data['subjects'], []) - - # Regression pt. II - Make sure saving the objects works. - bundle_3 = Bundle(data={ - 'author': '/api/v1/user/1/', - }) - hydrated3 = nrrnr.obj_create(bundle_2) - self.assertEqual(hydrated2.obj.author.username, u'johndoe') - self.assertEqual(hydrated2.obj.subjects.count(), 0) - - def test_per_user_authorization(self): - from django.contrib.auth.models import AnonymousUser, User - - punr = PerUserNoteResource() - empty_request = HttpRequest() - empty_request.method = 'GET' - empty_request.GET = {'format': 'json'} - - anony_request = HttpRequest() - anony_request.method = 'GET' - anony_request.GET = {'format': 'json'} - anony_request.user = AnonymousUser() - - authed_request = HttpRequest() - authed_request.method = 'GET' - authed_request.GET = {'format': 'json'} - authed_request.user = User.objects.get(username='johndoe') - - authed_request_2 = HttpRequest() - authed_request_2.method = 'GET' - authed_request_2.GET = {'format': 'json'} - authed_request_2.user = User.objects.get(username='janedoe') - - self.assertEqual(punr._meta.queryset.count(), 6) - - # Requests without a user get all active objects, regardless of author. - empty_bundle = punr.build_bundle(request=empty_request) - self.assertEqual(punr.authorized_read_list(punr.get_object_list(empty_request), empty_bundle).count(), 4) - self.assertEqual(punr._pre_limits, 0) - # Shouldn't hit the DB yet. - self.assertEqual(punr._post_limits, 0) - self.assertEqual(len(json.loads(force_text(punr.get_list(request=empty_request).content))['objects']), 4) - - # Requests with an Anonymous user get no objects. - anony_bundle = punr.build_bundle(request=anony_request) - self.assertEqual(punr.authorized_read_list(punr.get_object_list(anony_request), anony_bundle).count(), 0) - self.assertEqual(len(json.loads(force_text(punr.get_list(request=anony_request).content))['objects']), 0) - - # Requests with an authenticated user get all objects for that user - # that are active. - authed_bundle = punr.build_bundle(request=authed_request) - self.assertEqual(punr.authorized_read_list(punr.get_object_list(authed_request), authed_bundle).count(), 2) - self.assertEqual(len(json.loads(force_text(punr.get_list(request=authed_request).content))['objects']), 2) - - # Demonstrate that a different user gets different objects. - authed_bundle_2 = punr.build_bundle(request=authed_request_2) - self.assertEqual(punr.authorized_read_list(punr.get_object_list(authed_request_2), authed_bundle_2).count(), 2) - self.assertEqual(len(json.loads(force_text(punr.get_list(request=authed_request_2).content))['objects']), 2) - self.assertEqual(list(punr.authorized_read_list(punr.get_object_list(authed_request), authed_bundle).values_list('id', flat=True)), [1, 2]) - self.assertEqual(list(punr.authorized_read_list(punr.get_object_list(authed_request_2), authed_bundle_2).values_list('id', flat=True)), [4, 6]) - - def test_per_object_authorization(self): - ponr = PerObjectNoteResource() - empty_request = HttpRequest() - empty_request.method = 'GET' - empty_request.GET = {'format': 'json'} - - self.assertEqual(ponr._meta.queryset.count(), 6) - empty_bundle = ponr.build_bundle(request=empty_request) - - # Should return only two objects with 'post' in the ``title``. - self.assertEqual(len(ponr.get_object_list(empty_request)), 6) - self.assertEqual(len(ponr.authorized_read_list(ponr.get_object_list(empty_request), empty_bundle)), 2) - self.assertEqual(ponr._pre_limits, 0) - # Since the objects weren't filtered, we hit everything. - self.assertEqual(ponr._post_limits, 6) - - self.assertEqual(len(json.loads(force_text(ponr.get_list(request=empty_request).content))['objects']), 2) - self.assertEqual(ponr._pre_limits, 0) - # Since the objects weren't filtered, we again hit everything. - self.assertEqual(ponr._post_limits, 6) - - empty_request.GET['is_active'] = True - self.assertEqual(len(json.loads(force_text(ponr.get_list(request=empty_request).content))['objects']), 2) - self.assertEqual(ponr._pre_limits, 0) - # This time, the objects were filtered, so we should only iterate over - # a (hopefully much smaller) subset. - self.assertEqual(ponr._post_limits, 4) - - def regression_test_per_object_detail(self): - ponr = PerObjectNoteResource() - empty_request = type('MockRequest', (object,), {'GET': {}}) - base_bundle = Bundle(request=empty_request) - - self.assertEqual(ponr._meta.queryset.count(), 6) - - # Regression: Make sure that simple ``get_detail`` requests work. - self.assertTrue(isinstance(ponr.obj_get(bundle=base_bundle, pk=1), Note)) - self.assertEqual(ponr.obj_get(bundle=base_bundle, pk=1).pk, 1) - self.assertEqual(ponr._pre_limits, 0) - self.assertEqual(ponr._post_limits, 1) - - try: - too_many = ponr.obj_get(bundle=base_bundle, is_active=True, pk__gte=1) - self.fail() - except MultipleObjectsReturned as e: - self.assertEqual(str(e), "More than 'Note' matched 'is_active=True, pk__gte=1'.") - - try: - too_many = ponr.obj_get(bundle=base_bundle, pk=1000000) - self.fail() - except Note.DoesNotExist as e: - self.assertEqual(str(e), "Couldn't find an instance of 'Note' which matched 'pk=1000000'.") - - def test_browser_cache(self): - resource = NoteResource() - request = MockRequest() - request.GET = {'format': 'json'} - - # First as a normal request. - resp = resource.wrap_view('dispatch_detail')(request, pk=1) - # resp = resource.get_detail(request, pk=1) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - self.assertTrue(resp.has_header('Cache-Control')) - self.assertEqual(resp._headers['cache-control'], ('Cache-Control', 'no-cache')) - - # Now as Ajax. - request.META = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} - resp = resource.wrap_view('dispatch_detail')(request, pk=1) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - self.assertTrue(resp.has_header('cache-control')) - self.assertEqual(resp._headers['cache-control'], ('Cache-Control', 'no-cache')) - - def test_custom_paginator(self): - mock_request = MockRequest() - customs = CustomPageNoteResource().get_list(mock_request) - data = json.loads(customs.content.decode('utf-8')) - self.assertEqual(len(data), 3) - self.assertEqual(len(data['objects']), 6) - self.assertEqual(data['extra'], 'Some extra stuff here.') - - def test_readonly_full_hydrate(self): - rornr = ReadOnlyRelatedNoteResource() - note = Note.objects.get(pk=1) - dbundle = Bundle(obj=note) - - # Make sure the field is there on read. - dehydrated = rornr.full_dehydrate(dbundle) - self.assertTrue('author' in dehydrated.data) - - # Now check that it can be omitted in ``full_hydrate`` - hbundle = Bundle(obj=note, data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0), - }) - hydrated = rornr.full_hydrate(hbundle) - self.assertEqual(hydrated.obj.author.username, 'johndoe') - - # It also shouldn't accept a new value & should silently ignore it. - hbundle_2 = Bundle(obj=note, data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0), - 'author': '/api/v1/users/2/', - }) - hydrated_2 = rornr.full_hydrate(hbundle_2) - self.assertEqual(hydrated_2.obj.author.username, 'johndoe') - - - def test_readonly_save_related(self): - rornr = ReadOnlyRelatedNoteResource() - note = Note.objects.get(pk=1) - dbundle = Bundle(obj=note) - - # Make sure the field is there on read. - dehydrated = rornr.full_dehydrate(dbundle) - self.assertTrue('author' in dehydrated.data) - - # Fetch the bundle - hbundle = Bundle(obj=note, data={ - 'name': 'Daniel', - 'view_count': 6, - 'date_joined': aware_datetime(2010, 2, 15, 12, 0, 0), - 'author': '/api/v1/users/2/', - }) - hydrated = rornr.full_hydrate(hbundle) - - # Get the related object. - related_obj = getattr(hydrated.obj, "author") - - # Monkey Patch save to raise an exception - def fake_save(*args, **kwargs): - raise Exception("save() called in a readonly field") - - _real_save = related_obj.save - - try: - related_obj.save = fake_save - - rornr.save_related(hydrated) - finally: - related_obj.save = _real_save - - - def test_collection_name(self): - resource = AlternativeCollectionNameNoteResource() - request = HttpRequest() - response = resource.get_list(request) - response_data = json.loads(force_text(response.content)) - self.assertTrue('alt_objects' in response_data) - - - def test_collection_name_patch_list(self): - """Test that patch list accepts alternative names""" - resource = AlternativeCollectionNameNoteResource() - request = HttpRequest() - request._body = request._raw_post_data = json.dumps({ - 'alt_objects_delete': [], - 'alt_objects': [{'title': 'Testing'}] - }) - request._read_started = False - - response = resource.patch_list(request) - self.assertEqual(response.status_code, 202) - - -class BasicAuthResourceTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def test_dispatch_list(self): - resource = BasicAuthNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - try: - resp = resource.dispatch_list(request) - self.fail() - except ImmediateHttpResponse as e: - self.assertEqual(e.response.status_code, 401) - - # Try again with ``wrap_view`` for sanity. - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp.status_code, 401) - - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass'.encode('utf-8')).decode('utf-8') - - resp = resource.dispatch_list(request) - self.assertEqual(resp.status_code, 200) - - def test_dispatch_detail(self): - resource = BasicAuthNoteResource() - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - try: - resp = resource.dispatch_detail(request, pk=1) - self.fail() - except ImmediateHttpResponse as e: - self.assertEqual(e.response.status_code, 401) - - # Try again with ``wrap_view`` for sanity. - resp = resource.wrap_view('dispatch_detail')(request, pk=1) - self.assertEqual(resp.status_code, 401) - - john_doe = User.objects.get(username='johndoe') - john_doe.set_password('pass') - john_doe.save() - request.META['HTTP_AUTHORIZATION'] = 'Basic %s' % base64.b64encode('johndoe:pass'.encode('utf-8')).decode('utf-8') - - resp = resource.dispatch_list(request) - self.assertEqual(resp.status_code, 200) - - -# Test out the 500 behavior. -class YouFail(Exception): - pass - - -class BustedResource(BasicResource): - def get_list(self, request, **kwargs): - raise YouFail("Something blew up.") - - def get_detail(self, request, **kwargs): - raise NotFound("It's just not there.") - - def post_list(self, request, **kwargs): - raise Http404("Not here either") - - def post_detail(self, request, **kwargs): - raise YouFail("") - - -class BustedResourceTestCase(TestCase): - def setUp(self): - # We're going to heavily jack with settings. :/ - super(BustedResourceTestCase, self).setUp() - self.old_debug = settings.DEBUG - self.old_full_debug = getattr(settings, 'TASTYPIE_FULL_DEBUG', False) - self.old_canned_error = getattr(settings, 'TASTYPIE_CANNED_ERROR', "Sorry, this request could not be processed. Please try again later.") - self.old_broken_links = getattr(settings, 'SEND_BROKEN_LINK_EMAILS', False) - - self.resource = BustedResource() - self.request = HttpRequest() - self.request.GET = {'format': 'json'} - self.request.method = 'GET' - - def tearDown(self): - settings.DEBUG = self.old_debug - settings.TASTYPIE_FULL_DEBUG = self.old_full_debug - settings.TASTYPIE_CANNED_ERROR = self.old_canned_error - settings.SEND_BROKEN_LINK_EMAILS = self.old_broken_links - super(BustedResourceTestCase, self).setUp() - - def test_debug_on_with_full(self): - settings.DEBUG = True - settings.TASTYPIE_FULL_DEBUG = True - - try: - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.fail() - except YouFail: - pass - - def test_debug_on_without_full(self): - settings.DEBUG = True - settings.TASTYPIE_FULL_DEBUG = False - mail.outbox = [] - - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 500) - content = json.loads(resp.content.decode('utf-8')) - self.assertEqual(content['error_message'], 'Something blew up.') - self.assertTrue(len(content['traceback']) > 0) - self.assertEqual(len(mail.outbox), 0) - - def test_debug_off(self): - settings.DEBUG = False - settings.TASTYPIE_FULL_DEBUG = False - - if django.VERSION >= (1, 3, 0): - SimpleHandler.logged = [] - - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 500) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Sorry, this request could not be processed. Please try again later."}') - self.assertEqual(len(SimpleHandler.logged), 1) - - # Ensure that 404s don't send email. - resp = self.resource.wrap_view('get_detail')(self.request, pk=10000000) - self.assertEqual(resp.status_code, 404) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Sorry, this request could not be processed. Please try again later."}') - self.assertEqual(len(SimpleHandler.logged), 1) - - # Now with a custom message. - settings.TASTYPIE_CANNED_ERROR = "Oops, you bwoke it." - - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 500) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Oops, you bwoke it."}') - self.assertEqual(len(SimpleHandler.logged), 2) - SimpleHandler.logged = [] - else: - mail.outbox = [] - - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 500) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Sorry, this request could not be processed. Please try again later."}') - self.assertEqual(len(mail.outbox), 1) - - # Ensure that 404s don't send email. - resp = self.resource.wrap_view('get_detail')(self.request, pk=10000000) - self.assertEqual(resp.status_code, 404) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Sorry, this request could not be processed. Please try again later."}') - self.assertEqual(len(mail.outbox), 1) - - # Ensure that 404s (with broken link emails enabled) DO send email. - settings.SEND_BROKEN_LINK_EMAILS = True - resp = self.resource.wrap_view('get_detail')(self.request, pk=10000000) - self.assertEqual(resp.status_code, 404) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Sorry, this request could not be processed. Please try again later."}') - self.assertEqual(len(mail.outbox), 2) - - # Now with a custom message. - settings.TASTYPIE_CANNED_ERROR = "Oops, you bwoke it." - - resp = self.resource.wrap_view('get_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 500) - self.assertEqual(resp.content.decode('utf-8'), '{"error_message": "Oops, you bwoke it."}') - self.assertEqual(len(mail.outbox), 3) - mail.outbox = [] - - def test_http404_raises_404(self): - self.request.method = 'POST' - resp = self.resource.wrap_view('post_list')(self.request, pk=1) - self.assertEqual(resp.status_code, 404) - - def test_escaping(self): - settings.DEBUG = True - settings.TASTYPIE_FULL_DEBUG = False - - request = HttpRequest() - request.method = 'POST' - request.POST = { - 'whatever': 'stuff', - } - res = self.resource.wrap_view('dispatch_detail')(request, pk=1) - self.assertEqual(res.status_code, 500) - err_data = json.loads(res.content.decode('utf-8')) - self.assertTrue('<script>alert(1)</script>' in err_data['error_message']) - - -class ObjectlessResource(Resource): - test = fields.CharField(default='objectless_test') - - class Meta: - resource_name = 'objectless' - - -class ObjectlessResourceTestCase(TestCase): - def test_build_bundle(self): - resource = ObjectlessResource() - - bundle = resource.build_bundle() - - self.assertTrue(bundle is not None) diff -Nru django-tastypie-0.12.0/tests/core/tests/serializers.py django-tastypie-0.13.3/tests/core/tests/serializers.py --- django-tastypie-0.12.0/tests/core/tests/serializers.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/serializers.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,507 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import yaml -from decimal import Decimal -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase -from tastypie.bundle import Bundle -from tastypie import fields -from tastypie.exceptions import BadRequest -from tastypie.serializers import Serializer -from tastypie.resources import ModelResource -from core.models import Note - -try: - import biplist -except ImportError: - biplist = None - - -class UnsafeObject(object): - pass - - -class NoteResource(ModelResource): - class Meta: - resource_name = 'notes' - queryset = Note.objects.filter(is_active=True) - - -class AnotherNoteResource(ModelResource): - aliases = fields.ListField(attribute='aliases', null=True) - meta = fields.DictField(attribute='metadata', null=True) - owed = fields.DecimalField(attribute='money_owed', null=True) - - class Meta: - resource_name = 'anothernotes' - queryset = Note.objects.filter(is_active=True) - - def dehydrate(self, bundle): - bundle.data['aliases'] = ['Mr. Smith', 'John Doe'] - bundle.data['meta'] = {'threat': 'high'} - bundle.data['owed'] = Decimal('102.57') - return bundle - - -class SerializerTestCase(TestCase): - def test_init(self): - serializer_1 = Serializer() - self.assertEqual(serializer_1.formats, ['json', 'xml', 'yaml', 'html', 'plist']) - self.assertEqual(serializer_1.content_types, {'xml': 'application/xml', 'yaml': 'text/yaml', 'json': 'application/json', 'jsonp': 'text/javascript', 'html': 'text/html', 'plist': 'application/x-plist'}) - self.assertEqual(serializer_1.supported_formats, ['application/json', 'application/xml', 'text/yaml', 'text/html', 'application/x-plist']) - - serializer_2 = Serializer(formats=['json', 'xml']) - self.assertEqual(serializer_2.formats, ['json', 'xml']) - self.assertEqual(serializer_2.content_types, {'xml': 'application/xml', 'yaml': 'text/yaml', 'json': 'application/json', 'jsonp': 'text/javascript', 'html': 'text/html', 'plist': 'application/x-plist'}) - self.assertEqual(serializer_2.supported_formats, ['application/json', 'application/xml']) - - serializer_3 = Serializer(formats=['json', 'xml'], content_types={'json': 'text/json', 'xml': 'application/xml'}) - self.assertEqual(serializer_3.formats, ['json', 'xml']) - self.assertEqual(serializer_3.content_types, {'xml': 'application/xml', 'json': 'text/json'}) - self.assertEqual(serializer_3.supported_formats, ['text/json', 'application/xml']) - - serializer_4 = Serializer(formats=['plist', 'json'], content_types={'plist': 'application/x-plist', 'json': 'application/json'}) - self.assertEqual(serializer_4.formats, ['plist', 'json']) - self.assertEqual(serializer_4.content_types, {'plist': 'application/x-plist', 'json': 'application/json'}) - self.assertEqual(serializer_4.supported_formats, ['application/x-plist', 'application/json']) - - self.assertRaises(ImproperlyConfigured, Serializer, formats=['json', 'xml'], content_types={'json': 'text/json'}) - - def test_default_formats_setting(self): - # When we drop support for Django 1.3 this boilerplate can be replaced with - # a simple django.test.utils.override_settings decorator: - - old_formats = getattr(settings, 'TASTYPIE_DEFAULT_FORMATS', None) - - try: - # Confirm that the setting will override the default values: - settings.TASTYPIE_DEFAULT_FORMATS = ('json', 'xml') - s = Serializer() - self.assertEqual(list(s.formats), ['json', 'xml']) - self.assertEqual(list(s.supported_formats), ['application/json', 'application/xml']) - self.assertEqual(s.content_types, {'xml': 'application/xml', 'yaml': 'text/yaml', 'json': 'application/json', 'jsonp': 'text/javascript', 'html': 'text/html', 'plist': 'application/x-plist'}) - - # Confirm that subclasses which set their own formats list won't be overriden: - class JSONSerializer(Serializer): - formats = ['json'] - - js = JSONSerializer() - self.assertEqual(list(js.formats), ['json']) - self.assertEqual(list(js.supported_formats), ['application/json']) - - finally: - if old_formats is None: - del settings.TASTYPIE_DEFAULT_FORMATS - else: - settings.TASTYPIE_DEFAULT_FORMATS = old_formats - - def get_sample1(self): - return { - 'name': 'Daniel', - 'age': 27, - 'date_joined': datetime.date(2010, 3, 27), - 'snowman': u'☃', - } - - def get_sample2(self): - return { - 'somelist': ['hello', 1, None], - 'somehash': {'pi': 3.14, 'foo': 'bar'}, - 'somestring': 'hello', - 'true': True, - 'false': False, - } - - def test_format_datetime(self): - serializer = Serializer() - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), '2010-12-16T02:31:33') - - serializer = Serializer(datetime_formatting='iso-8601') - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), '2010-12-16T02:31:33') - - serializer = Serializer(datetime_formatting='iso-8601-strict') - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33, 10)), '2010-12-16T02:31:33') - - serializer = Serializer(datetime_formatting='rfc-2822') - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), u'Thu, 16 Dec 2010 02:31:33 -0600') - - serializer = Serializer(datetime_formatting='random-garbage') - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), '2010-12-16T02:31:33') - - # Stow. - old_format = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601') - - settings.TASTYPIE_DATETIME_FORMATTING = 'iso-8601' - serializer = Serializer() - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), '2010-12-16T02:31:33') - - settings.TASTYPIE_DATETIME_FORMATTING = 'iso-8601-strict' - serializer = Serializer() - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33, 10)), '2010-12-16T02:31:33') - - settings.TASTYPIE_DATETIME_FORMATTING = 'rfc-2822' - serializer = Serializer() - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), u'Thu, 16 Dec 2010 02:31:33 -0600') - - settings.TASTYPIE_DATETIME_FORMATTING = 'random-garbage' - serializer = Serializer() - self.assertEqual(serializer.format_datetime(datetime.datetime(2010, 12, 16, 2, 31, 33)), '2010-12-16T02:31:33') - - # Restore. - settings.TASTYPIE_DATETIME_FORMATTING = old_format - - def test_format_date(self): - serializer = Serializer() - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), '2010-12-16') - - serializer = Serializer(datetime_formatting='iso-8601') - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), '2010-12-16') - - serializer = Serializer(datetime_formatting='rfc-2822') - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), u'16 Dec 2010') - - serializer = Serializer(datetime_formatting='random-garbage') - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), '2010-12-16') - - # Stow. - old_format = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601') - - settings.TASTYPIE_DATETIME_FORMATTING = 'iso-8601' - serializer = Serializer() - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), '2010-12-16') - - settings.TASTYPIE_DATETIME_FORMATTING = 'rfc-2822' - serializer = Serializer() - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), u'16 Dec 2010') - - settings.TASTYPIE_DATETIME_FORMATTING = 'random-garbage' - serializer = Serializer() - self.assertEqual(serializer.format_date(datetime.date(2010, 12, 16)), '2010-12-16') - - # Restore. - settings.TASTYPIE_DATETIME_FORMATTING = old_format - - def test_format_time(self): - serializer = Serializer() - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), '02:31:33') - - serializer = Serializer(datetime_formatting='iso-8601') - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), '02:31:33') - - serializer = Serializer(datetime_formatting='iso-8601-strict') - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33, 10)), '02:31:33') - - serializer = Serializer(datetime_formatting='rfc-2822') - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), u'02:31:33 -0600') - - serializer = Serializer(datetime_formatting='random-garbage') - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), '02:31:33') - - # Stow. - old_format = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601') - - settings.TASTYPIE_DATETIME_FORMATTING = 'iso-8601' - serializer = Serializer() - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), '02:31:33') - - settings.TASTYPIE_DATETIME_FORMATTING = 'iso-8601-strict' - serializer = Serializer() - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33, 10)), '02:31:33') - - settings.TASTYPIE_DATETIME_FORMATTING = 'rfc-2822' - serializer = Serializer() - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), u'02:31:33 -0600') - - settings.TASTYPIE_DATETIME_FORMATTING = 'random-garbage' - serializer = Serializer() - self.assertEqual(serializer.format_time(datetime.time(2, 31, 33)), '02:31:33') - - # Restore. - settings.TASTYPIE_DATETIME_FORMATTING = old_format - - def test_to_xml(self): - serializer = Serializer() - sample_1 = self.get_sample1() - # This needs a little explanation. - # From http://lxml.de/parsing.html, what comes out of ``tostring`` - # (despite encoding as UTF-8) is a bytestring. This is because that's - # what other libraries expect (& will do the decode). We decode here - # so we can make extra special sure it looks right. - binary_xml = serializer.to_xml(sample_1) - unicode_xml = binary_xml.decode('utf-8') - self.assertEqual(unicode_xml, u'\n272010-03-27Daniel☃') - - def test_to_xml2(self): - serializer = Serializer() - sample_2 = self.get_sample2() - binary_xml = serializer.to_xml(sample_2) - unicode_xml = binary_xml.decode('utf-8') - self.assertEqual(unicode_xml, '\nFalsebar3.14hello1helloTrue') - - def test_from_xml(self): - serializer = Serializer() - data = u'\n☃27Daniel2010-03-27True' - self.assertEqual(serializer.from_xml(data), {'rocksdahouse': True, 'age': 27, 'name': 'Daniel', 'date_joined': '2010-03-27', 'snowman': u'☃'}) - - def test_from_xml2(self): - serializer = Serializer() - data = '\nhello13.14barFalseTruehello' - self.assertEqual(serializer.from_xml(data), self.get_sample2()) - - def test_malformed_xml(self): - serializer = Serializer() - data = '\n]> - &a; - """ - self.assertRaises(BadRequest, serializer.from_xml, data) - - def test_to_jsonp(self): - serializer = Serializer() - - sample_1 = self.get_sample1() - options = {'callback': 'myCallback'} - serialized = serializer.to_jsonp(sample_1, options=options) - serialized_json = serializer.to_json(sample_1) - self.assertEqual('myCallback(%s)' % serialized_json, - serialized) - - def test_invalid_jsonp_characters(self): - """ - The newline characters \u2028 and \u2029 need to be escaped - in JSONP. - """ - serializer = Serializer() - - jsonp = serializer.to_jsonp({'foo': u'Hello \u2028\u2029world!'}, - {'callback': 'callback'}) - self.assertEqual(jsonp, u'callback({"foo": "Hello \\u2028\\u2029world!"})') - - def test_to_plist(self): - if not biplist: - return - - serializer = Serializer() - - sample_1 = self.get_sample1() - self.assertTrue(serializer.to_plist(sample_1).startswith(b'bplist00bybiplist1.0')) - - def test_from_plist(self): - if not biplist: - return - - serializer = Serializer() - - sample_1 = serializer.from_plist(b'bplist00bybiplist1.0\x00\xd4\x01\x02\x03\x04\x05\x06\x07\x08WsnowmanSageTname[date_joineda&\x03\x10\x1bf\x00D\x00a\x00n\x00i\x00e\x00lZ2010-03-27\x15\x1e&*/;>@M\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00X') - self.assertEqual(len(sample_1), 4) - self.assertEqual(sample_1[b'name'], 'Daniel') - self.assertEqual(sample_1[b'age'], 27) - self.assertEqual(sample_1[b'date_joined'], b'2010-03-27') - self.assertEqual(sample_1[b'snowman'], u'☃') - -class ResourceSerializationTestCase(TestCase): - fixtures = ['note_testdata.json'] - - def setUp(self): - super(ResourceSerializationTestCase, self).setUp() - self.resource = NoteResource() - base_bundle = Bundle() - self.obj_list = [self.resource.full_dehydrate(self.resource.build_bundle(obj=obj)) for obj in self.resource.obj_get_list(base_bundle)] - self.another_resource = AnotherNoteResource() - self.another_obj_list = [self.another_resource.full_dehydrate(self.resource.build_bundle(obj=obj)) for obj in self.another_resource.obj_get_list(base_bundle)] - - def test_to_xml_multirepr(self): - serializer = Serializer() - binary_xml = serializer.to_xml(self.obj_list) - unicode_xml = binary_xml.decode('utf-8') - self.assertEqual(unicode_xml, '\nThis is my very first post using my shiny new API. Pretty sweet, huh?2010-03-30T20:05:001Truefirst-postFirst Post!2010-03-30T20:05:00The dog ate my cat today. He looks seriously uncomfortable.2010-03-31T20:05:002Trueanother-postAnother Post2010-03-31T20:05:00My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.2010-04-01T20:05:004Truerecent-volcanic-activityRecent Volcanic Activity.2010-04-01T20:05:00Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!2010-04-02T10:05:006Truegrannys-goneGranny\'s Gone2010-04-02T10:05:00') - - def test_to_xml_single(self): - serializer = Serializer() - resource = self.obj_list[0] - binary_xml = serializer.to_xml(resource) - unicode_xml = binary_xml.decode('utf-8') - self.assertEqual(unicode_xml, '\nThis is my very first post using my shiny new API. Pretty sweet, huh?2010-03-30T20:05:001Truefirst-postFirst Post!2010-03-30T20:05:00') - - def test_to_xml_nested(self): - serializer = Serializer() - resource = self.obj_list[0] - data = { - 'stuff': { - 'foo': 'bar', - 'object': resource, - } - } - binary_xml = serializer.to_xml(data) - unicode_xml = binary_xml.decode('utf-8') - self.assertEqual(unicode_xml, '\nbarThis is my very first post using my shiny new API. Pretty sweet, huh?2010-03-30T20:05:001Truefirst-postFirst Post!2010-03-30T20:05:00') - - def test_to_json_multirepr(self): - serializer = Serializer() - self.assertEqual(serializer.to_json(self.obj_list), '[{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}, {"content": "The dog ate my cat today. He looks seriously uncomfortable.", "created": "2010-03-31T20:05:00", "id": 2, "is_active": true, "resource_uri": "", "slug": "another-post", "title": "Another Post", "updated": "2010-03-31T20:05:00"}, {"content": "My neighborhood\'s been kinda weird lately, especially after the lava flow took out the corner store. Granny can hardly outrun the magma with her walker.", "created": "2010-04-01T20:05:00", "id": 4, "is_active": true, "resource_uri": "", "slug": "recent-volcanic-activity", "title": "Recent Volcanic Activity.", "updated": "2010-04-01T20:05:00"}, {"content": "Man, the second eruption came on fast. Granny didn\'t have a chance. On the upshot, I was able to save her walker and I got a cool shawl out of the deal!", "created": "2010-04-02T10:05:00", "id": 6, "is_active": true, "resource_uri": "", "slug": "grannys-gone", "title": "Granny\'s Gone", "updated": "2010-04-02T10:05:00"}]') - - def test_to_json_single(self): - serializer = Serializer() - resource = self.obj_list[0] - self.assertEqual(serializer.to_json(resource), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - - def test_to_json_decimal_list_dict(self): - serializer = Serializer() - resource = self.another_obj_list[0] - self.assertEqual(serializer.to_json(resource), '{"aliases": ["Mr. Smith", "John Doe"], "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "meta": {"threat": "high"}, "owed": "102.57", "resource_uri": "", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}') - - def test_to_json_nested(self): - serializer = Serializer() - resource = self.obj_list[0] - data = { - 'stuff': { - 'foo': 'bar', - 'object': resource, - } - } - self.assertEqual(serializer.to_json(data), '{"stuff": {"foo": "bar", "object": {"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}}}') - - -class StubbedSerializer(Serializer): - def __init__(self, *args, **kwargs): - super(StubbedSerializer, self).__init__(*args, **kwargs) - self.from_json_called = False - self.from_xml_called = False - self.from_yaml_called = False - self.from_html_called = False - self.from_jsonp_called = False - - def from_json(self, data): - self.from_json_called = True - return True - - def from_xml(self, data): - self.from_xml_called = True - return True - - def from_yaml(self, data): - self.from_yaml_called = True - return True - - def from_html(self, data): - self.from_html_called = True - return True - - def from_jsonp(self, data): - self.from_jsonp_called = True - return True - -class ContentHeaderTest(TestCase): - def test_deserialize_json(self): - serializer = StubbedSerializer() - serializer.deserialize('{}', 'application/json') - self.assertTrue(serializer.from_json_called) - - def test_deserialize_json_with_charset(self): - serializer = StubbedSerializer() - serializer.deserialize('{}', 'application/json; charset=UTF-8') - self.assertTrue(serializer.from_json_called) - - def test_deserialize_xml(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'application/xml') - self.assertTrue(serializer.from_xml_called) - - def test_deserialize_xml_with_charset(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'application/xml; charset=UTF-8') - self.assertTrue(serializer.from_xml_called) - - def test_deserialize_yaml(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'text/yaml') - self.assertTrue(serializer.from_yaml_called) - - def test_deserialize_yaml_with_charset(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'text/yaml; charset=UTF-8') - self.assertTrue(serializer.from_yaml_called) - - def test_deserialize_jsonp(self): - serializer = StubbedSerializer() - serializer.deserialize('{}', 'text/javascript') - self.assertTrue(serializer.from_jsonp_called) - - def test_deserialize_jsonp_with_charset(self): - serializer = StubbedSerializer() - serializer.deserialize('{}', 'text/javascript; charset=UTF-8') - self.assertTrue(serializer.from_jsonp_called) - - def test_deserialize_html(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'text/html') - self.assertTrue(serializer.from_html_called) - - def test_deserialize_html_with_charset(self): - serializer = StubbedSerializer() - serializer.deserialize('', 'text/html; charset=UTF-8') - self.assertTrue(serializer.from_html_called) - diff -Nru django-tastypie-0.12.0/tests/core/tests/throttle.py django-tastypie-0.13.3/tests/core/tests/throttle.py --- django-tastypie-0.12.0/tests/core/tests/throttle.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/throttle.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,148 +0,0 @@ -import mock -import time - -from django.core.cache import cache -from django.test import TestCase -from django.utils.encoding import force_text - -from tastypie.models import ApiAccess -from tastypie.throttle import BaseThrottle, CacheThrottle, CacheDBThrottle - - -class NoThrottleTestCase(TestCase): - def test_init(self): - throttle_1 = BaseThrottle() - self.assertEqual(throttle_1.throttle_at, 150) - self.assertEqual(throttle_1.timeframe, 3600) - self.assertEqual(throttle_1.expiration, 604800) - - throttle_2 = BaseThrottle(throttle_at=50, timeframe=60*30, expiration=1) - self.assertEqual(throttle_2.throttle_at, 50) - self.assertEqual(throttle_2.timeframe, 1800) - self.assertEqual(throttle_2.expiration, 1) - - def test_convert_identifier_to_key(self): - throttle_1 = BaseThrottle() - self.assertEqual(throttle_1.convert_identifier_to_key(''), '_accesses') - self.assertEqual(throttle_1.convert_identifier_to_key('alnum10'), 'alnum10_accesses') - self.assertEqual(throttle_1.convert_identifier_to_key('Mr. Pants'), 'Mr.Pants_accesses') - self.assertEqual(throttle_1.convert_identifier_to_key('Mr_Pants'), 'Mr_Pants_accesses') - self.assertEqual(throttle_1.convert_identifier_to_key('%^@@$&!a'), 'a_accesses') - - def test_should_be_throttled(self): - throttle_1 = BaseThrottle() - self.assertEqual(throttle_1.should_be_throttled('foobaz'), False) - - def test_accessed(self): - throttle_1 = BaseThrottle() - self.assertEqual(throttle_1.accessed('foobaz'), None) - - -class CacheThrottleTestCase(TestCase): - def tearDown(self): - cache.delete('daniel_accesses') - cache.delete('cody_accesses') - - def test_throttling(self): - throttle_1 = CacheThrottle(throttle_at=2, timeframe=5, expiration=2) - - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 0) - self.assertEqual(throttle_1.accessed('daniel'), None) - - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 1) - self.assertEqual(cache.get('cody_accesses'), None) - self.assertEqual(throttle_1.accessed('daniel'), None) - - self.assertEqual(throttle_1.accessed('cody'), None) - self.assertEqual(throttle_1.should_be_throttled('cody'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 2) - self.assertEqual(len(cache.get('cody_accesses')), 1) - - # THROTTLE'D! - self.assertEqual(throttle_1.should_be_throttled('daniel'), True) - self.assertEqual(len(cache.get('daniel_accesses')), 2) - self.assertEqual(throttle_1.accessed('daniel'), None) - - self.assertEqual(throttle_1.should_be_throttled('daniel'), True) - self.assertEqual(len(cache.get('daniel_accesses')), 3) - self.assertEqual(throttle_1.accessed('daniel'), None) - - # Should be no interplay. - self.assertEqual(throttle_1.should_be_throttled('cody'), False) - self.assertEqual(throttle_1.accessed('cody'), None) - - # Test the timeframe. - ret_time = time.time() + throttle_1.timeframe + 1 - - with mock.patch('tastypie.throttle.time') as mocked_time: - mocked_time.time.return_value = ret_time - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 0) - - -class CacheDBThrottleTestCase(TestCase): - def tearDown(self): - cache.delete('daniel_accesses') - cache.delete('cody_accesses') - - def test_throttling(self): - throttle_1 = CacheDBThrottle(throttle_at=2, timeframe=5, expiration=2) - - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 0) - self.assertEqual(ApiAccess.objects.count(), 0) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 0) - self.assertEqual(throttle_1.accessed('daniel'), None) - - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 1) - self.assertEqual(cache.get('cody_accesses'), None) - self.assertEqual(ApiAccess.objects.count(), 1) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 1) - self.assertEqual(throttle_1.accessed('daniel'), None) - - self.assertEqual(throttle_1.accessed('cody'), None) - self.assertEqual(throttle_1.should_be_throttled('cody'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 2) - self.assertEqual(len(cache.get('cody_accesses')), 1) - self.assertEqual(ApiAccess.objects.count(), 3) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 2) - self.assertEqual(throttle_1.accessed('cody'), None) - - # THROTTLE'D! - self.assertEqual(throttle_1.accessed('daniel'), None) - self.assertEqual(throttle_1.should_be_throttled('daniel'), True) - self.assertEqual(len(cache.get('daniel_accesses')), 3) - self.assertEqual(ApiAccess.objects.count(), 5) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 3) - - self.assertEqual(throttle_1.accessed('daniel'), None) - self.assertEqual(throttle_1.should_be_throttled('daniel'), True) - self.assertEqual(len(cache.get('daniel_accesses')), 4) - self.assertEqual(ApiAccess.objects.count(), 6) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 4) - - # Should be no interplay. - self.assertEqual(throttle_1.should_be_throttled('cody'), True) - self.assertEqual(throttle_1.accessed('cody'), None) - self.assertEqual(ApiAccess.objects.count(), 7) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 4) - - # Test the timeframe. - ret_time = time.time() + throttle_1.timeframe + 1 - - with mock.patch('tastypie.throttle.time') as mocked_time: - mocked_time.time.return_value = ret_time - self.assertEqual(throttle_1.should_be_throttled('daniel'), False) - self.assertEqual(len(cache.get('daniel_accesses')), 0) - self.assertEqual(ApiAccess.objects.count(), 7) - self.assertEqual(ApiAccess.objects.filter(identifier='daniel').count(), 4) - - -class ModelTestCase(TestCase): - def test_unicode(self): - access = ApiAccess(identifier="testing", accessed=0) - self.assertEqual(force_text(access), 'testing @ 0') - diff -Nru django-tastypie-0.12.0/tests/core/tests/utils.py django-tastypie-0.13.3/tests/core/tests/utils.py --- django-tastypie-0.12.0/tests/core/tests/utils.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/utils.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,117 +0,0 @@ -import datetime -import mock - -from django.http import HttpRequest -from django.test import TestCase - -from tastypie.exceptions import BadRequest -from tastypie.serializers import Serializer -from tastypie.utils.mime import determine_format, build_content_type -from tastypie.utils.timezone import now - -try: - from django.utils import timezone as dj_tz - TZ_AVAILABLE = True -except ImportError: - TZ_AVAILABLE = False - - -class MimeTestCase(TestCase): - def test_build_content_type(self): - # JSON & JSONP don't include charset. - self.assertEqual(build_content_type('application/json'), 'application/json') - self.assertEqual(build_content_type('text/javascript'), 'text/javascript') - self.assertEqual(build_content_type('application/json', encoding='ascii'), 'application/json') - # Everything else should. - self.assertEqual(build_content_type('application/xml'), 'application/xml; charset=utf-8') - self.assertEqual(build_content_type('application/xml', encoding='ascii'), 'application/xml; charset=ascii') - - def test_determine_format(self): - serializer = Serializer() - full_serializer = Serializer(formats=['json', 'jsonp', 'xml', 'yaml', 'html', 'plist']) - request = HttpRequest() - - # Default. - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Test forcing the ``format`` parameter. - request.GET = {'format': 'json'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Disabled by default. - request.GET = {'format': 'jsonp'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Explicitly enabled. - request.GET = {'format': 'jsonp'} - self.assertEqual(determine_format(request, full_serializer), 'text/javascript') - - request.GET = {'format': 'xml'} - self.assertEqual(determine_format(request, serializer), 'application/xml') - - request.GET = {'format': 'yaml'} - self.assertEqual(determine_format(request, serializer), 'text/yaml') - - request.GET = {'format': 'plist'} - self.assertEqual(determine_format(request, serializer), 'application/x-plist') - - request.GET = {'format': 'foo'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Test the ``Accept`` header. - request.META = {'HTTP_ACCEPT': 'application/json'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Again, disabled by default. - request.META = {'HTTP_ACCEPT': 'text/javascript'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - # Again, explicitly enabled. - request.META = {'HTTP_ACCEPT': 'text/javascript'} - self.assertEqual(determine_format(request, full_serializer), 'text/javascript') - - request.META = {'HTTP_ACCEPT': 'application/xml'} - self.assertEqual(determine_format(request, serializer), 'application/xml') - - request.META = {'HTTP_ACCEPT': 'text/yaml'} - self.assertEqual(determine_format(request, serializer), 'text/yaml') - - request.META = {'HTTP_ACCEPT': 'application/x-plist'} - self.assertEqual(determine_format(request, serializer), 'application/x-plist') - - request.META = {'HTTP_ACCEPT': 'text/html'} - self.assertEqual(determine_format(request, serializer), 'text/html') - - request.META = {'HTTP_ACCEPT': '*/*'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - request.META = {'HTTP_ACCEPT': 'application/json,application/xml;q=0.9,*/*;q=0.8'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - request.META = {'HTTP_ACCEPT': 'text/plain,application/xml,application/json;q=0.9,*/*;q=0.8'} - self.assertEqual(determine_format(request, serializer), 'application/xml') - - request.META = {'HTTP_ACCEPT': 'application/json; charset=UTF-8'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - request.META = {'HTTP_ACCEPT': 'text/javascript,application/json'} - self.assertEqual(determine_format(request, serializer), 'application/json') - - request.META = {'HTTP_ACCEPT': 'bogon'} - self.assertRaises(BadRequest, determine_format, request, serializer) - - -if TZ_AVAILABLE: - from pytz.reference import Pacific - - class TimezoneTestCase(TestCase): - def test_now(self): - without_tz = datetime.datetime(2013, 8, 7, 22, 54, 52) - with_tz = without_tz.replace(tzinfo=Pacific) - - with mock.patch('django.utils.timezone.now', return_value=with_tz): - self.assertEqual(now().isoformat(), '2013-08-08T00:54:52-05:00') - - with mock.patch('django.utils.timezone.now', return_value=without_tz): - self.assertEqual(now().isoformat(), '2013-08-07T22:54:52') - diff -Nru django-tastypie-0.12.0/tests/core/tests/validation.py django-tastypie-0.13.3/tests/core/tests/validation.py --- django-tastypie-0.12.0/tests/core/tests/validation.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/tests/validation.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,151 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured -from django import forms -from django.test import TestCase -from tastypie.bundle import Bundle -from tastypie.validation import Validation, FormValidation, CleanedDataFormValidation - - -class NoteForm(forms.Form): - title = forms.CharField(max_length=100) - slug = forms.CharField(max_length=50) - content = forms.CharField(required=False, widget=forms.Textarea) - is_active = forms.BooleanField() - - def clean_title(self): - return self.cleaned_data.get('title', '').upper() - - # Define a custom clean to make sure non-field errors are making it - # through. - def clean(self): - if not self.cleaned_data.get('content', ''): - raise forms.ValidationError('Having no content makes for a very boring note.') - - return self.cleaned_data - - -class ValidationTestCase(TestCase): - def test_init(self): - try: - valid = Validation() - except Exception as e: - self.fail("Initialization failed when it should have succeeded.") - - try: - valid = Validation(form_class='foo') - except Exception as e: - self.fail("Initialization failed when it should have succeeded again.") - - def test_is_valid(self): - valid = Validation() - bundle = Bundle() - self.assertEqual(valid.is_valid(bundle), {}) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': 'bar', - 'content': '', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), {}) - - -class FormValidationTestCase(TestCase): - def test_init(self): - self.assertRaises(ImproperlyConfigured, FormValidation) - - try: - valid = FormValidation(form_class=NoteForm) - except Exception as e: - self.fail("Initialization failed when it should have succeeded.") - - def test_is_valid(self): - valid = FormValidation(form_class=NoteForm) - bundle = Bundle() - self.assertEqual(valid.is_valid(bundle), { - 'is_active': [u'This field is required.'], - 'slug': [u'This field is required.'], - '__all__': [u'Having no content makes for a very boring note.'], - 'title': [u'This field is required.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': '123456789012345678901234567890123456789012345678901234567890', - 'content': '', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), { - 'slug': [u'Ensure this value has at most 50 characters (it has 60).'], - '__all__': [u'Having no content makes for a very boring note.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': 'bar', - 'content': '', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), { - '__all__': [u'Having no content makes for a very boring note.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': 'bar', - 'content': 'This! Is! CONTENT!', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), {}) - # NOTE: Bundle data is left untouched! - self.assertEqual(bundle.data['title'], 'Foo.') - - -class CleanedDataFormValidationTestCase(TestCase): - def test_init(self): - self.assertRaises(ImproperlyConfigured, CleanedDataFormValidation) - - try: - valid = CleanedDataFormValidation(form_class=NoteForm) - except Exception as e: - self.fail("Initialization failed when it should have succeeded.") - - def test_is_valid(self): - valid = CleanedDataFormValidation(form_class=NoteForm) - bundle = Bundle() - self.assertEqual(valid.is_valid(bundle), { - 'is_active': [u'This field is required.'], - 'slug': [u'This field is required.'], - '__all__': [u'Having no content makes for a very boring note.'], - 'title': [u'This field is required.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': '123456789012345678901234567890123456789012345678901234567890', - 'content': '', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), { - 'slug': [u'Ensure this value has at most 50 characters (it has 60).'], - '__all__': [u'Having no content makes for a very boring note.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': 'bar', - 'content': '', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), { - '__all__': [u'Having no content makes for a very boring note.'], - }) - - bundle = Bundle(data={ - 'title': 'Foo.', - 'slug': 'bar', - 'content': 'This! Is! CONTENT!', - 'is_active': True, - }) - self.assertEqual(valid.is_valid(bundle), {}) - # NOTE: Bundle data is modified! - self.assertEqual(bundle.data['title'], u'FOO.') diff -Nru django-tastypie-0.12.0/tests/core/utils.py django-tastypie-0.13.3/tests/core/utils.py --- django-tastypie-0.12.0/tests/core/utils.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/core/utils.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -import logging - - -class SimpleHandler(logging.Handler): - logged = [] - - def emit(self, record): - SimpleHandler.logged.append(record) diff -Nru django-tastypie-0.12.0/tests/customuser/fixtures/customuser.json django-tastypie-0.13.3/tests/customuser/fixtures/customuser.json --- django-tastypie-0.12.0/tests/customuser/fixtures/customuser.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/customuser/fixtures/customuser.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -[ - { - "pk": "1", - "model": "customuser.customuser", - "fields": { - "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", - "last_login": "2006-12-17 07:03:31", - "email": "staffmember@example.com", - "is_active": true, - "is_admin": false, - "date_of_birth": "1976-11-08" - } - } -] diff -Nru django-tastypie-0.12.0/tests/customuser/models.py django-tastypie-0.13.3/tests/customuser/models.py --- django-tastypie-0.12.0/tests/customuser/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/customuser/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -from django.contrib.auth.models import AbstractBaseUser -from django.db import models - - -class CustomUser(AbstractBaseUser): - email = models.EmailField(verbose_name='email address', max_length=255, unique=True) - is_active = models.BooleanField(default=True) - is_admin = models.BooleanField(default=False) - date_of_birth = models.DateField() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['date_of_birth'] - - def get_full_name(self): - return self.email - - def get_short_name(self): - return self.email - - def __unicode__(self): - return self.email - - # Maybe required? - def get_group_permissions(self, obj=None): - return set() - - def get_all_permissions(self, obj=None): - return set() - - def has_perm(self, perm, obj=None): - return True - - def has_perms(self, perm_list, obj=None): - return True - - def has_module_perms(self, app_label): - return True - - # Admin required fields - @property - def is_staff(self): - return self.is_admin diff -Nru django-tastypie-0.12.0/tests/customuser/tests/custom_user.py django-tastypie-0.13.3/tests/customuser/tests/custom_user.py --- django-tastypie-0.12.0/tests/customuser/tests/custom_user.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/customuser/tests/custom_user.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -from django.conf import settings -from django.http import HttpRequest -from django.test import TestCase -from customuser.models import CustomUser -from tastypie.models import ApiKey, create_api_key -from django import get_version as django_version -from django.test import TestCase -from tastypie.authentication import ApiKeyAuthentication -from tastypie.http import HttpUnauthorized - - -class CustomUserTestCase(TestCase): - fixtures = ['customuser.json'] - def setUp(self): - super(CustomUserTestCase, self).setUp() - ApiKey.objects.all().delete() - - def test_is_authenticated_get_params(self): - auth = ApiKeyAuthentication() - request = HttpRequest() - - # Simulate sending the signal. - john_doe = CustomUser.objects.get(pk=1) - create_api_key(CustomUser, instance=john_doe, created=True) - - # No username/api_key details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong username (email) details. - request.GET['username'] = 'foo@bar.com' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # No api_key. - request.GET['username'] = john_doe.email - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong user/api_key. - request.GET['username'] = john_doe.email - request.GET['api_key'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Correct user/api_key. - ApiKey.objects.all().delete() - create_api_key(CustomUser, instance=john_doe, created=True) - request.GET['username'] = john_doe.email - request.GET['api_key'] = john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - self.assertEqual(auth.get_identifier(request), john_doe.email) diff -Nru django-tastypie-0.12.0/tests/customuser/tests/__init__.py django-tastypie-0.13.3/tests/customuser/tests/__init__.py --- django-tastypie-0.12.0/tests/customuser/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/customuser/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -from customuser.tests.custom_user import * diff -Nru django-tastypie-0.12.0/tests/gis/api/resources.py django-tastypie-0.13.3/tests/gis/api/resources.py --- django-tastypie-0.12.0/tests/gis/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -from django.contrib.auth.models import User -from tastypie import fields -from tastypie.resources import ALL -from tastypie.contrib.gis.resources import ModelResource -from tastypie.authorization import Authorization -from gis.models import GeoNote - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - authorization = Authorization() - - -class GeoNoteResource(ModelResource): - user = fields.ForeignKey(UserResource, 'user') - - class Meta: - resource_name = 'geonotes' - queryset = GeoNote.objects.all() - authorization = Authorization() - filtering = { - 'points': ALL, - 'lines': ALL, - 'polys': ALL, - } diff -Nru django-tastypie-0.12.0/tests/gis/api/urls.py django-tastypie-0.13.3/tests/gis/api/urls.py --- django-tastypie-0.12.0/tests/gis/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * -from tastypie.api import Api -from gis.api.resources import GeoNoteResource, UserResource - -api = Api(api_name='v1') -api.register(GeoNoteResource()) -api.register(UserResource()) - -urlpatterns = api.urls diff -Nru django-tastypie-0.12.0/tests/gis/fixtures/initial_data.json django-tastypie-0.13.3/tests/gis/fixtures/initial_data.json --- django-tastypie-0.12.0/tests/gis/fixtures/initial_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/fixtures/initial_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "this_is_not_a_valid_password_string" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "content": "Wooo two points inside Golden Gate park", - "created": "2010-03-30 20:05:00", - "is_active": true, - "lines": null, - "points": "MULTIPOINT (-122.4752327727999983 37.7686172428039981, -122.4704155326000006 37.7673818328299973)", - "polys": null, - "slug": "points-inside-golden-gate-park-note", - "title": "Points inside Golden Gate Park note", - "updated": "2012-03-07 21:47:37", - "user": 1 - }, - "model": "gis.geonote", - "pk": 1 - }, - { - "fields": { - "content": "This is a note about Golden Gate Park. It contains Golden Gate Park's polygon", - "created": "2010-03-31 20:05:00", - "is_active": true, - "lines": null, - "points": null, - "polys": "MULTIPOLYGON (((-122.5110670852699997 37.7712760364340028, -122.5100371170100004 37.7663909693299971, -122.5100371170100004 37.7638126093670010, -122.4568220901600029 37.7658481641770010, -122.4529597091700026 37.7664588196950035, -122.4548479843199971 37.7739898231570024, -122.4753615188599980 37.7730400091340002, -122.5110670852699997 37.7712760364340028)))", - "slug": "another-post", - "title": "Golden Gate Park", - "updated": "2012-03-07 21:48:48", - "user": 1 - }, - "model": "gis.geonote", - "pk": 2 - }, - { - "fields": { - "content": "A path inside Golden Gate Park! Huzzah!", - "created": "2012-03-07 21:51:52", - "is_active": true, - "lines": "MULTILINESTRING ((-122.5045439385699950 37.7670016534969974, -122.4999949120799982 37.7682229404420013, -122.4959608697199940 37.7691728163430014, -122.4950167321500061 37.7692406641549994, -122.4916693353000028 37.7709368392189972, -122.4849745415900060 37.7707333002660022))", - "points": null, - "polys": null, - "slug": "line-inside-golden-gate-park", - "title": "Line inside Golden Gate Park", - "updated": "2012-03-07 21:52:21", - "user": 1 - }, - "model": "gis.geonote", - "pk": 3 - } -] diff -Nru django-tastypie-0.12.0/tests/gis/models.py django-tastypie-0.13.3/tests/gis/models.py --- django-tastypie-0.12.0/tests/gis/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -from django.contrib.auth.models import User -from django.contrib.gis.db import models - -from tastypie.utils import now - - -class GeoNote(models.Model): - user = models.ForeignKey(User, related_name='notes') - title = models.CharField(max_length=255) - slug = models.SlugField() - content = models.TextField() - is_active = models.BooleanField(default=True) - created = models.DateTimeField(default=now) - updated = models.DateTimeField(default=now) - - points = models.MultiPointField(null=True, blank=True) - lines = models.MultiLineStringField(null=True, blank=True) - polys = models.MultiPolygonField(null=True, blank=True) - - objects = models.GeoManager() - - def __unicode__(self): - return self.title - - def save(self, *args, **kwargs): - self.updated = now() - return super(GeoNote, self).save(*args, **kwargs) - - -class AnnotatedGeoNote(models.Model): - note = models.OneToOneField(GeoNote, related_name='annotated', null=True) - annotations = models.TextField() - - def __unicode__(self): - return u"Annotated %s" % self.note.title diff -Nru django-tastypie-0.12.0/tests/gis/tests/http.py django-tastypie-0.13.3/tests/gis/tests/http.py --- django-tastypie-0.12.0/tests/gis/tests/http.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/tests/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,207 +0,0 @@ -from testcases import TestServerTestCase -import json - -try: - from http.client import HTTPConnection -except ImportError: - from httplib import HTTPConnection - -try: - from urllib.parse import quote -except ImportError: - from urllib import quote - - -class HTTPTestCase(TestServerTestCase): - def setUp(self): - self.start_test_server(address='localhost', port=8001) - - def tearDown(self): - self.stop_test_server() - - def get_connection(self): - return HTTPConnection('localhost', 8001) - - def test_get_apis_json(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - data = json.loads(response.read().decode('utf-8')) - self.assertEqual(response.status, 200) - self.assertEqual(data, {"geonotes": {"list_endpoint": "/api/v1/geonotes/", "schema": "/api/v1/geonotes/schema/"}, "users": {"list_endpoint": "/api/v1/users/", "schema": "/api/v1/users/schema/"}}) - - def test_get_apis_xml(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/', headers={'Accept': 'application/xml'}) - response = connection.getresponse() - connection.close() - data = response.read().decode('utf-8') - self.assertEqual(response.status, 200) - self.assertEqual(data, '\n/api/v1/geonotes//api/v1/geonotes/schema//api/v1/users//api/v1/users/schema/') - - def test_get_list(self): - connection = self.get_connection() - connection.request('GET', '/api/v1/geonotes/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - data = json.loads(response.read().decode('utf-8')) - self.assertEqual(response.status, 200) - self.assertEqual(len(data['objects']), 3) - - # Because floating point. - self.assertEqual(data['objects'][0]['content'], "Wooo two points inside Golden Gate park") - self.assertEqual(data['objects'][0]['points']['type'], 'MultiPoint') - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][0][0], -122.475233, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][0][1], 37.768616, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][1][0], -122.470416, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][1][1], 37.767381, places=5) - - self.assertEqual(data['objects'][1]['content'], "This is a note about Golden Gate Park. It contains Golden Gate Park\'s polygon") - self.assertEqual(data['objects'][1]['polys']['type'], 'MultiPolygon') - self.assertEqual(len(data['objects'][1]['polys']['coordinates']), 1) - self.assertEqual(len(data['objects'][1]['polys']['coordinates'][0]), 1) - self.assertEqual(len(data['objects'][1]['polys']['coordinates'][0][0]), 8) - - self.assertEqual(data['objects'][2]['content'], "A path inside Golden Gate Park! Huzzah!") - self.assertEqual(data['objects'][2]['lines']['type'], 'MultiLineString') - self.assertAlmostEqual(data['objects'][2]['lines']['coordinates'][0][0][0], -122.504544, places=5) - self.assertAlmostEqual(data['objects'][2]['lines']['coordinates'][0][0][1], 37.767002, places=5) - self.assertAlmostEqual(data['objects'][2]['lines']['coordinates'][0][1][0], -122.499995, places=5) - self.assertAlmostEqual(data['objects'][2]['lines']['coordinates'][0][1][1], 37.768223, places=5) - - def test_post_object(self): - connection = self.get_connection() - post_data = '{"content": "A new post.", "is_active": true, "title": "New Title", "slug": "new-title", "user": "/api/v1/users/1/"}' - connection.request('POST', '/api/v1/geonotes/', body=post_data, headers={'Accept': 'application/json', 'Content-type': 'application/json'}) - response = connection.getresponse() - self.assertEqual(response.status, 201) - self.assertEqual(dict(response.getheaders())['location'], 'http://localhost:8001/api/v1/geonotes/4/') - - # make sure posted object exists - connection.request('GET', '/api/v1/geonotes/4/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - - data = response.read().decode('utf-8') - obj = json.loads(data) - - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_post_geojson(self): - connection = self.get_connection() - post_data = """{ - "content": "A new post.", "is_active": true, "title": "New Title2", - "slug": "new-title2", "user": "/api/v1/users/1/", - "polys": { "type": "MultiPolygon", "coordinates": [ [ [ [ -122.511067, 37.771276 ], [ -122.510037, 37.766391 ], [ -122.510037, 37.763813 ], [ -122.456822, 37.765848 ], [ -122.452960, 37.766459 ], [ -122.454848, 37.773990 ], [ -122.475362, 37.773040 ], [ -122.511067, 37.771276 ] ] ] ] } - }""" - connection.request('POST', '/api/v1/geonotes/', body=post_data, headers={'Accept': 'application/json', 'Content-type': 'application/json'}) - response = connection.getresponse() - self.assertEqual(response.status, 201) - self.assertEqual(dict(response.getheaders())['location'], 'http://localhost:8001/api/v1/geonotes/4/') - - # make sure posted object exists - connection.request('GET', '/api/v1/geonotes/4/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - - data = response.read().decode('utf-8') - obj = json.loads(data) - - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - self.assertEqual(obj['polys'], {u'type': u'MultiPolygon', u'coordinates': [[[[-122.511067, 37.771276], [-122.510037, 37.766390999999999], [-122.510037, 37.763812999999999], [-122.456822, 37.765847999999998], [-122.45296, 37.766458999999998], [-122.454848, 37.773989999999998], [-122.475362, 37.773040000000002], [-122.511067, 37.771276]]]]}) - - def test_post_xml(self): - connection = self.get_connection() - post_data = """2010-03-30T20:05:00TruePoints inside Golden Gate Park note 2points-inside-golden-gate-park-note-2A new post.MultiPoint-122.47523337.768617-122.47041637.767382/api/v1/users/1/""" - connection.request('POST', '/api/v1/geonotes/', body=post_data, headers={'Accept': 'application/xml', 'Content-type': 'application/xml'}) - response = connection.getresponse() - self.assertEqual(response.status, 201) - self.assertEqual(dict(response.getheaders())['location'], 'http://localhost:8001/api/v1/geonotes/4/') - - # make sure posted object exists - connection.request('GET', '/api/v1/geonotes/4/', headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - - data = response.read().decode('utf-8') - obj = json.loads(data) - - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - # Weeeee! GeoJSON returned! - self.assertEqual(obj['points'], {"coordinates": [[-122.475233, 37.768616999999999], [-122.470416, 37.767381999999998]], "type": "MultiPoint"}) - - # Or we can ask for XML - connection.request('GET', '/api/v1/geonotes/4/', headers={'Accept': 'application/xml'}) - response = connection.getresponse() - connection.close() - - self.assertEqual(response.status, 200) - data = response.read().decode('utf-8') - - self.assertIn('-122.47523337.768617-122.47041637.767382MultiPoint', data) - - def test_filter_within(self): - golden_gate_park_json = """{"type": "MultiPolygon", "coordinates": [[[[-122.511067, 37.771276], [-122.510037, 37.766391], [-122.510037, 37.763813], [-122.456822, 37.765848], [-122.452960, 37.766459], [-122.454848, 37.773990], [-122.475362, 37.773040], [-122.511067, 37.771276]]]]}""" - - # Get points - connection = self.get_connection() - connection.request('GET', '/api/v1/geonotes/?points__within=%s' % quote(golden_gate_park_json), headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - data = json.loads(response.read().decode('utf-8')) - # We get back the points inside Golden Gate park! - self.assertEqual(data['objects'][0]['content'], "Wooo two points inside Golden Gate park") - self.assertEqual(data['objects'][0]['points']['type'], 'MultiPoint') - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][0][0], -122.475233, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][0][1], 37.768616, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][1][0], -122.470416, places=5) - self.assertAlmostEqual(data['objects'][0]['points']['coordinates'][1][1], 37.767381, places=5) - - # Get lines - connection = self.get_connection() - connection.request('GET', '/api/v1/geonotes/?lines__within=%s' % quote(golden_gate_park_json), headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - data = json.loads(response.read().decode('utf-8')) - # We get back the line inside Golden Gate park! - self.assertEqual(data['objects'][0]['content'], "A path inside Golden Gate Park! Huzzah!") - self.assertEqual(data['objects'][0]['lines']['type'], 'MultiLineString') - self.assertAlmostEqual(data['objects'][0]['lines']['coordinates'][0][0][0], -122.504544, places=5) - self.assertAlmostEqual(data['objects'][0]['lines']['coordinates'][0][0][1], 37.767002, places=5) - self.assertAlmostEqual(data['objects'][0]['lines']['coordinates'][0][1][0], -122.499995, places=5) - self.assertAlmostEqual(data['objects'][0]['lines']['coordinates'][0][1][1], 37.768223, places=5) - - def test_filter_contains(self): - points_inside_golden_gate_park = """{"coordinates": [[-122.475233, 37.768616999999999], [-122.470416, 37.767381999999998]], "type": "MultiPoint"}""" - - # Get polys that contain the points - connection = self.get_connection() - connection.request('GET', '/api/v1/geonotes/?polys__contains=%s' % quote(points_inside_golden_gate_park), headers={'Accept': 'application/json'}) - response = connection.getresponse() - connection.close() - self.assertEqual(response.status, 200) - - data = json.loads(response.read().decode('utf-8')) - # We get back the golden gate park polygon! - self.assertEqual(data['objects'][0]['content'], "This is a note about Golden Gate Park. It contains Golden Gate Park\'s polygon") - self.assertEqual(data['objects'][0]['polys']['type'], 'MultiPolygon') - self.assertEqual(len(data['objects'][0]['polys']['coordinates']), 1) - self.assertEqual(len(data['objects'][0]['polys']['coordinates'][0]), 1) - self.assertEqual(len(data['objects'][0]['polys']['coordinates'][0][0]), 8) diff -Nru django-tastypie-0.12.0/tests/gis/tests/__init__.py django-tastypie-0.13.3/tests/gis/tests/__init__.py --- django-tastypie-0.12.0/tests/gis/tests/__init__.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/tests/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -from gis.tests.http import * -from gis.tests.views import * diff -Nru django-tastypie-0.12.0/tests/gis/tests/views.py django-tastypie-0.13.3/tests/gis/tests/views.py --- django-tastypie-0.12.0/tests/gis/tests/views.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/tests/views.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -from django.http import HttpRequest -from django.test import TestCase -import json - - -class ViewsTestCase(TestCase): - def test_gets(self): - resp = self.client.get('/api/v1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['geonotes'], {'list_endpoint': '/api/v1/geonotes/', 'schema': '/api/v1/geonotes/schema/'}) - - resp = self.client.get('/api/v1/geonotes/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['meta']['limit'], 20) - self.assertEqual(len(deserialized['objects']), 3) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'Points inside Golden Gate Park note', u'Golden Gate Park', u'Line inside Golden Gate Park']) - - resp = self.client.get('/api/v1/geonotes/1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 12) - self.assertEqual(deserialized['title'], u'Points inside Golden Gate Park note') - - resp = self.client.get('/api/v1/geonotes/set/2;1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 1) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'Golden Gate Park', u'Points inside Golden Gate Park note']) - - def test_posts(self): - request = HttpRequest() - post_data = '{"content": "A new post.", "is_active": true, "title": "New Title", "slug": "new-title", "user": "/api/v1/users/1/"}' - request._body = request._raw_post_data = post_data - - resp = self.client.post('/api/v1/geonotes/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - self.assertEqual(resp['location'], 'http://testserver/api/v1/geonotes/4/') - - # make sure posted object exists - resp = self.client.get('/api/v1/geonotes/4/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['content'], 'A new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_puts(self): - request = HttpRequest() - post_data = '{"content": "Another new post.", "is_active": true, "title": "Another New Title", "slug": "new-title", "user": "/api/v1/users/1/", "lines": null, "points": null, "polys": null}' - request._body = request._raw_post_data = post_data - - resp = self.client.put('/api/v1/geonotes/1/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 204) - - # make sure posted object exists - resp = self.client.get('/api/v1/geonotes/1/', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content.decode('utf-8')) - self.assertEqual(obj['content'], 'Another new post.') - self.assertEqual(obj['is_active'], True) - self.assertEqual(obj['user'], '/api/v1/users/1/') - - def test_api_field_error(self): - # When a field error is encountered, we should be presenting the message - # back to the user. - request = HttpRequest() - post_data = '{"content": "More internet memes.", "is_active": true, "title": "IT\'S OVER 9000!", "slug": "its-over", "user": "/api/v1/users/9001/"}' - request._body = request._raw_post_data = post_data - - resp = self.client.post('/api/v1/geonotes/', data=post_data, content_type='application/json') - self.assertEqual(resp.status_code, 400) - self.assertEqual(resp.content.decode('utf-8'), '{"error": "Could not find the provided object via resource URI \'/api/v1/users/9001/\'."}') - - def test_options(self): - resp = self.client.options('/api/v1/geonotes/') - self.assertEqual(resp.status_code, 200) - allows = 'GET,POST,PUT,DELETE,PATCH' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/geonotes/1/') - self.assertEqual(resp.status_code, 200) - allows = 'GET,POST,PUT,DELETE,PATCH' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/geonotes/schema/') - self.assertEqual(resp.status_code, 200) - allows = 'GET' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) - - resp = self.client.options('/api/v1/geonotes/set/2;1/') - self.assertEqual(resp.status_code, 200) - allows = 'GET' - self.assertEqual(resp['Allow'], allows) - self.assertEqual(resp.content.decode('utf-8'), allows) diff -Nru django-tastypie-0.12.0/tests/gis/urls.py django-tastypie-0.13.3/tests/gis/urls.py --- django-tastypie-0.12.0/tests/gis/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/gis/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -try: - from django.conf.urls import patterns, include -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include - -urlpatterns = patterns('', - (r'^api/', include('gis.api.urls')), -) diff -Nru django-tastypie-0.12.0/tests/manage_alphanumeric.py django-tastypie-0.13.3/tests/manage_alphanumeric.py --- django-tastypie-0.12.0/tests/manage_alphanumeric.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_alphanumeric.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_alphanumeric") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_authorization.py django-tastypie-0.13.3/tests/manage_authorization.py --- django-tastypie-0.12.0/tests/manage_authorization.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_authorization.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_authorization") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_basic.py django-tastypie-0.13.3/tests/manage_basic.py --- django-tastypie-0.12.0/tests/manage_basic.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_basic.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_basic") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_complex.py django-tastypie-0.13.3/tests/manage_complex.py --- django-tastypie-0.12.0/tests/manage_complex.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_complex.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_complex") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_content_gfk.py django-tastypie-0.13.3/tests/manage_content_gfk.py --- django-tastypie-0.12.0/tests/manage_content_gfk.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_content_gfk.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_content_gfk") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_core.py django-tastypie-0.13.3/tests/manage_core.py --- django-tastypie-0.12.0/tests/manage_core.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_core.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_core") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_customuser.py django-tastypie-0.13.3/tests/manage_customuser.py --- django-tastypie-0.12.0/tests/manage_customuser.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_customuser.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_customuser") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_gis.py django-tastypie-0.13.3/tests/manage_gis.py --- django-tastypie-0.12.0/tests/manage_gis.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_gis.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_gis") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_namespaced.py django-tastypie-0.13.3/tests/manage_namespaced.py --- django-tastypie-0.12.0/tests/manage_namespaced.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_namespaced.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_namespaced") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_related.py django-tastypie-0.13.3/tests/manage_related.py --- django-tastypie-0.12.0/tests/manage_related.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_related.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_related") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_slashless.py django-tastypie-0.13.3/tests/manage_slashless.py --- django-tastypie-0.12.0/tests/manage_slashless.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_slashless.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_slashless") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/manage_validation.py django-tastypie-0.13.3/tests/manage_validation.py --- django-tastypie-0.12.0/tests/manage_validation.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/manage_validation.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -#!/usr/bin/env python -import os, sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_validation") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff -Nru django-tastypie-0.12.0/tests/namespaced/api/resources.py django-tastypie-0.13.3/tests/namespaced/api/resources.py --- django-tastypie-0.12.0/tests/namespaced/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/namespaced/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -from django.contrib.auth.models import User -from tastypie import fields -from tastypie.resources import ModelResource, NamespacedModelResource -from tastypie.authorization import Authorization -from basic.models import Note - - -class NamespacedUserResource(NamespacedModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - authorization = Authorization() - - -class NamespacedNoteResource(NamespacedModelResource): - user = fields.ForeignKey(NamespacedUserResource, 'user') - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - authorization = Authorization() diff -Nru django-tastypie-0.12.0/tests/namespaced/api/urls.py django-tastypie-0.13.3/tests/namespaced/api/urls.py --- django-tastypie-0.12.0/tests/namespaced/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/namespaced/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -try: - from django.conf.urls import patterns, include, url -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include, url -from tastypie.api import NamespacedApi -from namespaced.api.resources import NamespacedNoteResource, NamespacedUserResource - -api = NamespacedApi(api_name='v1', urlconf_namespace='special') -api.register(NamespacedNoteResource(), canonical=True) -api.register(NamespacedUserResource(), canonical=True) - -urlpatterns = patterns('', - url(r'^api/', include(api.urls, namespace='special')), -) diff -Nru django-tastypie-0.12.0/tests/namespaced/fixtures/test_data.json django-tastypie-0.13.3/tests/namespaced/fixtures/test_data.json --- django-tastypie-0.12.0/tests/namespaced/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/namespaced/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "this_is_not_a_valid_password_string" - }, - "model": "auth.user", - "pk": 1 - }, - - { - "fields": { - "user": 1, - "title": "First Post!", - "slug": "first-post", - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "is_active": true, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "basic.note", - "pk": 1 - }, - { - "fields": { - "user": 1, - "title": "Another Post", - "slug": "another-post", - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "is_active": true, - "created": "2010-03-31 20:05:00", - "updated": "2010-03-31 20:05:00" - }, - "model": "basic.note", - "pk": 2 - } -] diff -Nru django-tastypie-0.12.0/tests/namespaced/tests.py django-tastypie-0.13.3/tests/namespaced/tests.py --- django-tastypie-0.12.0/tests/namespaced/tests.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/namespaced/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -from django.conf import settings -from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import HttpRequest -import json -from testcases import TestCaseWithFixture - - -class NamespacedViewsTestCase(TestCaseWithFixture): - urls = 'namespaced.api.urls' - - def test_urls(self): - from namespaced.api.urls import api - patterns = api.urls - self.assertEqual(len(patterns), 3) - self.assertEqual(sorted([pattern.name for pattern in patterns if hasattr(pattern, 'name')]), ['api_v1_top_level']) - self.assertEqual([[pattern.name for pattern in include.url_patterns if hasattr(pattern, 'name')] for include in patterns if hasattr(include, 'reverse_dict')], [['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail'], ['api_dispatch_list', 'api_get_schema', 'api_get_multiple', 'api_dispatch_detail']]) - - self.assertRaises(NoReverseMatch, reverse, 'api_v1_top_level') - self.assertRaises(NoReverseMatch, reverse, 'special:api_v1_top_level') - self.assertEquals(reverse('special:api_v1_top_level', kwargs={'api_name': 'v1'}), '/api/v1/') - self.assertEquals(reverse('special:api_dispatch_list', kwargs={'api_name': 'v1', 'resource_name': 'notes'}), '/api/v1/notes/') diff -Nru django-tastypie-0.12.0/tests/related_resource/api/resources.py django-tastypie-0.13.3/tests/related_resource/api/resources.py --- django-tastypie-0.12.0/tests/related_resource/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,212 +0,0 @@ -from django.contrib.auth.models import User - -from tastypie import fields -from tastypie.resources import ModelResource -from tastypie.authorization import Authorization - -from core.models import Note, MediaBit - -from related_resource.models import Category, Tag, ExtraData, Taggable,\ - TaggableTag, Person, Company, Product, Address, Dog, Forum, DogHouse, Bone, Job, Payment, Label, Post - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - allowed_methods = ['get'] - authorization = Authorization() - - -class NoteResource(ModelResource): - author = fields.ForeignKey(UserResource, 'author') - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - authorization = Authorization() - - -class CategoryResource(ModelResource): - parent = fields.ToOneField('self', 'parent', null=True) - - class Meta: - resource_name = 'category' - queryset = Category.objects.all() - authorization = Authorization() - - -class TagResource(ModelResource): - taggabletags = fields.ToManyField( - 'related_resource.api.resources.TaggableTagResource', 'taggabletags', - null=True) - - extradata = fields.ToOneField( - 'related_resource.api.resources.ExtraDataResource', 'extradata', - null=True, blank=True, full=True) - - class Meta: - resource_name = 'tag' - queryset = Tag.objects.all() - authorization = Authorization() - - -class TaggableResource(ModelResource): - taggabletags = fields.ToManyField( - 'related_resource.api.resources.TaggableTagResource', 'taggabletags', - null=True) - - class Meta: - resource_name = 'taggable' - queryset = Taggable.objects.all() - authorization = Authorization() - - -class TaggableTagResource(ModelResource): - tag = fields.ToOneField( - 'related_resource.api.resources.TagResource', 'tag', - null=True) - taggable = fields.ToOneField( - 'related_resource.api.resources.TaggableResource', 'taggable', - null=True) - - class Meta: - resource_name = 'taggabletag' - queryset = TaggableTag.objects.all() - authorization = Authorization() - - -class ExtraDataResource(ModelResource): - tag = fields.ToOneField( - 'related_resource.api.resources.TagResource', 'tag', - null=True) - - class Meta: - resource_name = 'extradata' - queryset = ExtraData.objects.all() - authorization = Authorization() - - -class FreshNoteResource(ModelResource): - media_bits = fields.ToManyField('related_resource.api.resources.FreshMediaBitResource', 'media_bits', related_name='note') - - class Meta: - queryset = Note.objects.all() - resource_name = 'freshnote' - authorization = Authorization() - - -class FreshMediaBitResource(ModelResource): - note = fields.ToOneField(FreshNoteResource, 'note') - - class Meta: - queryset = MediaBit.objects.all() - resource_name = 'freshmediabit' - authorization = Authorization() - - -class AddressResource(ModelResource): - class Meta: - queryset = Address.objects.all() - resource_name = 'address' - authorization = Authorization() - - -class ProductResource(ModelResource): - producer = fields.ToOneField('related_resource.api.resources.CompanyResource', 'producer') - - class Meta: - queryset = Product.objects.all() - resource_name = 'product' - authorization = Authorization() - - -class CompanyResource(ModelResource): - address = fields.ToOneField(AddressResource, 'address', null=True, full=True) - products = fields.ToManyField(ProductResource, 'products', full=True, related_name='producer', null=True) - - class Meta: - queryset = Company.objects.all() - resource_name = 'company' - authorization = Authorization() - - -class PersonResource(ModelResource): - company = fields.ToOneField(CompanyResource, 'company', null=True, full=True) - dogs = fields.ToManyField('related_resource.api.resources.DogResource', 'dogs', full=True, related_name='owner', null=True) - - class Meta: - queryset = Person.objects.all() - resource_name = 'person' - authorization = Authorization() - - -class DogHouseResource(ModelResource): - class Meta: - queryset = DogHouse.objects.all() - resource_name = 'doghouse' - authorization = Authorization() - - -class BoneResource(ModelResource): - dog = fields.ToOneField('related_resource.api.resources.DogResource', 'dog', null=True) - - class Meta: - queryset = Bone.objects.all() - resource_name = 'bone' - authorization = Authorization() - - -class DogResource(ModelResource): - owner = fields.ToOneField(PersonResource, 'owner') - house = fields.ToOneField(DogHouseResource, 'house', full=True, null=True) - bones = fields.ToManyField(BoneResource, 'bones', full=True, null=True, related_name='dog') - - class Meta: - queryset = Dog.objects.all() - resource_name = 'dog' - authorization = Authorization() - -class LabelResource(ModelResource): - class Meta: - resource_name = 'label' - queryset = Label.objects.all() - authorization = Authorization() - - -class PostResource(ModelResource): - label = fields.ToManyField(LabelResource, 'label', null=True) - - class Meta: - queryset = Post.objects.all() - resource_name = 'post' - authorization = Authorization() - -class PaymentResource(ModelResource): - job = fields.ToOneField('related_resource.api.resources.JobResource', 'job') - - class Meta: - queryset = Payment.objects.all() - resource_name = 'payment' - authorization = Authorization() - allowed_methods = ('get','put','post') - -class JobResource(ModelResource): - payment = fields.ToOneField(PaymentResource, 'payment', related_name='job') - - class Meta: - queryset = Job.objects.all() - resource_name = 'job' - authorization = Authorization() - allowed_methods = ('get','put','post') - -class ForumResource(ModelResource): - moderators = fields.ManyToManyField(UserResource, 'moderators', full=True) - members = fields.ManyToManyField(UserResource, 'members', full=True) - - class Meta: - resource_name = 'forum' - queryset = Forum.objects.all() - authorization = Authorization() - always_return_data = True - diff -Nru django-tastypie-0.12.0/tests/related_resource/api/urls.py django-tastypie-0.13.3/tests/related_resource/api/urls.py --- django-tastypie-0.12.0/tests/related_resource/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -try: - from django.conf.urls import * -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import * - -from tastypie.api import Api - -from related_resource.api.resources import NoteResource, UserResource, \ - CategoryResource, TagResource, TaggableTagResource, TaggableResource, \ - ExtraDataResource, FreshNoteResource, FreshMediaBitResource, \ - ForumResource, CompanyResource, ProductResource, AddressResource, \ - PersonResource, DogResource, DogHouseResource, BoneResource - -from tests.related_resource.api.resources import LabelResource, PostResource - -api = Api(api_name='v1') -api.register(NoteResource(), canonical=True) -api.register(UserResource(), canonical=True) -api.register(CategoryResource(), canonical=True) -api.register(TagResource(), canonical=True) -api.register(TaggableResource(), canonical=True) -api.register(TaggableTagResource(), canonical=True) -api.register(ExtraDataResource(), canonical=True) -api.register(FreshNoteResource(), canonical=True) -api.register(FreshMediaBitResource(), canonical=True) -api.register(ForumResource(), canonical=True) -api.register(CompanyResource(), canonical=True) -api.register(ProductResource(), canonical=True) -api.register(AddressResource(), canonical=True) -api.register(PersonResource(), canonical=True) -api.register(DogResource(), canonical=True) -api.register(DogHouseResource(), canonical=True) -api.register(BoneResource(), canonical=True) -api.register(PostResource(), canonical=True) -api.register(LabelResource(), canonical=True) - -urlpatterns = api.urls diff -Nru django-tastypie-0.12.0/tests/related_resource/fixtures/test_data.json django-tastypie-0.13.3/tests/related_resource/fixtures/test_data.json --- django-tastypie-0.12.0/tests/related_resource/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "this_is_not_a_valid_password_string" - }, - "model": "auth.user", - "pk": 1 - }, - - { - "fields": { - "author": 1, - "title": "First Post!", - "slug": "first-post", - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "is_active": true, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "core.note", - "pk": 1 - }, - { - "fields": { - "author": 1, - "title": "Another Post", - "slug": "another-post", - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "is_active": true, - "created": "2010-03-31 20:05:00", - "updated": "2010-03-31 20:05:00" - }, - "model": "core.note", - "pk": 2 - }, - { - "fields": { - "name": "coffee" - }, - "model": "related_resource.label", - "pk": 1 - } -] diff -Nru django-tastypie-0.12.0/tests/related_resource/models.py django-tastypie-0.13.3/tests/related_resource/models.py --- django-tastypie-0.12.0/tests/related_resource/models.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -from django.contrib.auth.models import User -from django.db import models - -# A self-referrential model to test regressions. -class Category(models.Model): - parent = models.ForeignKey('self', null=True) - name = models.CharField(max_length=32) - - def __unicode__(self): - return u"%s (%s)" % (self.name, self.parent) - - -# A taggable model. Just that. -class Taggable(models.Model): - name = models.CharField(max_length=32) - - -# Explicit intermediary 'through' table -class TaggableTag(models.Model): - tag = models.ForeignKey( - 'Tag', - related_name='taggabletags', - null=True, blank=True, # needed at creation time - ) - taggable = models.ForeignKey( - 'Taggable', - related_name='taggabletags', - null=True, blank=True, # needed at creation time - ) - extra = models.IntegerField(default=0) #extra data about the relationship - - -# Tags to Taggable model through explicit M2M table -class Tag(models.Model): - name = models.CharField(max_length=32) - tagged = models.ManyToManyField( - 'Taggable', - through='TaggableTag', - related_name='tags', - ) - - def __unicode__(self): - return u"%s" % (self.name) - - -# A model that contains additional data for Tag -class ExtraData(models.Model): - name = models.CharField(max_length=32) - tag = models.OneToOneField( - 'Tag', - related_name='extradata', - null=True, blank=True, - ) - - def __unicode__(self): - return u"%s" % (self.name) - - -class Address(models.Model): - line = models.CharField(max_length=32) - - def __unicode__(self): - return u"%s" % (self.line) - - -class Company(models.Model): - name = models.CharField(max_length=32) - address = models.ForeignKey(Address, null=True) - - def __unicode__(self): - return u"%s" % (self.name) - - -class Product(models.Model): - name = models.CharField(max_length=32) - producer = models.ForeignKey(Company, related_name="products") - - def __unicode__(self): - return u"%s" % (self.name) - -class Person(models.Model): - name = models.CharField(max_length=32) - company = models.ForeignKey(Company, related_name="employees", null=True) - - def __unicode__(self): - return u"%s" % (self.name) - -class DogHouse(models.Model): - color = models.CharField(max_length=32) - - def __unicode__(self): - return u"%s" % (self.color) - -class Dog(models.Model): - name = models.CharField(max_length=32) - owner = models.ForeignKey(Person, related_name="dogs") - house = models.ForeignKey(DogHouse, related_name="dogs", null=True) - - def __unicode__(self): - return u"%s" % (self.name) - -class Bone(models.Model): - dog = models.ForeignKey(Dog, related_name='bones', null=True) - color = models.CharField(max_length=32) - - def __unicode__(self): - return u"%s" % (self.color) - -class Forum(models.Model): - moderators = models.ManyToManyField(User, related_name='forums_moderated') - members = models.ManyToManyField(User, related_name='forums_member') - -class Label(models.Model): - name = models.CharField(max_length=32) - -class Job(models.Model): - name = models.CharField(max_length=200) - -class Payment(models.Model): - scheduled = models.DateTimeField() - job = models.OneToOneField(Job, related_name="payment", null=True) - -class Post(models.Model): - name = models.CharField(max_length=200) - label = models.ManyToManyField(Label, null=True) - diff -Nru django-tastypie-0.12.0/tests/related_resource/tests.py django-tastypie-0.13.3/tests/related_resource/tests.py --- django-tastypie-0.12.0/tests/related_resource/tests.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,748 +0,0 @@ -from datetime import datetime, tzinfo, timedelta -import json - -import django -from django.conf import settings -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.db.models.signals import pre_save - -from tastypie import fields -from tastypie.exceptions import NotFound - -from core.models import Note, MediaBit -from core.tests.mocks import MockRequest -from core.tests.resources import HttpRequest - -from related_resource.api.resources import CategoryResource, ForumResource, FreshNoteResource, JobResource, NoteResource, PersonResource, UserResource -from related_resource.api.urls import api -from related_resource.models import Category, Label, Tag, Taggable, TaggableTag, ExtraData, Company, Person, Dog, DogHouse, Bone, Product, Address, Job, Payment -from testcases import TestCaseWithFixture - - -class M2MResourcesTestCase(TestCaseWithFixture): - def test_same_object_added(self): - """ - From Issue #1035 - """ - - user=User.objects.create(username='gjcourt') - - ur=UserResource() - fr=ForumResource() - - resp = self.client.post(fr.get_resource_uri(), content_type='application/json', data=json.dumps({ - 'name': 'Test Forum', - 'members': [ur.get_resource_uri(user)], - 'moderators': [ur.get_resource_uri(user)], - })) - self.assertEqual(resp.status_code, 201, resp.content) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(data['moderators']), 1) - self.assertEqual(len(data['members']), 1) - - -class RelatedResourceTest(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def setUp(self): - super(RelatedResourceTest, self).setUp() - self.user = User.objects.create(username="testy_mctesterson") - - def test_cannot_access_user_resource(self): - resource = api.canonical_resource_for('users') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.set_body('{"username": "foobar"}') - resp = resource.wrap_view('dispatch_detail')(request, pk=self.user.pk) - - self.assertEqual(resp.status_code, 405) - self.assertEqual(User.objects.get(id=self.user.id).username, self.user.username) - - def test_related_resource_authorization(self): - resource = api.canonical_resource_for('notes') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body('{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00", "author": null}') - - resp = resource.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(User.objects.get(id=self.user.id).username, 'testy_mctesterson') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body('{"content": "The cat is back. The dog coughed him up out back.", "created": "2010-04-03 20:05:00", "is_active": true, "slug": "cat-is-back-2", "title": "The Cat Is Back", "updated": "2010-04-03 20:05:00", "author": {"id": %s, "username": "foobar"}}' % self.user.id) - - resp = resource.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(User.objects.get(id=self.user.id).username, 'foobar') - - -class CategoryResourceTest(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def setUp(self): - super(CategoryResourceTest, self).setUp() - self.parent_cat_1 = Category.objects.create(parent=None, name='Dad') - self.parent_cat_2 = Category.objects.create(parent=None, name='Mom') - self.child_cat_1 = Category.objects.create(parent=self.parent_cat_1, name='Son') - self.child_cat_2 = Category.objects.create(parent=self.parent_cat_2, name='Daughter') - - def test_correct_relation(self): - resource = api.canonical_resource_for('category') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - resp = resource.wrap_view('dispatch_detail')(request, pk=self.parent_cat_1.pk) - - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(data['parent'], None) - self.assertEqual(data['name'], 'Dad') - - # Now try a child. - resp = resource.wrap_view('dispatch_detail')(request, pk=self.child_cat_2.pk) - - self.assertEqual(resp.status_code, 200) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(data['parent'], '/v1/category/2/') - self.assertEqual(data['name'], 'Daughter') - - def test_put_null(self): - resource = api.canonical_resource_for('category') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.set_body('{"parent": null, "name": "Son"}') - - # Before the PUT, there should be a parent. - self.assertEqual(Category.objects.get(pk=self.child_cat_1.pk).parent.pk, self.parent_cat_1.pk) - - # After the PUT, the parent should be ``None``. - resp = resource.put_detail(request, pk=self.child_cat_1.pk) - self.assertEqual(resp.status_code, 204) - self.assertEqual(Category.objects.get(pk=self.child_cat_1.pk).name, 'Son') - self.assertEqual(Category.objects.get(pk=self.child_cat_1.pk).parent, None) - - -class ExplicitM2MResourceRegressionTest(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def setUp(self): - super(ExplicitM2MResourceRegressionTest, self).setUp() - self.tag_1 = Tag.objects.create(name='important') - self.taggable_1 = Taggable.objects.create(name='exam') - - # Create relations between tags and taggables through the explicit m2m table - self.taggabletag_1 = TaggableTag.objects.create(tag=self.tag_1, taggable=self.taggable_1) - - # Give each tag some extra data (the lookup of this data is what makes the test fail) - self.extradata_1 = ExtraData.objects.create(tag=self.tag_1, name='additional') - - - def test_correct_setup(self): - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'GET' - - # Verify the explicit 'through' relationships has been created correctly - resource = api.canonical_resource_for('taggabletag') - resp = resource.wrap_view('dispatch_detail')(request, pk=self.taggabletag_1.pk) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['tag'], '/v1/tag/1/') - self.assertEqual(data['taggable'], '/v1/taggable/1/') - - resource = api.canonical_resource_for('taggable') - resp = resource.wrap_view('dispatch_detail')(request, pk=self.taggable_1.pk) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['name'], 'exam') - - resource = api.canonical_resource_for('tag') - request.path = "/v1/tag/%(pk)s/" % {'pk': self.tag_1.pk} - resp = resource.wrap_view('dispatch_detail')(request, pk=self.tag_1.pk) - data = json.loads(resp.content.decode('utf-8')) - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['name'], 'important') - - # and check whether the extradata is present - self.assertEqual(data['extradata']['name'], u'additional') - - - def test_post_new_tag(self): - resource = api.canonical_resource_for('tag') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body('{"name": "school", "taggabletags": [ ]}') - - # Prior to the addition of ``blank=True``, this would - # fail badly. - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp.status_code, 201) - - # GET the created object (through its headers.location) - self.assertTrue(resp.has_header('location')) - location = resp['Location'] - - resp = self.client.get(location, data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 5) - self.assertEqual(deserialized['name'], 'school') - - -class OneToManySetupTestCase(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def test_one_to_many(self): - # Sanity checks. - self.assertEqual(Note.objects.count(), 2) - self.assertEqual(MediaBit.objects.count(), 0) - - fnr = FreshNoteResource() - - data = { - 'title': 'Create with related URIs', - 'slug': 'create-with-related-uris', - 'content': 'Some content here', - 'is_active': True, - 'media_bits': [ - { - 'title': 'Picture #1' - } - ] - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - - resp = fnr.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Note.objects.count(), 3) - note = Note.objects.latest('created') - self.assertEqual(note.media_bits.count(), 1) - self.assertEqual(note.media_bits.all()[0].title, u'Picture #1') - - -class FullCategoryResource(CategoryResource): - parent = fields.ToOneField('self', 'parent', null=True, full=True) - -class RelationshipOppositeFromModelTestCase(TestCaseWithFixture): - ''' - On the model, the Job relationship is defined on the Payment. - On the resource, the PaymentResource is defined on the JobResource as well - ''' - def setUp(self): - super(RelationshipOppositeFromModelTestCase, self).setUp() - - # a job with a payment exists to start with - self.some_time_str = datetime.now().strftime('%Y-%m-%d %H:%M') - job = Job.objects.create(name='SomeJob') - payment = Payment.objects.create(job=job, scheduled=self.some_time_str) - - def test_create_similar(self): - # We submit to job with the related payment included. - # Note that on the resource, the payment related resource is defined - # On the model, the Job class does not have a payment field, - # but it has a reverse relationship defined by the Payment class - resource = JobResource() - data = { - 'name': 'OtherJob', - 'payment': { - 'scheduled': self.some_time_str - } - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - - resp = resource.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Job.objects.count(), 2) - self.assertEqual(Payment.objects.count(), 2) - - new_job = Job.objects.all().order_by('-id')[0] - new_payment = Payment.objects.all().order_by('-id')[0] - - self.assertEqual(new_job.name, 'OtherJob') - self.assertEqual(new_job, new_payment.job) - - - -class RelatedPatchTestCase(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def setUp(self): - super(RelatedPatchTestCase, self).setUp() - #this test doesn't use MockRequest, so the body attribute is different. - if django.VERSION >= (1, 4): - self.body_attr = "_body" - else: - self.body_attr = "_raw_post_data" - - def test_patch_to_one(self): - resource = FullCategoryResource() - cat1 = Category.objects.create(name='Dad') - cat2 = Category.objects.create(parent=cat1, name='Child') - - request = HttpRequest() - request.GET = {'format': 'json'} - request.method = 'PATCH' - request.path = "/v1/category/%(pk)s/" % {'pk': cat2.pk} - request._read_started = False - - data = { - 'name': 'Kid' - } - - setattr(request, self.body_attr, json.dumps(data)) - self.assertEqual(cat2.name, 'Child') - resp = resource.patch_detail(request, pk=cat2.pk) - self.assertEqual(resp.status_code, 202) - cat2 = Category.objects.get(pk=2) - self.assertEqual(cat2.name, 'Kid') - - -class NestedRelatedResourceTest(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def test_one_to_one(self): - """ - Test a related ToOne resource with a nested full ToOne resource - """ - self.assertEqual(Person.objects.count(), 0) - self.assertEqual(Company.objects.count(), 0) - self.assertEqual(Address.objects.count(), 0) - - pr = PersonResource() - - data = { - 'name': 'Joan Rivers', - 'company': { - 'name': 'Yum Yum Pie Factory!', - 'address': { - 'line': 'Somewhere, Utah' - } - } - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - resp = pr.post_list(request) - self.assertEqual(resp.status_code, 201) - - pk = Person.objects.all()[0].pk - request = MockRequest() - request.method = 'GET' - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.get_detail(request, pk=pk) - self.assertEqual(resp.status_code, 200) - - person = json.loads(resp.content.decode('utf-8')) - self.assertEqual(person['name'], 'Joan Rivers') - - company = person['company'] - self.assertEqual(company['name'], 'Yum Yum Pie Factory!') - - address = company['address'] - self.assertEqual(address['line'], 'Somewhere, Utah') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - request.set_body(resp.content.decode('utf-8')) - resp = pr.put_detail(request, pk=pk) - self.assertEqual(resp.status_code, 204) - - def test_one_to_many(self): - """ - Test a related ToOne resource with a nested full ToMany resource - """ - self.assertEqual(Person.objects.count(), 0) - self.assertEqual(Company.objects.count(), 0) - self.assertEqual(Product.objects.count(), 0) - - pr = PersonResource() - - data = { - 'name': 'Joan Rivers', - 'company': { - 'name': 'Yum Yum Pie Factory!', - 'products': [ - { - 'name': 'Tasty Pie' - } - ] - } - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - resp = pr.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Person.objects.count(), 1) - self.assertEqual(Company.objects.count(), 1) - self.assertEqual(Product.objects.count(), 1) - - pk = Person.objects.all()[0].pk - request = MockRequest() - request.method = 'GET' - resp = pr.get_detail(request, pk=pk) - self.assertEqual(resp.status_code, 200) - - person = json.loads(resp.content.decode('utf-8')) - self.assertEqual(person['name'], 'Joan Rivers') - - company = person['company'] - self.assertEqual(company['name'], 'Yum Yum Pie Factory!') - self.assertEqual(len(company['products']), 1) - - product = company['products'][0] - self.assertEqual(product['name'], 'Tasty Pie') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.set_body(json.dumps(person)) - resp = pr.put_detail(request, pk=pk) - self.assertEqual(resp.status_code, 204) - - def test_many_to_one(self): - """ - Test a related ToMany resource with a nested full ToOne resource - """ - self.assertEqual(Person.objects.count(), 0) - self.assertEqual(Dog.objects.count(), 0) - self.assertEqual(DogHouse.objects.count(), 0) - - pr = PersonResource() - - data = { - 'name': 'Joan Rivers', - 'dogs': [ - { - 'name': 'Snoopy', - 'house': { - 'color': 'Red' - } - } - ] - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - resp = pr.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Person.objects.count(), 1) - self.assertEqual(Dog.objects.count(), 1) - self.assertEqual(DogHouse.objects.count(), 1) - - pk = Person.objects.all()[0].pk - request = MockRequest() - request.method = 'GET' - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.get_detail(request, pk=pk) - self.assertEqual(resp.status_code, 200) - - person = json.loads(resp.content.decode('utf-8')) - self.assertEqual(person['name'], 'Joan Rivers') - self.assertEqual(len(person['dogs']), 1) - - dog = person['dogs'][0] - self.assertEqual(dog['name'], 'Snoopy') - - house = dog['house'] - self.assertEqual(house['color'], 'Red') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.set_body(json.dumps(person)) - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.put_detail(request, pk=pk) - self.assertEqual(resp.status_code, 204) - - def test_many_to_many(self): - """ - Test a related ToMany resource with a nested full ToMany resource - """ - self.assertEqual(Person.objects.count(), 0) - self.assertEqual(Dog.objects.count(), 0) - self.assertEqual(Bone.objects.count(), 0) - - pr = PersonResource() - - data = { - 'name': 'Joan Rivers', - 'dogs': [ - { - 'name': 'Snoopy', - 'bones': [ - { - 'color': 'white' - } - ] - } - ] - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.path = reverse('api_dispatch_list', kwargs={'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - request.set_body(json.dumps(data)) - resp = pr.post_list(request) - self.assertEqual(resp.status_code, 201) - self.assertEqual(Person.objects.count(), 1) - self.assertEqual(Dog.objects.count(), 1) - self.assertEqual(Bone.objects.count(), 1) - - pk = Person.objects.all()[0].pk - request = MockRequest() - request.method = 'GET' - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.get_detail(request, pk=pk) - self.assertEqual(resp.status_code, 200) - - person = json.loads(resp.content.decode('utf-8')) - self.assertEqual(person['name'], 'Joan Rivers') - self.assertEqual(len(person['dogs']), 1) - - dog = person['dogs'][0] - self.assertEqual(dog['name'], 'Snoopy') - self.assertEqual(len(dog['bones']), 1) - - bone = dog['bones'][0] - self.assertEqual(bone['color'], 'white') - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request.set_body(json.dumps(person)) - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.put_detail(request, pk=pk) - self.assertEqual(resp.status_code, 204) - - #Change just a nested resource via PUT - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - person['dogs'][0]['bones'][0]['color'] = 'gray' - body = json.dumps(person) - request.set_body(body) - request.path = reverse('api_dispatch_detail', kwargs={'pk': pk, 'resource_name': pr._meta.resource_name, 'api_name': pr._meta.api_name}) - resp = pr.put_detail(request, pk=pk) - self.assertEqual(resp.status_code, 204) - - self.assertEqual(Bone.objects.count(), 1) - bone = Bone.objects.all()[0] - self.assertEqual(bone.color, 'gray') - - -class RelatedSaveCallsTest(TestCaseWithFixture): - urls = 'related_resource.api.urls' - - def test_one_query_for_post_list(self): - """ - Posting a new detail with no related objects - should require one query to save the object - """ - resource = api.canonical_resource_for('category') - - request = MockRequest() - body = json.dumps({ - 'name': 'Foo', - 'parent': None - }) - request.set_body(body) - - with self.assertNumQueries(1): - resp = resource.post_list(request) - - - def test_two_queries_for_post_list(self): - """ - Posting a new detail with one related object, referenced via its - ``resource_uri`` should require two queries: one to save the - object, and one to lookup the related object. - """ - parent = Category.objects.create(name='Bar') - resource = api.canonical_resource_for('category') - - request = MockRequest() - body = json.dumps({ - 'name': 'Foo', - 'parent': resource.get_resource_uri(parent) - }) - - request.set_body(body) - - with self.assertNumQueries(2): - resp = resource.post_list(request) - - def test_no_save_m2m_unchanged(self): - """ - Posting a new detail with a related m2m object shouldn't - save the m2m object unless the m2m object is provided inline. - """ - def _save_fails_test(sender, **kwargs): - self.fail("Should not have saved Label") - - pre_save.connect(_save_fails_test, sender=Label) - l1 = Label.objects.get(name='coffee') - resource = api.canonical_resource_for('post') - label_resource = api.canonical_resource_for('label') - - request = MockRequest() - - body = json.dumps({ - 'name': 'test post', - 'label': [label_resource.get_resource_uri(l1)], - }) - - request.set_body(body) - - resource.post_list(request) #_save_fails_test will explode if Label is saved - - - def test_save_m2m_changed(self): - """ - Posting a new or updated detail object with a related m2m object - should save the m2m object if it's included inline. - """ - - resource = api.canonical_resource_for('tag') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - body_dict = {'name':'school', - 'taggabletags':[{'extra':7}] - } - - request.set_body(json.dumps(body_dict)) - - resp = resource.wrap_view('dispatch_list')(request) - self.assertEqual(resp.status_code, 201) - - #'extra' should have been set - tag = Tag.objects.all()[0] - taggable_tag = tag.taggabletags.all()[0] - self.assertEqual(taggable_tag.extra, 7) - - body_dict['taggabletags'] = [{'extra':1234}] - - request.set_body(json.dumps(body_dict)) - - request.path = reverse('api_dispatch_detail', kwargs={'pk': tag.pk, - 'resource_name': resource._meta.resource_name, - 'api_name': resource._meta.api_name}) - - resource.put_detail(request) - - #'extra' should have changed - tag = Tag.objects.all()[0] - taggable_tag = tag.taggabletags.all()[0] - self.assertEqual(taggable_tag.extra, 1234) - - def test_no_save_m2m_unchanged_existing_data_persists(self): - """ - Data should persist when posting an updated detail object with - unchanged reverse realated objects. - """ - - person = Person.objects.create(name='Ryan') - dog = Dog.objects.create(name='Wilfred', owner=person) - bone1 = Bone.objects.create(color='White', dog=dog) - bone2 = Bone.objects.create(color='Grey', dog=dog) - - self.assertEqual(dog.bones.count(), 2) - - resource = api.canonical_resource_for('dog') - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'PUT' - request._load_post_and_files = lambda *args, **kwargs: None - body_dict = { - 'id': dog.id, - 'name': 'Wilfred', - 'bones': [ - {'id': bone1.id, 'color': bone1.color}, - {'id': bone2.id, 'color': bone2.color} - ] - } - - request.set_body(json.dumps(body_dict)) - - resp = resource.wrap_view('dispatch_detail')(request, pk=dog.pk) - - self.assertEqual(resp.status_code, 204) - - dog = Dog.objects.all()[0] - - dog_bones = dog.bones.all() - - self.assertEqual(len(dog_bones), 2) - - self.assertEqual(dog_bones[0], bone1) - self.assertEqual(dog_bones[1], bone2) - - -class CorrectUriRelationsTestCase(TestCaseWithFixture): - """ - Validate that incorrect URI (with PKs that line up to valid data) are not - accepted. - """ - urls = 'related_resource.api.urls' - - def test_incorrect_uri(self): - self.assertEqual(Note.objects.count(), 2) - nr = NoteResource() - - # For this test, we need a ``User`` with the same PK as a ``Note``. - note_1 = Note.objects.latest('created') - user_2 = User.objects.create( - id=note_1.pk, - username='valid', - email='valid@exmaple.com', - password='junk' - ) - - data = { - # This URI is flat-out wrong (wrong resource). - # This should cause the request to fail. - 'author': '/v1/notes/{0}/'.format( - note_1.pk - ), - 'title': 'Nopenopenope', - 'slug': 'invalid-request', - 'content': "This shouldn't work.", - 'is_active': True, - } - - request = MockRequest() - request.GET = {'format': 'json'} - request.method = 'POST' - request.set_body(json.dumps(data)) - - with self.assertRaises(NotFound) as cm: - nr.post_list(request) - - self.assertEqual(str(cm.exception), "An incorrect URL was provided '/v1/notes/2/' for the 'UserResource' resource.") - self.assertEqual(Note.objects.count(), 2) diff -Nru django-tastypie-0.12.0/tests/related_resource/views.py django-tastypie-0.13.3/tests/related_resource/views.py --- django-tastypie-0.12.0/tests/related_resource/views.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/related_resource/views.py 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -# Create your views here. diff -Nru django-tastypie-0.12.0/tests/requirements.txt django-tastypie-0.13.3/tests/requirements.txt --- django-tastypie-0.12.0/tests/requirements.txt 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ --r ../requirements.txt -biplist -defusedxml -lxml -mock -pytz==2013b -PyYAML diff -Nru django-tastypie-0.12.0/tests/run_all_tests.sh django-tastypie-0.13.3/tests/run_all_tests.sh --- django-tastypie-0.12.0/tests/run_all_tests.sh 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/run_all_tests.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,51 +0,0 @@ -#!/bin/bash - -PYTHONPATH=$PWD:$PWD/..${PYTHONPATH:+:$PYTHONPATH} -export PYTHONPATH - -VERSION=`django-admin.py --version` -arrIN=(${VERSION//./ }) -major=${arrIN[0]} -minor=${arrIN[1]} - -#Don't run customuser tests if django's version is less than 1.5. -if [ $major -lt '2' -a $minor -lt '5' ]; then - ALL="core basic alphanumeric slashless namespaced related validation gis content_gfk authorization" -else - ALL="core customuser basic alphanumeric slashless namespaced related validation gis content_gfk authorization" -fi - -test_module='.tests' -if [ $major -lt '2' -a $minor -lt '6' ]; then - test_module='' -fi - -if [ $# -eq 0 ]; then - PYTESTPATHS=$ALL -elif [ $1 == '-h' ]; then - echo "Valid arguments are: $ALL" -else - PYTESTPATHS=$@ -fi - -for pytestpath in $PYTESTPATHS; do - IFS='.' read -r type type_remainder <<< "$pytestpath" - - echo "** $type **" - module_name=$type - - if [ $type == 'related' ]; then - module_name=${module_name}_resource - elif [ $type == 'gis' ]; then - createdb -T template_postgis tastypie.db - spatialite tastypie-spatialite.db "SELECT InitSpatialMetaData();" - fi - - test_name=$module_name - if [ -n "$type_remainder" ]; then - test_name=$test_name.$type_remainder - fi - - ./manage_$type.py test $test_name$test_module --traceback - echo; echo -done diff -Nru django-tastypie-0.12.0/tests/settings_alphanumeric.py django-tastypie-0.13.3/tests/settings_alphanumeric.py --- django-tastypie-0.12.0/tests/settings_alphanumeric.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_alphanumeric.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -from settings import * -INSTALLED_APPS.append('alphanumeric') - -ROOT_URLCONF = 'alphanumeric.urls' - diff -Nru django-tastypie-0.12.0/tests/settings_authorization.py django-tastypie-0.13.3/tests/settings_authorization.py --- django-tastypie-0.12.0/tests/settings_authorization.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_authorization.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ -from settings import * - -INSTALLED_APPS.append('django.contrib.sites') -INSTALLED_APPS.append('authorization') - -ROOT_URLCONF = 'authorization.urls' diff -Nru django-tastypie-0.12.0/tests/settings_basic.py django-tastypie-0.13.3/tests/settings_basic.py --- django-tastypie-0.12.0/tests/settings_basic.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_basic.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ -from settings import * -INSTALLED_APPS.append('django.contrib.sessions') -INSTALLED_APPS.append('basic') - -ROOT_URLCONF = 'basic.urls' - diff -Nru django-tastypie-0.12.0/tests/settings_complex.py django-tastypie-0.13.3/tests/settings_complex.py --- django-tastypie-0.12.0/tests/settings_complex.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_complex.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -from settings import * - -INSTALLED_APPS += [ - 'complex', - 'django.contrib.comments', - 'django.contrib.sites', -] - -ROOT_URLCONF = 'complex.urls' diff -Nru django-tastypie-0.12.0/tests/settings_content_gfk.py django-tastypie-0.13.3/tests/settings_content_gfk.py --- django-tastypie-0.12.0/tests/settings_content_gfk.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_content_gfk.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -from settings import * - -INSTALLED_APPS += [ - 'content_gfk', -] - -ROOT_URLCONF = 'content_gfk.urls' diff -Nru django-tastypie-0.12.0/tests/settings_core.py django-tastypie-0.13.3/tests/settings_core.py --- django-tastypie-0.12.0/tests/settings_core.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_core.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,31 +0,0 @@ -from settings import * -INSTALLED_APPS.append('django.contrib.sessions') -INSTALLED_APPS.append('core') - -try: - import oauth_provider - INSTALLED_APPS.append('oauth_provider') -except ImportError: - pass - -ROOT_URLCONF = 'core.tests.api_urls' -MEDIA_URL = 'http://localhost:8080/media/' - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'handlers': { - 'simple': { - 'level': 'ERROR', - 'class': 'core.utils.SimpleHandler', - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['simple'], - 'level': 'ERROR', - 'propagate': False, - }, - } -} - diff -Nru django-tastypie-0.12.0/tests/settings_customuser.py django-tastypie-0.13.3/tests/settings_customuser.py --- django-tastypie-0.12.0/tests/settings_customuser.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_customuser.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -from settings import * -INSTALLED_APPS.append('customuser') - -ROOT_URLCONF = 'core.tests.api_urls' -MEDIA_URL = 'http://localhost:8080/media/' - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'handlers': { - 'simple': { - 'level': 'ERROR', - 'class': 'core.utils.SimpleHandler', - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['simple'], - 'level': 'ERROR', - 'propagate': False, - }, - } -} - -AUTH_USER_MODEL = 'customuser.CustomUser' diff -Nru django-tastypie-0.12.0/tests/settings_gis.py django-tastypie-0.13.3/tests/settings_gis.py --- django-tastypie-0.12.0/tests/settings_gis.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_gis.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -from settings import * -INSTALLED_APPS.append('gis') - -# TIP: Try running with `sudo -u postgres ./run_all_tests.sh gis` -# We just hardcode postgis here. -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': DATABASE_NAME, - } -} - -# Run `spatialite tastypie-spatialite.db "SELECT InitSpatialMetaData();"` before -# trying spatialite on disk. -# "InitSpatiaMetaData ()error:"table spatial_ref_sys already exists" can be ignored. -#DATABASES['default']['ENGINE'] = 'django.contrib.gis.db.backends.spatialite' -#DATABASES['default']['NAME'] = 'tastypie-spatialite.db' - -ROOT_URLCONF = 'gis.urls' diff -Nru django-tastypie-0.12.0/tests/settings_namespaced.py django-tastypie-0.13.3/tests/settings_namespaced.py --- django-tastypie-0.12.0/tests/settings_namespaced.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_namespaced.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -from settings import * -INSTALLED_APPS.append('basic') -INSTALLED_APPS.append('namespaced') - -ROOT_URLCONF = 'namespaced.api.urls' diff -Nru django-tastypie-0.12.0/tests/settings.py django-tastypie-0.13.3/tests/settings.py --- django-tastypie-0.12.0/tests/settings.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -import os, sys - -from os.path import abspath, dirname, join -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -ADMINS = ( - ('test@example.com', 'Mr. Test'), -) - -BASE_PATH = os.path.abspath(os.path.dirname(__file__)) - -MEDIA_ROOT = os.path.normpath(os.path.join(BASE_PATH, 'media')) - -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = 'tastypie.db' -TEST_DATABASE_NAME = '' - -# for forwards compatibility -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.%s' % DATABASE_ENGINE, - 'NAME': DATABASE_NAME, - 'TEST_NAME': TEST_DATABASE_NAME, - } -} - - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'tastypie', -] - -DEBUG = True -TEMPLATE_DEBUG = DEBUG -CACHE_BACKEND = 'locmem://' -SECRET_KEY = 'verysecret' - -# weaker password hashing shoulod allow for faster tests -PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.CryptPasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', -) - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'loggers': { - 'py.warnings': { - 'level': 'ERROR',# change to WARNING to show DeprecationWarnings, etc. - }, - }, -} - -TASTYPIE_FULL_DEBUG = False - -# to make sure timezones are handled correctly in Django>=1.4 -USE_TZ = True - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) diff -Nru django-tastypie-0.12.0/tests/settings_related.py django-tastypie-0.13.3/tests/settings_related.py --- django-tastypie-0.12.0/tests/settings_related.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_related.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -from settings import * -INSTALLED_APPS.append('core') -INSTALLED_APPS.append('related_resource') - -ROOT_URLCONF = 'related_resource.api.urls' diff -Nru django-tastypie-0.12.0/tests/settings_slashless.py django-tastypie-0.13.3/tests/settings_slashless.py --- django-tastypie-0.12.0/tests/settings_slashless.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_slashless.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -from settings import * -INSTALLED_APPS.append('basic') -INSTALLED_APPS.append('slashless') - -ROOT_URLCONF = 'slashless.api.urls' - -APPEND_SLASH = False -TASTYPIE_ALLOW_MISSING_SLASH = True diff -Nru django-tastypie-0.12.0/tests/settings_validation.py django-tastypie-0.13.3/tests/settings_validation.py --- django-tastypie-0.12.0/tests/settings_validation.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/settings_validation.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -from settings import * -INSTALLED_APPS.append('basic') -INSTALLED_APPS.append('validation') - -ROOT_URLCONF = 'validation.api.urls' diff -Nru django-tastypie-0.12.0/tests/slashless/api/resources.py django-tastypie-0.13.3/tests/slashless/api/resources.py --- django-tastypie-0.12.0/tests/slashless/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/slashless/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -from django.contrib.auth.models import User -from tastypie import fields -from tastypie.resources import ModelResource -from tastypie.authorization import Authorization -from basic.models import Note - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - authorization = Authorization() - - -class NoteResource(ModelResource): - user = fields.ForeignKey(UserResource, 'user') - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - authorization = Authorization() diff -Nru django-tastypie-0.12.0/tests/slashless/api/urls.py django-tastypie-0.13.3/tests/slashless/api/urls.py --- django-tastypie-0.12.0/tests/slashless/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/slashless/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -try: - from django.conf.urls import patterns, include, url -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include, url -from tastypie.api import Api -from slashless.api.resources import NoteResource, UserResource - -api = Api(api_name='v1') -api.register(NoteResource(), canonical=True) -api.register(UserResource(), canonical=True) - -urlpatterns = patterns('', - url(r'^api/', include(api.urls)), -) diff -Nru django-tastypie-0.12.0/tests/slashless/fixtures/test_data.json django-tastypie-0.13.3/tests/slashless/fixtures/test_data.json --- django-tastypie-0.12.0/tests/slashless/fixtures/test_data.json 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/slashless/fixtures/test_data.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -[ - { - "fields": { - "username": "johndoe", - "email": "john@doe.com", - "password": "this_is_not_a_valid_password_string" - }, - "model": "auth.user", - "pk": 1 - }, - - { - "fields": { - "user": 1, - "title": "First Post!", - "slug": "first-post", - "content": "This is my very first post using my shiny new API. Pretty sweet, huh?", - "is_active": true, - "created": "2010-03-30 20:05:00", - "updated": "2010-03-30 20:05:00" - }, - "model": "basic.note", - "pk": 1 - }, - { - "fields": { - "user": 1, - "title": "Another Post", - "slug": "another-post", - "content": "The dog ate my cat today. He looks seriously uncomfortable.", - "is_active": true, - "created": "2010-03-31 20:05:00", - "updated": "2010-03-31 20:05:00" - }, - "model": "basic.note", - "pk": 2 - } -] diff -Nru django-tastypie-0.12.0/tests/slashless/tests.py django-tastypie-0.13.3/tests/slashless/tests.py --- django-tastypie-0.12.0/tests/slashless/tests.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/slashless/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -from django.conf import settings -from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import HttpRequest -import json -from testcases import TestCaseWithFixture - - -class ViewsWithoutSlashesTestCase(TestCaseWithFixture): - urls = 'slashless.api.urls' - - def setUp(self): - super(ViewsWithoutSlashesTestCase, self).setUp() - self.old_debug = settings.DEBUG - settings.DEBUG = True - - def tearDown(self): - settings.DEBUG = self.old_debug - super(ViewsWithoutSlashesTestCase, self).tearDown() - - def test_gets_without_trailing_slash(self): - resp = self.client.get('/api/v1', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['notes'], {'list_endpoint': '/api/v1/notes', 'schema': '/api/v1/notes/schema'}) - - resp = self.client.get('/api/v1/notes', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 2) - self.assertEqual(deserialized['meta']['limit'], 20) - self.assertEqual(len(deserialized['objects']), 2) - self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'First Post!', u'Another Post']) - - resp = self.client.get('/api/v1/notes/1', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized), 9) - self.assertEqual(deserialized['title'], u'First Post!') - - resp = self.client.get('/api/v1/notes/set/2;1', data={'format': 'json'}) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - obj_ids = [o["id"] for o in deserialized["objects"]] - self.assertEqual(sorted(obj_ids), [1,2]) - diff -Nru django-tastypie-0.12.0/tests/testcases.py django-tastypie-0.13.3/tests/testcases.py --- django-tastypie-0.12.0/tests/testcases.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/testcases.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -import socket -import threading - -from django.core.handlers.wsgi import WSGIHandler -from django.core.management import call_command -from django.core.servers import basehttp -from django.db import connections -from django.test.testcases import TransactionTestCase, TestCase - -class StoppableWSGIServer(basehttp.WSGIServer): - """WSGIServer with short timeout, so that server thread can stop this server.""" - - def server_bind(self): - """Sets timeout to 1 second.""" - basehttp.WSGIServer.server_bind(self) - self.socket.settimeout(1) - - def get_request(self): - """Checks for timeout when getting request.""" - try: - sock, address = self.socket.accept() - sock.settimeout(None) - return (sock, address) - except socket.timeout: - raise - -class TestServerThread(threading.Thread): - """Thread for running a http server while tests are running.""" - - def __init__(self, address, port): - self.address = address - self.port = port - self._stopevent = threading.Event() - self.started = threading.Event() - self.error = None - super(TestServerThread, self).__init__() - - def run(self): - """Sets up test server and database and loops over handling http requests.""" - try: - handler = WSGIHandler() - server_address = (self.address, self.port) - httpd = StoppableWSGIServer(server_address, basehttp.WSGIRequestHandler) - httpd.set_app(handler) - self.started.set() - except basehttp.WSGIServerException as e: - self.error = e - self.started.set() - return - - # Must do database stuff in this new thread if database in memory. - from django.conf import settings - - db = settings.DATABASES['default'] - - ENGINE = db['ENGINE'] - TEST_NAME = db.get('TEST_NAME') - - if ('sqlite3' in ENGINE or 'spatialite' in ENGINE) \ - and (not TEST_NAME or TEST_NAME == ':memory:'): - if 'spatialite' in ENGINE: - cursor = connections['default'].cursor() - - cursor.execute('SELECT InitSpatialMetaData()') - row = cursor.fetchone() - - call_command('syncdb', interactive=False, verbosity=0) - - # Import the fixture data into the test database. - if hasattr(self, 'fixtures'): - # We have to use this slightly awkward syntax due to the fact - # that we're using *args and **kwargs together. - call_command('loaddata', *self.fixtures, **{'verbosity': 0}) - - # Loop until we get a stop event. - while not self._stopevent.isSet(): - httpd.handle_request() - - def join(self, timeout=None): - """Stop the thread and wait for it to finish.""" - self._stopevent.set() - threading.Thread.join(self, timeout) - - -class TestServerTestCase(TransactionTestCase): - fixtures = ['test_data.json'] - - def start_test_server(self, address='localhost', port=8000): - """Creates a live test server object (instance of WSGIServer).""" - self.server_thread = TestServerThread(address, port) - self.server_thread.fixtures = self.fixtures - self.server_thread.start() - self.server_thread.started.wait() - if self.server_thread.error: - raise self.server_thread.error - - def stop_test_server(self): - if self.server_thread: - self.server_thread.join() - - -class TestCaseWithFixture(TestCase): - fixtures = ['test_data.json'] diff -Nru django-tastypie-0.12.0/tests/validation/api/resources.py django-tastypie-0.13.3/tests/validation/api/resources.py --- django-tastypie-0.12.0/tests/validation/api/resources.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/validation/api/resources.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -from django.contrib.auth.models import User -from tastypie import fields -from tastypie.constants import ALL -from tastypie.resources import ModelResource -from tastypie.authorization import Authorization -from basic.models import Note, AnnotatedNote, UserForm -from django import forms -from tastypie.validation import FormValidation - -# NOTES: -# model defaults don't matter since we are not rendering a form, if you want to use a default exclude the field. - - -class UserResource(ModelResource): - class Meta: - resource_name = 'users' - queryset = User.objects.all() - authorization = Authorization() - validation = FormValidation(form_class=UserForm) - -class AnnotatedNoteForm(forms.ModelForm): - - class Meta: - model = AnnotatedNote - exclude = ('note',) - -class AnnotatedNoteResource(ModelResource): - - class Meta: - resource_name = 'annotated' - queryset = AnnotatedNote.objects.all() - authorization = Authorization() - validation = FormValidation(form_class=AnnotatedNoteForm) - -class NoteForm(forms.ModelForm): - - class Meta: - model = Note - exclude = ('user', 'created', 'updated') - -class NoteResource(ModelResource): - user = fields.ForeignKey(UserResource, 'user') - annotated = fields.ForeignKey(AnnotatedNoteResource, 'annotated', related_name='note', null=True, full=True) - - class Meta: - resource_name = 'notes' - queryset = Note.objects.all() - authorization = Authorization() - validation = FormValidation(form_class=NoteForm) - filtering = { - "created": ALL - } - diff -Nru django-tastypie-0.12.0/tests/validation/api/urls.py django-tastypie-0.13.3/tests/validation/api/urls.py --- django-tastypie-0.12.0/tests/validation/api/urls.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/validation/api/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -try: - from django.conf.urls import patterns, include, url -except ImportError: # Django < 1.4 - from django.conf.urls.defaults import patterns, include, url -from tastypie.api import Api -from validation.api.resources import NoteResource, UserResource, AnnotatedNoteResource - -api = Api(api_name='v1') -api.register(NoteResource(), canonical=True) -api.register(UserResource(), canonical=True) -api.register(AnnotatedNoteResource(), canonical=True) - -urlpatterns = patterns('', - url(r'^api/', include(api.urls)), -) diff -Nru django-tastypie-0.12.0/tests/validation/tests.py django-tastypie-0.13.3/tests/validation/tests.py --- django-tastypie-0.12.0/tests/validation/tests.py 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tests/validation/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,163 +0,0 @@ -from django.conf import settings -from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import HttpRequest -import json - -from basic.models import Note -from testcases import TestCaseWithFixture - - -class FilteringErrorsTestCase(TestCaseWithFixture): - urls = 'validation.api.urls' - - def test_valid_date(self): - resp = self.client.get('/api/v1/notes/', data={ - 'format': 'json', - 'created__gte':'2010-03-31' - }) - self.assertEqual(resp.status_code, 200) - deserialized = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(deserialized['objects']), Note.objects.filter(created__gte='2010-03-31').count()) - - - def test_invalid_date(self): - resp = self.client.get('/api/v1/notes/', data={ - 'format': 'json', - 'created__gte':'foo-baz-bar' - }) - self.assertEqual(resp.status_code, 400) - -class PostNestResouceValidationTestCase(TestCaseWithFixture): - urls = 'validation.api.urls' - - def test_valid_data(self): - data = json.dumps({ - 'title' : 'Test Title', - 'slug' : 'test-title', - 'content' : 'This is the content', - 'user' : {'pk' : 1}, # loaded from fixtures - 'annotated' : {'annotations' : 'This is an annotations'}, - }) - - resp = self.client.post('/api/v1/notes/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 201) - note = json.loads(self.client.get(resp['location']).content.decode('utf-8')) - self.assertTrue(note['annotated']) - - def test_invalid_data(self): - data = json.dumps({ - 'title' : '', - 'slug' : 'test-title', - 'content' : 'This is the content', - 'user' : {'pk' : 1}, # loaded from fixtures - 'annotated' : {'annotations' : ''}, - }) - - resp = self.client.post('/api/v1/notes/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content.decode('utf-8')), { - 'notes': { - 'title': ['This field is required.'] - }, - 'annotated': { - 'annotations': ['This field is required.'] - } - }) - - -class PutDetailNestResouceValidationTestCase(TestCaseWithFixture): - urls = 'validation.api.urls' - - def test_valid_data(self): - data = json.dumps({ - 'title' : 'Test Title', - 'slug' : 'test-title', - 'content' : 'This is the content', - 'annotated' : {'annotations' : 'This is another annotations'}, - }) - - resp = self.client.put('/api/v1/notes/1/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 204) - note = json.loads(self.client.get('/api/v1/notes/1/', content_type='application/json').content.decode('utf-8')) - self.assertTrue(note['annotated']) - self.assertEqual('test-title', note['slug']) - - def test_invalid_data(self): - data = json.dumps({ - 'title' : '', - 'slug' : '', - 'content' : 'This is the content', - 'annotated' : {'annotations' : None}, - }) - - resp = self.client.put('/api/v1/notes/1/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content.decode('utf-8')), { - 'notes': { - 'slug': ['This field is required.'], - 'title': ['This field is required.'] - }, - 'annotated': { - 'annotations': ['This field is required.'] - } - }) - - -class PutListNestResouceValidationTestCase(TestCaseWithFixture): - urls = 'validation.api.urls' - - def test_valid_data(self): - data = json.dumps({'objects' : [ - { - 'id' : 1, - 'title' : 'Test Title', - 'slug' : 'test-title', - 'content' : 'This is the content', - 'annotated' : {'annotations' : 'This is another annotations'}, - 'user' : {'id' : 1} - }, - { - 'id' : 2, - 'title' : 'Test Title', - 'slug' : 'test-title', - 'content' : 'This is the content', - 'annotated' : {'annotations' : 'This is the third annotations'}, - 'user' : {'id' : 1} - } - - ]}) - - resp = self.client.put('/api/v1/notes/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 204) - note = json.loads(self.client.get('/api/v1/notes/1/', content_type='application/json').content.decode('utf-8')) - self.assertTrue(note['annotated']) - note = json.loads(self.client.get('/api/v1/notes/2/', content_type='application/json').content.decode('utf-8')) - self.assertTrue(note['annotated']) - - def test_invalid_data(self): - data = json.dumps({'objects' : [ - { - 'id' : 1, - 'title' : 'Test Title', - 'slug' : 'test-title', - 'annotated' : {'annotations' : None}, - 'user' : {'id' : 1} - }, - { - 'id' : 2, - 'title' : 'Test Title', - 'annotated' : {'annotations' : None}, - 'user' : {'id' : 1} - } - ]}) - - resp = self.client.put('/api/v1/notes/', data=data, content_type='application/json') - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content.decode('utf-8')), { - 'notes': { - 'content': ['This field is required.'] - }, - 'annotated': { - 'annotations': ['This field is required.'] - } - }) diff -Nru django-tastypie-0.12.0/TODO django-tastypie-0.13.3/TODO --- django-tastypie-0.12.0/TODO 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/TODO 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -TODO -==== - -Short Term ----------- - -Cody: -- Test support for serialization/deserialization. - -Daniel: -- Authentication - - ``OauthAuthentication`` - - ``DigestAuthentication`` -- More integration tests (intermediate, advanced, composite, non-model) - - -Long Term ---------- - -- HTML browsing diff -Nru django-tastypie-0.12.0/tox.ini django-tastypie-0.13.3/tox.ini --- django-tastypie-0.12.0/tox.ini 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/tox.ini 1970-01-01 00:00:00.000000000 +0000 @@ -1,170 +0,0 @@ -[tox] -envlist = py34-dev,py33-dev,py27-dev,py34-1.7,py33-1.7,py27-1.7,py34-1.6,py33-1.6,py27-1.6,py26-1.6,py34-1.5,py33-1.5,py27-1.5,py26-1.5,docs - -[testenv] -setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/tests -commands = - {envbindir}/django-admin.py test -p '*' core.tests --settings=settings_core - {envbindir}/django-admin.py test basic.tests --settings=settings_basic - {envbindir}/django-admin.py test related_resource.tests --settings=settings_related - {envbindir}/django-admin.py test alphanumeric.tests --settings=settings_alphanumeric - {envbindir}/django-admin.py test authorization.tests --settings=settings_authorization - {envbindir}/django-admin.py test complex.tests --settings=settings_complex - {envbindir}/django-admin.py test content_gfk.tests --settings=settings_content_gfk - {envbindir}/django-admin.py test customuser.tests --settings=settings_customuser - {envbindir}/django-admin.py test namespaced.tests --settings=settings_namespaced - {envbindir}/django-admin.py test slashless.tests --settings=settings_slashless - {envbindir}/django-admin.py test validation.tests --settings=settings_validation - -deps-py2 = - -r{toxinidir}/tests/requirements.txt - python-digest - -ehg+https://bitbucket.org/coagulant/django-oauth-plus/@django-1.7-proper#egg=django-oauth-plus - # django-oauth-plus does not support Django 1.7 yet - oauth2 - -deps-py3 = - -r{toxinidir}/tests/requirements.txt - python3-digest>=1.8b4 - -[testenv:py34-dev] -basepython = python3.4 -deps = - {[testenv]deps-py3} - https://github.com/django/django/zipball/master - -[testenv:py34-1.7] -basepython = python3.4 -deps = - {[testenv]deps-py3} - django==1.7 - -[testenv:py34-1.6] -basepython = python3.4 -deps = - {[testenv]deps-py3} - django==1.6.2 - -[testenv:py34-1.5] -basepython = python3.4 -deps = - {[testenv]deps-py3} - django==1.5.5 -commands = - {envbindir}/django-admin.py test core --settings=settings_core - {envbindir}/django-admin.py test basic --settings=settings_basic - {envbindir}/django-admin.py test related_resource --settings=settings_related - {envbindir}/django-admin.py test alphanumeric --settings=settings_alphanumeric - {envbindir}/django-admin.py test authorization --settings=settings_authorization - {envbindir}/django-admin.py test complex --settings=settings_complex - {envbindir}/django-admin.py test content_gfk --settings=settings_content_gfk - {envbindir}/django-admin.py test customuser --settings=settings_customuser - {envbindir}/django-admin.py test namespaced --settings=settings_namespaced - {envbindir}/django-admin.py test slashless --settings=settings_slashless - {envbindir}/django-admin.py test validation --settings=settings_validation - -[testenv:py33-dev] -basepython = python3.3 -deps = - {[testenv]deps-py3} - https://github.com/django/django/zipball/master - -[testenv:py33-1.7] -basepython = python3.3 -deps = - {[testenv]deps-py3} - django==1.7 - -[testenv:py33-1.6] -basepython = python3.3 -deps = - {[testenv]deps-py3} - django==1.6.2 - -[testenv:py33-1.5] -basepython = python3.3 -deps = - {[testenv]deps-py3} - django==1.5.5 -commands = - {envbindir}/django-admin.py test core --settings=settings_core - {envbindir}/django-admin.py test basic --settings=settings_basic - {envbindir}/django-admin.py test related_resource --settings=settings_related - {envbindir}/django-admin.py test alphanumeric --settings=settings_alphanumeric - {envbindir}/django-admin.py test authorization --settings=settings_authorization - {envbindir}/django-admin.py test complex --settings=settings_complex - {envbindir}/django-admin.py test content_gfk --settings=settings_content_gfk - {envbindir}/django-admin.py test customuser --settings=settings_customuser - {envbindir}/django-admin.py test namespaced --settings=settings_namespaced - {envbindir}/django-admin.py test slashless --settings=settings_slashless - {envbindir}/django-admin.py test validation --settings=settings_validation - -[testenv:py27-dev] -basepython = python2.7 -deps = - {[testenv]deps-py2} - https://github.com/django/django/zipball/master - -[testenv:py27-1.7] -basepython = python2.7 -deps = - {[testenv]deps-py2} - django==1.7 - -[testenv:py27-1.6] -basepython = python2.7 -deps = - {[testenv]deps-py2} - django==1.6.2 - -[testenv:py27-1.5] -basepython = python2.7 -deps = - {[testenv]deps-py2} - django==1.5.5 -commands = - {envbindir}/django-admin.py test core --settings=settings_core - {envbindir}/django-admin.py test basic --settings=settings_basic - {envbindir}/django-admin.py test related_resource --settings=settings_related - {envbindir}/django-admin.py test alphanumeric --settings=settings_alphanumeric - {envbindir}/django-admin.py test authorization --settings=settings_authorization - {envbindir}/django-admin.py test complex --settings=settings_complex - {envbindir}/django-admin.py test content_gfk --settings=settings_content_gfk - {envbindir}/django-admin.py test customuser --settings=settings_customuser - {envbindir}/django-admin.py test namespaced --settings=settings_namespaced - {envbindir}/django-admin.py test slashless --settings=settings_slashless - {envbindir}/django-admin.py test validation --settings=settings_validation - -[testenv:py26-1.6] -basepython = python2.6 -deps = - {[testenv]deps-py2} - django==1.6.2 - -[testenv:py26-1.5] -basepython = python2.6 -deps = - {[testenv]deps-py2} - django==1.5.5 -commands = - {envbindir}/django-admin.py test core --settings=settings_core - {envbindir}/django-admin.py test basic --settings=settings_basic - {envbindir}/django-admin.py test related_resource --settings=settings_related - {envbindir}/django-admin.py test alphanumeric --settings=settings_alphanumeric - {envbindir}/django-admin.py test authorization --settings=settings_authorization - {envbindir}/django-admin.py test complex --settings=settings_complex - {envbindir}/django-admin.py test content_gfk --settings=settings_content_gfk - {envbindir}/django-admin.py test customuser --settings=settings_customuser - {envbindir}/django-admin.py test namespaced --settings=settings_namespaced - {envbindir}/django-admin.py test slashless --settings=settings_slashless - {envbindir}/django-admin.py test validation --settings=settings_validation - -[testenv:docs] -sitepackages = True -deps = - Sphinx -whitelist_externals = sphinx-build -changedir = docs -commands = - sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html diff -Nru django-tastypie-0.12.0/.travis.yml django-tastypie-0.13.3/.travis.yml --- django-tastypie-0.12.0/.travis.yml 2014-09-12 01:17:30.000000000 +0000 +++ django-tastypie-0.13.3/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -language: python - -python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - -env: - - DJANGO_VERSION=1.5 - - DJANGO_VERSION=1.6 - - DJANGO_VERSION=1.7 - - DJANGO_VERSION=dev - -matrix: - allow_failures: - - env: DJANGO_VERSION=dev - - env: DJANGO_VERSION=1.7 - exclude: - - env: DJANGO_VERSION=dev - python: "2.6" - - env: DJANGO_VERSION=1.7 - python: "2.6" - -before_install: - - sudo apt-get install python-sphinx python3-sphinx - -# command to install dependencies -install: - - pip install tox - -# command to run tests -script: - - tox -e py${TRAVIS_PYTHON_VERSION/./}-${DJANGO_VERSION},docs