[Django] 08 - StreamField of Wagtail
怎么说呢,streamfield 太赞了!
用户页面
一、基本套路
-
定义
(1) 添加 app: streams,
(2) 新建 一个文件 blocks.py
(3) 定义 各种 block 类,如下: TitleAndTextBlock。
""" Streamfields live in here. """ from wagtail.core import blocks class TitleAndTextBlock(blocks.StructBlock): """ Title and text and nothing else. """ title = blocks.CharBlock(required=True, help_text='Add your title') text = blocks.TextBlock(required=True, help_text='Add additional text') class Meta: # noqa template = "streams/title_and_text_block.html" icon = "edit" label = "Title & Text"
-
调用
只是在 flex/models.py 中添加上这项就好了。
from django.db import models from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.core.models import Page from wagtail.core.fields import StreamField from streams import blocks class FlexPage(Page): """Flexible page class.""" template = "flex/flex_page.html"
# --------------------------------------------------------------------
content = StreamField( [ ('title_and_text', blocks.TitleAndTextBlock()) # <---- ], null = True, blank = True ) subtitle = models.CharField(max_length=100, null=True, blank=True)
# --------------------------------------------------------------------
content_panels = Page.content_panels + [ FieldPanel("subtitle"), StreamFieldPanel("content") ]
# --------------------------------------------------------------------
class Meta: # noaq verbose_name = "Flex Page" verbose_name_plural = "Flex Pages"
二、自定义 "富文本"
class RichTextBlock(blocks.RichTextBlock): """ Rick text and nothing else. """ # 默认就提供了一个富文本编辑框 class Meta: # noqa template = "streams/richtext_block.html" icon = "doc-full" label = "Full RichText
三、自定义 "图卡"
-
admin
class CardBlack(blocks.StructBlock): """ Cards with image and text and button(s) """ title = blocks.CharBlock(required=True, help_text='Add your title') cards = blocks.ListBlock( blocks.StructBlock( [ ("image", ImageChooserBlock(required=True)), ("title", blocks.CharBlock(required=True, max_length=40)), ("text", blocks.TextBlock(required=True, max_length=200)), ("button_page", blocks.PageChooserBlock(required=True)), ("button_url", blocks.URLBlock(required=False)), # 与 button_page是“或”的关系 ] ) ) class Meta: # noqa template = "streams/card_block.html" icon = "placeholder" label = "Staff Cards"
-
client
页面如何实现?
Goto: https://getbootstrap.com/docs/4.2/components/card/【直接找example代码即可】
<div class="card" style="width: 18rem;"> <img src="..." class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Card title</h5> <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p> <a href="#" class="btn btn-primary">Go somewhere</a> </div> </div>
四、Call To Action
与 card 的sub-block非常像,没有图片而已。
Creating a Call to Action StreamField in Wagtail CMS using a StructBlock
五、Saved in the Database
设计 rest api 时,考虑的“表”。
streamfield的数据库存储格式是JSON。
[
{
"type": "title_and_embed",
"value": {
"title": "Canva Embedded link",
"text": "js + iframe code"
},
"id": "7a104840-c03a-41dc-b539-ff6ea1c06fde"
},
{
"type": "title_and_embed",
"value": {
"title": "Google slices link",
"text": "iframe code"
},
"id": "f834e380-e75d-4a8c-b5d6-8366cc9d6a59"
}
]
六、Draftail RichText (WYSIWYG)
Ref: Wagtail CMS: How to extend the Draftail RichText (WYSIWYG) Editor
-
减少选项
通过 重写 self.features,
# 自定义的富文本特性 class SimpleRichtextBlock(blocks.RichTextBlock): """Richtext without (limited) all the features.""" # 这里实际上就是重写覆盖 def __init__(self, required=True, help_text=None, editor='default', features=None, **kwargs): super().__init__(**kwargs) self.features = ["bold", "italic", "link"] # <---- 这里做了限制 class Meta: # noqa template = "streams/richtext_block.html" icon = "edit" label = "Simple RichText"
-
添加选项
貌似也是react.js实现的,这里使用hook的方式,添加新的选项。
这个则要保证:setting中添加 app: 'streams'。
[ CODE ]
@hooks.register("register_rich_text_features") def register_code_styling(features): """Add the <code> to the richtext editor and page.""" # Step 1 feature_name = "code" type_ = "CODE" tag = "code" # Step 2 control = { "type": type_, "label": "</>", "description": "Code" } # Step 3 features.register_editor_plugin( "draftail", feature_name, draftail_features.InlineStyleFeature(control) ) # Step 4 db_conversion = { "from_database_format": {tag: InlineStyleElementHandler(type_)}, "to_database_format": {"style_map": {type_: {"element": tag}}} } # Step 5 features.register_converter_rule("contentstate", feature_name, db_conversion) # Step 6. This is optional # This will register this feature with all richtext editors by default features.default_features.append(feature_name)
[ HTML ]
@hooks.register("register_rich_text_features") def register_centertext_feature(features): """Creates centered text in our richtext editor and page.""" # Step 1 feature_name = "center" type_ = "CENTERTEXT" tag = "div" # Step 2 control = { "type": type_, "label": "Center", "description": "Center Text", "style": { "display": "block", "text-align": "center", }, } # Step 3 features.register_editor_plugin( "draftail", feature_name, draftail_features.InlineStyleFeature(control) ) # Step 4 db_conversion = { "from_database_format": {tag: InlineStyleElementHandler(type_)}, "to_database_format": { "style_map": { type_: { "element": tag, "props": { "class": "d-block text-center" } } } } } # Step 5 features.register_converter_rule("contentstate", feature_name, db_conversion) # Step 6, This is optional. features.default_features.append(feature_name)
Home页
Ref: How to use Orderables in Wagtail CMS【美化自己的主页】
一、添加"图片滑动栏"
-
定义 model
文件 ./home/models.py,可以看成是一个“子组件”的定义。
# 主页的图片栏 class HomePageCarouselImages(Orderable): """Between 1 and 5 images for the home page carousel."""
# inline到HomePage上的控件 page = ParentalKey("home.HomePage", related_name="carousel_images") # 体现了 inline 的特性 carousel_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=False, on_delete=models.SET_NULL, related_name="+", ) panels = [ ImageChooserPanel("carousel_image") ]
-
编写 template
样例代码:https://getbootstrap.com/docs/4.2/components/carousel/
<div id="carouselExampleControls" class="carousel slide" data-ride="carousel"> <div class="carousel-inner"> {% comment %} 结合model,这里体现了 carousel_images 与 carousel_image 的关系 {% endcomment %} {% for loop_cycle in self.carousel_images.all %} {% image loop_cycle.carousel_image fill-900x400 as img %} {% comment %} 注意这里的 active {% endcomment %} <div class="carousel-item{% if forloop.counter == 1 %} active{% endif %}"> <img src="{{ img.url }}" class="d-block w-100" alt="{{ img.alt }}"> </div> {% endfor %} </div> <a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div>
可见,若干图片,依次 active 其中的一张,就显示了出来。
二、添加"主页内容"
-
定义 model
因为是StreamField,添加多个block;
到底提供了多少个block类型呢?这里只提供了一个:CTABlock。
# 5. 内容栏 content = StreamField( [ ("cta", blocks.CTABlock()), ], null=True, blank=True, )
效果图。
-
编写 template
样例代码:https://getbootstrap.com/docs/4.2/components/carousel/
{% for block in page.content %}
{% include_block block %}
{% endfor %}
三、集成各种 Panels
注意类中的这几个变量的对应关系。按规则办事即可。
# Finally, 再将 以上的内容 依次 加入到 orm model (数据库)中 # 通过 MultiFieldPanel对admin的组件进行了layout updated. content_panels = Page.content_panels + [ MultiFieldPanel([ FieldPanel("banner_title"), FieldPanel("banner_subtitle"), ImageChooserPanel("banner_image"), PageChooserPanel("banner_cta"), ], heading="Banner Options"), MultiFieldPanel([ InlinePanel("carousel_images"), ], heading="Carousel Images"), StreamFieldPanel("content"), ]
也可以给 content 加个 MultiFieldPanel,看上去,更为一致。
Custom StreamField "Logic"
Ref: Wagtail CMS: Adding Custom StreamField Logic
一、过去的问题
希望能将逻辑写在model里,而不是模板里。
[古板写法]
{% load wagtailimages_tags %} <div class="container mb-sm-5 mt-sm-5"> <h1 class="text-center mb-sm-5">{{ self.title }}</h1> <div class="card-deck"> {% for card in self.cards %} {% image card.image fill-300x200 as img %} <div class="card"> <img src="{{ img.url }}" alt="{{ img.alt }}" class="card-img-top" /> <div class="card-body"> <h5 class="card-title">{{ card.title }}</h5> <p class="card-text">{{ card.text }}</p>
--------------------------- 以下使用了 if ... else ---------------- {% if card.button_page %} <a href="{{ card.button_page.url }}" class="btn btn-primary"> Learn More </a> {% elif card.button_url %} <a href="{{ card.button_url }}" class="btn btn-primary"> Learn More </a> {% endif %}
----------------------------------------------------------------- </div> </div> {% endfor %} </div> </div>
二、添加 button block
-
block.py 内新增 ButtonBlock
有一点要注意,block的角色是子模型,而不是主模型,对应了一个 page。
class LinkStructValue(blocks.StructValue): """Additional logic for our urls.""" def url(self): button_page = self.get('button_page') button_url = self.get('button_url') if button_page: return button_page.url elif button_url: return button_url return None # def latest_posts(self): # return BlogDetailPage.objects.live()[:3] class ButtonBlock(blocks.StructBlock): """An external or internal URL.""" button_page = blocks.PageChooserBlock(required=False, help_text='If selected, this url will be used first') button_url = blocks.URLBlock(required=False, help_text='If added, this url will be used secondarily to the button page') # def get_context(self, request, *args, **kwargs): # context = super().get_context(request, *args, **kwargs) # context['latest_posts'] = BlogDetailPage.objects.live().public()[:3] # return context class Meta: # noqa template = "streams/button_block.html" icon = "placeholder" label = "Single Button" value_class = LinkStructValue
-
-
定义 模板
-
[改进写法]
可见,逻辑转移到了 LinkStructValue 函数中。
<div class="container mb-sm-5 mt-sm-5"> <div class="row"> <div class="col-md-5 offset-md-1 col-sm-12 text-center"> <h1> <a href="{{ self.url }}">{{ self.url }}</a> </h1> </div> </div> </div>
-
使用 ButtonBlock
content = StreamField(
[
("title_and_text", blocks.TitleAndTextBlock()),
("full_richtext", blocks.RichtextBlock()),
("simple_richtext", blocks.SimpleRichtextBlock()),
("cards", blocks.CardBlock()),
("cta", blocks.CTABlock()),
("button", blocks.ButtonBlock()),
],
null=True,
blank=True
)
End.