kopia lustrzana https://github.com/wagtail/wagtail
250 wiersze
9.1 KiB
Markdown
250 wiersze
9.1 KiB
Markdown
(api_ninja)=
|
||
|
||
# How to set up Django Ninja
|
||
|
||
While Wagtail provides a [built-in API module](api) based on Django REST Framework, it is possible to use other API frameworks.
|
||
Here is information on usage with [Django Ninja](https://django-ninja.dev/), an API framework built on Python type hints and [Pydantic](https://docs.pydantic.dev/latest/), which includes built-in support for OpenAPI schemas.
|
||
|
||
## Basic configuration
|
||
|
||
Install `django-ninja`. Optionally you can also add `ninja` to your `INSTALLED_APPS` to avoid loading static files externally when using the OpenAPI documentation viewer.
|
||
|
||
### Create the API
|
||
|
||
We will create a new `api.py` module next to the existing `urls.py` file in the project root, instantiate the router.
|
||
|
||
```python
|
||
# api.py
|
||
from typing import Literal
|
||
from django.http import HttpRequest
|
||
from django.shortcuts import get_object_or_404
|
||
from ninja import Field, ModelSchema, NinjaAPI
|
||
from wagtail.models import Page
|
||
|
||
api = NinjaAPI()
|
||
```
|
||
|
||
Next, register the URLs so Django can route requests into the API. To test this is working, navigate to `/api/docs`, which displays the OpenAPI documentation (with no available endpoints yet).
|
||
|
||
```python
|
||
# urls.py
|
||
|
||
from .api import api
|
||
|
||
urlpatterns = [
|
||
...
|
||
|
||
path("api/", api.urls),
|
||
|
||
...
|
||
|
||
# Ensure that the api line appears above the default Wagtail page serving route
|
||
path("", include(wagtail_urls)),
|
||
]
|
||
```
|
||
|
||
### Our first endpoint
|
||
|
||
We will create a simple endpoint that returns a list of all pages in the site. We use the `@api.get` operation decorator to define what route the endpoint is available at, and how to format the response: here, using a custom schema we create.
|
||
|
||
```python
|
||
# api.py
|
||
|
||
|
||
class BasePageSchema(ModelSchema):
|
||
url: str = Field(None, alias="get_url")
|
||
|
||
class Config:
|
||
model = Page
|
||
model_fields = [
|
||
"id",
|
||
"title",
|
||
"slug",
|
||
]
|
||
|
||
|
||
@api.get("/pages/", response=list[BasePageSchema])
|
||
def list_pages(request: "HttpRequest"):
|
||
return Page.objects.live().public().exclude(id=1)
|
||
```
|
||
|
||
Our custom `BasePageSchema` combines two techniques: [schema generation from Django models](https://django-ninja.dev/guides/response/django-pydantic/), and calculated fields with [aliases](https://django-ninja.dev/guides/response/#aliases). Here, we use an alias to retrieve the page URL.
|
||
|
||
We can also add an extra `child_of: int = None` parameter to our endpoint to filter the pages by their parent:
|
||
|
||
```python
|
||
@api.get("/pages/", response=list[BasePageSchema])
|
||
def list_pages(request: "HttpRequest", child_of: int = None):
|
||
if child_of:
|
||
return get_object_or_404(Page, id=child_of).get_children().live().public()
|
||
# Exclude the page tree root.
|
||
return Page.objects.live().public().exclude(id=1)
|
||
```
|
||
|
||
Ninja treats every parameter of the `list_pages` function as a query parameter. It uses the provided type hint to parse the value, validate it, and generate the OpenAPI schema.
|
||
|
||
### Adding custom page fields
|
||
|
||
Next, let’s add a "detail" API endpoint to return a single page of a specific type. We can use the [path parameters](https://django-ninja.dev/guides/input/path-params/) from Ninja to retrieve our `page_id`.
|
||
We also create a new schema for a specific page type: here, `BlogPage`, with `BasePageSchema` as a base.
|
||
|
||
```python
|
||
from blog.models import BlogPage
|
||
|
||
|
||
class BlogPageSchema(BasePageSchema, ModelSchema):
|
||
class Config(BasePageSchema.Config):
|
||
model = BlogPage
|
||
model_fields = [
|
||
"intro",
|
||
]
|
||
|
||
|
||
@api.get("/pages/{page_id}/", response=BlogPageSchema)
|
||
def get_page(request: "HttpRequest", page_id: int):
|
||
return get_object_or_404(Page, id=page_id).specific
|
||
```
|
||
|
||
This works well, with the endpoint now returning generic `Page` fields and the `BlogPage` introduction.
|
||
But for sites where all page content is served via an API, it could become tedious to create new endpoints for every page type.
|
||
|
||
### Combining multiple schemas
|
||
|
||
To reflect that our response may return multiple page types, we use the type hint union syntax to combine multiple schemas.
|
||
This allows us to return different page types from the same endpoint.
|
||
Here is an example with an additional schema for our `HomePage` type:
|
||
|
||
```python
|
||
from home.models import HomePage
|
||
|
||
|
||
class HomePageSchema(BasePageSchema, ModelSchema):
|
||
class Config(BasePageSchema.Config):
|
||
model = HomePage
|
||
|
||
@api.get("/pages/{page_id}/", response=BlogPageSchema | HomePageSchema)
|
||
def get_page(request: "HttpRequest", page_id: int):
|
||
return get_object_or_404(Page, id=page_id).specific
|
||
```
|
||
|
||
With this in place, we are still missing a way to determine which of the schemas to use for a given page.
|
||
We want to do this by page type, adding an extra `content_type` class attribute annotation to our schemas.
|
||
|
||
- For `BasePageSchema`, we define `content_type: str`, as any page type can use this base.
|
||
- For `HomePageSchema`, we set `content_type: Literal["homepage"]`.
|
||
- And for `BlogPageSchema`, we set `content_type: Literal["blogpage"]`.
|
||
|
||
All we need now is to add a [resolver](https://django-ninja.dev/guides/response/#resolvers) calculated field to the `BasePageSchema`, to return the correct content type for each page. Here is the final version of `BasePageSchema`:
|
||
|
||
```python
|
||
class BasePageSchema(ModelSchema):
|
||
url: str = Field(None, alias="get_url")
|
||
content_type: str
|
||
|
||
@staticmethod
|
||
def resolve_content_type(page: Page) -> str:
|
||
return page.specific_class._meta.model_name
|
||
|
||
class Config:
|
||
model = Page
|
||
model_fields = [
|
||
"id",
|
||
"title",
|
||
"slug",
|
||
]
|
||
```
|
||
|
||
With this in place, Pydantic is able to validate the page data returned in `get_page` according to one of the schemas in the `response` union.
|
||
It then serializes the data according to the specific schema.
|
||
|
||
### Nested data
|
||
|
||
Where the page schema references data in separate models, rather than creating new endpoints, we can add the data directly to the page schema.
|
||
Here is an example, adding blog page authors (a snippet with a `ParentalManyToManyField`):
|
||
|
||
```python
|
||
class BlogPageSchema(BasePageSchema, ModelSchema):
|
||
content_type: Literal["blogpage"]
|
||
authors: list[str] = []
|
||
|
||
class Config(BasePageSchema.Config):
|
||
model = BlogPage
|
||
model_fields = [
|
||
"intro",
|
||
]
|
||
|
||
@staticmethod
|
||
def resolve_authors(page: BlogPage, context) -> list[str]:
|
||
return [author.name for author in page.authors.all()]
|
||
```
|
||
|
||
This could also be done with the `Field` class if the `BlogPage` class had a method to retrieve author names directly: `authors: list[str] = Field([], alias="get_author_names")`.
|
||
|
||
### Rich text in the API
|
||
|
||
Rich text fields in Wagtail use a specific internal format, described in [](../../extending/rich_text_internals). They can be added to the schema as `str`, but it’s often more useful for the API to provide a “display” representation, where references to pages and images are replaced with URLs.
|
||
This can also be done with [Ninja resolvers](https://django-ninja.dev/guides/response/#resolvers). Here is an example with the `HomePageSchema`:
|
||
|
||
```python
|
||
from wagtail.rich_text import expand_db_html
|
||
|
||
|
||
class HomePageSchema(BasePageSchema, ModelSchema):
|
||
content_type: Literal["homepage"]
|
||
body: str
|
||
|
||
class Config(BasePageSchema.Config):
|
||
model = HomePage
|
||
|
||
@staticmethod
|
||
def resolve_body(page: HomePage, context) -> str:
|
||
return expand_db_html(page.body)
|
||
```
|
||
|
||
Here, `body` is defined as a `str`, and the resolver uses the `expand_db_html` function to convert the internal representation to HTML.
|
||
|
||
### Images in the API
|
||
|
||
We can use a similar technique for images, combining resolvers and aliases to generate the data.
|
||
We use the [`get_renditions()` method](image_renditions_multiple) to retrieve the formatted images, and a custom `RenditionSchema` to define their API representation.
|
||
|
||
```python
|
||
from wagtail.images.models import AbstractRendition
|
||
|
||
|
||
class RenditionSchema(ModelSchema):
|
||
# We need to use the Field / alias API for properties
|
||
url: str = Field(None, alias="file.url")
|
||
alt: str = Field(None, alias="alt")
|
||
|
||
class Config:
|
||
model = AbstractRendition
|
||
model_fields = [
|
||
"width",
|
||
"height",
|
||
]
|
||
```
|
||
|
||
On the `BlogPageSchema`, we define our image field as: `main_image: list[RenditionSchema] = []`. Then add the resolver for it:
|
||
|
||
```python
|
||
@staticmethod
|
||
def resolve_main_image(page: BlogPage) -> list[AbstractRendition]:
|
||
filters = [
|
||
"fill-800x600|format-webp",
|
||
"fill-800x600",
|
||
]
|
||
if image := page.main_image():
|
||
return image.get_renditions(*filters).values()
|
||
return []
|
||
```
|
||
|
||
In JSON, our `main_image` is now represented as an array, where each item is an object with `url`, `alt`, `width`, and `height` properties.
|
||
|
||
## OpenAPI documentation
|
||
|
||
Django Ninja generates [OpenAPI](https://swagger.io/specification/) documentation automatically, based on the defined operations and schemas.
|
||
This also includes a documentation viewer, with support to try out the API directly from the browser. With the above example, you can try accessing the docs at `/api/docs`.
|
||
|
||
To make the most of this capability, consider the supported [operations parameters](https://django-ninja.dev/reference/operations-parameters/).
|