[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" } }, ... ...
[解决]
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.