mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-29 11:04:49 +00:00
Remove wagtail.contrib.wagtailapi
This commit is contained in:
parent
5ce8fe3566
commit
fa302c0853
22 changed files with 0 additions and 3503 deletions
|
|
@ -7,23 +7,11 @@ content as raw field data. This is useful for cases like serving content to
|
|||
non-web clients (such as a mobile phone app) or pulling content out of Wagtail
|
||||
for use in another site.
|
||||
|
||||
There are currently two versions of the API available: v1 and v2. Both versions
|
||||
are "stable" so it is recommended to use v2. V1 is only provided for backwards
|
||||
compatibility and will be removed from Wagtail soon.
|
||||
|
||||
See `RFC 8: Wagtail API <https://github.com/wagtail/rfcs/blob/master/text/008-wagtail-api.md#12---stable-and-unstable-versions>`_
|
||||
for full details on our stabilisation policy.
|
||||
|
||||
Version 2 (recommended)
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
v2/configuration
|
||||
v2/usage
|
||||
|
||||
Version 1
|
||||
=========
|
||||
|
||||
See :doc:`/reference/contrib/api/index`
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
Wagtail API Configuration
|
||||
=========================
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
``WAGTAILAPI_BASE_URL`` (required when using frontend cache invalidation)
|
||||
|
||||
This is used in two places, when generating absolute URLs to document files and invalidating the cache.
|
||||
|
||||
Generating URLs to documents will fall back the the current request's hostname if this is not set. Cache invalidation cannot do this, however, so this setting must be set when using this module alongside the ``wagtailfrontendcache`` module.
|
||||
|
||||
|
||||
``WAGTAILAPI_SEARCH_ENABLED`` (default: True)
|
||||
|
||||
Setting this to false will disable full text search. This applies to all endpoints.
|
||||
|
||||
|
||||
``WAGTAILAPI_LIMIT_MAX`` (default: 20)
|
||||
|
||||
This allows you to change the maximum number of results a user can request at a time. This applies to all endpoints. Set to ``None`` to remove maximum.
|
||||
|
||||
|
||||
Adding more fields to the pages endpoint
|
||||
----------------------------------------
|
||||
|
||||
By default, the pages endpoint only includes the ``id``, ``title`` and ``type`` fields in both the listing and detail views.
|
||||
|
||||
You can add more fields to the pages endpoint by setting an attribute called ``api_fields`` to a ``list`` of field names:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class BlogPage(Page):
|
||||
posted_by = models.CharField()
|
||||
posted_at = models.DateTimeField()
|
||||
content = RichTextField()
|
||||
|
||||
api_fields = ['posted_by', 'posted_at', 'content']
|
||||
|
||||
|
||||
This list also supports child relations (which will be nested inside the returned JSON document):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class BlogPageRelatedLink(Orderable):
|
||||
page = ParentalKey('BlogPage', related_name='related_links')
|
||||
link = models.URLField()
|
||||
|
||||
api_fields = ['link']
|
||||
|
||||
class BlogPage(Page):
|
||||
posted_by = models.CharField()
|
||||
posted_at = models.DateTimeField()
|
||||
content = RichTextField()
|
||||
|
||||
api_fields = ['posted_by', 'posted_at', 'content', 'related_links']
|
||||
|
||||
|
||||
Frontend cache invalidation
|
||||
---------------------------
|
||||
|
||||
If you have a Varnish, Squid, Cloudflare or CloudFront instance in front of your API, the ``wagtailapi`` module can automatically invalidate cached responses for you whenever they are updated in the database.
|
||||
|
||||
To enable it, firstly configure the ``wagtail.contrib.wagtailfrontendcache`` module within your project (see [Wagtail frontend cache docs](http://docs.wagtail.io/en/latest/contrib_components/frontendcache.html) for more information).
|
||||
|
||||
Then make sure that the ``WAGTAILAPI_BASE_URL`` setting is set correctly (Example: ``WAGTAILAPI_BASE_URL = 'http://api.mysite.com'``).
|
||||
|
||||
Then finally, switch it on by setting ``WAGTAILAPI_USE_FRONTENDCACHE`` to ``True``.
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
Wagtail API v1
|
||||
==============
|
||||
|
||||
.. warning::
|
||||
|
||||
This documentation covers the deprecated version 1 of the API, which is provided for backwards compatibility only. For the current version, see :doc:`/advanced_topics/api/index`
|
||||
|
||||
The ``wagtailapi`` module can be used to create a read-only, JSON-based API for public Wagtail content.
|
||||
|
||||
There are three endpoints to the API:
|
||||
|
||||
* **Pages:** ``/api/v1/pages/``
|
||||
* **Images:** ``/api/v1/images/``
|
||||
* **Documents:** ``/api/v1/documents/``
|
||||
|
||||
See :doc:`installation` and :doc:`configuration` if you're looking to add this module to your Wagtail site.
|
||||
|
||||
See :doc:`usage` for documentation on the API.
|
||||
|
||||
|
||||
Index
|
||||
-----
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
configuration
|
||||
usage
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
Wagtail API Installation
|
||||
========================
|
||||
|
||||
|
||||
To install, add ``wagtail.contrib.wagtailapi`` and ``rest_framework`` to ``INSTALLED_APPS`` in your Django settings and configure a URL for it in ``urls.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'wagtail.contrib.wagtailapi',
|
||||
'rest_framework',
|
||||
]
|
||||
|
||||
# urls.py
|
||||
|
||||
from wagtail.contrib.wagtailapi import urls as wagtailapi_urls
|
||||
|
||||
urlpatterns = [
|
||||
...
|
||||
|
||||
url(r'^api/', include(wagtailapi_urls)),
|
||||
|
||||
...
|
||||
|
||||
# Ensure that the wagtailapi_urls line appears above the default Wagtail page serving route
|
||||
url(r'', include(wagtail_urls)),
|
||||
]
|
||||
|
|
@ -1,871 +0,0 @@
|
|||
Wagtail API Usage Guide
|
||||
=======================
|
||||
|
||||
Listing views
|
||||
-------------
|
||||
|
||||
Performing a ``GET`` request against one of the endpoints will get you a listing of objects in that endpoint. The response will look something like this:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/endpoint_name/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": "total number of results"
|
||||
},
|
||||
"endpoint_name": [
|
||||
{
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "app_name.ModelName",
|
||||
"detail_url": "http://api.example.com/api/v1/endpoint_name/1/"
|
||||
},
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "app_name.ModelName",
|
||||
"detail_url": "http://api.example.com/api/v1/endpoint_name/2/"
|
||||
},
|
||||
"field": "different value"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
This is the basic structure of all of the listing views. They all have a ``meta`` section with a ``total_count`` variable and a listing of things.
|
||||
|
||||
|
||||
Detail views
|
||||
------------
|
||||
|
||||
All of the endpoints also contain a "detail" view which returns information on an individual object. This view is always accessed by appending the id of the object to the URL.
|
||||
|
||||
|
||||
The ``pages`` endpoint
|
||||
----------------------
|
||||
|
||||
This endpoint includes all live pages in your site that have not been put in a private section.
|
||||
|
||||
|
||||
The listing view (``/api/v1/pages/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is what a typical response from a ``GET`` request to this listing would look like:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 2
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "demo.HomePage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/2/"
|
||||
},
|
||||
"title": "Homepage"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "demo.BlogIndexPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/3/"
|
||||
},
|
||||
"title": "Blog"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Each page object contains the ``id``, a ``meta`` section and the fields with their values.
|
||||
|
||||
|
||||
``meta``
|
||||
^^^^^^^^
|
||||
|
||||
This section is used to hold "metadata" fields which aren't fields in the database. Wagtail API adds two by default:
|
||||
|
||||
- ``type`` - The app label/model name of the object
|
||||
- ``detail_url`` - A URL linking to the detail view for this object
|
||||
|
||||
|
||||
Selecting a page type
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Most Wagtail sites are made up of multiple different types of page that each have their own specific fields. In order to view/filter/order on fields specific to one page type, you must select that page type using the ``type`` query parameter.
|
||||
|
||||
|
||||
The ``type`` query parameter must be set to the Pages model name in the format: ``app_label.ModelName``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?type=demo.BlogPage
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "My blog 1"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"title": "My blog 3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Specifying a list of fields to return
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As you can see, we still only get the ``title`` field, even though we have selected a type. That's because listing pages require you to explicitly tell it what extra fields you would like to see. You can do this with the ``fields`` query parameter.
|
||||
|
||||
Just set ``fields`` to a command-separated list of field names that you would like to use.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?type=demo.BlogPage&fields=title,date_posted,feed_image
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "My blog 1",
|
||||
"date_posted": "2015-01-23",
|
||||
"feed_image": {
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/1/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2",
|
||||
"date_posted": "2015-01-24",
|
||||
"feed_image": {
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/2/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"title": "My blog 3",
|
||||
"date_posted": "2015-01-25",
|
||||
"feed_image": {
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/3/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
We now have enough information to make a basic blog listing with a feed image and date that the blog was posted.
|
||||
|
||||
|
||||
Filtering on fields
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Exact matches on field values can be done by using a query parameter with the same name as the field. Any pages with the field that exactly matches the value of this parameter will be returned.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?type=demo.BlogPage&fields=title,date_posted&date_posted=2015-01-24
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 1
|
||||
},
|
||||
"pages": [
|
||||
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2",
|
||||
"date_posted": "2015-01-24",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Filtering by section of the tree
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
It is also possible to filter the listing to only include pages with a particular parent or ancestor. This is useful if you have multiple blogs on your site and only want to view the contents of one of them.
|
||||
|
||||
|
||||
**child_of**
|
||||
|
||||
Filters the listing to only include direct children of the specified page.
|
||||
|
||||
For example, to get all the pages that are direct children of page 7.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?child_of=7
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 1
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "Other blog 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
**descendant_of**
|
||||
|
||||
Filters the listing to only include descendants of the specified page.
|
||||
|
||||
For example, to get all pages underneath the homepage:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?descendant_of=2
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 1
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "demo.BlogIndexPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/3/"
|
||||
},
|
||||
"title": "Blog"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "My blog 1",
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2",
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"title": "My blog 3",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Ordering
|
||||
^^^^^^^^
|
||||
|
||||
Like filtering, it is also possible to order on database fields. The endpoint accepts a query parameter called ``order`` which should be set to the field name to order by. Field names can be prefixed with a ``-`` to reverse the ordering. It is also possible to order randomly by setting this parameter to ``random``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?type=demo.BlogPage&fields=title,date_posted,feed_image&order=-date_posted
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"title": "My blog 3",
|
||||
"date_posted": "2015-01-25",
|
||||
"feed_image": {
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/3/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2",
|
||||
"date_posted": "2015-01-24",
|
||||
"feed_image": {
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/2/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "My blog 1",
|
||||
"date_posted": "2015-01-23",
|
||||
"feed_image": {
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/1/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Pagination
|
||||
^^^^^^^^^^
|
||||
|
||||
Pagination is done using two query parameters called ``limit`` and ``offset``. ``limit`` sets the number of results to return and ``offset`` is the index of the first result to return. The default and maximum value for ``limit`` is ``20``. The maximum value can be changed using the ``WAGTAILAPI_LIMIT_MAX`` setting. Set ``WAGTAILAPI_LIMIT_MAX`` to ``None`` for no maximum value.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?limit=1&offset=1
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 2
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "demo.BlogIndexPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/3/"
|
||||
},
|
||||
"title": "Blog"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Pagination will not change the ``total_count`` value in the meta.
|
||||
|
||||
|
||||
Searching
|
||||
^^^^^^^^^
|
||||
|
||||
To perform a full-text search, set the ``search`` parameter to the query string you would like to search on.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?search=Blog
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "demo.BlogIndexPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/3/"
|
||||
},
|
||||
"title": "Blog"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/4/"
|
||||
},
|
||||
"title": "My blog 1",
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
},
|
||||
"title": "My blog 2",
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"title": "My blog 3",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
The results are ordered by relevance. It is not possible to use the ``order`` parameter with a search query.
|
||||
|
||||
If your Wagtail site is using Elasticsearch, you do not need to select a type to access specific fields. This will search anything that's defined in the models' ``search_fields``.
|
||||
|
||||
|
||||
The detail view (``/api/v1/pages/{id}/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This view gives you access to all of the details for a particular page.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/6/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/6/"
|
||||
},
|
||||
"parent": {
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "demo.BlogIndexPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/3/"
|
||||
}
|
||||
},
|
||||
"title": "My blog 3",
|
||||
"date_posted": "2015-01-25",
|
||||
"feed_image": {
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/3/"
|
||||
}
|
||||
},
|
||||
"related_links": [
|
||||
{
|
||||
"title": "Other blog page",
|
||||
"page": {
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "demo.BlogPage",
|
||||
"detail_url": "http://api.example.com/api/v1/pages/5/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
The format is the same as that which is returned inside the listing view, with two additions:
|
||||
- All of the available fields are added to the detail page by default
|
||||
- A ``parent`` field has been included that contains information about the parent page
|
||||
|
||||
|
||||
The ``images`` endpoint
|
||||
-----------------------
|
||||
|
||||
This endpoint gives access to all uploaded images. This will use the custom image model if one was specified. Otherwise, it falls back to ``wagtailimages.Image``.
|
||||
|
||||
|
||||
The listing view (``/api/v1/images/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is what a typical response from a ``GET`` request to this listing would look like:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/4/"
|
||||
},
|
||||
"title": "Wagtail by Mark Harkin"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/6/"
|
||||
},
|
||||
"title": "David Mitchell"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Each image object contains the ``id`` and ``title`` of the image.
|
||||
|
||||
|
||||
Getting ``width``, ``height`` and other fields
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Like the pages endpoint, the images endpoint supports the ``fields`` query parameter.
|
||||
|
||||
By default, this will allow you to add the ``width`` and ``height`` fields to your results. If your Wagtail site uses a custom image model, it is possible to have more.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/?fields=title,width,height
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/4/"
|
||||
},
|
||||
"title": "Wagtail by Mark Harkin",
|
||||
"width": 640,
|
||||
"height": 427
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce",
|
||||
"width": 500,
|
||||
"height": 392
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/6/"
|
||||
},
|
||||
"title": "David Mitchell",
|
||||
"width": 360,
|
||||
"height": 282
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Filtering on fields
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Exact matches on field values can be done by using a query parameter with the same name as the field. Any images with the field that exactly matches the value of this parameter will be returned.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/pages/?title=James Joyce
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Ordering
|
||||
^^^^^^^^
|
||||
|
||||
The images endpoint also accepts the ``order`` parameter which should be set to a field name to order by. Field names can be prefixed with a ``-`` to reverse the ordering. It is also possible to order randomly by setting this parameter to ``random``.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/?fields=title,width&order=width
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": 6,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/6/"
|
||||
},
|
||||
"title": "David Mitchell",
|
||||
"width": 360
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce",
|
||||
"width": 500
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/4/"
|
||||
},
|
||||
"title": "Wagtail by Mark Harkin",
|
||||
"width": 640
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Pagination
|
||||
^^^^^^^^^^
|
||||
|
||||
Pagination is done using two query parameters called ``limit`` and ``offset``. ``limit`` sets the number of results to return and ``offset`` is the index of the first result to return. The default and maximum value for ``limit`` is ``20``. The maximum value can be changed using the ``WAGTAILAPI_LIMIT_MAX`` setting. Set ``WAGTAILAPI_LIMIT_MAX`` to ``None`` for no maximum value.
|
||||
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/?limit=1&offset=1
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 3
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce",
|
||||
"width": 500,
|
||||
"height": 392
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Pagination will not change the ``total_count`` value in the meta.
|
||||
|
||||
|
||||
Searching
|
||||
^^^^^^^^^
|
||||
|
||||
To perform a full-text search, set the ``search`` parameter to the query string you would like to search on.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/?search=James
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"total_count": 1
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce",
|
||||
"width": 500,
|
||||
"height": 392
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Like the pages endpoint, the results are ordered by relevance and it is not possible to use the ``order`` parameter with a search query.
|
||||
|
||||
|
||||
|
||||
The detail view (``/api/v1/images/{id}/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This view gives you access to all of the details for a particular image.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/images/5/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 5,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/api/v1/images/5/"
|
||||
},
|
||||
"title": "James Joyce",
|
||||
"width": 500,
|
||||
"height": 392
|
||||
}
|
||||
|
||||
|
||||
The ``documents`` endpoint
|
||||
--------------------------
|
||||
|
||||
This endpoint gives access to all uploaded documents.
|
||||
|
||||
|
||||
The listing view (``/api/v1/documents/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The documents listing supports the same features as the images listing (documented above) but works with Documents instead.
|
||||
|
||||
|
||||
The detail view (``/api/v1/documents/{id}/``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This view gives you access to all of the details for a particular document.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /api/v1/documents/1/
|
||||
|
||||
HTTP 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtaildocs.Document",
|
||||
"detail_url": "http://api.example.com/api/v1/documents/1/",
|
||||
"download_url": "http://api.example.com/documents/1/usage.md"
|
||||
},
|
||||
"title": "Wagtail API usage"
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ Wagtail ships with a variety of extra optional modules.
|
|||
sitemaps
|
||||
frontendcache
|
||||
routablepage
|
||||
api/index
|
||||
modeladmin/index
|
||||
postgres_search
|
||||
searchpromotions
|
||||
|
|
@ -49,11 +48,6 @@ A module for automatically purging pages from a cache (Varnish, Squid, Cloudflar
|
|||
Provides a way of embedding Django URLconfs into pages.
|
||||
|
||||
|
||||
:doc:`api/index`
|
||||
----------------
|
||||
|
||||
A module for adding a read only, JSON based web API to your Wagtail site
|
||||
|
||||
|
||||
:doc:`modeladmin/index`
|
||||
-----------------------
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
default_app_config = 'wagtail.contrib.wagtailapi.apps.WagtailAPIAppConfig'
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class WagtailAPIAppConfig(AppConfig):
|
||||
name = 'wagtail.contrib.wagtailapi'
|
||||
label = 'wagtailapi_v1'
|
||||
verbose_name = "Wagtail API"
|
||||
|
||||
def ready(self):
|
||||
# Install cache purging signal handlers
|
||||
if getattr(settings, 'WAGTAILAPI_USE_FRONTENDCACHE', False):
|
||||
if apps.is_installed('wagtail.contrib.wagtailfrontendcache'):
|
||||
from wagtail.contrib.wagtailapi.signal_handlers import register_signal_handlers
|
||||
register_signal_handlers()
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"The setting 'WAGTAILAPI_USE_FRONTENDCACHE' is True but "
|
||||
"'wagtail.contrib.wagtailfrontendcache' is not in INSTALLED_APPS."
|
||||
)
|
||||
|
||||
if not apps.is_installed('rest_framework'):
|
||||
raise ImproperlyConfigured(
|
||||
"The 'wagtailapi' module requires Django REST framework. "
|
||||
"Please add 'rest_framework' to INSTALLED_APPS."
|
||||
)
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from wagtail.api import APIField
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailcore.utils import resolve_model_string
|
||||
from wagtail.wagtaildocs.models import get_document_model
|
||||
from wagtail.wagtailimages import get_image_model
|
||||
|
||||
from .filters import ChildOfFilter, DescendantOfFilter, FieldsFilter, OrderingFilter, SearchFilter
|
||||
from .pagination import WagtailPagination
|
||||
from .serializers import (
|
||||
BaseSerializer, DocumentSerializer, ImageSerializer, PageSerializer, get_serializer_class)
|
||||
from .utils import BadRequestError
|
||||
|
||||
|
||||
class BaseAPIEndpoint(GenericViewSet):
|
||||
renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
|
||||
pagination_class = WagtailPagination
|
||||
base_serializer_class = BaseSerializer
|
||||
filter_backends = []
|
||||
model = None # Set on subclass
|
||||
|
||||
known_query_parameters = frozenset([
|
||||
'limit',
|
||||
'offset',
|
||||
'fields',
|
||||
'order',
|
||||
'search',
|
||||
|
||||
# Used by jQuery for cache-busting. See #1671
|
||||
'_',
|
||||
|
||||
# Required by BrowsableAPIRenderer
|
||||
'format',
|
||||
])
|
||||
extra_api_fields = []
|
||||
name = None # Set on subclass.
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.all().order_by('id')
|
||||
|
||||
def listing_view(self, request):
|
||||
queryset = self.get_queryset()
|
||||
self.check_query_parameters(queryset)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
queryset = self.paginate_queryset(queryset)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
def detail_view(self, request, pk):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
if isinstance(exc, Http404):
|
||||
data = {'message': str(exc)}
|
||||
return Response(data, status=status.HTTP_404_NOT_FOUND)
|
||||
elif isinstance(exc, BadRequestError):
|
||||
data = {'message': str(exc)}
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super(BaseAPIEndpoint, self).handle_exception(exc)
|
||||
|
||||
def get_api_fields(self, model):
|
||||
"""
|
||||
This returns a list of field names that are allowed to
|
||||
be used in the API (excluding the id field).
|
||||
"""
|
||||
api_fields = self.extra_api_fields[:]
|
||||
|
||||
if hasattr(model, 'api_fields'):
|
||||
api_fields.extend(model.api_fields)
|
||||
|
||||
# Remove any new-style API field configs (only supported in v2)
|
||||
def convert_api_fields(fields):
|
||||
for field in fields:
|
||||
if isinstance(field, APIField):
|
||||
warnings.warn(
|
||||
"class-based api_fields are not supported by the v1 API module. "
|
||||
"Please update the .api_fields attribute of {}.{} or update to the "
|
||||
"v2 API.".format(model._meta.app_label, model.__name__)
|
||||
)
|
||||
|
||||
# Ignore fields with custom serializers
|
||||
if field.serializer is None:
|
||||
yield field.name
|
||||
else:
|
||||
yield field
|
||||
|
||||
return list(convert_api_fields(api_fields))
|
||||
|
||||
def check_query_parameters(self, queryset):
|
||||
"""
|
||||
Ensure that only valid query paramters are included in the URL.
|
||||
"""
|
||||
query_parameters = set(self.request.GET.keys())
|
||||
|
||||
# All query paramters must be either a field or an operation
|
||||
allowed_parameters = set(self.get_api_fields(queryset.model))
|
||||
allowed_parameters = allowed_parameters.union(self.known_query_parameters)
|
||||
allowed_parameters.add('id')
|
||||
unknown_parameters = query_parameters - allowed_parameters
|
||||
if unknown_parameters:
|
||||
raise BadRequestError(
|
||||
"query parameter is not an operation or a recognised field: %s"
|
||||
% ', '.join(sorted(unknown_parameters))
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
request = self.request
|
||||
|
||||
# Get model
|
||||
if self.action == 'listing_view':
|
||||
model = self.get_queryset().model
|
||||
else:
|
||||
model = type(self.get_object())
|
||||
|
||||
# Get all available fields
|
||||
all_fields = self.get_api_fields(model)
|
||||
# Removes any duplicates in case the developer put "title" in api_fields
|
||||
all_fields = list(OrderedDict.fromkeys(all_fields))
|
||||
|
||||
if self.action == 'listing_view':
|
||||
# Listing views just show the title field and any other allowed field the user specified
|
||||
if 'fields' in request.GET:
|
||||
fields = set(request.GET['fields'].split(','))
|
||||
else:
|
||||
fields = {'title'}
|
||||
|
||||
unknown_fields = fields - set(all_fields)
|
||||
|
||||
if unknown_fields:
|
||||
raise BadRequestError("unknown fields: %s" % ', '.join(sorted(unknown_fields)))
|
||||
|
||||
# Reorder fields so it matches the order of all_fields
|
||||
fields = [field for field in all_fields if field in fields]
|
||||
else:
|
||||
# Detail views show all fields all the time
|
||||
fields = all_fields
|
||||
|
||||
# Always show id and meta first
|
||||
fields = ['id', 'meta'] + fields
|
||||
|
||||
# If showing details, add the parent field
|
||||
if isinstance(self, PagesAPIEndpoint) and self.get_serializer_context().get('show_details', False):
|
||||
fields.insert(2, 'parent')
|
||||
|
||||
return get_serializer_class(model, fields, base=self.base_serializer_class)
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
The serialization context differs between listing and detail views.
|
||||
"""
|
||||
context = {
|
||||
'request': self.request,
|
||||
'view': self,
|
||||
'router': self.request.wagtailapi_router
|
||||
}
|
||||
|
||||
if self.action == 'detail_view':
|
||||
context['show_details'] = True
|
||||
|
||||
return context
|
||||
|
||||
def get_renderer_context(self):
|
||||
context = super(BaseAPIEndpoint, self).get_renderer_context()
|
||||
context['indent'] = 4
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def get_urlpatterns(cls):
|
||||
"""
|
||||
This returns a list of URL patterns for the endpoint
|
||||
"""
|
||||
return [
|
||||
url(r'^$', cls.as_view({'get': 'listing_view'}), name='listing'),
|
||||
url(r'^(?P<pk>\d+)/$', cls.as_view({'get': 'detail_view'}), name='detail'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_object_detail_urlpath(cls, model, pk, namespace=''):
|
||||
if namespace:
|
||||
url_name = namespace + ':detail'
|
||||
else:
|
||||
url_name = 'detail'
|
||||
|
||||
return reverse(url_name, args=(pk, ))
|
||||
|
||||
|
||||
class PagesAPIEndpoint(BaseAPIEndpoint):
|
||||
base_serializer_class = PageSerializer
|
||||
filter_backends = [
|
||||
FieldsFilter,
|
||||
ChildOfFilter,
|
||||
DescendantOfFilter,
|
||||
OrderingFilter,
|
||||
SearchFilter
|
||||
]
|
||||
known_query_parameters = BaseAPIEndpoint.known_query_parameters.union([
|
||||
'type',
|
||||
'child_of',
|
||||
'descendant_of',
|
||||
])
|
||||
extra_api_fields = ['title']
|
||||
name = 'pages'
|
||||
model = Page
|
||||
|
||||
def get_queryset(self):
|
||||
request = self.request
|
||||
|
||||
# Allow pages to be filtered to a specific type
|
||||
if 'type' not in request.GET:
|
||||
model = Page
|
||||
else:
|
||||
model_name = request.GET['type']
|
||||
try:
|
||||
model = resolve_model_string(model_name)
|
||||
except LookupError:
|
||||
raise BadRequestError("type doesn't exist")
|
||||
if not issubclass(model, Page):
|
||||
raise BadRequestError("type doesn't exist")
|
||||
|
||||
# Get live pages that are not in a private section
|
||||
queryset = model.objects.public().live()
|
||||
|
||||
# Filter by site
|
||||
queryset = queryset.descendant_of(request.site.root_page, inclusive=True)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
base = super(PagesAPIEndpoint, self).get_object()
|
||||
return base.specific
|
||||
|
||||
|
||||
class ImagesAPIEndpoint(BaseAPIEndpoint):
|
||||
base_serializer_class = ImageSerializer
|
||||
filter_backends = [FieldsFilter, OrderingFilter, SearchFilter]
|
||||
extra_api_fields = ['title', 'tags', 'width', 'height']
|
||||
name = 'images'
|
||||
model = get_image_model()
|
||||
|
||||
|
||||
class DocumentsAPIEndpoint(BaseAPIEndpoint):
|
||||
base_serializer_class = DocumentSerializer
|
||||
filter_backends = [FieldsFilter, OrderingFilter, SearchFilter]
|
||||
extra_api_fields = ['title', 'tags']
|
||||
name = 'documents'
|
||||
model = get_document_model()
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from taggit.managers import _TaggableManager
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailsearch.backends import get_search_backend
|
||||
|
||||
from .utils import BadRequestError, pages_for_site
|
||||
|
||||
|
||||
class FieldsFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
"""
|
||||
This performs field level filtering on the result set
|
||||
Eg: ?title=James Joyce
|
||||
"""
|
||||
fields = set(view.get_api_fields(queryset.model)).union({'id'})
|
||||
|
||||
for field_name, value in request.GET.items():
|
||||
if field_name in fields:
|
||||
field = getattr(queryset.model, field_name, None)
|
||||
|
||||
if isinstance(field, _TaggableManager):
|
||||
for tag in value.split(','):
|
||||
queryset = queryset.filter(**{field_name + '__name': tag})
|
||||
|
||||
# Stick a message on the queryset to indicate that tag filtering has been performed
|
||||
# This will let the do_search method know that it must raise an error as searching
|
||||
# and tag filtering at the same time is not supported
|
||||
queryset._filtered_by_tag = True
|
||||
else:
|
||||
queryset = queryset.filter(**{field_name: value})
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
"""
|
||||
This applies ordering to the result set
|
||||
Eg: ?order=title
|
||||
|
||||
It also supports reverse ordering
|
||||
Eg: ?order=-title
|
||||
|
||||
And random ordering
|
||||
Eg: ?order=random
|
||||
"""
|
||||
if 'order' in request.GET:
|
||||
# Prevent ordering while searching
|
||||
if 'search' in request.GET:
|
||||
raise BadRequestError("ordering with a search query is not supported")
|
||||
|
||||
order_by = request.GET['order']
|
||||
|
||||
# Random ordering
|
||||
if order_by == 'random':
|
||||
# Prevent ordering by random with offset
|
||||
if 'offset' in request.GET:
|
||||
raise BadRequestError("random ordering with offset is not supported")
|
||||
|
||||
return queryset.order_by('?')
|
||||
|
||||
# Check if reverse ordering is set
|
||||
if order_by.startswith('-'):
|
||||
reverse_order = True
|
||||
order_by = order_by[1:]
|
||||
else:
|
||||
reverse_order = False
|
||||
|
||||
# Add ordering
|
||||
if order_by == 'id' or order_by in view.get_api_fields(queryset.model):
|
||||
queryset = queryset.order_by(order_by)
|
||||
else:
|
||||
# Unknown field
|
||||
raise BadRequestError("cannot order by '%s' (unknown field)" % order_by)
|
||||
|
||||
# Reverse order
|
||||
if reverse_order:
|
||||
queryset = queryset.reverse()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
"""
|
||||
This performs a full-text search on the result set
|
||||
Eg: ?search=James Joyce
|
||||
"""
|
||||
search_enabled = getattr(settings, 'WAGTAILAPI_SEARCH_ENABLED', True)
|
||||
|
||||
if 'search' in request.GET:
|
||||
if not search_enabled:
|
||||
raise BadRequestError("search is disabled")
|
||||
|
||||
# Searching and filtering by tag at the same time is not supported
|
||||
if getattr(queryset, '_filtered_by_tag', False):
|
||||
raise BadRequestError("filtering by tag with a search query is not supported")
|
||||
|
||||
search_query = request.GET['search']
|
||||
|
||||
sb = get_search_backend()
|
||||
queryset = sb.search(search_query, queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class ChildOfFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
if 'child_of' in request.GET:
|
||||
try:
|
||||
parent_page_id = int(request.GET['child_of'])
|
||||
assert parent_page_id >= 0
|
||||
except (ValueError, AssertionError):
|
||||
raise BadRequestError("child_of must be a positive integer")
|
||||
|
||||
site_pages = pages_for_site(request.site)
|
||||
try:
|
||||
parent_page = site_pages.get(id=parent_page_id)
|
||||
queryset = queryset.child_of(parent_page)
|
||||
queryset._filtered_by_child_of = True
|
||||
return queryset
|
||||
except Page.DoesNotExist:
|
||||
raise BadRequestError("parent page doesn't exist")
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class DescendantOfFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
if 'descendant_of' in request.GET:
|
||||
if getattr(queryset, '_filtered_by_child_of', False):
|
||||
raise BadRequestError("filtering by descendant_of with child_of is not supported")
|
||||
try:
|
||||
ancestor_page_id = int(request.GET['descendant_of'])
|
||||
assert ancestor_page_id >= 0
|
||||
except (ValueError, AssertionError):
|
||||
raise BadRequestError("descendant_of must be a positive integer")
|
||||
|
||||
site_pages = pages_for_site(request.site)
|
||||
try:
|
||||
ancestor_page = site_pages.get(id=ancestor_page_id)
|
||||
return queryset.descendant_of(ancestor_page)
|
||||
except Page.DoesNotExist:
|
||||
raise BadRequestError("ancestor page doesn't exist")
|
||||
|
||||
return queryset
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.pagination import BasePagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .utils import BadRequestError
|
||||
|
||||
|
||||
class WagtailPagination(BasePagination):
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
limit_max = getattr(settings, 'WAGTAILAPI_LIMIT_MAX', 20)
|
||||
|
||||
try:
|
||||
offset = int(request.GET.get('offset', 0))
|
||||
assert offset >= 0
|
||||
except (ValueError, AssertionError):
|
||||
raise BadRequestError("offset must be a positive integer")
|
||||
|
||||
try:
|
||||
limit_default = 20 if not limit_max else min(20, limit_max)
|
||||
limit = int(request.GET.get('limit', limit_default))
|
||||
|
||||
if limit_max and limit > limit_max:
|
||||
raise BadRequestError("limit cannot be higher than %d" % limit_max)
|
||||
|
||||
assert limit >= 0
|
||||
except (ValueError, AssertionError):
|
||||
raise BadRequestError("limit must be a positive integer")
|
||||
|
||||
start = offset
|
||||
stop = offset + limit
|
||||
|
||||
self.view = view
|
||||
self.total_count = queryset.count()
|
||||
return queryset[start:stop]
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
data = OrderedDict([
|
||||
('meta', OrderedDict([
|
||||
('total_count', self.total_count),
|
||||
])),
|
||||
(self.view.name, data),
|
||||
])
|
||||
return Response(data)
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import functools
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from wagtail.utils.urlpatterns import decorate_urlpatterns
|
||||
|
||||
|
||||
class WagtailAPIRouter(object):
|
||||
"""
|
||||
A class that provides routing and cross-linking for a collection
|
||||
of API endpoints
|
||||
"""
|
||||
def __init__(self, url_namespace):
|
||||
self.url_namespace = url_namespace
|
||||
self._endpoints = {}
|
||||
|
||||
def register_endpoint(self, name, class_):
|
||||
self._endpoints[name] = class_
|
||||
|
||||
def get_model_endpoint(self, model):
|
||||
"""
|
||||
Finds the endpoint in the API that represents a model
|
||||
|
||||
Returns a (name, endpoint_class) tuple. Or None if an
|
||||
endpoint is not found.
|
||||
"""
|
||||
for name, class_ in self._endpoints.items():
|
||||
if issubclass(model, class_.model):
|
||||
return name, class_
|
||||
|
||||
def get_object_detail_urlpath(self, model, pk):
|
||||
"""
|
||||
Returns a URL path (excluding scheme and hostname) to the detail
|
||||
page of an object.
|
||||
|
||||
Returns None if the object is not represented by any endpoints.
|
||||
"""
|
||||
endpoint = self.get_model_endpoint(model)
|
||||
|
||||
if endpoint:
|
||||
endpoint_name, endpoint_class = endpoint[0], endpoint[1]
|
||||
url_namespace = self.url_namespace + ':' + endpoint_name
|
||||
return endpoint_class.get_object_detail_urlpath(model, pk, namespace=url_namespace)
|
||||
|
||||
def wrap_view(self, func):
|
||||
@functools.wraps(func)
|
||||
def wrapped(request, *args, **kwargs):
|
||||
request.wagtailapi_router = self
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
def get_urlpatterns(self):
|
||||
urlpatterns = []
|
||||
|
||||
for name, class_ in self._endpoints.items():
|
||||
pattern = url(
|
||||
r'^{}/'.format(name),
|
||||
include(class_.get_urlpatterns(), namespace=name)
|
||||
)
|
||||
urlpatterns.append(pattern)
|
||||
|
||||
decorate_urlpatterns(urlpatterns, self.wrap_view)
|
||||
|
||||
return urlpatterns
|
||||
|
||||
@property
|
||||
def urls(self):
|
||||
"""
|
||||
A shortcut to allow quick registration of the API in a URLconf.
|
||||
|
||||
Use with Django's include() function:
|
||||
|
||||
url(r'api/', include(myapi.urls)),
|
||||
"""
|
||||
return self.get_urlpatterns(), self.url_namespace, self.url_namespace
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from modelcluster.models import get_all_child_relations
|
||||
from rest_framework import relations, serializers
|
||||
from rest_framework.fields import Field
|
||||
from taggit.managers import _TaggableManager
|
||||
|
||||
from wagtail.wagtailcore import fields as wagtailcore_fields
|
||||
|
||||
from .utils import get_full_url, pages_for_site
|
||||
|
||||
|
||||
def get_object_detail_url(context, model, pk):
|
||||
url_path = context['router'].get_object_detail_urlpath(model, pk)
|
||||
|
||||
if url_path:
|
||||
return get_full_url(context['request'], url_path)
|
||||
|
||||
|
||||
class MetaField(Field):
|
||||
"""
|
||||
Serializes the "meta" section of each object.
|
||||
|
||||
This section is used for storing non-field data such as model name, urls, etc.
|
||||
|
||||
Example:
|
||||
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/v1/images/1/"
|
||||
}
|
||||
"""
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, obj):
|
||||
return OrderedDict([
|
||||
('type', type(obj)._meta.app_label + '.' + type(obj).__name__),
|
||||
('detail_url', get_object_detail_url(self.context, type(obj), obj.pk)),
|
||||
])
|
||||
|
||||
|
||||
class PageMetaField(MetaField):
|
||||
"""
|
||||
A subclass of MetaField for Page objects.
|
||||
|
||||
Changes the "type" field to use the name of the specific model of the page.
|
||||
|
||||
Example:
|
||||
|
||||
"meta": {
|
||||
"type": "blog.BlogPage",
|
||||
"detail_url": "http://api.example.com/v1/pages/1/"
|
||||
}
|
||||
"""
|
||||
def to_representation(self, page):
|
||||
return OrderedDict([
|
||||
('type', page.specific_class._meta.app_label + '.' + page.specific_class.__name__),
|
||||
('detail_url', get_object_detail_url(self.context, type(page), page.pk)),
|
||||
])
|
||||
|
||||
|
||||
class DocumentMetaField(MetaField):
|
||||
"""
|
||||
A subclass of MetaField for Document objects.
|
||||
|
||||
Adds a "download_url" field.
|
||||
|
||||
"meta": {
|
||||
"type": "wagtaildocs.Document",
|
||||
"detail_url": "http://api.example.com/v1/documents/1/",
|
||||
"download_url": "http://api.example.com/documents/1/my_document.pdf"
|
||||
}
|
||||
"""
|
||||
def to_representation(self, document):
|
||||
data = OrderedDict([
|
||||
('type', "wagtaildocs.Document"),
|
||||
('detail_url', get_object_detail_url(self.context, type(document), document.pk)),
|
||||
])
|
||||
|
||||
# Add download url
|
||||
if self.context.get('show_details', False):
|
||||
data['download_url'] = get_full_url(self.context['request'], document.url)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RelatedField(relations.RelatedField):
|
||||
"""
|
||||
Serializes related objects (eg, foreign keys).
|
||||
|
||||
Example:
|
||||
|
||||
"feed_image": {
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/v1/images/1/"
|
||||
}
|
||||
}
|
||||
"""
|
||||
meta_field_serializer_class = MetaField
|
||||
|
||||
def to_representation(self, value):
|
||||
meta_serializer = self.meta_field_serializer_class()
|
||||
meta_serializer.bind('meta', self)
|
||||
|
||||
return OrderedDict([
|
||||
('id', value.pk),
|
||||
('meta', meta_serializer.to_representation(value)),
|
||||
])
|
||||
|
||||
|
||||
class PageParentField(RelatedField):
|
||||
"""
|
||||
Serializes the "parent" field on Page objects.
|
||||
|
||||
Pages don't have a "parent" field so some extra logic is needed to find the
|
||||
parent page. That logic is implemented in this class.
|
||||
|
||||
The representation is the same as the RelatedField class.
|
||||
"""
|
||||
meta_field_serializer_class = PageMetaField
|
||||
|
||||
def get_attribute(self, instance):
|
||||
parent = instance.get_parent()
|
||||
|
||||
site_pages = pages_for_site(self.context['request'].site)
|
||||
if site_pages.filter(id=parent.id).exists():
|
||||
return parent
|
||||
|
||||
|
||||
class ChildRelationField(Field):
|
||||
"""
|
||||
Serializes child relations.
|
||||
|
||||
Child relations are any model that is related to a Page using a ParentalKey.
|
||||
They are used for repeated fields on a page such as carousel items or related
|
||||
links.
|
||||
|
||||
Child objects are part of the pages content so we nest them. The relation is
|
||||
represented as a list of objects.
|
||||
|
||||
Example:
|
||||
|
||||
"carousel_items": [
|
||||
{
|
||||
"title": "First carousel item",
|
||||
"image": {
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtailimages.Image",
|
||||
"detail_url": "http://api.example.com/v1/images/1/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"carousel_items": [
|
||||
{
|
||||
"title": "Second carousel item (no image)",
|
||||
"image": null
|
||||
}
|
||||
]
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.child_fields = kwargs.pop('child_fields')
|
||||
super(ChildRelationField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
serializer_class = get_serializer_class(value.model, self.child_fields)
|
||||
serializer = serializer_class(context=self.context)
|
||||
|
||||
return [
|
||||
serializer.to_representation(child_object)
|
||||
for child_object in value.all()
|
||||
]
|
||||
|
||||
|
||||
class StreamField(Field):
|
||||
"""
|
||||
Serializes StreamField values.
|
||||
|
||||
Stream fields are stored in JSON format in the database. We reuse that in
|
||||
the API.
|
||||
|
||||
Example:
|
||||
|
||||
"body": [
|
||||
{
|
||||
"type": "heading",
|
||||
"value": {
|
||||
"text": "Hello world!",
|
||||
"size": "h1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"value": "Some content"
|
||||
}
|
||||
{
|
||||
"type": "image",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
|
||||
Where "heading" is a struct block containing "text" and "size" fields, and
|
||||
"paragraph" is a simple text block.
|
||||
|
||||
Note that foreign keys are represented slightly differently in stream fields
|
||||
to other parts of the API. In stream fields, a foreign key is represented
|
||||
by an integer (the ID of the related object) but elsewhere in the API,
|
||||
foreign objects are nested objects with id and meta as attributes.
|
||||
"""
|
||||
def to_representation(self, value):
|
||||
return value.stream_block.get_prep_value(value)
|
||||
|
||||
|
||||
class TagsField(Field):
|
||||
"""
|
||||
Serializes django-taggit TaggableManager fields.
|
||||
|
||||
These fields are a common way to link tags to objects in Wagtail. The API
|
||||
serializes these as a list of strings taken from the name attribute of each
|
||||
tag.
|
||||
|
||||
Example:
|
||||
|
||||
"tags": ["bird", "wagtail"]
|
||||
"""
|
||||
def to_representation(self, value):
|
||||
return list(value.all().order_by('name').values_list('name', flat=True))
|
||||
|
||||
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
# Add StreamField to serializer_field_mapping
|
||||
serializer_field_mapping = serializers.ModelSerializer.serializer_field_mapping.copy()
|
||||
serializer_field_mapping.update({
|
||||
wagtailcore_fields.StreamField: StreamField,
|
||||
})
|
||||
serializer_related_field = RelatedField
|
||||
|
||||
meta = MetaField()
|
||||
|
||||
def build_property_field(self, field_name, model_class):
|
||||
# TaggableManager is not a Django field so it gets treated as a property
|
||||
field = getattr(model_class, field_name)
|
||||
if isinstance(field, _TaggableManager):
|
||||
return TagsField, {}
|
||||
|
||||
return super(BaseSerializer, self).build_property_field(field_name, model_class)
|
||||
|
||||
|
||||
class PageSerializer(BaseSerializer):
|
||||
meta = PageMetaField()
|
||||
parent = PageParentField(read_only=True)
|
||||
|
||||
def build_relational_field(self, field_name, relation_info):
|
||||
# Find all relation fields that point to child class and make them use
|
||||
# the ChildRelationField class.
|
||||
if relation_info.to_many:
|
||||
model = getattr(self.Meta, 'model')
|
||||
child_relations = {
|
||||
child_relation.field.rel.related_name: child_relation.related_model
|
||||
for child_relation in get_all_child_relations(model)
|
||||
}
|
||||
|
||||
if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
|
||||
return ChildRelationField, {'child_fields': child_relations[field_name].api_fields}
|
||||
|
||||
return super(PageSerializer, self).build_relational_field(field_name, relation_info)
|
||||
|
||||
|
||||
class ImageSerializer(BaseSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class DocumentSerializer(BaseSerializer):
|
||||
meta = DocumentMetaField()
|
||||
|
||||
|
||||
def get_serializer_class(model_, fields_, base=BaseSerializer):
|
||||
class Meta:
|
||||
model = model_
|
||||
fields = fields_
|
||||
|
||||
return type(str(model_.__name__ + 'Serializer'), (base, ), {
|
||||
'Meta': Meta
|
||||
})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
|
||||
from wagtail.contrib.wagtailfrontendcache.utils import purge_url_from_cache
|
||||
from wagtail.wagtailcore.models import get_page_models
|
||||
from wagtail.wagtailcore.signals import page_published, page_unpublished
|
||||
from wagtail.wagtaildocs.models import get_document_model
|
||||
from wagtail.wagtailimages import get_image_model
|
||||
|
||||
from .utils import get_base_url
|
||||
|
||||
|
||||
def purge_page_from_cache(instance, **kwargs):
|
||||
base_url = get_base_url()
|
||||
purge_url_from_cache(base_url + reverse('wagtailapi_v1:pages:detail', args=(instance.id, )))
|
||||
|
||||
|
||||
def purge_image_from_cache(instance, **kwargs):
|
||||
if not kwargs.get('created', False):
|
||||
base_url = get_base_url()
|
||||
purge_url_from_cache(base_url + reverse('wagtailapi_v1:images:detail', args=(instance.id, )))
|
||||
|
||||
|
||||
def purge_document_from_cache(instance, **kwargs):
|
||||
if not kwargs.get('created', False):
|
||||
base_url = get_base_url()
|
||||
purge_url_from_cache(base_url + reverse('wagtailapi_v1:documents:detail', args=(instance.id, )))
|
||||
|
||||
|
||||
def register_signal_handlers():
|
||||
Image = get_image_model()
|
||||
Document = get_document_model()
|
||||
|
||||
for model in get_page_models():
|
||||
page_published.connect(purge_page_from_cache, sender=model)
|
||||
page_unpublished.connect(purge_page_from_cache, sender=model)
|
||||
|
||||
post_save.connect(purge_image_from_cache, sender=Image)
|
||||
post_delete.connect(purge_image_from_cache, sender=Image)
|
||||
post_save.connect(purge_document_from_cache, sender=Document)
|
||||
post_delete.connect(purge_document_from_cache, sender=Document)
|
||||
|
||||
|
||||
def unregister_signal_handlers():
|
||||
Image = get_image_model()
|
||||
Document = get_document_model()
|
||||
|
||||
for model in get_page_models():
|
||||
page_published.disconnect(purge_page_from_cache, sender=model)
|
||||
page_unpublished.disconnect(purge_page_from_cache, sender=model)
|
||||
|
||||
post_save.disconnect(purge_image_from_cache, sender=Image)
|
||||
post_delete.disconnect(purge_image_from_cache, sender=Image)
|
||||
post_save.disconnect(purge_document_from_cache, sender=Document)
|
||||
post_delete.disconnect(purge_document_from_cache, sender=Document)
|
||||
|
|
@ -1,393 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
import mock
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from wagtail.contrib.wagtailapi import signal_handlers
|
||||
from wagtail.wagtaildocs.models import get_document_model
|
||||
|
||||
|
||||
class TestDocumentListing(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:documents:listing'), params)
|
||||
|
||||
def get_document_id_list(self, content):
|
||||
return [page['id'] for page in content['documents']]
|
||||
|
||||
|
||||
# BASIC TESTS
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check that the total count is there and correct
|
||||
self.assertIn('total_count', content['meta'])
|
||||
self.assertIsInstance(content['meta']['total_count'], int)
|
||||
self.assertEqual(content['meta']['total_count'], get_document_model().objects.count())
|
||||
|
||||
# Check that the documents section is there
|
||||
self.assertIn('documents', content)
|
||||
self.assertIsInstance(content['documents'], list)
|
||||
|
||||
# Check that each document has a meta section with type and detail_url attributes
|
||||
for document in content['documents']:
|
||||
self.assertIn('meta', document)
|
||||
self.assertIsInstance(document['meta'], dict)
|
||||
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url'})
|
||||
|
||||
# Type should always be wagtaildocs.Document
|
||||
self.assertEqual(document['meta']['type'], 'wagtaildocs.Document')
|
||||
|
||||
# Check detail_url
|
||||
self.assertEqual(document['meta']['detail_url'], 'http://localhost/api/v1/documents/%d/' % document['id'])
|
||||
|
||||
|
||||
# EXTRA FIELDS
|
||||
|
||||
def test_extra_fields_default(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for document in content['documents']:
|
||||
self.assertEqual(set(document.keys()), {'id', 'meta', 'title'})
|
||||
|
||||
def test_extra_fields(self):
|
||||
response = self.get_response(fields='title,tags')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for document in content['documents']:
|
||||
self.assertEqual(set(document.keys()), {'id', 'meta', 'title', 'tags'})
|
||||
|
||||
def test_extra_fields_tags(self):
|
||||
response = self.get_response(fields='tags')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for document in content['documents']:
|
||||
self.assertIsInstance(document['tags'], list)
|
||||
|
||||
def test_extra_fields_which_are_not_in_api_fields_gives_error(self):
|
||||
response = self.get_response(fields='uploaded_by_user')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: uploaded_by_user"})
|
||||
|
||||
def test_extra_fields_unknown_field_gives_error(self):
|
||||
response = self.get_response(fields='123,title,abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
|
||||
|
||||
|
||||
# FILTERING
|
||||
|
||||
def test_filtering_exact_filter(self):
|
||||
response = self.get_response(title='James Joyce')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list, [2])
|
||||
|
||||
def test_filtering_on_id(self):
|
||||
response = self.get_response(id=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list, [10])
|
||||
|
||||
def test_filtering_tags(self):
|
||||
get_document_model().objects.get(id=3).tags.add('test')
|
||||
|
||||
response = self.get_response(tags='test')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list, [3])
|
||||
|
||||
def test_filtering_unknown_field_gives_error(self):
|
||||
response = self.get_response(not_a_field='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "query parameter is not an operation or a recognised field: not_a_field"})
|
||||
|
||||
|
||||
# ORDERING
|
||||
|
||||
def test_ordering_by_title(self):
|
||||
response = self.get_response(order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list, [3, 12, 10, 2, 7, 8, 5, 4, 1, 11, 9, 6])
|
||||
|
||||
def test_ordering_by_title_backwards(self):
|
||||
response = self.get_response(order='-title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list, [6, 9, 11, 1, 4, 5, 8, 7, 2, 10, 12, 3])
|
||||
|
||||
def test_ordering_by_random(self):
|
||||
response_1 = self.get_response(order='random')
|
||||
content_1 = json.loads(response_1.content.decode('UTF-8'))
|
||||
document_id_list_1 = self.get_document_id_list(content_1)
|
||||
|
||||
response_2 = self.get_response(order='random')
|
||||
content_2 = json.loads(response_2.content.decode('UTF-8'))
|
||||
document_id_list_2 = self.get_document_id_list(content_2)
|
||||
|
||||
self.assertNotEqual(document_id_list_1, document_id_list_2)
|
||||
|
||||
def test_ordering_by_random_backwards_gives_error(self):
|
||||
response = self.get_response(order='-random')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'random' (unknown field)"})
|
||||
|
||||
def test_ordering_by_random_with_offset_gives_error(self):
|
||||
response = self.get_response(order='random', offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "random ordering with offset is not supported"})
|
||||
|
||||
def test_ordering_by_unknown_field_gives_error(self):
|
||||
response = self.get_response(order='not_a_field')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'not_a_field' (unknown field)"})
|
||||
|
||||
|
||||
# LIMIT
|
||||
|
||||
def test_limit_only_two_results_returned(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['documents']), 2)
|
||||
|
||||
def test_limit_total_count(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "limit"
|
||||
self.assertEqual(content['meta']['total_count'], get_document_model().objects.count())
|
||||
|
||||
def test_limit_not_integer_gives_error(self):
|
||||
response = self.get_response(limit='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit must be a positive integer"})
|
||||
|
||||
def test_limit_too_high_gives_error(self):
|
||||
response = self.get_response(limit=1000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 20"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=None)
|
||||
def test_limit_max_none_gives_no_errors(self):
|
||||
response = self.get_response(limit=1000000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(content['documents']), get_document_model().objects.count())
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=10)
|
||||
def test_limit_maximum_can_be_changed(self):
|
||||
response = self.get_response(limit=20)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 10"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=2)
|
||||
def test_limit_default_changes_with_max(self):
|
||||
# The default limit is 20. If WAGTAILAPI_LIMIT_MAX is less than that,
|
||||
# the default should change accordingly.
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['documents']), 2)
|
||||
|
||||
|
||||
# OFFSET
|
||||
|
||||
def test_offset_5_usually_appears_5th_in_list(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list.index(5), 4)
|
||||
|
||||
def test_offset_5_moves_after_offset(self):
|
||||
response = self.get_response(offset=4)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
self.assertEqual(document_id_list.index(5), 0)
|
||||
|
||||
def test_offset_total_count(self):
|
||||
response = self.get_response(offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "offset"
|
||||
self.assertEqual(content['meta']['total_count'], get_document_model().objects.count())
|
||||
|
||||
def test_offset_not_integer_gives_error(self):
|
||||
response = self.get_response(offset='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "offset must be a positive integer"})
|
||||
|
||||
|
||||
# SEARCH
|
||||
|
||||
def test_search_for_james_joyce(self):
|
||||
response = self.get_response(search='james')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
document_id_list = self.get_document_id_list(content)
|
||||
|
||||
self.assertEqual(set(document_id_list), set([2]))
|
||||
|
||||
def test_search_when_ordering_gives_error(self):
|
||||
response = self.get_response(search='james', order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "ordering with a search query is not supported"})
|
||||
|
||||
@override_settings(WAGTAILAPI_SEARCH_ENABLED=False)
|
||||
def test_search_when_disabled_gives_error(self):
|
||||
response = self.get_response(search='james')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "search is disabled"})
|
||||
|
||||
def test_search_when_filtering_by_tag_gives_error(self):
|
||||
response = self.get_response(search='james', tags='wagtail')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "filtering by tag with a search query is not supported"})
|
||||
|
||||
|
||||
class TestDocumentDetail(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, image_id, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:documents:detail', args=(image_id, )), params)
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response(1)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check the id field
|
||||
self.assertIn('id', content)
|
||||
self.assertEqual(content['id'], 1)
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check the meta type
|
||||
self.assertIn('type', content['meta'])
|
||||
self.assertEqual(content['meta']['type'], 'wagtaildocs.Document')
|
||||
|
||||
# Check the meta detail_url
|
||||
self.assertIn('detail_url', content['meta'])
|
||||
self.assertEqual(content['meta']['detail_url'], 'http://localhost/api/v1/documents/1/')
|
||||
|
||||
# Check the meta download_url
|
||||
self.assertIn('download_url', content['meta'])
|
||||
self.assertEqual(content['meta']['download_url'], 'http://localhost/documents/1/wagtail_by_markyharky.jpg')
|
||||
|
||||
# Check the title field
|
||||
self.assertIn('title', content)
|
||||
self.assertEqual(content['title'], "Wagtail by mark Harkin")
|
||||
|
||||
# Check the tags field
|
||||
self.assertIn('tags', content)
|
||||
self.assertEqual(content['tags'], [])
|
||||
|
||||
def test_tags(self):
|
||||
get_document_model().objects.get(id=1).tags.add('hello')
|
||||
get_document_model().objects.get(id=1).tags.add('world')
|
||||
|
||||
response = self.get_response(1)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertIn('tags', content)
|
||||
self.assertEqual(content['tags'], ['hello', 'world'])
|
||||
|
||||
@override_settings(WAGTAILAPI_BASE_URL='http://api.example.com/')
|
||||
def test_download_url_with_custom_base_url(self):
|
||||
response = self.get_response(1)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertIn('download_url', content['meta'])
|
||||
self.assertEqual(
|
||||
content['meta']['download_url'], 'http://api.example.com/documents/1/wagtail_by_markyharky.jpg'
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
WAGTAILFRONTENDCACHE={
|
||||
'varnish': {
|
||||
'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend',
|
||||
'LOCATION': 'http://localhost:8000',
|
||||
},
|
||||
},
|
||||
WAGTAILAPI_BASE_URL='http://api.example.com',
|
||||
)
|
||||
@mock.patch('wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend.purge')
|
||||
class TestDocumentCacheInvalidation(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDocumentCacheInvalidation, cls).setUpClass()
|
||||
signal_handlers.register_signal_handlers()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestDocumentCacheInvalidation, cls).tearDownClass()
|
||||
signal_handlers.unregister_signal_handlers()
|
||||
|
||||
def test_resave_document_purges(self, purge):
|
||||
get_document_model().objects.get(id=5).save()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/documents/5/')
|
||||
|
||||
def test_delete_document_purges(self, purge):
|
||||
get_document_model().objects.get(id=5).delete()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/documents/5/')
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
import mock
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from wagtail.contrib.wagtailapi import signal_handlers
|
||||
from wagtail.wagtailimages import get_image_model
|
||||
|
||||
|
||||
class TestImageListing(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:images:listing'), params)
|
||||
|
||||
def get_image_id_list(self, content):
|
||||
return [page['id'] for page in content['images']]
|
||||
|
||||
|
||||
# BASIC TESTS
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check that the total count is there and correct
|
||||
self.assertIn('total_count', content['meta'])
|
||||
self.assertIsInstance(content['meta']['total_count'], int)
|
||||
self.assertEqual(content['meta']['total_count'], get_image_model().objects.count())
|
||||
|
||||
# Check that the images section is there
|
||||
self.assertIn('images', content)
|
||||
self.assertIsInstance(content['images'], list)
|
||||
|
||||
# Check that each image has a meta section with type and detail_url attributes
|
||||
for image in content['images']:
|
||||
self.assertIn('meta', image)
|
||||
self.assertIsInstance(image['meta'], dict)
|
||||
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url'})
|
||||
|
||||
# Type should always be wagtailimages.Image
|
||||
self.assertEqual(image['meta']['type'], 'wagtailimages.Image')
|
||||
|
||||
# Check detail url
|
||||
self.assertEqual(image['meta']['detail_url'], 'http://localhost/api/v1/images/%d/' % image['id'])
|
||||
|
||||
|
||||
# EXTRA FIELDS
|
||||
|
||||
def test_extra_fields_default(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for image in content['images']:
|
||||
self.assertEqual(set(image.keys()), {'id', 'meta', 'title'})
|
||||
|
||||
def test_extra_fields(self):
|
||||
response = self.get_response(fields='title,width,height')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for image in content['images']:
|
||||
self.assertEqual(set(image.keys()), {'id', 'meta', 'title', 'width', 'height'})
|
||||
|
||||
def test_extra_fields_tags(self):
|
||||
response = self.get_response(fields='tags')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for image in content['images']:
|
||||
self.assertEqual(set(image.keys()), {'id', 'meta', 'tags'})
|
||||
self.assertIsInstance(image['tags'], list)
|
||||
|
||||
def test_extra_fields_which_are_not_in_api_fields_gives_error(self):
|
||||
response = self.get_response(fields='uploaded_by_user')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: uploaded_by_user"})
|
||||
|
||||
def test_extra_fields_unknown_field_gives_error(self):
|
||||
response = self.get_response(fields='123,title,abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
|
||||
|
||||
|
||||
# FILTERING
|
||||
|
||||
def test_filtering_exact_filter(self):
|
||||
response = self.get_response(title='James Joyce')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list, [5])
|
||||
|
||||
def test_filtering_on_id(self):
|
||||
response = self.get_response(id=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list, [10])
|
||||
|
||||
def test_filtering_tags(self):
|
||||
get_image_model().objects.get(id=6).tags.add('test')
|
||||
|
||||
response = self.get_response(tags='test')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list, [6])
|
||||
|
||||
def test_filtering_unknown_field_gives_error(self):
|
||||
response = self.get_response(not_a_field='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "query parameter is not an operation or a recognised field: not_a_field"})
|
||||
|
||||
|
||||
# ORDERING
|
||||
|
||||
def test_ordering_by_title(self):
|
||||
response = self.get_response(order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list, [6, 15, 13, 5, 10, 11, 8, 7, 4, 14, 12, 9])
|
||||
|
||||
def test_ordering_by_title_backwards(self):
|
||||
response = self.get_response(order='-title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list, [9, 12, 14, 4, 7, 8, 11, 10, 5, 13, 15, 6])
|
||||
|
||||
def test_ordering_by_random(self):
|
||||
response_1 = self.get_response(order='random')
|
||||
content_1 = json.loads(response_1.content.decode('UTF-8'))
|
||||
image_id_list_1 = self.get_image_id_list(content_1)
|
||||
|
||||
response_2 = self.get_response(order='random')
|
||||
content_2 = json.loads(response_2.content.decode('UTF-8'))
|
||||
image_id_list_2 = self.get_image_id_list(content_2)
|
||||
|
||||
self.assertNotEqual(image_id_list_1, image_id_list_2)
|
||||
|
||||
def test_ordering_by_random_backwards_gives_error(self):
|
||||
response = self.get_response(order='-random')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'random' (unknown field)"})
|
||||
|
||||
def test_ordering_by_random_with_offset_gives_error(self):
|
||||
response = self.get_response(order='random', offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "random ordering with offset is not supported"})
|
||||
|
||||
def test_ordering_by_unknown_field_gives_error(self):
|
||||
response = self.get_response(order='not_a_field')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'not_a_field' (unknown field)"})
|
||||
|
||||
|
||||
# LIMIT
|
||||
|
||||
def test_limit_only_two_results_returned(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['images']), 2)
|
||||
|
||||
def test_limit_total_count(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "limit"
|
||||
self.assertEqual(content['meta']['total_count'], get_image_model().objects.count())
|
||||
|
||||
def test_limit_not_integer_gives_error(self):
|
||||
response = self.get_response(limit='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit must be a positive integer"})
|
||||
|
||||
def test_limit_too_high_gives_error(self):
|
||||
response = self.get_response(limit=1000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 20"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=None)
|
||||
def test_limit_max_none_gives_no_errors(self):
|
||||
response = self.get_response(limit=1000000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(content['images']), get_image_model().objects.count())
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=10)
|
||||
def test_limit_maximum_can_be_changed(self):
|
||||
response = self.get_response(limit=20)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 10"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=2)
|
||||
def test_limit_default_changes_with_max(self):
|
||||
# The default limit is 20. If WAGTAILAPI_LIMIT_MAX is less than that,
|
||||
# the default should change accordingly.
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['images']), 2)
|
||||
|
||||
|
||||
# OFFSET
|
||||
|
||||
def test_offset_10_usually_appears_7th_in_list(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list.index(10), 6)
|
||||
|
||||
def test_offset_10_moves_after_offset(self):
|
||||
response = self.get_response(offset=4)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
self.assertEqual(image_id_list.index(10), 2)
|
||||
|
||||
def test_offset_total_count(self):
|
||||
response = self.get_response(offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "offset"
|
||||
self.assertEqual(content['meta']['total_count'], get_image_model().objects.count())
|
||||
|
||||
def test_offset_not_integer_gives_error(self):
|
||||
response = self.get_response(offset='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "offset must be a positive integer"})
|
||||
|
||||
|
||||
# SEARCH
|
||||
|
||||
def test_search_for_james_joyce(self):
|
||||
response = self.get_response(search='james')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
image_id_list = self.get_image_id_list(content)
|
||||
|
||||
self.assertEqual(set(image_id_list), set([5]))
|
||||
|
||||
def test_search_when_ordering_gives_error(self):
|
||||
response = self.get_response(search='james', order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "ordering with a search query is not supported"})
|
||||
|
||||
@override_settings(WAGTAILAPI_SEARCH_ENABLED=False)
|
||||
def test_search_when_disabled_gives_error(self):
|
||||
response = self.get_response(search='james')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "search is disabled"})
|
||||
|
||||
def test_search_when_filtering_by_tag_gives_error(self):
|
||||
response = self.get_response(search='james', tags='wagtail')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "filtering by tag with a search query is not supported"})
|
||||
|
||||
|
||||
class TestImageDetail(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, image_id, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:images:detail', args=(image_id, )), params)
|
||||
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response(5)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check the id field
|
||||
self.assertIn('id', content)
|
||||
self.assertEqual(content['id'], 5)
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check the meta type
|
||||
self.assertIn('type', content['meta'])
|
||||
self.assertEqual(content['meta']['type'], 'wagtailimages.Image')
|
||||
|
||||
# Check the meta detail_url
|
||||
self.assertIn('detail_url', content['meta'])
|
||||
self.assertEqual(content['meta']['detail_url'], 'http://localhost/api/v1/images/5/')
|
||||
|
||||
# Check the title field
|
||||
self.assertIn('title', content)
|
||||
self.assertEqual(content['title'], "James Joyce")
|
||||
|
||||
# Check the width and height fields
|
||||
self.assertIn('width', content)
|
||||
self.assertIn('height', content)
|
||||
self.assertEqual(content['width'], 500)
|
||||
self.assertEqual(content['height'], 392)
|
||||
|
||||
# Check the tags field
|
||||
self.assertIn('tags', content)
|
||||
self.assertEqual(content['tags'], [])
|
||||
|
||||
def test_tags(self):
|
||||
image = get_image_model().objects.get(id=5)
|
||||
image.tags.add('hello')
|
||||
image.tags.add('world')
|
||||
|
||||
response = self.get_response(5)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertIn('tags', content)
|
||||
self.assertEqual(content['tags'], ['hello', 'world'])
|
||||
|
||||
|
||||
@override_settings(
|
||||
WAGTAILFRONTENDCACHE={
|
||||
'varnish': {
|
||||
'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend',
|
||||
'LOCATION': 'http://localhost:8000',
|
||||
},
|
||||
},
|
||||
WAGTAILAPI_BASE_URL='http://api.example.com',
|
||||
)
|
||||
@mock.patch('wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend.purge')
|
||||
class TestImageCacheInvalidation(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestImageCacheInvalidation, cls).setUpClass()
|
||||
signal_handlers.register_signal_handlers()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestImageCacheInvalidation, cls).tearDownClass()
|
||||
signal_handlers.unregister_signal_handlers()
|
||||
|
||||
def test_resave_image_purges(self, purge):
|
||||
get_image_model().objects.get(id=5).save()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/images/5/')
|
||||
|
||||
def test_delete_image_purges(self, purge):
|
||||
get_image_model().objects.get(id=5).delete()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/images/5/')
|
||||
|
|
@ -1,746 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import collections
|
||||
import json
|
||||
|
||||
import mock
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from wagtail.contrib.wagtailapi import signal_handlers
|
||||
from wagtail.tests.demosite import models
|
||||
from wagtail.tests.testapp.models import StreamPage
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
|
||||
def get_total_page_count():
|
||||
# Need to take away 1 as the root page is invisible over the API
|
||||
return Page.objects.live().public().count() - 1
|
||||
|
||||
|
||||
class TestPageListing(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:pages:listing'), params)
|
||||
|
||||
def get_page_id_list(self, content):
|
||||
return [page['id'] for page in content['pages']]
|
||||
|
||||
|
||||
# BASIC TESTS
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check that the total count is there and correct
|
||||
self.assertIn('total_count', content['meta'])
|
||||
self.assertIsInstance(content['meta']['total_count'], int)
|
||||
self.assertEqual(content['meta']['total_count'], get_total_page_count())
|
||||
|
||||
# Check that the pages section is there
|
||||
self.assertIn('pages', content)
|
||||
self.assertIsInstance(content['pages'], list)
|
||||
|
||||
# Check that each page has a meta section with type and detail_url attributes
|
||||
for page in content['pages']:
|
||||
self.assertIn('meta', page)
|
||||
self.assertIsInstance(page['meta'], dict)
|
||||
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url'})
|
||||
|
||||
def test_unpublished_pages_dont_appear_in_list(self):
|
||||
total_count = get_total_page_count()
|
||||
|
||||
page = models.BlogEntryPage.objects.get(id=16)
|
||||
page.unpublish()
|
||||
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
self.assertEqual(content['meta']['total_count'], total_count - 1)
|
||||
|
||||
def test_private_pages_dont_appear_in_list(self):
|
||||
total_count = get_total_page_count()
|
||||
|
||||
page = models.BlogIndexPage.objects.get(id=5)
|
||||
page.view_restrictions.create(password='test')
|
||||
|
||||
new_total_count = get_total_page_count()
|
||||
self.assertNotEqual(total_count, new_total_count)
|
||||
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
self.assertEqual(content['meta']['total_count'], new_total_count)
|
||||
|
||||
|
||||
# TYPE FILTER
|
||||
|
||||
def test_type_filter_results_are_all_blog_entries(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
self.assertEqual(page['meta']['type'], 'demosite.BlogEntryPage')
|
||||
|
||||
def test_type_filter_total_count(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Total count must be reduced as this filters the results
|
||||
self.assertEqual(content['meta']['total_count'], 3)
|
||||
|
||||
def test_non_existant_type_gives_error(self):
|
||||
response = self.get_response(type='demosite.IDontExist')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "type doesn't exist"})
|
||||
|
||||
def test_non_page_type_gives_error(self):
|
||||
response = self.get_response(type='auth.User')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "type doesn't exist"})
|
||||
|
||||
# EXTRA FIELDS
|
||||
|
||||
def test_extra_fields_default(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
self.assertEqual(set(page.keys()), {'id', 'meta', 'title'})
|
||||
|
||||
def test_extra_fields(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', fields='title,date,feed_image')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'date', 'feed_image'})
|
||||
|
||||
def test_extra_fields_child_relation(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', fields='title,related_links')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'related_links'})
|
||||
self.assertIsInstance(page['related_links'], list)
|
||||
|
||||
def test_extra_fields_foreign_key(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', fields='title,date,feed_image')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
feed_image = page['feed_image']
|
||||
|
||||
if feed_image is not None:
|
||||
self.assertIsInstance(feed_image, dict)
|
||||
self.assertEqual(set(feed_image.keys()), {'id', 'meta'})
|
||||
self.assertIsInstance(feed_image['id'], int)
|
||||
self.assertIsInstance(feed_image['meta'], dict)
|
||||
self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url'})
|
||||
self.assertEqual(feed_image['meta']['type'], 'wagtailimages.Image')
|
||||
self.assertEqual(
|
||||
feed_image['meta']['detail_url'], 'http://localhost/api/v1/images/%d/' % feed_image['id']
|
||||
)
|
||||
|
||||
def test_extra_fields_tags(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', fields='tags')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
for page in content['pages']:
|
||||
self.assertEqual(set(page.keys()), {'id', 'meta', 'tags'})
|
||||
self.assertIsInstance(page['tags'], list)
|
||||
|
||||
def test_extra_field_ordering(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', fields='date,title,feed_image,related_links')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Test field order
|
||||
content = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(response.content.decode('UTF-8'))
|
||||
field_order = [
|
||||
'id',
|
||||
'meta',
|
||||
'title',
|
||||
'date',
|
||||
'feed_image',
|
||||
'related_links',
|
||||
]
|
||||
self.assertEqual(list(content['pages'][0].keys()), field_order)
|
||||
|
||||
def test_extra_fields_without_type_gives_error(self):
|
||||
response = self.get_response(fields='title,related_links')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: related_links"})
|
||||
|
||||
def test_extra_fields_which_are_not_in_api_fields_gives_error(self):
|
||||
response = self.get_response(fields='path')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: path"})
|
||||
|
||||
def test_extra_fields_unknown_field_gives_error(self):
|
||||
response = self.get_response(fields='123,title,abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
|
||||
|
||||
|
||||
# FILTERING
|
||||
|
||||
def test_filtering_exact_filter(self):
|
||||
response = self.get_response(title='Home page')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [2])
|
||||
|
||||
def test_filtering_exact_filter_on_specific_field(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', date='2013-12-02')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16])
|
||||
|
||||
def test_filtering_on_id(self):
|
||||
response = self.get_response(id=16)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16])
|
||||
|
||||
def test_filtering_doesnt_work_on_specific_fields_without_type(self):
|
||||
response = self.get_response(date='2013-12-02')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "query parameter is not an operation or a recognised field: date"})
|
||||
|
||||
def test_filtering_tags(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', tags='wagtail')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16, 18])
|
||||
|
||||
def test_filtering_multiple_tags(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', tags='wagtail,bird')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16])
|
||||
|
||||
def test_filtering_unknown_field_gives_error(self):
|
||||
response = self.get_response(not_a_field='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "query parameter is not an operation or a recognised field: not_a_field"})
|
||||
|
||||
|
||||
# CHILD OF FILTER
|
||||
|
||||
def test_child_of_filter(self):
|
||||
response = self.get_response(child_of=5)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16, 18, 19])
|
||||
|
||||
def test_child_of_with_type(self):
|
||||
response = self.get_response(type='demosite.EventPage', child_of=5)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [])
|
||||
|
||||
def test_child_of_unknown_page_gives_error(self):
|
||||
response = self.get_response(child_of=1000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "parent page doesn't exist"})
|
||||
|
||||
def test_child_of_not_integer_gives_error(self):
|
||||
response = self.get_response(child_of='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "child_of must be a positive integer"})
|
||||
|
||||
def test_child_of_page_thats_not_in_same_site_gives_error(self):
|
||||
# Root page is not in any site, so pretend it doesn't exist
|
||||
response = self.get_response(child_of=1)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "parent page doesn't exist"})
|
||||
|
||||
|
||||
# DESCENDANT OF FILTER
|
||||
|
||||
def test_descendant_of_filter(self):
|
||||
response = self.get_response(descendant_of=6)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [10, 15, 17, 21, 22, 23])
|
||||
|
||||
def test_descendant_of_with_type(self):
|
||||
response = self.get_response(type='tests.EventPage', descendant_of=6)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [])
|
||||
|
||||
def test_descendant_of_unknown_page_gives_error(self):
|
||||
response = self.get_response(descendant_of=1000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "ancestor page doesn't exist"})
|
||||
|
||||
def test_descendant_of_not_integer_gives_error(self):
|
||||
response = self.get_response(descendant_of='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "descendant_of must be a positive integer"})
|
||||
|
||||
def test_descendant_of_page_thats_not_in_same_site_gives_error(self):
|
||||
# Root page is not in any site, so pretend it doesn't exist
|
||||
response = self.get_response(descendant_of=1)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "ancestor page doesn't exist"})
|
||||
|
||||
def test_descendant_of_when_filtering_by_child_of_gives_error(self):
|
||||
response = self.get_response(descendant_of=6, child_of=5)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "filtering by descendant_of with child_of is not supported"})
|
||||
|
||||
|
||||
# ORDERING
|
||||
|
||||
def test_ordering_default(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12])
|
||||
|
||||
def test_ordering_by_title(self):
|
||||
response = self.get_response(order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [21, 22, 19, 23, 5, 16, 18, 12, 14, 8, 9, 4, 2, 13, 20, 17, 6, 10, 15])
|
||||
|
||||
def test_ordering_by_title_backwards(self):
|
||||
response = self.get_response(order='-title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [15, 10, 6, 17, 20, 13, 2, 4, 9, 8, 14, 12, 18, 16, 5, 23, 19, 22, 21])
|
||||
|
||||
def test_ordering_by_random(self):
|
||||
response_1 = self.get_response(order='random')
|
||||
content_1 = json.loads(response_1.content.decode('UTF-8'))
|
||||
page_id_list_1 = self.get_page_id_list(content_1)
|
||||
|
||||
response_2 = self.get_response(order='random')
|
||||
content_2 = json.loads(response_2.content.decode('UTF-8'))
|
||||
page_id_list_2 = self.get_page_id_list(content_2)
|
||||
|
||||
self.assertNotEqual(page_id_list_1, page_id_list_2)
|
||||
|
||||
def test_ordering_by_random_backwards_gives_error(self):
|
||||
response = self.get_response(order='-random')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'random' (unknown field)"})
|
||||
|
||||
def test_ordering_by_random_with_offset_gives_error(self):
|
||||
response = self.get_response(order='random', offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "random ordering with offset is not supported"})
|
||||
|
||||
def test_ordering_default_with_type(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16, 18, 19])
|
||||
|
||||
def test_ordering_by_title_with_type(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [19, 16, 18])
|
||||
|
||||
def test_ordering_by_specific_field_with_type(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', order='date')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list, [16, 18, 19])
|
||||
|
||||
def test_ordering_by_unknown_field_gives_error(self):
|
||||
response = self.get_response(order='not_a_field')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "cannot order by 'not_a_field' (unknown field)"})
|
||||
|
||||
|
||||
# LIMIT
|
||||
|
||||
def test_limit_only_two_results_returned(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['pages']), 2)
|
||||
|
||||
def test_limit_total_count(self):
|
||||
response = self.get_response(limit=2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "limit"
|
||||
self.assertEqual(content['meta']['total_count'], get_total_page_count())
|
||||
|
||||
def test_limit_not_integer_gives_error(self):
|
||||
response = self.get_response(limit='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit must be a positive integer"})
|
||||
|
||||
def test_limit_too_high_gives_error(self):
|
||||
response = self.get_response(limit=1000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 20"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=None)
|
||||
def test_limit_max_none_gives_no_errors(self):
|
||||
response = self.get_response(limit=1000000)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(content['pages']), get_total_page_count())
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=10)
|
||||
def test_limit_maximum_can_be_changed(self):
|
||||
response = self.get_response(limit=20)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "limit cannot be higher than 10"})
|
||||
|
||||
@override_settings(WAGTAILAPI_LIMIT_MAX=2)
|
||||
def test_limit_default_changes_with_max(self):
|
||||
# The default limit is 20. If WAGTAILAPI_LIMIT_MAX is less than that,
|
||||
# the default should change accordingly.
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(len(content['pages']), 2)
|
||||
|
||||
|
||||
# OFFSET
|
||||
|
||||
def test_offset_5_usually_appears_5th_in_list(self):
|
||||
response = self.get_response()
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list.index(5), 4)
|
||||
|
||||
def test_offset_5_moves_after_offset(self):
|
||||
response = self.get_response(offset=4)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
self.assertEqual(page_id_list.index(5), 0)
|
||||
|
||||
def test_offset_total_count(self):
|
||||
response = self.get_response(offset=10)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# The total count must not be affected by "offset"
|
||||
self.assertEqual(content['meta']['total_count'], get_total_page_count())
|
||||
|
||||
def test_offset_not_integer_gives_error(self):
|
||||
response = self.get_response(offset='abc')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "offset must be a positive integer"})
|
||||
|
||||
|
||||
# SEARCH
|
||||
|
||||
def test_search_for_blog(self):
|
||||
response = self.get_response(search='blog')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
|
||||
# Check that the results are the blog index and three blog pages
|
||||
self.assertEqual(set(page_id_list), set([5, 16, 18, 19]))
|
||||
|
||||
def test_search_with_type(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', search='blog')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
page_id_list = self.get_page_id_list(content)
|
||||
|
||||
self.assertEqual(set(page_id_list), set([16, 18, 19]))
|
||||
|
||||
def test_search_when_ordering_gives_error(self):
|
||||
response = self.get_response(search='blog', order='title')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "ordering with a search query is not supported"})
|
||||
|
||||
@override_settings(WAGTAILAPI_SEARCH_ENABLED=False)
|
||||
def test_search_when_disabled_gives_error(self):
|
||||
response = self.get_response(search='blog')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "search is disabled"})
|
||||
|
||||
def test_search_when_filtering_by_tag_gives_error(self):
|
||||
response = self.get_response(type='demosite.BlogEntryPage', search='blog', tags='wagtail')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(content, {'message': "filtering by tag with a search query is not supported"})
|
||||
|
||||
def test_empty_searches_work(self):
|
||||
response = self.get_response(search='')
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
self.assertEqual(content['meta']['total_count'], 0)
|
||||
|
||||
|
||||
class TestPageDetail(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
def get_response(self, page_id, **params):
|
||||
return self.client.get(reverse('wagtailapi_v1:pages:detail', args=(page_id, )), params)
|
||||
|
||||
def test_basic(self):
|
||||
response = self.get_response(16)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-type'], 'application/json')
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Check the id field
|
||||
self.assertIn('id', content)
|
||||
self.assertEqual(content['id'], 16)
|
||||
|
||||
# Check that the meta section is there
|
||||
self.assertIn('meta', content)
|
||||
self.assertIsInstance(content['meta'], dict)
|
||||
|
||||
# Check the meta type
|
||||
self.assertIn('type', content['meta'])
|
||||
self.assertEqual(content['meta']['type'], 'demosite.BlogEntryPage')
|
||||
|
||||
# Check the meta detail_url
|
||||
self.assertIn('detail_url', content['meta'])
|
||||
self.assertEqual(content['meta']['detail_url'], 'http://localhost/api/v1/pages/16/')
|
||||
|
||||
# Check the parent field
|
||||
self.assertIn('parent', content)
|
||||
self.assertIsInstance(content['parent'], dict)
|
||||
self.assertEqual(set(content['parent'].keys()), {'id', 'meta'})
|
||||
self.assertEqual(content['parent']['id'], 5)
|
||||
self.assertIsInstance(content['parent']['meta'], dict)
|
||||
self.assertEqual(set(content['parent']['meta'].keys()), {'type', 'detail_url'})
|
||||
self.assertEqual(content['parent']['meta']['type'], 'demosite.BlogIndexPage')
|
||||
self.assertEqual(content['parent']['meta']['detail_url'], 'http://localhost/api/v1/pages/5/')
|
||||
|
||||
# Check that the custom fields are included
|
||||
self.assertIn('date', content)
|
||||
self.assertIn('body', content)
|
||||
self.assertIn('tags', content)
|
||||
self.assertIn('feed_image', content)
|
||||
self.assertIn('related_links', content)
|
||||
self.assertIn('carousel_items', content)
|
||||
|
||||
# Check that the date was serialised properly
|
||||
self.assertEqual(content['date'], '2013-12-02')
|
||||
|
||||
# Check that the tags were serialised properly
|
||||
self.assertEqual(content['tags'], ['bird', 'wagtail'])
|
||||
|
||||
# Check that the feed image was serialised properly
|
||||
self.assertIsInstance(content['feed_image'], dict)
|
||||
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta'})
|
||||
self.assertEqual(content['feed_image']['id'], 7)
|
||||
self.assertIsInstance(content['feed_image']['meta'], dict)
|
||||
self.assertEqual(set(content['feed_image']['meta'].keys()), {'type', 'detail_url'})
|
||||
self.assertEqual(content['feed_image']['meta']['type'], 'wagtailimages.Image')
|
||||
self.assertEqual(content['feed_image']['meta']['detail_url'], 'http://localhost/api/v1/images/7/')
|
||||
|
||||
# Check that the child relations were serialised properly
|
||||
self.assertEqual(content['related_links'], [])
|
||||
for carousel_item in content['carousel_items']:
|
||||
self.assertEqual(set(carousel_item.keys()), {'embed_url', 'link', 'caption', 'image'})
|
||||
|
||||
def test_meta_parent_id_doesnt_show_root_page(self):
|
||||
# Root page isn't in the site so don't show it if the user is looking at the home page
|
||||
response = self.get_response(2)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertNotIn('parent', content['meta'])
|
||||
|
||||
def test_field_ordering(self):
|
||||
response = self.get_response(16)
|
||||
|
||||
# Will crash if the JSON is invalid
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
# Test field order
|
||||
content = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(response.content.decode('UTF-8'))
|
||||
field_order = [
|
||||
'id',
|
||||
'meta',
|
||||
'parent',
|
||||
'title',
|
||||
'body',
|
||||
'tags',
|
||||
'date',
|
||||
'feed_image',
|
||||
'carousel_items',
|
||||
'related_links',
|
||||
]
|
||||
self.assertEqual(list(content.keys()), field_order)
|
||||
|
||||
def test_null_foreign_key(self):
|
||||
models.BlogEntryPage.objects.filter(id=16).update(feed_image_id=None)
|
||||
|
||||
response = self.get_response(16)
|
||||
content = json.loads(response.content.decode('UTF-8'))
|
||||
|
||||
self.assertIn('related_links', content)
|
||||
self.assertEqual(content['feed_image'], None)
|
||||
|
||||
|
||||
class TestPageDetailWithStreamField(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def setUp(self):
|
||||
self.homepage = Page.objects.get(url_path='/home/')
|
||||
|
||||
def make_stream_page(self, body):
|
||||
stream_page = StreamPage(
|
||||
title='stream page',
|
||||
body=body
|
||||
)
|
||||
return self.homepage.add_child(instance=stream_page)
|
||||
|
||||
def test_can_fetch_streamfield_content(self):
|
||||
stream_page = self.make_stream_page('[{"type": "text", "value": "foo"}]')
|
||||
|
||||
response_url = reverse('wagtailapi_v1:pages:detail', args=(stream_page.id, ))
|
||||
response = self.client.get(response_url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['content-type'], 'application/json')
|
||||
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
self.assertIn('id', content)
|
||||
self.assertEqual(content['id'], stream_page.id)
|
||||
self.assertIn('body', content)
|
||||
self.assertEqual(len(content['body']), 1)
|
||||
self.assertEqual(content['body'][0]['type'], 'text')
|
||||
self.assertEqual(content['body'][0]['value'], 'foo')
|
||||
self.assertTrue(content['body'][0]['id'])
|
||||
|
||||
def test_image_block(self):
|
||||
stream_page = self.make_stream_page('[{"type": "image", "value": 1}]')
|
||||
|
||||
response_url = reverse('wagtailapi_v1:pages:detail', args=(stream_page.id, ))
|
||||
response = self.client.get(response_url)
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
# ForeignKeys in a StreamField shouldn't be translated into dictionary representation
|
||||
self.assertEqual(content['body'][0]['type'], 'image')
|
||||
self.assertEqual(content['body'][0]['value'], 1)
|
||||
|
||||
|
||||
@override_settings(
|
||||
WAGTAILFRONTENDCACHE={
|
||||
'varnish': {
|
||||
'BACKEND': 'wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend',
|
||||
'LOCATION': 'http://localhost:8000',
|
||||
},
|
||||
},
|
||||
WAGTAILAPI_BASE_URL='http://api.example.com',
|
||||
)
|
||||
@mock.patch('wagtail.contrib.wagtailfrontendcache.backends.HTTPBackend.purge')
|
||||
class TestPageCacheInvalidation(TestCase):
|
||||
fixtures = ['demosite.json']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestPageCacheInvalidation, cls).setUpClass()
|
||||
signal_handlers.register_signal_handlers()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestPageCacheInvalidation, cls).tearDownClass()
|
||||
signal_handlers.unregister_signal_handlers()
|
||||
|
||||
def test_republish_page_purges(self, purge):
|
||||
Page.objects.get(id=2).save_revision().publish()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/pages/2/')
|
||||
|
||||
def test_unpublish_page_purges(self, purge):
|
||||
Page.objects.get(id=2).unpublish()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/pages/2/')
|
||||
|
||||
def test_delete_page_purges(self, purge):
|
||||
Page.objects.get(id=16).delete()
|
||||
|
||||
purge.assert_any_call('http://api.example.com/api/v1/pages/16/')
|
||||
|
||||
def test_save_draft_doesnt_purge(self, purge):
|
||||
Page.objects.get(id=2).save_revision()
|
||||
|
||||
purge.assert_not_called()
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .endpoints import DocumentsAPIEndpoint, ImagesAPIEndpoint, PagesAPIEndpoint
|
||||
from .router import WagtailAPIRouter
|
||||
|
||||
v1 = WagtailAPIRouter('wagtailapi_v1')
|
||||
v1.register_endpoint('pages', PagesAPIEndpoint)
|
||||
v1.register_endpoint('images', ImagesAPIEndpoint)
|
||||
v1.register_endpoint('documents', DocumentsAPIEndpoint)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v1/', v1.urls),
|
||||
]
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
|
||||
class BadRequestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_base_url(request=None):
|
||||
base_url = getattr(settings, 'WAGTAILAPI_BASE_URL', request.site.root_url if request else None)
|
||||
|
||||
if base_url:
|
||||
# We only want the scheme and netloc
|
||||
base_url_parsed = urlparse(base_url)
|
||||
|
||||
return base_url_parsed.scheme + '://' + base_url_parsed.netloc
|
||||
|
||||
|
||||
def get_full_url(request, path):
|
||||
base_url = get_base_url(request) or ''
|
||||
return base_url + path
|
||||
|
||||
|
||||
def pages_for_site(site):
|
||||
pages = Page.objects.public().live()
|
||||
pages = pages.descendant_of(site.root_page, inclusive=True)
|
||||
return pages
|
||||
|
|
@ -114,7 +114,6 @@ INSTALLED_APPS = (
|
|||
'wagtail.contrib.wagtailstyleguide',
|
||||
'wagtail.contrib.wagtailroutablepage',
|
||||
'wagtail.contrib.wagtailfrontendcache',
|
||||
'wagtail.contrib.wagtailapi',
|
||||
'wagtail.contrib.wagtailsearchpromotions',
|
||||
'wagtail.contrib.settings',
|
||||
'wagtail.contrib.modeladmin',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from django.conf.urls import include, url
|
|||
|
||||
from wagtail.api.v2.endpoints import PagesAPIEndpoint
|
||||
from wagtail.api.v2.router import WagtailAPIRouter
|
||||
from wagtail.contrib.wagtailapi import urls as wagtailapi_urls
|
||||
from wagtail.contrib.wagtailsitemaps import views as sitemaps_views
|
||||
from wagtail.contrib.wagtailsitemaps import Sitemap
|
||||
from wagtail.tests.testapp import urls as testapp_urls
|
||||
|
|
@ -31,7 +30,6 @@ urlpatterns = [
|
|||
url(r'^testimages/', include(wagtailimages_test_urls)),
|
||||
url(r'^images/', include(wagtailimages_urls)),
|
||||
|
||||
url(r'^api/', include(wagtailapi_urls)),
|
||||
url(r'^api/v2beta/', api_router.urls),
|
||||
url(r'^sitemap\.xml$', sitemaps_views.sitemap),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue