pax_global_header00006660000000000000000000000064124044446520014517gustar00rootroot0000000000000052 comment=a6e63f3ca6da184ae5c51fb605b1502797f78fa4 django-tastypie-0.12.0/000077500000000000000000000000001240444465200147015ustar00rootroot00000000000000django-tastypie-0.12.0/.gitignore000066400000000000000000000001531240444465200166700ustar00rootroot00000000000000*.pyc .DS_Store _build .*.sw[po] *.egg-info dist build MANIFEST tests/tastypie.db .buildinfo .tox env env3 django-tastypie-0.12.0/.travis.yml000066400000000000000000000011271240444465200170130ustar00rootroot00000000000000language: 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 django-tastypie-0.12.0/AUTHORS000066400000000000000000000137111240444465200157540ustar00rootroot00000000000000Primary authors: * Daniel Lindsley * Cody Soyland * Matt Croydon * Josh Bohde * Issac Kelly Contributors: * mdornseif for various patches * Jeff Triplett (jefftriplett) for various reports/patches. * Christian Klein (cklein) for the work on alphanumeric PK support. * Charles Leifer (coleifer) for a patch related to nullable OneToOneFields. * Sean Bleier (sebleier) for a patch that prevents error hiding on delete. * Chris Adams (acdha) for documentation fixes & other patches. * jonozzz for a patch regarding Paginator optimizations & a patch to multiple sort_by parameters. * David Guaraglia (dguaraglia) for the requirements file & a patch to limits. * whiteblack for a patch to Meta inheritance. * Travis Cline (traviscline) for patches regarding URLconf namespacing & pagination. * Jannis Leidel (jezdez) for patches regarding the ApiKey model, an admin inline and Django 1.4's timezone support. * Malcolm Tredinnick (malcolmt) for documentation corrections. * Vincent Driessen (nvie) for FileField related patches. * Christopher Grebs (EnTeQuAk) for various patches. * Roman Bogorodskiy (novel) for documentation fixes. * D. Hageman (dhageman) for a patch adding default values to RelatedFields. * James Hsiao (thepeopleseason) for finishing the DigestAuthentication bits. * jesperp for a patch adding TimeField. * Marcel van den Elst (MacMaru) for testing regarding ``blank=True``. * ulmus for a cookbook example. * Jason Kraus (zbyte64) for a patch related to script prefix. * Timothée Peignier (cyberdelia) for various patches. * Chris Beaven (SmileyChris) for a documentation patch. * Evan Borgstrom (fatbox) for a documentation patch. * Madis V (madisvain) for a README patch. * Ed Summers (edsu) for a setup.py patch. * Sébastien Fievet (zyegfryed) for the initial OAuth implementation. * Jacob Kaplan-Moss (jacobkm) for the PATCH patch. * jorgeecardona for a patch in renaming the ``objects`` name of the response. * vbabiy for a patch on improved use of ``bundle.request`` & related resource validation. * philipn (Philip Neustrom) for GeoDjango integration. * dgerzo (Daniel Gerzo) for GeoDjango integration, work on PATCH and related fields, improving the ``run_all_tests.sh`` script & several smaller patches.. * ipmb (Peter Baumgartner) for a patch regarding the order of object saves. * easel for a documentation patch on failing syncdbs. * gaftech (Gabriel) for a patch regarding the ``__in`` filter syntax. * Christopher Groskopf (onyxfish) for a patch regarding PATCH & ``always_return_data``. * dericcrago (Deric Crago) for documentation patches. * gwrtheyrn (Danilo Bargen) for a documentation patch. * panta (Marco Pantaleoni) for a patch to ``setup.py``. * pelme (Andreas Pelme) for a patch involving re-raising exceptions during testing. * Rue La La for funding the development & documentation on the testing bits. * Aaron Elliot Ross for a patch to invalid pagination. * Randall Degges for patch_list method documentation patch. * Alexis Svinartchouk (zvin) for patching field type fix for AutoFields. * Mike W (mik3y) for a cookbook entry on pretty-printed JSON serialization & converting 'objects' to Meta.collection_name. * Taylor Mitchell (tmitchell) for finding and patching a bug in the paginator query encoding. * D.B. Tsai (dbtsai) & DuJour for funding: * The work to make non-pk URIs work better * Ryan West (ryanisnan) for finding and patching a bug in patch_list where requests were not included in the bundle. * Julien Bouquillon (revolunet) for authentication and authorization docs fixes. * Andrey Voronov (eyvoro) for fixing a typo in the AUTHORS file. * D.B. Tsai (dbtsai) for a fix relating to ``detail_uri_kwargs``. * maraujop for a patch adding to ``X-HTTP-Method-Override`` support. * Donald Stufft (dstufft) for patching ToOneField callable attributes & for bumping the dateutil reqs. * Vladimir Volodin (vvolodin) for patching ToManyField callable attributes. * Mike Urbanski (mcu) for patching ``Paginator.get_limit``. * Jason Brownbridge (jbrownbridge) for patching multiple ``offset/limit`` params appearing in pagination URIs. * Mitar for compatability patches with django-tastypie-mongoengine * Jeremy Dunck (jdunck) for a patch adding an index to ``ApiKey``. * Wes Winham (winhamwr) for a documentation patch. * Andrew Austin (andrewaustin) for triaging, verifying and patching several tickets. * Numan Sachwani (numan) and Curtis Maloney (funkybob) for patch which allows fields to be omitted from list/detail form of resources. * Anton V. Yanchenko (simplylizz) for reporting and providing a patch for a security vulnerability involving ``MultiAuthentication`` and ``ApiKeyAuthentication`` * Cristiano Lopes (cristiano2lopes) for minor bug fix in feature that allows fields to be omitted from list/detail form of resources. * Andrew McCloud (amccloud) for a patch improving integration with error logging tools by dispatching Django's got_request_exception when appropriate * Vitaly Babiy (vbabiy) form validation, and django 1.5 support. * Nathaniel Tucker (ntucker) django 1.5 support. * Soren Hansen (sorenh) Fixing tests for django 1.3. * Matt DeBoard (mattdeboard) for a patch to optionally set ``abstract = True`` on the ApiKey model. * Paul Grau (graup) Addition of iso-8601-strict to available TASTYPIE_DATETIME_FORMATTING. * Eric Plumb (professorplumb) for fixing the nested resource example in the cookbook. * Willem Bult (willembult) for fixing a bug with updating updating foreign key resources during create. * Yuri Govorushchenko (metametadata) for enhancing documentation on how to convert list of resources into JSON within a django template. * 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. Thanks to Tav for providing validate_jsonp.py, placed in public domain. Thanks to various contributors of the Django test server patch, borrowed from http://code.djangoproject.com/ticket/2879 and placed in tests/testcases.py. django-tastypie-0.12.0/BACKWARDS-INCOMPATIBLE.txt000066400000000000000000000016771240444465200206220ustar00rootroot00000000000000Master (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-"``.django-tastypie-0.12.0/CONTRIBUTING000066400000000000000000000121361240444465200165360ustar00rootroot00000000000000============ 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. django-tastypie-0.12.0/LICENSE000066400000000000000000000027151240444465200157130ustar00rootroot00000000000000Copyright (c) 2010, Daniel Lindsley All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the tastypie nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL tastypie BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.django-tastypie-0.12.0/MANIFEST.in000066400000000000000000000002211240444465200164320ustar00rootroot00000000000000recursive-include docs * recursive-include tastypie/templates/tastypie *.html include AUTHORS include LICENSE include README.rst include VERSION django-tastypie-0.12.0/README.rst000066400000000000000000000063161240444465200163760ustar00rootroot00000000000000=============== django-tastypie =============== Creating delicious APIs for Django apps since 2010. Currently in beta (v0.11.2-dev) but being used actively in production on several sites. Requirements ============ Core ---- * Python 2.6+ or Python 3.3+ * Django 1.5+ * 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:: # 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.defaults import * from tastypie.api import Api from myapp.api import EntryResource v1_api = Api(api_name='v1') v1_api.register(EntryResource()) urlpatterns = patterns('', # The normal jazz here then... (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, better known 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). * 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 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/ 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. django-tastypie-0.12.0/TODO000066400000000000000000000004401240444465200153670ustar00rootroot00000000000000TODO ==== Short Term ---------- Cody: - Test support for serialization/deserialization. Daniel: - Authentication - ``OauthAuthentication`` - ``DigestAuthentication`` - More integration tests (intermediate, advanced, composite, non-model) Long Term --------- - HTML browsing django-tastypie-0.12.0/docs/000077500000000000000000000000001240444465200156315ustar00rootroot00000000000000django-tastypie-0.12.0/docs/Makefile000066400000000000000000000066021240444465200172750ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " texinfo to make a Texinfo source file" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tastypie.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tastypie.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished; the Texinfo files are in $(BUILDDIR)/texinfo." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) _build/epub @echo @echo "Build finished. The epub file is in _build/epub." django-tastypie-0.12.0/docs/api.rst000066400000000000000000000047671240444465200171520ustar00rootroot00000000000000.. _ref-api: === Api === In terms of a REST-style architecture, the "api" is a collection of resources. In Tastypie, the ``Api`` gathers together the ``Resources`` & provides a nice way to use them as a set. It handles many of the URLconf details for you, provides a helpful "top-level" view to show what endpoints are available & some extra URL resolution juice. Quick Start =========== A sample api definition might look something like (usually located in a URLconf):: from tastypie.api import Api from myapp.api.resources import UserResource, EntryResource v1_api = Api(api_name='v1') v1_api.register(UserResource()) v1_api.register(EntryResource()) # Standard bits... urlpatterns = patterns('', (r'^api/', include(v1_api.urls)), ) ``Api`` Methods =============== Implements a registry to tie together the various resources that make up an API. Especially useful for navigation, HATEOAS and for providing multiple versions of your API. Optionally supplying ``api_name`` allows you to name the API. Generally, this is done with version numbers (i.e. ``v1``, ``v2``, etc.) but can be named any string. ``register`` ~~~~~~~~~~~~ .. method:: Api.register(self, resource, canonical=True): Registers an instance of a ``Resource`` subclass with the API. Optionally accept a ``canonical`` argument, which indicates that the resource being registered is the canonical variant. Defaults to ``True``. ``unregister`` ~~~~~~~~~~~~~~ .. method:: Api.unregister(self, resource_name): If present, unregisters a resource from the API. ``canonical_resource_for`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: Api.canonical_resource_for(self, resource_name): Returns the canonical resource for a given ``resource_name``. ``override_urls`` ----------------- .. method:: Api.override_urls(self): Deprecated. Will be removed by v1.0.0. Please use ``Api.prepend_urls`` instead. ``prepend_urls`` ---------------- .. method:: Api.prepend_urls(self): 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``). ``urls`` ~~~~~~~~ .. method:: Api.urls(self): *Property* Provides URLconf details for the ``Api`` and all registered ``Resources`` beneath it. ``top_level`` ~~~~~~~~~~~~~ .. method:: Api.top_level(self, request, api_name=None): A view that returns a serialized list of all resources registers to the ``Api``. Useful for discovery. django-tastypie-0.12.0/docs/authentication.rst000066400000000000000000000173631240444465200214140ustar00rootroot00000000000000.. _authentication: ============== Authentication ============== Authentication is the component needed to verify who a certain user is and to validate their access to the API. Authentication answers the question "Who is this person?" This usually involves requiring credentials, such as an API key or username/password or oAuth tokens. Usage ===== Using these classes is simple. Simply provide them (or your own class) as a ``Meta`` option to the ``Resource`` in question. For example:: from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. authentication = BasicAuthentication() Authentication Options ====================== Tastypie ships with the following ``Authentication`` classes: .. warning: Tastypie, when used with ``django.contrib.auth.models.User``, will check to ensure that the ``User.is_active = True`` by default. You can disable this behavior by initializing your ``Authentication`` class with ``require_active=False``:: class UserResource(ModelResource): class Meta: # ... authentication = BasicAuthentication(require_active=False) *The behavior changed to active-by-default in v0.9.12.* ``Authentication`` ~~~~~~~~~~~~~~~~~~ The no-op authentication option, the client is always allowed through. Very useful for development and read-only APIs. ``BasicAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~ This authentication scheme uses HTTP Basic Auth to check a user's credentials. The username is their ``django.contrib.auth.models.User`` username (assuming it is present) and their password should also correspond to that entry. .. warning:: If you're using Apache & ``mod_wsgi``, you will need to enable ``WSGIPassAuthorization On``. See `this post`_ for details. .. _`this post`: http://www.nerdydork.com/basic-authentication-on-mod_wsgi.html ``ApiKeyAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~~ As an alternative to requiring sensitive data like a password, the ``ApiKeyAuthentication`` allows you to collect just username & a machine-generated api key. Tastypie ships with a special ``Model`` just for this purpose, so you'll need to ensure ``tastypie`` is in ``INSTALLED_APPS`` and that the model's database tables have been created (e.g. via ``django-admin.py syncdb``). To use this mechanism, the end user can either specify an ``Authorization`` header or pass the ``username/api_key`` combination as ``GET/POST`` parameters. Examples:: # As a header # Format is ``Authorization: ApiKey : Authorization: ApiKey daniel:204db7bcfafb2deb7506b89eb3b9b715b09905c8 # As GET params http://127.0.0.1:8000/api/v1/entries/?username=daniel&api_key=204db7bcfafb2deb7506b89eb3b9b715b09905c8 Tastypie includes a signal function you can use to auto-create ``ApiKey`` objects. Hooking it up looks like:: from django.contrib.auth.models import User from django.db import models from tastypie.models import create_api_key models.signals.post_save.connect(create_api_key, sender=User) .. warning:: If you're using Apache & ``mod_wsgi``, you will need to enable ``WSGIPassAuthorization On``, otherwise ``mod_wsgi`` strips out the ``Authorization`` header. See `this post`_ for details (even though it only mentions Basic auth). .. note:: In some cases it may be useful to make the ``ApiKey`` model an `abstract base class`_. To enable this, set ``settings.TASTYPIE_ABSTRACT_APIKEY`` to ``True``. See `the documentation for this setting`_ for more information. .. _`this post`: http://www.nerdydork.com/basic-authentication-on-mod_wsgi.html .. _`abstract base class`: https://docs.djangoproject.com/en/dev/topics/db/models/#abstract-base-classes .. _`the documentation for this setting`: http://django-tastypie.readthedocs.org/en/latest/settings.html#tastypie-abstract-apikey ``SessionAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~~~ This authentication scheme uses the built-in Django sessions to check if a user is logged. This is typically useful when used by Javascript on the same site as the API is hosted on. It requires that the user has logged in & has an active session. They also must have a valid CSRF token. ``DigestAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~~~ This authentication scheme uses HTTP Digest Auth to check a user's credentials. The username is their ``django.contrib.auth.models.User`` username (assuming it is present) and their password should be their machine-generated api key. As with ApiKeyAuthentication, ``tastypie`` should be included in ``INSTALLED_APPS``. .. warning:: If you're using Apache & ``mod_wsgi``, you will need to enable ``WSGIPassAuthorization On``. See `this post`_ for details (even though it only mentions Basic auth). .. _`this post`: http://www.nerdydork.com/basic-authentication-on-mod_wsgi.html ``OAuthAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~ Handles OAuth, which checks a user's credentials against a separate service. Currently verifies against OAuth 1.0a services. This does *NOT* provide OAuth authentication in your API, strictly consumption. .. warning:: If you're used to in-browser OAuth flow (click a "Sign In" button, get redirected, login on remote service, get redirected back), this isn't the same. Most prominently, expecting that would cause API clients to have to use tools like mechanize_ to fill in forms, which would be difficult. This authentication expects that you're already followed some sort of OAuth flow & that the credentials (Nonce/token/etc) are simply being passed to it. It merely checks that the credentials are valid. No requests are made to remote services as part of this authentication class. .. _mechanize: http://pypi.python.org/pypi/mechanize/ ``MultiAuthentication`` ~~~~~~~~~~~~~~~~~~~~~~~ This authentication class actually wraps any number of other authentication classes, attempting each until successfully authenticating. For example:: from django.contrib.auth.models import User from tastypie.authentication import BasicAuthentication, ApiKeyAuthentication, MultiAuthentication from tastypie.authorization import DjangoAuthorization from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] authentication = MultiAuthentication(BasicAuthentication(), ApiKeyAuthentication()) authorization = DjangoAuthorization() In the case of an authentication returning a customized HttpUnauthorized, MultiAuthentication defaults to the first returned one. Authentication schemes that need to control the response, such as the included BasicAuthentication and DigestAuthentication, should be placed first. Implementing Your Own Authentication/Authorization ================================================== Implementing your own ``Authentication`` classes is a simple process. ``Authentication`` has two methods to override (one of which is optional but recommended to be customized):: from tastypie.authentication import Authentication class SillyAuthentication(Authentication): def is_authenticated(self, request, **kwargs): if 'daniel' in request.user.username: return True return False # Optional but recommended def get_identifier(self, request): return request.user.username Under this scheme, only users with 'daniel' in their username will be allowed in. django-tastypie-0.12.0/docs/authorization.rst000066400000000000000000000123211240444465200212620ustar00rootroot00000000000000.. _authorization: ============= Authorization ============= Authorization is the component needed to verify what someone can do with the resources within an API. Authorization answers the question "Is permission granted for this user to take this action?" This usually involves checking permissions such as Create/Read/Update/Delete access, or putting limits on what data the user can access. Usage ===== Using these classes is simple. Simply provide them (or your own class) as a ``Meta`` option to the ``Resource`` in question. For example:: from django.contrib.auth.models import User from tastypie.authorization import DjangoAuthorization from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. authorization = DjangoAuthorization() Authorization Options ===================== Tastypie ships with the following ``Authorization`` classes: ``Authorization`` ~~~~~~~~~~~~~~~~~ The no-op authorization option, no permissions checks are performed. .. warning:: This is a potentially dangerous option, as it means *ANY* recognized user can modify *ANY* data they encounter in the API. Be careful who you trust. ``ReadOnlyAuthorization`` ~~~~~~~~~~~~~~~~~~~~~~~~~ This authorization class only permits reading data, regardless of what the ``Resource`` might think is allowed. This is the default ``Authorization`` class and the safe option. ``DjangoAuthorization`` ~~~~~~~~~~~~~~~~~~~~~~~ The most advanced form of authorization, this checks the permission a user has granted to them (via ``django.contrib.auth.models.Permission``). In conjunction with the admin, this is a very effective means of control. The ``Authorization`` API ========================= An ``Authorization``-compatible class implements the following methods: * ``read_list`` * ``read_detail`` * ``create_list`` * ``create_detail`` * ``update_list`` * ``update_detail`` * ``delete_list`` * ``delete_detail`` Each method takes two parameters, ``object_list`` & ``bundle``. ``object_list`` is the collection of objects being processed as part of the request. **FILTERING** & other restrictions to the set will have already been applied prior to this call. ``bundle`` is the populated ``Bundle`` object for the request. You'll likely frequently be accessing ``bundle.request.user``, though raw access to the data can be helpful. What you return from the method varies based on the type of method. Return Values: The List Case ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the case of the ``*_list`` methods, you'll want to filter the ``object_list`` & return only the objects the user has access to. Returning an empty list simply won't allow the action to be applied to any objects. However, they will not get a HTTP error status code. If you'd rather they received an unauthorized status code, raising ``Unauthorized`` will return a HTTP ``401``. Return Values: The Detail Case ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the case of the ``*_detail`` methods, you'll have access to the ``object_list`` (so you know if a given object fits within the overall set), **BUT** you'll want to be inspecting ``bundle.obj`` & either returning ``True`` if they should be allowed to continue or raising the ``Unauthorized`` exception if not. Raising ``Unauthorized`` will cause a HTTP ``401`` error status code in the response. Implementing Your Own Authorization =================================== Implementing your own ``Authorization`` classes is a relatively simple process. Anything that is API-compatible is acceptable, only the method names matter to Tastypie. An example implementation of a user only being able to access or modify "their" objects might look like:: from tastypie.authorization import Authorization from tastypie.exceptions import Unauthorized class UserObjectsOnlyAuthorization(Authorization): def read_list(self, object_list, bundle): # This assumes a ``QuerySet`` from ``ModelResource``. return object_list.filter(user=bundle.request.user) def read_detail(self, object_list, bundle): # Is the requested object owned by the user? return bundle.obj.user == bundle.request.user def create_list(self, object_list, bundle): # Assuming they're auto-assigned to ``user``. return object_list def create_detail(self, object_list, bundle): return bundle.obj.user == bundle.request.user def update_list(self, object_list, bundle): allowed = [] # Since they may not all be saved, iterate over them. for obj in object_list: if obj.user == bundle.request.user: allowed.append(obj) return allowed def update_detail(self, object_list, bundle): return bundle.obj.user == bundle.request.user def delete_list(self, object_list, bundle): # Sorry user, no deletes for you! raise Unauthorized("Sorry, no deletes.") def delete_detail(self, object_list, bundle): raise Unauthorized("Sorry, no deletes.") django-tastypie-0.12.0/docs/bundles.rst000066400000000000000000000027341240444465200200250ustar00rootroot00000000000000.. ref-bundle: ======= Bundles ======= What Are Bundles? ================= Bundles are a small abstraction that allow Tastypie to pass data between resources. This allows us not to depend on passing ``request`` to every single method (especially in places where this would be overkill). It also allows resources to work with data coming into the application paired together with an unsaved instance of the object in question. Finally, it aids in keeping Tastypie more thread-safe. Think of it as package of user data & an object instance (either of which are optionally present). Attributes ========== All data within a bundle can be optional, especially depending on how it's being used. If you write custom code using ``Bundle``, make sure appropriate guards are in place. ``obj`` ------- Either a Python object or ``None``. Usually a Django model, though it may/may not have been saved already. ``data`` -------- Always a plain Python dictionary of data. If not provided, it will be empty. ``request`` ----------- Either the Django ``request`` that's part of the issued request or an empty ``HttpRequest`` if it wasn't provided. ``related_obj`` --------------- Either another "parent" Python object or ``None``. Useful when handling one-to-many relations. Used in conjunction with ``related_name``. ``related_name`` ---------------- Either a Python string name of an attribute or ``None``. Useful when handling one-to-many relations. Used in conjunction with ``related_obj``. django-tastypie-0.12.0/docs/caching.rst000066400000000000000000000150151240444465200177610ustar00rootroot00000000000000.. _ref-caching: ======= Caching ======= When adding an API to your site, it's important to understand that most consumers of the API will not be people, but instead machines. This means that the traditional "fetch-read-click" cycle is no longer measured in minutes but in seconds or milliseconds. As such, caching is a very important part of the deployment of your API. Tastypie ships with two classes to make working with caching easier. These caches store at the object level, reducing access time on the database. However, it's worth noting that these do *NOT* cache serialized representations. For heavy traffic, we'd encourage the use of a caching proxy, especially Varnish_, as it shines under this kind of usage. It's far faster than Django views and already neatly handles most situations. .. _Varnish: http://www.varnish-cache.org/ The first section below demonstrates how to cache at the Django level, reducing the amount of work required to satisfy a request. In many cases your API serves web browsers or is behind by a caching proxy such as Varnish_ and it is possible to set HTTP Cache-Control headers to avoid issuing a request to your application at all. This is discussed in the :ref:`http-cache-control` section below. Usage ===== Using these classes is simple. Simply provide them (or your own class) as a ``Meta`` option to the ``Resource`` in question. For example:: from django.contrib.auth.models import User from tastypie.cache import SimpleCache from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. cache = SimpleCache(timeout=10) Caching Options =============== Tastypie ships with the following ``Cache`` classes: ``NoCache`` ~~~~~~~~~~~ The no-op cache option, this does no caching but serves as an api-compatible plug. Very useful for development. ``SimpleCache`` ~~~~~~~~~~~~~~~ This option does basic object caching, attempting to find the object in the cache & writing the object to the cache. By default, it uses the ``default`` cache backend as configured in the ``CACHES`` setting. However, an optional `cache_name` parameter can be passed to the constructor to specify a different backend. For example, if ``CACHES`` looks like:: CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'TIMEOUT': 60 }, 'resources': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'TIMEOUT': 60 } } you can configure your resource's `cache_name` property like so:: cache = SimpleCache(cache_name='resources', timeout=10) In this case, the cache used will be the one named, and the default `timeout` specified in ``CACHES['resources']`` will be overriden by the `timeout` parameter. Implementing Your Own Cache =========================== Implementing your own ``Cache`` class is as simple as subclassing ``NoCache`` and overriding the ``get`` & ``set`` methods. For example, a json-backed cache might look like:: import json from django.conf import settings from tastypie.cache import NoCache class JSONCache(NoCache): def _load(self): data_file = open(settings.TASTYPIE_JSON_CACHE, 'r') return json.load(data_file) def _save(self, data): data_file = open(settings.TASTYPIE_JSON_CACHE, 'w') return json.dump(data, data_file) def get(self, key): data = self._load() return data.get(key, None) def set(self, key, value, timeout=60): data = self._load() data[key] = value self._save(data) Note that this is *NOT* necessarily an optimal solution, but is simply demonstrating how one might go about implementing your own ``Cache``. .. _http-cache-control: HTTP Cache-Control ================== The HTTP protocol defines a ``Cache-Control`` header, which can be used to tell clients and intermediaries who is allowed to cache a response and for how long. Mark Nottingham has a `general caching introduction`_ and the `Django cache documentation`_ describes how to set caching-related headers in your code. The range of possible options is beyond the scope of this documentation, but it's important to know that, by default, Tastypie will prevent responses from being cached to ensure that clients always receive current information. .. _general caching introduction: http://www.mnot.net/cache_docs/ .. _Django cache documentation: https://docs.djangoproject.com/en/dev/topics/cache/#controlling-cache-using-other-headers To override the default ``no-cache`` response, your ``Resource`` should ensure that your ``cache`` class implements ``cache_control``. The default ``SimpleCache`` does this by default. It uses the timeout passed to the initialization as the ``max-age`` and ``s-maxage``. By default, it does not claim to know if the results should be public or privately cached but this can be changed by passing either a ``public=True`` or a ``private=True`` to the initialization of the ``SimpleClass``. Behind the scenes, the return value from the ``cache_control`` method is passed to the `cache_control`_ helper provided by Django. If you wish to add your own methods to it, you can do so by overloading the ``cache_control`` method and modifying the dictionary it returns.:: from tastypie.cache import SimpleCache class NoTransformCache(SimpleCache): def cache_control(self): control = super(NoTransformCache, self).cache_control() control.update({"no_transform": True}) return control .. _cache_control: https://docs.djangoproject.com/en/dev/topics/cache/?from=olddocs#controlling-cache-using-other-headers HTTP Vary ========= The HTTP protocol defines a ``Vary`` header, which can be used to tell clients and intermediaries on what headers your response varies. This allows clients to store a correct response for each type. By default, Tastypie will send the ``Vary: Accept`` header so that a seperate response is cached for each ``Content-Type``. However, if you wish to change this, simply pass a list to the ``varies`` kwarg of any ``Cache`` class. It is important to note that if a list is passed, Tastypie not automatically include the ``Vary: Accept`` and you should include it as a member of your list.:: class ExampleResource(Resource): class Meta: cache = SimpleCache(varies=["Accept", "Cookie"]) django-tastypie-0.12.0/docs/compatibility_notes.rst000066400000000000000000000017541240444465200224530ustar00rootroot00000000000000.. _ref-compatibility-notes: =================== Compatibility Notes =================== Tastypie does its best to be a good third-party app, trying to interoperate with the widest range of Django environments it can. However, there are times where certain things aren't possible. We'll do our best to document them here. ``ApiKey`` Database Index ------------------------- When the ``ApiKey`` model was added to Tastypie, an index was lacking on the ``key`` field. This was the case until the v0.9.12 release. The model was updated & a migration was added (``0002_add_apikey_index.py``). However, due 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 manually add an index for the the ``ApiKey.key`` field. Something to the effect of:: BEGIN; -- LOLMySQL CREATE INDEX tastypie_apikey_key_index ON tastypie_apikey (`key`); COMMIT; django-tastypie-0.12.0/docs/conf.py000066400000000000000000000150521240444465200171330ustar00rootroot00000000000000# -*- coding: utf-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. # # Note that not all possible configuration values are present in this # autogenerated file. # # 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('.')) # -- 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 = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #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' # 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' # The full version, including alpha/beta/rc tags. release = '0.12.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #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 # If true, '()' will be appended to :func: etc. cross-reference text. #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 # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #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 = [] # -- 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' # 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 = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #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 # 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 # 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, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # 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' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #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 = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Tastypiedoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (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'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # -- Options for Texinfo output ------------------------------------------------ # List of tuples (startdocname, targetname, title, author, dir_entry, # description, category, toctree_only) texinfo_documents=[( master_doc, "django-tastypie", "", "", "Tastypie", "Documentation of the Tastypie framework", "Web development", False )] django-tastypie-0.12.0/docs/content_types.rst000066400000000000000000000045751240444465200212740ustar00rootroot00000000000000.. _ref-content_types: =================================== ContentTypes and GenericForeignKeys =================================== `Content Types`_ and GenericForeignKeys are for relationships where the model on one end is not defined by the model's schema. .. _Content Types: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ If you're using GenericForeignKeys in django, you can use a GenericForeignKeyField in Tastypie. Usage ===== Here's an example model with a GenericForeignKey taken from the Django docs:: from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') def __unicode__(self): return self.tag A simple ModelResource for this model might look like this:: from tastypie.contrib.contenttypes.fields import GenericForeignKeyField from tastypie.resources import ModelResource from .models import Note, Quote, TaggedItem class QuoteResource(ModelResource): class Meta: resource_name = 'quotes' queryset = Quote.objects.all() class NoteResource(ModelResource): class Meta: resource_name = 'notes' queryset = Note.objects.all() class TaggedItemResource(ModelResource): content_object = GenericForeignKeyField({ Note: NoteResource, Quote: QuoteResource }, 'content_object') class Meta: resource_name = 'tagged_items' queryset = TaggedItem.objects.all() A ModelResource that is to be used as a relation to a GenericForeignKeyField must also be registered to the Api instance defined in your URLconf in order to provide a reverse uri for lookups. Like ToOneField, you must add your GenericForeignKey attribute to your ModelResource definition. It will not be added automatically like most other field or attribute types. When you define it, you must also define the other models and match them to their resources in a dictionary, and pass that as the first argument, the second argument is the name of the attribute on the model that holds the GenericForeignKey. django-tastypie-0.12.0/docs/contributing.rst000066400000000000000000000121361240444465200210750ustar00rootroot00000000000000============ 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. django-tastypie-0.12.0/docs/cookbook.rst000066400000000000000000000447361240444465200202070ustar00rootroot00000000000000.. _ref-cookbook: ================= Tastypie Cookbook ================= Creating a Full OAuth 2.0 API ----------------------------- 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.:: # api.py from tastypie.resources import ModelResource from tastypie.authorization import DjangoAuthorization from polls.models import Poll, Choice from tastypie import fields from authentication import OAuth20Authentication class ChoiceResource(ModelResource): class Meta: queryset = Choice.objects.all() resource_name = 'choice' 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() Adding Custom Values -------------------- 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:: class MyModelResource(Resource): class Meta: qs = MyModel.objects.all() def dehydrate(self, bundle): bundle.data['custom_field'] = "Whatever you want" return bundle 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``:: from tastypie.utils import now class MyResource(ModelResource): class Meta: queryset = MyObject.objects.all() def get_object_list(self, request): return super(MyResource, self).get_object_list(request).filter(start_date__gte=now) Using Your ``Resource`` In Regular Views ---------------------------------------- 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:: # views.py from django.shortcuts import render_to_response from myapp.api.resources import UserResource def user_detail(request, username): ur = UserResource() user = ur.obj_get(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', { # Other things here. "user_json": ur.serialize(None, ur.full_dehydrate(ur_bundle), 'application/json'), }) Example of getting a list of users:: def user_list(request): res = UserResource() request_bundle = res.build_bundle(request=request) queryset = res.obj_get_list(request_bundle) bundles = [] for obj in queryset: bundle = res.build_bundle(obj=obj, request=request) bundles.append(res.full_dehydrate(bundle, for_list=True)) list_json = res.serialize(None, bundles, "application/json") return render_to_response('myapp/user_list.html', { # Other things here. "list_json": list_json, }) Then in template you could convert JSON into JavaScript object:: Using Non-PK Data For Your URLs ------------------------------- By convention, ``ModelResource``\s usually expose the detail endpoints utilizing the primary key of the ``Model`` they represent. However, this is not a strict requirement. Each URL can take other named URLconf parameters that can be used for the lookup. For example, if you want to expose ``User`` resources by username, you can do something like the following:: # myapp/api/resources.py class UserResource(ModelResource): class Meta: queryset = User.objects.all() detail_uri_name = 'username' def prepend_urls(self): return [ url(r"^(?P%s)/(?P[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), ] 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:: # myapp/api/resources.py class EntryResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: queryset = Entry.objects.all() resource_name = 'entry' 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) # urls.py from django.conf.urls.defaults import * from myapp.api import EntryResource entry_resource = EntryResource() urlpatterns = patterns('', # The normal jazz here, then... (r'^api/(?P\w+)/', include(entry_resource.urls)), ) Nested Resources ---------------- 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:: class ParentResource(ModelResource): children = fields.ToManyField(ChildResource, 'children') def prepend_urls(self): return [ url(r"^(?P%s)/(?P\w[\w/-]*)/children%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_children'), name="api_get_children"), ] def get_children(self, request, **kwargs): try: bundle = self.build_bundle(data={'pk': kwargs['pk']}, request=request) obj = self.cached_obj_get(bundle=bundle, **self.remove_api_resource_names(kwargs)) except ObjectDoesNotExist: return HttpGone() except MultipleObjectsReturned: return HttpMultipleChoices("More than one resource is found at this URI.") child_resource = ChildResource() return child_resource.get_detail(request, parent_id=obj.pk) Adding Search Functionality --------------------------- Another common request is being able to integrate search functionality. This approach uses Haystack_, though you could hook it up to any search technology. 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.core.paginator import Paginator, InvalidPage from django.http import Http404 from haystack.query import SearchQuerySet from tastypie.resources import ModelResource from tastypie.utils import trailing_slash from notes.models import Note class NoteResource(ModelResource): class Meta: queryset = Note.objects.all() resource_name = 'notes' def prepend_urls(self): return [ url(r"^(?P%s)/search%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_search'), name="api_get_search"), ] def get_search(self, request, **kwargs): self.method_check(request, allowed=['get']) self.is_authenticated(request) self.throttle_check(request) # Do the query. sqs = SearchQuerySet().models(Note).load_all().auto_query(request.GET.get('q', '')) paginator = Paginator(sqs, 20) try: page = paginator.page(int(request.GET.get('page', 1))) except InvalidPage: raise Http404("Sorry, no results on that page.") objects = [] for result in page.object_list: bundle = self.build_bundle(obj=result.object, request=request) bundle = self.full_dehydrate(bundle) objects.append(bundle) object_list = { 'objects': objects, } self.log_throttled_access(request) return self.create_response(request, object_list) .. _Haystack: http://haystacksearch.org/ Creating per-user resources --------------------------- One might want to create an API which will require every user to authenticate and every user will be working only with objects associated with them. Let's see 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. For creating we'd have to wrap ``obj_create`` method of ``ModelResource``. Then the resulting code will look something like:: # myapp/api/resources.py class EnvironmentResource(ModelResource): class Meta: queryset = Environment.objects.all() resource_name = 'environment' 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) def apply_authorization_limits(self, request, object_list): return object_list.filter(user=request.user) camelCase JSON Serialization ---------------------------- 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:: from tastypie.serializers import Serializer import re import json class CamelCaseJSONSerializer(Serializer): formats = ['json'] content_types = { 'json': 'application/json', } def to_json(self, data, options=None): # Changes underscore_separated names to camelCase names to go from python convention to javacsript convention data = self.to_simple(data, options) def underscoreToCamel(match): return match.group()[0] + match.group()[2].upper() def camelize(data): if isinstance(data, dict): new_dict = {} for key, value in data.items(): new_key = re.sub(r"[a-z]_[a-z]", underscoreToCamel, key) new_dict[new_key] = camelize(value) return new_dict if isinstance(data, (list, tuple)): for i in range(len(data)): data[i] = camelize(data[i]) return data return data camelized_data = camelize(data) return json.dumps(camelized_data, sort_keys=True) def from_json(self, content): # Changes camelCase names to underscore_separated names to go from javascript convention to python convention data = json.loads(content) def camelToUnderscore(match): return match.group()[0] + "_" + match.group()[1].lower() def underscorize(data): if isinstance(data, dict): new_dict = {} for key, value in data.items(): new_key = re.sub(r"[a-z][A-Z]", camelToUnderscore, key) new_dict[new_key] = underscorize(value) return new_dict if isinstance(data, (list, tuple)): for i in range(len(data)): data[i] = underscorize(data[i]) return data return data underscored_data = underscorize(data) return underscored_data 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:: import json from django.core.serializers.json import DjangoJSONEncoder from tastypie.serializers import Serializer class PrettyJSONSerializer(Serializer): json_indent = 2 def to_json(self, data, options=None): options = options or {} data = self.to_simple(data, options) return json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True, ensure_ascii=False, indent=self.json_indent) 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:: # myapp/api/resources.py # Piggy-back on internal csrf_exempt existence handling from tastypie.resources import csrf_exempt class UserResource(ModelResource): class Meta: queryset = User.objects.all() def prepend_urls(self): """ Returns a URL scheme based on the default scheme to specify the response format as a file extension, e.g. /api/v1/users.json """ return [ url(r"^(?P%s)\.(?P\w+)$" % self._meta.resource_name, self.wrap_view('dispatch_list'), name="api_dispatch_list"), url(r"^(?P%s)/schema\.(?P\w+)$" % self._meta.resource_name, self.wrap_view('get_schema'), name="api_get_schema"), url(r"^(?P%s)/set/(?P\w[\w/;-]*)\.(?P\w+)$" % self._meta.resource_name, self.wrap_view('get_multiple'), name="api_get_multiple"), url(r"^(?P%s)/(?P\w[\w/-]*)\.(?P\w+)$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), ] def determine_format(self, request): """ Used to determine the desired format from the request.format attribute. """ if (hasattr(request, 'format') and request.format in self._meta.serializer.formats): return self._meta.serializer.get_mime_for_format(request.format) return super(UserResource, self).determine_format(request) def wrap_view(self, view): @csrf_exempt def wrapper(request, *args, **kwargs): request.format = kwargs.pop('format', None) wrapped_view = super(UserResource, self).wrap_view(view) return wrapped_view(request, *args, **kwargs) return wrapper Adding to the Django Admin -------------------------- If you're using the django admin and ApiKeyAuthentication, you may want to see 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.:: from tastypie.admin import ApiKeyInline from tastypie.models import ApiAccess, ApiKey from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User admin.site.register(ApiKey) admin.site.register(ApiAccess) class UserModelAdmin(UserAdmin): inlines = UserAdmin.inlines + [ApiKeyInline] admin.site.unregister(User) admin.site.register(User,UserModelAdmin) Using ``SessionAuthentication`` ------------------------------- 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``:: from django.contrib.auth.models import User from tastypie.authentication import SessionAuthentication from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: resource_name = 'users' queryset = User.objects.all() authentication = SessionAuthentication() Then you'd build a template like:: {% csrf_token %} There are other ways to make this function, with other libraries or other techniques for supplying the token (see https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax for an alternative). This is simply a starting point for getting things working. django-tastypie-0.12.0/docs/debugging.rst000066400000000000000000000063211240444465200203200ustar00rootroot00000000000000.. ref-debugging: ================== Debugging Tastypie ================== There are some common problems people run into when using Tastypie for the first time. Some of the common problems and things to try appear below. "I'm getting XML output in my browser but I want JSON output!" ============================================================== This is actually not a bug and JSON support is present in your ``Resource``. 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 Note that ``application/xml`` comes first, which is a format that Tastypie handles by default, hence why you receive XML. If you use ``curl`` from the command line, you should receive JSON by default:: curl http://localhost:8000/api/v1/ If you want JSON in the browser, simply append ``?format=json`` to your URL. Tastypie always respects this override first, before it falls back to the ``Accept`` header. "What's the format for a POST or PUT?" ====================================== You can view full schema for your resource through :ref:`schema-inspection`. In general, Tastypie will accept resources in the same format as it gives you. This means that you can see what any POST or PUT should look like by performing a GET of that resource. Creating a duplicate of an entry, using Python and Requests_:: import requests import json response = requests.get('http://localhost:8000/api/v1/entry/1/') event = json.loads(response.content) del event['id'] # We want the server to assign a new id response = requests.post('http://localhost:8000/api/v1/entry/', data=json.dumps(event), headers={'content-type': 'application/json'}) The new event should be almost identical, with the exception of readonly fields. This method may fail if your model has a unique constraint, or otherwise fails validation. This is less likely to happen on PUT, except for application logic changes (e.g. a `last_update` field). The following two ``curl`` commands replace and entry with an copy:: curl -H 'Accept: application/json' 'http://localhost:8000/api/v1/entry/1/' | \ curl -H 'Content-Type: application/json' -X PUT --data @- "http://localhost:8000/api/v1/entry/1/" You can do this over an entire collection as well:: curl -H 'Accept: application/json' 'http://localhost:8000/api/v1/entry/?limit=0' | \ 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. django-tastypie-0.12.0/docs/fields.rst000066400000000000000000000206321240444465200176340ustar00rootroot00000000000000.. _ref-fields: =============== Resource Fields =============== When designing an API, an important component is defining the representation of the data you're presenting. Like Django models, you can control the representation of a ``Resource`` using fields. There are a variety of fields for various types of data. Quick Start =========== For the impatient:: from tastypie import fields, utils from tastypie.resources import Resource from myapp.api.resources import ProfileResource, NoteResource class PersonResource(Resource): name = fields.CharField(attribute='name') age = fields.IntegerField(attribute='years_old', null=True) created = fields.DateTimeField(readonly=True, help_text='When the person was created', default=utils.now) is_active = fields.BooleanField(default=True) profile = fields.ToOneField(ProfileResource, 'profile') notes = fields.ToManyField(NoteResource, 'notes', full=True) Standard Data Fields ==================== All standard data fields have a common base class ``ApiField`` which handles the basic implementation details. .. note:: You should not use the ``ApiField`` class directly. Please use one of the subclasses that is more correct for your data. Common Field Options -------------------- All ``ApiField`` objects accept the following options. ``attribute`` ~~~~~~~~~~~~~ .. attribute:: ApiField.attribute A string naming an instance attribute of the object wrapped by the Resource. The attribute will be accessed during the ``dehydrate`` or or written during the ``hydrate``. Defaults to ``None``, meaning data will be manually accessed. ``default`` ~~~~~~~~~~~ .. attribute:: ApiField.default Provides default data when the object being ``dehydrated``/``hydrated`` has no data on the field. Defaults to ``tastypie.fields.NOT_PROVIDED``. ``null`` ~~~~~~~~ .. attribute:: ApiField.null Indicates whether or not a ``None`` is allowable data on the field. Defaults to ``False``. ``blank`` ~~~~~~~~~ .. attribute:: ApiField.blank Indicates whether or not data may be omitted on the field. Defaults to ``False``. This is useful for allowing the user to omit data that you can populate based on the request, such as the ``user`` or ``site`` to associate a record with. ``readonly`` ~~~~~~~~~~~~ .. attribute:: ApiField.readonly Indicates whether the field is used during the ``hydrate`` or not. Defaults to ``False``. ``unique`` ~~~~~~~~~~ .. attribute:: ApiField.unique Indicates whether the field is a unique identifier for the object. ``help_text`` ~~~~~~~~~~~~~ .. attribute:: ApiField.help_text A human-readable description of the field exposed at the schema level. Defaults to the per-Field definition. ``use_in`` ~~~~~~~~~~ .. attribute:: ApiField.use_in Optionally omit this field in list or detail views. It can be either 'all', 'list', or 'detail' or a callable which accepts a bundle and returns a boolean value. Field Types ----------- .. module:: tastypie.fields ``BooleanField`` ---------------- A boolean field. Covers both ``models.BooleanField`` and ``models.NullBooleanField``. ``CharField`` ------------- A text field of arbitrary length. Covers both ``models.CharField`` and ``models.TextField``. ``DateField`` ------------- A date field. ``DateTimeField`` ----------------- A datetime field. ``DecimalField`` ---------------- A decimal field. ``DictField`` ------------- A dictionary field. ``FileField`` ------------- A file-related field. Covers both ``models.FileField`` and ``models.ImageField``. ``FloatField`` -------------- A floating point field. ``IntegerField`` ---------------- An integer field. Covers ``models.IntegerField``, ``models.PositiveIntegerField``, ``models.PositiveSmallIntegerField`` and ``models.SmallIntegerField``. ``ListField`` ------------- A list field. ``TimeField`` ------------- A time field. Relationship Fields =================== Provides access to data that is related within the database. The ``RelatedField`` base class is not intended for direct use but provides functionality that ``ToOneField`` and ``ToManyField`` build upon. The contents of this field actually point to another ``Resource``, rather than the related object. This allows the field to represent its data in different ways. The abstractions based around this are "leaky" in that, unlike the other fields provided by ``tastypie``, these fields don't handle arbitrary objects very well. The subclasses use Django's ORM layer to make things go, though there is no ORM-specific code at this level. Common Field Options -------------------- In addition to the common attributes for all `ApiField`, relationship fields accept the following. ``to`` ~~~~~~ .. attribute:: RelatedField.to The ``to`` argument should point to a ``Resource`` class, NOT to a ``Model``. Required. ``full`` ~~~~~~~~ .. attribute:: RelatedField.full Indicates how the related ``Resource`` will appear post-``dehydrate``. If ``False``, the related ``Resource`` will appear as a URL to the endpoint of that resource. If ``True``, the result of the sub-resource's ``dehydrate`` will be included in full. You can further control post-``dehydrate`` behaviour when requesting a resource or a list of resources by setting ``full_list`` and ``full_detail``. ``full_list`` ~~~~~~~~~~~~~ .. attribute:: RelatedField.full_list Indicates how the related ``Resource`` will appear post-``dehydrate`` when requesting a list of resources. The value is one of ``True``, ``False`` or a callable that accepts a bundle and returns ``True`` or ``False``. If ``False``, the related ``Resource`` will appear as a URL to the endpoint of that resource if accessing a list of resources. If ``True`` and ``full`` is also ``True``, the result of thesub-resource's ``dehydrate`` will be included in full. Default is ``True`` ``full_detail`` ~~~~~~~~~~~~~~~ .. attribute:: RelatedField.full_detail Indicates how the related ``Resource`` will appear post-``dehydrate`` when requesting a single resource. The value is one of ``True``, ``False`` or a callable that accepts a bundle and returns ``True`` or ``False``. If ``False``, the related ``Resource`` will appear as a URL to the endpoint of that resource if accessing a specific resources. If ``True`` and ``full`` is also ``True``, the result of thesub-resource's ``dehydrate`` will be included in full. Default is ``True`` ``related_name`` ~~~~~~~~~~~~~~~~ .. attribute:: RelatedField.related_name Used to help automatically populate reverse relations when creating data. Defaults to ``None``. In order for this option to work correctly, there must be a field on the other ``Resource`` with this as an ``attribute/instance_name``. Usually this just means adding a reflecting ``ToOneField`` pointing back. Example:: class EntryResource(ModelResource): authors = fields.ToManyField('path.to.api.resources.AuthorResource', 'author_set', related_name='entry') class Meta: queryset = Entry.objects.all() resource_name = 'entry' class AuthorResource(ModelResource): entry = fields.ToOneField(EntryResource, 'entry') class Meta: queryset = Author.objects.all() resource_name = 'author' Field Types ----------- ``ToOneField`` ~~~~~~~~~~~~~~ Provides access to related data via foreign key. This subclass requires Django's ORM layer to work properly. ``OneToOneField`` ~~~~~~~~~~~~~~~~~ An alias to ``ToOneField`` for those who prefer to mirror ``django.db.models``. ``ForeignKey`` ~~~~~~~~~~~~~~ An alias to ``ToOneField`` for those who prefer to mirror ``django.db.models``. ``ToManyField`` ~~~~~~~~~~~~~~~ Provides access to related data via a join table. This subclass requires Django's ORM layer to work properly. This field also has special behavior when dealing with ``attribute`` in that it can take a callable. For instance, if you need to filter the reverse relation, you can do something like:: subjects = fields.ToManyField(SubjectResource, attribute=lambda bundle: Subject.objects.filter(notes=bundle.obj, name__startswith='Personal')) Note that the ``hydrate`` portions of this field are quite different than any other field. ``hydrate_m2m`` actually handles the data and relations. This is due to the way Django implements M2M relationships. ``ManyToManyField`` ~~~~~~~~~~~~~~~~~~~ An alias to ``ToManyField`` for those who prefer to mirror ``django.db.models``. ``OneToManyField`` ~~~~~~~~~~~~~~~~~~ An alias to ``ToManyField`` for those who prefer to mirror ``django.db.models``. django-tastypie-0.12.0/docs/geodjango.rst000066400000000000000000000062741240444465200203310ustar00rootroot00000000000000.. _ref-geodjango: ========= GeoDjango ========= 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. Usage ===== Here's an example geographic model for leaving notes in polygonal regions:: from django.contrib.gis import models class GeoNote(models.Model): content = models.TextField() polys = models.MultiPolygonField(null=True, blank=True) objects = models.GeoManager() To define a resource that takes advantage of the geospatial features, we use ``tastypie.contrib.gis.resources.ModelResource``:: from tastypie.contrib.gis.resources import ModelResource class GeoNoteResource(ModelResource): class Meta: resource_name = 'geonotes' queryset = GeoNote.objects.all() filtering = { 'polys': ALL, } Now when we do a ``GET`` on our GeoNoteResource we get back GeoJSON in our response:: { "content": "My note content", "id": "1", "polys": { "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] ]]], "type": "MultiPolygon" }, "resource_uri": "/api/v1/geonotes/1/" } When updating or creating new resources, simply provide GeoJSON or the GeoJSON analog for your perferred format. Filtering --------- 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 `_:: /api/v1/geonotes/?polys__contains={"type": "Point", "coordinates": [-122.475233, 37.768617]} Returns:: { "meta": { "limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 1}, "objects": [ { "content": "My note content", "id": "1", "polys": { "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] ]]], "type": "MultiPolygon" }, "resource_uri": "/api/geonotes/1/" } ] } We get back the ``GeoNote`` resource defining Golden Gate Park. Awesome! django-tastypie-0.12.0/docs/index.rst000066400000000000000000000106241240444465200174750ustar00rootroot00000000000000Welcome to Tastypie! ==================== Tastypie is a webservice API framework for Django. It provides a convenient, yet powerful and highly customizable, abstraction for creating REST-style interfaces. .. toctree:: :maxdepth: 1 tutorial interacting settings non_orm_data_sources tools testing compatibility_notes python3 resources bundles api fields caching validation authentication authorization serialization throttling paginator geodjango content_types cookbook debugging who_uses contributing .. toctree:: :maxdepth: 2 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 =========== 1. Add ``tastypie`` to ``INSTALLED_APPS``. 2. Create an ``api`` directory in your app with a bare ``__init__.py``. 3. Create an ``/api/resources.py`` file and place the following in it:: from tastypie.resources import ModelResource from my_app.models import MyModel class MyModelResource(ModelResource): class Meta: queryset = MyModel.objects.all() allowed_methods = ['get'] 4. In your root URLconf, add the following code (around where the admin code might be):: from tastypie.api import Api from my_app.api.resources import MyModelResource v1_api = Api(api_name='v1') v1_api.register(MyModelResource()) urlpatterns = patterns('', # ...more URLconf bits here... # Then add: (r'^api/', include(v1_api.urls)), ) 5. Hit http://localhost:8000/api/v1/?format=json in your browser! Requirements ============ Tastypie requires the following modules. If you use Pip_, you can install the necessary bits via the included ``requirements.txt``: Required -------- * Python 2.6+ or Python 3.3+ * Django 1.5+ * dateutil (http://labix.org/python-dateutil) >= 2.1 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/ Why Tastypie? ============= There are other, better known 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). * 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 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/ 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 $ cd django-tastypie $ virtualenv env $ . env/bin/activate $ ./env/bin/pip install -U -r requirements.txt Then running the tests is as simple as:: # From the same directory as above: $ ./env/bin/pip install -U -r tests/requirements.txt $ ./env/bin/pip install tox $ tox Tastypie is maintained with all tests passing at all times for released dependencies. (At times tests may fail with development versions of Django. These will be noted as allowed failures in the ``.travis.yml`` file.) If you find a failure, please `report it`_ along with the versions of the installed software. .. _`report it`: https://github.com/toastdriven/django-tastypie/issues django-tastypie-0.12.0/docs/interacting.rst000066400000000000000000000553321240444465200207020ustar00rootroot00000000000000.. _ref-interacting: ======================== Interacting With The API ======================== Now that you've got a shiny new REST-style API in place, let's demonstrate how to interact with it. We'll assume that you have cURL_ installed on your system (generally available on most modern Mac & Linux machines), but any tool that allows you to control headers & bodies on requests will do. .. _cURL: http://curl.haxx.se/ We'll assume that we're interacting with the following Tastypie code:: # myapp/api/resources.py from django.contrib.auth.models import User from tastypie.authorization import Authorization from tastypie import fields from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS from myapp.models import Entry class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'user' excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser'] filtering = { 'username': ALL, } class EntryResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: queryset = Entry.objects.all() resource_name = 'entry' authorization = Authorization() filtering = { 'user': ALL_WITH_RELATIONS, 'pub_date': ['exact', 'lt', 'lte', 'gte', 'gt'], } # urls.py from django.conf.urls.defaults import * from tastypie.api import Api from myapp.api.resources import EntryResource, UserResource v1_api = Api(api_name='v1') v1_api.register(UserResource()) v1_api.register(EntryResource()) urlpatterns = patterns('', # The normal jazz here... (r'^blog/', include('myapp.urls')), (r'^api/', include(v1_api.urls)), ) Let's fire up a shell & start exploring the API! Front Matter ============ Tastypie tries to treat all clients & all serialization types as equally as possible. It also tries to be a good 'Net citizen & respects the HTTP method used as well as the ``Accepts`` headers sent. Between these two, you control all interactions with Tastypie through relatively few endpoints. .. warning:: Should you try these URLs in your browser, be warned you **WILL** need to append ``?format=json`` (or ``xml`` or ``yaml``) to the URL. Your browser requests ``application/xml`` before ``application/json``, so you'll always get back XML if you don't specify it. That's also why it's recommended that you explore via curl, because you avoid your browser's opinionated requests & get something closer to what any programmatic clients will get. Fetching Data ============= Since reading data out of an API is a very common activity (and the easiest type of request to make), we'll start there. Tastypie tries to expose various parts of the API & interlink things within the API (HATEOAS). Api-Wide -------- We'll start at the highest level:: curl http://localhost:8000/api/v1/ You'll get back something like:: { "entry": { "list_endpoint": "/api/v1/entry/", "schema": "/api/v1/entry/schema/" }, "user": { "list_endpoint": "/api/v1/user/", "schema": "/api/v1/user/schema/" } } This lists out all the different ``Resource`` classes you registered in your URLconf with the API. Each one is listed by the ``resource_name`` you gave it and provides the ``list_endpoint`` & the ``schema`` for the resource. Note that these links try to direct you to other parts of the API, to make exploration/discovery easier. We'll use these URLs in the next several sections. To demonstrate another format, you could run the following to get the XML variant of the same information:: curl -H "Accept: application/xml" http://localhost:8000/api/v1/ To which you'd receive:: /api/v1/entry/ /api/v1/entry/schema/ /api/v1/user/ /api/v1/user/schema/ We'll stick to JSON for the rest of this document, but using XML should be OK to do at any time. .. _schema-inspection: Inspecting The Resource's Schema -------------------------------- Since the api-wide view gave us a ``schema`` URL, let's inspect that next. We'll use the ``entry`` resource. Again, a simple GET request by curl:: curl http://localhost:8000/api/v1/entry/schema/ This time, we get back a lot more data:: { "default_format": "application/json", "fields": { "body": { "help_text": "Unicode string data. Ex: \"Hello World\"", "nullable": false, "readonly": false, "type": "string" }, "id": { "help_text": "Unicode string data. Ex: \"Hello World\"", "nullable": false, "readonly": false, "type": "string" }, "pub_date": { "help_text": "A date & time as a string. Ex: \"2010-11-10T03:07:43\"", "nullable": false, "readonly": false, "type": "datetime" }, "resource_uri": { "help_text": "Unicode string data. Ex: \"Hello World\"", "nullable": false, "readonly": true, "type": "string" }, "slug": { "help_text": "Unicode string data. Ex: \"Hello World\"", "nullable": false, "readonly": false, "type": "string" }, "title": { "help_text": "Unicode string data. Ex: \"Hello World\"", "nullable": false, "readonly": false, "type": "string" }, "user": { "help_text": "A single related resource. Can be either a URI or set of nested resource data.", "nullable": false, "readonly": false, "type": "related" } }, "filtering": { "pub_date": ["exact", "lt", "lte", "gte", "gt"], "user": 2 } } This lists out the ``default_format`` this resource responds with, the ``fields`` on the resource & the ``filtering`` options available. This information can be used to prepare the other aspects of the code for the data it can obtain & ways to filter the resources. Getting A Collection Of Resources --------------------------------- Let's get down to fetching live data. From the api-wide view, we'll hit the ``list_endpoint`` for ``entry``:: curl http://localhost:8000/api/v1/entry/ We get back data that looks like:: { "meta": { "limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 3 }, "objects": [{ "body": "Welcome to my blog!", "id": "1", "pub_date": "2011-05-20T00:46:38", "resource_uri": "/api/v1/entry/1/", "slug": "first-post", "title": "First Post", "user": "/api/v1/user/1/" }, { "body": "Well, it's been awhile and I still haven't updated. ", "id": "2", "pub_date": "2011-05-21T00:46:58", "resource_uri": "/api/v1/entry/2/", "slug": "second-post", "title": "Second Post", "user": "/api/v1/user/1/" }, { "body": "I'm really excited to get started with this new blog. It's gonna be great!", "id": "3", "pub_date": "2011-05-20T00:47:30", "resource_uri": "/api/v1/entry/3/", "slug": "my-blog", "title": "My Blog", "user": "/api/v1/user/2/" }] } Some things to note: * By default, you get a paginated set of objects (20 per page is the default). * In the ``meta``, you get a ``previous`` & ``next``. If available, these are URIs to the previous & next pages. * You get a list of resources/objects under the ``objects`` key. * Each resources/object has a ``resource_uri`` field that points to the detail view for that object. * The foreign key to ``User`` is represented as a URI by default. If you're looking for the full ``UserResource`` to be embedded in this view, you'll need to add ``full=True`` to the ``fields.ToOneField``. If you want to skip paginating, simply run:: curl http://localhost:8000/api/v1/entry/?limit=0 Be warned this will return all objects, so it may be a CPU/IO-heavy operation on large datasets. Let's try filtering on the resource. Since we know we can filter on the ``user``, we'll fetch all posts by the ``daniel`` user with:: curl http://localhost:8000/api/v1/entry/?user__username=daniel We get back what we asked for:: { "meta": { "limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 2 }, "objects": [{ "body": "Welcome to my blog!", "id": "1", "pub_date": "2011-05-20T00:46:38", "resource_uri": "/api/v1/entry/1/", "slug": "first-post", "title": "First Post", "user": "/api/v1/user/1/" }, { "body": "Well, it's been awhile and I still haven't updated. ", "id": "2", "pub_date": "2011-05-21T00:46:58", "resource_uri": "/api/v1/entry/2/", "slug": "second-post", "title": "Second Post", "user": "/api/v1/user/1/" }] } Where there were three posts before, now there are only two. Getting A Detail Resource ------------------------- Since each resource/object in the list view had a ``resource_uri``, let's explore what's there:: curl http://localhost:8000/api/v1/entry/1/ We get back a similar set of data that we received from the list view:: { "body": "Welcome to my blog!", "id": "1", "pub_date": "2011-05-20T00:46:38", "resource_uri": "/api/v1/entry/1/", "slug": "first-post", "title": "First Post", "user": "/api/v1/user/1/" } Where this proves useful (for example) is present in the data we got back. We know the URI of the ``User`` associated with this blog entry. Let's run:: curl http://localhost:8000/api/v1/user/1/ Without ever seeing any aspect of the ``UserResource`` & just following the URI given, we get back:: { "date_joined": "2011-05-20T00:42:14.990617", "first_name": "", "id": "1", "last_login": "2011-05-20T00:44:57.510066", "last_name": "", "resource_uri": "/api/v1/user/1/", "username": "daniel" } You can do a similar fetch using the following Javascript/jQuery (though be wary of same-domain policy):: $.ajax({ url: 'http://localhost:8000/api/v1/user/1/', type: 'GET', accepts: 'application/json', dataType: 'json' }) Selecting A Subset Of Resources ------------------------------- Sometimes you may want back more than one record, but not an entire list view nor do you want to do multiple requests. Tastypie includes a "set" view, which lets you cherry-pick the objects you want. For example, if we just want the first & third ``Entry`` resources, we'd run:: curl "http://localhost:8000/api/v1/entry/set/1;3/" .. note:: Quotes are needed in this case because of the semicolon delimiter between primary keys. Without the quotes, bash tries to split it into two statements. No extraordinary quoting will be necessary in your application (unless your API client is written in bash :D). And we get back just those two objects:: { "objects": [{ "body": "Welcome to my blog!", "id": "1", "pub_date": "2011-05-20T00:46:38", "resource_uri": "/api/v1/entry/1/", "slug": "first-post", "title": "First Post", "user": "/api/v1/user/1/" }, { "body": "I'm really excited to get started with this new blog. It's gonna be great!", "id": "3", "pub_date": "2011-05-20T00:47:30", "resource_uri": "/api/v1/entry/3/", "slug": "my-blog", "title": "My Blog", "user": "/api/v1/user/2/" }] } Note that, like the list view, you get back a list of ``objects``. Unlike the list view, there is **NO** pagination applied to these objects. You asked for them, you're going to get them all. Sending Data ============ Tastypie also gives you full write capabilities in the API. Since the ``EntryResource`` has the no-limits ``Authentication`` & ``Authorization`` on it, we can freely write data. .. warning:: Note that this is a huge security hole as well. Don't put unauthorized write-enabled resources on the Internet, because someone will trash your data. This is why ``ReadOnlyAuthorization`` is the default in Tastypie & why you must override to provide more access. The good news is that there are no new URLs to learn. The "list" & "detail" URLs we've been using to fetch data *ALSO* support the ``POST``/``PUT``/``DELETE`` HTTP methods. Creating A New Resource (POST) ------------------------------ Let's add a new entry. To create new data, we'll switch from ``GET`` requests to the familiar ``POST`` request. .. note:: Tastypie encourages "round-trippable" data, which means the data you can GET should be able to be POST/PUT'd back to recreate the same object. If you're ever in question about what you should send, do a GET on another object & see what Tastypie thinks it should look like. To create new resources/objects, you will ``POST`` to the list endpoint of a resource. Trying to ``POST`` to a detail endpoint has a different meaning in the REST mindset (meaning to add a resource as a child of a resource of the same type). As with all Tastypie requests, the headers we request are important. Since we've been using primarily JSON throughout, let's send a new entry in JSON format:: curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post", "user": "/api/v1/user/1/"}' http://localhost:8000/api/v1/entry/ The ``Content-Type`` header here informs Tastypie that we're sending it JSON. We send the data as a JSON-serialized body (**NOT** as form-data in the form of URL parameters). What we get back is the following response:: HTTP/1.0 201 CREATED Date: Fri, 20 May 2011 06:48:36 GMT Server: WSGIServer/0.1 Python/2.7 Content-Type: text/html; charset=utf-8 Location: http://localhost:8000/api/v1/entry/4/ You'll also note that we get a correct HTTP status code back (201) & a ``Location`` header, which gives us the URI to our newly created resource. Passing ``--dump-header -`` is important, because it gives you all the headers as well as the status code. When things go wrong, this will be useful information to help with debugging. For instance, if we send a request without a ``user``:: curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post"}' http://localhost:8000/api/v1/entry/ We get back:: HTTP/1.0 400 BAD REQUEST Date: Fri, 20 May 2011 06:53:02 GMT Server: WSGIServer/0.1 Python/2.7 Content-Type: text/html; charset=utf-8 The 'user' field has no data and doesn't allow a default or null value. You can do a similar POST using the following Javascript/jQuery (though be wary of same-domain policy):: # This may require the ``json2.js`` library for older browsers. var data = JSON.stringify({ "body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post" }); $.ajax({ url: 'http://localhost:8000/api/v1/entry/', type: 'POST', contentType: 'application/json', data: data, dataType: 'json', processData: false }) Updating An Existing Resource (PUT) ----------------------------------- You might have noticed that we made some typos when we submitted the POST request. We can fix this using a ``PUT`` request to the detail endpoint (modify this instance of a resource).:: curl --dump-header - -H "Content-Type: application/json" -X PUT --data '{"body": "This will probably be my last post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post", "user": "/api/v1/user/1/"}' http://localhost:8000/api/v1/entry/4/ After fixing up the ``body``, we get back:: HTTP/1.0 204 NO CONTENT Date: Fri, 20 May 2011 07:13:21 GMT Server: WSGIServer/0.1 Python/2.7 Content-Length: 0 Content-Type: text/html; charset=utf-8 We get a 204 status code, meaning our update was successful. We don't get a ``Location`` header back because we did the ``PUT`` on a detail URL, which presumably did not change. .. note:: 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) ----------------------------------------------- In some cases, you may not want to send the entire resource when updating. To update just a subset of the fields, we can send a ``PATCH`` request to the detail endpoint.:: curl --dump-header - -H "Content-Type: application/json" -X PATCH --data '{"body": "This actually is my last post."}' http://localhost:8000/api/v1/entry/4/ To which we should get back:: HTTP/1.0 202 ACCEPTED Date: Fri, 20 May 2011 07:13:21 GMT Server: WSGIServer/0.1 Python/2.7 Content-Length: 0 Content-Type: text/html; charset=utf-8 Updating A Whole Collection Of Resources (PUT) ---------------------------------------------- You can also, in rare circumstances, update an entire collection of objects. By sending a ``PUT`` request to the list view of a resource, you can replace the entire collection. .. warning:: This deletes all of the objects first, then creates the objects afresh. This is done because determining which objects are the same is actually difficult to get correct in the general case for all people. Send a request like:: curl --dump-header - -H "Content-Type: application/json" -X PUT --data '{"objects": [{"body": "Welcome to my blog!","id": "1","pub_date": "2011-05-20T00:46:38","resource_uri": "/api/v1/entry/1/","slug": "first-post","title": "First Post","user": "/api/v1/user/1/"},{"body": "I'm really excited to get started with this new blog. It's gonna be great!","id": "3","pub_date": "2011-05-20T00:47:30","resource_uri": "/api/v1/entry/3/","slug": "my-blog","title": "My Blog","user": "/api/v1/user/2/"}]}' http://localhost:8000/api/v1/entry/ And you'll get back a response like:: HTTP/1.0 204 NO CONTENT Date: Fri, 20 May 2011 07:13:21 GMT Server: WSGIServer/0.1 Python/2.7 Content-Length: 0 Content-Type: text/html; charset=utf-8 Deleting Data ============= No CRUD setup would be complete without the ability to delete resources/objects. Deleting also requires significantly less complicated requests than ``POST``/``PUT``. Deleting A Single Resource -------------------------- We've decided that we don't like the entry we added & edited earlier. Let's delete it (but leave the other objects alone):: curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/4/ Once again, we get back the "Accepted" response of a 204:: HTTP/1.0 204 NO CONTENT Date: Fri, 20 May 2011 07:28:01 GMT Server: WSGIServer/0.1 Python/2.7 Content-Length: 0 Content-Type: text/html; charset=utf-8 If we request that resource, we get a 410 to show it's no longer there:: curl --dump-header - http://localhost:8000/api/v1/entry/4/ HTTP/1.0 410 GONE Date: Fri, 20 May 2011 07:29:02 GMT Server: WSGIServer/0.1 Python/2.7 Content-Type: text/html; charset=utf-8 Additionally, if we try to run the ``DELETE`` again (using the same original command), we get the "Gone" response again:: HTTP/1.0 410 GONE Date: Fri, 20 May 2011 07:30:00 GMT Server: WSGIServer/0.1 Python/2.7 Content-Type: text/html; charset=utf-8 Deleting A Whole Collection Of Resources ---------------------------------------- Finally, it's possible to remove an entire collection of resources. This is as destructive as it sounds. Once again, we use the ``DELETE`` method, this time on the entire list endpoint:: curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/ As a response, we get:: HTTP/1.0 204 NO CONTENT Date: Fri, 20 May 2011 07:32:51 GMT Server: WSGIServer/0.1 Python/2.7 Content-Length: 0 Content-Type: text/html; charset=utf-8 Hitting the list view:: curl --dump-header - http://localhost:8000/api/v1/entry/ Gives us a 200 but no objects:: { "meta": { "limit": 20, "next": null, "offset": 0, "previous": null, "total_count": 0 }, "objects": [] } Bulk Operations =============== As an optimization, it is possible to do many creations, updates, and deletions to a collection in a single request by sending a ``PATCH`` to the list endpoint.:: curl --dump-header - -H "Content-Type: application/json" -X PATCH --data '{"objects": [{"body": "Surprise! Another post!.", "pub_date": "2012-02-16T00:46:38", "slug": "yet-another-post", "title": "Yet Another Post"}], "deleted_objects": ["http://localhost:8000/api/v1/entry/4/"]}' http://localhost:8000/api/v1/entry/ We should get back:: HTTP/1.0 202 ACCEPTED Date: Fri, 16 Feb 2012 00:46:38 GMT Server: WSGIServer/0.1 Python/2.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. For detailed information on the format of a bulk request, see :ref:`patch-list`. You Did It! =========== That's a whirlwind tour of interacting with a Tastypie API. There's additional functionality present, such as: * ``POST``/``PUT`` the other supported content-types * More filtering/``order_by``/``limit``/``offset`` tricks * Using overridden URLconfs to support complex or non-PK lookups * Authentication But this grounds you in the basics & hopefully clarifies usage/debugging better. django-tastypie-0.12.0/docs/non_orm_data_sources.rst000066400000000000000000000131051240444465200225660ustar00rootroot00000000000000.. _ref-non_orm_data_sources: ======================================== Using Tastypie With Non-ORM Data Sources ======================================== Much of this documentation demonstrates the use of Tastypie with Django's ORM. You might think that Tastypie depended on the ORM, when in fact, it was purpose-built to handle non-ORM data. This documentation should help you get started providing APIs using other data sources. Virtually all of the code that makes Tastypie actually process requests & return data is within the ``Resource`` class. ``ModelResource`` is actually a light wrapper around ``Resource`` that provides ORM-specific access. The methods that ``ModelResource`` overrides are the same ones you'll need to override when hooking up your data source. Approach ======== When working with ``Resource``, many things are handled for you. All the authentication/authorization/caching/serialization/throttling bits should work as normal and Tastypie can support all the REST-style methods. Schemas & discovery views all work the same as well. What you don't get out of the box are the fields you're choosing to expose & the lowest level data access methods. If you want a full read-write API, there are nine methods you need to implement. They are: * ``detail_uri_kwargs`` * ``get_object_list`` * ``obj_get_list`` * ``obj_get`` * ``obj_create`` * ``obj_update`` * ``obj_delete_list`` * ``obj_delete`` * ``rollback`` If read-only is all you're exposing, you can cut that down to four methods to override. Using Riak for MessageResource ============================== As an example, we'll take integrating with Riak_ (a Dynamo-like NoSQL store) since it has both a simple API and demonstrate what hooking up to a non-relational datastore looks like:: # We need a generic object to shove data in/get data from. # Riak generally just tosses around dictionaries, so we'll lightly # wrap that. class RiakObject(object): def __init__(self, initial=None): self.__dict__['_data'] = {} if hasattr(initial, 'items'): self.__dict__['_data'] = initial def __getattr__(self, name): return self._data.get(name, None) def __setattr__(self, name, value): self.__dict__['_data'][name] = value def to_dict(self): return self._data class MessageResource(Resource): # Just like a Django ``Form`` or ``Model``, we're defining all the # fields we're going to handle with the API here. uuid = fields.CharField(attribute='uuid') user_uuid = fields.CharField(attribute='user_uuid') message = fields.CharField(attribute='message') created = fields.IntegerField(attribute='created') class Meta: resource_name = 'riak' object_class = RiakObject authorization = Authorization() # Specific to this resource, just to get the needed Riak bits. def _client(self): return riak.RiakClient() def _bucket(self): client = self._client() # Note that we're hard-coding the bucket to use. Fine for # example purposes, but you'll want to abstract this. return client.bucket('messages') # The following methods will need overriding regardless of your # data source. def detail_uri_kwargs(self, bundle_or_obj): kwargs = {} if isinstance(bundle_or_obj, Bundle): kwargs['pk'] = bundle_or_obj.obj.uuid else: kwargs['pk'] = bundle_or_obj.uuid return kwargs def get_object_list(self, request): query = self._client().add('messages') query.map("function(v) { var data = JSON.parse(v.values[0].data); return [[v.key, data]]; }") results = [] for result in query.run(): new_obj = RiakObject(initial=result[1]) new_obj.uuid = result[0] results.append(new_obj) return results def obj_get_list(self, bundle, **kwargs): # Filtering disabled for brevity... return self.get_object_list(bundle.request) def obj_get(self, bundle, **kwargs): bucket = self._bucket() message = bucket.get(kwargs['pk']) return RiakObject(initial=message.get_data()) def obj_create(self, bundle, **kwargs): bundle.obj = RiakObject(initial=kwargs) bundle = self.full_hydrate(bundle) bucket = self._bucket() new_message = bucket.new(bundle.obj.uuid, data=bundle.obj.to_dict()) new_message.store() return bundle def obj_update(self, bundle, **kwargs): return self.obj_create(bundle, **kwargs) def obj_delete_list(self, bundle, **kwargs): bucket = self._bucket() for key in bucket.get_keys(): obj = bucket.get(key) obj.delete() def obj_delete(self, bundle, **kwargs): bucket = self._bucket() obj = bucket.get(kwargs['pk']) obj.delete() def rollback(self, bundles): pass This represents a full, working, Riak-powered API endpoint. All REST-style actions (GET/POST/PUT/DELETE) all work correctly. The only shortcut taken in this example was skipping filter-abilty, as adding in the MapReduce bits would have decreased readability. 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 django-tastypie-0.12.0/docs/paginator.rst000066400000000000000000000131711240444465200203520ustar00rootroot00000000000000.. _ref-paginator: ========= Paginator ========= Similar to Django's ``Paginator``, Tastypie includes a ``Paginator`` object which limits result sets down to sane amounts for passing to the client. This is used in place of Django's ``Paginator`` due to the way pagination works. ``limit`` & ``offset`` (tastypie) are used in place of ``page`` (Django) so none of the page-related calculations are necessary. This implementation also provides additional details like the ``total_count`` of resources seen and convenience links to the ``previous``/``next`` pages of data as available. Usage ===== Using this class is simple, but slightly different than the other classes used by Tastypie. Like the others, you provide the ``Paginator`` (or your own subclass) as a ``Meta`` option to the ``Resource`` in question. **Unlike** the others, you provide the class, *NOT* an instance. For example:: from django.contrib.auth.models import User from tastypie.paginator import Paginator from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. paginator_class = Paginator .. warning:: The default paginator contains the ``total_count`` value, which shows how many objects are in the underlying object list. Obtaining this data from the database may be inefficient, especially with large datasets, and unfiltered API requests. See http://wiki.postgresql.org/wiki/Slow_Counting and http://www.wikivs.com/wiki/MySQL_vs_PostgreSQL#COUNT.28.2A.29 for reference, on why this may be a problem when using PostgreSQL and MySQL's InnoDB engine. Here's an :ref:`example solution ` to this problem. Implementing Your Own Paginator =============================== Adding other features to a paginator usually consists of overriding one of the built-in methods. For instance, adding a page number to the output might look like:: from tastypie.paginator import Paginator class PageNumberPaginator(Paginator): def page(self): output = super(PageNumberPaginator, self).page() output['page_number'] = int(self.offset / self.limit) + 1 return output Another common request is to alter the structure Tastypie uses in the list view. Here's an example of renaming:: from tastypie.paginator import Paginator class BlogEntryPaginator(Paginator): def page(self): output = super(BlogEntryPaginator, self).page() # First keep a reference. output['pagination'] = output['meta'] output['entries'] = output['objects'] # Now nuke the original keys. del output['meta'] del output['objects'] return output .. _paginator-estimated-count: ``Estimated count instead of total count`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's an example, of how you can omit ``total_count`` from the resource, and instead add an ``estimated_count`` for efficiency. See the warning above for details:: import json from django.db import connection from tastypie.paginator import Paginator class EstimatedCountPaginator(Paginator): def get_next(self, limit, offset, count): # The parent method needs an int which is higher than "limit + offset" # to return a url. Setting it to an unreasonably large value, so that # the parent method will always return the url. count = 2 ** 64 return super(NoTotalCountPaginator, self).get_next(limit, offset, count) def get_count(self): return None def get_estimated_count(self): """Get the estimated count by using the database query planner.""" # If you do not have PostgreSQL as your DB backend, alter this method # accordingly. return self._get_postgres_estimated_count() def _get_postgres_estimated_count(self): # This method only works with postgres >= 9.0. # If you need postgres vesrions less than 9.0, remove "(format json)" # below and parse the text explain output. def _get_postgres_version(): # Due to django connections being lazy, we need a cursor to make # sure the connection.connection attribute is not None. connection.cursor() return connection.connection.server_version try: if _get_postgres_version() < 90000: return except AttributeError: return cursor = connection.cursor() query = self.objects.all().query # Remove limit and offset from the query, and extract sql and params. query.low_mark = None query.high_mark = None query, params = self.objects.query.sql_with_params() # Fetch the estimated rowcount from EXPLAIN json output. query = 'explain (format json) %s' % query cursor.execute(query, params) explain = cursor.fetchone()[0] # Older psycopg2 versions do not convert json automatically. if isinstance(explain, basestring): explain = json.loads(explain) rows = explain[0]['Plan']['Plan Rows'] return rows def page(self): data = super(NoTotalCountPaginator, self).page() data['meta']['estimated_count'] = self.get_estimated_count() return data django-tastypie-0.12.0/docs/python3.rst000066400000000000000000000040221240444465200177650ustar00rootroot00000000000000.. _ref-python3: ================ Python 3 Support ================ 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 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 Incompatibilities ================= Oauth Is Unsupported -------------------- Tastypie was depending on several Oauth libraries for that authentication mechanism. Unfortunately, none of them have been ported to Python 3. They're still usable from Python 2, but that will be blocked until the underlying libraries port (or an alternative can be found). Changed Requirements -------------------- Several requirements have changed under Python 3 (mostly due to unofficial ports). They are: * python3-digest instead of python-digest * python-mimeparse instead of mimeparse Notes ===== Request/Response Bodies ----------------------- For explicitness, Django on Python 3 reads request bodies & sends response bodies as **binary** data. This requires an explicit ``.decode('utf-8')`` that was not required (but works fine) under Python 2. If you're sending or reading the bodies from Python, you'll need to keep this in mind. Testing ------- If you were testing things such as the XML/JSON generated by a given response, under Python 3.3.2+, `hash randomization`_ is in effect, which means that the ordering of dictionaries is no longer consistent, even on the same platform. To mitigate this, Tastypie now tries to ensure that serialized data is sorted alphabetically. So if you were making string assertions, you'll need to update them for the new payloads. .. _`hash randomization`: http://docs.python.org/3/whatsnew/3.3.html#builtin-functions-and-types django-tastypie-0.12.0/docs/release_notes/000077500000000000000000000000001240444465200204615ustar00rootroot00000000000000django-tastypie-0.12.0/docs/release_notes/index.rst000066400000000000000000000002231240444465200223170ustar00rootroot00000000000000Release Notes ============= .. toctree:: :maxdepth: 1 v0.12.0 v0.11.1 v0.11.0 v0.10.0 v0.9.16 v0.9.15 v0.9.14 v0.9.13 django-tastypie-0.12.0/docs/release_notes/v0.10.0.rst000066400000000000000000000005671240444465200221250ustar00rootroot00000000000000v0.10.0 ======= :date: 2013-08-03 This release adds official Python 3 support! This effort was partially funded by RevSys_ & the PSF_, so much thanks to them! .. _RevSys: http://revsys.com/ .. _PSF: http://www.python.org/psf/ Features -------- * Python 3 support! Not a lot to say, beyond there are a couple dependency changes. Please see the :ref:`ref-python3` docs! django-tastypie-0.12.0/docs/release_notes/v0.11.0.rst000066400000000000000000000035601240444465200221220ustar00rootroot00000000000000v0.11.0 ======= :date: 2013-12-03 This release is a bugfix release. This also fixes installing Tastypie on Python 3 (due to the ``python-mimeparse`` dependency change). This release is also the first to be released as a `Python wheel`_ as well. Ex:: pip install wheel pip install --use-wheel django-tastypie .. _`Python wheel`: http://wheel.readthedocs.org/ Bugfixes -------- * Now raises ``ApiFieldError`` when a datetime can't be parsed. (SHA: b59ac03) * Reduced the length of the ``ApiKey.key`` field to 128. (SHA: 1cdf2c4) * A bunch of test improvements. (SHA: 4320db4, SHA: f1bc584 & SHA: bd75a92) * Updated ``SimpleCache`` to the post-Django 1.3 world. (SHA: 4963d97) * Fixed a bug where ``tastypie.utils.timezone.now`` could throw an exception. (SHA: b78175d) * Fixed the ``python-mimeparse`` dependency in ``setup.py``. (SHA: 26dc473) * Pinned to the beta of ``python3-digest``. (SHA: cc8ef0f) * Fixed ``CacheThrottle`` to work with Django's ``DummyCache``. (SHA: 5b8a316) * Improved the error message from ``assertHttpAccepted``. (SHA: 7dbed92) * Now raises ``BadRequest`` if we fail to decode JSON. (SHA: e9048fd) * Removed a duplicate ``if`` check. (SHA: 823d007) * Added further exception checking for new versions of ``dateutil``. (SHA: 9739a35 & SHA: a8734cf) * Removed ``simplejson``. (SHA: ae58615) * Removed old Django 1.4 code. (SHA: 3c7ce47) * Documentation updates: * Added examples of how to implement throttling. (SHA: a305549) * Added docs on how to disable all list endpoints. (SHA: f4f4df2) * Added docs on GFKs. (SHA: 50189d7) * Added to the Tools docs. (SHA: bdde083 & SHA: 8c59e2c) * Added docs about running tests with ``tox``. (SHA: f0222ea) * Fixed docs on resources. (SHA: d5a290b, SHA: 0c859bf, SHA: bc6defd & SHA: 95be355) * Added ``defusedxml`` to the docs. (SHA: 521a696) * Added to Who Uses. (SHA: dea9bf8) django-tastypie-0.12.0/docs/release_notes/v0.11.1.rst000066400000000000000000000055751240444465200221330ustar00rootroot00000000000000v0.11.1 ======= :date: 2014-05-22 This release is primarily a security release. The two issues fixed have been present but unknown for a long time & **ALL** users are recommended to upgrade where possible. 1. Tastypie previously would accept a relation URI & *solely* parse out the identifiers, ignoring if the URI was for the right resource. Where ``'user': '/api/v1/users/1/',`` would be accepted as a ``User`` URI, you could accidentally/intentionally pass something like ``'user': '/api/v1/notes/1/',`` (**notes** rather than **users**), which would assign it to the ``User`` with a ``pk=1``. Tastypie would resolve the URI, but proceed to *only* care about the ``kwargs``, not validating it was for the correct resource. Tastypie now checks to ensure the resolving resource has a matching URI, so these cases of mistaken identity can no longer happen (& with quicker lookups). Thanks to Sergey Orshanskiy for the report! Fixed in SHA: 6da76c6 2. In some browsers (specifically Firefox), it was possible to construct a URL that would include an XSS attack (specifically around the ``offset/limit`` pagination parameters). Firefox seems to evaluate the JSON returned, completing the attack. Safari & Chrome do not appear to be affected. Tastypie now escapes all error messages that could be returned to the user to prevent this kind of attack in the future. Thanks to Micah Hausler for the report! Fixed in SHA: ae515bd Should you find a security issue in Tastypie, please report it to tastypie-security@googlegroups.com. Please **DO NOT** open GitHub issues or post the issues on the main Tastypie mailing list. Thanks! Bugfixes -------- * Removed a mutable argument to ``Serializer.serialize``. (SHA: fb7326d) * Fixed the ``unquote`` import in ``tastypie.contrib.gis``. (SHA: 1958df0) * Enabled testing on Travis for Python 3.4. (SHA: 6596935) * Documentation updates: * Fixed indentation in v0.9.16 release notes. (SHA: dd3725c) * Updated the v0.10.0 release notes. (SHA: e4c2455) * Fixed a Cookbook example to import ``json`` correctly. (SHA: bc7eb42) * Updated the non-ORM docs to match Resource. (SHA: da6a629) * Fixed grammar in Authorization docs. (SHA: 765ebf3) * Fixed a typo in Authorization docs. (SHA: 4818f08) * Updated the Cookbook to move an alternative approach to the correct place. (SHA: 803d679) * Updated the tutorial to import ``slugify`` from a more modern location. (SHA: 86bb5d9) * Fixed up inheritence in the Tools docs. (SHA: a1a2e64 & SHA: 12aa298) * Added a section about ``httpie`` to the Tools docs. (SHA: 5e49436) * Corrected the URL for ``biplist``. (SHA: 859ce97) * Added Postman to the list of Tools. (SHA: b9f0dec) * Fixed a typo on the docs index. (SHA: 17e5a91) * Fixed incorrect apostrophes. (SHA: 635729c) * Fixed a typo in the Resources docs. (SHA: d789eea) django-tastypie-0.12.0/docs/release_notes/v0.12.0.rst000066400000000000000000000021631240444465200221210ustar00rootroot00000000000000v0.12.0 ======= :date: 2014-09-11 This release adds official support for both Django 1.7, as well as several bugfixes. .. warning:: If you were previously relying on importing the ``User`` model from ``tastypie.compat``, this import will no longer work correctly. This was removed due to the way app-loading works in Django 1.7 & no great solution for dodging this issue exists. If you were using either of:: from tastypie.compat import User from tastypie.compat import username_field Please update your code as follows:: from tastypie.compat import get_user_model from tastypie.compat import get_username_field Bugfixes -------- * Drastic reworking of the test suite. (SHA: 95f57f7) * Fixed Travis to run Python 3.4 tests. (SHA: 7af528c) * Fixed a bug where URLs would be incorrectly handled if the ``api_name`` & ``resource_name`` were the same. (SHA: fd55aa3) * Fixed a test requirement for PyYAML. (SHA: b4f6531) * Added support for Django 1.7. (SHA: 7881bb6) * Documentation updates: * Fixed header in ``tools.rst``. (SHA: f8af772) * Fixed header in ``resources.rst``. (SHA: 9508cbf) django-tastypie-0.12.0/docs/release_notes/v0.9.13.rst000066400000000000000000000033001240444465200221250ustar00rootroot00000000000000v0.9.13 ======= *SECURITY HARDENING* The latest version of Tastypie includes a number of important security fixes and all users are strongly encouraged to upgrade. Please note that the fixes might cause backwards incompatibility issues, so please check the upgrade notes carefully. Security hardening improvements ------------------------------- * XML decoding has been wrapped in the defusedxml library * XML requests may no longer include DTDs by default * Deserialization will return HTTP 400 for any XML decode errors * 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 `_): 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 `_) * The serialization documentation has been upgraded with security advice: http://django-tastypie.readthedocs.org/en/v0.9.14/serialization.html#serialization-security Upgrade notes: * If you use XML serialization (enabled by default): - defusedxml is now required - defusedxml requires lxml 3 or later ``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.django-tastypie-0.12.0/docs/release_notes/v0.9.14.rst000066400000000000000000000004351240444465200221340ustar00rootroot00000000000000v0.9.14 ======= :date: 2013-03-19 An emergency release **removing** a failed attempt at using ``rose`` to handle versioning. Features -------- * None Bugfixes -------- * Removed the dependency on ``rose``, which wasn't effective at de-duplicating the version information. :( django-tastypie-0.12.0/docs/release_notes/v0.9.15.rst000066400000000000000000000024661240444465200221430ustar00rootroot00000000000000v0.9.15 ======= :date: 2013-05-02 This release is primarily a bugfix release. It makes using Tastypie with Django 1.5 more friendly, better exceptions Features -------- * Drops Python 2.5 support. Yes, this is a feature that will pave the way for Tastypie on Python 3 support. * Added ``TASTYPIE_ABSTRACT_APIKEY``, which allows switching the ``ApiKey`` model out. (SHA: b8f4b9c) Bugfixes -------- * Better support for Django 1.5: * Removed deprecation warnings (SHA: bb01761) * Numerous custom User improvements (SHA: d24b390) * Expanded places ``use_in`` is used (SHA: b32c45) * Authorization is now only called once with a full bundle (SHA: f06f41) * Changed ``for_list`` to accept a boolean (SHA: 01e620) * Only save related models that have changed (SHA: 6efdea) * Better exception reporting, especially in conjunction with Sentry * (SHA: 3f9ce0) * (SHA: 4adf11) * Configuration warning about ``defusedxml`` (SHA: aa8d9fd) * Fixed a dependency in ``setup.py`` (SHA: cb0fe7) * Release notes became a thing! Hooray! (SHA: 95e2499) * Documentation updates: * Typo in ``CleanedDataFormValidation`` (SHA: bf252a) * CamelCase JSON (SHA: bf252a) * Docstring typo (SHA: e86dad) * Per-user resource (SHA: a77b28c) * Added more details about creating the ``ApiKey`` model (SHA: 80f9b8) django-tastypie-0.12.0/docs/release_notes/v0.9.16.rst000066400000000000000000000025501240444465200221360ustar00rootroot00000000000000v0.9.16 ======= :date: 2013-08-03 This release is a bugfix release, the last one before Python 3 support will be added. This is the final release of Tastypie that is compatible with Django 1.4. Features -------- * Added support for Unicode HTTP method names. (SHA: 2ebb362) * Added a response for HTTP 422 Unprocessable Entity. (SHA: 7aadbf8) * Added ISO-8601 strict support. (SHA: 3043140) Bugfixes -------- * Added a ``None`` value for ``username_field`` in the compat module. (SHA: 569a72e) * Fixed a bug where ``obj_create`` would check authorization twice. (SHA: 9a404fa) * 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. (SHA: abc0bef) * Documentation updates: * Added an OAuth 2.0 example to the cookbook. (SHA: 7c93ae2) * Added a ``detail_uri_name`` to the Non-PK example in the cookbook. (SHA: 1fde565) * Added docs warning about ``total_count`` & performance overhead by default. (SHA: ebfbb7f) * Updated the Nested Resources example in the cookbook. (SHA: d582ead) * Added an example of converting a list of objects to JSON in the cookbook. (SHA: 2e81342) django-tastypie-0.12.0/docs/resources.rst000066400000000000000000001536611240444465200204110ustar00rootroot00000000000000.. _ref-resources: ========= Resources ========= In terms of a REST-style architecture, a "resource" is a collection of similar data. This data could be a table of a database, a collection of other resources or a similar form of data storage. In Tastypie, these resources are generally intermediaries between the end user & objects, usually Django models. As such, ``Resource`` (and its model-specific twin ``ModelResource``) form the heart of Tastypie's functionality. Quick Start =========== A sample resource definition might look something like:: from django.contrib.auth.models import User from tastypie import fields from tastypie.authorization import DjangoAuthorization from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS from myapp.models import Entry class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] class EntryResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: queryset = Entry.objects.all() list_allowed_methods = ['get', 'post'] detail_allowed_methods = ['get', 'post', 'put', 'delete'] resource_name = 'myapp/entry' authorization = DjangoAuthorization() filtering = { 'slug': ALL, 'user': ALL_WITH_RELATIONS, 'created': ['exact', 'range', 'gt', 'gte', 'lt', 'lte'], } Why Class-Based? ================ Using class-based resources make it easier to extend/modify the code to meet your needs. APIs are rarely a one-size-fits-all problem space, so Tastypie tries to get the fundamentals right and provide you with enough hooks to customize things to work your way. As is standard, this raises potential problems for thread-safety. Tastypie has been designed to minimize the possibility of data "leaking" between threads. This does however sometimes introduce some small complexities & you should be careful not to store state on the instances if you're going to be using the code in a threaded environment. Why ``Resource`` vs. ``ModelResource``? ======================================= Make no mistake that Django models are far and away the most popular source of data. However, in practice, there are many times where the ORM isn't the data source. Hooking up things like a NoSQL store (see :doc:`non_orm_data_sources`), a search solution like Haystack or even managed filesystem data are all good use cases for ``Resource`` knowing nothing about the ORM. Flow Through The Request/Response Cycle ======================================= Tastypie can be thought of as a set of class-based views that provide the API functionality. As such, many part of the request/response cycle are standard Django behaviors. For instance, all routing/middleware/response-handling aspects are the same as a typical Django app. Where it differs is in the view itself. As an example, we'll walk through what a GET request to a list endpoint (say ``/api/v1/user/?format=json``) looks like: * The ``Resource.urls`` are checked by Django's url resolvers. * On a match for the list view, ``Resource.wrap_view('dispatch_list')`` is called. ``wrap_view`` provides basic error handling & allows for returning serialized errors. * Because ``dispatch_list`` was passed to ``wrap_view``, ``Resource.dispatch_list`` is called next. This is a thin wrapper around ``Resource.dispatch``. * ``dispatch`` does a bunch of heavy lifting. It ensures: * 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``). * ``get_list`` does the actual work of the API. It does: * A fetch of the available objects via ``Resource.obj_get_list``. In the case 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 can work with) and applies the built filters to it. * It then sorts the objects based on user input (``ModelResource.apply_sorting``). * Then it paginates the results using the supplied ``Paginator`` & pulls out the data to be serialized. * The objects in the page have ``full_dehydrate`` applied to each of them, causing Tastypie to translate the raw object data into the fields the endpoint supports. * Finally, it calls ``Resource.create_response``. * ``create_response`` is a shortcut method that: * Determines the desired response format (``Resource.determine_format``), * Serializes the data given to it in the proper format, * And returns a Django ``HttpResponse`` (200 OK) with the serialized data. * We bubble back up the call stack to ``dispatch``. The last thing ``dispatch`` does is potentially store that a request occurred for future throttling (``Resource.log_throttled_access``) then either returns the ``HttpResponse`` or wraps whatever data came back in a response (so Django doesn't freak out). Processing on other endpoints or using the other HTTP methods results in a similar cycle, usually differing only in what "actual work" method gets called (which follows the format of "``_``"). In the case of POST/PUT, the ``hydrate`` cycle additionally takes place and is used to take the user data & convert it to raw data for storage. Why Resource URIs? ================== Resource URIs play a heavy role in how Tastypie delivers data. This can seem very different from other solutions which simply inline related data. Though Tastypie can inline data like that (using ``full=True`` on the field with the relation), the default is to provide URIs. URIs are useful because it results in smaller payloads, letting you fetch only the data that is important to you. You can imagine an instance where an object has thousands of related items that you may not be interested in. URIs are also very cache-able, because the data at each endpoint is less likely to frequently change. And URIs encourage proper use of each endpoint to display the data that endpoint covers. Ideology aside, you should use whatever suits you. If you prefer fewer requests & fewer endpoints, use of ``full=True`` is available, but be aware of the consequences of each approach. Accessing The Current Request ============================= Being able to change behavior based on the current request is a very commmon need. Virtually anywhere within ``Resource/ModelResource``, if a ``bundle`` is available, you can access it using ``bundle.request``. This is useful for altering querysets, ensuring headers are present, etc. Most methods you may need to override/extend should get a ``bundle`` passed to them. If you're using the ``Resource/ModelResource`` directly, with no ``request`` available, an empty ``Request`` will be supplied instead. If this is a common pattern/usage in your code, you'll want to accommodate for data that potentially isn't there. Advanced Data Preparation ========================= Not all data can be easily pulled off an object/model attribute. And sometimes, you (or the client) may need to send data that doesn't neatly fit back into the data model on the server side. For this, Tastypie has the "dehydrate/hydrate" cycle. The Dehydrate Cycle ------------------- Tastypie uses a "dehydrate" cycle to prepare data for serialization, which is to say that it takes the raw, potentially complicated data model & turns it into a (generally simpler) processed data structure for client consumption. This usually means taking a complex data object & turning it into a dictionary of simple data types. Broadly speaking, this takes the ``bundle.obj`` instance & builds ``bundle.data``, which is what is actually serialized. The cycle looks like: * Put the data model into a ``Bundle`` instance, which is then passed through the various methods. * Run through all fields on the ``Resource``, letting each field perform its own ``dehydrate`` method on the ``bundle``. * While processing each field, look for a ``dehydrate_`` method on the ``Resource``. If it's present, call it with the ``bundle``. * Finally, after all fields are processed, if the ``dehydrate`` method is present on the ``Resource``, it is called & given the entire ``bundle``. The goal of this cycle is to populate the ``bundle.data`` dictionary with data suitable for serialization. With the exception of the ``alter_*`` methods (as hooks to manipulate the overall structure), this cycle controls what is actually handed off to be serialized & sent to the client. Per-field ``dehydrate`` ~~~~~~~~~~~~~~~~~~~~~~~ Each field (even custom ``ApiField`` subclasses) has its own ``dehydrate`` method. If it knows how to access data (say, given the ``attribute`` kwarg), it will attempt to populate values itself. The return value is put in the ``bundle.data`` dictionary (by the ``Resource``) with the fieldname as the key. ``dehydrate_FOO`` ~~~~~~~~~~~~~~~~~ Since not all data may be ready for consumption based on just attribute access (or may require an advanced lookup/calculation), this hook enables you to fill in data or massage whatever the field generated. .. note:: The ``FOO`` here is not literal. Instead, it is a placeholder that should be replaced with the fieldname in question. Defining these methods is especially common when denormalizing related data, providing statistics or filling in unrelated data. A simple example:: class MyResource(ModelResource): # The ``title`` field is already added to the class by ``ModelResource`` # and populated off ``Note.title``. But we want allcaps titles... class Meta: queryset = Note.objects.all() def dehydrate_title(self, bundle): return bundle.data['title'].upper() A complex example:: class MyResource(ModelResource): # As is, this is just an empty field. Without the ``dehydrate_rating`` # method, no data would be populated for it. rating = fields.FloatField(readonly=True) class Meta: queryset = Note.objects.all() def dehydrate_rating(self, bundle): total_score = 0.0 # Make sure we don't have to worry about "divide by zero" errors. if not bundle.obj.rating_set.count(): return total_score # We'll run over all the ``Rating`` objects & calculate an average. for rating in bundle.obj.rating_set.all(): total_score += rating.rating return total_score / bundle.obj.rating_set.count() The return value is updated in the ``bundle.data``. You should avoid altering ``bundle.data`` here if you can help it. ``dehydrate`` ~~~~~~~~~~~~~ The ``dehydrate`` method takes a now fully-populated ``bundle.data`` & make any last alterations to it. This is useful for when a piece of data might depend on more than one field, if you want to shove in extra data that isn't worth having its own field or if you want to dynamically remove things from the data to be returned. A simple example:: class MyResource(ModelResource): class Meta: queryset = Note.objects.all() def dehydrate(self, bundle): # Include the request IP in the bundle. bundle.data['request_ip'] = bundle.request.META.get('REMOTE_ADDR') return bundle A complex example:: class MyResource(ModelResource): class Meta: queryset = User.objects.all() excludes = ['email', 'password', 'is_staff', 'is_superuser'] def dehydrate(self, bundle): # If they're requesting their own record, add in their email address. if bundle.request.user.pk == bundle.obj.pk: # Note that there isn't an ``email`` field on the ``Resource``. # By this time, it doesn't matter, as the built data will no # longer be checked against the fields on the ``Resource``. bundle.data['email'] = bundle.obj.email return bundle This method should return a ``bundle``, whether it modifies the existing one or creates a whole new one. You can even remove any/all data from the ``bundle.data`` if you wish. The Hydrate Cycle ------------------- Tastypie uses a "hydrate" cycle to take serializated data from the client and turn it into something the data model can use. This is the reverse process from the ``dehydrate`` cycle. In fact, by default, Tastypie's serialized data should be "round-trip-able", meaning the data that comes out should be able to be fed back in & result in the same original data model. This usually means taking a dictionary of simple data types & turning it into a complex data object. Broadly speaking, this takes the recently-deserialized ``bundle.data`` dictionary & builds ``bundle.obj`` (but does **NOT** save it). The cycle looks like: * Put the data from the client into a ``Bundle`` instance, which is then passed through the various methods. * If the ``hydrate`` method is present on the ``Resource``, it is called & given the entire ``bundle``. * Then run through all fields on the ``Resource``, look for a ``hydrate_`` method on the ``Resource``. If it's present, call it with the ``bundle``. * Finally after all other processing is done, while processing each field, let each field perform its own ``hydrate`` method on the ``bundle``. The goal of this cycle is to populate the ``bundle.obj`` data model with data suitable for saving/persistence. Again, with the exception of the ``alter_*`` methods (as hooks to manipulate the overall structure), this cycle controls how the data from the client is interpreted & placed on the data model. ``hydrate`` ~~~~~~~~~~~ The ``hydrate`` method allows you to make initial changes to the ``bundle.obj``. This includes things like prepopulating fields you don't expose over the API, recalculating related data or mangling data. Example:: class MyResource(ModelResource): # The ``title`` field is already added to the class by ``ModelResource`` # and populated off ``Note.title``. We'll use that title to build a # ``Note.slug`` as well. class Meta: queryset = Note.objects.all() def hydrate(self, bundle): # Don't change existing slugs. # In reality, this would be better implemented at the ``Note.save`` # level, but is for demonstration. if not bundle.obj.pk: bundle.obj.slug = slugify(bundle.data['title']) return bundle This method should return a ``bundle``, whether it modifies the existing one or creates a whole new one. You can even remove any/all data from the ``bundle.obj`` if you wish. ``hydrate_FOO`` ~~~~~~~~~~~~~~~ Data from the client may not map directly onto the data model or might need augmentation. This hook lets you take that data & convert it. .. note:: The ``FOO`` here is not literal. Instead, it is a placeholder that should be replaced with the fieldname in question. A simple example:: class MyResource(ModelResource): # The ``title`` field is already added to the class by ``ModelResource`` # and populated off ``Note.title``. But we want lowercase titles... class Meta: queryset = Note.objects.all() def hydrate_title(self, bundle): bundle.data['title'] = bundle.data['title'].lower() return bundle The return value is the ``bundle``. Per-field ``hydrate`` ~~~~~~~~~~~~~~~~~~~~~ Each field (even custom ``ApiField`` subclasses) has its own ``hydrate`` method. If it knows how to access data (say, given the ``attribute`` kwarg), it will attempt to take data from the ``bundle.data`` & assign it on the data model. The return value is put in the ``bundle.obj`` attribute for that fieldname. Reverse "Relationships" ======================= Unlike Django's ORM, Tastypie does not automatically create reverse relations. This is because there is substantial technical complexity involved, as well as perhaps unintentionally exposing related data in an incorrect way to the end user of the API. However, it is still possible to create reverse relations. Instead of handing the ``ToOneField`` or ``ToManyField`` a class, pass them a string that represents the full path to the desired class. Implementing a reverse relationship looks like so:: # myapp/api/resources.py from tastypie import fields from tastypie.resources import ModelResource from myapp.models import Note, Comment class NoteResource(ModelResource): comments = fields.ToManyField('myapp.api.resources.CommentResource', 'comments') class Meta: queryset = Note.objects.all() class CommentResource(ModelResource): note = fields.ToOneField(NoteResource, 'notes') class Meta: queryset = Comment.objects.all() .. warning:: Unlike Django, you can't use just the class name (i.e. ``'CommentResource'``), even if it's in the same module. Tastypie (intentionally) lacks a construct like the ``AppCache`` which makes that sort of thing work in Django. Sorry. Tastypie also supports self-referential relations. If you assume we added the appropriate self-referential ``ForeignKey`` to the ``Note`` model, implementing a similar relation in Tastypie would look like:: # myapp/api/resources.py from tastypie import fields from tastypie.resources import ModelResource from myapp.models import Note class NoteResource(ModelResource): sub_notes = fields.ToManyField('self', 'notes') class Meta: queryset = Note.objects.all() Resource Options (AKA ``Meta``) =============================== The inner ``Meta`` class allows for class-level configuration of how the ``Resource`` should behave. The following options are available: ``serializer`` -------------- Controls which serializer class the ``Resource`` should use. Default is ``tastypie.serializers.Serializer()``. ``authentication`` ------------------ Controls which authentication class the ``Resource`` should use. Default is ``tastypie.authentication.Authentication()``. ``authorization`` ----------------- Controls which authorization class the ``Resource`` should use. Default is ``tastypie.authorization.ReadOnlyAuthorization()``. ``validation`` -------------- Controls which validation class the ``Resource`` should use. Default is ``tastypie.validation.Validation()``. ``paginator_class`` ------------------- Controls which paginator class the ``Resource`` should use. Default is ``tastypie.paginator.Paginator``. .. note:: This is different than the other options in that you supply a class rather than an instance. This is done because the Paginator has some per-request initialization options. ``cache`` --------- Controls which cache class the ``Resource`` should use. Default is ``tastypie.cache.NoCache()``. ``throttle`` ------------ Controls which throttle class the ``Resource`` should use. Default is ``tastypie.throttle.BaseThrottle()``. ``allowed_methods`` ------------------- Controls what list & detail REST methods the ``Resource`` should respond to. Default is ``None``, which means delegate to the more specific ``list_allowed_methods`` & ``detail_allowed_methods`` options. You may specify a list like ``['get', 'post', 'put', 'delete', 'patch']`` as a shortcut to prevent having to specify the other options. ``list_allowed_methods`` ------------------------ Controls what list REST methods the ``Resource`` should respond to. Default is ``['get', 'post', 'put', 'delete', 'patch']``. Set it to an empty list (i.e. `[]`) to disable all methods. .. _detail-allowed-methods: ``detail_allowed_methods`` -------------------------- Controls what detail REST methods the ``Resource`` should respond to. Default is ``['get', 'post', 'put', 'delete', 'patch']``. Set it to an empty list (i.e. `[]`) to disable all methods. ``limit`` --------- Controls how many results the ``Resource`` will show at a time. Default is either the ``API_LIMIT_PER_PAGE`` setting (if provided) or ``20`` if not specified. ``max_limit`` ------------- Controls the maximum number of results the ``Resource`` will show at a time. If the user-specified ``limit`` is higher than this, it will be capped to this limit. Set to ``0`` or ``None`` to allow unlimited results. ``api_name`` ------------ An override for the ``Resource`` to use when generating resource URLs. Default is ``None``. ``resource_name`` ----------------- An override for the ``Resource`` to use when generating resource URLs. Default is ``None``. If not provided, the ``Resource`` or ``ModelResource`` will attempt to name itself. This means a lowercase version of the classname preceding the word ``Resource`` if present (i.e. ``SampleContentResource`` would become ``samplecontent``). ``default_format`` ------------------ Specifies the default serialization format the ``Resource`` should use if one is not requested (usually by the ``Accept`` header or ``format`` GET parameter). Default is ``application/json``. ``filtering`` ------------- Provides a list of 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. ``ordering`` ------------ Specifies the what fields the ``Resource`` should allow ordering on. Default is ``[]``. Values should be the fieldnames as strings. When provided to the ``Resource`` by the ``order_by`` GET parameter, you can specify either the ``fieldname`` (ascending order) or ``-fieldname`` (descending order). ``object_class`` ---------------- Provides the ``Resource`` with the object that serves as the data source. Default is ``None``. In the case of ``ModelResource``, this is automatically populated by the ``queryset`` option and is the model class. ``queryset`` ------------ Provides the ``Resource`` with the set of Django models to respond with. Default is ``None``. Unused by ``Resource`` but present for consistency. .. warning:: If you place any callables in this, they'll only be evaluated once (when the ``Meta`` class is instantiated). This especially affects things that are date/time related. Please see the :doc:`cookbook` for a way around this. ``fields`` ---------- Controls what introspected fields the ``Resource`` should include. A whitelist of fields. Default is ``[]``. ``excludes`` ------------ Controls what introspected fields the ``Resource`` should *NOT* include. A blacklist of fields. Default is ``[]``. ``include_resource_uri`` ------------------------ Specifies if the ``Resource`` should include an extra field that displays the detail URL (within the api) for that resource. Default is ``True``. ``include_absolute_url`` ------------------------ Specifies if the ``Resource`` should include an extra field that displays the ``get_absolute_url`` for that object (on the site proper). Default is ``False``. ``always_return_data`` ------------------------ Specifies all HTTP methods (except ``DELETE``) should return a serialized form of the data. Default is ``False``. If ``False``, ``HttpNoContent`` (204) is returned on ``PUT`` with an empty body & a ``Location`` header of where to request the full resource. If ``True``, ``HttpResponse`` (200) is returned on ``POST/PUT`` with a body containing all the data in a serialized form. ``collection_name`` ------------------- Specifies the collection of objects returned in the ``GET`` list will be named. Default is ``objects``. ``detail_uri_name`` ------------------- Specifies the name for the regex group that matches on detail views. Defaults to ``pk``. Basic Filtering =============== :class:`~tastypie.resources.ModelResource` provides a basic Django ORM filter interface. Simply list the resource fields which you'd like to filter on and the allowed expression in a `filtering` property of your resource's Meta class:: from tastypie.constants import ALL, ALL_WITH_RELATIONS class MyResource(ModelResource): class Meta: filtering = { "slug": ('exact', 'startswith',), "title": ALL, } Valid filtering values are: `Django ORM filters`_ (e.g. ``startswith``, ``exact``, ``lte``, etc.) or the ``ALL`` or ``ALL_WITH_RELATIONS`` constants defined in :mod:`tastypie.constants`. .. _Django ORM filters: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups These filters will be extracted from URL query strings using the same double-underscore syntax as the Django ORM:: /api/v1/myresource/?slug=myslug /api/v1/myresource/?slug__startswith=test Advanced Filtering ================== If you need to filter things other than ORM resources or wish to apply additional constraints (e.g. text filtering using `django-haystack `_ rather than simple database queries) your :class:`~tastypie.resources.Resource` may define a custom :meth:`~tastypie.resource.Resource.build_filters` method which allows you to filter the queryset before processing a request:: from haystack.query import SearchQuerySet class MyResource(Resource): def build_filters(self, filters=None): if filters is None: filters = {} orm_filters = super(MyResource, self).build_filters(filters) if "q" in filters: sqs = SearchQuerySet().auto_query(filters['q']) orm_filters["pk__in"] = [i.pk for i in sqs] return orm_filters Using PUT/DELETE/PATCH In Unsupported Places ============================================ Some places, like in certain browsers or hosts, don't allow the ``PUT/DELETE/PATCH`` methods. In these environments, you can simulate those kinds of requests by providing an ``X-HTTP-Method-Override`` header. For example, to send a ``PATCH`` request over ``POST``, you'd send a request like:: curl --dump-header - -H "Content-Type: application/json" -H "X-HTTP-Method-Override: PATCH" -X POST --data '{"title": "I Visited Grandma Today"}' http://localhost:8000/api/v1/entry/1/ ``Resource`` Methods ==================== Handles the data, request dispatch and responding to requests. Serialization/deserialization is handled "at the edges" (i.e. at the beginning/end of the request/response cycle) so that everything internally is Python data structures. This class tries to be non-model specific, so it can be hooked up to other data sources, such as search results, files, other data, etc. ``wrap_view`` ------------- .. method:: Resource.wrap_view(self, view) Wraps methods so they can be called in a more functional way as well as handling exceptions better. Note that if ``BadRequest`` or an exception with a ``response`` attr are seen, there is special handling to either present a message back to the user or return the response traveling with the exception. ``base_urls`` ------------- .. method:: Resource.base_urls(self) 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``). ``override_urls`` ----------------- .. method:: Resource.override_urls(self) Deprecated. Will be removed by v1.0.0. Please use ``Resource.prepend_urls`` instead. ``prepend_urls`` ---------------- .. method:: Resource.prepend_urls(self) 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``). ``urls`` -------- .. method:: Resource.urls(self) *Property* The endpoints this ``Resource`` responds to. A combination of ``base_urls`` & ``override_urls``. Mostly a standard URLconf, this is suitable for either automatic use when registered with an ``Api`` class or for including directly in a URLconf should you choose to. ``determine_format`` -------------------- .. method:: Resource.determine_format(self, request) Used to determine the desired format. Largely relies on ``tastypie.utils.mime.determine_format`` but here as a point of extension. ``serialize`` ------------- .. method:: Resource.serialize(self, request, data, format, options=None) Given a request, data and a desired format, produces a serialized version suitable for transfer over the wire. Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. ``deserialize`` --------------- .. method:: Resource.deserialize(self, request, data, format='application/json') Given a request, data and a format, deserializes the given data. It relies on the request properly sending a ``CONTENT_TYPE`` header, falling back to ``application/json`` if not provided. Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. ``alter_list_data_to_serialize`` -------------------------------- .. method:: Resource.alter_list_data_to_serialize(self, request, data) A hook to alter list data just before it gets serialized & sent to the user. Useful for restructuring/renaming aspects of the what's going to be sent. Should accommodate for a list of objects, generally also including meta data. ``alter_detail_data_to_serialize`` ---------------------------------- .. method:: Resource.alter_detail_data_to_serialize(self, request, data) A hook to alter detail data just before it gets serialized & sent to the user. Useful for restructuring/renaming aspects of the what's going to be sent. Should accommodate for receiving a single bundle of data. ``alter_deserialized_list_data`` -------------------------------- .. method:: Resource.alter_deserialized_list_data(self, request, data) A hook to alter list data just after it has been received from the user & gets deserialized. Useful for altering the user data before any hydration is applied. ``alter_deserialized_detail_data`` ---------------------------------- .. method:: Resource.alter_deserialized_detail_data(self, request, data) A hook to alter detail data just after it has been received from the user & gets deserialized. Useful for altering the user data before any hydration is applied. ``dispatch_list`` ----------------- .. method:: Resource.dispatch_list(self, request, **kwargs) A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over the entire list of resources. Relies on ``Resource.dispatch`` for the heavy-lifting. ``dispatch_detail`` ------------------- .. method:: Resource.dispatch_detail(self, request, **kwargs) A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on a single resource. Relies on ``Resource.dispatch`` for the heavy-lifting. ``dispatch`` ------------ .. method:: Resource.dispatch(self, request_type, request, **kwargs) Handles the common operations (allowed HTTP method, authentication, throttling, method lookup) surrounding most CRUD interactions. ``remove_api_resource_names`` ----------------------------- .. method:: Resource.remove_api_resource_names(self, url_dict) Given a dictionary of regex matches from a URLconf, removes ``api_name`` and/or ``resource_name`` if found. This is useful for converting URLconf matches into something suitable for data lookup. For example:: Model.objects.filter(**self.remove_api_resource_names(matches)) ``method_check`` ---------------- .. method:: Resource.method_check(self, request, allowed=None) Ensures that the HTTP method used on the request is allowed to be handled by the resource. Takes an ``allowed`` parameter, which should be a list of lowercase HTTP methods to check against. Usually, this looks like:: # The most generic lookup. self.method_check(request, self._meta.allowed_methods) # A lookup against what's allowed for list-type methods. self.method_check(request, self._meta.list_allowed_methods) # A useful check when creating a new endpoint that only handles # 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`` -------------------- .. method:: Resource.is_authenticated(self, request) Handles checking if the user is authenticated and dealing with unauthenticated users. Mostly a hook, this uses class assigned to ``authentication`` from ``Resource._meta``. ``throttle_check`` ------------------ .. method:: Resource.throttle_check(self, request) Handles checking if the user should be throttled. Mostly a hook, this uses class assigned to ``throttle`` from ``Resource._meta``. ``log_throttled_access`` ------------------------ .. method:: Resource.log_throttled_access(self, request) Handles the recording of the user's access for throttling purposes. Mostly a hook, this uses class assigned to ``throttle`` from ``Resource._meta``. ``build_bundle`` ---------------- .. method:: Resource.build_bundle(self, obj=None, data=None, request=None) Given either an object, a data dictionary or both, builds a ``Bundle`` for use throughout the ``dehydrate/hydrate`` cycle. If no object is provided, an empty object from ``Resource._meta.object_class`` is created so that attempts to access ``bundle.obj`` do not fail. ``build_filters`` ----------------- .. method:: Resource.build_filters(self, filters=None) Allows for the filtering of applicable objects. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``apply_sorting`` ----------------- .. method:: Resource.apply_sorting(self, obj_list, options=None) Allows for the sorting of objects being returned. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``get_bundle_detail_data`` -------------------------- .. method:: Resource.get_bundle_detail_data(self, bundle) Convenience method to return the ``detail_uri_name`` attribute off ``bundle.obj``. Usually just accesses ``bundle.obj.pk`` by default. ``get_resource_uri`` -------------------- .. method:: Resource.get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list') Handles generating a resource URI. If the ``bundle_or_obj`` argument is not provided, it builds the URI for the list endpoint. If the ``bundle_or_obj`` argument is provided, it builds the URI for the detail endpoint. Return the generated URI. If that URI can not be reversed (not found in the URLconf), it will return an empty string. ``resource_uri_kwargs`` ----------------------- .. method:: Resource.resource_uri_kwargs(self, bundle_or_obj=None) Handles generating a resource URI. If the ``bundle_or_obj`` argument is not provided, it builds the URI for the list endpoint. If the ``bundle_or_obj`` argument is provided, it builds the URI for the detail endpoint. Return the generated URI. If that URI can not be reversed (not found in the URLconf), it will return ``None``. ``detail_uri_kwargs`` --------------------- .. method:: Resource.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. ``ModelResource`` includes a full working version specific to Django's ``Models``. ``get_via_uri`` --------------- .. method:: Resource.get_via_uri(self, uri, request=None) This pulls apart the salient bits of the URI and populates the resource via a ``obj_get``. Optionally accepts a ``request``. If you need custom behavior based on other portions of the URI, simply override this method. ``full_dehydrate`` ------------------ .. 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. The for_list flag is used to control which fields are excluded by the ``use_in`` attribute. ``dehydrate`` ------------- .. method:: Resource.dehydrate(self, bundle) A hook to allow a final manipulation of data once all fields/methods have built out the dehydrated data. Useful if you need to access more than one dehydrated field or want to annotate on additional data. Must return the modified bundle. ``full_hydrate`` ---------------- .. method:: Resource.full_hydrate(self, bundle) Given a populated bundle, distill it and turn it back into a full-fledged object instance. ``hydrate`` ----------- .. method:: Resource.hydrate(self, bundle) A hook to allow a final manipulation of data once all fields/methods have built out the hydrated data. Useful if you need to access more than one hydrated field or want to annotate on additional data. Must return the modified bundle. ``hydrate_m2m`` --------------- .. method:: Resource.hydrate_m2m(self, bundle) Populate the ManyToMany data on the instance. ``build_schema`` ---------------- .. method:: Resource.build_schema(self) Returns a dictionary of all the fields on the resource and some properties about those fields. Used by the ``schema/`` endpoint to describe what will be available. ``dehydrate_resource_uri`` -------------------------- .. method:: Resource.dehydrate_resource_uri(self, bundle) For the automatically included ``resource_uri`` field, dehydrate the URI for the given bundle. Returns empty string if no URI can be generated. ``generate_cache_key`` ---------------------- .. method:: Resource.generate_cache_key(self, *args, **kwargs) Creates a unique-enough cache key. This is based off the current api_name/resource_name/args/kwargs. ``get_object_list`` ------------------- .. method:: Resource.get_object_list(self, request) A hook to allow making returning the list of available objects. *This needs to be implemented at the user level.* ``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`` -------------- .. method:: Resource.can_create(self) Checks to ensure ``post`` is within ``allowed_methods``. ``can_update`` -------------- .. method:: Resource.can_update(self) Checks to ensure ``put`` is within ``allowed_methods``. Used when hydrating related data. ``can_delete`` -------------- .. method:: Resource.can_delete(self) Checks to ensure ``delete`` is within ``allowed_methods``. ``apply_filters`` ----------------- .. method:: Resource.apply_filters(self, request, applicable_filters) A hook to alter how the filters are applied to the object list. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. ``obj_get_list`` ---------------- .. method:: Resource.obj_get_list(self, bundle, **kwargs) Fetches the list of objects available on the resource. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``cached_obj_get_list`` ----------------------- .. method:: Resource.cached_obj_get_list(self, bundle, **kwargs) A version of ``obj_get_list`` that uses the cache as a means to get commonly-accessed data faster. ``obj_get`` ----------- .. method:: Resource.obj_get(self, bundle, **kwargs) Fetches an individual object on the resource. *This needs to be implemented at the user level.* If the object can not be found, this should raise a ``NotFound`` exception. ``ModelResource`` includes a full working version specific to Django's ``Models``. ``cached_obj_get`` ------------------ .. method:: Resource.cached_obj_get(self, bundle, **kwargs) A version of ``obj_get`` that uses the cache as a means to get commonly-accessed data faster. ``obj_create`` -------------- .. method:: Resource.obj_create(self, bundle, **kwargs) Creates a new object based on the provided data. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``lookup_kwargs_with_identifiers`` ---------------------------------- .. method:: Resource.lookup_kwargs_with_identifiers(self, bundle, kwargs) Kwargs here represent uri identifiers. Ex: /repos/// We need to turn those identifiers into Python objects for generating lookup parameters that can find them in the DB. ``obj_update`` -------------- .. method:: Resource.obj_update(self, bundle, **kwargs) Updates an existing object (or creates a new object) based on the provided data. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``obj_delete_list`` ------------------- .. method:: Resource.obj_delete_list(self, bundle, **kwargs) Deletes an entire list of objects. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``obj_delete_list_for_update`` ------------------------------ .. method:: Resource.obj_delete_list_for_update(self, bundle, **kwargs) Deletes an entire list of objects, specific to PUT list. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``obj_delete`` -------------- .. method:: Resource.obj_delete(self, bundle, **kwargs) Deletes a single object. *This needs to be implemented at the user level.* ``ModelResource`` includes a full working version specific to Django's ``Models``. ``create_response`` ------------------- .. method:: Resource.create_response(self, request, data, response_class=HttpResponse, **response_kwargs) Extracts the common "which-format/serialize/return-response" cycle. Mostly a useful shortcut/hook. ``is_valid`` ------------ .. method:: Resource.is_valid(self, bundle) Handles checking if the data provided by the user is valid. Mostly a hook, this uses class assigned to ``validation`` from ``Resource._meta``. If validation fails, an error is raised with the error messages serialized inside it. ``rollback`` ------------ .. method:: Resource.rollback(self, bundles) Given the list of bundles, delete all objects pertaining to those bundles. This needs to be implemented at the user level. No exceptions should be raised if possible. ``ModelResource`` includes a full working version specific to Django's ``Models``. ``get_list`` ------------ .. method:: Resource.get_list(self, request, **kwargs) Returns a serialized list of resources. Calls ``obj_get_list`` to provide the data, then handles that result set and serializes it. Should return a HttpResponse (200 OK). ``get_detail`` -------------- .. method:: Resource.get_detail(self, request, **kwargs) Returns a single serialized resource. Calls ``cached_obj_get/obj_get`` to provide the data, then handles that result set and serializes it. Should return a HttpResponse (200 OK). ``put_list`` ------------ .. method:: Resource.put_list(self, request, **kwargs) Replaces a collection of resources with another collection. Calls ``delete_list`` to clear out the collection then ``obj_create`` with the provided the data to create the new collection. Return ``HttpNoContent`` (204 No Content) if ``Meta.always_return_data = False`` (default). Return ``HttpAccepted`` (202 Accepted) if ``Meta.always_return_data = True``. ``put_detail`` -------------- .. method:: Resource.put_detail(self, request, **kwargs) Either updates an existing resource or creates a new one with the provided data. Calls ``obj_update`` with the provided data first, but falls back to ``obj_create`` if the object does not already exist. If a new resource is created, return ``HttpCreated`` (201 Created). If ``Meta.always_return_data = True``, there will be a populated body of serialized data. If an existing resource is modified and ``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). ``post_list`` ------------- .. method:: Resource.post_list(self, request, **kwargs) Creates a new resource/object with the provided data. Calls ``obj_create`` with the provided data and returns a response with the new resource's location. If a new resource is created, return ``HttpCreated`` (201 Created). If ``Meta.always_return_data = True``, there will be a populated body of serialized data. ``post_detail`` --------------- .. method:: Resource.post_detail(self, request, **kwargs) Creates a new subcollection of the resource under a resource. This is not implemented by default because most people's data models aren't self-referential. If a new resource is created, return ``HttpCreated`` (201 Created). ``delete_list`` --------------- .. method:: Resource.delete_list(self, request, **kwargs) Destroys a collection of resources/objects. Calls ``obj_delete_list``. If the resources are deleted, return ``HttpNoContent`` (204 No Content). ``delete_detail`` ----------------- .. method:: Resource.delete_detail(self, request, **kwargs) Destroys a single resource/object. Calls ``obj_delete``. If the resource is deleted, return ``HttpNoContent`` (204 No Content). If the resource did not exist, return ``HttpNotFound`` (404 Not Found). .. _patch-list: ``patch_list`` -------------- .. method:: Resource.patch_list(self, request, **kwargs) Updates a collection in-place. The exact behavior of ``PATCH`` to a list resource is still the matter of some debate in REST circles, and the ``PATCH`` RFC isn't standard. So the behavior this method implements (described below) is something of a stab in the dark. It's mostly cribbed from GData, with a smattering of ActiveResource-isms and maybe even an original idea or two. The ``PATCH`` format is one that's similar to the response returned from a ``GET`` on a list resource:: { "objects": [{object}, {object}, ...], "deleted_objects": ["URI", "URI", "URI", ...], } For each object in ``objects``: * If the dict does not have a ``resource_uri`` key then the item is considered "new" and is handled like a ``POST`` to the resource list. * If the dict has a ``resource_uri`` key and the ``resource_uri`` refers to an existing resource then the item is an update; it's treated like a ``PATCH`` to the corresponding resource detail. * If the dict has a ``resource_uri`` but the resource *doesn't* exist, then this is considered to be a create-via-``PUT``. Each entry in ``deleted_objects`` refers to a resource URI of an existing resource to be deleted; each is handled like a ``DELETE`` to the relevant resource. In any case: * If there's a resource URI it *must* refer to a resource of this type. It's an error to include a URI of a different resource. * ``PATCH`` is all or nothing. If a single sub-operation fails, the entire request will fail and all resources will be rolled back. * For ``PATCH`` to work, you **must** have ``patch`` in your :ref:`detail-allowed-methods` setting. * To delete objects via ``deleted_objects`` in a ``PATCH`` request you **must** have ``delete`` in your :ref:`detail-allowed-methods` setting. ``patch_detail`` ---------------- .. method:: Resource.patch_detail(self, request, **kwargs) Updates a resource in-place. Calls ``obj_update``. If the resource is updated, return ``HttpAccepted`` (202 Accepted). If the resource did not exist, return ``HttpNotFound`` (404 Not Found). ``get_schema`` -------------- .. method:: Resource.get_schema(self, request, **kwargs) Returns a serialized form of the schema of the resource. Calls ``build_schema`` to generate the data. This method only responds to HTTP GET. Should return a HttpResponse (200 OK). ``get_multiple`` ---------------- .. method:: Resource.get_multiple(self, request, **kwargs) Returns a serialized list of resources based on the identifiers from the URL. Calls ``obj_get`` to fetch only the objects requested. This method only responds to HTTP GET. Should return a HttpResponse (200 OK). ``ModelResource`` Methods ========================= A subclass of ``Resource`` designed to work with Django's ``Models``. This class will introspect a given ``Model`` and build a field list based on the fields found on the model (excluding relational fields). Given that it is aware of Django's ORM, it also handles the CRUD data operations of the resource. ``should_skip_field`` --------------------- .. method:: ModelResource.should_skip_field(cls, field) *Class method* Given a Django model field, return if it should be included in the contributed ApiFields. ``api_field_from_django_field`` ------------------------------- .. method:: ModelResource.api_field_from_django_field(cls, f, default=CharField) *Class method* Returns the field type that would likely be associated with each Django type. ``get_fields`` -------------- .. method:: ModelResource.get_fields(cls, fields=None, excludes=None) *Class method* Given any explicit fields to include and fields to exclude, add additional fields based on the associated model. ``check_filtering`` ------------------- .. method:: ModelResource.check_filtering(self, field_name, filter_type='exact', filter_bits=None) Given a field name, an optional filter type and an optional list of additional relations, determine if a field can be filtered on. If a filter does not meet the needed conditions, it should raise an ``InvalidFilterError``. If the filter meets the conditions, a list of attribute names (not field names) will be returned. ``build_filters`` ----------------- .. method:: ModelResource.build_filters(self, filters=None) Given a dictionary of filters, create the necessary ORM-level filters. Keys should be resource fields, **NOT** model fields. Valid values are either a list of Django filter types (i.e. ``['startswith', 'exact', 'lte']``), the ``ALL`` constant or the ``ALL_WITH_RELATIONS`` constant. At the declarative level:: filtering = { 'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'], 'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'], 'resource_field_name_3': ALL, 'resource_field_name_4': ALL_WITH_RELATIONS, ... } Accepts the filters as a dict. ``None`` by default, meaning no filters. ``apply_sorting`` ----------------- .. method:: ModelResource.apply_sorting(self, obj_list, options=None) Given a dictionary of options, apply some ORM-level sorting to the provided ``QuerySet``. Looks for the ``order_by`` key and handles either ascending (just the field name) or descending (the field name with a ``-`` in front). The field name should be the resource field, **NOT** model field. ``apply_filters`` ----------------- .. method:: ModelResource.apply_filters(self, request, applicable_filters) An ORM-specific implementation of ``apply_filters``. The default simply applies the ``applicable_filters`` as ``**kwargs``, but should make it possible to do more advanced things. ``get_object_list`` ------------------- .. method:: ModelResource.get_object_list(self, request) A ORM-specific implementation of ``get_object_list``. Returns a ``QuerySet`` that may have been limited by other overrides. ``obj_get_list`` ---------------- .. method:: ModelResource.obj_get_list(self, filters=None, **kwargs) A ORM-specific implementation of ``obj_get_list``. Takes an optional ``filters`` dictionary, which can be used to narrow the query. ``obj_get`` ----------- .. method:: ModelResource.obj_get(self, **kwargs) A ORM-specific implementation of ``obj_get``. Takes optional ``kwargs``, which are used to narrow the query to find the instance. ``obj_create`` -------------- .. method:: ModelResource.obj_create(self, bundle, **kwargs) A ORM-specific implementation of ``obj_create``. ``obj_update`` -------------- .. method:: ModelResource.obj_update(self, bundle, **kwargs) A ORM-specific implementation of ``obj_update``. ``obj_delete_list`` ------------------- .. method:: ModelResource.obj_delete_list(self, **kwargs) A ORM-specific implementation of ``obj_delete_list``. Takes optional ``kwargs``, which can be used to narrow the query. ``obj_delete_list_for_update`` ------------------------------ .. method:: ModelResource.obj_delete_list_for_update(self, **kwargs) A ORM-specific implementation of ``obj_delete_list_for_update``. Takes optional ``kwargs``, which can be used to narrow the query. ``obj_delete`` -------------- .. method:: ModelResource.obj_delete(self, **kwargs) A ORM-specific implementation of ``obj_delete``. Takes optional ``kwargs``, which are used to narrow the query to find the instance. ``rollback`` ------------ .. method:: ModelResource.rollback(self, bundles) A ORM-specific implementation of ``rollback``. Given the list of bundles, delete all models pertaining to those bundles. ``save_related`` ---------------- .. method:: ModelResource.save_related(self, bundle) Handles the saving of related non-M2M data. Calling assigning ``child.parent = parent`` & then calling ``Child.save`` isn't good enough to make sure the ``parent`` is saved. To get around this, we go through all our related fields & call ``save`` on them if they have related, non-M2M data. M2M data is handled by the ``ModelResource.save_m2m`` method. ``save_m2m`` ------------ .. method:: ModelResource.save_m2m(self, bundle) Handles the saving of related M2M data. Due to the way Django works, the M2M data must be handled after the main instance, which is why this isn't a part of the main ``save`` bits. Currently slightly inefficient in that it will clear out the whole relation and recreate the related data as needed. ``get_resource_uri`` -------------------- .. method:: ModelResource.get_resource_uri(self, bundle_or_obj) Handles generating a resource URI for a single resource. Uses the model's ``pk`` in order to create the URI. django-tastypie-0.12.0/docs/serialization.rst000066400000000000000000000263061240444465200212470ustar00rootroot00000000000000.. _ref-serialization: ============= Serialization ============= Serialization can be one of the most contentious areas of an API. Everyone has their own requirements, their own preferred output format & the desire to have control over what is returned. As a result, Tastypie ships with a serializer that tries to meet the basic needs of most use cases, and the flexibility to go outside of that when you need to. Usage ===== Using this class is simple. It is the default option on all ``Resource`` classes unless otherwise specified. The following code is identical to the defaults but demonstrate how you could use your own serializer:: from django.contrib.auth.models import User from tastypie.resources import ModelResource from tastypie.serializers import Serializer class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. serializer = Serializer() Configuring Allowed Formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The default ``Serializer`` supports the following formats: * json * 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 the :ref:`TASTYPIE_DEFAULT_FORMATS setting ` allows you to set the default format list site-wide. If you wish to change the format list for a specific resource, you can pass the list of supported formats using the ``formats=`` kwarg. For example, to provide only JSON & binary plist serialization:: from django.contrib.auth.models import User from tastypie.resources import ModelResource from tastypie.serializers import Serializer class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] serializer = Serializer(formats=['json', 'plist']) Enabling the built-in (but disabled by default) JSONP support looks like:: from django.contrib.auth.models import User from tastypie.resources import ModelResource from tastypie.serializers import Serializer class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] serializer = Serializer(formats=['json', 'jsonp', 'xml', 'yaml', 'html', 'plist']) Serialization Security ====================== Deserialization of input from unknown or untrusted sources is an intrinsically risky endeavor and vulnerabilities are regularly found in popular format libraries. Tastypie adopts and recommends the following approach: * Support the minimum required set of formats in your application. If you do not require a format, it's much safer to disable it completely. See :ref:`TASTYPIE_DEFAULT_FORMATS setting `. * Some parsers offer additional safety check for use with untrusted content. The standard Tastypie Serializer attempts to be secure by default using features like PyYAML's `safe_load `_ function and the defusedxml_ security wrapper for popular Python XML libraries. .. note:: Tastypie's precautions only apply to the default :class:`Serializer`. If you have written your own serializer subclass we strongly recommend that you review your code to ensure that it uses the same precautions. If backwards compatibility forces you to load files which require risky features we strongly recommend enabling those features only for the necessary resources and making your authorization checks as strict as possible. The :doc:`authentication` and :doc:`authorization` checks happen before deserialization so, for example, a resource which only allowed POST or PUT requests to be made by administrators is far less exposed than a general API open to the unauthenticated internet. .. _defusedxml: https://pypi.python.org/pypi/defusedxml Implementing Your Own Serializer ================================ There are several different use cases here. We'll cover simple examples of wanting a tweaked format & adding a different format. To tweak a format, simply override it's ``to_`` & ``from_`` methods. So adding the server time to all output might look like so:: import time import json from django.core.serializers.json import DjangoJSONEncoder from tastypie.serializers import Serializer class CustomJSONSerializer(Serializer): def to_json(self, data, options=None): options = options or {} data = self.to_simple(data, options) # Add in the current time. data['requested_time'] = time.time() return json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True) def from_json(self, content): data = json.loads(content) if 'requested_time' in data: # Log the request here... pass return data In the case of adding a different format, let's say you want to add a CSV output option to the existing set. Your ``Serializer`` subclass might look like:: import csv import StringIO from tastypie.serializers import Serializer 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', } 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 def from_csv(self, content): raw_data = StringIO.StringIO(content) data = [] # Untested, so this might not work exactly right. for item in csv.DictReader(raw_data): data.append(item) return data ``Serializer`` Methods ====================== A swappable class for serialization. This handles most types of data as well as the following output formats:: * json * jsonp * xml * yaml * html * plist It was designed to make changing behavior easy, either by overridding the various format methods (i.e. ``to_json``), by changing the ``formats/content_types`` options or by altering the other hook methods. ``get_mime_for_format`` ~~~~~~~~~~~~~~~~~~~~~~~ .. method:: Serializer.get_mime_for_format(self, format): Given a format, attempts to determine the correct MIME type. If not available on the current ``Serializer``, returns ``application/json`` by default. ``format_datetime`` ~~~~~~~~~~~~~~~~~~~ .. method:: Serializer.format_datetime(data): A hook to control how datetimes are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "2010-12-16T03:02:14". ``format_date`` ~~~~~~~~~~~~~~~ .. method:: Serializer.format_date(data): A hook to control how dates are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "2010-12-16". ``format_time`` ~~~~~~~~~~~~~~~ .. method:: Serializer.format_time(data): A hook to control how times are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "03:02:14". ``serialize`` ~~~~~~~~~~~~~ .. method:: Serializer.serialize(self, bundle, format='application/json', options={}): Given some data and a format, calls the correct method to serialize the data and returns the result. ``deserialize`` ~~~~~~~~~~~~~~~ .. method:: Serializer.deserialize(self, content, format='application/json'): Given some data and a format, calls the correct method to deserialize the data and returns the result. ``to_simple`` ~~~~~~~~~~~~~ .. method:: Serializer.to_simple(self, data, options): For a piece of data, attempts to recognize it and provide a simplified form of something complex. This brings complex Python data structures down to native types of the serialization format(s). ``to_etree`` ~~~~~~~~~~~~ .. method:: Serializer.to_etree(self, data, options=None, name=None, depth=0): Given some data, converts that data to an ``etree.Element`` suitable for use in the XML output. ``from_etree`` ~~~~~~~~~~~~~~ .. method:: Serializer.from_etree(self, data): Not the smartest deserializer on the planet. At the request level, it first tries to output the deserialized subelement called "object" or "objects" and falls back to deserializing based on hinted types in the XML element attribute "type". ``to_json`` ~~~~~~~~~~~ .. method:: Serializer.to_json(self, data, options=None): Given some Python data, produces JSON output. ``from_json`` ~~~~~~~~~~~~~ .. method:: Serializer.from_json(self, content): Given some JSON data, returns a Python dictionary of the decoded data. ``to_jsonp`` ~~~~~~~~~~~~ .. method:: Serializer.to_jsonp(self, data, options=None): Given some Python data, produces JSON output wrapped in the provided callback. ``to_xml`` ~~~~~~~~~~ .. method:: Serializer.to_xml(self, data, options=None): Given some Python data, produces XML output. ``from_xml`` ~~~~~~~~~~~~ .. method:: Serializer.from_xml(self, content): Given some XML data, returns a Python dictionary of the decoded data. ``to_yaml`` ~~~~~~~~~~~ .. method:: Serializer.to_yaml(self, data, options=None): Given some Python data, produces YAML output. ``from_yaml`` ~~~~~~~~~~~~~ .. method:: Serializer.from_yaml(self, content): Given some YAML data, returns a Python dictionary of the decoded data. ``to_plist`` ~~~~~~~~~~~~ .. method:: Serializer.to_plist(self, data, options=None): Given some Python data, produces binary plist output. ``from_plist`` ~~~~~~~~~~~~~~ .. method:: Serializer.from_plist(self, content): 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. django-tastypie-0.12.0/docs/settings.rst000066400000000000000000000067031240444465200202310ustar00rootroot00000000000000.. _ref-settings: ================= Tastypie Settings ================= This is a comprehensive list of the settings Tastypie recognizes. ``API_LIMIT_PER_PAGE`` ====================== **Optional** This setting controls the default number of records Tastypie will show in a list view. This is only used when a user does not specify a ``limit`` GET parameter and the ``Resource`` subclass has not overridden the number to be shown. An example:: API_LIMIT_PER_PAGE = 50 If you don't want to limit the number of records by default, you can set this setting to 0:: API_LIMIT_PER_PAGE = 0 Defaults to 20. ``TASTYPIE_FULL_DEBUG`` ======================= **Optional** This setting controls what the behavior is when an unhandled exception occurs. If set to ``True`` and ``settings.DEBUG = True``, the standard Django technical 500 is displayed. If not set or set to ``False``, Tastypie will return a serialized response. If ``settings.DEBUG`` is ``True``, you'll get the actual exception message plus a traceback. If ``settings.DEBUG`` is ``False``, Tastypie will call ``mail_admins()`` and provide a canned error message (which you can override with ``TASTYPIE_CANNED_ERROR``) in the response. An example:: TASTYPIE_FULL_DEBUG = True Defaults to ``False``. ``TASTYPIE_CANNED_ERROR`` ========================= **Optional** This setting allows you to override the canned error response when an unhandled exception is raised and ``settings.DEBUG`` is ``False``. An example:: TASTYPIE_CANNED_ERROR = "Oops, we broke it!" Defaults to ``"Sorry, this request could not be processed. Please try again later."``. ``TASTYPIE_ALLOW_MISSING_SLASH`` ================================ **Optional** This setting allows your URLs to be missing the final slash. Useful for integrating with other systems. You must also have ``settings.APPEND_SLASH = False`` so that Django does not emit HTTP 302 redirects. An example:: TASTYPIE_ALLOW_MISSING_SLASH = True Defaults to ``False``. ``TASTYPIE_DATETIME_FORMATTING`` ================================ **Optional** This setting allows you to globally choose what format your datetime/date/time data will be formatted in. Valid options are ``iso-8601``, ``iso-8601-strict`` & ``rfc-2822``. An example:: TASTYPIE_DATETIME_FORMATTING = 'rfc-2822' Defaults to ``iso-8601``. ``iso-8601`` includes microseconds if available, use ``iso-8601-strict`` to strip them. .. _settings.TASTYPIE_DEFAULT_FORMATS: ``TASTYPIE_DEFAULT_FORMATS`` ================================ **Optional** This setting allows you to globally configure the list of allowed serialization formats for your entire site. An example:: TASTYPIE_DEFAULT_FORMATS = ['json', 'xml'] Defaults to ``['json', 'xml', 'yaml', 'html', 'plist']``. ``TASTYPIE_ABSTRACT_APIKEY`` ============================ **Optional** This setting makes the ``ApiKey`` model an `abstract base class`_. This may be useful in multi-database setups where many databases each have their own table for user data and ``ApiKeyAuthentication`` is not used. Without this setting, the ``tastypie_apikey`` table would have to be created on each database containing user account data (such as Django's built-in ``auth_user`` table generated by ``django.contrib.auth.models.User``). Valid options are ``True`` & ``False``. An example:: TASTYPIE_ABSTRACT_APIKEY = True Defaults to ``False``. .. _`abstract base class`: https://docs.djangoproject.com/en/dev/topics/db/models/#abstract-base-classes django-tastypie-0.12.0/docs/testing.rst000066400000000000000000000461221240444465200200450ustar00rootroot00000000000000.. _ref-testing: ======= Testing ======= Having integrated unit tests that cover your API's behavior is important, as it helps provide verification that your API code is still valid & working 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``. .. _`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 ``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 API is behaving correctly. For the purposes of this example, we'll assume the resource in question looks like:: from tastypie.authentication import BasicAuthentication from tastypie.resources import ModelResource from entries.models import Entry class EntryResource(ModelResource): class Meta: queryset = Entry.objects.all() authentication = BasicAuthentication() An example usage might look like:: import datetime from django.contrib.auth.models import User from tastypie.test import ResourceTestCase from entries.models import Entry class EntryResourceTest(ResourceTestCase): # Use ``fixtures`` & ``urls`` as normal. See Django's ``TestCase`` # documentation for the gory details. fixtures = ['test_entries.json'] def setUp(self): super(EntryResourceTest, self).setUp() # Create a user. self.username = 'daniel' self.password = 'pass' self.user = User.objects.create_user(self.username, 'daniel@example.com', self.password) # Fetch the ``Entry`` object we'll use in testing. # Note that we aren't using PKs because they can change depending # on what other tests are running. self.entry_1 = Entry.objects.get(slug='first-post') # We also build a detail URI, since we will be using it all over. # DRY, baby. DRY. self.detail_url = '/api/v1/entry/{0}/'.format(self.entry_1.pk) # The data we'll send on POST requests. Again, because we'll use it # frequently (enough). self.post_data = { 'user': '/api/v1/user/{0}/'.format(self.user.pk), 'title': 'Second Post!', 'slug': 'second-post', 'created': '2012-05-01T22:05:12' } def get_credentials(self): return self.create_basic(username=self.username, password=self.password) def test_get_list_unauthorzied(self): self.assertHttpUnauthorized(self.api_client.get('/api/v1/entries/', format='json')) def test_get_list_json(self): resp = self.api_client.get('/api/v1/entries/', format='json', authentication=self.get_credentials()) self.assertValidJSONResponse(resp) # Scope out the data for correctness. self.assertEqual(len(self.deserialize(resp)['objects']), 12) # Here, we're checking an entire structure for the expected data. self.assertEqual(self.deserialize(resp)['objects'][0], { 'pk': str(self.entry_1.pk), 'user': '/api/v1/user/{0}/'.format(self.user.pk), 'title': 'First post', 'slug': 'first-post', 'created': '2012-05-01T19:13:42', 'resource_uri': '/api/v1/entry/{0}/'.format(self.entry_1.pk) }) def test_get_list_xml(self): self.assertValidXMLResponse(self.api_client.get('/api/v1/entries/', format='xml', authentication=self.get_credentials())) def test_get_detail_unauthenticated(self): self.assertHttpUnauthorized(self.api_client.get(self.detail_url, format='json')) def test_get_detail_json(self): resp = self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials()) self.assertValidJSONResponse(resp) # We use ``assertKeys`` here to just verify the keys, not all the data. self.assertKeys(self.deserialize(resp), ['created', 'slug', 'title', 'user']) self.assertEqual(self.deserialize(resp)['name'], 'First post') def test_get_detail_xml(self): self.assertValidXMLResponse(self.api_client.get(self.detail_url, format='xml', authentication=self.get_credentials())) def test_post_list_unauthenticated(self): self.assertHttpUnauthorized(self.api_client.post('/api/v1/entries/', format='json', data=self.post_data)) def test_post_list(self): # Check how many are there first. self.assertEqual(Entry.objects.count(), 5) self.assertHttpCreated(self.api_client.post('/api/v1/entries/', format='json', data=self.post_data, authentication=self.get_credentials())) # Verify a new one has been added. self.assertEqual(Entry.objects.count(), 6) def test_put_detail_unauthenticated(self): self.assertHttpUnauthorized(self.api_client.put(self.detail_url, format='json', data={})) def test_put_detail(self): # Grab the current data & modify it slightly. original_data = self.deserialize(self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials())) new_data = original_data.copy() new_data['title'] = 'Updated: First Post' new_data['created'] = '2012-05-01T20:06:12' self.assertEqual(Entry.objects.count(), 5) self.assertHttpAccepted(self.api_client.put(self.detail_url, format='json', data=new_data, authentication=self.get_credentials())) # Make sure the count hasn't changed & we did an update. self.assertEqual(Entry.objects.count(), 5) # Check for updated data. self.assertEqual(Entry.objects.get(pk=25).title, 'Updated: First Post') self.assertEqual(Entry.objects.get(pk=25).slug, 'first-post') self.assertEqual(Entry.objects.get(pk=25).created, datetime.datetime(2012, 3, 1, 13, 6, 12)) def test_delete_detail_unauthenticated(self): self.assertHttpUnauthorized(self.api_client.delete(self.detail_url, format='json')) def test_delete_detail(self): self.assertEqual(Entry.objects.count(), 5) self.assertHttpAccepted(self.api_client.delete(self.detail_url, format='json', authentication=self.get_credentials())) self.assertEqual(Entry.objects.count(), 4) Note that this example doesn't cover other cases, such as filtering, ``PUT`` to a list endpoint, ``DELETE`` to a list endpoint, ``PATCH`` support, etc. ``ResourceTestCase`` API Reference ---------------------------------- The ``ResourceTestCase`` exposes the following methods for use. Most are enhanced assertions or provide API-specific behaviors. ``get_credentials`` ~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.get_credentials(self) A convenience method for the user as a way to shorten up the often repetitious calls to create the same authentication. Raises ``NotImplementedError`` by default. Usage:: class MyResourceTestCase(ResourceTestCase): def get_credentials(self): return self.create_basic('daniel', 'pass') # Then the usual tests... ``create_basic`` ~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.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) Creates & returns the HTTP ``Authorization`` header for use with ``ApiKeyAuthentication``. ``create_digest`` ~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.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) Creates & returns the HTTP ``Authorization`` header for use with Oauth. ``assertHttpOK`` ~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpOK(self, resp) Ensures the response is returning a HTTP 200. ``assertHttpCreated`` ~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpCreated(self, resp) Ensures the response is returning a HTTP 201. ``assertHttpAccepted`` ~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpAccepted(self, resp) Ensures the response is returning either a HTTP 202 or a HTTP 204. ``assertHttpMultipleChoices`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpMultipleChoices(self, resp) Ensures the response is returning a HTTP 300. ``assertHttpSeeOther`` ~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpSeeOther(self, resp) Ensures the response is returning a HTTP 303. ``assertHttpNotModified`` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpNotModified(self, resp) Ensures the response is returning a HTTP 304. ``assertHttpBadRequest`` ~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpBadRequest(self, resp) Ensures the response is returning a HTTP 400. ``assertHttpUnauthorized`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpUnauthorized(self, resp) Ensures the response is returning a HTTP 401. ``assertHttpForbidden`` ~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpForbidden(self, resp) Ensures the response is returning a HTTP 403. ``assertHttpNotFound`` ~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpNotFound(self, resp) Ensures the response is returning a HTTP 404. ``assertHttpMethodNotAllowed`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpMethodNotAllowed(self, resp) Ensures the response is returning a HTTP 405. ``assertHttpConflict`` ~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpConflict(self, resp) Ensures the response is returning a HTTP 409. ``assertHttpGone`` ~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpGone(self, resp) Ensures the response is returning a HTTP 410. ``assertHttpTooManyRequests`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpTooManyRequests(self, resp) Ensures the response is returning a HTTP 429. ``assertHttpApplicationError`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpApplicationError(self, resp) Ensures the response is returning a HTTP 500. ``assertHttpNotImplemented`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertHttpNotImplemented(self, resp) Ensures the response is returning a HTTP 501. ``assertValidJSON`` ~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidJSON(self, data) Given the provided ``data`` as a string, ensures that it is valid JSON & can be loaded properly. ``assertValidXML`` ~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidXML(self, data) Given the provided ``data`` as a string, ensures that it is valid XML & can be loaded properly. ``assertValidYAML`` ~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidYAML(self, data) Given the provided ``data`` as a string, ensures that it is valid YAML & can be loaded properly. ``assertValidPlist`` ~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidPlist(self, data) Given the provided ``data`` as a string, ensures that it is valid binary plist & can be loaded properly. ``assertValidJSONResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidJSONResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/json``) * The content is valid JSON ``assertValidXMLResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidXMLResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/xml``) * The content is valid XML ``assertValidYAMLResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidYAMLResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``text/yaml``) * The content is valid YAML ``assertValidPlistResponse`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertValidPlistResponse(self, resp) Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/x-plist``) * The content is valid binary plist data ``deserialize`` ~~~~~~~~~~~~~~~ .. method:: ResourceTestCase.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. It returns a Python datastructure (typically a ``dict``) of the serialized data. ``serialize`` ~~~~~~~~~~~~~ .. method:: ResourceTestCase.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. ``assertKeys`` ~~~~~~~~~~~~~~ .. method:: ResourceTestCase.assertKeys(self, data, 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. ``TestApiClient`` API Reference ------------------------------- The ``TestApiClient`` simulates a HTTP client making calls to the API. It's important to note that it uses Django's testing infrastructure, so it's not making actual calls against a webserver. ``__init__`` ~~~~~~~~~~~~ .. method:: TestApiClient.__init__(self, serializer=None) Sets up a fresh ``TestApiClient`` instance. If you are employing a custom serializer, you can pass the class to the ``serializer=`` kwarg. ``get_content_type`` ~~~~~~~~~~~~~~~~~~~~ .. method:: TestApiClient.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). ``get`` ~~~~~~~ .. method:: TestApiClient.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:: from tastypie.test import TestApiClient client = TestApiClient() 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. 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 for details. ``post`` ~~~~~~~~ .. method:: TestApiClient.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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.post('/api/v1/entry/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 for details. ``put`` ~~~~~~~ .. method:: TestApiClient.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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.put('/api/v1/entry/1/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 for details. ``patch`` ~~~~~~~~~ .. method:: TestApiClient.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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.patch('/api/v1/entry/1/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 for details. ``delete`` ~~~~~~~~~~ .. method:: TestApiClient.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:: from tastypie.test import TestApiClient client = TestApiClient() 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. 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 for details. django-tastypie-0.12.0/docs/throttling.rst000066400000000000000000000110041240444465200205550ustar00rootroot00000000000000.. _ref-throttling: ========== Throttling ========== Sometimes, the client on the other end may request data too frequently or you have a business use case that dictates that the client should be limited to a certain number of requests per hour. For this, Tastypie includes throttling as a way to limit the number of requests in a timeframe. Usage ===== To specify a throttle, add the ``Throttle`` class to the ``Meta`` class on the ``Resource``:: from django.contrib.auth.models import User from tastypie.resources import ModelResource from tastypie.throttle import BaseThrottle class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. throttle = BaseThrottle(throttle_at=100) Throttle Options ================ Each of the ``Throttle`` classes accepts the following initialization arguments: * ``throttle_at`` - the number of requests at which the user should be throttled. Default is 150 requests. * ``timeframe`` - the length of time (in seconds) in which the user make up to the ``throttle_at`` requests. Default is 3600 seconds ( 1 hour). * ``expiration`` - the length of time to retain the times the user has accessed the api in the cache. Default is 604800 (1 week). Tastypie ships with the following ``Throttle`` classes: ``BaseThrottle`` ~~~~~~~~~~~~~~~~ The no-op throttle option, this does no throttling but implements much of the common logic and serves as an api-compatible plug. Very useful for development. ``CacheThrottle`` ~~~~~~~~~~~~~~~~~ This uses just the cache to manage throttling. Fast but prone to cache misses and/or cache restarts. ``CacheDBThrottle`` ~~~~~~~~~~~~~~~~~~~ A write-through option that uses the cache first & foremost, but also writes through to the database to persist access times. Useful for logging client accesses & with RAM-only caches. Implementing Your Own Throttle ============================== Writing a ``Throttle`` class is not quite as simple as the other components. There are two important methods, ``should_be_throttled`` & ``accessed``. The ``should_be_throttled`` method dictates whether or not the client should be throttled. The ``accessed`` method allows for the recording of the hit to the API. An example of a subclass might be:: import random from tastypie.throttle import BaseThrottle class RandomThrottle(BaseThrottle): def should_be_throttled(self, identifier, **kwargs): if random.randint(0, 10) % 2 == 0: return True return False def accessed(self, identifier, **kwargs): pass This throttle class would pick a random number between 0 & 10. If the number is even, their request is allowed through; otherwise, their request is throttled & rejected. Usage with Resource =================== Using throttling with something like search, requires that you call ``throttle_check`` and ``log_throttled_access`` explicitly. An example of this might be:: from tastypie.throttle import CacheThrottle class NoteResource(Resource): class Meta: allowed_methods = ['get'] resource_name = 'notes' throttle = CacheThrottle() def prepend_urls(self): return [ url(r"^(?P%s)/search%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_search'), name="api_get_search"), ] def search(self, request, **kwargs): self.method_check(request, allowed=self.Meta.allowed_methods) self.is_authenticated(request) self.throttle_check(request) self.log_throttled_access(request) # Do the query. sqs = SearchQuerySet().models(Note).load_all().auto_query(request.GET.get('q', '')) paginator = Paginator(sqs, 20) try: page = paginator.page(int(request.GET.get('page', 1))) except InvalidPage: raise Http404("Sorry, no results on that page.") objects = [] for result in page.object_list: bundle = self.build_bundle(obj=result.object, request=request) bundle = self.full_dehydrate(bundle) objects.append(bundle) object_list = { 'objects': objects, } return self.create_response(request, object_list) django-tastypie-0.12.0/docs/toc.rst000066400000000000000000000007411240444465200171520ustar00rootroot00000000000000Table Of Contents ================= .. toctree:: :maxdepth: 2 index tutorial interacting settings non_orm_data_sources tools testing compatibility_notes python3 resources bundles api fields caching validation authentication authorization serialization throttling paginator geodjango content_types cookbook debugging who_uses contributing Indices and tables ================== * :ref:`search` django-tastypie-0.12.0/docs/tools.rst000066400000000000000000000075261240444465200175350ustar00rootroot00000000000000.. _ref-tools: ===== Tools ===== Here are some tools that might help in interacting with the API that Tastypie provides: Browser ======= JSONView -------- * Firefox - https://addons.mozilla.org/en-US/firefox/addon/jsonview/ * Chrome - https://chrome.google.com/webstore/detail/chklaanhfefbnpoihckbnefhakgolnmc A plugin (actually two different ones that closely mirror each other) that nicely reformats JSON data in the browser. Postman - Rest Client --------------------- * Chrome - https://chrome.google.com/webstore/detail/fdmmgilgnpjigdojojpjoooidkmcomcm A feature rich Chrome extension with JSON and XML support Extensions ========== Tastypie-msgpack ---------------- https://github.com/stephenmcd/tastypie-msgpack Adds MsgPack_ support to Tastypie's serializer. .. _MsgPack: http://msgpack.org/ Python ====== Slumber ------- http://slumber.in/ Slumber is a small Python library that makes it easy to access & work with APIs. It works for many others, but works especially well with Tastypie. Hammock ------- https://github.com/kadirpekel/hammock Hammock is a fun module lets you deal with rest APIs by converting them into dead simple programmatic APIs. It uses popular ``requests`` module in backyard to provide full-fledged rest experience. Here is what it looks like:: >>> import hammock >>> api = hammock.Hammock('http://localhost:8000') >>> api.users('foo').posts('bar').comments.GET() drest ----- http://drest.rtfd.org/ drest is another small Python library. It focuses on extensibility & can also work with many different API, with an emphasis on Tastypie. httpie ------ https://github.com/jkbr/httpie HTTPie is a command line HTTP client written in Python. Its goal is to make command-line interaction with web services as human-friendly as possible and allows much conciser statements compared with curl. For example for POSTing a JSON object you simply call: $ http localhost:8000/api/v1/entry/ title="Foo" body="Bar" user="/api/v1/user/1/" Now compare this with curl: $ curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"title": "Foo", "body": "Bar", "user": "/api/v1/user/1/"}' http://localhost:8000/api/v1/entry/ json.tool --------- Included with Python, this tool makes reformatting JSON easy. For example:: $ curl http://localhost:8000/api/v1/note/ | python -m json.tool Will return nicely reformatted data like:: { "meta": { "total_count": 1 }, "objects": [ { "content": "Hello world!", "user": "/api/v1/user/1/" } ] } django-permissionsx ------------------- https://github.com/thinkingpotato/django-permissionsx This package allows using one set of rules both for Django class-based views] and Tastypie authorization. For example: **articles/permissions.py**:: class StaffPermissions(Permissions): permissions = P(profile__is_editor=True) | P(profile__is_administrator=True) **articles/views.py**:: class ArticleDeleteView(PermissionsViewMixin, DeleteView): model = Article success_url = reverse_lazy('article_list') permissions = StaffPermissions **articles/api.py**:: class StaffOnlyAuthorization(TastypieAuthorization): permissions_class = StaffPermissions django-superbulk ---------------- https://github.com/thelonecabbage/django-superbulk This app adds bulk operation support to any Django view-based app, allowing for better transactional behavior. Javascript ========== backbone-tastypie ----------------- https://github.com/PaulUithol/backbone-tastypie A small layer that makes Backbone & Tastypie plan nicely together. backbone-relational ------------------- https://github.com/PaulUithol/Backbone-relational/ Allows Backbone to work with relational data, like the kind of data Tastypie provides. django-tastypie-0.12.0/docs/tutorial.rst000066400000000000000000000311071240444465200202300ustar00rootroot00000000000000.. _ref-tutorial: ============================= Getting Started with Tastypie ============================= Tastypie is a reusable app (that is, it relies only on its own code and focuses on providing just a REST-style API) and is suitable for providing an API to any application without having to modify the sources of that app. Not everyone's needs are the same, so Tastypie goes out of its way to provide plenty of hooks for overriding or extending how it works. .. note:: If you hit a stumbling block, you can join `#tastypie on irc.freenode.net`_ to get help. .. _#tastypie on irc.freenode.net: irc://irc.freenode.net/tastypie This tutorial assumes that you have a basic understanding of Django as well as how proper REST-style APIs ought to work. We will only explain the portions of the code that are Tastypie-specific in any kind of depth. For example purposes, we'll be adding an API to a simple blog application. Here is ``myapp/models.py``:: from tastypie.utils.timezone import now from django.contrib.auth.models import User from django.db import models from django.utils.text import slugify class Entry(models.Model): user = models.ForeignKey(User) pub_date = models.DateTimeField(default=now) title = models.CharField(max_length=200) slug = models.SlugField() body = models.TextField() def __unicode__(self): return self.title def save(self, *args, **kwargs): # For automatic slug generation. if not self.slug: self.slug = slugify(self.title)[:50] return super(Entry, self).save(*args, **kwargs) With that, we'll move on to installing and configuring Tastypie. Installation ============ Installing Tastypie is as simple as checking out the source and adding it to your project or ``PYTHONPATH``. 1. Download the dependencies: * Python 2.6+ or Python 3.3+ * Django 1.5+ * ``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 * **OPTIONAL** - ``pyyaml`` (http://pyyaml.org/) if using the YAML serializer 2. Either check out tastypie from GitHub_ or to pull a release off PyPI_. Doing ``sudo pip install django-tastypie`` or ``sudo easy_install django-tastypie`` is all that should be required. 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 .. _PyPI: http://pypi.python.org/pypi/django-tastypie Configuration ============= The only mandatory configuration is adding ``'tastypie'`` to your ``INSTALLED_APPS``. This isn't strictly necessary, as Tastypie has only two non-required models, but may ease usage. You have the option to set up a number of settings (see :doc:`settings`) but they all have sane defaults and are not required unless you need to tweak their values. Creating Resources ================== REST-style architecture talks about resources, so unsurprisingly integrating with Tastypie involves creating :class:`~tastypie.resources.Resource` classes. For our simple application, we'll create a file for these in ``myapp/api.py``, though they can live anywhere in your application:: # myapp/api.py from tastypie.resources import ModelResource from myapp.models import Entry class EntryResource(ModelResource): class Meta: queryset = Entry.objects.all() resource_name = 'entry' This class, by virtue of being a :class:`~tastypie.resources.ModelResource` subclass, will introspect all non-relational fields on the ``Entry`` model and create its own :mod:`ApiFields ` that map to those fields, much like the way Django's ``ModelForm`` class introspects. .. note:: The ``resource_name`` within the ``Meta`` class is optional. If not provided, it is automatically generated off the classname, removing any instances of :class:`~tastypie.resources.Resource` and lowercasing the string. So ``EntryResource`` would become just ``entry``. We've included the ``resource_name`` attribute in this example for clarity, especially when looking at the URLs, but you should feel free to omit it if you're comfortable with the automatic behavior. Hooking Up The Resource(s) ========================== Now that we have our ``EntryResource``, we can hook it up in our URLconf. To do this, we simply instantiate the resource in our URLconf and hook up its ``urls``:: # urls.py from django.conf.urls.defaults import * from myapp.api import EntryResource entry_resource = EntryResource() urlpatterns = patterns('', # The normal jazz here... (r'^blog/', include('myapp.urls')), (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 list of ``Entry``-like objects. .. note:: The ``?format=json`` is an override required to make things look decent in the browser (accept headers vary between browsers). Tastypie properly handles the ``Accept`` header. So the following will work properly:: curl -H "Accept: application/json" http://127.0.0.1:8000/api/entry/ But if you're sure you want something else (or want to test in a browser), Tastypie lets you specify ``?format=...`` when you really want to force a certain type. At this point, a bunch of other URLs are also available. Try out any/all of the following (assuming you have at least three records in the database): * http://127.0.0.1:8000/api/entry/?format=json * http://127.0.0.1:8000/api/entry/1/?format=json * http://127.0.0.1:8000/api/entry/schema/?format=json * http://127.0.0.1:8000/api/entry/set/1;3/?format=json However, if you try sending a POST/PUT/DELETE to the resource, you find yourself getting "401 Unauthorized" errors. For safety, Tastypie ships with the ``authorization`` class ("what are you allowed to do") set to ``ReadOnlyAuthorization``. This makes it safe to expose on the web, but prevents us from doing POST/PUT/DELETE. Let's enable those:: # myapp/api.py from tastypie.authorization import Authorization from tastypie.resources import ModelResource from myapp.models import Entry class EntryResource(ModelResource): class Meta: queryset = Entry.objects.all() resource_name = 'entry' authorization= Authorization() .. warning:: This is now great for testing in development but **VERY INSECURE**. You should never put a ``Resource`` like this out on the internet. Please spend some time looking at the authentication/authorization classes available in Tastypie. With just nine lines of code, we have a full working REST interface to our ``Entry`` model. In addition, full GET/POST/PUT/DELETE support is already there, so it's possible to really work with all of the data. Well, *almost*. You see, you'll note that not quite all of our data is there. Markedly absent is the ``user`` field, which is a ``ForeignKey`` to Django's ``User`` model. Tastypie does **NOT** introspect related data because it has no way to know how you want to represent that data. And since that relation isn't there, any attempt to POST/PUT new data will fail, because no ``user`` is present, which is a required field on the model. This is easy to fix, but we'll need to flesh out our API a little more. Creating More Resources ======================= In order to handle our ``user`` relation, we'll need to create a ``UserResource`` and tell the ``EntryResource`` to use it. So we'll modify ``myapp/api.py`` to match the following code:: # myapp/api.py from django.contrib.auth.models import User from tastypie import fields from tastypie.resources import ModelResource from myapp.models import Entry class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'user' class EntryResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: queryset = Entry.objects.all() resource_name = 'entry' We simply created a new :class:`~tastypie.resources.ModelResource` subclass called ``UserResource``. Then we added a field to ``EntryResource`` that specified that the ``user`` field points to a ``UserResource`` for that data. Now we should be able to get all of the fields back in our response. But since we have another full, working resource on our hands, we should hook that up to our API as well. And there's a better way to do it. Adding To The Api ================= Tastypie ships with an :class:`~tastypie.api.Api` class, which lets you bind multiple :class:`Resources ` together to form a coherent API. Adding it to the mix is simple. We'll go back to our URLconf (``urls.py``) and change it to match the following:: # urls.py from django.conf.urls.defaults import * from tastypie.api import Api from myapp.api import EntryResource, UserResource v1_api = Api(api_name='v1') v1_api.register(UserResource()) v1_api.register(EntryResource()) urlpatterns = patterns('', # The normal jazz here... (r'^blog/', include('myapp.urls')), (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 that we've modified the urls to now point to ``v1_api.urls``. This makes even more data accessible, so if we start up the ``runserver`` again, the following URLs should work: * http://127.0.0.1:8000/api/v1/?format=json * http://127.0.0.1:8000/api/v1/user/?format=json * http://127.0.0.1:8000/api/v1/user/1/?format=json * http://127.0.0.1:8000/api/v1/user/schema/?format=json * http://127.0.0.1:8000/api/v1/user/set/1;3/?format=json * http://127.0.0.1:8000/api/v1/entry/?format=json * http://127.0.0.1:8000/api/v1/entry/1/?format=json * http://127.0.0.1:8000/api/v1/entry/schema/?format=json * http://127.0.0.1:8000/api/v1/entry/set/1;3/?format=json Additionally, the representations out of ``EntryResource`` will now include the ``user`` field and point to an endpoint like ``/api/v1/users/1/`` to access that user's data. And full POST/PUT delete support should now work. But there's several new problems. One is that our new ``UserResource`` leaks too much data, including fields like ``email``, ``password``, ``is_active`` and ``is_staff``. Another is that we may not want to allow end users to alter ``User`` data. Both of these problems are easily fixed as well. Limiting Data And Access ======================== Cutting out the ``email``, ``password``, ``is_active`` and ``is_staff`` fields is easy to do. We simply modify our ``UserResource`` code to match the following:: class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'user' excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser'] The ``excludes`` directive tells ``UserResource`` which fields not to include in the output. If you'd rather whitelist fields, you could do:: class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'user' fields = ['username', 'first_name', 'last_name', 'last_login'] Now that the undesirable fields are no longer included, we can look at limiting access. This is also easy and involves making our ``UserResource`` look like:: class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'user' excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser'] allowed_methods = ['get'] Now only HTTP GET requests will be allowed on ``/api/v1/user/`` endpoints. If you require more granular control, both ``list_allowed_methods`` and ``detail_allowed_methods`` options are supported. Beyond The Basics ================= We now have a full working API for our application. But Tastypie supports many more features, like: * :doc:`authentication` * :doc:`authorization` * :doc:`caching` * :doc:`throttling` * :doc:`resources` (filtering & sorting) * :doc:`serialization` Tastypie is also very easy to override and extend. For some common patterns and approaches, you should refer to the :doc:`cookbook` documentation. django-tastypie-0.12.0/docs/validation.rst000066400000000000000000000070141240444465200205170ustar00rootroot00000000000000.. _ref-validation: ========== Validation ========== Validation allows you to ensure that the data being submitted by the user is appropriate for storage. This can range from simple type checking on up to complex validation that compares different fields together. If the data is valid, an empty dictionary is returned and processing continues as normal. If the data is invalid, a dictionary of error messages (keys being the field names, values being a list of error messages) is immediately returned to the user, serialized in the format they requested. Usage ===== Using these classes is simple. Simply provide them (or your own class) as a ``Meta`` option to the ``Resource`` in question. For example:: from django.contrib.auth.models import User from tastypie.validation import Validation from tastypie.resources import ModelResource class UserResource(ModelResource): class Meta: queryset = User.objects.all() resource_name = 'auth/user' excludes = ['email', 'password', 'is_superuser'] # Add it here. validation = Validation() Validation Options ================== Tastypie ships with the following ``Validation`` classes: ``Validation`` ~~~~~~~~~~~~~~ The no-op validation option, the data submitted is always considered to be valid. This is the default class hooked up to ``Resource/ModelResource``. ``FormValidation`` ~~~~~~~~~~~~~~~~~~ A more complex form of validation, this class accepts a ``form_class`` argument to its constructor. You supply a Django ``Form`` (or ``ModelForm``, though ``save`` will never get called) and Tastypie will verify the ``data`` in the ``Bundle`` against the form. This class **DOES NOT** alter the data sent, only verifies it. If you want to alter the data, please use the ``CleanedDataFormValidation`` class instead. .. warning:: Data in the bundle must line up with the fieldnames in the ``Form``. If they do not, you'll need to either munge the data or change your form. Usage looks like:: from django import forms 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() form = FormValidation(form_class=NoteForm) ``CleanedDataFormValidation`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to the ``FormValidation`` class, this uses a Django ``Form`` to handle validation. **However**, it will use the ``form.cleaned_data`` to replace the ``bundle`` data sent by user! Usage is identical to ``FormValidation``. Implementing Your Own Validation ================================ Implementing your own ``Validation`` classes is a simple process. The constructor can take whatever ``**kwargs`` it needs (if any). The only other method to implement is the ``is_valid`` method:: from tastypie.validation import Validation class AwesomeValidation(Validation): def is_valid(self, bundle, request=None): if not bundle.data: return {'__all__': 'Not quite what I had in mind.'} errors = {} for key, value in bundle.data.items(): if not isinstance(value, basestring): continue if not 'awesome' in value: errors[key] = ['NOT ENOUGH AWESOME. NEEDS MORE.'] return errors Under this validation, every field that's a string is checked for the word 'awesome'. If it's not in the string, it's an error. django-tastypie-0.12.0/docs/who_uses.rst000066400000000000000000000034441240444465200202240ustar00rootroot00000000000000.. _ref-who-uses: Sites Using Tastypie ==================== The following sites are a partial list of people using Tastypie. I'm always interested in adding more sites, so please find me (``daniellindsley``) via IRC or start a mailing list thread. LJWorld Marketplace ------------------- * http://www2.ljworld.com/marketplace/api/v1/?format=json Forkinit -------- Read-only API access to recipes. * http://forkinit.com/ * http://forkinit.com/api/v1/?format=json Read The Docs ------------- A hosted documentation site, primarily for Python docs. General purpose read-write access. * http://readthedocs.org/ * http://readthedocs.org/api/v1/?format=json Luzme ----- An e-book search site that lets you fetch pricing information. * http://luzme.com/ * http://luzme.readthedocs.org/en/latest/ Politifact ---------- To power their mobile (iPhone/Android/Playbook) applications. * http://www.politifact.com/mobile/ LocalWiki --------- `LocalWiki `_ is a tool for collaborating in local, geographic communities. It's using Tastypie to provide an geospatially-aware REST API. * http://localwiki.readthedocs.org/en/latest/api.html * http://localwiki.org/blog/2012/aug/31/localwiki-api-released/ I-Am-CC.org ----------- `I-Am-CC.org `_ is a tool for releasing Instagram photos under a Creative Commons license. * http://i-am-cc.org/api/?format=json Dbpatterns ---------- Dbpatterns is a service that allows you to create, share, explore database models on the web. * http://dbpatterns.com CourtListener ------------- Read-only API providing 2.5M legal opinions and other judicial data via Solr/Sunburnt and Postgres (Django models). * Site: https://www.courtlistener.com * Code : https://bitbucket.org/mlissner/search-and-awareness-platform-courtlistener/src django-tastypie-0.12.0/requirements.txt000066400000000000000000000002151240444465200201630ustar00rootroot00000000000000python-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 django-tastypie-0.12.0/setup.cfg000066400000000000000000000000261240444465200165200ustar00rootroot00000000000000[wheel] universal = 1 django-tastypie-0.12.0/setup.py000066400000000000000000000031131240444465200164110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- try: from setuptools import setup except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup setup( name='django-tastypie', version='0.12.0', description='A flexible & capable API layer for Django.', author='Daniel Lindsley', author_email='daniel@toastdriven.com', url='http://github.com/toastdriven/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', 'tastypie.contrib.contenttypes', ], package_data={ 'tastypie': ['templates/tastypie/*'], }, zip_safe=False, requires=[ 'python_mimeparse(>=0.1.4)', 'dateutil(>=1.5, !=2.0)', ], install_requires=[ 'python-mimeparse >= 0.1.4', 'python-dateutil >= 1.5, != 2.0', ], tests_require=['mock', 'PyYAML', 'lxml', 'defusedxml'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Utilities' ], ) django-tastypie-0.12.0/tastypie/000077500000000000000000000000001240444465200165435ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/__init__.py000066400000000000000000000001731240444465200206550ustar00rootroot00000000000000from __future__ import unicode_literals __author__ = 'Daniel Lindsley & the Tastypie core team' __version__ = (0, 12, 0) django-tastypie-0.12.0/tastypie/admin.py000066400000000000000000000011421240444465200202030ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings from django.contrib import admin if 'django.contrib.auth' in settings.INSTALLED_APPS: from tastypie.models import ApiKey class ApiKeyInline(admin.StackedInline): model = ApiKey extra = 0 ABSTRACT_APIKEY = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False) if ABSTRACT_APIKEY and not isinstance(ABSTRACT_APIKEY, bool): raise TypeError("'TASTYPIE_ABSTRACT_APIKEY' must be either 'True' " "or 'False'.") if not ABSTRACT_APIKEY: admin.site.register(ApiKey) django-tastypie-0.12.0/tastypie/api.py000066400000000000000000000151651240444465200176760ustar00rootroot00000000000000from __future__ import unicode_literals import warnings from django.conf.urls import url, patterns, 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.mime import determine_format, build_content_type class Api(object): """ Implements a registry to tie together the various resources that make up an API. Especially useful for navigation, HATEOAS and for providing multiple versions of your API. Optionally supplying ``api_name`` allows you to name the API. Generally, this is done with version numbers (i.e. ``v1``, ``v2``, etc.) but can be named any string. """ def __init__(self, api_name="v1", serializer_class=Serializer): self.api_name = api_name self._registry = {} self._canonicals = {} self.serializer = serializer_class() def register(self, resource, canonical=True): """ Registers an instance of a ``Resource`` subclass with the API. Optionally accept a ``canonical`` argument, which indicates that the resource being registered is the canonical variant. Defaults to ``True``. """ resource_name = getattr(resource._meta, 'resource_name', None) if resource_name is None: raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource) self._registry[resource_name] = resource if canonical is True: if resource_name in self._canonicals: warnings.warn("A new resource '%r' is replacing the existing canonical URL for '%s'." % (resource, resource_name), Warning, stacklevel=2) self._canonicals[resource_name] = resource # TODO: This is messy, but makes URI resolution on FK/M2M fields # work consistently. resource._meta.api_name = self.api_name resource.__class__.Meta.api_name = self.api_name def unregister(self, resource_name): """ If present, unregisters a resource from the API. """ if resource_name in self._registry: del(self._registry[resource_name]) if resource_name in self._canonicals: del(self._canonicals[resource_name]) def canonical_resource_for(self, resource_name): """ Returns the canonical resource for a given ``resource_name``. """ if resource_name in self._canonicals: return self._canonicals[resource_name] raise NotRegistered("No resource was registered as canonical for '%s'." % resource_name) def wrap_view(self, view): def wrapper(request, *args, **kwargs): try: return getattr(self, view)(request, *args, **kwargs) except BadRequest: return HttpResponseBadRequest() return wrapper def override_urls(self): """ Deprecated. Will be removed by v1.0.0. Please use ``prepend_urls`` instead. """ return [] def prepend_urls(self): """ A hook for adding your own URLs or matching before the default URLs. """ return [] @property def urls(self): """ Provides URLconf details for the ``Api`` and all registered ``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), ] 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))) urlpatterns = self.prepend_urls() overridden_urls = self.override_urls() if overridden_urls: 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 ) return urlpatterns def top_level(self, request, api_name=None): """ A view that returns a serialized list of all resources registers to the ``Api``. Useful for discovery. """ 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={ 'api_name': api_name, 'resource_name': name, }), 'schema': self._build_reverse_url("api_get_schema", kwargs={ 'api_name': api_name, 'resource_name': name, }), } desired_format = determine_format(request, self.serializer) options = {} if 'text/javascript' in desired_format: callback = request.GET.get('callback', 'callback') if not is_valid_jsonp_callback_value(callback): raise BadRequest('JSONP callback name is invalid.') options['callback'] = callback serialized = self.serializer.serialize(available_resources, desired_format, options) return HttpResponse(content=serialized, content_type=build_content_type(desired_format)) def _build_reverse_url(self, name, args=None, kwargs=None): """ A convenience hook for overriding how URLs are built. See ``NamespacedApi._build_reverse_url`` for an example. """ return reverse(name, args=args, kwargs=kwargs) class NamespacedApi(Api): """ An API subclass that respects Django namespaces. """ def __init__(self, api_name="v1", urlconf_namespace=None, **kwargs): super(NamespacedApi, self).__init__(api_name=api_name, **kwargs) self.urlconf_namespace = urlconf_namespace def register(self, resource, canonical=True): super(NamespacedApi, self).register(resource, canonical=canonical) if canonical is True: # Plop in the namespace here as well. resource._meta.urlconf_namespace = self.urlconf_namespace def _build_reverse_url(self, name, args=None, kwargs=None): namespaced = "%s:%s" % (self.urlconf_namespace, name) return reverse(namespaced, args=args, kwargs=kwargs) django-tastypie-0.12.0/tastypie/authentication.py000066400000000000000000000420621240444465200221400ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import hmac import time import uuid from django.conf import settings 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.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 try: import python_digest except ImportError: python_digest = None try: import oauth2 except ImportError: oauth2 = None try: import oauth_provider except ImportError: oauth_provider = None class Authentication(object): """ A simple base class to establish the protocol for auth. By default, this indicates the user is always authenticated. """ def __init__(self, require_active=True): self.require_active = require_active def is_authenticated(self, request, **kwargs): """ Identifies if the user is authenticated to continue or not. Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ return True def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns a combination of IP address and hostname. """ return "%s_%s" % (request.META.get('REMOTE_ADDR', 'noaddr'), request.META.get('REMOTE_HOST', 'nohost')) def check_active(self, user): """ Ensures the user has an active account. Optimized for the ``django.contrib.auth.models.User`` case. """ if not self.require_active: # Ignore & move on. return True return user.is_active class BasicAuthentication(Authentication): """ Handles HTTP Basic auth against a specific auth backend if provided, or against all configured authentication backends using the ``authenticate`` method from ``django.contrib.auth``. Optional keyword arguments: ``backend`` If specified, use a specific ``django.contrib.auth`` backend instead of checking all backends specified in the ``AUTHENTICATION_BACKENDS`` setting. ``realm`` The realm to use in the ``HttpUnauthorized`` response. Default: ``django-tastypie``. """ def __init__(self, backend=None, realm='django-tastypie', **kwargs): super(BasicAuthentication, self).__init__(**kwargs) self.backend = backend self.realm = realm def _unauthorized(self): response = HttpUnauthorized() # FIXME: Sanitize realm. response['WWW-Authenticate'] = 'Basic Realm="%s"' % self.realm return response def is_authenticated(self, request, **kwargs): """ Checks a user's basic auth credentials against the current Django auth backend. 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: return self._unauthorized() bits = user_pass.split(':', 1) if len(bits) != 2: return self._unauthorized() if self.backend: user = self.backend.authenticate(username=bits[0], password=bits[1]) else: user = authenticate(username=bits[0], password=bits[1]) if user is None: return self._unauthorized() if not self.check_active(user): return False request.user = user return True def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns the user's basic auth username. """ return request.META.get('REMOTE_USER', 'nouser') class ApiKeyAuthentication(Authentication): """ Handles API key auth, in which a user provides a username & API key. Uses the ``ApiKey`` model that ships with tastypie. If you wish to use a different model, override the ``get_key`` method to perform the key check as suits your needs. """ 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: username = request.GET.get('username') or request.POST.get('username') api_key = request.GET.get('api_key') or request.POST.get('api_key') return username, api_key def is_authenticated(self, request, **kwargs): """ Finds the user and checks their API key. 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: return self._unauthorized() if not username or not api_key: return self._unauthorized() username_field = get_username_field() User = get_user_model() try: lookup_kwargs = {username_field: username} user = User.objects.get(**lookup_kwargs) except (User.DoesNotExist, User.MultipleObjectsReturned): return self._unauthorized() if not self.check_active(user): return False key_auth_check = self.get_key(user, api_key) if key_auth_check and not isinstance(key_auth_check, HttpUnauthorized): request.user = user return key_auth_check def get_key(self, user, api_key): """ Attempts to find the API key for the user. Uses ``ApiKey`` by default but can be overridden. """ from tastypie.models import ApiKey try: ApiKey.objects.get(user=user, key=api_key) except ApiKey.DoesNotExist: return self._unauthorized() return True def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns the user's username. """ username, api_key = self.extract_credentials(request) return username or 'nouser' class SessionAuthentication(Authentication): """ An authentication mechanism that piggy-backs on Django sessions. This is useful when the API is talking to Javascript on the same site. Relies on the user being logged in through the standard Django login setup. Requires a valid CSRF token. """ def is_authenticated(self, request, **kwargs): """ Checks to make sure the user is logged in & has a Django session. """ # Cargo-culted from Django 1.3/1.4's ``django/middleware/csrf.py``. # We can't just use what's there, since the return values will be # wrong. # We also can't risk accessing ``request.POST``, which will break with # the serialized bodies. if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): return request.user.is_authenticated() if getattr(request, '_dont_enforce_csrf_checks', False): return request.user.is_authenticated() csrf_token = _sanitize_token(request.COOKIES.get(settings.CSRF_COOKIE_NAME, '')) if request.is_secure(): referer = request.META.get('HTTP_REFERER') if referer is None: return False good_referer = 'https://%s/' % request.get_host() if not same_origin(referer, good_referer): return False request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') if not constant_time_compare(request_csrf_token, csrf_token): return False return request.user.is_authenticated() def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns the user's username. """ return getattr(request.user, get_username_field()) class DigestAuthentication(Authentication): """ Handles HTTP Digest auth against a specific auth backend if provided, or against all configured authentication backends using the ``authenticate`` method from ``django.contrib.auth``. However, instead of the user's password, their API key should be used. Optional keyword arguments: ``backend`` If specified, use a specific ``django.contrib.auth`` backend instead of checking all backends specified in the ``AUTHENTICATION_BACKENDS`` setting. ``realm`` The realm to use in the ``HttpUnauthorized`` response. Default: ``django-tastypie``. """ def __init__(self, backend=None, realm='django-tastypie', **kwargs): super(DigestAuthentication, self).__init__(**kwargs) self.backend = backend self.realm = realm if python_digest is None: raise ImproperlyConfigured("The 'python_digest' package could not be imported. It is required for use with the 'DigestAuthentication' class.") def _unauthorized(self): response = HttpUnauthorized() new_uuid = uuid.uuid4() 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', ''), realm=self.realm, opaque=opaque, stale=False ) return response def is_authenticated(self, request, **kwargs): """ Finds the user and checks their API key. 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: 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', '')): return self._unauthorized() user = self.get_user(digest_response.username) api_key = self.get_key(user) if user is False or api_key is False: return self._unauthorized() expected = python_digest.calculate_request_digest( request.method, python_digest.calculate_partial_digest(digest_response.username, self.realm, api_key), digest_response) if not digest_response.response == expected: return self._unauthorized() if not self.check_active(user): return False request.user = user return True def get_user(self, username): username_field = get_username_field() User = get_user_model() try: lookup_kwargs = {username_field: username} user = User.objects.get(**lookup_kwargs) except (User.DoesNotExist, User.MultipleObjectsReturned): return False return user def get_key(self, user): """ Attempts to find the API key for the user. Uses ``ApiKey`` by default but can be overridden. Note that this behaves differently than the ``ApiKeyAuthentication`` method of the same name. """ from tastypie.models import ApiKey try: key = ApiKey.objects.get(user=user) except ApiKey.DoesNotExist: return False return key.key def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns the user's username. """ if hasattr(request, 'user'): if hasattr(request.user, 'username'): return request.user.username return 'nouser' class OAuthAuthentication(Authentication): """ Handles OAuth, which checks a user's credentials against a separate service. Currently verifies against OAuth 1.0a services. This does *NOT* provide OAuth authentication in your API, strictly consumption. """ def __init__(self, **kwargs): super(OAuthAuthentication, self).__init__(**kwargs) if oauth2 is None: raise ImproperlyConfigured("The 'python-oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") if oauth_provider is None: raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") def is_authenticated(self, request, **kwargs): from oauth_provider.store import store, InvalidTokenError if self.is_valid_request(request): oauth_request = oauth_provider.utils.get_oauth_request(request) consumer = store.get_consumer(request, oauth_request, oauth_request.get_parameter('oauth_consumer_key')) try: token = store.get_access_token(request, oauth_request, consumer, oauth_request.get_parameter('oauth_token')) except oauth_provider.store.InvalidTokenError: return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid access token: %s') % oauth_request.get_parameter('oauth_token'))) try: self.validate_token(request, consumer, token) except oauth2.Error as e: return oauth_provider.utils.send_oauth_error(e) if consumer and token: if not self.check_active(token.user): return False request.user = token.user return True return oauth_provider.utils.send_oauth_error(oauth2.Error(_('You are not allowed to access this resource.'))) return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid request parameters.'))) def is_in(self, params): """ Checks to ensure that all the OAuth parameter names are in the provided ``params``. """ from oauth_provider.consts import OAUTH_PARAMETERS_NAMES for param_name in OAUTH_PARAMETERS_NAMES: if param_name not in params: return False return True def is_valid_request(self, request): """ Checks whether the required parameters are either in the HTTP ``Authorization`` header sent by some clients (the preferred method according to OAuth spec) or fall back to ``GET/POST``. """ auth_params = request.META.get("HTTP_AUTHORIZATION", []) return self.is_in(auth_params) or self.is_in(request.REQUEST) def validate_token(self, request, consumer, token): oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request) return oauth_server.verify_request(oauth_request, consumer, token) class MultiAuthentication(object): """ An authentication backend that tries a number of backends in order. """ def __init__(self, *backends, **kwargs): super(MultiAuthentication, self).__init__(**kwargs) self.backends = backends def is_authenticated(self, request, **kwargs): """ Identifies if the user is authenticated to continue or not. Should return either ``True`` if allowed, ``False`` if not or an ``HttpResponse`` if you need something custom. """ unauthorized = False for backend in self.backends: check = backend.is_authenticated(request, **kwargs) if check: if isinstance(check, HttpUnauthorized): unauthorized = unauthorized or check else: request._authentication_backend = backend return check return unauthorized def get_identifier(self, request): """ Provides a unique string identifier for the requestor. This implementation returns a combination of IP address and hostname. """ try: return request._authentication_backend.get_identifier(request) except AttributeError: return 'nouser' django-tastypie-0.12.0/tastypie/authorization.py000066400000000000000000000167171240444465200220310ustar00rootroot00000000000000from __future__ import unicode_literals from tastypie.exceptions import TastypieError, Unauthorized class Authorization(object): """ A base class that provides no permissions checking. """ def __get__(self, instance, owner): """ Makes ``Authorization`` a descriptor of ``ResourceOptions`` and creates a reference to the ``ResourceOptions`` object that may be used by methods of ``Authorization``. """ 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. Should return an empty list if none are allowed. Returns the entire list by default. """ return object_list def read_detail(self, object_list, bundle): """ Returns either ``True`` if the user is allowed to read the object in question or throw ``Unauthorized`` if they are not. Returns ``True`` by default. """ return True def create_list(self, object_list, bundle): """ Unimplemented, as Tastypie never creates entire new lists, but present for consistency & possible extension. """ raise NotImplementedError("Tastypie has no way to determine if all objects should be allowed to be created.") def create_detail(self, object_list, bundle): """ Returns either ``True`` if the user is allowed to create the object in question or throw ``Unauthorized`` if they are not. Returns ``True`` by default. """ return True def update_list(self, object_list, bundle): """ Returns a list of all the objects a user is allowed to update. Should return an empty list if none are allowed. Returns the entire list by default. """ return object_list def update_detail(self, object_list, bundle): """ Returns either ``True`` if the user is allowed to update the object in question or throw ``Unauthorized`` if they are not. Returns ``True`` by default. """ return True def delete_list(self, object_list, bundle): """ Returns a list of all the objects a user is allowed to delete. Should return an empty list if none are allowed. Returns the entire list by default. """ return object_list def delete_detail(self, object_list, bundle): """ Returns either ``True`` if the user is allowed to delete the object in question or throw ``Unauthorized`` if they are not. Returns ``True`` by default. """ return True class ReadOnlyAuthorization(Authorization): """ Default Authentication class for ``Resource`` objects. Only allows ``GET`` requests. """ def read_list(self, object_list, bundle): return object_list def read_detail(self, object_list, bundle): return True def create_list(self, object_list, bundle): return [] def create_detail(self, object_list, bundle): raise Unauthorized("You are not allowed to access that resource.") def update_list(self, object_list, bundle): return [] def update_detail(self, object_list, bundle): raise Unauthorized("You are not allowed to access that resource.") def delete_list(self, object_list, bundle): return [] def delete_detail(self, object_list, bundle): raise Unauthorized("You are not allowed to access that resource.") class DjangoAuthorization(Authorization): """ Uses permission checking from ``django.contrib.auth`` to map ``POST / PUT / DELETE / PATCH`` to their equivalent Django auth permissions. 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. """ 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): return False # User must be logged in to check permissions. if not hasattr(request, 'user'): return False return model_klass def read_list(self, object_list, bundle): klass = self.base_checks(bundle.request, object_list.model) if klass is False: return [] # GET-style methods are always allowed. return object_list def read_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.") # GET-style methods are always allowed. return True def create_list(self, object_list, bundle): klass = self.base_checks(bundle.request, object_list.model) if klass is False: return [] permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name) if not bundle.request.user.has_perm(permission): return [] return 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 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 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 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 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 django-tastypie-0.12.0/tastypie/bundle.py000066400000000000000000000020741240444465200203710ustar00rootroot00000000000000from __future__ import unicode_literals from django.http import HttpRequest # In a separate file to avoid circular imports... class Bundle(object): """ A small container for instances and converted data for the ``dehydrate/hydrate`` cycle. Necessary because the ``dehydrate/hydrate`` cycle needs to access data at different points. """ def __init__(self, obj=None, data=None, request=None, related_obj=None, related_name=None, objects_saved=None, related_objects_to_save=None, ): self.obj = obj self.data = data or {} self.request = request or HttpRequest() self.related_obj = related_obj self.related_name = related_name self.errors = {} self.objects_saved = objects_saved or set() self.related_objects_to_save = related_objects_to_save or {} def __repr__(self): return "" % (self.obj, self.data) django-tastypie-0.12.0/tastypie/cache.py000066400000000000000000000051411240444465200201610ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.cache import get_cache class NoCache(object): """ A simplified, swappable base class for caching. Does nothing save for simulating the cache API. """ def __init__(self, varies=None, *args, **kwargs): """ Optionally accepts a ``varies`` list that will be used in the Vary header. Defaults to ["Accept"]. """ super(NoCache, self).__init__(*args, **kwargs) self.varies = varies if self.varies is None: self.varies = ["Accept"] def get(self, key): """ Always returns ``None``. """ return None def set(self, key, value, timeout=60): """ No-op for setting values in the cache. """ pass def cacheable(self, request, response): """ Returns True or False if the request -> response is capable of being cached. """ return bool(request.method == "GET" and response.status_code == 200) def cache_control(self): """ No-op for returning values for cache-control """ return { 'no_cache': True, } class SimpleCache(NoCache): """ Uses Django's current ``CACHES`` configuration to store cached data. """ def __init__(self, cache_name='default', timeout=None, public=None, private=None, *args, **kwargs): """ Optionally accepts a ``timeout`` in seconds for the resource's cache. 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.public = public self.private = private def get(self, key, **kwargs): """ Gets a key from the cache. Returns ``None`` if the key is not found. """ return self.cache.get(key, **kwargs) def set(self, key, value, timeout=None): """ Sets a key-value in the cache. Optionally accepts a ``timeout`` in seconds. Defaults to ``None`` which uses the resource's default timeout. """ if timeout is None: timeout = self.timeout self.cache.set(key, value, timeout) def cache_control(self): control = { 'max_age': self.timeout, 's_maxage': self.timeout, } if self.public is not None: control["public"] = self.public if self.private is not None: control["private"] = self.private return control django-tastypie-0.12.0/tastypie/compat.py000066400000000000000000000012371240444465200204030ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings import django __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 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 return User def get_username_field(): return 'username' django-tastypie-0.12.0/tastypie/constants.py000066400000000000000000000003211240444465200211250ustar00rootroot00000000000000from __future__ import unicode_literals # Enable all basic ORM filters but do not allow filtering across relationships. ALL = 1 # Enable all ORM filters, including across relationships ALL_WITH_RELATIONS = 2 django-tastypie-0.12.0/tastypie/contrib/000077500000000000000000000000001240444465200202035ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/__init__.py000066400000000000000000000000001240444465200223020ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/contenttypes/000077500000000000000000000000001240444465200227425ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/contenttypes/__init__.py000066400000000000000000000000001240444465200250410ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/contenttypes/fields.py000066400000000000000000000042761240444465200245730ustar00rootroot00000000000000from __future__ import unicode_literals from functools import partial from tastypie import fields from tastypie.resources import Resource from tastypie.exceptions import ApiFieldError from django.db import models from django.core.exceptions import ObjectDoesNotExist from .resources import GenericResource class GenericForeignKeyField(fields.ToOneField): """ Provides access to GenericForeignKey objects from the django content_types framework. """ def __init__(self, to, attribute, **kwargs): if not isinstance(to, dict): raise ValueError('to field must be a dictionary in GenericForeignKeyField') if len(to) <= 0: raise ValueError('to field must have some values') for k, v in to.items(): if not issubclass(k, models.Model) or not issubclass(v, Resource): raise ValueError('to field must map django models to tastypie resources') super(GenericForeignKeyField, self).__init__(to, attribute, **kwargs) def get_related_resource(self, related_instance): self._to_class = self.to.get(type(related_instance), None) if self._to_class is None: raise TypeError('no resource for model %s' % type(related_instance)) return super(GenericForeignKeyField, self).get_related_resource(related_instance) @property def to_class(self): if self._to_class and not issubclass(GenericResource, self._to_class): return self._to_class return partial(GenericResource, resources=self.to.values()) def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None): try: obj = fk_resource.get_via_uri(uri, request=request) fk_resource = self.get_related_resource(obj) return super(GenericForeignKeyField, self).resource_from_uri(fk_resource, uri, request, related_obj, related_name) except ObjectDoesNotExist: raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri) def build_related_resource(self, *args, **kwargs): self._to_class = None return super(GenericForeignKeyField, self).build_related_resource(*args, **kwargs) django-tastypie-0.12.0/tastypie/contrib/contenttypes/resources.py000066400000000000000000000031141240444465200253250ustar00rootroot00000000000000from __future__ import unicode_literals from tastypie.bundle import Bundle from tastypie.resources import ModelResource from tastypie.exceptions import NotFound from django.core.urlresolvers import resolve, Resolver404, get_script_prefix class GenericResource(ModelResource): """ 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) def get_via_uri(self, uri, request=None): """ This pulls apart the salient bits of the URI and populates the resource via a ``obj_get``. Optionally accepts a ``request``. If you need custom behavior based on other portions of the URI, simply override this method. """ prefix = get_script_prefix() chomped_uri = uri if prefix and chomped_uri.startswith(prefix): chomped_uri = chomped_uri[len(prefix)-1:] try: view, args, kwargs = resolve(chomped_uri) resource_name = kwargs['resource_name'] resource_class = self.resource_mapping[resource_name] except (Resolver404, KeyError): raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri) parent_resource = resource_class(api_name=self._meta.api_name) kwargs = parent_resource.remove_api_resource_names(kwargs) bundle = Bundle(request=request) return parent_resource.obj_get(bundle, **kwargs) django-tastypie-0.12.0/tastypie/contrib/gis/000077500000000000000000000000001240444465200207655ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/gis/COPYING000066400000000000000000000023041240444465200220170ustar00rootroot00000000000000resources.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. django-tastypie-0.12.0/tastypie/contrib/gis/__init__.py000066400000000000000000000000001240444465200230640ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/contrib/gis/resources.py000066400000000000000000000046441240444465200233610ustar00rootroot00000000000000# See COPYING file in this directory. # Some code originally from django-boundaryservice from __future__ import unicode_literals import json try: from urllib.parse import unquote except ImportError: from urllib import unquote from django.contrib.gis.db.models import GeometryField from django.contrib.gis.geos import GEOSGeometry from tastypie.fields import ApiField, CharField from tastypie import resources class GeometryApiField(ApiField): """ Custom ApiField for dealing with data from GeometryFields (by serializing them as GeoJSON). """ dehydrated_type = 'geometry' help_text = 'Geometry data.' def hydrate(self, bundle): value = super(GeometryApiField, self).hydrate(bundle) if value is None: return value return json.dumps(value) def dehydrate(self, obj, for_list=False): return self.convert(super(GeometryApiField, self).dehydrate(obj)) def convert(self, value): if value is None: return None if isinstance(value, dict): return value # Get ready-made geojson serialization and then convert it _back_ to # a Python object so that tastypie can serialize it as part of the # bundle. return json.loads(value.geojson) class ModelResource(resources.ModelResource): """ ModelResource subclass that handles geometry fields as GeoJSON. """ @classmethod def api_field_from_django_field(cls, f, default=CharField): """ Overrides default field handling to support custom GeometryApiField. """ if isinstance(f, GeometryField): return GeometryApiField return super(ModelResource, cls).api_field_from_django_field(f, default) def filter_value_to_python(self, value, field_name, filters, filter_expr, filter_type): value = super(ModelResource, self).filter_value_to_python( value, field_name, filters, filter_expr, filter_type) # If we are filtering on a GeometryApiField then we should try # and convert this to a GEOSGeometry object. The conversion # will fail if we don't have value JSON, so in that case we'll # just return ``value`` as normal. if isinstance(self.fields[field_name], GeometryApiField): try: value = GEOSGeometry(unquote(value)) except ValueError: pass return value django-tastypie-0.12.0/tastypie/exceptions.py000066400000000000000000000041221240444465200212750ustar00rootroot00000000000000from __future__ import unicode_literals from django.http import HttpResponse class TastypieError(Exception): """A base exception for other tastypie-related errors.""" pass class HydrationError(TastypieError): """Raised when there is an error hydrating data.""" pass class NotRegistered(TastypieError): """ Raised when the requested resource isn't registered with the ``Api`` class. """ pass class NotFound(TastypieError): """ Raised when the resource/object in question can't be found. """ pass class Unauthorized(TastypieError): """ Raised when the request object is not accessible to the user. This is different than the ``tastypie.http.HttpUnauthorized`` & is handled differently internally. """ pass class ApiFieldError(TastypieError): """ Raised when there is a configuration error with a ``ApiField``. """ pass class UnsupportedFormat(TastypieError): """ Raised when an unsupported serialization format is requested. """ pass class BadRequest(TastypieError): """ A generalized exception for indicating incorrect request parameters. Handled specially in that the message tossed by this exception will be presented to the end user. """ pass class BlueberryFillingFound(TastypieError): pass class InvalidFilterError(BadRequest): """ Raised when the end user attempts to use a filter that has not be explicitly allowed. """ pass class InvalidSortError(BadRequest): """ Raised when the end user attempts to sort on a field that has not be explicitly allowed. """ pass class ImmediateHttpResponse(TastypieError): """ This exception is used to interrupt the flow of processing to immediately return a custom HttpResponse. Common uses include:: * for authentication (like digest/OAuth) * for throttling """ _response = HttpResponse("Nothing provided.") def __init__(self, response): self._response = response @property def response(self): return self._response django-tastypie-0.12.0/tastypie/fields.py000066400000000000000000000774441240444465200204030ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from dateutil.parser import parse from decimal import Decimal import re from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.utils import datetime_safe, importlib from django.utils import six from tastypie.bundle import Bundle from tastypie.exceptions import ApiFieldError, NotFound from tastypie.utils import dict_strip_unicode_keys, make_aware class NOT_PROVIDED: def __str__(self): 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.""" 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'): """ Sets up the field. This is generally called when the containing ``Resource`` is initialized. Optionally accepts an ``attribute``, which should be a string of either an instance attribute or callable off the object during the ``dehydrate`` or push data onto an object during the ``hydrate``. Defaults to ``None``, meaning data will be manually accessed. Optionally accepts a ``default``, which provides default data when the object being ``dehydrated``/``hydrated`` has no data on the field. Defaults to ``NOT_PROVIDED``. Optionally accepts a ``null``, which indicated whether or not a ``None`` is allowable data on the field. Defaults to ``False``. Optionally accepts a ``blank``, which indicated whether or not data may be omitted on the field. Defaults to ``False``. Optionally accepts a ``readonly``, which indicates whether the field is used during the ``hydrate`` or not. Defaults to ``False``. Optionally accepts a ``unique``, which indicates if the field is a unique identifier for the object. Optionally accepts ``help_text``, which lets you provide a human-readable description of the field exposed at the schema level. Defaults to the per-Field definition. Optionally accepts ``use_in``. This may be one of ``list``, ``detail`` ``all`` or a callable which accepts a ``bundle`` and returns ``True`` or ``False``. Indicates wheather this field will be included during dehydration of a list of objects or a single object. If ``use_in`` is a callable, and returns ``True``, the field will be included during dehydration. Defaults to ``all``. """ # Track what the index thinks this field is called. self.instance_name = None self._resource = None self.attribute = attribute 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 if help_text: self.help_text = help_text def contribute_to_class(self, cls, name): # Do the least we can here so that we don't hate ourselves in the # morning. self.instance_name = name self._resource = cls def has_default(self): """Returns a boolean of whether this field has a default value.""" return self._default is not NOT_PROVIDED @property def default(self): """Returns the default value for the field.""" if callable(self._default): return self._default() return self._default def dehydrate(self, bundle, for_list=True): """ Takes data from the provided object and prepares it for the 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: previous_object = current_object current_object = getattr(current_object, attr, None) if current_object is None: if self.has_default(): current_object = self._default # Fall out of the loop, given any further attempts at # accesses will fail miserably. break elif self.null: current_object = None # Fall out of the loop, given any further attempts at # accesses will fail miserably. break else: raise ApiFieldError("The object '%r' has an empty attribute '%s' and doesn't allow a default or null value." % (previous_object, attr)) if callable(current_object): current_object = current_object() return self.convert(current_object) if self.has_default(): return self.convert(self.default) else: return None def convert(self, value): """ Handles conversion between the data found and the type of the field. Extending classes should override this method and provide correct data coercion. """ return value def hydrate(self, bundle): """ Takes data stored in the bundle for the field and returns it. Used for taking simple data and building a instance object. """ 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): # 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 callable(self._default): return self._default() return self._default elif 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) return bundle.data[self.instance_name] class CharField(ApiField): """ A text field of arbitrary length. Covers both ``models.CharField`` and ``models.TextField``. """ dehydrated_type = 'string' help_text = 'Unicode string data. Ex: "Hello World"' def convert(self, value): if value is None: return None return six.text_type(value) class FileField(ApiField): """ A file-related field. Covers both ``models.FileField`` and ``models.ImageField``. """ dehydrated_type = 'string' help_text = 'A file URL as a string. Ex: "http://media.example.com/media/photos/my_photo.jpg"' def convert(self, value): if value is None: return None try: # Try to return the URL if it's a ``File``, falling back to the string # itself if it's been overridden or is a default. return getattr(value, 'url', value) except ValueError: return None class IntegerField(ApiField): """ An integer field. Covers ``models.IntegerField``, ``models.PositiveIntegerField``, ``models.PositiveSmallIntegerField`` and ``models.SmallIntegerField``. """ dehydrated_type = 'integer' help_text = 'Integer data. Ex: 2673' def convert(self, value): if value is None: return None return int(value) class FloatField(ApiField): """ A floating point field. """ dehydrated_type = 'float' help_text = 'Floating point numeric data. Ex: 26.73' def convert(self, value): if value is None: return None return float(value) class DecimalField(ApiField): """ A decimal field. """ dehydrated_type = 'decimal' help_text = 'Fixed precision numeric data. Ex: 26.73' def convert(self, value): if value is None: return None return Decimal(value) def hydrate(self, bundle): value = super(DecimalField, self).hydrate(bundle) if value and not isinstance(value, Decimal): value = Decimal(value) return value class BooleanField(ApiField): """ A boolean field. Covers both ``models.BooleanField`` and ``models.NullBooleanField``. """ dehydrated_type = 'boolean' help_text = 'Boolean data. Ex: True' def convert(self, value): if value is None: return None return bool(value) class ListField(ApiField): """ A list field. """ dehydrated_type = 'list' help_text = "A list of data. Ex: ['abc', 26.73, 8]" def convert(self, value): if value is None: return None return list(value) class DictField(ApiField): """ A dictionary field. """ dehydrated_type = 'dict' help_text = "A dictionary of data. Ex: {'price': 26.73, 'name': 'Daniel'}" def convert(self, value): if value is None: return None return dict(value) class DateField(ApiField): """ A date field. """ dehydrated_type = 'date' help_text = 'A date as a string. Ex: "2010-11-10"' def convert(self, value): if value is None: return None if isinstance(value, six.string_types): match = DATE_REGEX.search(value) if match: data = match.groupdict() return datetime_safe.date(int(data['year']), int(data['month']), int(data['day'])) else: raise ApiFieldError("Date provided to '%s' field doesn't appear to be a valid date string: '%s'" % (self.instance_name, value)) return value def hydrate(self, bundle): value = super(DateField, self).hydrate(bundle) if value and not hasattr(value, 'year'): try: # Try to rip a date/datetime out of it. value = make_aware(parse(value)) if hasattr(value, 'hour'): value = value.date() except ValueError: pass return value class DateTimeField(ApiField): """ A datetime field. """ dehydrated_type = 'datetime' help_text = 'A date & time as a string. Ex: "2010-11-10T03:07:43"' def convert(self, value): if value is None: return None if isinstance(value, six.string_types): match = DATETIME_REGEX.search(value) 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: raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value)) return value def hydrate(self, bundle): value = super(DateTimeField, self).hydrate(bundle) if value and not hasattr(value, 'year'): if isinstance(value, six.string_types): try: # Try to rip a date/datetime out of it. value = make_aware(parse(value)) except (ValueError, TypeError): raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value)) else: raise ApiFieldError("Datetime provided to '%s' field must be a string: %s" % (self.instance_name, value)) return value class RelatedField(ApiField): """ Provides access to data that is related within the database. The ``RelatedField`` base class is not intended for direct use but provides functionality that ``ToOneField`` and ``ToManyField`` build upon. The contents of this field actually point to another ``Resource``, rather than the related object. This allows the field to represent its data in different ways. The abstractions based around this are "leaky" in that, unlike the other fields provided by ``tastypie``, these fields don't handle arbitrary objects very well. The subclasses use Django's ORM layer to make things go, though there is no ORM-specific code at this level. """ 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): """ Builds the field and prepares it to access to related data. The ``to`` argument should point to a ``Resource`` class, NOT to a ``Model``. Required. The ``attribute`` argument should specify what field/callable points to the related data on the instance object. Required. Optionally accepts a ``related_name`` argument. Currently unused, as unlike Django's ORM layer, reverse relations between ``Resource`` classes are not automatically created. Defaults to ``None``. Optionally accepts a ``null``, which indicated whether or not a ``None`` is allowable data on the field. Defaults to ``False``. Optionally accepts a ``blank``, which indicated whether or not data may be omitted on the field. Defaults to ``False``. Optionally accepts a ``readonly``, which indicates whether the field is used during the ``hydrate`` or not. Defaults to ``False``. Optionally accepts a ``full``, which indicates how the related ``Resource`` will appear post-``dehydrate``. If ``False``, the related ``Resource`` will appear as a URL to the endpoint of that resource. If ``True``, the result of the sub-resource's ``dehydrate`` will be included in full. Optionally accepts a ``unique``, which indicates if the field is a unique identifier for the object. Optionally accepts ``help_text``, which lets you provide a human-readable description of the field exposed at the schema level. Defaults to the per-Field definition. Optionally accepts ``use_in``. This may be one of ``list``, ``detail`` ``all`` or a callable which accepts a ``bundle`` and returns ``True`` or ``False``. Indicates wheather this field will be included during dehydration of a list of objects or a single object. If ``use_in`` is a callable, and returns ``True``, the field will be included during dehydration. Defaults to ``all``. 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 a bundle and returns ``True`` or ``False``. Depends on ``full`` being ``True``. Defaults to ``True``. Optionally accepts a ``full_detail``, which indicated whether or not data should be fully dehydrated when then request is for a single resource. Accepts ``True``, ``False`` or a callable that accepts a 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 self.related_name = related_name self._default = default self.null = null self.blank = blank self.readonly = readonly self.full = full 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_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: related_resource._meta.api_name = self._resource._meta.api_name # Try to be efficient about DB queries. related_resource.instance = related_instance return related_resource @property def to_class(self): # We need to be lazy here, because when the metaclass constructs the # Resources, other classes may not exist yet. # That said, memoize this so we never have to relookup/reimport. if self._to_class: return self._to_class if not isinstance(self.to, six.string_types): self._to_class = self.to return self._to_class # It's a string. Let's figure it out. if '.' in self.to: # Try to import. module_bits = self.to.split('.') module_path, class_name = '.'.join(module_bits[:-1]), module_bits[-1] module = importlib.import_module(module_path) else: # We've got a bare class name here, which won't work (No AppCache # to rely on). Try to throw a useful error. raise ImportError("Tastypie requires a Python-style path () to lazy load related resources. Only given '%s'." % self.to) self._to_class = getattr(module, class_name, None) if self._to_class is None: raise ImportError("Module '%s' does not appear to have a class called '%s'." % (module_path, class_name)) return self._to_class def dehydrate_related(self, bundle, related_resource, for_list=True): """ Based on the ``full_resource``, returns either the endpoint or the data from ``full_dehydrate`` for the related resource. """ should_dehydrate_full_resource = self.should_full_dehydrate(bundle, for_list=for_list) if not should_dehydrate_full_resource: # Be a good netizen. return related_resource.get_resource_uri(bundle) else: # ZOMG extra data and big payloads. bundle = related_resource.build_bundle( obj=related_resource.instance, request=bundle.request, objects_saved=bundle.objects_saved ) return related_resource.full_dehydrate(bundle) def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None): """ Given a URI is provided, the related resource is attempted to be loaded based on the identifiers in the URI. """ try: obj = fk_resource.get_via_uri(uri, request=request) bundle = fk_resource.build_bundle( obj=obj, request=request ) return fk_resource.full_dehydrate(bundle) except ObjectDoesNotExist: raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri) def resource_from_data(self, fk_resource, data, request=None, related_obj=None, related_name=None): """ Given a dictionary-like structure is provided, a fresh related resource is created using that data. """ # Try to hydrate the data provided. data = dict_strip_unicode_keys(data) fk_bundle = fk_resource.build_bundle( data=data, request=request ) if related_obj: 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)) # 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(): try: return fk_resource.obj_update(fk_bundle, skip_errors=True, **data) except (NotFound, TypeError): try: # Attempt lookup by primary key return fk_resource.obj_update(fk_bundle, skip_errors=True, **unique_keys) except NotFound: pass except MultipleObjectsReturned: pass # If we shouldn't update a resource, or we couldn't find a matching # resource we'll just return a populated bundle instead # of mistakenly updating something that should be read-only. fk_bundle = fk_resource.full_hydrate(fk_bundle) fk_resource.is_valid(fk_bundle) return fk_bundle def resource_from_pk(self, fk_resource, obj, request=None, related_obj=None, related_name=None): """ Given an object with a ``pk`` attribute, the related resource is attempted to be loaded via that PK. """ bundle = fk_resource.build_bundle( obj=obj, request=request ) return fk_resource.full_dehydrate(bundle) def build_related_resource(self, value, request=None, related_obj=None, related_name=None): """ Returns a bundle of data built by the related resource, usually via ``hydrate`` with the data provided. Accepts either a URI, a data dictionary (or dictionary-like structure) or an object with a ``pk``. """ self.fk_resource = self.to_class() kwargs = { 'request': request, 'related_obj': related_obj, 'related_name': related_name, } if isinstance(value, Bundle): # Already hydrated, probably nested bundles. Just return. 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'): # 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) elif hasattr(value, 'pk'): # We've got an object with a primary key. return self.resource_from_pk(self.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)) def should_full_dehydrate(self, bundle, for_list): """ Based on the ``full``, ``list_full`` and ``detail_full`` returns ``True`` or ``False`` indicating weather the resource should be fully dehydrated. """ should_dehydrate_full_resource = False 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)): 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)): should_dehydrate_full_resource = True return should_dehydrate_full_resource class ToOneField(RelatedField): """ Provides access to related data via foreign key. This subclass requires Django's ORM layer to work properly. """ help_text = 'A single 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): 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 ) self.fk_resource = None def dehydrate(self, bundle, for_list=True): foreign_obj = None if isinstance(self.attribute, six.string_types): attrs = self.attribute.split('__') foreign_obj = bundle.obj for attr in 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)) return None self.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) def hydrate(self, bundle): value = super(ToOneField, self).hydrate(bundle) if value is None: return value return self.build_related_resource(value, request=bundle.request) class ForeignKey(ToOneField): """ A convenience subclass for those who prefer to mirror ``django.db.models``. """ pass class OneToOneField(ToOneField): """ A convenience subclass for those who prefer to mirror ``django.db.models``. """ pass class ToManyField(RelatedField): """ Provides access to related data via a join table. This subclass requires Django's ORM layer to work properly. Note that the ``hydrate`` portions of this field are quite different than any other field. ``hydrate_m2m`` actually handles the data and relations. This is due to the way Django implements M2M relationships. """ is_m2m = True help_text = 'Many related resources. Can be either a list of URIs or list of individually 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): 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 ) self.m2m_bundles = [] def dehydrate(self, bundle, for_list=True): if not bundle.obj or not bundle.obj.pk: if not self.null: raise ApiFieldError("The model '%r' does not have a primary key and can not be used in a ToMany context." % bundle.obj) return [] the_m2ms = None previous_obj = bundle.obj attr = self.attribute if isinstance(self.attribute, six.string_types): attrs = self.attribute.split('__') the_m2ms = bundle.obj for attr in attrs: previous_obj = the_m2ms try: the_m2ms = getattr(the_m2ms, attr, None) except ObjectDoesNotExist: the_m2ms = None 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 = [] # 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)) return m2m_dehydrated def hydrate(self, bundle): pass def hydrate_m2m(self, bundle): if self.readonly: return None if bundle.data.get(self.instance_name) is None: if self.blank: return [] elif self.null: return [] else: raise ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name) m2m_hydrated = [] 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)) return m2m_hydrated class ManyToManyField(ToManyField): """ A convenience subclass for those who prefer to mirror ``django.db.models``. """ pass class OneToManyField(ToManyField): """ A convenience subclass for those who prefer to mirror ``django.db.models``. """ pass class TimeField(ApiField): dehydrated_type = 'time' help_text = 'A time as string. Ex: "20:05:23"' def dehydrate(self, obj, for_list=True): return self.convert(super(TimeField, self).dehydrate(obj)) def convert(self, value): if isinstance(value, six.string_types): return self.to_time(value) return value def to_time(self, s): try: dt = parse(s) except (ValueError, TypeError) as e: raise ApiFieldError(str(e)) else: return datetime.time(dt.hour, dt.minute, dt.second) def hydrate(self, bundle): value = super(TimeField, self).hydrate(bundle) if value and not isinstance(value, datetime.time): value = self.to_time(value) return value django-tastypie-0.12.0/tastypie/http.py000066400000000000000000000025401240444465200200750ustar00rootroot00000000000000""" The various HTTP responses for use in returning proper HTTP codes. """ from __future__ import unicode_literals from django.http import HttpResponse class HttpCreated(HttpResponse): status_code = 201 def __init__(self, *args, **kwargs): location = kwargs.pop('location', '') super(HttpCreated, self).__init__(*args, **kwargs) self['Location'] = location class HttpAccepted(HttpResponse): status_code = 202 class HttpNoContent(HttpResponse): status_code = 204 class HttpMultipleChoices(HttpResponse): status_code = 300 class HttpSeeOther(HttpResponse): status_code = 303 class HttpNotModified(HttpResponse): status_code = 304 class HttpBadRequest(HttpResponse): status_code = 400 class HttpUnauthorized(HttpResponse): status_code = 401 class HttpForbidden(HttpResponse): status_code = 403 class HttpNotFound(HttpResponse): status_code = 404 class HttpMethodNotAllowed(HttpResponse): status_code = 405 class HttpConflict(HttpResponse): status_code = 409 class HttpGone(HttpResponse): status_code = 410 class HttpUnprocessableEntity(HttpResponse): status_code = 422 class HttpTooManyRequests(HttpResponse): status_code = 429 class HttpApplicationError(HttpResponse): status_code = 500 class HttpNotImplemented(HttpResponse): status_code = 501 django-tastypie-0.12.0/tastypie/management/000077500000000000000000000000001240444465200206575ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/management/__init__.py000066400000000000000000000000001240444465200227560ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/management/commands/000077500000000000000000000000001240444465200224605ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/management/commands/__init__.py000066400000000000000000000000001240444465200245570ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/management/commands/backfill_api_keys.py000066400000000000000000000021361240444465200264670ustar00rootroot00000000000000from __future__ import print_function from __future__ import unicode_literals from django.core.management.base import NoArgsCommand from tastypie.compat import get_user_model from tastypie.models import ApiKey class Command(NoArgsCommand): 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.""" self.verbosity = int(options.get('verbosity', 1)) User = get_user_model() for user in User.objects.all().iterator(): try: api_key = ApiKey.objects.get(user=user) if not api_key.key: # Autogenerate the key. api_key.save() if self.verbosity >= 1: print(u"Generated a new key for '%s'" % user.username) except ApiKey.DoesNotExist: api_key = ApiKey.objects.create(user=user) if self.verbosity >= 1: print(u"Created a new key for '%s'" % user.username) django-tastypie-0.12.0/tastypie/migrations/000077500000000000000000000000001240444465200207175ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/migrations/0001_initial.py000066400000000000000000000030121240444465200233560ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings import tastypie.utils.timezone class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='ApiAccess', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('identifier', models.CharField(max_length=255)), ('url', models.CharField(default='', max_length=255, blank=True)), ('request_method', models.CharField(default='', max_length=10, blank=True)), ('accessed', models.PositiveIntegerField()), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='ApiKey', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('key', models.CharField(default='', max_length=128, db_index=True, blank=True)), ('created', models.DateTimeField(default=tastypie.utils.timezone.now)), ('user', models.OneToOneField(related_name='api_key', to=settings.AUTH_USER_MODEL)), ], options={ 'abstract': False, }, bases=(models.Model,), ), ] django-tastypie-0.12.0/tastypie/migrations/__init__.py000066400000000000000000000000001240444465200230160ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/models.py000066400000000000000000000040341240444465200204010ustar00rootroot00000000000000from __future__ import unicode_literals 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): """A simple model for use with the ``CacheDBThrottle`` behaviors.""" identifier = models.CharField(max_length=255) url = models.CharField(max_length=255, blank=True, default='') request_method = models.CharField(max_length=10, blank=True, default='') accessed = models.PositiveIntegerField() def __str__(self): return "%s @ %s" % (self.identifier, self.accessed) def save(self, *args, **kwargs): self.accessed = int(time.time()) return super(ApiAccess, self).save(*args, **kwargs) 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) created = models.DateTimeField(default=now) def __unicode__(self): return u"%s for %s" % (self.key, self.user) def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() return super(ApiKey, self).save(*args, **kwargs) def generate_key(self): # Get a random UUID. new_uuid = uuid.uuid4() # Hmac that beast. return hmac.new(new_uuid.bytes, digestmod=sha1).hexdigest() class Meta: abstract = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False) def create_api_key(sender, **kwargs): """ A signal for hooking up automatic ``ApiKey`` creation. """ if kwargs.get('created') is True: ApiKey.objects.create(user=kwargs.get('instance')) django-tastypie-0.12.0/tastypie/paginator.py000066400000000000000000000160431240444465200211050ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings from django.utils import six from tastypie.exceptions import BadRequest try: from urllib.parse import urlencode except ImportError: from urllib import urlencode class Paginator(object): """ Limits result sets down to sane amounts for passing to the client. This is used in place of Django's ``Paginator`` due to the way pagination works. ``limit`` & ``offset`` (tastypie) are used in place of ``page`` (Django) so none of the page-related calculations are necessary. This implementation also provides additional details like the ``total_count`` of resources seen and convenience links to the ``previous``/``next`` pages of data as available. """ def __init__(self, request_data, objects, resource_uri=None, limit=None, offset=0, max_limit=1000, collection_name='objects'): """ Instantiates the ``Paginator`` and allows for some configuration. The ``request_data`` argument ought to be a dictionary-like object. May provide ``limit`` and/or ``offset`` to override the defaults. Commonly provided ``request.GET``. Required. The ``objects`` should be a list-like object of ``Resources``. This is typically a ``QuerySet`` but can be anything that implements slicing. Required. Optionally accepts a ``limit`` argument, which specifies how many items to show at a time. Defaults to ``None``, which is no limit. Optionally accepts an ``offset`` argument, which specifies where in the ``objects`` to start displaying results from. Defaults to 0. Optionally accepts a ``max_limit`` argument, which the upper bound limit. Defaults to ``1000``. If you set it to 0 or ``None``, no upper bound will be enforced. """ self.request_data = request_data self.objects = objects self.limit = limit self.max_limit = max_limit self.offset = offset self.resource_uri = resource_uri self.collection_name = collection_name def get_limit(self): """ Determines the proper maximum number of results to return. In order of importance, it will use: * The user-requested ``limit`` from the GET parameters, if specified. * The object-level ``limit`` if specified. * ``settings.API_LIMIT_PER_PAGE`` if specified. Default is 20 per page. """ limit = self.request_data.get('limit', self.limit) if limit is None: limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20) try: limit = int(limit) except ValueError: raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer." % limit) if limit < 0: raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer >= 0." % limit) if self.max_limit and (not limit or limit > self.max_limit): # If it's more than the max, we're only going to return the max. # This is to prevent excessive DB (or other) load. return self.max_limit return limit def get_offset(self): """ Determines the proper starting offset of results to return. It attempts to use the user-provided ``offset`` from the GET parameters, if specified. Otherwise, it falls back to the object-level ``offset``. Default is 0. """ offset = self.offset if 'offset' in self.request_data: offset = self.request_data['offset'] try: offset = int(offset) except ValueError: raise BadRequest("Invalid offset '%s' provided. Please provide an integer." % offset) if offset < 0: raise BadRequest("Invalid offset '%s' provided. Please provide a positive integer >= 0." % offset) return offset def get_slice(self, limit, offset): """ Slices the result set to the specified ``limit`` & ``offset``. """ if limit == 0: return self.objects[offset:] return self.objects[offset:offset + limit] def get_count(self): """ Returns a count of the total number of objects seen. """ try: return self.objects.count() except (AttributeError, TypeError): # If it's not a QuerySet (or it's ilk), fallback to ``len``. return len(self.objects) def get_previous(self, limit, offset): """ If a previous page is available, will generate a URL to request that page. If not available, this returns ``None``. """ if offset - limit < 0: return None return self._generate_uri(limit, offset-limit) def get_next(self, limit, offset, count): """ If a next page is available, will generate a URL to request that page. If not available, this returns ``None``. """ if offset + limit >= count: return None return self._generate_uri(limit, offset+limit) def _generate_uri(self, limit, offset): if self.resource_uri is None: return None try: # QueryDict has a urlencode method that can handle multiple values for the same key request_params = self.request_data.copy() if 'limit' in request_params: del request_params['limit'] if 'offset' in request_params: del request_params['offset'] request_params.update({'limit': limit, 'offset': offset}) encoded_params = request_params.urlencode() except AttributeError: request_params = {} for k, v in self.request_data.items(): if isinstance(v, six.text_type): request_params[k] = v.encode('utf-8') else: request_params[k] = v if 'limit' in request_params: del request_params['limit'] if 'offset' in request_params: del request_params['offset'] request_params.update({'limit': limit, 'offset': offset}) encoded_params = urlencode(request_params) return '%s?%s' % ( self.resource_uri, encoded_params ) def page(self): """ Generates all pertinent data about the requested page. Handles getting the correct ``limit`` & ``offset``, then slices off the correct set of results and returns all pertinent metadata. """ limit = self.get_limit() offset = self.get_offset() count = self.get_count() objects = self.get_slice(limit, offset) meta = { 'offset': offset, 'limit': limit, 'total_count': count, } if limit: meta['previous'] = self.get_previous(limit, offset) meta['next'] = self.get_next(limit, offset, count) return { self.collection_name: objects, 'meta': meta, } django-tastypie-0.12.0/tastypie/resources.py000066400000000000000000002663211240444465200211410ustar00rootroot00000000000000from __future__ import unicode_literals from __future__ import with_statement from copy import deepcopy import logging import warnings 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.core.signals import got_request_exception from django.db import transaction from django.db.models.constants import LOOKUP_SEP from django.db.models.sql.constants import QUERY_TERMS from django.http import HttpResponse, HttpResponseNotFound, Http404 from django.utils.cache import patch_cache_control, patch_vary_headers from django.utils.html import escape from django.utils import six 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 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.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 def sanitize(text): # We put the single quotes back, due to their frequent usage in exception # messages. 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``. Provides sane defaults and the logic needed to augment these settings with the internal ``class Meta`` used on ``Resource`` subclasses. """ serializer = Serializer() authentication = Authentication() authorization = ReadOnlyAuthorization() cache = NoCache() throttle = BaseThrottle() validation = Validation() paginator_class = Paginator allowed_methods = ['get', 'post', 'put', 'delete', 'patch'] list_allowed_methods = None detail_allowed_methods = None limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20) max_limit = 1000 api_name = None resource_name = None urlconf_namespace = None default_format = 'application/json' filtering = {} ordering = [] object_class = None queryset = None fields = [] excludes = [] include_resource_uri = True include_absolute_url = False always_return_data = False collection_name = 'objects' detail_uri_name = 'pk' def __new__(cls, meta=None): overrides = {} # Handle overrides. if meta: for override_name in dir(meta): # No internals please. if not override_name.startswith('_'): overrides[override_name] = getattr(meta, override_name) allowed_methods = overrides.get('allowed_methods', ['get', 'post', 'put', 'delete', 'patch']) if overrides.get('list_allowed_methods', None) is None: overrides['list_allowed_methods'] = allowed_methods if overrides.get('detail_allowed_methods', None) is None: overrides['detail_allowed_methods'] = allowed_methods if six.PY3: return object.__new__(type('ResourceOptions', (cls,), overrides)) else: return object.__new__(type(b'ResourceOptions', (cls,), overrides)) class DeclarativeMetaclass(type): def __new__(cls, name, bases, attrs): attrs['base_fields'] = {} 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 for field_name, obj in attrs.copy().items(): # Look for ``dehydrated_type`` instead of doing ``isinstance``, # which can break down if Tastypie is re-namespaced as something # else. if hasattr(obj, 'dehydrated_type'): field = attrs.pop(field_name) declared_fields[field_name] = field attrs['base_fields'].update(declared_fields) attrs['declared_fields'] = declared_fields new_class = super(DeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) opts = getattr(new_class, 'Meta', None) new_class._meta = ResourceOptions(opts) if not getattr(new_class._meta, 'resource_name', None): # No ``resource_name`` provided. Attempt to auto-name the resource. class_name = new_class.__name__ name_bits = [bit for bit in class_name.split('Resource') if bit] resource_name = ''.join(name_bits).lower() 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: del(new_class.base_fields['resource_uri']) for field_name, field_object in new_class.base_fields.items(): if hasattr(field_object, 'contribute_to_class'): field_object.contribute_to_class(new_class, field_name) return new_class class Resource(six.with_metaclass(DeclarativeMetaclass)): """ Handles the data, request dispatch and responding to requests. Serialization/deserialization is handled "at the edges" (i.e. at the beginning/end of the request/response cycle) so that everything internally is Python data structures. This class tries to be non-model specific, so it can be hooked up to other data sources, such as search results, files, other data, etc. """ def __init__(self, api_name=None): self.fields = deepcopy(self.base_fields) if not api_name is None: self._meta.api_name = api_name def __getattr__(self, name): if name in self.fields: return self.fields[name] raise AttributeError(name) def wrap_view(self, view): """ Wraps methods so they can be called in a more functional way as well as handling exceptions better. Note that if ``BadRequest`` or an exception with a ``response`` attr are seen, there is special handling to either present a message back to the user or return the response traveling with the exception. """ @csrf_exempt def wrapper(request, *args, **kwargs): try: callback = getattr(self, view) response = callback(request, *args, **kwargs) # Our response can vary based on a number of factors, use # the cache class to determine what we should ``Vary`` on so # caches won't return the wrong (cached) version. varies = getattr(self._meta.cache, "varies", []) if varies: patch_vary_headers(response, varies) if self._meta.cache.cacheable(request, response): if self._meta.cache.cache_control(): # If the request is cacheable and we have a # ``Cache-Control`` available then patch the header. patch_cache_control(response, **self._meta.cache.cache_control()) if request.is_ajax() and not response.has_header("Cache-Control"): # IE excessively caches XMLHttpRequests, so we're disabling # the browser cache here. # See http://www.enhanceie.com/ie/bugs.asp for details. patch_cache_control(response, no_cache=True) return response except (BadRequest, fields.ApiFieldError) as e: data = {"error": sanitize(e.args[0]) if getattr(e, 'args') else ''} return self.error_response(request, data, response_class=http.HttpBadRequest) except ValidationError as e: data = {"error": sanitize(e.messages)} return self.error_response(request, data, response_class=http.HttpBadRequest) except Exception as e: if hasattr(e, 'response'): return e.response # A real, non-expected exception. # Handle the case where the full traceback is more helpful # than the serialized error. if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False): raise # Re-raise the error to get a proper traceback when the error # happend during a test case if request.META.get('SERVER_NAME') == 'testserver': raise # Rather than re-raising, we're going to things similar to # what Django does. The difference is returning a serialized # error message. return self._handle_500(request, e) 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 NOT_FOUND_EXCEPTIONS = (NotFound, ObjectDoesNotExist, Http404) if isinstance(exception, NOT_FOUND_EXCEPTIONS): response_class = HttpResponseNotFound response_code = 404 if settings.DEBUG: data = { "error_message": sanitize(six.text_type(exception)), "traceback": the_trace, } return self.error_response(request, data, response_class=response_class) # 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: log = logging.getLogger('django.request.tastypie') log.error('Internal Server Error: %s' % request.path, exc_info=True, extra={'status_code': response_code, 'request': request}) # Send the signal so other apps are aware of the exception. got_request_exception.send(self.__class__, request=request) # Prep the data going out. data = { "error_message": getattr(settings, 'TASTYPIE_CANNED_ERROR', "Sorry, this request could not be processed. Please try again later."), } return self.error_response(request, data, response_class=response_class) def _build_reverse_url(self, name, args=None, kwargs=None): """ A convenience hook for overriding how URLs are built. See ``NamespacedModelResource._build_reverse_url`` for an example. """ return reverse(name, args=args, kwargs=kwargs) def base_urls(self): """ 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"), ] def override_urls(self): """ Deprecated. Will be removed by v1.0.0. Please use ``prepend_urls`` instead. """ return [] def prepend_urls(self): """ A hook for adding your own URLs or matching before the default URLs. """ return [] @property def urls(self): """ The endpoints this ``Resource`` responds to. Mostly a standard URLconf, this is suitable for either automatic use when registered with an ``Api`` class or for including directly in a URLconf should you choose to. """ urls = self.prepend_urls() overridden_urls = self.override_urls() if overridden_urls: warnings.warn("'override_urls' is a deprecated method & will be removed by v1.0.0. Please rename your method to ``prepend_urls``.") urls += overridden_urls urls += self.base_urls() urlpatterns = patterns('', *urls ) return urlpatterns def determine_format(self, request): """ Used to determine the desired format. Largely relies on ``tastypie.utils.mime.determine_format`` but here as a point of extension. """ return determine_format(request, self._meta.serializer, default_format=self._meta.default_format) def serialize(self, request, data, format, options=None): """ Given a request, data and a desired format, produces a serialized version suitable for transfer over the wire. Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. """ options = options or {} if 'text/javascript' in format: # get JSONP callback name. default to "callback" callback = request.GET.get('callback', 'callback') if not is_valid_jsonp_callback_value(callback): raise BadRequest('JSONP callback name is invalid.') options['callback'] = callback return self._meta.serializer.serialize(data, format, options) def deserialize(self, request, data, format='application/json'): """ Given a request, data and a format, deserializes the given data. It relies on the request properly sending a ``CONTENT_TYPE`` header, falling back to ``application/json`` if not provided. Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. """ deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', 'application/json')) return deserialized def alter_list_data_to_serialize(self, request, data): """ A hook to alter list data just before it gets serialized & sent to the user. Useful for restructuring/renaming aspects of the what's going to be sent. Should accommodate for a list of objects, generally also including meta data. """ return data def alter_detail_data_to_serialize(self, request, data): """ A hook to alter detail data just before it gets serialized & sent to the user. Useful for restructuring/renaming aspects of the what's going to be sent. Should accommodate for receiving a single bundle of data. """ return data def alter_deserialized_list_data(self, request, data): """ A hook to alter list data just after it has been received from the user & gets deserialized. Useful for altering the user data before any hydration is applied. """ return data def alter_deserialized_detail_data(self, request, data): """ A hook to alter detail data just after it has been received from the user & gets deserialized. Useful for altering the user data before any hydration is applied. """ return data def dispatch_list(self, request, **kwargs): """ A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over the entire list of resources. Relies on ``Resource.dispatch`` for the heavy-lifting. """ return self.dispatch('list', request, **kwargs) def dispatch_detail(self, request, **kwargs): """ A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on a single resource. Relies on ``Resource.dispatch`` for the heavy-lifting. """ return self.dispatch('detail', request, **kwargs) def dispatch(self, request_type, request, **kwargs): """ Handles the common operations (allowed HTTP method, authentication, throttling, method lookup) surrounding most CRUD interactions. """ allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None) if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META: request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] request_method = self.method_check(request, allowed=allowed_methods) method = getattr(self, "%s_%s" % (request_method, request_type), None) if method is None: raise ImmediateHttpResponse(response=http.HttpNotImplemented()) self.is_authenticated(request) self.throttle_check(request) # All clear. Process the request. request = convert_post_to_put(request) response = method(request, **kwargs) # Add the throttled request. self.log_throttled_access(request) # If what comes back isn't a ``HttpResponse``, assume that the # request was accepted and that some action occurred. This also # prevents Django from freaking out. if not isinstance(response, HttpResponse): return http.HttpNoContent() return response def remove_api_resource_names(self, url_dict): """ Given a dictionary of regex matches from a URLconf, removes ``api_name`` and/or ``resource_name`` if found. This is useful for converting URLconf matches into something suitable for data lookup. For example:: Model.objects.filter(**self.remove_api_resource_names(matches)) """ kwargs_subset = url_dict.copy() for key in ['api_name', 'resource_name']: try: del(kwargs_subset[key]) except KeyError: pass return kwargs_subset def method_check(self, request, allowed=None): """ Ensures that the HTTP method used on the request is allowed to be handled by the resource. Takes an ``allowed`` parameter, which should be a list of lowercase HTTP methods to check against. Usually, this looks like:: # The most generic lookup. self.method_check(request, self._meta.allowed_methods) # A lookup against what's allowed for list-type methods. self.method_check(request, self._meta.list_allowed_methods) # A useful check when creating a new endpoint that only handles # GET. self.method_check(request, ['get']) """ if allowed is None: allowed = [] request_method = request.method.lower() allows = ','.join([meth.upper() for meth in allowed]) if request_method == "options": response = HttpResponse(allows) response['Allow'] = allows raise ImmediateHttpResponse(response=response) if not request_method in allowed: response = http.HttpMethodNotAllowed(allows) response['Allow'] = allows raise ImmediateHttpResponse(response=response) return request_method def is_authenticated(self, request): """ Handles checking if the user is authenticated and dealing with unauthenticated users. Mostly a hook, this uses class assigned to ``authentication`` from ``Resource._meta``. """ # Authenticate the request as needed. auth_result = self._meta.authentication.is_authenticated(request) if isinstance(auth_result, HttpResponse): raise ImmediateHttpResponse(response=auth_result) if not auth_result is True: raise ImmediateHttpResponse(response=http.HttpUnauthorized()) def throttle_check(self, request): """ Handles checking if the user should be throttled. Mostly a hook, this uses class assigned to ``throttle`` from ``Resource._meta``. """ identifier = self._meta.authentication.get_identifier(request) # Check to see if they should be throttled. if self._meta.throttle.should_be_throttled(identifier): # Throttle limit exceeded. raise ImmediateHttpResponse(response=http.HttpTooManyRequests()) def log_throttled_access(self, request): """ Handles the recording of the user's access for throttling purposes. Mostly a hook, this uses class assigned to ``throttle`` from ``Resource._meta``. """ request_method = request.method.lower() self._meta.throttle.accessed(self._meta.authentication.get_identifier(request), url=request.get_full_path(), request_method=request_method) def unauthorized_result(self, exception): raise ImmediateHttpResponse(response=http.HttpUnauthorized()) def authorized_read_list(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to GET this resource. """ try: auth_result = self._meta.authorization.read_list(object_list, bundle) except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_read_detail(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to GET this resource. """ try: auth_result = self._meta.authorization.read_detail(object_list, bundle) if not auth_result is True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_create_list(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to POST this resource. """ try: auth_result = self._meta.authorization.create_list(object_list, bundle) except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_create_detail(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to POST this resource. """ try: auth_result = self._meta.authorization.create_detail(object_list, bundle) if not auth_result is True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_update_list(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to PUT this resource. """ try: auth_result = self._meta.authorization.update_list(object_list, bundle) except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_update_detail(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to PUT this resource. """ try: auth_result = self._meta.authorization.update_detail(object_list, bundle) if not auth_result is True: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_delete_list(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to DELETE this resource. """ try: auth_result = self._meta.authorization.delete_list(object_list, bundle) except Unauthorized as e: self.unauthorized_result(e) return auth_result def authorized_delete_detail(self, object_list, bundle): """ Handles checking of permissions to see if the user has authorization to DELETE this resource. """ try: auth_result = self._meta.authorization.delete_detail(object_list, bundle) if not auth_result: raise Unauthorized() except Unauthorized as e: self.unauthorized_result(e) return auth_result def build_bundle(self, obj=None, data=None, request=None, objects_saved=None): """ Given either an object, a data dictionary or both, builds a ``Bundle`` for use throughout the ``dehydrate/hydrate`` cycle. If no object is provided, an empty object from ``Resource._meta.object_class`` is created so that attempts to access ``bundle.obj`` do not fail. """ if obj is None and self._meta.object_class: obj = self._meta.object_class() return Bundle( obj=obj, data=data, request=request, objects_saved=objects_saved ) def build_filters(self, filters=None): """ Allows for the filtering of applicable objects. This needs to be implemented at the user level.' ``ModelResource`` includes a full working version specific to Django's ``Models``. """ return filters def apply_sorting(self, obj_list, options=None): """ Allows for the sorting of objects being returned. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ return obj_list def get_bundle_detail_data(self, bundle): """ Convenience method to return the ``detail_uri_name`` attribute off ``bundle.obj``. Usually just accesses ``bundle.obj.pk`` by default. """ return getattr(bundle.obj, self._meta.detail_uri_name) # 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. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def resource_uri_kwargs(self, bundle_or_obj=None): """ Builds a dictionary of kwargs to help generate URIs. Automatically provides the ``Resource.Meta.resource_name`` (and optionally the ``Resource.Meta.api_name`` if populated by an ``Api`` object). If the ``bundle_or_obj`` argument is provided, it calls ``Resource.detail_uri_kwargs`` for additional bits to create """ kwargs = { 'resource_name': self._meta.resource_name, } if self._meta.api_name is not None: kwargs['api_name'] = self._meta.api_name if bundle_or_obj is not None: kwargs.update(self.detail_uri_kwargs(bundle_or_obj)) return kwargs def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): """ Handles generating a resource URI. If the ``bundle_or_obj`` argument is not provided, it builds the URI for the list endpoint. If the ``bundle_or_obj`` argument is provided, it builds the URI for the detail endpoint. Return the generated URI. If that URI can not be reversed (not found in the URLconf), it will return an empty string. """ if bundle_or_obj is not None: url_name = 'api_dispatch_detail' try: return self._build_reverse_url(url_name, kwargs=self.resource_uri_kwargs(bundle_or_obj)) except NoReverseMatch: return '' def get_via_uri(self, uri, request=None): """ This pulls apart the salient bits of the URI and populates the resource via a ``obj_get``. Optionally accepts a ``request``. If you need custom behavior based on other portions of the URI, simply override this method. """ prefix = get_script_prefix() chomped_uri = uri if prefix and chomped_uri.startswith(prefix): 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 # incorrect data. found_at = chomped_uri.rfind(self._meta.resource_name) if found_at == -1: raise NotFound("An incorrect URL was provided '%s' for the '%s' resource." % (uri, self.__class__.__name__)) chomped_uri = chomped_uri[found_at:] try: for url_resolver in getattr(self, 'urls', []): result = url_resolver.resolve(chomped_uri) if result is not None: view, args, kwargs = result break else: raise Resolver404("URI not found in 'self.urls'.") except Resolver404: raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri) bundle = self.build_bundle(request=request) return self.obj_get(bundle=bundle, **self.remove_api_resource_names(kwargs)) # Data preparation. def full_dehydrate(self, bundle, for_list=False): """ 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'] # 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') if callable(field_use_in): if not field_use_in(bundle): continue else: if field_use_in not in use_in: 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 bundle.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) bundle = self.dehydrate(bundle) return bundle def dehydrate(self, bundle): """ A hook to allow a final manipulation of data once all fields/methods have built out the dehydrated data. Useful if you need to access more than one dehydrated field or want to annotate on additional data. Must return the modified bundle. """ return bundle def full_hydrate(self, bundle): """ Given a populated bundle, distill it and turn it back into a full-fledged object instance. """ if bundle.obj is None: bundle.obj = self._meta.object_class() bundle = self.hydrate(bundle) for field_name, field_object in self.fields.items(): if field_object.readonly is True: continue # Check for an optional method to do further hydration. method = getattr(self, "hydrate_%s" % field_name, None) if method: bundle = method(bundle) if field_object.attribute: value = field_object.hydrate(bundle) # NOTE: We only get back a bundle when it is related field. if isinstance(value, Bundle) and value.errors.get(field_name): bundle.errors[field_name] = value.errors[field_name] 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): setattr(bundle.obj, field_object.attribute, value) elif not getattr(field_object, 'is_m2m', False): 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 # be saved later in save_related. try: setattr(bundle.obj, field_object.attribute, value.obj) except (ValueError, ObjectDoesNotExist): bundle.related_objects_to_save[field_object.attribute] = value.obj elif field_object.blank: continue elif field_object.null: setattr(bundle.obj, field_object.attribute, value) return bundle def hydrate(self, bundle): """ A hook to allow an initial manipulation of data before all methods/fields have built out the hydrated data. Useful if you need to access more than one hydrated field or want to annotate on additional data. Must return the modified bundle. """ return bundle def hydrate_m2m(self, bundle): """ Populate the ManyToMany data on the instance. """ if bundle.obj is None: 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): continue if field_object.attribute: # Note that we only hydrate the data, leaving the instance # unmodified. It's up to the user's code to handle this. # The ``ModelResource`` provides a working baseline # in this regard. 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): continue method = getattr(self, "hydrate_%s" % field_name, None) if method: method(bundle) return bundle def build_schema(self): """ Returns a dictionary of all the fields on the resource and some properties about those fields. Used by the ``schema/`` endpoint to describe what will be available. """ data = { 'fields': {}, 'default_format': self._meta.default_format, 'allowed_list_http_methods': self._meta.list_allowed_methods, 'allowed_detail_http_methods': self._meta.detail_allowed_methods, 'default_limit': self._meta.limit, } if self._meta.ordering: data['ordering'] = self._meta.ordering if self._meta.filtering: data['filtering'] = self._meta.filtering for field_name, field_object in self.fields.items(): data['fields'][field_name] = { 'default': field_object.default, 'type': field_object.dehydrated_type, 'nullable': field_object.null, 'blank': field_object.blank, 'readonly': field_object.readonly, 'help_text': field_object.help_text, 'unique': field_object.unique, } if field_object.dehydrated_type == 'related': if getattr(field_object, 'is_m2m', False): related_type = 'to_many' else: related_type = 'to_one' data['fields'][field_name]['related_type'] = related_type return data def dehydrate_resource_uri(self, bundle): """ For the automatically included ``resource_uri`` field, dehydrate the URI for the given bundle. Returns empty string if no URI can be generated. """ try: return self.get_resource_uri(bundle) except NotImplementedError: return '' except NoReverseMatch: return '' def generate_cache_key(self, *args, **kwargs): """ Creates a unique-enough cache key. 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)) # 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))) # Data access methods. def get_object_list(self, request): """ A hook to allow making returning the list of available objects. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ 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``. """ allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) return 'post' in allowed def can_update(self): """ Checks to ensure ``put`` is within ``allowed_methods``. Used when hydrating related data. """ allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) return 'put' in allowed def can_delete(self): """ Checks to ensure ``delete`` is within ``allowed_methods``. """ allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) return 'delete' in allowed def apply_filters(self, request, applicable_filters): """ A hook to alter how the filters are applied to the object list. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def obj_get_list(self, bundle, **kwargs): """ Fetches the list of objects available on the resource. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def cached_obj_get_list(self, bundle, **kwargs): """ A version of ``obj_get_list`` that uses the cache as a means to get commonly-accessed data faster. """ cache_key = self.generate_cache_key('list', **kwargs) obj_list = self._meta.cache.get(cache_key) if obj_list is None: obj_list = self.obj_get_list(bundle=bundle, **kwargs) self._meta.cache.set(cache_key, obj_list) return obj_list def obj_get(self, bundle, **kwargs): """ Fetches an individual object on the resource. This needs to be implemented at the user level. If the object can not be found, this should raise a ``NotFound`` exception. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def cached_obj_get(self, bundle, **kwargs): """ A version of ``obj_get`` that uses the cache as a means to get commonly-accessed data faster. """ cache_key = self.generate_cache_key('detail', **kwargs) cached_bundle = self._meta.cache.get(cache_key) if cached_bundle is None: cached_bundle = self.obj_get(bundle=bundle, **kwargs) self._meta.cache.set(cache_key, cached_bundle) return cached_bundle def obj_create(self, bundle, **kwargs): """ Creates a new object based on the provided data. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def obj_update(self, bundle, **kwargs): """ Updates an existing object (or creates a new object) based on the provided data. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def obj_delete_list(self, bundle, **kwargs): """ Deletes an entire list of objects. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def obj_delete_list_for_update(self, bundle, **kwargs): """ Deletes an entire list of objects, specific to PUT list. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def obj_delete(self, bundle, **kwargs): """ Deletes a single object. This needs to be implemented at the user level. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() def create_response(self, request, data, response_class=HttpResponse, **response_kwargs): """ Extracts the common "which-format/serialize/return-response" cycle. Mostly a useful shortcut/hook. """ desired_format = self.determine_format(request) serialized = self.serialize(request, data, desired_format) return response_class(content=serialized, content_type=build_content_type(desired_format), **response_kwargs) def error_response(self, request, errors, response_class=None): """ Extracts the common "which-format/serialize/return-error-response" cycle. Should be used as much as possible to return errors. """ if response_class is None: response_class = http.HttpBadRequest desired_format = None if request: if request.GET.get('callback', None) is None: try: desired_format = self.determine_format(request) except BadRequest: pass # Fall through to default handler below else: # JSONP can cause extra breakage. desired_format = 'application/json' if not desired_format: desired_format = self._meta.default_format try: serialized = self.serialize(request, errors, desired_format) except BadRequest as e: error = "Additional errors occurred, but serialization of those errors failed." if settings.DEBUG: error += " %s" % e return response_class(content=error, content_type='text/plain') return response_class(content=serialized, content_type=build_content_type(desired_format)) def is_valid(self, bundle): """ Handles checking if the data provided by the user is valid. Mostly a hook, this uses class assigned to ``validation`` from ``Resource._meta``. If validation fails, an error is raised with the error messages serialized inside it. """ errors = self._meta.validation.is_valid(bundle, bundle.request) if errors: bundle.errors[self._meta.resource_name] = errors return False return True def rollback(self, bundles): """ Given the list of bundles, delete all objects pertaining to those bundles. This needs to be implemented at the user level. No exceptions should be raised if possible. ``ModelResource`` includes a full working version specific to Django's ``Models``. """ raise NotImplementedError() # Views. def get_list(self, request, **kwargs): """ Returns a serialized list of resources. Calls ``obj_get_list`` to provide the data, then handles that result set and serializes it. Should return a HttpResponse (200 OK). """ # TODO: Uncached for now. Invalidation that works for everyone may be # impossible. base_bundle = self.build_bundle(request=request) objects = self.obj_get_list(bundle=base_bundle, **self.remove_api_resource_names(kwargs)) sorted_objects = self.apply_sorting(objects, options=request.GET) paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit, collection_name=self._meta.collection_name) 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)) to_be_serialized[self._meta.collection_name] = bundles to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) return self.create_response(request, to_be_serialized) def get_detail(self, request, **kwargs): """ Returns a single serialized resource. Calls ``cached_obj_get/obj_get`` to provide the data, then handles that result set and serializes it. Should return a HttpResponse (200 OK). """ basic_bundle = self.build_bundle(request=request) try: obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) except ObjectDoesNotExist: return http.HttpNotFound() except MultipleObjectsReturned: return http.HttpMultipleChoices("More than one resource is found at this URI.") bundle = self.build_bundle(obj=obj, request=request) bundle = self.full_dehydrate(bundle) bundle = self.alter_detail_data_to_serialize(request, bundle) return self.create_response(request, bundle) def post_list(self, request, **kwargs): """ Creates a new resource/object with the provided data. Calls ``obj_create`` with the provided data and returns a response with the new resource's location. If a new resource is created, return ``HttpCreated`` (201 Created). If ``Meta.always_return_data = True``, there will be a populated body of serialized data. """ deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) deserialized = self.alter_deserialized_detail_data(request, deserialized) bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) updated_bundle = self.obj_create(bundle, **self.remove_api_resource_names(kwargs)) location = self.get_resource_uri(updated_bundle) if not self._meta.always_return_data: return http.HttpCreated(location=location) else: 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, response_class=http.HttpCreated, location=location) def post_detail(self, request, **kwargs): """ Creates a new subcollection of the resource under a resource. This is not implemented by default because most people's data models aren't self-referential. If a new resource is created, return ``HttpCreated`` (201 Created). """ return http.HttpNotImplemented() def put_list(self, request, **kwargs): """ Replaces a collection of resources with another collection. Calls ``delete_list`` to clear out the collection then ``obj_create`` with the provided the data to create the new collection. Return ``HttpNoContent`` (204 No Content) if ``Meta.always_return_data = False`` (default). Return ``HttpAccepted`` (200 OK) if ``Meta.always_return_data = True``. """ 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.") basic_bundle = self.build_bundle(request=request) self.obj_delete_list_for_update(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) bundles_seen = [] for object_data in deserialized[self._meta.collection_name]: bundle = self.build_bundle(data=dict_strip_unicode_keys(object_data), request=request) # Attempt to be transactional, deleting any previously created # objects if validation fails. try: self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) bundles_seen.append(bundle) except ImmediateHttpResponse: self.rollback(bundles_seen) raise 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.alter_list_data_to_serialize(request, to_be_serialized) return self.create_response(request, to_be_serialized) def put_detail(self, request, **kwargs): """ Either updates an existing resource or creates a new one with the provided data. Calls ``obj_update`` with the provided data first, but falls back to ``obj_create`` if the object does not already exist. If a new resource is created, return ``HttpCreated`` (201 Created). If ``Meta.always_return_data = True``, there will be a populated body of serialized data. If an existing resource is modified and ``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`` (200 OK). """ deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) deserialized = self.alter_deserialized_detail_data(request, deserialized) bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) try: updated_bundle = self.obj_update(bundle=bundle, **self.remove_api_resource_names(kwargs)) if not self._meta.always_return_data: return http.HttpNoContent() else: 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) except (NotFound, MultipleObjectsReturned): updated_bundle = self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) location = self.get_resource_uri(updated_bundle) if not self._meta.always_return_data: return http.HttpCreated(location=location) else: 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, response_class=http.HttpCreated, location=location) def delete_list(self, request, **kwargs): """ Destroys a collection of resources/objects. Calls ``obj_delete_list``. If the resources are deleted, return ``HttpNoContent`` (204 No Content). """ bundle = self.build_bundle(request=request) self.obj_delete_list(bundle=bundle, request=request, **self.remove_api_resource_names(kwargs)) return http.HttpNoContent() def delete_detail(self, request, **kwargs): """ Destroys a single resource/object. Calls ``obj_delete``. If the resource is deleted, return ``HttpNoContent`` (204 No Content). If the resource did not exist, return ``Http404`` (404 Not Found). """ # Manually construct the bundle here, since we don't want to try to # delete an empty instance. bundle = Bundle(request=request) try: self.obj_delete(bundle=bundle, **self.remove_api_resource_names(kwargs)) return http.HttpNoContent() except NotFound: return http.HttpNotFound() def patch_list(self, request, **kwargs): """ Updates a collection in-place. The exact behavior of ``PATCH`` to a list resource is still the matter of some debate in REST circles, and the ``PATCH`` RFC isn't standard. So the behavior this method implements (described below) is something of a stab in the dark. It's mostly cribbed from GData, with a smattering of ActiveResource-isms and maybe even an original idea or two. The ``PATCH`` format is one that's similar to the response returned from a ``GET`` on a list resource:: { "objects": [{object}, {object}, ...], "deleted_objects": ["URI", "URI", "URI", ...], } For each object in ``objects``: * If the dict does not have a ``resource_uri`` key then the item is considered "new" and is handled like a ``POST`` to the resource list. * If the dict has a ``resource_uri`` key and the ``resource_uri`` refers to an existing resource then the item is a update; it's treated like a ``PATCH`` to the corresponding resource detail. * If the dict has a ``resource_uri`` but the resource *doesn't* exist, then this is considered to be a create-via-``PUT``. Each entry in ``deleted_objects`` referes to a resource URI of an existing resource to be deleted; each is handled like a ``DELETE`` to the relevent resource. In any case: * If there's a resource URI it *must* refer to a resource of this type. It's an error to include a URI of a different resource. * ``PATCH`` is all or nothing. If a single sub-operation fails, the entire request will fail and all resources will be rolled back. * For ``PATCH`` to work, you **must** have ``put`` in your :ref:`detail-allowed-methods` setting. * To delete objects via ``deleted_objects`` in a ``PATCH`` request you **must** have ``delete`` in your :ref:`detail-allowed-methods` setting. Substitute appropriate names for ``objects`` and ``deleted_objects`` if ``Meta.collection_name`` is set to something other than ``objects`` (default). """ request = convert_post_to_patch(request) deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) collection_name = self._meta.collection_name deleted_collection_name = 'deleted_%s' % collection_name if collection_name not in deserialized: raise BadRequest("Invalid data sent: missing '%s'" % collection_name) if len(deserialized[collection_name]) and 'put' not in self._meta.detail_allowed_methods: raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) bundles_seen = [] for data in deserialized[collection_name]: # If there's a resource_uri then this is either an # update-in-place or a create-via-PUT. if "resource_uri" in data: uri = data.pop('resource_uri') try: obj = self.get_via_uri(uri, request=request) # The object does exist, so this is an update-in-place. bundle = self.build_bundle(obj=obj, request=request) bundle = self.full_dehydrate(bundle, for_list=True) bundle = self.alter_detail_data_to_serialize(request, bundle) self.update_in_place(request, bundle, data) except (ObjectDoesNotExist, MultipleObjectsReturned): # The object referenced by resource_uri doesn't exist, # so this is a create-by-PUT equivalent. data = self.alter_deserialized_detail_data(request, data) bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) self.obj_create(bundle=bundle) else: # There's no resource URI, so this is a create call just # like a POST to the list resource. data = self.alter_deserialized_detail_data(request, data) bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) self.obj_create(bundle=bundle) bundles_seen.append(bundle) deleted_collection = deserialized.get(deleted_collection_name, []) if deleted_collection: if 'delete' not in self._meta.detail_allowed_methods: raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) for uri in deleted_collection: obj = self.get_via_uri(uri, request=request) bundle = self.build_bundle(obj=obj, request=request) self.obj_delete(bundle=bundle) 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 = self.alter_list_data_to_serialize(request, to_be_serialized) return self.create_response(request, to_be_serialized, response_class=http.HttpAccepted) def patch_detail(self, request, **kwargs): """ Updates a resource in-place. Calls ``obj_update``. If the resource is updated, return ``HttpAccepted`` (202 Accepted). If the resource did not exist, return ``HttpNotFound`` (404 Not Found). """ request = convert_post_to_patch(request) basic_bundle = self.build_bundle(request=request) # We want to be able to validate the update, but we can't just pass # the partial data into the validator since all data needs to be # present. Instead, we basically simulate a PUT by pulling out the # original data and updating it in-place. # So first pull out the original object. This is essentially # ``get_detail``. try: obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) except ObjectDoesNotExist: return http.HttpNotFound() except MultipleObjectsReturned: return http.HttpMultipleChoices("More than one resource is found at this URI.") bundle = self.build_bundle(obj=obj, request=request) bundle = self.full_dehydrate(bundle) bundle = self.alter_detail_data_to_serialize(request, bundle) # Now update the bundle in-place. deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) self.update_in_place(request, bundle, deserialized) if not self._meta.always_return_data: return http.HttpAccepted() else: bundle = self.full_dehydrate(bundle) bundle = self.alter_detail_data_to_serialize(request, bundle) return self.create_response(request, bundle, response_class=http.HttpAccepted) def update_in_place(self, request, original_bundle, new_data): """ Update the object in original_bundle in-place using new_data. """ original_bundle.data.update(**dict_strip_unicode_keys(new_data)) # Now we've got a bundle with the new data sitting in it and we're # we're basically in the same spot as a PUT request. SO the rest of this # function is cribbed from put_detail. self.alter_deserialized_detail_data(request, original_bundle.data) kwargs = { self._meta.detail_uri_name: self.get_bundle_detail_data(original_bundle), 'request': request, } return self.obj_update(bundle=original_bundle, **kwargs) def get_schema(self, request, **kwargs): """ Returns a serialized form of the schema of the resource. Calls ``build_schema`` to generate the data. This method only responds to HTTP GET. Should return a HttpResponse (200 OK). """ self.method_check(request, allowed=['get']) self.is_authenticated(request) self.throttle_check(request) self.log_throttled_access(request) bundle = self.build_bundle(request=request) self.authorized_read_detail(self.get_object_list(bundle.request), bundle) return self.create_response(request, self.build_schema()) def get_multiple(self, request, **kwargs): """ Returns a serialized list of resources based on the identifiers from the URL. Calls ``obj_get`` to fetch only the objects requested. This method only responds to HTTP GET. Should return a HttpResponse (200 OK). """ self.method_check(request, allowed=['get']) self.is_authenticated(request) self.throttle_check(request) # Rip apart the list then iterate. kwarg_name = '%s_list' % self._meta.detail_uri_name obj_identifiers = kwargs.get(kwarg_name, '').split(';') objects = [] not_found = [] base_bundle = self.build_bundle(request=request) for identifier in obj_identifiers: try: obj = self.obj_get(bundle=base_bundle, **{self._meta.detail_uri_name: identifier}) bundle = self.build_bundle(obj=obj, request=request) bundle = self.full_dehydrate(bundle, for_list=True) objects.append(bundle) except (ObjectDoesNotExist, Unauthorized): not_found.append(identifier) object_list = { self._meta.collection_name: objects, } if len(not_found): object_list['not_found'] = not_found self.log_throttled_access(request) return self.create_response(request, object_list) class ModelDeclarativeMetaclass(DeclarativeMetaclass): def __new__(cls, name, bases, attrs): meta = attrs.get('Meta') if meta and hasattr(meta, 'queryset'): setattr(meta, 'object_class', meta.queryset.model) new_class = super(ModelDeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) include_fields = getattr(new_class._meta, 'fields', []) excludes = getattr(new_class._meta, 'excludes', []) field_names = list(new_class.base_fields.keys()) for field_name in field_names: if field_name == 'resource_uri': continue if field_name in new_class.declared_fields: continue if len(include_fields) and not field_name 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]) # Add in the new fields. 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: 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: del(new_class.base_fields['absolute_url']) return new_class class BaseModelResource(Resource): """ A subclass of ``Resource`` designed to work with Django's ``Models``. This class will introspect a given ``Model`` and build a field list based on the fields found on the model (excluding relational fields). Given that it is aware of Django's ORM, it also handles the CRUD data operations of the resource. """ @classmethod def should_skip_field(cls, field): """ Given a Django model field, return if it should be included in the contributed ApiFields. """ # Ignore certain fields (related fields). if getattr(field, 'rel'): return True return False @classmethod def api_field_from_django_field(cls, f, default=fields.CharField): """ Returns the field type that would likely be associated with each Django type. """ result = default internal_type = f.get_internal_type() if internal_type in ('DateField', 'DateTimeField'): result = fields.DateTimeField elif internal_type in ('BooleanField', 'NullBooleanField'): result = fields.BooleanField elif internal_type in ('FloatField',): result = fields.FloatField elif internal_type in ('DecimalField',): result = fields.DecimalField elif internal_type in ('IntegerField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'SmallIntegerField', 'AutoField'): result = fields.IntegerField elif internal_type in ('FileField', 'ImageField'): result = fields.FileField elif internal_type == 'TimeField': result = fields.TimeField # TODO: Perhaps enable these via introspection. The reason they're not enabled # by default is the very different ``__init__`` they have over # the other fields. # elif internal_type == 'ForeignKey': # result = ForeignKey # elif internal_type == 'ManyToManyField': # result = ManyToManyField return result @classmethod def get_fields(cls, fields=None, excludes=None): """ Given any explicit fields to include and fields to exclude, add additional fields based on the associated model. """ final_fields = {} fields = fields or [] excludes = excludes or [] if not cls._meta.object_class: return final_fields for f in cls._meta.object_class._meta.fields: # If the field name is already present, skip if f.name in cls.base_fields: continue # If field is not present in explicit field listing, skip if fields and f.name not in fields: continue # If field is in exclude list, skip if excludes and f.name in excludes: continue if cls.should_skip_field(f): continue api_field_class = cls.api_field_from_django_field(f) kwargs = { 'attribute': f.name, 'help_text': f.help_text, } if f.null is True: kwargs['null'] = True kwargs['unique'] = f.unique if not f.null and f.blank is True: kwargs['default'] = '' kwargs['blank'] = True if f.get_internal_type() == 'TextField': kwargs['default'] = '' if f.has_default(): kwargs['default'] = f.default if getattr(f, 'auto_now', False): kwargs['default'] = f.auto_now if getattr(f, 'auto_now_add', False): kwargs['default'] = f.auto_now_add final_fields[f.name] = api_field_class(**kwargs) final_fields[f.name].instance_name = f.name return final_fields def check_filtering(self, field_name, filter_type='exact', filter_bits=None): """ Given a field name, a optional filter type and an optional list of additional relations, determine if a field can be filtered on. If a filter does not meet the needed conditions, it should raise an ``InvalidFilterError``. If the filter meets the conditions, a list of attribute names (not field names) will be returned. """ if filter_bits is None: filter_bits = [] if not field_name 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): # Must be an explicit whitelist. if not filter_type 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: raise InvalidFilterError("The '%s' field has no 'attribute' for searching with." % field_name) # Check to see if it's a relational lookup and if that's allowed. if len(filter_bits): if not getattr(self.fields[field_name], 'is_related', False): raise InvalidFilterError("The '%s' field does not support relations." % field_name) if not self._meta.filtering[field_name] == ALL_WITH_RELATIONS: raise InvalidFilterError("Lookups are not allowed more than one level deep on the '%s' field." % field_name) # Recursively descend through the remaining lookups in the filter, # if any. We should ensure that all along the way, we're allowed # to filter on that field by the related resource. related_resource = self.fields[field_name].get_related_resource(None) return [self.fields[field_name].attribute] + related_resource.check_filtering(filter_bits[0], filter_type, filter_bits[1:]) return [self.fields[field_name].attribute] def filter_value_to_python(self, value, field_name, filters, filter_expr, filter_type): """ 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 # Split on ',' if not empty string and either an in or range filter. if filter_type in ('in', 'range') and len(value): if hasattr(filters, 'getlist'): value = [] for part in filters.getlist(filter_expr): value.extend(part.split(',')) else: value = value.split(',') return value def build_filters(self, filters=None): """ Given a dictionary of filters, create the necessary ORM-level filters. Keys should be resource fields, **NOT** model fields. Valid values are either a list of Django filter types (i.e. ``['startswith', 'exact', 'lte']``), the ``ALL`` constant or the ``ALL_WITH_RELATIONS`` constant. """ # At the declarative level: # filtering = { # 'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'], # 'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'], # 'resource_field_name_3': ALL, # 'resource_field_name_4': ALL_WITH_RELATIONS, # ... # } # Accepts the filters as a dict. None by default, meaning no filters. if filters is None: filters = {} qs_filters = {} if getattr(self._meta, 'queryset', None) is not None: # Get the possible query terms from the current QuerySet. query_terms = self._meta.queryset.query.query_terms else: query_terms = QUERY_TERMS 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: # 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) value = self.filter_value_to_python(value, field_name, filters, filter_expr, filter_type) db_field_name = LOOKUP_SEP.join(lookup_bits) qs_filter = "%s%s%s" % (db_field_name, LOOKUP_SEP, filter_type) qs_filters[qs_filter] = value return dict_strip_unicode_keys(qs_filters) def apply_sorting(self, obj_list, options=None): """ Given a dictionary of options, apply some ORM-level sorting to the provided ``QuerySet``. Looks for the ``order_by`` key and handles either ascending (just the field name) or descending (the field name with a ``-`` in front). The field name should be the resource field, **NOT** model field. """ if options is None: options = {} parameter_name = 'order_by' if not 'order_by' in options: if not 'sort_by' in options: # Nothing to alter the order. Return what we've got. return obj_list else: warnings.warn("'sort_by' is a deprecated parameter. Please use 'order_by' instead.") parameter_name = 'sort_by' order_by_args = [] if hasattr(options, 'getlist'): order_bits = options.getlist(parameter_name) else: order_bits = options.get(parameter_name) if not isinstance(order_bits, (list, tuple)): order_bits = [order_bits] for order_by in order_bits: order_by_bits = order_by.split(LOOKUP_SEP) field_name = order_by_bits[0] order = '' if order_by_bits[0].startswith('-'): field_name = order_by_bits[0][1:] order = '-' if not field_name 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: raise InvalidSortError("The '%s' field does not allow ordering." % field_name) if self.fields[field_name].attribute is None: raise InvalidSortError("The '%s' field has no 'attribute' for ordering with." % field_name) order_by_args.append("%s%s" % (order, LOOKUP_SEP.join([self.fields[field_name].attribute] + order_by_bits[1:]))) return obj_list.order_by(*order_by_args) def apply_filters(self, request, applicable_filters): """ An ORM-specific implementation of ``apply_filters``. The default simply applies the ``applicable_filters`` as ``**kwargs``, but should make it possible to do more advanced things. """ return self.get_object_list(request).filter(**applicable_filters) def get_object_list(self, request): """ An ORM-specific implementation of ``get_object_list``. Returns a queryset that may have been limited by other overrides. """ return self._meta.queryset._clone() def obj_get_list(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_get_list``. Takes an optional ``request`` object, whose ``GET`` dictionary can be used to narrow the query. """ filters = {} if hasattr(bundle.request, 'GET'): # Grab a mutable copy. filters = bundle.request.GET.copy() # Update with the provided kwargs. filters.update(kwargs) applicable_filters = self.build_filters(filters=filters) try: objects = self.apply_filters(bundle.request, applicable_filters) return self.authorized_read_list(objects, bundle) except ValueError: raise BadRequest("Invalid resource lookup data provided (mismatched type).") def obj_get(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_get``. Takes optional ``kwargs``, which are used to narrow the query to find the instance. """ try: object_list = self.get_object_list(bundle.request).filter(**kwargs) stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in kwargs.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)) elif len(object_list) > 1: raise MultipleObjectsReturned("More than '%s' matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) bundle.obj = object_list[0] self.authorized_read_detail(object_list, bundle) return bundle.obj except ValueError: raise NotFound("Invalid resource lookup data provided (mismatched type).") def obj_create(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_create``. """ bundle.obj = self._meta.object_class() for key, value in kwargs.items(): setattr(bundle.obj, key, value) bundle = self.full_hydrate(bundle) return self.save(bundle) def lookup_kwargs_with_identifiers(self, bundle, kwargs): """ Kwargs here represent uri identifiers Ex: /repos/// We need to turn those identifiers into Python objects for generating lookup parameters that can find them in the DB """ lookup_kwargs = {} bundle.obj = self.get_object_list(bundle.request).model() # Override data values, we rely on uri identifiers bundle.data.update(kwargs) # We're going to manually hydrate, as opposed to calling # ``full_hydrate``, to ensure we don't try to flesh out related # resources & keep things speedy. bundle = self.hydrate(bundle) for identifier in kwargs: if identifier == self._meta.detail_uri_name: lookup_kwargs[identifier] = kwargs[identifier] continue field_object = self.fields[identifier] # Skip readonly or related fields. if field_object.readonly is True or getattr(field_object, 'is_related', False): continue # Check for an optional method to do further hydration. method = getattr(self, "hydrate_%s" % identifier, None) if method: bundle = method(bundle) if field_object.attribute: value = field_object.hydrate(bundle) lookup_kwargs[identifier] = value return lookup_kwargs def obj_update(self, bundle, skip_errors=False, **kwargs): """ A ORM-specific implementation of ``obj_update``. """ if not bundle.obj or not self.get_bundle_detail_data(bundle): try: lookup_kwargs = self.lookup_kwargs_with_identifiers(bundle, kwargs) except: # if there is trouble hydrating the data, fall back to just # using kwargs by itself (usually it only contains a "pk" key # and this will work fine. lookup_kwargs = kwargs try: bundle.obj = self.obj_get(bundle=bundle, **lookup_kwargs) except ObjectDoesNotExist: raise NotFound("A model instance matching the provided arguments could not be found.") bundle = self.full_hydrate(bundle) return self.save(bundle, skip_errors=skip_errors) def obj_delete_list(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_delete_list``. """ objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) deletable_objects = self.authorized_delete_list(objects_to_delete, bundle) if hasattr(deletable_objects, 'delete'): # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. deletable_objects.delete() else: for authed_obj in deletable_objects: authed_obj.delete() def obj_delete_list_for_update(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_delete_list_for_update``. """ objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) deletable_objects = self.authorized_update_list(objects_to_delete, bundle) if hasattr(deletable_objects, 'delete'): # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. deletable_objects.delete() else: for authed_obj in deletable_objects: authed_obj.delete() def obj_delete(self, bundle, **kwargs): """ A ORM-specific implementation of ``obj_delete``. Takes optional ``kwargs``, which are used to narrow the query to find the instance. """ if not hasattr(bundle.obj, 'delete'): try: bundle.obj = self.obj_get(bundle=bundle, **kwargs) except ObjectDoesNotExist: raise NotFound("A model instance matching the provided arguments could not be found.") self.authorized_delete_detail(self.get_object_list(bundle.request), bundle) bundle.obj.delete() @transaction.commit_on_success() def patch_list(self, request, **kwargs): """ An ORM-specific implementation of ``patch_list``. Necessary because PATCH should be atomic (all-success or all-fail) and the only way to do this neatly is at the database level. """ return super(BaseModelResource, self).patch_list(request, **kwargs) def rollback(self, bundles): """ A ORM-specific implementation of ``rollback``. Given the list of bundles, delete all models pertaining to those bundles. """ for bundle in bundles: if bundle.obj and self.get_bundle_detail_data(bundle): bundle.obj.delete() def create_identifier(self, obj): return u"%s.%s.%s" % (obj._meta.app_label, obj._meta.module_name, obj.pk) def save(self, bundle, skip_errors=False): self.is_valid(bundle) if bundle.errors and not skip_errors: raise ImmediateHttpResponse(response=self.error_response(bundle.request, bundle.errors)) # Check if they're authorized. if bundle.obj.pk: self.authorized_update_detail(self.get_object_list(bundle.request), bundle) else: self.authorized_create_detail(self.get_object_list(bundle.request), bundle) # Save FKs just in case. self.save_related(bundle) # Save the main object. bundle.obj.save() bundle.objects_saved.add(self.create_identifier(bundle.obj)) # Now pick up the M2M bits. m2m_bundle = self.hydrate_m2m(bundle) self.save_m2m(m2m_bundle) return bundle def save_related(self, bundle): """ Handles the saving of related non-M2M data. Calling assigning ``child.parent = parent`` & then calling ``Child.save`` isn't good enough to make sure the ``parent`` is saved. To get around this, we go through all our related fields & call ``save`` on them if they have related, non-M2M data. 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): continue if getattr(field_object, 'is_m2m', False): continue if not field_object.attribute: continue if field_object.readonly: continue if field_object.blank and not field_name in bundle.data: continue # Get the object. try: related_obj = getattr(bundle.obj, field_object.attribute) except ObjectDoesNotExist: 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() setattr(related_obj, field_object.related_name, bundle.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. 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'): # Only build & save if there's data, not just a URI. related_bundle = related_resource.build_bundle( obj=related_obj, data=bundle.data.get(field_name), request=bundle.request, objects_saved=bundle.objects_saved ) related_resource.save(related_bundle) setattr(bundle.obj, field_object.attribute, related_obj) def save_m2m(self, bundle): """ Handles the saving of related M2M data. Due to the way Django works, the M2M data must be handled after the main instance, which is why this isn't a part of the main ``save`` bits. Currently slightly inefficient in that it will clear out the whole 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): continue if not field_object.attribute: continue if field_object.readonly: continue # Get the manager. related_mngr = None if isinstance(field_object.attribute, six.string_types): related_mngr = getattr(bundle.obj, field_object.attribute) elif callable(field_object.attribute): related_mngr = field_object.attribute(bundle) if not related_mngr: continue if hasattr(related_mngr, 'clear'): # FIXME: Dupe the original bundle, copy in the new object & # check the perms on that (using the related resource)? # Clear it out, just to be safe. related_mngr.clear() related_objs = [] 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 ) #Only save related models if they're newly added. if updated_related_bundle.obj._state.adding: 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 class NamespacedModelResource(ModelResource): """ A ModelResource subclass that respects Django namespaces. """ def _build_reverse_url(self, name, args=None, kwargs=None): namespaced = "%s:%s" % (self._meta.urlconf_namespace, name) return reverse(namespaced, args=args, kwargs=kwargs) # Based off of ``piston.utils.coerce_put_post``. Similarly BSD-licensed. # And no, the irony is not lost on me. def convert_post_to_VERB(request, verb): """ Force Django to process the VERB. """ if request.method == verb: if hasattr(request, '_post'): del(request._post) del(request._files) try: request.method = "POST" request._load_post_and_files() request.method = verb except AttributeError: request.META['REQUEST_METHOD'] = 'POST' request._load_post_and_files() request.META['REQUEST_METHOD'] = verb setattr(request, verb, request.POST) return request def convert_post_to_put(request): return convert_post_to_VERB(request, verb='PUT') def convert_post_to_patch(request): return convert_post_to_VERB(request, verb='PATCH') django-tastypie-0.12.0/tastypie/serializers.py000066400000000000000000000464021240444465200214570ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import re import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils import six from django.utils.encoding import force_text, smart_bytes from django.core.serializers import json as djangojson from tastypie.bundle import Bundle from tastypie.exceptions import BadRequest, UnsupportedFormat 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 except ImportError: lxml = None try: import yaml from django.core.serializers import pyyaml except ImportError: yaml = None try: import biplist except ImportError: biplist = None import json XML_ENCODING = re.compile('<\?xml.*?\?>', re.IGNORECASE) # Ugh & blah. # So doing a regular dump is generally fine, since Tastypie doesn't usually # serialize advanced types. *HOWEVER*, it will dump out Python Unicode strings # as a custom YAML tag, which of course ``yaml.safe_load`` can't handle. if yaml is not None: from yaml.constructor import SafeConstructor from yaml.loader import Reader, Scanner, Parser, Composer, Resolver class TastypieConstructor(SafeConstructor): def construct_yaml_unicode_dammit(self, node): value = self.construct_scalar(node) try: return value.encode('ascii') except UnicodeEncodeError: return value TastypieConstructor.add_constructor(u'tag:yaml.org,2002:python/unicode', TastypieConstructor.construct_yaml_unicode_dammit) class TastypieLoader(Reader, Scanner, Parser, Composer, TastypieConstructor, Resolver): def __init__(self, stream): Reader.__init__(self, stream) Scanner.__init__(self) Parser.__init__(self) Composer.__init__(self) TastypieConstructor.__init__(self) Resolver.__init__(self) class Serializer(object): """ A swappable class for serialization. This handles most types of data as well as the following output formats:: * json * 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 various format methods (i.e. ``to_json``), by changing the ``formats/content_types`` options or by altering the other hook methods. """ formats = ['json', 'xml', 'yaml', 'html', 'plist'] content_types = {'json': 'application/json', 'jsonp': 'text/javascript', 'xml': 'application/xml', 'yaml': 'text/yaml', 'html': 'text/html', '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.supported_formats = [] if content_types is not None: self.content_types = content_types if formats is not None: 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: 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) for format in self.formats: try: self.supported_formats.append(self.content_types[format]) 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) def get_mime_for_format(self, format): """ Given a format, attempts to determine the correct MIME type. If not available on the current ``Serializer``, returns ``application/json`` by default. """ try: return self.content_types[format] except KeyError: return 'application/json' def format_datetime(self, data): """ A hook to control how datetimes are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "2010-12-16T03:02:14". """ data = make_naive(data) if self.datetime_formatting == 'rfc-2822': 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) return data.isoformat() def format_date(self, data): """ A hook to control how dates are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "2010-12-16". """ if self.datetime_formatting == 'rfc-2822': return format_date(data) return data.isoformat() def format_time(self, data): """ A hook to control how times are formatted. Can be overridden at the ``Serializer`` level (``datetime_formatting``) or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``). Default is ``iso-8601``, which looks like "03:02:14". """ if self.datetime_formatting == 'rfc-2822': 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() return data.isoformat() def serialize(self, bundle, format='application/json', options=None): """ Given some data and a format, calls the correct method to serialize the data and returns the result. """ desired_format = 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 if desired_format 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 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 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 if desired_format 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 def to_simple(self, data, options): """ For a piece of data, attempts to recognize it and provide a simplified form of something complex. 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): return self.format_datetime(data) elif isinstance(data, datetime.date): return self.format_date(data) elif isinstance(data, datetime.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): """ Given some data, converts that data to an ``etree.Element`` suitable for use in the XML output. """ if isinstance(data, (list, tuple)): element = Element(name or 'objects') if name: element = Element(name) element.set('type', 'list') else: element = Element('objects') for item in data: 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: element = Element(name or 'response') else: 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[:] = 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[:] = 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) data_type = get_type_string(simple_data) if data_type != 'string': element.set('type', get_type_string(simple_data)) if data_type != 'null': if isinstance(simple_data, six.text_type): element.text = simple_data else: element.text = force_text(simple_data) return element def from_etree(self, data): """ Not the smartest deserializer on the planet. At the request level, it first tries to output the deserialized subelement called "object" or "objects" and falls back to deserializing based on hinted types in the XML element attribute "type". """ if data.tag == 'request': # if "object" or "objects" exists, return deserialized forms. elements = data.getchildren() 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) elif data.tag == 'object' or data.get('type') == 'hash': return dict((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: type_string = data.get('type') if type_string in ('string', None): return data.text elif type_string == 'integer': return int(data.text) elif type_string == 'float': return float(data.text) elif type_string == 'boolean': if data.text == 'True': return True else: return False else: return None def to_json(self, data, options=None): """ Given some Python data, produces JSON output. """ options = options or {} data = self.to_simple(data, options) return djangojson.json.dumps(data, cls=djangojson.DjangoJSONEncoder, sort_keys=True, ensure_ascii=False) def from_json(self, content): """ Given some JSON data, returns a Python dictionary of the decoded data. """ try: return json.loads(content) except ValueError: raise BadRequest def to_jsonp(self, data, options=None): """ Given some Python data, produces JSON output wrapped in the provided callback. Due to a difference between JSON and Javascript, two newline characters, \u2028 and \u2029, need to be escaped. See http://timelessrepo.com/json-isnt-a-javascript-subset for 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) def to_xml(self, data, options=None): """ Given some Python data, produces XML output. """ options = options or {} if lxml is None: raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.") return tostring(self.to_etree(data, options), xml_declaration=True, encoding='utf-8') def from_xml(self, content, forbid_dtd=True, forbid_entities=True): """ Given some XML data, returns a Python dictionary of the decoded data. By default XML entity declarations and DTDs will raise a BadRequest exception content but subclasses may choose to override this if necessary. """ if lxml is None: raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.") try: # Stripping the encoding declaration. Because lxml. # See http://lxml.de/parsing.html, "Python unicode strings". content = XML_ENCODING.sub('', content) parsed = parse_xml( six.StringIO(content), forbid_dtd=forbid_dtd, forbid_entities=forbid_entities ) except (LxmlError, DefusedXmlException): raise BadRequest() return self.from_etree(parsed.getroot()) def to_yaml(self, data, options=None): """ Given some Python data, produces YAML output. """ options = options or {} if yaml is None: raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.") return yaml.dump(self.to_simple(data, options)) def from_yaml(self, content): """ 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.") return yaml.load(content, Loader=TastypieLoader) def to_plist(self, data, options=None): """ Given some Python data, produces binary plist output. """ options = options or {} if biplist is None: 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. """ if biplist is None: 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): """ Translates a Python data type into a string format. """ data_type = type(data) if data_type in six.integer_types: return 'integer' elif data_type == float: return 'float' elif data_type == bool: return 'boolean' elif data_type in (list, tuple): return 'list' elif data_type == dict: return 'hash' elif data is None: return 'null' elif isinstance(data, six.string_types): return 'string' django-tastypie-0.12.0/tastypie/south_migrations/000077500000000000000000000000001240444465200221415ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/south_migrations/0001_initial.py000066400000000000000000000135171240444465200246130ustar00rootroot00000000000000# 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'] django-tastypie-0.12.0/tastypie/south_migrations/0002_add_apikey_index.py000066400000000000000000000117071240444465200264430ustar00rootroot00000000000000# -*- 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']django-tastypie-0.12.0/tastypie/south_migrations/__init__.py000066400000000000000000000000001240444465200242400ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/templates/000077500000000000000000000000001240444465200205415ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/templates/tastypie/000077500000000000000000000000001240444465200224035ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/templates/tastypie/basic.html000066400000000000000000000015161240444465200243550ustar00rootroot00000000000000 {% block title %}API for example.com{% endblock %}

API for example.com

{% block resources_nav %}
    {% for resource in resources_nav_items %}
  • {{ resource }}
  • {% endfor %}
{% endblock %}
{% block content %}{% endblock %}
django-tastypie-0.12.0/tastypie/templates/tastypie/detail.html000066400000000000000000000001071240444465200245310ustar00rootroot00000000000000{% extends "tastypie/basic.html" %} {% block content %} {% endblock %}django-tastypie-0.12.0/tastypie/templates/tastypie/list.html000066400000000000000000000001071240444465200242420ustar00rootroot00000000000000{% extends "tastypie/basic.html" %} {% block content %} {% endblock %}django-tastypie-0.12.0/tastypie/test.py000066400000000000000000000447351240444465200201110ustar00rootroot00000000000000from __future__ import unicode_literals import time from django.conf import settings from django.test import TestCase from django.test.client import FakePayload, 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): """ Sets up a fresh ``TestApiClient`` instance. If you are employing a custom serializer, you can pass the class to the ``serializer=`` kwarg. """ self.client = Client() self.serializer = serializer if not self.serializer: self.serializer = Serializer() 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). """ return self.serializer.content_types.get(short_format, 'json') 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:: from tastypie.test import TestApiClient client = TestApiClient() 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. 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 for details. """ content_type = self.get_content_type(format) kwargs['HTTP_ACCEPT'] = content_type # GET & DELETE are the only times we don't serialize the data. if data is not None: kwargs['data'] = data if authentication is not None: kwargs['HTTP_AUTHORIZATION'] = authentication return self.client.get(uri, **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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.post('/api/v1/entry/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 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) 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): """ 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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.put('/api/v1/entry/1/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 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) 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): """ 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. Example:: from tastypie.test import TestApiClient client = TestApiClient() response = client.patch('/api/v1/entry/1/', data={ 'created': '2012-05-01T20:02:36', 'slug': 'another-post', 'title': 'Another Post', 'user': '/api/v1/user/1/', }) 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 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) 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) 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:: from tastypie.test import TestApiClient client = TestApiClient() 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. 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 for details. """ content_type = self.get_content_type(format) kwargs['content_type'] = content_type # GET & DELETE are the only times we don't serialize the data. if data is not None: kwargs['data'] = data if authentication is not None: kwargs['HTTP_AUTHORIZATION'] = authentication return self.client.delete(uri, **kwargs) class ResourceTestCase(TestCase): """ A useful base class for the start of testing Tastypie APIs. """ def setUp(self): super(ResourceTestCase, self).setUp() self.serializer = Serializer() self.api_client = TestApiClient() def get_credentials(self): """ A convenience method for the user as a way to shorten up the often repetitious calls to create the same authentication. Raises ``NotImplementedError`` by default. Usage:: class MyResourceTestCase(ResourceTestCase): def get_credentials(self): return self.create_basic('daniel', 'pass') # Then the usual tests... """ raise NotImplementedError("You must return the class for your Resource to test.") def create_basic(self, username, password): """ Creates & returns the HTTP ``Authorization`` header for use with BASIC Auth. """ import base64 return 'Basic %s' % base64.b64encode(':'.join([username, password]).encode('utf-8')).decode('utf-8') def create_apikey(self, username, api_key): """ Creates & returns the HTTP ``Authorization`` header for use with ``ApiKeyAuthentication``. """ return 'ApiKey %s:%s' % (username, api_key) def create_digest(self, username, api_key, method, uri): """ Creates & returns the HTTP ``Authorization`` header for use with Digest Auth. """ 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') 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), password=api_key ) def create_oauth(self, user): """ Creates & returns the HTTP ``Authorization`` header for use with Oauth. """ from oauth_provider.models import Consumer, Token, Resource # Necessary setup for ``oauth_provider``. resource, _ = Resource.objects.get_or_create(url='test', defaults={ 'name': 'Test Resource' }) consumer, _ = Consumer.objects.get_or_create(key='123', defaults={ 'name': 'Test', 'description': 'Testing...' }) 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 = { 'oauth_consumer_key': '123', 'oauth_nonce': 'abc', 'oauth_signature': '&', 'oauth_signature_method': 'PLAINTEXT', 'oauth_timestamp': str(int(time.time())), 'oauth_token': 'foo', } return 'OAuth %s' % ','.join([key+'='+value for key, value in oauth_data.items()]) def assertHttpOK(self, resp): """ Ensures the response is returning a HTTP 200. """ return self.assertEqual(resp.status_code, 200) def assertHttpCreated(self, resp): """ Ensures the response is returning a HTTP 201. """ return self.assertEqual(resp.status_code, 201) def assertHttpAccepted(self, resp): """ Ensures the response is returning either a HTTP 202 or a HTTP 204. """ return self.assertIn(resp.status_code, [202, 204]) def assertHttpMultipleChoices(self, resp): """ Ensures the response is returning a HTTP 300. """ return self.assertEqual(resp.status_code, 300) def assertHttpSeeOther(self, resp): """ Ensures the response is returning a HTTP 303. """ return self.assertEqual(resp.status_code, 303) def assertHttpNotModified(self, resp): """ Ensures the response is returning a HTTP 304. """ return self.assertEqual(resp.status_code, 304) def assertHttpBadRequest(self, resp): """ Ensures the response is returning a HTTP 400. """ return self.assertEqual(resp.status_code, 400) def assertHttpUnauthorized(self, resp): """ Ensures the response is returning a HTTP 401. """ return self.assertEqual(resp.status_code, 401) def assertHttpForbidden(self, resp): """ Ensures the response is returning a HTTP 403. """ return self.assertEqual(resp.status_code, 403) def assertHttpNotFound(self, resp): """ Ensures the response is returning a HTTP 404. """ return self.assertEqual(resp.status_code, 404) def assertHttpMethodNotAllowed(self, resp): """ Ensures the response is returning a HTTP 405. """ return self.assertEqual(resp.status_code, 405) def assertHttpConflict(self, resp): """ Ensures the response is returning a HTTP 409. """ return self.assertEqual(resp.status_code, 409) def assertHttpGone(self, resp): """ Ensures the response is returning a HTTP 410. """ return self.assertEqual(resp.status_code, 410) def assertHttpUnprocessableEntity(self, resp): """ Ensures the response is returning a HTTP 422. """ return self.assertEqual(resp.status_code, 422) def assertHttpTooManyRequests(self, resp): """ Ensures the response is returning a HTTP 429. """ return self.assertEqual(resp.status_code, 429) def assertHttpApplicationError(self, resp): """ Ensures the response is returning a HTTP 500. """ return self.assertEqual(resp.status_code, 500) def assertHttpNotImplemented(self, resp): """ Ensures the response is returning a HTTP 501. """ return self.assertEqual(resp.status_code, 501) def assertValidJSON(self, data): """ 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. self.serializer.from_json(data) def assertValidXML(self, data): """ 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. 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. """ # Just try the load. If it throws an exception, the test case will fail. self.serializer.from_yaml(data) def assertValidPlist(self, data): """ 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. self.serializer.from_plist(data) def assertValidJSONResponse(self, resp): """ Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/json``) * The content is valid JSON """ self.assertHttpOK(resp) self.assertTrue(resp['Content-Type'].startswith('application/json')) self.assertValidJSON(force_text(resp.content)) def assertValidXMLResponse(self, resp): """ Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/xml``) * The content is valid XML """ self.assertHttpOK(resp) self.assertTrue(resp['Content-Type'].startswith('application/xml')) self.assertValidXML(force_text(resp.content)) def assertValidYAMLResponse(self, resp): """ Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``text/yaml``) * The content is valid YAML """ self.assertHttpOK(resp) self.assertTrue(resp['Content-Type'].startswith('text/yaml')) self.assertValidYAML(force_text(resp.content)) def assertValidPlistResponse(self, resp): """ Given a ``HttpResponse`` coming back from using the ``client``, assert that you get back: * An HTTP 200 * The correct content-type (``application/x-plist``) * The content is valid binary plist data """ self.assertHttpOK(resp) self.assertTrue(resp['Content-Type'].startswith('application/x-plist')) self.assertValidPlist(force_text(resp.content)) 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. It returns a Python datastructure (typically a ``dict``) of the serialized data. """ 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. """ 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``. 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)) django-tastypie-0.12.0/tastypie/throttle.py000066400000000000000000000104501240444465200207620ustar00rootroot00000000000000from __future__ import unicode_literals import time from django.core.cache import cache class BaseThrottle(object): """ A simplified, swappable base class for throttling. Does nothing save for simulating the throttling API and implementing some common bits for the subclasses. Accepts a number of optional kwargs:: * ``throttle_at`` - the number of requests at which the user should be throttled. Default is 150 requests. * ``timeframe`` - the length of time (in seconds) in which the user make up to the ``throttle_at`` requests. Default is 3600 seconds ( 1 hour). * ``expiration`` - the length of time to retain the times the user has accessed the api in the cache. Default is 604800 (1 week). """ def __init__(self, throttle_at=150, timeframe=3600, expiration=None): self.throttle_at = throttle_at # In seconds, please. self.timeframe = timeframe if expiration is None: # Expire in a week. expiration = 604800 self.expiration = int(expiration) def convert_identifier_to_key(self, identifier): """ Takes an identifier (like a username or IP address) and converts it into a key usable by the cache system. """ bits = [] for char in identifier: if char.isalnum() or char in ['_', '.', '-']: bits.append(char) safe_string = ''.join(bits) return "%s_accesses" % safe_string def should_be_throttled(self, identifier, **kwargs): """ Returns whether or not the user has exceeded their throttle limit. Always returns ``False``, as this implementation does not actually throttle the user. """ return False def accessed(self, identifier, **kwargs): """ Handles recording the user's access. Does nothing in this implementation. """ pass class CacheThrottle(BaseThrottle): """ A throttling mechanism that uses just the cache. """ def should_be_throttled(self, identifier, **kwargs): """ Returns whether or not the user has exceeded their throttle limit. Maintains a list of timestamps when the user accessed the api within the cache. Returns ``False`` if the user should NOT be throttled or ``True`` if the user should be throttled. """ key = self.convert_identifier_to_key(identifier) # Weed out anything older than the timeframe. minimum_time = int(time.time()) - int(self.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): # Throttle them. return True # Let them through. return False def accessed(self, identifier, **kwargs): """ Handles recording the user's access. Stores the current timestamp in the "accesses" list within the cache. """ key = self.convert_identifier_to_key(identifier) times_accessed = cache.get(key, []) times_accessed.append(int(time.time())) cache.set(key, times_accessed, self.expiration) class CacheDBThrottle(CacheThrottle): """ A throttling mechanism that uses the cache for actual throttling but writes-through to the database. This is useful for tracking/aggregating usage through time, to possibly build a statistics interface or a billing mechanism. """ def accessed(self, identifier, **kwargs): """ Handles recording the user's access. Does everything the ``CacheThrottle`` class does, plus logs the access within the database using the ``ApiAccess`` model. """ # Do the import here, instead of top-level, so that the model is # only required when using this throttling mechanism. from tastypie.models import ApiAccess super(CacheDBThrottle, self).accessed(identifier, **kwargs) # Write out the access to the DB for logging purposes. ApiAccess.objects.create( identifier=identifier, url=kwargs.get('url', ''), request_method=kwargs.get('request_method', '') ) django-tastypie-0.12.0/tastypie/utils/000077500000000000000000000000001240444465200177035ustar00rootroot00000000000000django-tastypie-0.12.0/tastypie/utils/__init__.py000066400000000000000000000005501240444465200220140ustar00rootroot00000000000000from 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 django-tastypie-0.12.0/tastypie/utils/dict.py000066400000000000000000000006251240444465200212030ustar00rootroot00000000000000from django.utils.encoding import smart_bytes from django.utils import six def dict_strip_unicode_keys(uni_dict): """ Converts a dict of unicode keys into a dict of ascii keys. Useful for converting a dict to a kwarg-able format. """ if six.PY3: return uni_dict data = {} for key, value in uni_dict.items(): data[smart_bytes(key)] = value return data django-tastypie-0.12.0/tastypie/utils/formatting.py000066400000000000000000000021661240444465200224340ustar00rootroot00000000000000from __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)))) def format_datetime(dt): """ RFC 2822 datetime formatter """ return dateformat.format(make_naive(dt), 'r') def format_date(d): """ RFC 2822 date formatter """ # workaround because Django's dateformat utility requires a datetime # object (not just date) 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 """ # again, workaround dateformat input requirement dt = aware_datetime(2000, 1, 1, t.hour, t.minute, t.second) return dateformat.format(dt, 'H:i:s O') django-tastypie-0.12.0/tastypie/utils/mime.py000066400000000000000000000040711240444465200212060ustar00rootroot00000000000000from __future__ import unicode_literals import mimeparse from tastypie.exceptions import BadRequest def determine_format(request, serializer, default_format='application/json'): """ Tries to "smartly" determine which output format is desired. First attempts to find a ``format`` override from the request and supplies that if found. If no request format was demanded, it falls back to ``mimeparse`` and the ``Accepts`` header, allowing specification that way. If still no format is found, returns the ``default_format`` (which defaults to ``application/json`` if not provided). NOTE: callers *must* be prepared to handle BadRequest exceptions due to 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']) # 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() try: best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT']) except ValueError: raise BadRequest('Invalid Accept header') if best_format: return best_format # No valid 'Accept' header/formats. Sane default. return default_format def build_content_type(format, encoding='utf-8'): """ Appends character encoding to the provided format if not already present. """ if 'charset' in format: return format if format in ('application/json', 'text/javascript'): return format return "%s; charset=%s" % (format, encoding) django-tastypie-0.12.0/tastypie/utils/timezone.py000066400000000000000000000020101240444465200221000ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from django.conf import settings try: from django.utils import timezone 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 def aware_date(*args, **kwargs): return make_aware(datetime.date(*args, **kwargs)) def aware_datetime(*args, **kwargs): return make_aware(datetime.datetime(*args, **kwargs)) django-tastypie-0.12.0/tastypie/utils/urls.py000066400000000000000000000003061240444465200212410ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings def trailing_slash(): if getattr(settings, 'TASTYPIE_ALLOW_MISSING_SLASH', False): return '/?' return '/' django-tastypie-0.12.0/tastypie/utils/validate_jsonp.py000066400000000000000000000141711240444465200232630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Placed into the Public Domain by tav # Modified for Python 3 compatibility. """Validate Javascript Identifiers for use as JSON-P callback parameters.""" from __future__ import unicode_literals import re from unicodedata import category 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', # potentially reserved in a future version of the ES5 standard # 'let', 'yield' ]).__contains__ # ------------------------------------------------------------------------------ # the core validation functions # ------------------------------------------------------------------------------ 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: return False if not isinstance(identifier, six.text_type): try: identifier = six.text_type(identifier, 'utf-8') except UnicodeDecodeError: return False if escape in identifier: new = []; add_char = new.append split_id = identifier.split(escape) add_char(split_id.pop(0)) for segment in split_id: if len(segment) < 4: return False try: add_char(unichr(int('0x' + segment[:4], 16))) except Exception: return False add_char(segment[4:]) identifier = u''.join(new) if is_reserved_js_word(identifier): return False first_char = identifier[0] if not ((first_char in valid_jsid_chars) or (ucd_cat(first_char) in valid_jsid_categories_start)): return False for char in identifier[1:]: if not ((char in valid_jsid_chars) or (ucd_cat(char) in valid_jsid_categories)): return False return True def is_valid_jsonp_callback_value(value): """Return whether the given ``value`` can be used as a JSON-P callback.""" for identifier in value.split(u'.'): while '[' in identifier: if not has_valid_array_index(identifier): return False identifier = replace_array_index(u'', identifier) if not is_valid_javascript_identifier(identifier): return False 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: >>> is_valid_javascript_identifier('hello') True >>> is_valid_javascript_identifier('alert()') False >>> is_valid_javascript_identifier('a-b') False >>> is_valid_javascript_identifier('23foo') False >>> is_valid_javascript_identifier('foo23') True >>> is_valid_javascript_identifier('$210') True >>> is_valid_javascript_identifier(u'Stra\u00dfe') True >>> is_valid_javascript_identifier(r'\u0062') # u'b' True >>> is_valid_javascript_identifier(r'\u0020') False >>> is_valid_javascript_identifier('_bar') True >>> is_valid_javascript_identifier('some_var') True >>> is_valid_javascript_identifier('$') True But ``is_valid_jsonp_callback_value`` is the function you want to use for validating JSON-P callback parameter values: >>> is_valid_jsonp_callback_value('somevar') True >>> is_valid_jsonp_callback_value('function') False >>> is_valid_jsonp_callback_value(' somevar') False It supports the possibility of '.' being present in the callback name, e.g. >>> is_valid_jsonp_callback_value('$.ajaxHandler') True >>> is_valid_jsonp_callback_value('$.23') False As well as the pattern of providing an array index lookup, e.g. >>> is_valid_jsonp_callback_value('array_of_functions[42]') True >>> is_valid_jsonp_callback_value('array_of_functions[42][1]') True >>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo') True >>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]') False >>> is_valid_jsonp_callback_value('array_of_functions[]') False >>> is_valid_jsonp_callback_value('array_of_functions["key"]') False Enjoy! """ if __name__ == '__main__': import doctest doctest.testmod() django-tastypie-0.12.0/tastypie/validation.py000066400000000000000000000071451240444465200212560ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.forms import ModelForm from django.forms.models import model_to_dict class Validation(object): """ A basic validation stub that does no validation. """ def __init__(self, **kwargs): pass def is_valid(self, bundle, request=None): """ Performs a check on the data within the bundle (and optionally the request) to ensure it is valid. Should return a dictionary of error messages. If the dictionary has zero items, the data is considered valid. If there are errors, keys in the dictionary should be field names and the values should be a list of errors, even if there is only one. """ return {} class FormValidation(Validation): """ A validation class that uses a Django ``Form`` to validate the data. This class **DOES NOT** alter the data sent, only verifies it. If you want to alter the data, please use the ``CleanedDataFormValidation`` class instead. This class requires a ``form_class`` argument, which should be a Django ``Form`` (or ``ModelForm``, though ``save`` will never be called) class. 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.") self.form_class = kwargs.pop('form_class') super(FormValidation, self).__init__(**kwargs) def form_args(self, bundle): data = bundle.data # Ensure we get a bound Form, regardless of the state of the bundle. if data is None: data = {} kwargs = {'data': {}} if hasattr(bundle.obj, 'pk'): if issubclass(self.form_class, ModelForm): kwargs['instance'] = bundle.obj kwargs['data'] = model_to_dict(bundle.obj) kwargs['data'].update(data) return kwargs def is_valid(self, bundle, request=None): """ Performs a check on ``bundle.data``to ensure it is valid. If the form is valid, an empty list (all valid) will be returned. If not, a list of errors will be returned. """ form = self.form_class(**self.form_args(bundle)) if form.is_valid(): return {} # The data is invalid. Let's collect all the error messages & return # them. return form.errors class CleanedDataFormValidation(FormValidation): """ A validation class that uses a Django ``Form`` to validate the data. This class **ALTERS** data sent by the user!!! This class requires a ``form_class`` argument, which should be a Django ``Form`` (or ``ModelForm``, though ``save`` will never be called) class. This form will be used to validate the data in ``bundle.data``. """ def is_valid(self, bundle, request=None): """ Checks ``bundle.data``to ensure it is valid & replaces it with the cleaned results. If the form is valid, an empty list (all valid) will be returned. If not, a list of errors will be returned. """ form = self.form_class(**self.form_args(bundle)) if form.is_valid(): # We're different here & relying on having a reference to the same # bundle the rest of the process is using. bundle.data = form.cleaned_data return {} # The data is invalid. Let's collect all the error messages & return # them. return form.errors django-tastypie-0.12.0/tests/000077500000000000000000000000001240444465200160435ustar00rootroot00000000000000django-tastypie-0.12.0/tests/__init__.py000066400000000000000000000000001240444465200201420ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/000077500000000000000000000000001240444465200205135ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/__init__.py000066400000000000000000000000001240444465200226120ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/api/000077500000000000000000000000001240444465200212645ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/api/__init__.py000066400000000000000000000000001240444465200233630ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/api/resources.py000066400000000000000000000005301240444465200236460ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/alphanumeric/api/urls.py000066400000000000000000000004551240444465200226270ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/alphanumeric/fixtures/000077500000000000000000000000001240444465200223645ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/fixtures/test_data.json000066400000000000000000000031631240444465200252320ustar00rootroot00000000000000[ { "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" } ] django-tastypie-0.12.0/tests/alphanumeric/models.py000066400000000000000000000010431240444465200223460ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/alphanumeric/tests/000077500000000000000000000000001240444465200216555ustar00rootroot00000000000000django-tastypie-0.12.0/tests/alphanumeric/tests/__init__.py000066400000000000000000000001151240444465200237630ustar00rootroot00000000000000from alphanumeric.tests.views import * from alphanumeric.tests.http import * django-tastypie-0.12.0/tests/alphanumeric/tests/http.py000066400000000000000000000126671240444465200232220ustar00rootroot00000000000000import 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') django-tastypie-0.12.0/tests/alphanumeric/tests/views.py000066400000000000000000000212501240444465200233640ustar00rootroot00000000000000import 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') django-tastypie-0.12.0/tests/alphanumeric/urls.py000066400000000000000000000003471240444465200220560ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/authorization/000077500000000000000000000000001240444465200207435ustar00rootroot00000000000000django-tastypie-0.12.0/tests/authorization/__init__.py000066400000000000000000000000001240444465200230420ustar00rootroot00000000000000django-tastypie-0.12.0/tests/authorization/api/000077500000000000000000000000001240444465200215145ustar00rootroot00000000000000django-tastypie-0.12.0/tests/authorization/api/__init__.py000066400000000000000000000000001240444465200236130ustar00rootroot00000000000000django-tastypie-0.12.0/tests/authorization/api/resources.py000066400000000000000000000057401240444465200241060ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/authorization/models.py000066400000000000000000000024261240444465200226040ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/authorization/tests.py000066400000000000000000000312241240444465200224610ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/authorization/urls.py000066400000000000000000000010001240444465200222710ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/basic/000077500000000000000000000000001240444465200171245ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/__init__.py000066400000000000000000000000001240444465200212230ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/api/000077500000000000000000000000001240444465200176755ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/api/__init__.py000066400000000000000000000000001240444465200217740ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/api/resources.py000066400000000000000000000040121240444465200222560ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/basic/api/urls.py000066400000000000000000000014631240444465200212400ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/basic/fixtures/000077500000000000000000000000001240444465200207755ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/fixtures/test_data.json000066400000000000000000000030021240444465200236330ustar00rootroot00000000000000[ { "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" } ] django-tastypie-0.12.0/tests/basic/models.py000066400000000000000000000025061240444465200207640ustar00rootroot00000000000000import 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 django-tastypie-0.12.0/tests/basic/templates/000077500000000000000000000000001240444465200211225ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/templates/404.html000066400000000000000000000000001240444465200223050ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/tests/000077500000000000000000000000001240444465200202665ustar00rootroot00000000000000django-tastypie-0.12.0/tests/basic/tests/__init__.py000066400000000000000000000001421240444465200223740ustar00rootroot00000000000000from basic.tests.http import * from basic.tests.resources import * from basic.tests.views import *django-tastypie-0.12.0/tests/basic/tests/http.py000066400000000000000000000177261240444465200216340ustar00rootroot00000000000000import 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')) django-tastypie-0.12.0/tests/basic/tests/resources.py000066400000000000000000000144051240444465200226560ustar00rootroot00000000000000from 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') django-tastypie-0.12.0/tests/basic/tests/views.py000066400000000000000000000171341240444465200220030ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/basic/urls.py000066400000000000000000000003401240444465200204600ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/complex/000077500000000000000000000000001240444465200175125ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/__init__.py000066400000000000000000000000001240444465200216110ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/api/000077500000000000000000000000001240444465200202635ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/api/__init__.py000066400000000000000000000000001240444465200223620ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/api/resources.py000066400000000000000000000022331240444465200226470ustar00rootroot00000000000000from 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' django-tastypie-0.12.0/tests/complex/api/urls.py000066400000000000000000000010351240444465200216210ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/complex/fixtures/000077500000000000000000000000001240444465200213635ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/fixtures/test_data.json000066400000000000000000000123231240444465200242270ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/complex/models.py000066400000000000000000000043161240444465200213530ustar00rootroot00000000000000import 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 django-tastypie-0.12.0/tests/complex/tests/000077500000000000000000000000001240444465200206545ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/tests/__init__.py000066400000000000000000000000001240444465200227530ustar00rootroot00000000000000django-tastypie-0.12.0/tests/complex/urls.py000066400000000000000000000003421240444465200210500ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/content_gfk/000077500000000000000000000000001240444465200203445ustar00rootroot00000000000000django-tastypie-0.12.0/tests/content_gfk/__init__.py000066400000000000000000000000001240444465200224430ustar00rootroot00000000000000django-tastypie-0.12.0/tests/content_gfk/api/000077500000000000000000000000001240444465200211155ustar00rootroot00000000000000django-tastypie-0.12.0/tests/content_gfk/api/__init__.py000066400000000000000000000000001240444465200232140ustar00rootroot00000000000000django-tastypie-0.12.0/tests/content_gfk/api/resources.py000066400000000000000000000016261240444465200235060ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/content_gfk/api/urls.py000066400000000000000000000006601240444465200224560ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/content_gfk/models.py000066400000000000000000000014121240444465200221770ustar00rootroot00000000000000from 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') django-tastypie-0.12.0/tests/content_gfk/tests/000077500000000000000000000000001240444465200215065ustar00rootroot00000000000000django-tastypie-0.12.0/tests/content_gfk/tests/__init__.py000066400000000000000000000002131240444465200236130ustar00rootroot00000000000000import warnings warnings.simplefilter('ignore', Warning) from content_gfk.tests.fields import * from content_gfk.tests.resources import * django-tastypie-0.12.0/tests/content_gfk/tests/fields.py000066400000000000000000000053761240444465200233410ustar00rootroot00000000000000from __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) django-tastypie-0.12.0/tests/content_gfk/tests/resources.py000066400000000000000000000012021240444465200240650ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/content_gfk/urls.py000066400000000000000000000003461240444465200217060ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/core/000077500000000000000000000000001240444465200167735ustar00rootroot00000000000000django-tastypie-0.12.0/tests/core/__init__.py000066400000000000000000000000001240444465200210720ustar00rootroot00000000000000django-tastypie-0.12.0/tests/core/fixtures/000077500000000000000000000000001240444465200206445ustar00rootroot00000000000000django-tastypie-0.12.0/tests/core/fixtures/note_testdata.json000066400000000000000000000077461240444465200244130ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/core/forms.py000066400000000000000000000007631240444465200205010ustar00rootroot00000000000000from 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). django-tastypie-0.12.0/tests/core/models.py000066400000000000000000000045011240444465200206300ustar00rootroot00000000000000import 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 django-tastypie-0.12.0/tests/core/tests/000077500000000000000000000000001240444465200201355ustar00rootroot00000000000000django-tastypie-0.12.0/tests/core/tests/__init__.py000066400000000000000000000007651240444465200222560ustar00rootroot00000000000000import 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 * django-tastypie-0.12.0/tests/core/tests/api.py000066400000000000000000000166071240444465200212720ustar00rootroot00000000000000from 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']) django-tastypie-0.12.0/tests/core/tests/api_urls.py000066400000000000000000000005341240444465200223270ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/core/tests/authentication.py000066400000000000000000000555441240444465200235430ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/core/tests/authorization.py000066400000000000000000000251641240444465200234170ustar00rootroot00000000000000from 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)) django-tastypie-0.12.0/tests/core/tests/cache.py000066400000000000000000000037141240444465200215570ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/core/tests/commands.py000066400000000000000000000027631240444465200223200ustar00rootroot00000000000000from 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.") django-tastypie-0.12.0/tests/core/tests/field_urls.py000066400000000000000000000015631240444465200226440ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/core/tests/fields.py000066400000000000000000001554361240444465200217730ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/core/tests/http.py000066400000000000000000000032331240444465200214670ustar00rootroot00000000000000# 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) django-tastypie-0.12.0/tests/core/tests/manual_urls.py000066400000000000000000000004551240444465200230350ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/core/tests/mocks.py000066400000000000000000000011401240444465200216170ustar00rootroot00000000000000import 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)django-tastypie-0.12.0/tests/core/tests/paginator.py000066400000000000000000000266651240444465200225120ustar00rootroot00000000000000# -*- 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) django-tastypie-0.12.0/tests/core/tests/resources.py000066400000000000000000005612351240444465200225350ustar00rootroot00000000000000import 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) django-tastypie-0.12.0/tests/core/tests/serializers.py000066400000000000000000000633361240444465200230560ustar00rootroot00000000000000# -*- 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) django-tastypie-0.12.0/tests/core/tests/throttle.py000066400000000000000000000147261240444465200223660ustar00rootroot00000000000000import 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') django-tastypie-0.12.0/tests/core/tests/utils.py000066400000000000000000000115351240444465200216540ustar00rootroot00000000000000import 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') django-tastypie-0.12.0/tests/core/tests/validation.py000066400000000000000000000120531240444465200226420ustar00rootroot00000000000000from 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.') django-tastypie-0.12.0/tests/core/utils.py000066400000000000000000000002201240444465200204770ustar00rootroot00000000000000import logging class SimpleHandler(logging.Handler): logged = [] def emit(self, record): SimpleHandler.logged.append(record) django-tastypie-0.12.0/tests/customuser/000077500000000000000000000000001240444465200202545ustar00rootroot00000000000000django-tastypie-0.12.0/tests/customuser/__init__.py000066400000000000000000000000001240444465200223530ustar00rootroot00000000000000django-tastypie-0.12.0/tests/customuser/fixtures/000077500000000000000000000000001240444465200221255ustar00rootroot00000000000000django-tastypie-0.12.0/tests/customuser/fixtures/customuser.json000066400000000000000000000006021240444465200252270ustar00rootroot00000000000000[ { "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" } } ] django-tastypie-0.12.0/tests/customuser/models.py000066400000000000000000000020121240444465200221040ustar00rootroot00000000000000from 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 django-tastypie-0.12.0/tests/customuser/tests/000077500000000000000000000000001240444465200214165ustar00rootroot00000000000000django-tastypie-0.12.0/tests/customuser/tests/__init__.py000066400000000000000000000000531240444465200235250ustar00rootroot00000000000000from customuser.tests.custom_user import * django-tastypie-0.12.0/tests/customuser/tests/custom_user.py000066400000000000000000000035711240444465200243460ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/gis/000077500000000000000000000000001240444465200166255ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/__init__.py000066400000000000000000000000001240444465200207240ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/api/000077500000000000000000000000001240444465200173765ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/api/__init__.py000066400000000000000000000000001240444465200214750ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/api/resources.py000066400000000000000000000013461240444465200217660ustar00rootroot00000000000000from 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, } django-tastypie-0.12.0/tests/gis/api/urls.py000066400000000000000000000004771240444465200207450ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/gis/fixtures/000077500000000000000000000000001240444465200204765ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/fixtures/initial_data.json000066400000000000000000000046641240444465200240250ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/gis/models.py000066400000000000000000000021001240444465200204530ustar00rootroot00000000000000from 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 django-tastypie-0.12.0/tests/gis/templates/000077500000000000000000000000001240444465200206235ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/templates/404.html000066400000000000000000000000001240444465200220060ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/tests/000077500000000000000000000000001240444465200177675ustar00rootroot00000000000000django-tastypie-0.12.0/tests/gis/tests/__init__.py000066400000000000000000000000731240444465200221000ustar00rootroot00000000000000from gis.tests.http import * from gis.tests.views import * django-tastypie-0.12.0/tests/gis/tests/http.py000066400000000000000000000303061240444465200213220ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/gis/tests/views.py000066400000000000000000000122251240444465200215000ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/gis/urls.py000066400000000000000000000003361240444465200201660ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/manage_alphanumeric.py000077500000000000000000000003711240444465200224010ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_authorization.py000077500000000000000000000003721240444465200226320ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_basic.py000077500000000000000000000003621240444465200210120ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_complex.py000077500000000000000000000003641240444465200214020ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_content_gfk.py000077500000000000000000000003701240444465200222310ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_core.py000077500000000000000000000003611240444465200206600ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_customuser.py000077500000000000000000000003671240444465200221470ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_gis.py000077500000000000000000000003601240444465200205110ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_namespaced.py000077500000000000000000000003671240444465200220360ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_related.py000077500000000000000000000003641240444465200213530ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_slashless.py000077500000000000000000000003661240444465200217360ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/manage_validation.py000077500000000000000000000003671240444465200220700ustar00rootroot00000000000000#!/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) django-tastypie-0.12.0/tests/namespaced/000077500000000000000000000000001240444465200201435ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/__init__.py000066400000000000000000000000001240444465200222420ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/api/000077500000000000000000000000001240444465200207145ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/api/__init__.py000066400000000000000000000000001240444465200230130ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/api/resources.py000066400000000000000000000012151240444465200232770ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/namespaced/api/urls.py000066400000000000000000000010441240444465200222520ustar00rootroot00000000000000try: 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')), ) django-tastypie-0.12.0/tests/namespaced/fixtures/000077500000000000000000000000001240444465200220145ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/fixtures/test_data.json000066400000000000000000000020051240444465200246540ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/namespaced/models.py000066400000000000000000000000001240444465200217660ustar00rootroot00000000000000django-tastypie-0.12.0/tests/namespaced/tests.py000066400000000000000000000023541240444465200216630ustar00rootroot00000000000000from 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/') django-tastypie-0.12.0/tests/related_resource/000077500000000000000000000000001240444465200213725ustar00rootroot00000000000000django-tastypie-0.12.0/tests/related_resource/__init__.py000066400000000000000000000000001240444465200234710ustar00rootroot00000000000000django-tastypie-0.12.0/tests/related_resource/api/000077500000000000000000000000001240444465200221435ustar00rootroot00000000000000django-tastypie-0.12.0/tests/related_resource/api/__init__.py000066400000000000000000000000001240444465200242420ustar00rootroot00000000000000django-tastypie-0.12.0/tests/related_resource/api/resources.py000066400000000000000000000144261240444465200245360ustar00rootroot00000000000000from 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 django-tastypie-0.12.0/tests/related_resource/api/urls.py000066400000000000000000000030161240444465200235020ustar00rootroot00000000000000try: 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 django-tastypie-0.12.0/tests/related_resource/fixtures/000077500000000000000000000000001240444465200232435ustar00rootroot00000000000000django-tastypie-0.12.0/tests/related_resource/fixtures/test_data.json000066400000000000000000000022161240444465200261070ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/related_resource/models.py000066400000000000000000000066011240444465200232320ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/related_resource/tests.py000066400000000000000000000653221240444465200231160ustar00rootroot00000000000000from 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) django-tastypie-0.12.0/tests/related_resource/views.py000066400000000000000000000000321240444465200230740ustar00rootroot00000000000000# Create your views here. django-tastypie-0.12.0/tests/requirements.txt000066400000000000000000000001071240444465200213250ustar00rootroot00000000000000-r ../requirements.txt biplist defusedxml lxml mock pytz==2013b PyYAML django-tastypie-0.12.0/tests/run_all_tests.sh000077500000000000000000000024631240444465200212650ustar00rootroot00000000000000#!/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 django-tastypie-0.12.0/tests/settings.py000066400000000000000000000035301240444465200202560ustar00rootroot00000000000000import 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', ) django-tastypie-0.12.0/tests/settings_alphanumeric.py000066400000000000000000000001421240444465200230020ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('alphanumeric') ROOT_URLCONF = 'alphanumeric.urls' django-tastypie-0.12.0/tests/settings_authorization.py000066400000000000000000000002221240444465200232310ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('django.contrib.sites') INSTALLED_APPS.append('authorization') ROOT_URLCONF = 'authorization.urls' django-tastypie-0.12.0/tests/settings_basic.py000066400000000000000000000002051240444465200214130ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('django.contrib.sessions') INSTALLED_APPS.append('basic') ROOT_URLCONF = 'basic.urls' django-tastypie-0.12.0/tests/settings_complex.py000066400000000000000000000002271240444465200220050ustar00rootroot00000000000000from settings import * INSTALLED_APPS += [ 'complex', 'django.contrib.comments', 'django.contrib.sites', ] ROOT_URLCONF = 'complex.urls' django-tastypie-0.12.0/tests/settings_content_gfk.py000066400000000000000000000001441240444465200226350ustar00rootroot00000000000000from settings import * INSTALLED_APPS += [ 'content_gfk', ] ROOT_URLCONF = 'content_gfk.urls' django-tastypie-0.12.0/tests/settings_core.py000066400000000000000000000012161240444465200212650ustar00rootroot00000000000000from 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, }, } } django-tastypie-0.12.0/tests/settings_customuser.py000066400000000000000000000010441240444465200225450ustar00rootroot00000000000000from 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' django-tastypie-0.12.0/tests/settings_gis.py000066400000000000000000000012001240444465200211100ustar00rootroot00000000000000from 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' django-tastypie-0.12.0/tests/settings_namespaced.py000066400000000000000000000002001240444465200224250ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('basic') INSTALLED_APPS.append('namespaced') ROOT_URLCONF = 'namespaced.api.urls' django-tastypie-0.12.0/tests/settings_related.py000066400000000000000000000002131240444465200217510ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('core') INSTALLED_APPS.append('related_resource') ROOT_URLCONF = 'related_resource.api.urls' django-tastypie-0.12.0/tests/settings_slashless.py000066400000000000000000000002701240444465200223350ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('basic') INSTALLED_APPS.append('slashless') ROOT_URLCONF = 'slashless.api.urls' APPEND_SLASH = False TASTYPIE_ALLOW_MISSING_SLASH = True django-tastypie-0.12.0/tests/settings_validation.py000066400000000000000000000002001240444465200224570ustar00rootroot00000000000000from settings import * INSTALLED_APPS.append('basic') INSTALLED_APPS.append('validation') ROOT_URLCONF = 'validation.api.urls' django-tastypie-0.12.0/tests/slashless/000077500000000000000000000000001240444465200200445ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/__init__.py000066400000000000000000000000001240444465200221430ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/api/000077500000000000000000000000001240444465200206155ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/api/__init__.py000066400000000000000000000000001240444465200227140ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/api/resources.py000066400000000000000000000011021240444465200231730ustar00rootroot00000000000000from 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() django-tastypie-0.12.0/tests/slashless/api/urls.py000066400000000000000000000006651240444465200221630ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/slashless/fixtures/000077500000000000000000000000001240444465200217155ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/fixtures/test_data.json000066400000000000000000000020051240444465200245550ustar00rootroot00000000000000[ { "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 } ] django-tastypie-0.12.0/tests/slashless/models.py000066400000000000000000000000001240444465200216670ustar00rootroot00000000000000django-tastypie-0.12.0/tests/slashless/tests.py000066400000000000000000000037271240444465200215710ustar00rootroot00000000000000from 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]) django-tastypie-0.12.0/tests/testcases.py000066400000000000000000000070471240444465200204230ustar00rootroot00000000000000import 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'] django-tastypie-0.12.0/tests/validation/000077500000000000000000000000001240444465200201755ustar00rootroot00000000000000django-tastypie-0.12.0/tests/validation/__init__.py000066400000000000000000000000001240444465200222740ustar00rootroot00000000000000django-tastypie-0.12.0/tests/validation/api/000077500000000000000000000000001240444465200207465ustar00rootroot00000000000000django-tastypie-0.12.0/tests/validation/api/__init__.py000066400000000000000000000000001240444465200230450ustar00rootroot00000000000000django-tastypie-0.12.0/tests/validation/api/resources.py000066400000000000000000000031111240444465200233260ustar00rootroot00000000000000from 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 } django-tastypie-0.12.0/tests/validation/api/urls.py000066400000000000000000000010031240444465200222770ustar00rootroot00000000000000try: 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)), ) django-tastypie-0.12.0/tests/validation/models.py000066400000000000000000000000001240444465200220200ustar00rootroot00000000000000django-tastypie-0.12.0/tests/validation/tests.py000066400000000000000000000133641240444465200217200ustar00rootroot00000000000000from 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.'] } }) django-tastypie-0.12.0/tox.ini000066400000000000000000000145451240444465200162250ustar00rootroot00000000000000[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