[Django] 16 - Headless CMS

写在前面


Ref: Enable the v2 API to Create a Headless CMS

学完之前的章节,也算是正式宣告度过了Django阶段,开始入门了 Wagtail。

  

 

 

 

REST FRAMEWORK


一、配置

mysite/settings/base.py

    'wagtail.api.v2',
    'rest_framework',

 

mysite/api.py

依次是三大板块:pages, images, documents

from wagtail.api.v2.router              import WagtailAPIRouter
from wagtail.api.v2.endpoints           import PagesAPIEndpoint
from wagtail.images.api.v2.endpoints    import ImagesAPIEndpoint
from wagtail.documents.api.v2.endpoints import DocumentsAPIEndpoint

api_router = WagtailAPIRouter('wagtailapi')

api_router.register_endpoint('pages',     PagesAPIEndpoint)
api_router.register_endpoint('images',    ImagesAPIEndpoint)
api_router.register_endpoint('documents', DocumentsAPIEndpoint)

 

mysite/urls.py

专门添加一个 rest api。 

urlpatterns = [
    url(r'^django-admin/', admin.site.urls),

    url(r'^admin/', include(wagtailadmin_urls)),
    url(r'^documents/', include(wagtaildocs_urls)),

    url(r'^search/$', search_views.search, name='search'),

    url(r'^api/v2/', api_router.urls),  # <----
... ...

 

 

二、v2/api 测试

  • DRF

Ref: http://127.0.0.1:8000/api/v2/images/

 

  • 前端测试 rest api

Ref: https://telusworldofscienceedmonton.ca/【一个vue.js的前端】

fetch('http://localhost:8000/api/v2/pages/')
.then(res => res.json())
.then(response => console.log(response))

 

 

三、增强返回内容

  • 第一步

{
    "meta": {
        "total_count": 12
    },
    "items": [
        {
            "id": 3,
            "meta": {
                "type": "home.HomePage",
                "detail_url": "http://localhost/api/v2/pages/3/",
                "html_url": "http://localhost/",
                "slug": "this-is-useless",
                "first_published_at": "2020-12-25T00:59:29.199338Z"
            },
            "title": "[Title] Mobilar"
        },

该page的详细信息:

{
    "id": 3,
    "meta": {
        "type": "home.HomePage",
        "detail_url": "http://localhost/api/v2/pages/3/",
        "html_url": "http://localhost/",
        "slug": "this-is-useless",
        "show_in_menus": false,
        "seo_title": "",
        "search_description": "",
        "first_published_at": "2020-12-25T00:59:29.199338Z",
        "parent": null
    },
    "title": "[Title] Mobilar"
}

  

  • 第二步

    api_fields = [
        APIField("banner_title"),
        APIField("banner_subtitle"),
        APIField("banner_image"),
        APIField("banner_cta"),
        # APIField("carousel_images"),
        # APIField("content"),
    ]

详细信息变多了。

{
    "id": 3,
    "meta": {
        "type": "home.HomePage",
        "detail_url": "http://localhost/api/v2/pages/3/",
        "html_url": "http://localhost/",
        "slug": "this-is-useless",
        "show_in_menus": false,
        "seo_title": "",
        "search_description": "",
        "first_published_at": "2020-12-25T00:59:29.199338Z",
        "parent": null
    },
    "title": "[Title] Mobilar",
    "banner_title": "[Banner title] Wecome to Mobilar",
    "banner_subtitle": "<p>We help startups start up.</p>",
    "banner_image": {
        "id": 9,
        "meta": {
            "type": "wagtailimages.Image",
            "detail_url": "http://localhost/api/v2/images/9/",
            "download_url": "/media/original_images/banner-sydney.jpeg"
        },
        "title": "banner-sydney.jpeg"
    },
    "banner_cta": {
        "id": 3,
        "meta": {
            "type": "home.HomePage",
            "detail_url": "http://localhost/api/v2/pages/3/"
        },
        "title": "[Title] Mobilar"
    }
}

 

    • 不显示个别选项

“减号”,表示 “排除”。多个选项则用“逗号”隔开。

127.0.0.1:8000/api/v2/pages/3/?fields=-seo_title

 

 

四、参数

Ref: Headless Wagtail CMS: Fetching Specific Fields from the v2 API

Doc: https://docs.wagtail.io/en/v2.4/advanced_topics/api/v2/usage.html#fetching-content

[只读]

获取某一个 Page

http://127.0.0.1:8000/api/v2/pages/?type=blog.ArticleBlogPage
http://127.0.0.1:8000/api/v2/pages/3/?fields=*
http://127.0.0.1:8000/api/v2/pages/3/?fields=_,id,title,banner_title
http://127.0.0.1:8000/api/v2/pages/?id=5

 

获取某一组 Pages

http://127.0.0.1:8000/api/v2/pages/?limit=4&offset=4  # 倒数的四个
http://127.0.0.1:8000/api/v2/pages/?order=title
http://127.0.0.1:8000/api/v2/pages/?order=-title  # 倒序
http://127.0.0.1:8000/api/v2/pages/?order=random
http://127.0.0.1:8000/api/v2/pages/?slug=home

 

继承关系。

http://127.0.0.1:8000/api/v2/pages/?child_of=5
http://127.0.0.1:8000/api/v2/pages/?descendent_of=5

 

 

五、嵌套结构 Orderable

  • Home Page

[问题]

打开这个“嵌套”:Orderable。

    api_fields = [
        APIField("banner_title"),
        APIField("banner_subtitle"),
        APIField("banner_image"),
        APIField("banner_cta"),
        APIField("carousel_images"),  # <----
        # APIField("content"),
    ]

显示的内容不多。希望能显示更多的detail,怎么办呢?

    "carousel_images": [
        {
            "id": 9,
            "meta": {
                "type": "home.HomePageCarouselImages"
            }
        },
        ... ...

 

[解决]

(1) 在 content_panels 中,是 InlinePanel 角色。
故,在当前类中定义APIField,只显示如上部分内容,没有细节。
        MultiFieldPanel([
            InlinePanel("carousel_images", max_num=5, min_num=1, label="Image"),
        ], heading="Carousel Images"),

(2) 进一步的,在 carousel_images 的类中,如下定义 api_field。

class HomePageCarouselImages(Orderable):
    """Between 1 and 5 images for the home page carousel."""
    ... ...
# inline到HomePage上的控件,跟数据库有关系的一个概念 page = ParentalKey("home.HomePage", related_name="carousel_images") ... ...
api_fields
= [ APIField("carousel_image"), ]

如此,便显示了细节。

    "carousel_images": [
        {
            "id": 9,
            "meta": {
                "type": "home.HomePageCarouselImages"
            },
            "carousel_image": {
                "id": 13,
                "meta": {
                    "type": "wagtailimages.Image",
                    "detail_url": "http://localhost/api/v2/images/13/",
                    "download_url": "/media/original_images/sea1.jpeg"
                },
                "title": "sea1.jpeg"
            }
        },
        ... ...

  

  • Blog Page

[解决]

定义第一层。

    # Integration.
    content_panels = Page.content_panels + [
        FieldPanel("custom_title"),
        ImageChooserPanel("banner_image"),
        MultiFieldPanel(
            [
                # 因为是 InlinePanel,所以通过 blog_authors 去定位。
                InlinePanel("blog_authors", label="Author", min_num=1, max_num=2)
            ],
            heading="Author(s)"
        ),
        MultiFieldPanel(
            [
                FieldPanel("categories", widget=forms.CheckboxSelectMultiple)  # forms中的多选控件
            ],
            heading="Categories"
        ),
        StreamFieldPanel("content"),
    ]

    api_fields = [
        APIField("blog_authors"),  # Orderable: 这个是 inlinePanel 的套路
        APIField("content"),        # StreamField:这个则是 “通用模式”
    ]

再进入嵌套,定义细节。

class BlogAuthorsOrderable(Orderable):
    """This allows us to select one or more blog authors from Snippets."""

    # (1) 因为是 InlinePanel,所以 依附的父表在这里要指定出来
    page = ParentalKey("blog.BlogDetailPage", related_name="blog_authors")
    author = models.ForeignKey(
        # (2) 这里代表了数据库中的一个表,而表是与类的名字挂钩 ----> [snippet] BlogAuthor()
        "blog.BlogAuthor",
        on_delete=models.CASCADE,
    )

    panels = [
        SnippetChooserPanel("author"),
    ]
    --------------------------------------------
    @property
    def author_name(self):
        return self.author.name

    @property
    def author_website(self):
        return self.author.website
    ---------------------------------------------
    api_fields = [
        APIField("author_name"),
        APIField("author_website"),
    ]

 

[效果]

Original。

{
    "id": 20,
    "meta": {
        "type": "blog.BlogDetailPage",
        "detail_url": "http://localhost/api/v2/pages/20/",
        "html_url": "http://localhost/user1/blog-listing-page/blog-title-blog-detail-page-2/",
        "slug": "blog-title-blog-detail-page-2",
        "show_in_menus": false,
        "seo_title": "",
        "search_description": "",
        "first_published_at": "2020-12-27T13:32:15.406376Z",
        "parent": {
            "id": 18,
            "meta": {
                "type": "blog.BlogListingPage",
                "detail_url": "http://localhost/api/v2/pages/18/",
                "html_url": "http://localhost/user1/blog-listing-page/"
            },
            "title": "Blog Listing Page"
        }
    },
    "title": "Blog Title for  Blog detail page 2"
}

打开第一层。

{
    "id": 20,
    "meta": {
        "type": "blog.BlogDetailPage",
        "detail_url": "http://localhost/api/v2/pages/20/",
        "html_url": "http://localhost/user1/blog-listing-page/blog-title-blog-detail-page-2/",
        "slug": "blog-title-blog-detail-page-2",
        "show_in_menus": false,
        "seo_title": "",
        "search_description": "",
        "first_published_at": "2020-12-27T13:32:15.406376Z",
        "parent": {
            "id": 18,
            "meta": {
                "type": "blog.BlogListingPage",
                "detail_url": "http://localhost/api/v2/pages/18/",
                "html_url": "http://localhost/user1/blog-listing-page/"
            },
            "title": "Blog Listing Page"
        }
    },
    "title": "Blog Title for  Blog detail page 2",
    "blog_authors": [
        {
            "id": 3,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            }
        },
        {
            "id": 4,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            }
        }
    ],
    "content": [
        {
            "type": "simple_richtext",
            "value": "<p>There is nothing to say, thank.</p><p>Regards,</p><p>/Jeff</p>",
            "id": "73f2f323-2da1-425e-9639-98e148570771"
        }
    ]
}

再打开细节。

{
    "id": 20,
    "meta": {
        "type": "blog.BlogDetailPage",
        "detail_url": "http://localhost/api/v2/pages/20/",
        "html_url": "http://localhost/user1/blog-listing-page/blog-title-blog-detail-page-2/",
        "slug": "blog-title-blog-detail-page-2",
        "show_in_menus": false,
        "seo_title": "",
        "search_description": "",
        "first_published_at": "2020-12-27T13:32:15.406376Z",
        "parent": {
            "id": 18,
            "meta": {
                "type": "blog.BlogListingPage",
                "detail_url": "http://localhost/api/v2/pages/18/",
                "html_url": "http://localhost/user1/blog-listing-page/"
            },
            "title": "Blog Listing Page"
        }
    },
    "title": "Blog Title for  Blog detail page 2",
    "blog_authors": [
        {
            "id": 3,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            },
            "author_name": "Jesse Hao",
            "author_website": "http://www.twitter.com"
        },
        {
            "id": 4,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            },
            "author_name": "Jeffrey Hao",
            "author_website": "http://www.cnblogs.com"
        }
    ],
    "content": [
        {
            "type": "simple_richtext",
            "value": "<p>There is nothing to say, thank.</p><p>Regards,</p><p>/Jeff</p>",
            "id": "73f2f323-2da1-425e-9639-98e148570771"
        }
    ]
}

 

 

六、传图片

如何返回图片的问题,在这里,一个author有一个头像图片。

from rest_framework.fields import Field

class ImageSerializedField(Field):
    """A custom serializer used in Wagtails v2 API."""

    def to_representation(self, value):
        """Return the image URL, title and dimensions."""
        return {
            "url": value.file.url,
            "title": value.title,
            "width": value.width,
            "height": value.height,
        }

    --------------------------------------------------------

... ...
@property
def author_image(self): return self.author.image api_fields = [ APIField("author_name"), APIField("author_website"), APIField("author_image", serializer=ImageSerializedField()),  # <---- 自定义 一个序列化的方法 ]

可见,通过链接可以获得头像。

    "blog_authors": [
        {
            "id": 3,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            },
            "author_name": "Jesse Hao",
            "author_website": "http://www.twitter.com",
            "author_image": {
                "url": "/media/original_images/001.png",
                "title": "001.png",
                "width": 225,
                "height": 225
            }
        },
        {
            "id": 4,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            },
            "author_name": "Jeffrey Hao",
            "author_website": "http://www.cnblogs.com",
            "author_image": {
                "url": "/media/original_images/002.png",
                "title": "002.png",
                "width": 225,
                "height": 225
            }
        }
    ],

 

 

七、Rendition 图片

from wagtail.images.api.fields import ImageRenditionField

添加一部分内容。

    --------------------------------------------------------

    ... ...

    @property
    def author_image(self):
        return self.author.image

    api_fields = [
        APIField("author_name"),
        APIField("author_website"),
        APIField("author_image", serializer=ImageSerializedField()),
        # The below APIField is using a Wagtail-built DRF Serializer that supports
        # custom image rendition sizes
        APIField(
            "image",
            serializer=ImageRenditionField(
                'fill-200x250',
                source="author_image"
            )
        ),
    ]

显示指定的大小,简直绝了,妙哉 ~

        {
            "id": 3,
            "meta": {
                "type": "blog.BlogAuthorsOrderable"
            },
            "author_name": "Jesse Hao",
            "author_website": "http://www.twitter.com",
            "author_image": {
                "url": "/media/original_images/001.png",
                "title": "001.png",
                "width": 225,
                "height": 225
            },
            "image": {
                "url": "/media/images/001.2e16d0ba.fill-200x250.png",
                "width": 181,
                "height": 225
            }
        },

 

End.

posted @ 2021-01-01 21:38  郝壹贰叁  阅读(403)  评论(0编辑  收藏  举报