├── baidu_verify_kaObX83Nj0.html ├── getting_started ├── integrating_into_django.md ├── images │ ├── tutorial_1.png │ ├── tutorial_10.jpg │ ├── tutorial_2.png │ ├── tutorial_3.png │ ├── tutorial_4a.png │ ├── tutorial_4b.png │ ├── tutorial_5.png │ ├── tutorial_6.jpg │ ├── tutorial_7.png │ ├── tutorial_8.png │ ├── tutorial_9.png │ ├── wechat-pay.png │ ├── drupal_content_type.png │ └── a6x09981lks9yco3b8xcqf0.png ├── index.md ├── the_zen_of_wagtail.md └── tutorial.md ├── advanced_topics ├── customisation │ ├── extending_hallo.md │ ├── images │ │ ├── wechat-pay.png │ │ ├── a6x09981lks9yco3b8xcqf0.png │ │ ├── draftail_entity_stock_source.gif │ │ └── draftail_entity_stock_rendering.png │ ├── index.md │ ├── custom_user_model.md │ ├── admin_templates.md │ ├── page_editing_interface.md │ ├── rich_text_internals.md │ └── extending_draftail.md ├── images │ ├── wechat-pay.png │ ├── images │ │ ├── wechat-pay.png │ │ └── a6x09981lks9yco3b8xcqf0.png │ ├── a6x09981lks9yco3b8xcqf0.png │ ├── animated_gifs.md │ ├── index.md │ ├── renditions.md │ ├── custom_image_model.md │ ├── feature_detection.md │ └── image_serve_view.md ├── api │ ├── images │ │ ├── wechat-pay.png │ │ └── a6x09981lks9yco3b8xcqf0.png │ ├── v2 │ │ ├── images │ │ │ ├── wechat-pay.png │ │ │ └── a6x09981lks9yco3b8xcqf0.png │ │ ├── configuration.md │ │ └── usage.md │ └── index.md ├── i18n │ ├── images │ │ ├── wechat-pay.png │ │ └── a6x09981lks9yco3b8xcqf0.png │ ├── duplicate_tree.md │ └── index.md ├── documents │ ├── images │ │ ├── wechat-pay.png │ │ └── a6x09981lks9yco3b8xcqf0.png │ ├── index.md │ └── custom_document_model.md ├── deploying.md ├── third_party_tutorials.md ├── settings.md ├── index.md ├── performance.md ├── jinja2.md ├── privacy.md ├── testing.md └── embeds.md ├── images ├── wechat-pay.png └── a6x09981lks9yco3b8xcqf0.png ├── topics ├── images │ ├── wechat-pay.png │ ├── image_filter_fill.png │ ├── image_filter_max.png │ ├── image_filter_min.png │ ├── a6x09981lks9yco3b8xcqf0.png │ ├── image_filter_fill_focal.png │ └── image_filter_fill_focal_close.png ├── search │ ├── images │ │ ├── wechat-pay.png │ │ └── a6x09981lks9yco3b8xcqf0.png │ ├── index.md │ ├── backends.md │ ├── indexing.md │ └── searching.md ├── index.md ├── permissions.md ├── writing_templates.md ├── snippets.md ├── images.md └── pages.md ├── git-push.sh ├── .gitignore ├── book.json ├── SUMMARY.md └── README.md /baidu_verify_kaObX83Nj0.html: -------------------------------------------------------------------------------- 1 | kaObX83Nj0 -------------------------------------------------------------------------------- /getting_started/integrating_into_django.md: -------------------------------------------------------------------------------- 1 | # 将Wagtail集成到Django项目 2 | 3 | 4 | -------------------------------------------------------------------------------- /advanced_topics/customisation/extending_hallo.md: -------------------------------------------------------------------------------- 1 | # 对 Hallo 编辑器进行扩展 2 | 3 | (略)。 4 | -------------------------------------------------------------------------------- /images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/images/wechat-pay.png -------------------------------------------------------------------------------- /topics/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/wechat-pay.png -------------------------------------------------------------------------------- /images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /topics/images/image_filter_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/image_filter_fill.png -------------------------------------------------------------------------------- /topics/images/image_filter_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/image_filter_max.png -------------------------------------------------------------------------------- /topics/images/image_filter_min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/image_filter_min.png -------------------------------------------------------------------------------- /topics/search/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/search/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/images/wechat-pay.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_1.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_10.jpg -------------------------------------------------------------------------------- /getting_started/images/tutorial_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_2.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_3.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_4a.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_4b.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_5.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_6.jpg -------------------------------------------------------------------------------- /getting_started/images/tutorial_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_7.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_8.png -------------------------------------------------------------------------------- /getting_started/images/tutorial_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/tutorial_9.png -------------------------------------------------------------------------------- /getting_started/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/api/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/api/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/i18n/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/i18n/images/wechat-pay.png -------------------------------------------------------------------------------- /topics/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /topics/images/image_filter_fill_focal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/image_filter_fill_focal.png -------------------------------------------------------------------------------- /advanced_topics/api/v2/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/api/v2/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/images/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/images/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/documents/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/documents/images/wechat-pay.png -------------------------------------------------------------------------------- /getting_started/images/drupal_content_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/drupal_content_type.png -------------------------------------------------------------------------------- /topics/images/image_filter_fill_focal_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/images/image_filter_fill_focal_close.png -------------------------------------------------------------------------------- /topics/search/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/topics/search/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /getting_started/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/getting_started/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/customisation/images/wechat-pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/customisation/images/wechat-pay.png -------------------------------------------------------------------------------- /advanced_topics/api/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/api/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/i18n/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/i18n/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/api/v2/configuration.md: -------------------------------------------------------------------------------- 1 | # Wagtail 编程接口第二版配置手册 2 | 3 | 4 | ## 基础配置 5 | 6 | 7 | ## 附加设置项 8 | -------------------------------------------------------------------------------- /advanced_topics/api/v2/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/api/v2/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/documents/index.md: -------------------------------------------------------------------------------- 1 | # 文档 2 | 3 | + [对文档模型进行定制](custom_document_model.md) 4 | 5 | - [对文档模型的引用](custom_document_model.md#module-wagtail.documents.models) 6 | -------------------------------------------------------------------------------- /advanced_topics/images/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/images/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/documents/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/documents/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/customisation/images/a6x09981lks9yco3b8xcqf0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/customisation/images/a6x09981lks9yco3b8xcqf0.png -------------------------------------------------------------------------------- /advanced_topics/customisation/images/draftail_entity_stock_source.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/customisation/images/draftail_entity_stock_source.gif -------------------------------------------------------------------------------- /advanced_topics/customisation/images/draftail_entity_stock_rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu4cn/wagtailCMS-tutorial/HEAD/advanced_topics/customisation/images/draftail_entity_stock_rendering.png -------------------------------------------------------------------------------- /git-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git add . 3 | 4 | if [ "$1" = "" ] 5 | then 6 | echo "没有commit -m的输入,请输入commit -m内容,以[ENTER]结束:" 7 | read msg 8 | git commit -m "$msg" 9 | git push 10 | else 11 | git commit -m "$1" 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /advanced_topics/api/v2/usage.md: -------------------------------------------------------------------------------- 1 | # Wagtail编程接口第二版使用手册 2 | 3 | __目录__ 4 | 5 | 6 | 7 | ## 获取内容 8 | 9 | 10 | 11 | ## 默认端点字段 12 | 13 | 14 | 15 | ## 自第一版后的改变 16 | -------------------------------------------------------------------------------- /advanced_topics/images/animated_gifs.md: -------------------------------------------------------------------------------- 1 | # 动画GIF的支持 2 | 3 | Wagtail默认的图片库 Pillow, 并不支持动画的GIFs。 4 | 5 | 要获得动画GIF的支持,就必须[安装Wand](http://docs.wand-py.org/en/0.4.2/guide/install.html),Wand是一个到ImageMagick的绑定,所以要确保有安装ImageMagick程序。 6 | 7 | 在安装好了之后,Wagtail将自动使用Wand来对GIF文件进行调整,而仍然使用Pillow来调整其他图片。 8 | -------------------------------------------------------------------------------- /advanced_topics/deploying.md: -------------------------------------------------------------------------------- 1 | # 部署Wagtail 2 | 3 | ## 在自己的服务器上 4 | 5 | Wagtail在现代基于Linux的发行版上的部署是直接了当的,但请查看[性能小节](performance.md)了解推荐的非Python服务。 6 | 7 | 当前偏好于Debian上的Nginx、Gunicorn与supervisor组合部署,不过Wagtail可与Django的[部署文档](https://docs.djangoproject.com/en/stable/howto/deployment/)中所介绍的全部组合运行起来。 8 | 9 | ## 在Divio云上 10 | 11 | ## 在 PythonAnywhere上 12 | 13 | ## 在其他PAASs 与 IAASs 上 14 | 15 | ## 部署技巧 16 | 17 | ### 关于静态文件 18 | 19 | ### 关于云存储 20 | 21 | 22 | -------------------------------------------------------------------------------- /advanced_topics/images/index.md: -------------------------------------------------------------------------------- 1 | # 图片 2 | 3 | + [以Python方式生成图片的转写](renditions.md) 4 | 5 | + [动画GIF的支持](animated_gifs.md) 6 | 7 | + [对图片模型进行定制](custom_image_model.md) 8 | 9 | - [到图片模型的引用](custom_image_model.md#module-wagtail.images) 10 | 11 | 12 | + [特性侦测](feature_detection.md) 13 | 14 | - [设置](feature_detection.md#setup) 15 | 16 | 17 | + [动态地提供图片的试图](image_serve_view.md) 18 | 19 | - [设置](image_serve_view.md#setup) 20 | - [用法](image_serve_view.md#usage) 21 | - [高级配置](image_serve_view.md#advanced-configuration) 22 | -------------------------------------------------------------------------------- /advanced_topics/api/index.md: -------------------------------------------------------------------------------- 1 | # Wagtail的编程接口 2 | 3 | API 模块提供了一个面向公众的、JSON 格式化的API,允许以原始字段数据形式获取到站点内容。此特性对于类似向非web客户端(比如移动应用)提供内容,以及抽取Wagtail站点内容为另一站点使用的情景下,是有用的。 4 | 5 | 请参阅 [RFC 8: Wagtail 编程借口](https://github.com/wagtail/rfcs/blob/master/text/008-wagtail-api.md#12---stable-and-unstable-versions) 了解有关 Wagtail 项目稳定政策的更多信息。 6 | 7 | + [Wagtail编程接口第二版配置手册](v2/configuration.md) 8 | 9 | - [基础配置](v2/configuration.md#basic-configuration) 10 | - [附加设置](v2/configuration.md#addtional-settings) 11 | 12 | 13 | + [Wagtail编程接口第二版使用手册](v2/usage.md) 14 | 15 | - [获取内容](v2/usage.md#fetching-content) 16 | - [默认端点字段](v2/usage.md#default-endpoint-fields) 17 | - [自第一版后的修改](v2/usage.md#changes-since-v1) 18 | -------------------------------------------------------------------------------- /advanced_topics/customisation/index.md: -------------------------------------------------------------------------------- 1 | # 定制Wagtail 2 | 3 | + [定制编辑界面](page_editing_interface.md) 4 | 5 | - 对分页界面进行定制(the tabbed interface) 6 | - 关于富文本(HTML) 7 | - 对生成的表单进行定制 8 | 9 | 10 | + [富文本的内部元素](rich_text_internals.md) 11 | 12 | - 关于富文本的数据格式 13 | - 特效注册表 14 | - 对处理器进行重写 15 | - 对重写处理器进行注册 16 | - 关于编辑器小部件 17 | - 关于编辑器插件 18 | - 格式转换器 19 | 20 | 21 | + [对Draftail编辑器进行扩展](extending_draftail.md) 22 | 23 | - 创建新的内联样式 24 | - 创建新的块 25 | - 创建新的实体 26 | - Draftail小部件的集成 27 | 28 | 29 | + 对Hallo编辑器进行扩展 30 | 31 | - 对富文本元素的白名单操作 32 | 33 | 34 | + 对管理模板进行定制 35 | 36 | - 站点标志的定制(custom branding) 37 | - 在站点标志中指定某个站点或页面 38 | - 对登录表单进行扩展 39 | - 对客户端组件进行扩展 40 | 41 | 42 | + 对用户模型进行定制 43 | 44 | - 对用户表单进行定制的示例 45 | -------------------------------------------------------------------------------- /advanced_topics/images/renditions.md: -------------------------------------------------------------------------------- 1 | # 以Python方式生成图片的转写 2 | 3 | 由Wagtail的`{% image %}`模板标签所生成的原始图片的渲染版本,被成为“转写(reditions)”,他们作为新的图片文件,在初次调用时,就存储在站点的`[media]/images`目录中了。 4 | 5 | 还可通过原生的`get_rendition()`方法,从Python动态地生成图片转写,比如: 6 | 7 | ```python 8 | newimage = myimage.get_rendition('fill-300x150|jpegquality-60') 9 | ``` 10 | 11 | 在`myimage`的文件名为`foo.jpg`时,那么将生成一个名为`foo.fill-300x150.jpegquality-60.jpg`的该图片文件的新的转写,并保存到该站点的`[media]/images`目录。该方法的参数选项与模板标签`{% image %}`的过滤器规范一致,且应使用`|`进行分隔。 12 | 13 | 所生成的`Rendition`对象,将有着一些特定于该图片版本的属性,比如`url`、`width`及`height`等,因此类似于这些就可在某个API生成器中加以使用,比如: 14 | 15 | ```python 16 | url = myimage.get_rendition('fill-300x150|jpegquality-60').url 17 | ``` 18 | 19 | 属于所生成转写的原始图片的那些参数,比如`title`,可通过该转写的`image`属性而访问到: 20 | 21 | ```sh 22 | >>> newimage.image.title 23 | 'Blue Sky' 24 | >>> newimage.image.is_landscape() 25 | True 26 | ``` 27 | 28 | 另请参阅:[在模板中使用图片](../../topics/images.md#image-tag) 29 | -------------------------------------------------------------------------------- /advanced_topics/third_party_tutorials.md: -------------------------------------------------------------------------------- 1 | # 一些第三方教程 2 | 3 | > __警告__ 下面的清单,是第三方开发者的一些教程与开发注解。其中的一些较早的链接,可能已不适应最新版的 Wagtail了。 4 | 5 | 6 | + [学习 Wagtail](https://learnwagtail.com/) -- 一些有关Wagtail方方面面的定期视频教程(2019年3月1日) 7 | + [Wagtail系列教程](https://www.accordbox.com/blog/wagtail-tutorials/) (2019年1月20日) 8 | + [致Django开发者的电子商务教程(带有 Wagtail 线上商店教程)](https://snipcart.com/blog/django-ecommerce-tutorial-wagtail-cms) (2018年7月5日) 9 | + [Wagtail 与 GraphQL](https://jossingram.wordpress.com/2018/04/19/wagtail-and-graphql/) (2018年4月19日) 10 | + [Wagtail 与 Azure 云存储的 blob 容器](https://jossingram.wordpress.com/2017/11/29/wagtail-and-azure-storage-blob-containers/) (2017年11月29日) 11 | + [使用 Twilio 同步技术、Django(包括 Wagtail)及 vue.js 来构建 TwilioQuest](https://www.twilio.com/blog/2017/11/building-twilioquest-with-twilio-sync-django-and-vue-js.html) (2017年11月6日) 12 | + [从Wagtail 1.0 升级到 Wagtail 1.1](https://www.caktusgroup.com/blog/2017/07/19/upgrading-wagtail/) (2017年7月19) 13 | + [Wagtail的多语种:一个用于演示如何实现多语种的简单项目](https://github.com/cristovao-alves/Wagtail-Multilingual) (2017年1月31日) 14 | 15 | [更多请参阅](http://docs.wagtail.io/en/v2.5.1/advanced_topics/third_party_tutorials.html) 16 | -------------------------------------------------------------------------------- /topics/index.md: -------------------------------------------------------------------------------- 1 | # 使用手册 2 | 3 | 4 | + [页面模型](pages.md) 5 | 6 | - Wagtail页面模型示例 7 | - 编写页面模型 8 | - 模板渲染 9 | - 行内模型(Inline models) 10 | - 使用页面 11 | - 技巧 12 | 13 | 14 | + [编写页面模型](writing_templates.md) 15 | 16 | - 模板 17 | - 静态文件 18 | - 模板标签与过滤器 19 | - Wagtail的用户栏 20 | - 在上线前通过预览来验证输出 21 | 22 | 23 | + [在模板中使用图片](images.md) 24 | 25 | - 经由`img`标签实现更多的控制 26 | - `attrs`捷径 27 | - 在富文本中嵌入的图片 28 | - 图片输出格式 29 | - 背景色 30 | - JPEG图片的质量 31 | 32 | 33 | + [搜索功能](search/index.md) 34 | 35 | - 建立索引 36 | - 搜索 37 | - 搜索的后端 38 | 39 | 40 | + [内容块(Snippets)](snippets.md) 41 | 42 | - 内容的模型 43 | - 在模板标签中包含内容块 44 | - 将页面绑定到内容块 45 | - 令到内容块可被搜索 46 | - 给内容块打上标签 47 | 48 | 49 | + [使用`StreamField`特性得到格式自由的页面内容](streamfield.md) 50 | 51 | - 使用`StreamField` 52 | - 基础的块类型 53 | - 结构化的块类型 54 | - 示例:`PersonBlock` 55 | - 模板的渲染 56 | - `BoundBlocks`与值 57 | - `StructBlock`的定制编辑界面 58 | - `StructBlock`的定制值类(Custom value class for `StructBlock`) 59 | - 定制的块类型 60 | - 数据库的迁移 61 | 62 | 63 | + [权限](permissions.md) 64 | 65 | - 页面的权限 66 | - 图片/文档的权限 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | _book/* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | -------------------------------------------------------------------------------- /advanced_topics/settings.md: -------------------------------------------------------------------------------- 1 | # Wagtail下对Django的配置 2 | 3 | 要从零开始安装Wagtail,就要创建一个Django项目,并在那个项目中建立一个应用。有关这些任务的教程,请参见[编写第一个Django应用](https://docs.djangoproject.com/en/stable/intro/tutorial01/)。项目的目录将像下面这样: 4 | 5 | ```sh 6 | myproject/ 7 | myproject/ 8 | __init__.py 9 | settings.py 10 | urls.py 11 | wsgi.py 12 | myapp/ 13 | __init__.py 14 | models.py 15 | tests.py 16 | admin.py 17 | views.py 18 | manage.py 19 | ``` 20 | 21 | 可安全地从应用目录中将`admin.py`与`views.py`移除,因为Wagtail将为模型提供到此功能。对Django进行加载Wagtail的配置,涉及将一些模块与变量加入到`settings.py`,以及将URL的配置加入到`urls.py`。请参考 [Django设置](https://docs.djangoproject.com/en/stable/topics/settings/)与[Django的URL调度器](https://docs.djangoproject.com/en/stable/topics/http/urls/),以获取对这些文件中定义了什么的全面了解。 22 | 23 | 下面的内容,是略过了许多的Django样板性设置的部分设置参考。如此时只想要尽快安装好Wagtail,而不想纠缠于这些设置,那么请参见[已准备好使用示例配置文件](#complete-example-config)。 24 | 25 | 26 | ## 关于中间件(`settings.py`) 27 | 28 | 29 | ## 关于应用(`settings.py`) 30 | 31 | 32 | ## 设置变量(`settings.py`) 33 | 34 | ## URL模式 35 | 36 | 37 | ## 已准备好使用示例配置文件 38 | 39 | -------------------------------------------------------------------------------- /topics/search/index.md: -------------------------------------------------------------------------------- 1 | # 搜索功能 2 | 3 | Wagtail提供了全面且可扩展的搜索接口。此外,其还经由“站点编辑精选(Editor's Picks)”方式,提供提升搜索结果的方法。Wagtail还将一些有关通过搜索接口进行的查询的、简单统计数据收集了起来。 4 | 5 | 6 | + [建立索引](indexing.md) 7 | 8 | - [更新索引](indexing.md#updating-the-index) 9 | - [对额外字段进行索引](indexing.md#indexing-extra-fields) 10 | - [对定制模型建立索引](indexing.md#indexing-custom-models) 11 | 12 | 13 | + [进行搜索](searching.md) 14 | 15 | - [搜索的QuerySet](searching.md#searching-querysets) 16 | - [页面搜索视图示例](searching.md#an-example-page-search-view) 17 | - [提升后的搜索结果](searching.md#promoted-search-results) 18 | 19 | 20 | + [搜索后端](#backends) 21 | 22 | - [`AUTO_UPDATE`](backends.md#auto-update) 23 | - [`ATOMIC_REBUILD`](backends.md#atomic-rebuild) 24 | - [`BACKEND`](backends.md#backend) 25 | 26 | 27 | ## 索引的建立 28 | 29 | 必须首先将对象加入到搜索索引,那么这些对象才是可搜索的。而这涉及到那些想要进行索引的模型与字段的配置(索引正是对页面、图片与文档进行的),随后才是将这些对象插入到索引中。 30 | 31 | 请参阅[更新索引](#updating-the-index)部分,了解如何领导搜索索引中的对象,与数据库中的对象保持同步。 32 | 33 | 在`Page`或`Image`基类的子类中已创建了一些额外字段时,或许打算将这些新的字段加入到搜索索引,从而令到用户的搜索查询,可以与这些页面或图片的额外内容进行匹配。请参阅[对额外字段进行索引](#indexing-extra-fields)。 34 | 35 | 在有着不是从`Page`或`Image`基类派生的定制模型时,却又要令到其可搜索,那么请参阅[建立定制模型的索引](#indexing-custom-models)。 36 | 37 | ## 进行搜索 38 | 39 | Wagtail提供了用于在模型上完成搜索的一个API。同时还可以在Django QuerySets上进行搜索查询。 40 | 41 | 请参阅[进行搜索](#searching)。 42 | 43 | 44 | ## 关于后端 45 | 46 | Wagtail提供了为搜索索引的存储与完成搜索查询提供了三种后端:Elasticsearch、数据库,以及 PostgreSQL(需要Django >= 1.10)。也可运行自己的搜索后端。 47 | 48 | 请参阅 [关于搜索后端](backends.html)。 49 | -------------------------------------------------------------------------------- /advanced_topics/documents/custom_document_model.md: -------------------------------------------------------------------------------- 1 | # 对文档模型进行定制 2 | 3 | 备用的`Document`模型,可用于添加定制的行为与额外字段。 4 | 5 | 要使用`Document`模型,需要完成以下步骤: 6 | 7 | + 创建出一个继承自`wagtail.documents.models.AbstractDocument`的新文档模型。那里就是要加入额外字段的地方。 8 | + 将`WAGTAILDOCS_DOCUMENT_MODEL`指向到该模型。 9 | 10 | 下面是一个示例: 11 | 12 | ```python 13 | # models.py 14 | 15 | from wagtail.documents.models import Document, AbstractDocument 16 | 17 | class CustomDocument(AbstractDocument): 18 | # 定制字段示例: 19 | source = models.CharField( 20 | max_length=255, 21 | # 下面两个参数必须进行设置,以允许Wagtail在上传时创建出一个文档实例 22 | blank=True, 23 | null=True 24 | ) 25 | 26 | admin_form_fields = Document.admin_form_fields + ( 27 | # 这里要加入所有定制字段的名称,以令到他们出现在表单中: 28 | 'source', 29 | ) 30 | ``` 31 | 32 | > **注意** 在定制文档模型上定义的那些字段,必须要么设置为非必须(`blank=True`),要么指定一个默认值。这是因为文档的上传与定制数据的输入是两个不同的动作。Wagtail需要在上传时能够立即创建出一条文档记录。 33 | 34 | 在应用设置模块中: 35 | 36 | ```python 37 | # 要确保将下面的 app_lebel,替换为之前把定制模型放入的那个应用 38 | WAGTAILDOCS_DOCUMENT_MODEL = 'app_lebel.CustomDocument' 39 | ``` 40 | 41 | __从内建文档模型进行迁移__ 42 | 43 | 在将某个既有站点修改为使用某个定制文档模型时,将不会自动拷贝原有文档到新的模型。这需要手动使用一个[数据迁移](https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations)来完成。 44 | 45 | 对内建文档模型进行引用的所有模板仍将如之前那样工作 46 | 47 | ## 对该定制文档模型的引用 48 | 49 | 50 | + `wagtail.documents.models.get_document_model()` 51 | 52 | 该方法从`WAGTAILDOCS_DOCUMENT_MODEL`设置获取到文档模型。在没有定义定制模型时,默认为标准的`Document`模型。 53 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Translated by Peng Hailin, laxers@gmail.com", 3 | "description": "这是一本wagtail教程, python, Django, web, framework, 框架, 内容管理系统, CMS, Content Management System", 4 | "generator": "xfoss.com 网站", 5 | "links": { 6 | "sharing": { 7 | "weibo": null 8 | }, 9 | "sidebar": { 10 | "Wagtail 教程": "https://wagtail.xfoss.com/" 11 | } 12 | }, 13 | "pdf": { 14 | "fontSize": 12, 15 | "footerTemplate": null, 16 | "headerTemplate": null, 17 | "margin": { 18 | "bottom": 36, 19 | "left": 62, 20 | "right": 62, 21 | "top": 36 22 | }, 23 | "pageNumbers": false, 24 | "paperSize": "a4" 25 | }, 26 | "plugins" : [ 27 | "highlight-code", 28 | "livereload", 29 | "lunr", 30 | "sharing", 31 | "fontsettings", 32 | "theme-comscore", 33 | "qrcode-list" 34 | ], 35 | "pluginsConfig": { 36 | "qrcode-list": { 37 | "title": "打尚&联系", 38 | "description": "如果您感觉有收获,欢迎给我打赏,以激励我输出更多的优质内容。", 39 | "except": ["谢谢你的支持"], 40 | "lists": [ 41 | { 42 | "src": "./images/a6x09981lks9yco3b8xcqf0.png", 43 | "content": "支付宝", 44 | "alt": "支付宝" 45 | }, 46 | { 47 | "src": "./images/wechat-pay.png", 48 | "content": "微信支付", 49 | "alt": "微信支付" 50 | } 51 | ] 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /getting_started/index.md: -------------------------------------------------------------------------------- 1 | # 入门 2 | 3 | Wagtail 是构建于 [Django web 框架](https://www.djangoproject.com/) 之上的,因此本文档假定已经安装好了必要的软件包。如尚未安装这些软件包,那么就要安装以下必要的软件/程序: 4 | 5 | + [Python](https://www.python.org/downloads/) 6 | + [pip](https://pip.pypa.io/en/latest/installing.html)(请注意 `pip` 已默认包含在Python 3.4及以后的发布中) 7 | 8 | 9 | 同时建议使用 Virtualenv, 其提供了隔离的Python环境: 10 | 11 | + [Virtualenv](https://virtualenv.pypa.io/en/latest/installation.html) 12 | 13 | 14 | > **要点** 在安装 Wagtail 之前,有必要安装 `libjpeg` 与 `zlib` 两个库,他们提供了处理 JPEG、PNG 与 GIF 图像的支持(通过 Python 的`Pillow`库)。而安装这两个库的方式,则根据平台的不同而有所区别 -- 请参阅 Pillow 的 [特定平台安装说明](http://pillow.readthedocs.org/en/latest/installation.html#external-libraries)。 15 | 16 | 17 | 在安装了上述库之后,安装Wagtail的最快方式就是: 18 | 19 | *若使用了 Virtualenv, 那么请运行* 20 | 21 | ```sh 22 | $python3 -m venv .venv 23 | ``` 24 | 25 | ```sh 26 | $ mkdir wagtail-demo && cd wagtail-demo 27 | $. ~/.venv/bin/activate 28 | $ pip install wagtail 29 | ``` 30 | 31 | 在安装完毕后,Wagtail提供了一个类似于 Django 的 `django-admin startproject`命令,用于生成一个新的站点/项目(stubs out a new site/project): 32 | 33 | ```sh 34 | $ wagtail start demo 35 | ``` 36 | 37 | 这将基于一个包含了满足起步所有需要的模板,而建立一个新的文件夹 `demo`。更多有关该模板的信息,请移步 [这里](getting_started/project_template.html)。 38 | 39 | 此时在 `demo` 文件夹中,只要运行一下对于所有Django项目来说都需要的必要几步: 40 | 41 | ```sh 42 | $ pip install -r requirements.txt 43 | $ ./manage.py migrate 44 | $ ./manage.py createsuperuser 45 | $ ./manage.py runserver 46 | ``` 47 | 48 | 现在就可以在`http://localhost:8000`访问到该站点了,同时在`http://localhost:8000/admin`出可以访问到管理后端。 49 | 50 | 这些步骤建立起来一个新的单机化的Wagtail项目。如要将Wagtail加入到某个既有的Django项目,则请参阅[将Wagtail集成到Django项目中](getting_started/integrating_into_django.html)。 51 | 52 | 有一些可选`pip`包没有包含到默认安装,但推荐使用他们来提高性能或赋予Wagtail某些特性,包括: 53 | 54 | + [Elasticsearch](advanced_topics/performance.html) 55 | + [特性发现](advanced_topics/images/feature_detection.html) 56 | -------------------------------------------------------------------------------- /advanced_topics/images/custom_image_model.md: -------------------------------------------------------------------------------- 1 | # 对图片模型进行定制 2 | 3 | `Image`模型可被定制,从而实现将额外字段添加到图片。 4 | 5 | 为了实现这个目的,需将以下两个模型添加到项目中: 6 | 7 | + 从`wagtail.images.models.AbstractImage`继承的图片模型本身。这就是要将额外字段进行添加的地方。 8 | + 从`wagtail.images.models.AbstractRendition`继承的转写模型。这用于存储新模型的转写。 9 | 10 | 11 | 以下是一个示例: 12 | 13 | ```python 14 | # models.py 15 | from django.db import models 16 | 17 | from wagtail.images.models import Image, AbstractImage, AdstractRendition 18 | 19 | class CustomImage(AbstractImage): 20 | # 在这里将所有额外字段添加到图片 21 | 22 | # 比如,要添加一个说明字段: 23 | # caption = models.CharField(max_length=255, blank=True) 24 | 25 | admin_form_fields = Image.admin_form_fields + ( 26 | # 随后在这里添加字段名称,以令到其出现在表单中: 27 | 'caption', 28 | ) 29 | 30 | class CustomRendition(AbstractRendition): 31 | image = models.ForeignKey(CustomImage, on_delete=models.CASACDE, related_name='renditions') 32 | 33 | class Meta: 34 | unique_together = ( 35 | ('image', 'filter_spec', 'focal_point_key'), 36 | ) 37 | ``` 38 | 39 | > **注意** 在某个定制图片模型上定义的字段,必须要么设置为非要求的(`blank=True`),要么指定一个默认值 -- 这是因为图片的上传与定制数据的输入,是两个独立发生的事情,同时Wagtail需要能够在上传时立即创建出一条图片记录。 40 | 41 | 之后要将`WAGTAILIMAGES_IMAGE_MODEL`设置为指向该定制图片模型: 42 | 43 | ```python 44 | WAGTAILIMAGES_IMAGE_MODEL = 'images.CustomImage' 45 | ``` 46 | 47 | __从内建图片模型进行迁移__ 48 | 49 | __Migrating from the builtin image model__ 50 | 51 | 在将某个既有站点更改为使用某种定制图片模型时,图片不会自动拷贝到新的模型。需要使用一个[数据迁移](https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations),来将原有图片拷贝到新的模型。 52 | 53 | 所有参考内建图片模型的模板,仍将之前那样继续工作,但要对其进行更新,才能观看到新的图片。 54 | 55 | 56 | ## 对定制图片模型的引用 57 | 58 | 59 | ### `wagtail.images.get_image_model()` 60 | 61 | 从`WAGTAILIMAGES_IMAGE_MODEL`设置取得图片模型。此方法对于那些制作Wagtail插件、需要图片模型的开发者有用。在没有定义定制模型时,默认为标准的`Image`模型。 62 | 63 | 64 | 65 | 获取图片模型的字符串形式的、带点的`app.Model`名称。对于制作Wagtail插件、需要对图片模型进行引用,比如在外键中,但又不需要模型本身的开发者有用。 66 | -------------------------------------------------------------------------------- /advanced_topics/images/feature_detection.md: -------------------------------------------------------------------------------- 1 | # 特征识别 2 | 3 | __Feature Detection__ 4 | 5 | Wagtail有着自动侦测面孔及图片中一些特征,并根据这些特征而对图片加以裁剪的能力。 6 | 7 | 特征识别特性,在图片上传时,使用OpenCV来识别图片中的面孔/特征。识别到的特征,作为`Image`模型上`focal_point_{x, y, width, height}`字段中的一个焦点,进行了内部保存(the detected features stored internally as a focal point in the `focal_point_{x, y, width, height}` fields on the `Image` model)。这些字段在图片于模板中被渲染时,为`fill`图片过滤器用来对图片进行裁剪。 8 | 9 | ## 设置 10 | 11 | 特征识别需要OpenCV,而OpenCV在安装时有点麻烦,因为其当前尚不支持`pip`的安装。 12 | 13 | ### 在Debian/Ubuntu上按照OpenCV 14 | 15 | Debian与Ubuntu提供了一个名为`python-opencv`的`apt-get`软件包: 16 | 17 | ```sh 18 | $ sudo apt-get install python-opencv python-numpy 19 | ``` 20 | 21 | 这将把`PyOpenCV`安装到站点包中。在使用虚拟环境时,就要确保开启了站点包,否则Wagtail将不能导入`PyOpenCV`。 22 | 23 | ### 在虚拟环境中开启站点包 24 | 25 | __Enabling site packages in the virtual environment__ 26 | 27 | 如未用到虚拟环境,那么可跳过此步骤。 28 | 29 | 站点包的开启,则会根据是否使用`pyvenv`(仅 Python 3.3以上版本),还是`virtualenv`来管理虚拟环境,而有所不同。 30 | 31 | + `pyvenv` 32 | 33 | 前往`pyvenv`目录并打开`pyvenv.cfg`文件,然后将`include-system-site-packages`设置为`true`。 34 | 35 | 36 | + `virtualenv` 37 | 38 | 前往虚拟环境目录并删除一个名为`lib/python-x.x/no-global-site-packages.txt`的文件。 39 | 40 | ## 对OpenCV安装进行测试 41 | 42 | 此时可通过打开一个Python命令行界面(在虚拟环境激活下),并进行下面的输入,对Wagtail是否能看到OpenCV进行测试: 43 | 44 | ```python 45 | import cv 46 | ``` 47 | 48 | > **译者注** 这里尚需本地安装 `opencv-python` 包,通过 `pip install opencv-pyton`,并输入`import cv2`。 49 | 50 | 此时如没有看到`ImportError`,那么就算已经安装好了(如看到`libdc1394 error: Failed to initialize libdc1394`,这是无害的告警,可被忽略)。 51 | 52 | ### 在Wagtail中开启特征识别特性 53 | 54 | 一旦OpenCV安装好了,就需要将`WAGTAILIMAGES_FEATURE_DETECTION_ENABLED`设置为`True`: 55 | 56 | ```python 57 | # settings.py 58 | 59 | WAGTAILIMAGES_FEATURE_DETECTION_ENABLED = True 60 | ``` 61 | 62 | ### 手动运行特征识别 63 | 64 | 特征识别是在有新的图片上传到Wagtail中时运行的。在站点中已有一些图片且想要在这些图片上运行特征识别时,就必须手动运行了。 65 | 66 | ```python 67 | from wagtail.images.models import Image 68 | 69 | for image in Image.objects.all(): 70 | if not image.has_focal_point(): 71 | image.set_focal_point(image.get_suggested_focal_point()) 72 | image.save() 73 | ``` 74 | -------------------------------------------------------------------------------- /advanced_topics/index.md: -------------------------------------------------------------------------------- 1 | # 高级话题 2 | 3 | + [关于图片](images/index.md) 4 | 5 | - [用Python生成图片的转写](images/renditions.md) 6 | - [对动画GIF的支持](images/animated_gifs.md) 7 | - [对图片模型进行定制](images/custom_image_model.md) 8 | - [特性侦测](images/feature_detection.md) 9 | - [动态的图片提供视图](images/image_serve_view.md) 10 | 11 | 12 | + [关于文档](documents/index.md) 13 | 14 | - [对文档模型进行定制](documents/custom_document_model.md) 15 | 16 | 17 | + [嵌入的内容](embeds.md) 18 | 19 | - [在站点上嵌入内容](embeds.md#embedding-content-on-your-site) 20 | - [配置嵌入的 “查找器”(configuring embed "finders")](embeds.md#configuring-embed-finders) 21 | 22 | 23 | + [为Wagtail配置Django](settings.md) 24 | 25 | - [关于中间件(`settings.py`)](settings.md#middleware) 26 | - [关于应用(`settings.py`)](settings.md#apps) 27 | - [关于设置变量(`settings.py`)](settings.md#settings-variables) 28 | - [URL的模式](settings.md#url-patterns) 29 | - [准备好使用示例配置文件](settings.md#complete-example-config) 30 | 31 | 32 | + [Wagtail的部署](deploying.md) 33 | 34 | - 在自己的服务器上部署 35 | - 在 Divio 云上部署 36 | - 在 PythonAnywhere 上部署 37 | - 在其他 PAASs 与 IAASs 上部署 38 | - 部署技巧 39 | 40 | 41 | + [性能问题](performance.md) 42 | 43 | - 编辑界面 44 | - 公开用户 45 | 46 | 47 | + [国际化](i18n/index.md) 48 | 49 | - Wagtail 管理节目的翻译 50 | - 针对每名用户而改变Wagtail管理界面的语言 51 | - 修改Wagtail安装的主要语言 52 | - 创建带有多种语言的站点 53 | 54 | 55 | + [私有页面](privacy.md) 56 | 57 | - 设置一个登录页面 58 | - 设置一个全局的“需要口令”的页面 59 | - 为某种特定页面类型设置“需要口令”页面 60 | 61 | 62 | + [Wagtail的定制](customisation/index.md) 63 | 64 | - [对编辑界面进行定制](customisation/page_editing_interface.md) 65 | - [富文本内部](customisation/rich_text_internals.md) 66 | - 对Draftail编辑器进行扩展 67 | - 对Hallo编辑器进行扩展 68 | - 对管理界面模板进行定制 69 | - 对用户模型进行定制 70 | 71 | 72 | + [第三方教程](third_party_tutorials.md) 73 | 74 | 75 | + [Jinja2模板的支持](jinja2.md) 76 | 77 | - 对Django进行配置 78 | - 模板中的`self` 79 | - 模板的标签、函数与过滤器 80 | 81 | 82 | + [对Wagtail站点进行测试](testing.md) 83 | 84 | - WagtailPageTests 85 | - 表单数据助手 86 | - Fixtures 87 | 88 | 89 | + [Wagtail编程接口](api/index.md) 90 | 91 | - [Wagtail API 第二版 配置手册](api/configuration.md) 92 | - [Wagtail API 第二版使用手册](api/usage.md) 93 | -------------------------------------------------------------------------------- /topics/permissions.md: -------------------------------------------------------------------------------- 1 | # 权限 2 | 3 | Wagtail采用并扩展了[Django权限系统](https://docs.djangoproject.com/en/stable/topics/auth/default/#topic-authorization),以满足网站内容创建的需求,诸如审批流程以及多个团队在某个站点(或在同一Wagtail安装下的多个站点)的不同区域上工作等等。经由Wagtail管理界面的“设置”下的“用户组(Group)”区域,可对权限进行配置。 4 | 5 | ## 页面的权限 6 | 7 | 在页面树的任何位置,都可将权限附加上去,并沿树向下传导。比如在某个站点有着这样的页面树时: 8 | 9 | ```sh 10 | MegaCorp/ 11 | About us 12 | Offices/ 13 | UK 14 | France 15 | Germany 16 | ``` 17 | 18 | 随后某个有着在“Offices”页面有着“编辑”权限的组,将自动收到编辑“UK”、“France”与“Germany”页面的能力。通过将权限指派给“根”页面,便可全局地设置权限 -- 因为所有页面都必须存在于根节点之下,同时根是无法删除的,那么此项权限就会覆盖当前与未来存在的所有页面。 19 | 20 | 在某名用户经由Wagtail管理界面创建一个页面的时候,那名用户就被制定为其所创建页面的所有者了。有着“添加页面”权限的所有用户,都具有编辑其拥有页面的权限,以及添加新页面的权限。这是出于页面的创建,典型的就是一个涉及到创建一些草稿版本的迭代过程 -- 赋予用于创建草稿的能力,而不让他们进行接下来的编辑工作,是毫无用处的(this is in recognition of the fact that creating pages is typically an iterative process involving creating a number of draft versions -- giving a user the ability to create a draft but not letting them subsquently edit it would not be very useful)。能对某个页面进行编辑,也就意味着能删除该页面;与Django的标准权限模型不同,Wagtail中没有明显的“删除”权限。 21 | 22 | 完整的可用权限集如下: 23 | 24 | + `Add` -- 赋予创建此页面之下新的子页面的能力(提供了页面模型允许这一点 -- 请参阅[父页面/子页面类型规则](pages.html#parent-page-subpage-type-rules)),以及编辑与删除当前用户所有的页面的能力。除非用户还具有“发布”权限,那么已发布的页面是无法删除的。 25 | 26 | + `Edit` -- 授予对该页面,以及其下所有页面的编辑与删除能力,而无关页面的归属。仅有“编辑”权限的用户,是不能创建新页面的,只能对既有页面进行编辑。对于已发布的页面,除非该用户还有着“发布”权限,否则不能被删除。 27 | 28 | + `Publish` -- 授予对该页面和/或其子页面进行发布或撤回的能力。不具有发布权限的用户,无法直接对站点作出访问者可见的修改;而是必须将修改提交审核(这将发出一条通知给有着发布权限的用户)。发布权限与编辑权限是分开的;仅有发布权限的用户,将不能够进行任何编辑工作。 29 | 30 | + `Bulk delete` -- 允许用户以单次操作,删除一些有着后代页面的页面。如没有此种权限,那么就必须在删除父页面之前,先逐个地删除后代页面。这实际上是一种避免误删除的安全措施。`Bulk delete`必须与“添加”/“编辑”权限结合使用,因为他并未提供了到其本身的任何删除权力;他仅提供了一直该用户已具有的权限的“捷径”。比如,某名仅有着“添加”与“批量删除”权限的用户,将只能在受影响的页面属于那名用户,且这些页面处于尚未发布状态时,进行批量删除。 31 | 32 | + `Lock` -- 授予对该页面(及其下的所有页面)进行编辑锁定或解锁的能力,以阻止用户再对其进行任何的编辑。 33 | 34 | 对于草稿,只有在用户有着编辑或发布权限时,才能查看到。 35 | 36 | ## 图片/文档的权限 37 | 38 | 图片与文档的权限规则,在与页面类似的基础上运作。图片与文档被看着是由上传他们的用户“所有”;有着“添加”权限的用户,也具有对他们所拥有的图片/文档内容进行编辑的能力;删除则被看着是与编辑等同的,而非一种特别的权限类型。 39 | 40 | 对特定的一些图片与文档的访问,可通过设置 “ *集合(collections)* ”进行控制。默认所有图片与文档,都属于“根”集合,但可经由管理界面的 `Settings` -> `Collections` 区域,创建处新的集合。在“根”上设置的权限,适用于所有集合,因此某名具有在根上的图片的 “编辑” 权限的用户,就可对所有图片进行编辑;在其他集合上设置的权限,则只适用与那个集合了。 41 | -------------------------------------------------------------------------------- /advanced_topics/performance.md: -------------------------------------------------------------------------------- 1 | # 性能问题 2 | 3 | Wagtail以速度为设计目标,体现在编辑器界面与前端上,但在想要更好的性能,或需要处理非常高的流量时,下面就是一些从安装中压榨更多性能的技巧。 4 | 5 | ## 编辑器界面的性能 6 | 7 | Wagtail开发者已经尽力将一个可以工作的Wagtail安装的外部依赖最小化了,目的就是令其尽可能简单的运作起来。尽管如此,仍可对一些默认设置进行配置,以获取更好的性能: 8 | 9 | ### 缓存方面 10 | 11 | 这里推荐使用 [Redis](http://redis.io/) 作为一个快速、持久的缓存。经由包管理器(在Debian或Ubuntu上:`sudo apt-get install redis-server`),并将`django-redis`添加到`requirements.txt`,且将其作为一个缓存后端进行开启: 12 | 13 | ```python 14 | CACHES = { 15 | 'default': { 16 | 'BACKEND': 'django_redis.cache.RedisCache', 17 | 'LOCATION': 'redis://127.0.0.1:6379/0', 18 | 'OPTIONS': { 19 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 20 | 'PASSWORD': '+7xYeaxVbiR/Kkm+gv5p2LoSALRpRmzSCqMTRC2KE2D+gHiDf4/7Sdhx+mW/szMtwEgZH96ZIJKUPJj/', 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | ### 搜索方面 27 | 28 | Wagtail有着对 [Elasticsearch](http://www.elasticsearch.org/)很强的支持 -- 同时对编辑器界面与站点用户来说 -- 但在没有 Elasticsearch时也可回滚到数据库搜索。比起Django用于文本搜索的ORM,Elasticsearch更为快速且更为强大,因此推荐安装Elasticsearch,或者使用一个像是 [Searchly](http://www.searchly.com/)这样的托管服务。 29 | 30 | 更多有关配置Elasticsearch下的Wagtail的内容,请参见[Elasticsearch后端](../topics/search/backends.md#wagtailsearch-backends-elasticsearch)。 31 | 32 | ### 数据库方面 33 | 34 | Wagtail在PostgreSQL、SQLite与MySQL上进行过测试。他也应工作在一些第三方数据库后端上(MS SQL已知可以工作但未进行测试)。这里推荐使用PostgreSQL作为生产用途。 35 | 36 | ## 模板方面 37 | 38 | 读取与编译模板方面的压力可能叠加起来。在某些情形下可通过使用 [Django带有缓存的模板加载器](https://docs.djangoproject.com/en/stable/ref/templates/api/#django.template.loaders.cached.Loader),而获取到显著的性能提升: 39 | 40 | ```python 41 | TEMPLATES = [{ 42 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 43 | 'DIR': [os.path.join(BASE_DIR, 'templates')], 44 | 'OPTIONS': { 45 | 'loaders': [ 46 | ('django.template.loaders.cached.Loader', [ 47 | 'django.template.loaders.filesystem.Loader', 48 | 'django.template.loaders.app_directories.Loader', 49 | ]) 50 | ] 51 | } 52 | }] 53 | ``` 54 | 55 | 但对使用此种加载器有一个注意事项。一旦模板被缓存起来,那么对模板文件的修改将不会生效。这就意味着 *不* 应在开发环境使用此加载器。 56 | 57 | ## 对于公开用户 58 | 59 | ### 缓存的代理服务器 60 | 61 | 为了支持有着出色响应时间的海量流量,推荐使用某种缓存代理方案。在生产中已对 [Vanish](http://www.varnish-cache.org/) 与 [Squid](http://www.squid-cache.org/) 进行过测试。像是 [Cloudflare](https://www.cloudflare.com/)一类的托管代理也应可以工作。 62 | 63 | Wagtail有着Vanish/Squid上自动缓存失效的支持。请参阅[前端缓存无效化](reference/contrib/frontendcache.md#frontend-cache-purging)。 64 | -------------------------------------------------------------------------------- /advanced_topics/customisation/custom_user_model.md: -------------------------------------------------------------------------------- 1 | # 定制用户模型 2 | 3 | ## 用户表单定制示例 4 | 5 | 本示例对如何将一个文本字段与外键,添加到某个定制的用户模型,及将Wagtail的用户表单配置为允许对这些字段进行更新进行了演示。 6 | 7 | 创建出一个定制用户模型。在此情形下,这里对`AbstractUser`类进行了扩展,并添加了两个字段。这里的外键引用了另一个模型(并未给出)。 8 | 9 | ```python 10 | class User(AbstractUser): 11 | country = models.CharField(verbose_name='国家', max_length=255) 12 | status = models.ForeignKey(MembershipStatus, on_delete=models.SET_NULL, null=True, default=1) 13 | ``` 14 | 15 | 将包含了这个用户模型的应用,添加到 `INSTALLED_APPS` 并将 `AUTH_USER_MODEL` 设置为对该模型的应用。在本示例中,应用名为 `users` 同时模型为 `User` 16 | 17 | ```python 18 | AUTH_USER_MODEL = 'users.USER' 19 | ``` 20 | 21 | 在应用中创建出定制的用户 `create` 与 `edit` 表单: 22 | 23 | ```python 24 | from django import forms 25 | from django.utils.translation import ugettext_lazy as _ 26 | 27 | from wagtail.users.forms import UserEditForm, UserCreationForm 28 | 29 | class CustomUserEditForm(UserEditForm): 30 | contry = forms.CharField(required=True, label=_("Country")) 31 | status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status")) 32 | 33 | class CustomUserCreationForm(UserCreationForm): 34 | country = forms.CharField(required=True, label=_("Country")) 35 | status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status")) 36 | ``` 37 | 38 | 对Wagtail的用户 `create` 与 `edit` 模板加以扩展。这些扩展的模板应放在模板目录`wagtailusers/users`下。 39 | 40 | 模板 `create.html`: 41 | 42 | {% raw %} 43 | 44 | {% extends "wagtailusers/users/create.html" %} 45 | 46 | {% block extra_fields %} 47 | {% include "wagtailadmin/shared/field_as_li.html" with field=form.country %} 48 | {% include "wagtailadmin/shared/field_as_li.html" with field=form.status %} 49 | {% endblock %} 50 | {% endraw %} 51 | 52 | 53 | 模板 `edit.html`: 54 | 55 | {% raw %} 56 | 57 | {% extends "wagtailusers/users/edit.html" %} 58 | 59 | {% block extra_fields %} 60 | {% include "wagtailadmin/shared/field_as_li.html" with field=form.country %} 61 | {% include "wagtailadmin/shared/field_as_li.html" with field=form.status %} 62 | {% endblock %} 63 | {% endraw %} 64 | 65 | 66 | 这里的 `extra_fields` 块允许将一些字段插入到默认模板中的 `last_name` 字段下面。有一些其他块的覆写选项,允许将一些字段追加到既有字段的末尾或开头,或是允许对所有字段进行重新定义。 67 | 68 | 将下面这些 Wagtail 设置项添加到项目,以对这些用户表单附加项进行引用: 69 | 70 | ```python 71 | WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm' 72 | WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm' 73 | WAGTAIL_USER_CUSTOM_FIELDS = ['country', 'status'] 74 | ``` 75 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | * [Wagtail 内容管理系统 CMS 教程](README.md) 4 | 5 | * [入门](getting_started/index.md) 6 | 7 | * [第一个Wagtail站点](getting_started/tutorial.md) 8 | * [将Wagtail集成到现有的Django项目](getting_started/integrating_into_django.md) 9 | * [Wagtail之禅](getting_started/the_zen_of_wagtail.md) 10 | 11 | * [使用手册](topics/index.md) 12 | 13 | * [关于页面模型](topics/pages.md) 14 | * [编写模板](topics/writing_templates.md) 15 | * [在模板中使用图片](topics/images.md) 16 | * [搜索功能](topics/search/index.md) 17 | * [建立索引](topics/search/indexing.md) 18 | * [进行搜索](topics/search/searching.md) 19 | * [搜索后端](topics/search/backends.md) 20 | * [内容片段](topics/snippets.md) 21 | * [使用`StreamField`特性的自由格式页面内容](topics/streamfield.md) 22 | * [权限问题](topics/permissions.md) 23 | 24 | * [高级话题](advanced_topics/index.md) 25 | 26 | * [图片](advanced_topics/images/index.md) 27 | * [以Python方式生成图片的转写](advanced_topics/images/renditions.md) 28 | * [动画GIF的支持](advanced_topics/images/animated_gifs.md) 29 | * [对图片模型进行定制](advanced_topics/images/custom_image_model.md) 30 | * [特性侦测](advanced_topics/images/feature_detection.md) 31 | * [动态地提供图片的视图](advanced_topics/images/image_serve_view.md) 32 | * [文档](advanced_topics/documents/index.md) 33 | * [对文档模型进行定制](advanced_topics/documents/custom_document_model.md) 34 | * [关于嵌入内容](advanced_topics/embeds.md) 35 | * [Wagtail下对Django的配置](advanced_topics/settings.md) 36 | * [部署Wagtail](advanced_topics/deploying.md) 37 | * [性能问题](advanced_topics/performance.md) 38 | * [国际化问题](advanced_topics/i18n/index.md) 39 | * [创建多语言站点(通过复制页面树的方式)](advanced_topics/i18n/duplicate_tree.md) 40 | * [私有页面](advanced_topics/privacy.md) 41 | * [定制Wagtail](advanced_topics/customisation/index.md) 42 | * [编辑接口的定制](advanced_topics/customisation/page_editing_interface.md) 43 | * [关于富文本内部元素](advanced_topics/customisation/rich_text_internals.md) 44 | * [对Draftail编辑器的扩展](advanced_topics/customisation/extending_draftail.md) 45 | * [对Hallo编辑器的扩展](advanced_topics/customisation/extending_hallo.md) 46 | * [管理模板的定制](advanced_topics/customisation/admin_templates.md) 47 | * [对用户模型进行定制](advanced_topics/customisation/custom_user_model.md) 48 | * [第三方教程](advanced_topics/third_party_tutorials.md) 49 | * [Jinja2模板的支持](advanced_topics/jinja2.md) 50 | * [对 Wagtail 站点进行测试](advanced_topics/testing.md) 51 | * [Wagtail的编程接口](advanced_topics/api/index.md) 52 | * [Wagtail编程接口第二版配置手册](advanced_topics/api/v2/configuration.md) 53 | * [Wagtail编程接口第二版使用手册](advanced_topics/api/v2/usage.md) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wagtail教程 2 | 3 | 你可以直接在 xfoss.com 阅读本书:[wagtail.xfoss.com](https://wagtail.xfoss.com/)。xfoss.com 还有其他书籍: 4 | 5 | + [60天通过CCNA考试](https://ccna60d.xfoss.com),一本基础的网络通讯书,翻译自国外经典 CCNA 60 days 6 | + [TypeScript 手册](https://ts.xfoss.com/),一本TypeScript的入门书籍,翻译自 TypeScript官方网站 7 | 8 | 9 | 你可以在 https://github.com/gnu4cn/wagtailCMS-tutorial 对本项目进行 fork, 并提交你的修正。 10 | 11 | Wagtail是一套以 Python 编写的、构建於 Django web框架之上的开放源代码内容管理系统。 12 | 13 | 本教程由以下章节构成,希望在学习这些内容后,能对其有全面掌握与切实使用。 14 | 15 | + 初步 16 | - [入门](getting_started/index.md) 17 | - [第一个Wagtail站点](getting_started/tutorial.md) 18 | - 演示网站 19 | 20 | + [使用Wagtail](topics/index.md) 21 | - [关于页面模型](topics/pages.md) 22 | - [编写模板](topics/writing_templates.md) 23 | - [在模板中使用图片](topics/images.md) 24 | + [搜索功能](topics/search/index.md) 25 | - [建立索引](topics/search/indexing.md) 26 | - [进行搜索](topics/search/searching.md) 27 | - [关于搜索后端](topics/search/backends.md) 28 | - [第三方教程](advanced_topics/third_party_tutorials.md) 29 | 30 | + 致内容编辑者 31 | - 内容编辑者指南 32 | 33 | ## 索引 34 | 35 | 36 | + [入门](getting_started.md) 37 | 38 | - [第一个Wagtail站点](getting_started/tutorial.md) 39 | - 演示站点 40 | - 将 Wagtail集成到某个Django项目 41 | - [Wagtail哲學](getting_started/the_zen_of_wagtail.md) 42 | 43 | 44 | + [使用手册](topics/index.md) 45 | 46 | - [页面模型](topics/pages.md) 47 | - [编写模板](topics/writing_templates.md) 48 | - [在模板中使用图片](topics/images.md) 49 | - [搜索功能](topics/search/index.md) 50 | - [内容片段](topics/snippets.md) 51 | - [使用 StreamField 的自由格式页面内容](topics/streamfield.md) 52 | - [权限相关](topics/permissions.md) 53 | 54 | 55 | + [高级话题](advanced_topics/index.md) 56 | 57 | - [图片](advanced_topics/images/index.md) 58 | - [文档](advanced_topics/documents/index.md) 59 | - [嵌入的内容](advanced_topics/embeds.md) 60 | - [Wagtail下Django的配置](advanced_topics/settings.md) 61 | - [Wagtail的部署](advanced_topics/deploying.md) 62 | - [性能问题](advanced_topics/performance.md) 63 | - [国际化](advanced_topics/i18n/index.md) 64 | - [私有页面](advanced_topics/privacy.md) 65 | - [定制Wagtail](advanced_topics/customisation/index.md) 66 | - [第三方教程](advanced_topics/third_party_tutorials.md) 67 | - [Jinja2 模板的支持](advanced_topics/jinja2.md) 68 | - [对Wagtail站点进行测试](advanced_topics/testing.md) 69 | - [Wagtail编程接口](advanced_topics/api/index.md) 70 | 71 | 72 | + 参考文档 73 | 74 | - 页面 75 | - 贡献模块 76 | - 管理命令 77 | - 钩子 78 | - 信号 79 | - 项目模板 80 | 81 | 82 | + 支持 83 | 84 | 85 | + Wagtail的使用:内容编辑手册 86 | 87 | - 简介 88 | - 入门 89 | - 找到你自己的方式 90 | - 建立新的页面 91 | - 编辑既有页面 92 | - 对文档或图片进行管理 93 | - 对重定向进行管理 94 | - 管理员的任务 95 | - 浏览器问题 96 | 97 | 98 | + 贡献到Wagtail 99 | 100 | - 问题追踪 101 | - 开发 102 | - 提交代码 103 | - 用户节目样式手册 104 | - 一般代码编写指南 105 | - Python代码编写指南 106 | - CSS 代码编写指南 107 | - JavaScript代码编写指南 108 | - Wagtail的发布流程 109 | -------------------------------------------------------------------------------- /advanced_topics/jinja2.md: -------------------------------------------------------------------------------- 1 | # Jinja2 模板的支持 2 | 3 | Wagtail支持 Jinja2 模板的全部前端特性。更多有关以下模板标签的信息,可在 [编写模板](https://wagtail.xfoss.com/topics/writing_templates.md#writing_templates) 文档中找到。 4 | 5 | ## 对Django进行配置 6 | 7 | 需要对 Django 进行配置,以支持到 Jinja2 模板。因为 Wagtail 的管理界面是以常规 Django 模板进行编写的,因此就必须将Django配置为同时使用两张模板引擎。请将下面的配置添加到应用的 `TEMPLATE` 设置项: 8 | 9 | ```python 10 | TEMPLATES = [ 11 | # ... 12 | 13 | { 14 | 'BACKEND': 'django.template.backends.jinja2.Jinja2', 15 | 'APP_DIRS': True, 16 | 'OPTIONS': { 17 | 'extensions': [ 18 | 'wagtail.core.jinja2tags.core', 19 | 'wagtail.admin.jinja2tags.userbar', 20 | 'wagtail.images.jinja2tags.images', 21 | ] 22 | } 23 | } 24 | ] 25 | ``` 26 | 27 | Jinja 的模板,必须放在应用的 `jinja2/` 目录中。比如`events` 应用中的 `EventPage` 模型的模板,就应被创建于 `events/jinja2/events/event_page.html`。 28 | 29 | 默认下,Jinja 的环境并没有任何的 Django 函数或过滤器。Django文档中有着有关 [为 Django 而对 Jinja 进行配置](https://docs.djangoproject.com/en/stable/topics/templates/#django.template.backends.jinja2.Jinja2)的更多信息。 30 | 31 | ## 模板中的 `self` 32 | 33 | 在 Django 的模板中,`self` 可用于对当前页面、流式块(stream block)或字段面板进行引用。但在 Jinja 中,`self`则是保留作内部用途的。在编写Jinja模板时,使用 `page` 来引用页面,使用`value`来引用流式块,同时使用`field_panel`来引用字段面板。 34 | 35 | 36 | ## 模板标签、函数与过滤器 37 | 38 | + `pageurl()` 39 | 40 | 生成某个页面实例的URL: 41 | 42 | ```html 43 | 更多信息 44 | ``` 45 | 46 | 请参考 [`pageurl`](https://wagtail.xfoss.com/topics/writing_templates.md#pageurl-tag) 了解更多信息。 47 | 48 | + `slugurl()` 49 | 50 | 生成某个带有别名的页面(a Page with a slug)的URL: 51 | 52 | ```html 53 | 关于我们 54 | ``` 55 | 56 | 请参考 [`slugurl`](https://wagtail.xfoss.com/topics/writing_template.md#slugurl-tag) 了解更多信息。 57 | 58 | + `image()` 59 | 60 | 对某个图片进行缩放,并输出一个 `` 标签: 61 | 62 | ```html 63 | {# 输出一个图片标签 #} 64 | {{ image(page.header_image), "fill-1024x200", class="header-image" }} 65 | 66 | {# 缩放某个图片 #} 67 | {% set background=image(page.background_image, "max-1024x1024") %} 68 |
69 | ``` 70 | 71 | 请参阅 [在模板中使用图片](https://wagtail.xfoss.com/topics/images.md#image-tag) 了解更多信息。 72 | 73 | + `|richtext` 74 | 75 | 对 Wagtail 的内部HTML表示进行转换,将一些内部引用扩展为页面与图片。 76 | 77 | ```html 78 | {{ page.body|richtext }} 79 | ``` 80 | 81 | 请参阅 [富文本(过滤器)](https://wagtail.xfoss.com/writing_templates.md#rich-text-filter) 以了解更多信息。 82 | 83 | + `wagtailuserbar()` 84 | 85 | 输出前端上用于对页面进行编辑的 Wagtail 上下文弹出式菜单 86 | 87 | ```html 88 | {{ wagtailuserbar() }} 89 | ``` 90 | 91 | 请参考 [Wagtail的用户栏](https://wagtail.xfoss.com/writing_templates.md#wagtailuserbar-tag) 以获取更多信息。 92 | 93 | {% raw %} 94 | 95 | {% include_block %} 96 | {% endraw %} 97 | 98 | 将流式内容(the stream content)作为一个整体,输出他的HTML表示,对于各个单独块也是作为整体输出为HTML表示的。 99 | 100 | 允许将模板上下文传递给`StreamField`的模板。 101 | 102 | ```html 103 | {% include_block page.body %} 104 | {% include_block page.body with context %} {# 与上面的写法相同 #} 105 | {% include_block page.body without context %} 106 | ``` 107 | 108 | 109 | 请参考 [`StreamField` 模板的渲染](https://wagtail.xfoss.com/topics/streamfield.md#streamfield-template-rendering) 以了解更多信息。 110 | 111 | 112 | > __注意__ `include_block` 标签设计用于严格遵循 Jinja 的 `include` 标签的语法与行为,因此其并没有实现 Django 版本的仅传递指定变量到上下文中的特性。 113 | 114 | -------------------------------------------------------------------------------- /getting_started/the_zen_of_wagtail.md: -------------------------------------------------------------------------------- 1 | # Wagtail之道 2 | 3 | **The Zen of Wagtail** 4 | 5 | Wagtail诞生于多年的网站开发经验中,诞生于哪些方法好用、哪些方法不好用的试错中,试图在功能的强大与程序的简洁、结构化与灵活性之间找到平衡(Wagtail has been born out of many years of experience building websites, learning approches that work and ones that don't, and striking a balance between power and simplicity, structure and flexibility)。创造Wagtail的人们,希望大家能发现,Wagtail正是处于这一最佳点位。但是,他作为一套软件,Wagtail也只能做到这里了 -- 此时正是大家可以创建一个优美而又有趣的网站的时候。正由于此,尽管大家都在磨拳擦掌,跃跃欲试,还是要花点时间来了解一下Wagtail是何种设计原则来建立的。 6 | 7 | 本着“[Python之道](https://www.python.org/dev/peps/pep-0020/)”精神,Wagtail哲学也是一套既包含了使用Wagtail构建网站的,也包含了持续进行的Wagtail自身开发的指导原则。 8 | 9 | ## Wagtail并非一个开箱即用的网站 10 | 11 | 是不能通过将一些现成模块拼在一起就得到一个漂亮的站点的 -- 需要写代码。 12 | 13 | ## 要一直清楚自己所扮演的角色 14 | 15 | **Always wear the right hat** 16 | 17 | 有效地使用Wagtail的关键,是要认识到在创建一个网站时,涉及到多个角色:内容作者、站点管理员、开发者与设计师。当然他们可能是不同的人,但也不必非得要是不同的人 -- 比如在使用Wagtail构建自己的个人博客是,就能发现你自己在这写不同角色之间变换。但无论哪种方式,都要注意在某时某刻你正在做什么,并要使用适当的功能来做那件事。 18 | 19 | 内容作者或站点管理员,会通过Wagtail的管理界面,来完成他们的工作;开发者或设计师则要花大量时间来编写Python、HTML或CSS代码。这实际上是个好事:Wagtail设计初衷不是要取代编程工作。或许未来的某一天,有人搞出来一种通过托放操作,就能建立出与经由代码编写而建立的同样强大的网站,但Wagtail绝不是那样的工具,Wagtail也不会尝试成为那样的工具。 20 | 21 | 一种常见的错误做法,就是将过多的权力与责任给予到内容作者与站点管理员 -- 实际上,当他们是你的客户时,这往往就是他们会吼你的原因。站点成功有赖于你敢于对此说不。内容管理的真正威力,不是来自将控制权交给内容管理系统的用户,而是来自设定好不同角色之间的边界。抛开其他的不说,这意味着不要让网站编辑在内容编辑界面去搞设计和布局,不要让站点管理员去构建那些可以通过代码来实现的复杂的交互流程。 22 | 23 | ## 内容管理系统应尽可能有效地、直接地让网站编辑头脑中的想法顺利表达出来,并存入到数据库 24 | 25 | ** A CMS should get information out of an editor's head and into a database, as efficiently and directly as possible** 26 | 27 | 不论网站是关于汽车、猫咪、甜点或房屋中介,内容作者都会带着他们想要发布在网站上的那些特定领域的信息,来到Wagtail的管理界面。因此作为网站建设者的目标,就是要将这些信息以他们原始的格式 -- 而不是依照某位特定作者认为信息应该怎样的想法,提炼并存储起来。 28 | 29 | 将设计方面的关注点,从页面内容分离出来,有着诸多好处。这样做确保设计在整个站点范围保持一致,而不会随着网站编辑们的朝令夕改而改变。这样做还可以实现对页面中的有用信息的充分利用 -- 比如在页面是关于活动组织时,那么就会有一个专门的“活动”页面,该页面带有活动日期与地点的数据字段,这些字段就允许以日历试图或过滤清单的方式来展示这些活动,这在仅通过在通用页面应用不同样式的标题,是做不到的。最后,如果在未来某个时期对站点进行了重新设计,或是将站点整个迁往了不同平台,这样做还可以确保站点内容将以其新的设置工作,而无需进行某种特定方式再进行格式化。 30 | 31 | 现在假设有某位内容作者过来提交一个请求:“我们要将这个文本设置为亮粉色的 Comic Sans字体”。你给他们的疑问可能是:“为什么啊?这点文本有什么特殊的地方吗” 如果他们的答复是“我只是喜欢那样子”,那么就必须委婉的劝导他们,设计方面的决定由不得他们(抱歉)。但如果他们的答复是“那个文本输入小朋友板块的”,此答复就给予了一种将编辑与设计的不同关注点分开的方法:要赋予网站编辑将某些页面指定为“小朋友板块”的能力(经由标签特性、不同页面模型,或者站点层次结构等方式),同时让设计师可以根据这种制定,来决定怎样应用各种样式。 32 | 33 | ## 程序员的最佳用户界面,往往就是某种编程语言 34 | 35 | **The best user interface for a programmer is usually a programming language** 36 | 37 | 通常见到的内容管理系统,在定义构成页面的数据模型时,总是采用点击式界面(a point-and-click interface)。 38 | 39 | ![Druple内容管理系统后台的内容类型界面](images/drupal_content_type.png) 40 | 41 | 在市场营销上这样做看起来很不错,但在现实中,绝不会有CMS的终端用户,能够切实地造成那种根本改变 -- 在某个线上站点,不会少 -- 除非他们有着程序员的那种对站点构建方式的洞察,以及知悉修改将带来何种影响(it looks nice in the sales pitch, but in reality, no CMS end-user can realistically make that kind of fundamental change -- on a live site, no less -- unless they have a programmer's insight into how the site is built, and what impact the change will have)。正是因为这样,那些涉及点击界面的事情,将总是需要程序员来完成 -- 所完成的所有工作,就是从舒适的代码编写世界,造出这些点击界面即可。在编写代码的世界里,有着整个的工具链生态,从文本编辑器到版本控制系统,可以帮助程序员进行代码修正的开发、测试与部署。 42 | 43 | Wagtail发现,在没有充分理由的情况下,绝大部分编程任务,都需要通过编写代码才能最好的完成,而不是尝试将这些编程任务转化为那种装箱式的练习(Wagtail recognises that most programming tasks are best done by writing code, and does not try to turn them into box-filling exercises when there's no good reason to)。同样,在给站点构建功能时,应始终记住某些特性注定要由程序员,而非内容编辑来维护,还要考虑将这些特性做成经由管理界面可配置的做法,到底会带来更多的便利,还是更多的障碍。比如Wagtail就提供了一个用于允许内容编辑创建通用目的数据收集表单的表单构建器。你或许打算将其用于一些与(比如)客户关系管理系统或支付处理器等进行集成的更为复杂的表单的基础 -- 而在此情况下,除了对后端逻辑进行重写之外,就没有办法对表单字段进行编辑,因此通过Wagtail来将他们做成可编辑,是没什么价值的。对于这类需求,使用Django的表单框架来构建更好,因为Django的表单框架中的表单字段,完全是用代码进行定义的。 44 | -------------------------------------------------------------------------------- /advanced_topics/privacy.md: -------------------------------------------------------------------------------- 1 | # 私有页面 2 | 3 | 有着某个页面发布权限的用户,可通过点击页面浏览器或编辑界面右上角的“私密(Privacy)”控件,将该页面设置为私有页面。这将设置一个仅允许一部分人查看该页面及其子页面的限制。有下面几种类型的限制: 4 | 5 | + __限制为仅登入用户可访问__:要查看该页面,用户需要登入。所有用户都可访问该页面,而与权限级别无关。 6 | 7 | + __限制为输入口令才可访问__:用户必须输入给定的口令,才能查看到该页面。这种方法适用于在打算将某个页面分享给信任的一组人,但又不适合给他们分别创建一个账户的情况。这时所有查看该页面的用户使用的是同一个口令,同时这种做法独立于站点上既有的全部用户账号。 8 | 9 | + __限制为仅对特定分组的用户可访问__:用户必须登入,且时一个或多个指定用户分组中的成员,才能查看到该页面。 10 | 11 | 12 | 与页面类似,文档也可通过将其放入到带有适当隐私设置的集合中,而令其成为私有的(参见:[图片/文档的权限](#image-document-permissions))。 13 | 14 | 在Wagtail中,私有页面与文档特性时开箱即用的 -- 站点实现者无需任何设置就可以建立起来。但默认的“登入”与“需要口令”表单,仅是一些基本的HTML页面,站点实现者可能希望将他们替换为适应站点设计的定制页面。 15 | 16 | ## 设置一个登录页面 17 | 18 | 通过将`WAGTAIL_FRONTEND_LOGIN_TEMPLATE`设置到希望使用的某个模板的路径,就可对Wagtail自带的基本登录页面进行定制: 19 | 20 | ```python 21 | # settings.py 22 | 23 | WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html' 24 | ``` 25 | 26 | 这里Wagtail使用了Django标准的`django.contrib.auth.views.LoginView`视图,因此在该模板上的上下文变量,在 [Django的登录视图文档](https://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.views.LoginView)中有详细说明。 27 | 28 | 在该原装的Django登录视图并不适合 -- 比如要使用某种外部认证系统,或将Wagtail集成到某个已有一个工作的登录视图的现有Django站点时 -- 便可通过`WAGTAIL_FRONTEND_LOGIN_URL`设置,指定登录视图的URL: 29 | 30 | ```python 31 | # settings.py 32 | 33 | WAGTAIL_FRONTEND_LOGIN_URL = '/account/login/' 34 | ``` 35 | 36 | 要将Wagtail集成到已有某种登录机制的Django站点,通常进行`WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL`设置,就足够了。 37 | 38 | ## 设置一个全局的“需要口令”页面 39 | 40 | 通过在Django设置文件中设置`PASSWORD_REQUIRED_TEMPLATE`,可指定某个将用于站点上所有“需要口令”表单的模板路径(除开那些特殊指定了覆写该项设置的页面类型,详情参见后面,by setting `PASSWORD_REQUIRED_TEMPLATE` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site(except for page types that specifically override it - see below)): 41 | 42 | ```python 43 | # settings.py 44 | 45 | PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html' 46 | ``` 47 | 48 | 此模板将接收到与屏蔽页面通过`get_context()`方法将传递给其自己的模板相同上下文变量集 -- 包括用于对该页面对象自身进行引用的`page` -- 以及以下的额外变量(这些变量将覆盖该页面本身的同名上下文变量): 49 | 50 | + __`form`__ -- 一个口令提示框的Django表单对象;该变量将包含一个名为`password`的字段,作为其可见字段。也可能有着一些隐藏的字段,因此在没有使用诸如`form.as_p`这样的Django渲染帮助器之一时,该页面就必须对`form.hidden_fields`进行遍历。 51 | 52 | + __`action_url`__ -- 该口令表单作为一个`POST`请求,将提交到的URL。 53 | 54 | 55 | 一个适合作为`PASSWORD_REQUIRED_TEMPLATE`进行使用的基本模板,将像下面这样: 56 | 57 | ```html 58 | 59 | 60 | 61 | 62 | 需要口令 63 | 64 | 65 | 66 |

需要口令

67 |

需要口令来访问此页面。

68 |
69 | {% csrf_token %} 70 | {{ form.non_field_errors }} 71 | 72 |
73 | {{ form.password.errors }} 74 | {{ form.password.label_tag }} 75 | {{ form.password }} 76 |
77 | 78 | {% for field in form.hidden_fields %} 79 | {{ field }} 80 | {% endfor %} 81 | 82 | 83 |
84 | 85 | 86 | ``` 87 | 88 | 文档上的口令限制,使用另外的模板,经由`DOCUMENT_PASSWORD_REQUIRED_TEMPLATE`设置项加以指定;该模板也将接收到上面讲到的`form`与`action_url`上下文变量。 89 | 90 | ## 设定特定页面类型的“需要口令”页面 91 | 92 | 可在页面模型上定义`password_required_template`属性,以使用仅在那个页面类型下、“需要口令”视图的定制模板。比如,某个站点有着一个用于与描述文字一道,显示嵌入视频的页面类型,该页面类型可能要使用定制的、通常需要展示视频描述文字,但又是在原本放置嵌入视频的地方,展示口令表单的“需要口令”模板: 93 | 94 | ```python 95 | class VideoPage(Page): 96 | ... 97 | 98 | password_required_template = 'video/password_required.html' 99 | ``` 100 | -------------------------------------------------------------------------------- /advanced_topics/images/image_serve_view.md: -------------------------------------------------------------------------------- 1 | # 动态的图片提供视图 2 | 3 | __Dynamic image serve view__ 4 | 5 | 在多数场合,想要在Python中生成图片副本的开发者,都应使用`get_rendition()`方法,请参考[以Python方式生成图片转写](renditions.md)。 6 | 7 | 在需要为某种诸如博客或移动应用这样的 _外部_ 系统生成图片的一些版本时,Wagtail提供了一种通过调用独特的URL,来动态生成图片副本的视图。 8 | 9 | 此种视图获取到URL中的图片id、过滤器规范与安全性签名。在这些参数为有效的情况下,将提供一个符合条件的图片文件。 10 | 11 | 与`{% image %}`标签一样,该图片副本是在第一次调用时生成的,后续调用将从缓存中予以提供。 12 | 13 | ## 设置 14 | 15 | 将该视图的一个条目,添加到站点URLs配置中: 16 | 17 | ```python 18 | # app/urls.py 19 | 20 | from wagtail.images.views.serve import ServeView 21 | 22 | urlpatterns = [ 23 | ... 24 | 25 | url(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(), name='wagtailimages_serve'), 26 | 27 | ... 28 | 29 | # 确保该 wagtailimages_serve 行,出现在默认Wagtail页面服务路由之上 30 | # Ensure that the wagtailimages_serve line appears above the default Wagtail page serving route 31 | 32 | url(r'', include(wagtail_urls)), 33 | ] 34 | ``` 35 | 36 | ## 用法 37 | 38 | ### 图片URL生成器的UI 39 | 40 | 在开启了动态提供图片特性之后,在管理界面中的图片URL生成器将自动变为可用。可经由任意图片的编辑页面,通过点击右侧的“URL生成器”按钮,访问到图片URL生成器。 41 | 42 | 该界面令到站点编辑可以生成到图片裁剪版本的URLs。 43 | 44 | ### 以Python方式,生成动态的图片URLs 45 | 46 | 也可使用Python代码来生成动态的图片URLs,并经由API加以提供,或直接在模板中进行使用。 47 | 48 | 在模板中使用动态的图片URLs的一个优势,就是在渲染时不会像`{% image %}`标签那样阻塞到初次响应。 49 | 50 | `wagtail.images.views.serve`中的`generate_image_url`函数,是生成动态的图片URL的一种方便的方法。 51 | 52 | 下面是在某个视图中该函数的使用示例: 53 | 54 | ```python 55 | def display_image(request, image_id): 56 | image = get_object_or_404(Image, id=image_id): 57 | 58 | return render(request, 'display_image.html', { 59 | 'image_url': generated_image_url(image, 'fill-100x100') 60 | }) 61 | ``` 62 | 63 | 对图片的操作,可通过将这些操作以`|`字符连接起来的方式,链接起来: 64 | 65 | ```python 66 | return render(request, 'display_image.html', { 67 | 'image_url': generate_image_url(image, 'fill-100x100|jpegquality-40') 68 | }) 69 | ``` 70 | 71 | 在模板中: 72 | 73 | {% raw %} 74 | 75 | {% load wagtailimages_tags %} 76 | ... 77 | 78 | 79 | {% image_url page.photo "width-400" %} 80 | 81 | 82 | {% image_url page.photo "fill-100x100|jpegquality-40" %} 83 | 84 | 85 | {% image_url page.photo "width-400" "mycustomview_serve" %} 86 | {% endraw %} 87 | 88 | 可传递一个将用于经由其提供图片的可选视图名称。默认的视图名称为`wagtailimages_serve`。 89 | 90 | ## 高级配置 91 | 92 | ### 令到视图重定向而非直接提供 93 | 94 | __Making the view redirect instead of serve__ 95 | 96 | 默认下该视图将直接提供图片文件。此行为可修改为一个`301`重定向,在图片留存于外部主机时,这样做会有用处。 97 | 98 | 要开启重定向,就要在urls配置中,将`action='redirect'`传递给`ServeView.as_view()`方法: 99 | 100 | ```python 101 | from wagtail.images.views.serve import ServeView 102 | 103 | urlpatterns = [ 104 | ... 105 | 106 | url(r'^images/([^/*])/(\d*)/[^/*]/[^/]*$', ServeView.as_view(action='redirect'), name='wagtailimages_serve'), 107 | ] 108 | ``` 109 | 110 | ### 与`django-sendfile`的集成 111 | 112 | [`django-sendfile`](https://github.com/johnsensible/django-sendfile) 可将图片数据传输工作交给web服务器,而不是直接由Django应用直接提供图片数据。在站点有着很多同时被下载的图片,却又无法使用[带有缓存的代理服务器](performance.md#caching-proxy)或CDN的情况下,这样做可极大地减低服务器负载。 113 | 114 | 首先要安装和配置好`django-sendfile`,并将web服务器配置好使用`django-sendfile`。如尚未准备好这些,请参考该[安装文档](https://github.com/johnsensible/django-sendfile#django-sendfile)。 115 | 116 | 可使用`SenfFileView`类,来以`django-sendfile`方式进行图片的提供。此视图可开箱即用: 117 | 118 | ```python 119 | from wagtail.images.views.serve import SendFileView 120 | 121 | urlpatterns = [ 122 | ... 123 | 124 | url(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', SendFileView.as_view(), name='wagtailimages_serve'), 125 | ] 126 | ``` 127 | 128 | 可对其加以定制,以对`SENDFILE_BACKEND`中定义的后端进行覆写: 129 | 130 | ```python 131 | from wagtail.images.views.serve import SendFileView 132 | from project.sendfile_backends import MyCustomBackend 133 | 134 | class MySendFileView(SendFileView): 135 | backend = MyCustomBackend 136 | ``` 137 | 138 | 还可将其定制为发送私有文件。比如在要求仅为要求认证(例如对于Django >= 1.9): 139 | 140 | ```python 141 | from django.contrib.auth.mixins import LoginRequiredMixin 142 | from wagtail.images.views.serve import SendFileView 143 | 144 | class PrivateSendFileView(LoginRequiredMixin, SendFileView): 145 | raise_exception = True 146 | ``` 147 | -------------------------------------------------------------------------------- /advanced_topics/i18n/duplicate_tree.md: -------------------------------------------------------------------------------- 1 | # 创建多语言站点(通过复制页面树的方式) 2 | 3 | 此教程将给出一种Wagtail中通过复制页面树的方式,创建多语言站点的方法。 4 | 5 | 比如: 6 | 7 | ```sh 8 | / 9 | en/ 10 | about/ 11 | contact/ 12 | fr/ 13 | about/ 14 | contact/ 15 | ``` 16 | 17 | 18 | ## 根页面 19 | 20 | 根页面(`/`)需要去侦测浏览器语言并将不同语言的浏览器请求,转发到正确语言的主页(`/en/`、`/fr/`)。根页面应位处站点根部(那里也是主页通常的所在)。 21 | 22 | 这里就必须设置 Django 的`LANGUAGES` 设置项,从而不会将那些非英语或法语的用户,重定向到不存在的页面。 23 | 24 | ```python 25 | # settings.py 26 | 27 | LANGUAGES = ( 28 | ('en', _("English")), 29 | ('fr', _("French")), 30 | ) 31 | ``` 32 | 33 | ```python 34 | # models.py 35 | 36 | from django.utils import translation 37 | from django.http import HttpResponseRedirect 38 | 39 | from wagtail.core.models import Page 40 | 41 | class LanguageRedirectionPage(Page): 42 | 43 | def serve(self, request): 44 | # 此方法仅会返回一个在 Django 的 LANGUAGE 设置项中的某个语言 45 | language = translation.get_language_from_request(request) 46 | 47 | return HttpResponseRedirect(self.url + language + '/') 48 | ``` 49 | 50 | 51 | ## 将全部页面链接起来 52 | 53 | 将同一页面的不同语言版本链接在一起,从而允许访问者可以轻松地在语言之间进行切换,将是有用的。但又不打算过多地增加站点编辑的负担,理想情况下,站点编辑只需将这些相同页面的一个版本,与其他版本链接起来,那么其他版本之间的链接将隐式地创建出来。 54 | 55 | 因为此做法需要添加到所有翻译的页面类型,所以最好将此做法放到混入中(a mixin)。 56 | 57 | 下面是一个如何实现此做法的示例(英语作为主语言,法语/西班牙语作为替代语言): 58 | 59 | ```python 60 | from wagtail.core.models import Page 61 | from wagtail.admin.edit_handlers import MultiFieldPanel, PageChooserPanel 62 | 63 | class TranslatablePageMixin(models.Model): 64 | # 每种替代语言对应一个链接 65 | # 这些属性与方法,应只应用在主语言的页面上(也就是英语) 66 | 67 | french_link = models.ForeignKey(Page, null=True, on_delete=models.SET_NULL, blank=True, related_name='+') 68 | spanish_link = models.ForeignKey(Page, null=True, on_delete=models.SET_NULL, blank=True, related_name='+') 69 | 70 | panels = [ 71 | PageChooserPanel('french_link'), 72 | PageChooserPanel('spanish_link'), 73 | ] 74 | 75 | def get_language(self): 76 | """ 77 | 此方法返回的时该页面的语言代码。 78 | """ 79 | 80 | # 通过对本页面的祖先进行查找,以找到他的语言代码 81 | # 语言主页位于深度 3 处(The language homepage is located at depth 3) 82 | 83 | language_homepage = self.get_ancestors(inclusive=True).get(depth=3) 84 | 85 | # 语言主页的别名,应总是被设置为语言代码 86 | return language_homepage.slug 87 | 88 | # 用于找出本页面主语言的方法 89 | # 这是通过对上面的链接进行反向追溯完成的(this works by reversing the above links) 90 | 91 | def english_page(self): 92 | """ 93 | 该方法找出本页面的英语版本 94 | """ 95 | language = self.get_language() 96 | 97 | if language == 'en': 98 | return self 99 | elif language == 'fr': 100 | return type(self).objects.filter(french_link=self).first().specific 101 | elif language == 'es': 102 | return type(self).objects.filter(spanish_link=self).first().specific 103 | 104 | 105 | # 这里需要某种找出本页面的每种替代语言的一个版本的方法。 106 | # 这些方法工作原理一样。他们首先找出页面的主要语言版本(也就是英语)。 107 | # 然后从主要语言版本开始,就只需跟随链接,就能获得相应语言的正确版本了。 108 | 109 | def french_page(self): 110 | """ 111 | 此方法找出该页面的法语版本 112 | """ 113 | 114 | english_page = self.english_page() 115 | 116 | if english_page and english_page.french_link: 117 | return english_page.french_link.specific 118 | 119 | def spanish_page(self): 120 | """ 121 | 此方法找出该页面的西班牙版本 122 | """ 123 | 124 | english_page = self.english_page() 125 | 126 | if english_page and english_page.spanish_link: 127 | return english_page.spanish_link.specific 128 | 129 | class Meta: 130 | abstract = True 131 | 132 | class AboutPage(Page, TranlatablePageMixin): 133 | ... 134 | 135 | content_panels = [ 136 | ... 137 | 138 | MutliFieldPanel(TranslatablePageMixin.panels, '语言链接') 139 | ] 140 | 141 | class ContactPage(Page, TranslatablePageMixin): 142 | ... 143 | 144 | content_panels = [ 145 | ... 146 | 147 | MultiFieldPanel(TranslatablePageMixin.panels, '语言链接') 148 | ] 149 | ``` 150 | 151 | 随后在模板中就可以向下面这样,利用上这些方法了: 152 | 153 | {% raw %} 154 | 155 | {% if page.english_page and page.get_language != 'en' %} 156 | {% trans "View in English" %} 157 | {% endif %} 158 | 159 | {% if page.french_page and page.get_language != 'fr' %} 160 | {% trans "View in English" %} 161 | {% endif %} 162 | 163 | {% if page.spanish_page and page.get_language != 'es' %} 164 | {% trans "View in English" %} 165 | {% endif %} 166 | {% endraw %} 167 | -------------------------------------------------------------------------------- /topics/search/backends.md: -------------------------------------------------------------------------------- 1 | # 关于搜索后端 2 | 3 | 4 | Wagtail有着对多种后端的支持,从而提供了使用数据库的搜索,或使用诸如Elasticsearch这样的外部服务进行搜索的选择。默认启用的是数据库后端。 5 | 6 | 可使用`WAGTAILSEARCH_BACKENDS`设置,来配置要使用的后端: 7 | 8 | ```python 9 | WAGTAILSEARCH_BACKENDS = { 10 | 'default': { 11 | 'BACKEND': 'wagtail.search.backends.db', 12 | } 13 | } 14 | ``` 15 | 16 | 17 | ## `AUTO_UPDATE` 18 | 19 | 默认Wagtail将自动保持所有索引处于更新状态。此默认行为在编辑内容时,将对性能造成影响,尤其是在索引驻留在外部服务上时。 20 | 21 | `AUTO_UPDATE`设置,允许在单个索引基础上,关闭默认行为: 22 | 23 | ```python 24 | WAGTAILSEARCH_BACKENDS = { 25 | 'default': { 26 | 'BACKEND': ..., 27 | 'AUTO_UPDATE': False, 28 | } 29 | } 30 | ``` 31 | 32 | 如关闭了自动更新,那么就必须定期运行`update_index`命令,以保持索引与数据库同步。 33 | 34 | ## `ATOMIC_REBUILD` 35 | 36 | > **警告** 此选项在Elasticsearch 5.4及更高版本上不会工作,是因为[别名处理中的一个bug](https://github.com/elastic/elasticsearch/issues/24644) 影响到这些版本。 37 | 38 | 默认(使用Elasticsearch后端时)在`update_index`运行时,Wagtail会删除索引并从头开始重建出来。这就会导致在重建完成之前搜索引擎不会返回结果,同时这也有着发生了错误时无法回滚的风险。 39 | 40 | 将`ATOMIC_REBUILD`设置项目,设置为`True`,就会令到Wagtail重建为一个独立的索引,而在新的索引完全建立之前,保持原有的索引处于活动状态。在重建完成之后,新旧索引进行原子交换,且旧的索引被删除。 41 | 42 | ## `BACKEND` 43 | 44 | 下面是Wagtail原生支持的后端清单。 45 | 46 | ### 数据库后端(默认支持 47 | 48 | `wagtail.search.backends.db` 49 | 50 | 数据库后端是甚为基础的,而仅是打算供开发与小型站点所用。其无法依相关度对结果进行排序,从而严重妨碍了其在对大量页面集进行搜索时的可用性。 51 | 52 | 数据库后端不具备对以下特性的支持: 53 | 54 | + 在`Page`基类的子类中字段的搜索(除非该类是直接进行搜索的) 55 | + [对可调用属性与其他属性的索引](#indexing-callable-fields) 56 | + 将重音字符转换成ASCII字符(注:accendted characters) 57 | 58 | 在上述任何一个特性都是重要的情况下,就要使用Elasticsearch了。 59 | 60 | ### PostgreSQL 的后端 61 | 62 | `wagtail.contrib.postgres_search.backend` 63 | 64 | 在使用PostgreSQL作为数据库,且站点页面数少于一百万时,就可能打算使用此后端了。 65 | 66 | 请参阅[PostgreSQL的搜索引擎](reference/contrib.html#postgres-search)以了解更多知识。 67 | 68 | 69 | ### Elasticsearch 后端 70 | 71 | *Wagtail 2.1 的改动: 加入了对 Elasticsearch 6.x 的支持* 72 | 73 | Wagtail支持 Elasticsearch 的版本2、5与6。请使用对应版本的后端: 74 | 75 | `wagtail.search.backends.elasticsearch2` (Elasticsearch 2.x) 76 | 77 | `wagtail.search.backends.elasticsearch5` (Elasticsearch 5.x) 78 | 79 | `wagtail.search.backends.elasticsearch6` (Elasticsearch 6.x) 80 | 81 | 使用此后端的前提,是先要有[Elasticsearch](https://www.elastic.co/downloads/elasticsearch)服务本身,以及通过`pip`安装上[elasticsearch-py](http://elasticsearch-py.readthedocs.org/)这个包。该包的大版本号要与所安装的Elasticsearch的版本匹配: 82 | 83 | ```sh 84 | $ pip install "elasticsearch>=2.0.0,<3.0.0" # 对于Elasticsearch 2.x 85 | 86 | $ pip install "elasticsearch>=5.0.0,<6.0.0" # 对于Elasticsearch 5.x 87 | 88 | $ pip install "elasticsearch>=6.0.0,<6.3.1" # 对于Elasticsearch 6.x 89 | ``` 90 | 91 | > **注意** 版本 `6.3.1` 的 Elasticsearch客户端库与Wagtail不兼容。请使用 `6.3.0`或更早版本。 92 | 93 | 94 | 后端实在设置中配置的: 95 | 96 | ```python 97 | WAGTAILSEARCH_BACKENDS = { 98 | 'default': { 99 | 'BACKEND': 'wagtail.search.backends.elasticsearch2', 100 | 'URLS': ['http://localhost:9200'], 101 | 'INDEX': 'wagtail', 102 | 'TIMEOUT': 5, 103 | 'OPTIONS': {}, 104 | 'INDEX_SETTINGS': {}, 105 | } 106 | } 107 | ``` 108 | 109 | 与`BACKEND`不同,其他键是可选的,且默认为上面的那些值。在`OPTIONS`中定义的所有键,都被直接作为区分大小写的关键字参数(比如`'max_retries': 1`),传递给 Elasticsearch的构造器。 110 | 111 | `INDEX_SETTINGS`则是一个用于对默认创建索引方式设置进行覆写的字典。该创建索引方式设置项,定义在模块`wagtail/wagtail/wagtailsearch/backends/elasticsearch.py`模块里`ElasticsearchSearchBacken`类的内容。将加入所有的新键,对于既有键,如其不是一个字典,那么都将以新的值进行替换。下面是一个如何配置分片数,以及将意大利语的语言分析器作为默认分析器的示例: 112 | 113 | ```python 114 | WAGTAILSEARCH_BACKENDS = { 115 | 'default': { 116 | ..., 117 | 'INDEX_SETTINGS': { 118 | 'settings': { 119 | 'index': { 120 | 'number_of_shards': 1, 121 | }, 122 | 'analysis': { 123 | 'analyzer': { 124 | 'default': { 125 | 'type': 'italian' 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | 若不在开发或生产环境选择运行一个Elasticsearch服务器,那么有着多个可用的第三方主机服务,包括[Bonsai](https://bonsai.io/signup),该站点提供了一个适合与测试与开发的免费帐号。要使用Bonsai: 136 | 137 | + 在Bonsai注册一个帐号 138 | + 使用Bonsai的仪表盘创建一个集群 139 | + 使用Bonsai仪表盘中的该集群URL,配置`WAGTAILSEARCH_BACKENDS`中的`URLS`条目 140 | + 运行`./manage.py update_index`命令 141 | 142 | 143 | ### Amazon AWS 的Elasticsearch 144 | 145 | Wagtail的Elasticsearch后端,是与[Amazon 的Elasticsearch服务](https://aws.amazon.com/elasticsearch-service/)兼容的,但需要额外配置,以处理基于IMA的认证。这可通过[requests-aws4auth](https://pypi.python.org/pypi/requests-aws4auth) `pip` 包,与以下的配置来完成: 146 | 147 | ```python 148 | from elasticsearch import RequestsHttpConnection 149 | from requests_aws4auth import AWS4AUTH 150 | 151 | WAGTAILSEARCH_BACKENDS = { 152 | 'default': { 153 | 'BACKEND': 'wagtail.search.backends.elasticsearch2', 154 | 'INDEX': 'wagtail', 155 | 'TIMEOUT': 5, 156 | 'HOSTS': [{ 157 | 'host': 'YOURCLUSTER.REGION.es.amazonaws.com', 158 | 'port': 443, 159 | 'use_ssl': True, 160 | 'verify_certs': True, 161 | 'http_auth': AWS4AUTH('ACCESS_KEY', 'SECRET_KEY', 'REGION', 'es'), 162 | }], 163 | 'OPTIONS': { 164 | 'connection_class': RequestsHttpConnection, 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | ### 构造自己的后端 171 | 172 | **Rolling Your Own** 173 | 174 | Wagtail搜索后端实现了在`wagtail/wagtail/wagtailsearch/backends/base.py`中的接口。在最低限度下,后端的`search()`方法必须返回一个对象集合或`model.objects.none()`。而对于一个具有完整特性的搜索后端,请在`elasticsearch.py`中查看Elasticsearch的后端代码。 175 | -------------------------------------------------------------------------------- /advanced_topics/customisation/admin_templates.md: -------------------------------------------------------------------------------- 1 | # 对管理模板进行定制 2 | 3 | 在Wagtail项目中,可能希望在管理界面中,使用自己的品牌元素,来替换默认的Wagtail徽标等元素。可经由Django的继承机制,实现此目标。 4 | 5 | 需要在某个应用目录下,创建一个`templates/wagtailadmin/`的文件夹 -- 这既可以是一个既有的文件夹,也可以是为此目的而新建的文件夹,比如 `dashboard`。该应用必须是已在 `INSTALLED_APPS` 中,于`wagtail.admin`之前已注册的应用: 6 | 7 | ```python 8 | INSTALLED_APPS = ( 9 | # ... 10 | 'dashboard', 11 | 12 | 'wagtail.core', 13 | 'wagtail.admin', 14 | 15 | # ... 16 | ) 17 | ``` 18 | 19 | 20 | ## 品牌的定制 21 | 22 | 可用于对管理界面中的品牌加以定制的模板块包含以下这些: 23 | 24 | + `branding_logo` 25 | 26 | 要替换默认的徽标,就要创建一个对默认的`branding_logo`块进行覆写的模板文件 `dashboard/templates/wagtailadmin/base.html`: 27 | 28 | {% raw %} 29 | 30 | {% extends "wagtailadmin/base.html" %} 31 | {% load static %} 32 | 33 | {% block branding_logo %} 34 | 定制项目 35 | {% endblock %} 36 | 37 | {% endraw %} 38 | 39 | 该徽标也出现在管理的 `404` 错误页面上;要在那里对其进行替换,就要创建一个对`branding_logo`块加以覆写的`dashboard/templates/wagtailadmin/404.html`的模板文件。 40 | 41 | + `branding_favicon` 42 | 43 | 要替换在查看管理页面时所显示的站点图标(the favicon),就要创建一个对`branding_favicon`块进行覆写的 `dashboard/templates/wagtailadmin/admin_base.html`的模板文件: 44 | 45 | {% raw %} 46 | 47 | {% extends "wagtailadmin/admin_base.html" %} 48 | {% load static %} 49 | 50 | {% block branding_favicon %} 51 | 52 | {% endblock %} 53 | {% endraw %} 54 | 55 | 56 | + `branding_login` 57 | 58 | 要替换登录消息,就要创建一个用于对`branding_login`块进行覆写的 `dashboard/templates/wagtailadmin/login.html`的模板文件: 59 | 60 | {% raw %} 61 | 62 | {% extends "wagtailadmin/login.html" %} 63 | {% block branding_login %}登入科服斯网站{% endblock %} 64 | {% endraw %} 65 | 66 | 67 | + `branding_welcome` 68 | 69 | 要替换站点仪表盘上的欢迎消息,就要创建一个对 `branding_welcome`块进行覆写的模板文件 `dashboard/templates/wagtailadmin/home.html`: 70 | 71 | {% raw %} 72 | 73 | {% extends "wagtailadmin/home.html" %} 74 | 75 | {% block branding_welcome %} 76 | 欢迎来到科服斯管理后台! 77 | {% endblock %} 78 | {% endraw %} 79 | 80 | ## 在站点品牌中指定一个站点或页面 81 | 82 | 管理界面有着一些对渲染器上下文可用的变量,他们可用于对管理页面中的品牌进行定制。在对多租户的Wagtail安装进行定制时,这些变量将是有用的: 83 | 84 | + `root_page` 85 | 86 | 返回的是对于当前登入用户来说最高的可浏览页面对象。在用户没有浏览权限时,这将默认为 `None`。 87 | 88 | + `root_site` 89 | 90 | 返回上述根页面的站点记录上的名称。 91 | 92 | + `site_name` 93 | 94 | 在计算出 `root_site` 的值不是 `None` 时,返回其值。在`root_site`的值为`None`时,将返回 `settings.WAGTAIL_SITE_NAME`的值。 95 | 96 | 要使用这些变量,就如同之前对仪表盘中的那些模板块的覆写一样,要创建一个`dashboard/templates/wagtailadmin/home.html`的模板文件,并像使用其他Django的模板变量一样,对这些变量进行使用: 97 | 98 | {% raw %} 99 | 100 | {% extends "wagtailadmin/home.html" %} 101 | {% block branding_welcome %} 102 | 欢迎来到 {{ root_site }} 的管理首页 103 | {% endblock %} 104 | {% endraw %} 105 | 106 | 107 | ## 对登录表单进行扩展 108 | 109 | 要将额外控件加入到登录表单,就要创建一个模板文件 `dashboard/templates/wagtailadmin/login.html`。 110 | 111 | + `above_login` 与 `below_login` 112 | 113 | 要在登录表单的上面或下面添加内容,就要对这些块进行覆写: 114 | 115 | {% raw %} 116 | 117 | {% extends "wagtailadmin/login.html" %} 118 | {% block branding_login %}登入 {{ site_name }}{% endblock %} 119 | 120 | {% block above_login %}请使用用户名和口令登入{% endblock %} 121 | 122 | {% block below_login %}若非本站会员,请不要尝试登入。{% endblock %} 123 | {% endraw %} 124 | 125 | 126 | + `fields` 127 | 128 | 要将额外字段添加到登录表单,就要对 `fields` 块进行覆写。将需要在块中的某处加入 `{{ block.super }}`,以将用户名与口令字段包含进来: 129 | 130 | {% raw %} 131 | 132 | {% extends "wagtailadmin/login.html" %} 133 | 134 | {% block fields %} 135 | {{ block.super }} 136 |
  • 137 |
    138 | 两步认证令牌 139 |
    140 | 141 |
    142 |
    143 |
  • 144 | {% endblock %} 145 | {% endraw %} 146 | 147 | 148 | + `submit_buttons` 149 | 150 | 要将额外按钮添加到登录表单,就要对 `submit_buttons` 块进行覆写。将需要在块中某处加入 `{{ block.super }}`,以将登入按钮包含进来: 151 | 152 | {% raw %} 153 | 154 | {% extends "wagtailadmin/login.html" %} 155 | 156 | {% block submit_buttons %} 157 | {{ block.super }} 158 | 159 | 160 | 161 | {% endblock %} 162 | {% endraw %} 163 | 164 | + `login_form` 165 | 166 | 要对登录表单进行完全的定制,就要对 `login_form` 块进行覆写。此块封装了`form`元素的全部内容: 167 | 168 | {% raw %} 169 | 170 | {% extends "wagtailadmin/login.html" %} 171 | 172 | {% block %} 173 |

    一些额外的表单内容

    174 | {{ block.super }} 175 | {% endblock %} 176 | {% endraw %} 177 | 178 | ## 对客户端组件进行扩展 179 | 180 | 一些Wagtail的管理界面,是作为带有 [React](https://reactjs.org/) 的客户端JavaScript代码编写而成的。为了对这些组件进行定制或扩展,就需要用到 React,以及其他一些相关的库。为令到此工作更为容易,Wagtail将其自己与React有关的依赖,作为管理界面中的全局依赖而加以了暴露。下面时这些可用的包: 181 | 182 | ```javascript 183 | // 'focus-trap-react' 184 | window.FocusTrapReact; 185 | // 'react' 186 | window.React; 187 | // `react-dom` 188 | window.ReactDOM; 189 | // 'react-transition-group/CSSTransitionGroup' 190 | window.CSSTransitionGroup; 191 | ``` 192 | 193 | Wagtail还暴露了其自己的一些 React 组件。可重用这些组件: 194 | 195 | ```javascript 196 | window.wagtail.components.Icon; 197 | window.wagtail.components.Portal; 198 | ``` 199 | 200 | 包含了富文本编辑器的页面还可以访问到下面这些组件: 201 | 202 | ```javascript 203 | // `draft.js` 204 | window.DraftJS; 205 | // 'draftail' 206 | window.Draftail; 207 | 208 | // Wagtail 与 Draftail有关的 AIPs 及组件。 209 | window.draftail; 210 | window.draftail.ModalWorkflowSource; 211 | window.draftail.Tooltip; 212 | window.draftail.TooltipEntity; 213 | ``` 214 | -------------------------------------------------------------------------------- /topics/search/indexing.md: -------------------------------------------------------------------------------- 1 | 2 | # 索引的建立 3 | 4 | 要令到模型可视化,就需要将其加入到搜索索引中。所有页面、图片与文档都被索引起来,那么就可以立即开始对他们进行搜索了。 5 | 6 | 在某个页面或图片基类的子类中建立了额外字段时,可能会将这些新的字段,也加入到搜索索引中,从而令到用户的搜索查询,对这些子类的内容进行匹配。请参阅[对额外字段建立索引](#indexing-extra-fields)。 7 | 8 | 在希望将定制模型纳入搜索时,请参阅[建立定制模型的索引](#indexing-custom-models)。 9 | 10 | ## 更新索引 11 | 12 | 在搜索索引与数据库分离的情况下(比如使用Elesticsearch时),就要让二者保持同步状态。有两种方式可以完成此操作:使用搜索信号处理器,或周期性地调用`update_index`命令。处于最佳速度与可靠性的考虑,最好的做法是在可能的情况下二者同时使用。 13 | 14 | ## 关于信号处理器 15 | 16 | `wagtailsearch`库提供了一些绑定到所有已索引模型的保存/删除信号的信号处理器。这将自动在已于`WAGTAILSEARCH_BACKENDS`中注册的所有后端中,添加及删除相应他们。在`wagtail.search`应用装入时,这些信号处理器自动进行了注册。 17 | 18 | **关于`update_index`命令** 19 | 20 | Wagtail还提供了用于从头开始重建索引的一个命令 21 | 22 | ```bash 23 | ./manage.py update_index 24 | ``` 25 | 26 | 推荐每周运行一次此命令,以及在以下时刻运行他: 27 | 28 | + 在经由脚本(比如导入内容后)创建了页面后 29 | + 在对模型或搜索的配置进行了修改之后 30 | 31 | 因为此命令运行时的搜索不会返回结果,因此要避免在峰值时段运行此命令。 32 | 33 | 34 | ## 对额外字段进行索引 35 | 36 | > **警告** 数据库后端并不支持对额外字段的索引。在使用数据库后端是,经由`search_fields`所定义的所有其他字段,都会被忽略。 37 | 38 | 39 | 必要要将字段显式地加入到那些由`Page`基类所派生的模型的`search_fields`字段,以便对这些字段进行搜索/过滤。这是通过对`search_fields`进行覆写,来将一个额外的`SearchField`/`FilterField`字段给他,而完成的。 40 | 41 | __示例__ 42 | 43 | 下面的代码创建了有着两个字段的`EventPage`模型:`description`与`date`。`description`作为了`SearchField`进行索引,而`date`则作为`FilterField`进行索引。 44 | 45 | ```python 46 | from watail.search import index 47 | from django.utils import timezone 48 | 49 | class EventPage(Page): 50 | description = models.TextField() 51 | date = models.DateField() 52 | 53 | search_fields = Page.search_fields + [ # 这里从 Page 基类继承 search_fields 54 | index.SearchField('description'), 55 | index.FilterField('date'), 56 | ] 57 | 58 | # 获取在标题或活动描述中包含 “Christmas” 字符串的未来活动 59 | >>> EventPage.objects.filter(date__gt=timezone.now()).search("Christmas") 60 | ``` 61 | 62 | ## 关于`index.SearchField` 63 | 64 | 指定用于在模型上执行全文搜索的字段,通常是指文本字段。 65 | 66 | **选项** 67 | 68 | + `partial_match(boolean)` -- 将该选项设置为真时,允许结果在部分词上匹配。比如此选项默认对标题就设置为真,那么在某个页面的标题为`Hello World!`时,若用户在搜索框中输入的是`Hel`,则该页面将会被找到。 69 | 70 | + `boost(int/float)` -- 此选项允许将一些字段设置为较其他字段更为重要。将某字段的此选项设置为一个较高的数字,将那些在该字段匹配的页面,在结果共排序靠前。通常页面标题字段的该选项被设置为`2`,所有其他字段的该选项被设置为`1`。 71 | 72 | + `es_extra(dict)` -- 指定该字段允许开发者设置或覆写Elasticsearch映射中该字段上的所有设置。在打算使用某些Wagtail尚不支持的Elasticsearch的特性时,就要用到此选项。 73 | 74 | 75 | ## 关于`index.FilterField` 76 | 77 | 指定要加入到搜索索引的字段,但这些字段不用于全文搜索。而是可以在搜索结果上运行过滤器。 78 | 79 | 80 | ## 关于`index.RelatedFields` 81 | 82 | 此特性允许对相关对象的字段进行索引。其工作于相关字段的所有类型,包含他们的反向访问器(This allows you to index fields from related objects. It works on all types of related fields, including their reverse accessors)。 83 | 84 | 比如这里有一个到书籍作者的`ForeignKey`,就可以将作者模型的`name`与`date_of_birth`字段,嵌入到书籍模型内: 85 | 86 | ```python 87 | from wagtail.search import index 88 | 89 | class Book(models.Model, index.Indexed): 90 | ... 91 | 92 | search_fields = [ 93 | index.SearchField('title'), 94 | index.FilterField('published_date'), 95 | 96 | index.RelatedFields('author', [ 97 | index.SearchField('name'), 98 | index.FilterField('date_of_birth'), 99 | ]), 100 | ] 101 | ``` 102 | 103 | __`index.RelatedFields`上的过滤__ 104 | 105 | 使用`QuerySet`编程接口在`index.RelatedFields`中的所有`index.FilterFields`上进行过滤,都是不可能的。不过这些字段既然有被索引起来,那么就有可能通过手动查询Elasticsearch来用到他们。 106 | 107 | Wagtail计划在未来的发行中,实现经由`QuerySet`在`index.RelatedFields`上的过滤。 108 | 109 | 110 | 111 | ## 对可调用及其他属性的索引 112 | 113 | **Indexing callables and other attributes** 114 | 115 | > **注意** [数据库后端(默认)](#backends-database) 不支持此特性。 116 | 117 | 搜索/过滤器字段无需是Django模型字段。他们还可以是模型类的方法或属性。 118 | 119 | 此特性的一种用处,是对那些Django自动创建的、带有选项的字段的`get_*_display`方法的索引。 120 | 121 | ```python 122 | from wagtail.search import index 123 | 124 | class EventPage(Page): 125 | 126 | IS_PRIVATE_CHOICES = ( 127 | (False, "公开的"), 128 | (False, "私有的"), 129 | ) 130 | 131 | is_private = models.BooleanField(choices=IS_PRIVATE_CHOICES) 132 | 133 | search_fields = Page.search_fields + [ 134 | # 对人类可读的字符串进行索引,以进行搜索 135 | index.SearchField('get_is_private_display'), 136 | 137 | # 对逻辑值进行索引,以进行过滤 138 | index.FilterField('is_private'), 139 | ] 140 | ``` 141 | 142 | 可调用属性还提供到一种对相关模型字段的索引方法(Callables also provide a way to index fields from related models)。在[内联面板与模型集群](reference/pages.html#panels-inline-panels)的示例中,就是通过相关链接的标题,来对各个`BookPage`进行索引的。 143 | 144 | ```python 145 | class BookPage(Page): 146 | 147 | # ... 148 | 149 | def get_related_link_titles(self): 150 | 151 | # 获取到标题清单,并将他们级联起来 152 | return '\n'.join(self.related_links.all().values_list('name', flat=True)) 153 | 154 | search_fields = Page.search_fields + [ 155 | # ... 156 | index.SearchField('get_realted_link_titles'), 157 | ] 158 | ``` 159 | 160 | 161 | ## 对定制模型进行索引 162 | 163 | 所有Django模型,都可以被索引与搜索。 164 | 165 | 要实现这一点,就要从`index.Indexed`进行继承,并将一些`search_fields`加入到该模型。 166 | 167 | ```python 168 | from wagtail.search import index 169 | 170 | class Book(index.Indexed, models.Model): 171 | title = models.CharField(max_length=255) 172 | genre = models.CharField(max_length=255, choices=GENRE_CHOICES) 173 | author = models.ForeignKey(Author, on_delete=models.CASCADE) 174 | published_date = models.DateTimeField() 175 | 176 | search_fields = [ 177 | index.SearchField('title', partial_match=True, boost=10), 178 | index.SearchField('get_genre_display'), 179 | 180 | index.FilterField('genre'), 181 | index.FilterField('author'), 182 | index.FilterField('published_date'), 183 | ] 184 | 185 | # 因为此模型在其QuerySet中并没有一个搜索方法,因此就必须直接在后端调用搜索 186 | >>> from wagtail.search.backends import get_search_backend 187 | >>> s = get_search_backend() 188 | 189 | # 运行一次对 Roald Dohl 所写的书的搜索 190 | >>> roald_dahl = Author.objects.get(name="Roald Dahl") 191 | >>> s.search("chocolate factory", Book.objects.filter(author=roald_dahl)) 192 | [] 193 | ``` 194 | 195 | -------------------------------------------------------------------------------- /advanced_topics/i18n/index.md: -------------------------------------------------------------------------------- 1 | # 国际化问题 2 | 3 | 本文档描述了Wagtail的国际化特性与如何创建多语言站点。 4 | 5 | Wagtail使用了Django的 [国际化框架](https://docs.djangoproject.com/en/stable/topics/i18n/),因此大多数步骤都与其他Django项目相同。 6 | 7 | __目录__ 8 | 9 | + 国际化问题 10 | 11 | - Wagtail管理界面的翻译 12 | - 在单个用户基础上改变Wagtail的管理界面语言 13 | - 改变Wagtail安装的主要语言 14 | + 创建具备多种语言的站点 15 | 16 | - 开启多语言支持 17 | - 根据不同URLs而提供不同的语言 18 | - 对模板进行翻译 19 | - 对内容进行翻译 20 | - 其他方法 21 | 22 | 23 | ## Wagtail管理界面的翻译 24 | 25 | Wagtail的管理后端已被翻译为了多种不同语言。在Wagtail的 [Transifex 页面](https://www.transifex.com/torchbox/wagtail/),可以找到一个当前可用翻译的清单(注意:若使用的时某个较早版本的Wagtail,那么该页面并不能精确反映此安装下有哪些可用的语言)。 26 | 27 | 如在那个页面上没有自己的语言,那么贡献新的语言,或修正其中的错误,也是容易的。在 [Transifex](https://www.transifex.com/torchbox/wagtail/)上注册病提交修改即可。翻译的更新通常会在提交后的一个月内,合并到Wagtail的正式发布中。 28 | 29 | ## 在用户层面修改Wagtail管理界面的语言 30 | 31 | 已登入用户可在`/admin/account`处设置其偏好的语言。默认下Wagtail提供了有着高于90%的翻译覆盖的语言清单。可通过`WAGTAILADMIN_PERMITTED_LANGUAGES`设置,来覆写此行为。 32 | 33 | 在没有语言或只有一种语言得以允许时,该表单将被隐藏。 34 | 35 | 在用户没有选择语言时,将使用`settings.py`中的`LANGUAGE_CODE`设置。 36 | 37 | ## 修改 Wagtail 安装的主要语言 38 | 39 | Wagtail的默认语言为`en-us`(美国英语)。可通过调整一两个Django的设置来修改其默认语言: 40 | 41 | + 确保 [`USE_I18N`](https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n) 被设置为 `True`。 42 | + 将 [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE)设置为网站的主要语言。 43 | 44 | 在有着所设置语言的翻译时,那么现在的Wagtail管理后端就应是所选的语言了。 45 | 46 | ## 创建带有多种语言的站点 47 | 48 | 可资利用 Django 的 [翻译特性](https://docs.djangoproject.com/en/stable/topics/i18n/translation/),来创建带有多种语言支持的站点。 49 | 50 | 本小节的文档,将给出如何结合Wagtail来使用 Django 的翻译特性,还将介绍几种使用Wagtail页面来存储/获取已翻译内容的方法。 51 | 52 | ### 开启多语言支持 53 | 54 | 首先要确保Django 的`USE_I18N`设置已被置为 `True`。 55 | 56 | 要开启多语言支持,就要将 `django.middleware.locale.LocaleMiddleware` 添加到`MIDDLEWARE`设置项: 57 | 58 | ```python 59 | MIDDLEWARE = ( 60 | ... 61 | 62 | 'django.middleware.locale.LocaleMiddleware', 63 | ) 64 | ``` 65 | 66 | 此中间件类对用户的浏览器语言进行查看,并[根据用户浏览器语言来对站点的语言加以设置](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference)。 67 | 68 | ### 根据不同URLs而提供不同的语言 69 | 70 | 有时仅开启Django的多语言支持还不够。默认Django提供到有着相同URL的同一页面提供不同语言。但这样做却有着一些弊端: 71 | 72 | + 用户无法在不修改他们的浏览器设置下改变语言 73 | + 在有着多种缓存设置时,这种做法可能无法工作(在内容随浏览器设置而变化时) 74 | 75 | 在开启了 Django 的 `i18n_patterns` 特性时,就会在URLs前冠以语言代码(比如`/en/about-us`)。此时用户在初次访问站点时,依据其浏览器语言,而转往其偏好的版本了。 76 | 77 | 此特性是通过项目的根URL配置进行开启的。只需将那些想要开启此特性的视图,放入到一个`i18n_patterns`清单中,并将那个清单追加到其他URL模式即可: 78 | 79 | ```python 80 | # mysite/urls.py 81 | 82 | from django.conf.urls import include, re_path 83 | from django.conf.urls.i18n import i18n_patterns 84 | from django.conf import settings 85 | from django.contrib import admin 86 | 87 | from wagtail.admin import urls as wagtailadmin_urls 88 | from wagtail.documents import urls as wagtaildocs_urls 89 | from wagtail.core import urls as wagtail_urls 90 | from search import views as search_views 91 | 92 | urlpatterns = [ 93 | re_path(r'^django-admin', include(admin.site.urls)), 94 | 95 | re_path(r'^admin', include(wagtailadmin_urls)), 96 | re_path(r'^documents', include(wagtaildocs_urls)), 97 | ] 98 | 99 | urlpatterns += i18n_patterns( 100 | # 下面这些URLs将带有前面追加的 // 101 | 102 | re_path(r'^search/$', search_views.search, name='search'), 103 | 104 | re_path(r'', include(wagtail_urls)), 105 | ) 106 | ``` 107 | 108 | 可通过改变URL前面部分,来实现语言的切换。因为每种语言都有其自己的URL,因此在设置有缓存的情况下,也可良好工作。 109 | 110 | ### 模板的翻译 111 | 112 | 模板中的静态文本,需要以某种方式标记起来,从而令到Django的 `makemessages` 命令能够为翻译者找到并导出那些字符串,同时在模板被提供时,允许这些字符串切换到翻译后的版本。 113 | 114 | 因为Wagtail使用了Django的模板,故这些标记的插入,以及模板中字符串的导出与翻译流程,与其他所有Django项目都是一样的。 115 | 116 | 请参阅:https://docs.djangoproject.com/en/stable/topics/i18n/translation/#internationalization-in-template-code 117 | 118 | ### 对内容的翻译 119 | 120 | Wagtail中对内容的翻译,最常用的方法,就是对各个可翻译文本字段进行复制,为每种语言提供一个单独的字段。 121 | 122 | 本小节将就如何手动实现这种方法进行讲解,但有一个可使用的第三方模块,[wagtail 模型翻译](https://github.com/infoportugal/wagtail-modeltranslation),在该模块满足需求时,要快捷一些。 123 | 124 | __复制模型中的字段__ 125 | 126 | 对那些认为可以翻译的各个字段,为所支持的每种语言都复制一份,并以语言代码作为后缀: 127 | 128 | ```python 129 | class BlogPage(Page): 130 | 131 | title_fr = models.CharField(max_length=255) 132 | 133 | body_en = StreamField(...) 134 | body_fr = StreamField(...) 135 | 136 | # 独立于不同语言的字段,无需进行复制 137 | thumbnail_image = models.ForeignKey('wagtailimages.Image' on_delete=model.SET_NULL, null=True, ...) 138 | ``` 139 | 140 | > **注意** 这里只定义了法语版本的`title`,因为Wagtail 已经提供了英语版本。 141 | 142 | __对管理界面中的字段进行组织__ 143 | 144 | 既可以将有着翻译的全部字段依次放在“内容”分页上,也可将其他语言的翻译放在不同分页上。 145 | 146 | 请参阅 [对分页界面进行定制](../customisation/page_editing_interface.md#customising-the-tabbed-interface) 了解有关如何将更多分页加入到管理界面的信息。 147 | 148 | __从模板访问这些字段__ 149 | 150 | 为了让这些翻译显式在站点的前端,就需要根据客户端所选的语言,在模板中使用正确的字段。 151 | 152 | 如果每次在模板中显示一个字段时,都必须加入语言的检查,那么将令到模板十分混乱。有个小小的窍门,令到既能实现此目的,又能保持模板与模型代码的整洁。 153 | 154 | 可使用类似下面的一小段代码,来给页面模型添加一些访问器字段。这些访问器字段将指向包含了用户选定语言的字段。 155 | 156 | 将下面的代码复制到项目中,并确保其在所有包含了有着翻译字段的`Page`页面模型类的`models.py`文件中有被导入。为了支持不同语言,将需要对此代码进行一些修改。 157 | 158 | ```python 159 | from django.utils import transaltion 160 | 161 | class TranslatedField: 162 | 163 | def __init__(self, en_field, fr_field): 164 | self.en_field = en_field 165 | self.fr_fiedl = fr_fiedl 166 | 167 | def __get__(self, instance, owner): 168 | if translation.get_language() == 'fr': 169 | return getattr(instance, self.fr_field) 170 | else: 171 | return getattr(instance, self.en_fiedl) 172 | ``` 173 | 174 | 随后为各个翻译字段,创建一个有着良好命名(因为那就是模板中将要引用的名称)的`TranslatedField`的实例。 175 | 176 | 比如以下就是将要应用到上面的`BlogPage`模型的方法: 177 | 178 | ```python 179 | class BlogPage(Page): 180 | ... 181 | 182 | translated_title = TranslatedField( 183 | 'title', 184 | 'title_fr', 185 | ) 186 | 187 | body = TranslatedField( 188 | 'body_en', 189 | 'body_fr', 190 | ) 191 | ``` 192 | 193 | 最后在模板中,对访问器而非底层的数据库字段进行引用: 194 | 195 | ```html 196 | {{ page.translated_title }} 197 | 198 | {{ page.body }} 199 | ``` 200 | 201 | ### 其他方式 202 | 203 | + [创建多语言站点(通过复制页面树)](duplicate_tree.md) 204 | 205 | - [根页面](duplicate_tree.md#the-root-page) 206 | - [将页面链接起来](duplicate_tree.md#linking-pages-together) 207 | -------------------------------------------------------------------------------- /topics/writing_templates.md: -------------------------------------------------------------------------------- 1 | # 编写模板 2 | 3 | Wagtail使用了Django的模板语言。若刚接触Django,请从Django自己的模板文档开始:[模板](https://docs.djangoproject.com/en/stable/topics/templates/)。 4 | 5 | 对与那些刚接触Django/Wagtail的Python程序员,可能偏好看更具技术性的文档:[Django模板语言:写给Python程序员](https://docs.djangoproject.com/en/stable/ref/templates/api/)。 6 | 7 | 在继续本文之前,应熟悉Django模板原理方面的基础知识。 8 | 9 | 10 | ## 关于模板 11 | 12 | 在Wagtail中的所有页面或“内容类型”,都是在一个叫做`models.py`的文件中,被定义为某个“模型”的(every type of page or "content type" in Wagtail is defined as a "model" in a file called `models.py`)。当站点有个博客应用时,那么就会有个`BlogPage`模型,以及另一个命名为`BlogPageListing`的模型。模型的名称,是有Django开发者决定的。 13 | 14 | 对于`models.py`中的每个页面模型,Wagtail都假定存在(多数情况下)一个同样名称的HTML模板文件。前端开发者可能需要自己负责创建出这些模板,他们通过参考`models.py`,从该文件中所定义的模型,来推断出模板的名称。 15 | 16 | Wagtail将把驼峰式的模型类名,转换成蛇形名称,以找出一个恰当的模板。因此对于`BlogPage`页面模型,就期望得到一个`blog_page.html`的模板。必要时可覆写模型对应的模板文件名称。 17 | 18 | 默认模板文件位于此处: 19 | 20 | ```bash 21 | name_of_project/ 22 | name_of_app/ 23 | templates/ 24 | name_of_app/ 25 | blog_page.html 26 | models.py 27 | ``` 28 | 29 | 有关此方面的更多信息,请参阅Django文档的 [应用目录的模板加载器](https://docs.djangoproject.com/en/stable/ref/templates/api/) 30 | 31 | ### 关于页面内容 32 | 33 | 进入到各个页面的数据/内容,是通过Django的 `{{ double-brace }}`表示法来i就那些访问/输出的。模型中的每个字段,都必须通过前缀`page.`来进行访问。比如页面标题`{{ page.title }}`或另一个`{{ page.author }}`字段。 34 | 35 | 此外,`request.`也是可用的,该对象包含了Django的请求对象。 36 | 37 | 38 | ## 静态文件 39 | 40 | 像是CSS、JS与图片这样的静态文件,通常存储在这里: 41 | 42 | ```bash 43 | name_of_project/ 44 | name_of_app/ 45 | static/ 46 | name_of_app/ 47 | css/ 48 | js/ 49 | images/ 50 | models.py 51 | ``` 52 | 53 | (其中的文件夹名`css`、`js`等并不重要,那只是他们在树中的位置) 54 | 55 | `static`文件中中的所有文件,都应通过使用`{% static %}`标签,插入到HTML中。更多有关此方面的内容:[静态文件(标签)](#static-tag)。 56 | 57 | ### 用户的图片 58 | 59 | 由用户上传到Wagtail站点的图片(与上面提到的开发者的静态文件相反),被放入到图片库中,并通过[页面编辑器界面](editor_manual/new_pages/inserting_images.md)从图片库添加到页面。 60 | 61 | 与其他内容管理系统不同,将图片添加到页面并不涉及到所使用图片“版本”的选择。Wagtail没有预先定义的图片“格式”或“尺寸”。而是有模板开发者,通过一种模板中的特殊语法,来定义图片操作,这种图片操作,实在图片被请求时, *于操作中* 进行的(Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template)。 62 | 63 | 图片库中的图片,必须通过这种语法加以请求,但对于开发者的静态图片,则可通过传统方式,比如`img` 标签,予以添加。只有图片库中的图片,才能够 *于操作中* 进行处理。 64 | 65 | 更过有关这种图片处理语法的信息,请参考:[在模板中使用图片](topics/images.md#image-tag)。 66 | 67 | 68 | ## 模板的标签与过滤器 69 | 70 | 除了Django的标准标签与过滤器外,Wagtail还其他了一些自己的标签与过滤器,这些标签与过滤器可[如同其他标签与过滤器一样](https://docs.djangoproject.com/en/stable/howto/custom-template-tags/),通过 `load` 进行加载。 71 | 72 | 73 | ### 图片(标签) 74 | 75 | `image`标签,将一个兼容XHTML的`img`元素,插入到页面中,并设置其 `src`、`width`、`height`与`alt`属性。请参阅[对`img`标签的更多控制](topics/images.md#image-tag-alt)。 76 | 77 | `image`标签的语法如下: 78 | 79 | {% image [image] [resize-rule] %} 80 | 81 | 示例: 82 | 83 | {% load wagtailimages_tags %} 84 | ... 85 | 86 | {% image page.photo width-400 %} 87 | 88 | 89 | {% image page.photo file-80x80 %} 90 | 91 | 请参阅完整文档 [在模板中使用图片](topics/images.md#image-tag)。 92 | 93 | ### 富文本(过滤器) 94 | 95 | 此过滤器将一块HTML内容,在页面中作为安全的HTML进行渲染。重要的是他还将HTML块内部的缩略式引用展开为嵌入式图片,以及将那些在Wagtail编辑器中制作的链接,展开为可供显示的恰当的HTML。 96 | 97 | 在模板中,只有那些使用了`RichTextField`的字段,才需要应用此过滤器。 98 | 99 | {% load wagtailcore_tags %} 100 | 101 | ... 102 | {% page.body|richtext %} 103 | 104 | 105 | ### 响应式嵌入内容 106 | 107 | **Responsive Embeds** 108 | 109 | Wagtail在包含嵌入内容与图片时,是以其完整宽度进行嵌入的,这样做可能超出在模板中定义的嵌入内容容器的边界。要让图片与嵌入内容成为响应式的 -- 那意味着这些内容将缩放到适合他们的容器的大小 -- 请将下面的CSS加入到站点样式表中: 110 | 111 | ```css 112 | .rich-text img { 113 | max-width: 100%; 114 | height: auto/ 115 | } 116 | 117 | .responsive-object { 118 | position: relative; 119 | } 120 | 121 | .responsive-object iframe; 122 | .responsive-object object; 123 | .responsive-object embed { 124 | position: absolute; 125 | top: 0; 126 | left 0; 127 | width: 100%; 128 | height: 100%; 129 | } 130 | ``` 131 | 132 | ### 内部的链接(标签) 133 | 134 | + `pageurl` 135 | 136 | 从某个页面对象,在该页面与当前页面为同一个站点时,返回一个相对的URL(`/foo/bar/`),在不是同一个站点时,返回一个绝对URL(`http://example.com/foo/bar/`)。 137 | 138 | {% load wagtailcore_tags %} 139 | ... 140 | 141 | 142 | 143 | + `slugurl` 144 | 145 | 从页面的“Promote”分页中定义的`slug`,返回一个与该页面匹配的URL。如该别名下存在多个页面,那么就确定不下来所选的页面了。 146 | 147 | 与`pageurl`类似,该标签在可能的情况下会提供一个相对链接,在所给页面位于不同站点时,则会默认为一个绝对链接。这在创建共享页面特性时,比如顶层的导航栏,或全站链接时,是最有用的(like `pageurl`, this will try to provide a relative link if possible, but will default to an absolute link if the Page is on a different Site. This is most useful when creating shared page feature, e.g. top level navigation or site-wide links)。 148 | 149 | {% load wagtailcore_tags %} 150 | ... 151 | News index 152 | 153 | 154 | ### 静态文件(标签) 155 | 156 | 该标签用于从静态文件目录装入任意文件。该标签的使用,避免了在主机环境变化时重写静态路径,因为在开发环境下与上线后这些静态路径可能有所不同。 157 | 158 | {% raw %} 159 | {% load static %} 160 | ... 161 | My image 162 | {% endraw %} 163 | 164 | 165 | ## Wagtail的用户栏 166 | 167 | 该标签为已登入用户提供了一个上下文弹出菜单(a contextual flyout menu)。该菜单给网站编辑提供了编辑当前页面或加入一个子页面的能力,以及在Wagtail页面浏览器中显示该页面,或前往Wagtail管理控制台的选项。对于网站主编,也能够通过用户栏,完成对某个提交预览的页面予以通过或撤回的内容审核工作。 168 | 169 | {% load wagtailuserbar %} 170 | ... 171 | {% wagtailuserbar %} 172 | 173 | 174 | 默认用户栏从浏览器窗口边缘插入到页面的右下角。如此默认行为与设计有冲突,就可通过传入一个参数到该模板标签,而对其进行移动。下面的示例给出了将用户栏放置于屏幕各个角落的方法: 175 | 176 | {% wagtailuserbar 'top-left' %} 177 | {% wagtailuserbar 'top-right' %} 178 | {% wagtailuserbar 'bottom-left' %} 179 | {% wagtailuserbar 'bottom-right' %} 180 | 181 | 用户栏也可以放置于任何最有利于设计的地方。此外,可在CSS文件中以CSS规则的方式,来确定他的放置位置,比如: 182 | 183 | ```css 184 | ... 185 | .wagtail-userbar { 186 | top: 200px !important; 187 | left: 10px !important; 188 | } 189 | ``` 190 | 191 | ## 在内容上线之前经由预览对输出进行检查 192 | 193 | 有的时候可能希望是否依据页面经由预览或上线查看,来对模板输出进行检查。比如在站点上有着诸如Google Analytics这样的访问者追踪代码时,那么在预览时进行检查就比较好,因为这样做的话,站点编辑的操作,不会出现在分析报告中。Wagtail提供了一个`request.is_preview`的变量,来对预览和上线进行区分: 194 | 195 | {% if not request.is_preview %} 196 | 201 | {% endif %} 202 | -------------------------------------------------------------------------------- /topics/snippets.md: -------------------------------------------------------------------------------- 1 | # 内容块(Snippets) 2 | 3 | 所谓片段(Snippets),是指小片的、没有充分理由渲染成为一个完整网页的内容。他们可用于次级内容,诸如页面头部、页脚以及侧边栏等的制作,在Wagtail管理界面可进行编辑。片段是一些没有从`Page`基类进行继承的Django模型,并因此没有组织到Wagtail树中。但他们仍可通过赋予其面板,而成为可编辑的内容,并经由`register_snippet`类装饰器,将某个模型标识为片段。 4 | 5 | 片段缺少页面的很多特性,比如在Wagtail管理界面可被排序,或拥有一个定义的URL。要仔细区分打算构建为片段的内容类型,是否更适合构建为一个页面。 6 | 7 | ## 片段模型 8 | 9 | 下面是一个片段模型的示例: 10 | 11 | ```python 12 | from django.db import models 13 | 14 | from wagtail.admin.edit_handlers import FieldPanel 15 | from wagtail.snippets.models import register_snippet 16 | 17 | ... 18 | @register_snippet 19 | class Advert(models.Model): 20 | url = models.URLField(null=True, blank=True) 21 | text = models.CharField(max_length=255) 22 | 23 | panels = [ 24 | FieldPanel('url'), 25 | FieldPanel('text'), 26 | ] 27 | 28 | def __str__(self): 29 | return self.text 30 | ``` 31 | 32 | 该`Advert`模型使用了基本的Django模型类,并定义了两个属性:`text`与`URL`。编辑节目与提供给派生自`Page`的类非常接近,有着在`panels`属性中所指派的几个字段。片段不会用到多个分栏的字段,同时也不提供“保存为草稿”或“提交审核”特性。 33 | 34 | `@register_snippet`告诉Wagtail将该模型作为一个片段进行处理。`panels`清单定义了在该片段的编辑页面上展示的字段。经由`def __str__(self):` 提供一个表示该类的字符串也很重要,这样做才能在片段于Wagtail管理界面中被列出时,片段对象才有意义。 35 | 36 | ## 在模板标签中将片段包含进去 37 | 38 | 将片段暴露给模板的最简单方式,就是通过使用模板标签。这主要是通过普通的Django实现的,因此最好复习一下Django的[django定制模板标签](https://docs.djangoproject.com/en/stable/howto/custom-template-tags/)文档,那将更有助益。这里仍将回顾到基础知识,还将指出一些对于Wagtail需要考虑的地方(We'll go over the basics, though, and point out any considerations to make for Wagtail)。 39 | 40 | 首先将一个新的Python文件加入到应用中的`templatetags`文件夹 -- 比如`myproject/demo/templatetags/demo_tags.py`。这里将装入一些Django的模块,以及应用的模型,并准备好`register`装饰器: 41 | 42 | ```python 43 | from django import template 44 | from demo.models import Advert 45 | 46 | register = template.Library() 47 | 48 | ... 49 | 50 | # Advert 片段 51 | @register.inclusion_tag('demo/tags/advert.html', takes_context=True) 52 | def adverts(context): 53 | return { 54 | 'adverts': Advert.objects.all(), 55 | 'request': conext['request'], 56 | } 57 | ``` 58 | 59 | `@register.inclusion_tag()` 需要两个变量:一个模板与一个该模板是否要传入一个请求上下文的逻辑值。将请求上下文包含在定制模板标签中,是种好的做法,因为某些特定于Wagtail的模板标签,如`pageurl`,就需要上下文才能正确工作。模板标签函数可取一些参数,并对该`adverts`进行过滤,以返回一个特定模型,这里因为简要而仅使用了`Advert.objects.all()`。 60 | 61 | 下面是使用到模板标签所使用的模板: 62 | 63 | ```html 64 | {% for advert in adverts %} 65 |

    66 | {{ advert.text }} 67 |

    68 | {% endfor %} 69 | ``` 70 | 71 | 随后在页面模板中,据可以这样来将该片段模板标签包含进来了: 72 | 73 | {% raw %} 74 | {% load wagtailcore_tags demo_tags %} 75 | 76 | ... 77 | {% block content %} 78 | ... 79 | 80 | {% adverts %} 81 | {% endblock %} 82 | {% endraw %} 83 | 84 | ## 将页面绑定到片段 85 | 86 | 在上述示例中,`adverts`的清单是一个固定清单,其显示是独立于页面内容的。这种形式对于某个侧边栏中的普通面板可能是预期的效果,但在其他场景下,可能希望对某个页面内容中的特定片段进行引用(this might be what you want for a common panel in a sidebar, say -- but in other scenarios you may wish to refer to a particular snippet from within a page's content)。这可通过在页面模型中定义一个到片段模型的外键,并将一个`SnippetChooserPanel`到添加到页面的`content_panels`来实现。比如在打算指定某个`advert`要出现在`BookPage`上时: 87 | 88 | ```python 89 | from wagtail.snippets.edit_handlers import SnippetChooserPanel 90 | 91 | # ... 92 | 93 | class BookPage(Page): 94 | 95 | advert = models.ForeignKey( 96 | 'demo.Advert', 97 | null=True, 98 | blank=True, 99 | on_delete=models.SET_NULL, 100 | related_name='+' 101 | ) 102 | 103 | content_panels = Page.content_panels + [ 104 | SnippetChooserPanel('advert'), 105 | # ... 106 | ] 107 | ``` 108 | 109 | 随后该片段就可以在模板中作为`page.advert`进行访问了。 110 | 111 | 要将多个`adverts`附加到某个页面,就可将`SnippetChooserPanel`放置在`BookPage`的某个内联子对象上,而不要在`BookPage`上。下面的子模型被命名为了`BookPageAdvertPlacement`(之所以这样命名,是因为对于每次将`advert`放置在`BookPage`上时,都有一个这样的对象,here this child model is named `BookPageAdvertPlacement`(so called because there is on such object for each time that an advert is placed on a BookPage))。 112 | 113 | ```python 114 | from django.db import models 115 | 116 | from wagtail.core.models import Page, Orderable 117 | from wagtail.snippets.edit_handlers import SnippetChooserPanel 118 | 119 | from modelcluster.fields import ParentalKey 120 | 121 | ... 122 | 123 | class BookPageAdvertPlacement(Orderable, models.Model): 124 | 125 | page = ParentalKey('demo.BookPage', on_delete=models.CASCADE, related_name='advert_placements') 126 | advert = models.ForeignKey('demo.Advert', on_delete=models.CASCADE, related_name='+') 127 | 128 | class Meta: 129 | verbose_name = "广告位" 130 | verbose_name_plural = "广告位" 131 | 132 | panels = [ 133 | SnippetChooserPanel('advert'), 134 | ] 135 | 136 | def __str__(self): 137 | return self.page.title + " -> " + self.advert.text 138 | 139 | class BookPage(Page): 140 | ... 141 | 142 | content_panels = Page.content_panels + [ 143 | InlinePanel('advert_placements', label="广告"), 144 | # ... 145 | ] 146 | ``` 147 | 148 | 现在这些子对象就可经由页面的`advert_placements`属性访问到了,且从那里可以`advert`访问到链接的`Advert`。在`BookPage`的模板中,可包含以下代码: 149 | 150 | ```html 151 | {% for advert_placement in page.advert_placements.all %} 152 |

    {{ advert_placement.advert.text }}

    153 | {% endfor %} 154 | ``` 155 | 156 | ## 令到片段可被搜索 157 | 158 | 在片段模型继承了[对定制模型进行索引](search.html#indexing-custom-models)所降到的`wagtail.search.index.Indexed`时,Wagtail将自动把一个搜索框添加到那个片段类型的选择器界面上。比如该`Advert`片段可像下面这样做成可搜索的: 159 | 160 | ```python 161 | ... 162 | 163 | from wagtail.search import index 164 | ... 165 | 166 | @register_snippet 167 | class Advert(index.Indexed, models.Model): 168 | url = models.URLField(null=True, blank=True) 169 | text = models.CharField(max_length=255) 170 | 171 | panels = [ 172 | FieldPanel('url'), 173 | FieldPanel('text'), 174 | ] 175 | 176 | search_fields = [ 177 | index.SearchField('text', partial_match=True), 178 | ] 179 | ``` 180 | 181 | ## 给片段打上标签 182 | 183 | 将标签添加到片段,与将标签添加到页面非常类似。唯一差别在于应在`ClusterTaggableManager`处使用`taggit.manager.TaggableManager`。 184 | 185 | ```python 186 | from modelcluster.fields import ParentalKey 187 | from modelcluster.models import ClusterableModel 188 | from taggit.models import TaggedItemBase 189 | from taggit.managers import TaggableManager 190 | 191 | class AvertTag(TaggedItemBase): 192 | content_object = ParentalKey('demo.Advert', on_delete=models.CASCADE, related_name='taggged_items') 193 | 194 | @register_snippet 195 | class Advert(ClusterableModel): 196 | 197 | ... 198 | tags = TaggableManager(through=AdvertTag, blank=True) 199 | 200 | panels = [ 201 | ... 202 | FieldPanel('tags'), 203 | ] 204 | ``` 205 | 206 | 关于更多有关在视图中使用标签的知识,请参阅[给页面打标签的文档](reference/pages.html#tagging)。 207 | -------------------------------------------------------------------------------- /advanced_topics/customisation/page_editing_interface.md: -------------------------------------------------------------------------------- 1 | # 编辑界面的定制 2 | 3 | ## 分页界面的定制 4 | 5 | 标准情况下,Wagtail将页面的面板,组织为三个分页:“内容”、“Promote”与“设置”。对于内容片段,Wagtail则是将所有面板放到一个分页中。依据站点的需求,可能希望对特定页面或内容片段下的此种默认做法进行定制 -- 比如为侧边栏内容而添加一个额外的分页。这可通过在页面或内容片段模型上指定一个`edit_handler`属性来实现。比如: 6 | 7 | ```python 8 | from wagtail.admin.edit_handler import TabbedInterface, ObjectList 9 | 10 | class BlogPage(Page): 11 | # 此处省略了字段定义部分 12 | 13 | content_panels = [ 14 | FieldPanel('title', classname="full title"), 15 | FieldPanel('data'), 16 | FieldPanel('body', classname="full"), 17 | ] 18 | 19 | sidebar_content_panels = [ 20 | SnippetChooserPanel('advert'), 21 | InlinePanel('related_links', label="相关链接") 22 | ] 23 | 24 | edit_handler = TabbedInterface([ 25 | ObjectList(content_panels, heading="内容"), 26 | ObjectList(sidebar_content_panels, heading="侧边栏内容"), 27 | ObjectList(Page.promote_panels, heading="Promote"), 28 | ObjectList(Page.settings_panels, heading="设置项", classname="settings"), 29 | ]) 30 | ``` 31 | 32 | 33 | ## 关于富文本(HTML) 34 | 35 | Wagtail提供了一个通用目的、用于创建富文本内容(HTML)及将诸如图片、视频与文档等媒体文件加以嵌入的所见即所得编辑器。在定义某个模型字段时,使用`RichTextField`函数,就可以将该编辑器包含到模型中: 36 | 37 | ```python 38 | from wagtail.core.fields import RichTextField 39 | from wagtail.admin.edit_handlers import FieldPanel 40 | 41 | class BookPage(Page): 42 | book_text = RichTextField() 43 | 44 | content_panels = Page.content_panels + [ 45 | FieldPanel('body', classname="full"), 46 | ] 47 | ``` 48 | 49 | `RichTextField`继承了Django的基本`TextField`字段,因此可像使用普通Django字段那样,将任意字段参数传入到`RichTextField`。该字段并不需要特别的面板,同时可使用`FieldPanel`进行定义。 50 | 51 | 但`RichTextField`的模板输出较为特殊,而需要加以过滤以保留所嵌入的内容。关于这个问题,请参阅[富文本(过滤器)](https://wagtail.xfoss.com/topics/writing_templates.html#rich-text-filter)。 52 | 53 | ### 对富文本字段中的特性加以限制 54 | 55 | 默认该富文本编辑器提供给用户相当多的文本格式化与插入诸如图片等嵌入式内容的选项。然而可能希望将某个富文本字段限制到更为受限的特性集合 -- 比如: 56 | 57 | + 该字段可能打算作为短文本的内容片段,比如将在索引页面上拉出来的一个概要,那样的话嵌入图片或视频就不适当了; 58 | 59 | + 在页面内容是通过使用 [`StreamField`](https://wagtail.xfoss.com/topics/streamfield.html#streamfield)进行定义的时,诸如标题、图片及视频等元素,就通常赋予了其自身的块类型,同时为普通段落文本使用某种富文本的块类型;在此情况下,再允许标题及图片存在于富文本内容,就会显得累赘(并导致设计上无法保持一致)。 60 | 61 | 可通过将带有希望使用的特性标识符的清单的一个`features`关键字参数,传递给`RichTextField`函数,来完成富文本字段特性的限制: 62 | 63 | ```python 64 | body = RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link']) 65 | ``` 66 | 67 | 默认Wagtail安装所提供了以下这些特性标识符: 68 | 69 | + `h1`、`h2`、`h3`、`h4`、`h5`、`h6` -- 标题元素 70 | + `bold`、`italic` -- 粗体/斜体文本 71 | + `ol`、`ul` -- 有序/无序列表 72 | + `hr` -- 水平线 73 | + `link` -- 页面、外部网页与电子邮件链接 74 | + `document-link` -- 到文档的链接 75 | + `image` -- 嵌入图片 76 | + `embed` -- 嵌入媒体(请参阅 [嵌入式内容](https://wagtail.xfoss.com/advanced_topics/embeds.html#embedded-content)) 77 | 78 | 还有几个额外的特性标识符。他们默认没有开启,但可在标识符清单中使用他们。这些额外标识符如下所示: 79 | 80 | + `code` -- 内联代码 81 | + `superscript`、`subscript`、`strikethrough` -- 文本格式化 82 | + `blockquote` -- 引用块(blockquote) 83 | 84 | 以下页面对创建新特性的流程,进行了讲解: 85 | 86 | + [富文本的内部元素](rich_text_internals.md) 87 | + [对Draftail编辑器进行扩展](extending_draftail.md) 88 | + [对Hallo编辑器进行扩展](extending_hallo.md) 89 | 90 | ### 富文本编辑器中的图片格式问题 91 | 92 | 在Wagtail加载时,他将搜寻任何带有`image_formats.py`文件的应用,并执行其中的代码。这就提供了一种在`RichTextField`中插入图片时,对暴露给编辑器的格式选项进行定制的途径。 93 | 94 | 比如加入一个“缩略图”的格式: 95 | 96 | ```python 97 | # image_formats.py 98 | 99 | from wagtail.images.formats import Format, register_image_format 100 | 101 | register_image_format(Format('thumbnail', 'Thumbnail', 'richtext-image thumbnail', 'max-120x120')) 102 | ``` 103 | 104 | 这里是以`Format`类、`register_image_format`函数,以及可选的`unregister_image_format`函数的导入开始的。要注册一个新的`Format`,就要以`Format`对象作为参数,调用`register_image_format`。类`Format`则要取以下的构造器参数: 105 | 106 | + `name` 107 | 108 | 用于标识该格式的唯一性键。在解除此格式的注册时,就要以这个字符串,作为唯一的参数来调用`unregister_image_format`。 109 | 110 | + `label` 111 | 112 | 将图片插入到`RichTextField`中时,在选择器表单中使用的标签。 113 | 114 | + `classnames` 115 | 116 | 指派给所生成的``标签的`class`属性的字符串。 117 | 118 | > __注意__ 所提供的任何CSS类名称,都必须有着单独编写的CSS规则,作为前端CSS代码的一部分与其匹配。指定一个`left`值的`classnames`,将只能确保在生成的标记语言代码中输出一个`left`的CSS类,而不会导致该图片左对齐。 119 | 120 | 121 | + `filter_spec` 122 | 123 | 用于创建图片副本的字符串规格。更多信息,请参考[在模板中使用图片](https://wagtail.xfoss.com/topics/images.html#image-tag)。 124 | 125 | 要解除注册,使用该`Format`的`name`字符串作为唯一参数,调用`unregister_image_format`即可。 126 | 127 | > __注意__ 解除`Format`对象的注册,将导致那些对这些`Format`进行应用的查看或编辑页面报出错误。 128 | 129 | 130 | ## 生成表单的定制 131 | 132 | 133 | [`class wagtail.admin.forms.WagtailAdminModelForm`](#wagtail.admin.forms.WagtailAdminModelForm) 134 | 135 | [`class wagtail.admin.forms.WagtailAdminPageForm`](#wagtail.admin.forms.WagtailAdminPageForm) 136 | 137 | Wagtail使用模型上所配置的面板,来自动生成表单。默认该表单对`WagtailAdminModelForm`,或页面的`WagtailAdminPageForm`进行子类化。可通过对所有模型上的`base_form_class`属性的设置,来配置定制基本表单类。内容片段的定制表单,必须是对`WagtailAdminModelForm`的子类化,而页面的定制表单,则必须是`WagtailAdminPageForm`的子类化。 138 | 139 | 此特性可用于将非模型字段,添加到表单,从而自动生成字段内容,或为模型加入定制的验证逻辑: 140 | 141 | ```python 142 | from django import forms 143 | import geocoder # 该库不是Wagtail中的,这里仅用于示例 -- http://geocoder.readthedocs.io/ 144 | from wagtail.admin.edit_handler import FieldPanel 145 | from wagtail.admin.forms import WagtailAdminPageForm 146 | from wagtail.core.models import Page 147 | 148 | class EventPageForm(WagtailAdminPageForm): 149 | address = form.CharField() 150 | 151 | def clean(self): 152 | cleaned_data = super().clean() 153 | 154 | # 这里确保活动的开始日期是在结束日期之前 155 | start_date = cleaned_data['start_date'] 156 | end_date = cleaned_data['end_date'] 157 | if start_date and end_date and start_date > end_date: 158 | self.add_error('end_date', '结束日期必须是在开始日期之后') 159 | 160 | return cleaned_data 161 | 162 | 163 | def save(self, commit=True): 164 | page = super().save(commit=False) 165 | 166 | # 根据提交的两个日期,对活动时长字段进行更新 167 | page.duration = (page.end_date = page.start_date).days 168 | 169 | # 通过对地址进行地理计算,来获取定位数据 170 | page.location = geocoder.arcgis(self.cleaned_date['address']) 171 | 172 | if commit: 173 | page.save() 174 | 175 | return page 176 | 177 | class EventPage(Page): 178 | start_date = models.DateField() 179 | end_date = models.DateField() 180 | duration = models.IntegerField() 181 | location = models.CharField(max_length=255) 182 | 183 | content_panels = [ 184 | FieldPanel('title'), 185 | FieldPanel('start_date'), 186 | FieldPanel('end_date'), 187 | FieldPanel('address'), 188 | ] 189 | 190 | base_form_class = EventPageForm 191 | ``` 192 | 193 | Wagtail将生成该模型的表单的一个新的子类,把`panels`与`content_panels`中的定义的所有字段都加入进来。模型上已经定义的所有字段,都不会被这些自动生成添加的字段所覆写,因此某个模型字段的表单字段,可通过将其添加到定制表单而加以覆写(Any fields already defined on the model will not be overridden by these automatically added fields, so the form field for a model field can be overridden by adding it to the custom field)。 194 | -------------------------------------------------------------------------------- /advanced_topics/testing.md: -------------------------------------------------------------------------------- 1 | # 对 Wagtail站点进行测试 2 | 3 | Wagtail自带了一些可以简化编写站点测试的工具。 4 | 5 | ## `WagtailPageTests` 6 | 7 | 8 | + `class wagtail.tests.utils.WagtailPageTests` 9 | 10 | `WagtailPageTests` 对 `django.test.TestCase` 进行了扩展,加入了一些新的 `assert` 方法。可对此类进行扩展,以利用其各个方法: 11 | 12 | ```python 13 | from wagtail.tests.utils import WagtailPageTests 14 | from myapp.models import MyPage 15 | 16 | class MyPageTests(WagtailPageTests): 17 | def test_can_create_a_page(self): 18 | ... 19 | ``` 20 | 21 | + `assertCanCreatAt(parent_model, child_model, msg=None)` 22 | 23 | 断言某个特定子页面类型可在某个父页面类型下进行创建。`parent_model` 与 `child_model` 应是要测试的页面类型。 24 | 25 | ```python 26 | def test_can_create_under_home_page(sefl): 27 | # 这里可在某个 HomePage 下创建一个 ContentPage 28 | self.assertCanCreateAt(HomePage, ContentPage) 29 | ``` 30 | 31 | + `assertCanNotCreateAt(parent_model, child_model, msg=None)` 32 | 33 | 断言不能在某个父页面类型下创建某个特定的子页面类型。`parent_model`与`child_model`应是要测试的页面类。 34 | 35 | ```python 36 | def test_cant_create_under_event_page(self): 37 | # 这里无法在 EventPage 下创建 ContentPage 的页面 38 | self.assertCanNotCreateAt(EventPage, ContentPage) 39 | ``` 40 | + `assertCanCreate(parent, child_model, data, msg=None)` 41 | 42 | 断言某个给定页面类型的子页面,可在该父页面下,使用提供的 POST 数据进行创建。 43 | 44 | `parent` 应是某个页面实例,同时`child_model`应是一个页面子类。`data`应是一个将在Wagtail管理界面的页面创建方法处将要 POST 提交的字典。 45 | 46 | ```python 47 | from wagtail.tests.utils.form_data import netsted_form_data, streamfield 48 | 49 | def test_can_create_content_page(self): 50 | 51 | # 获取主页 52 | root_page = HomePage.objects.get(pk=2) 53 | 54 | # 断言可在这里以该 POST 数据,制作一个 ContentPage 55 | self.assertCanCreate(root_page, ContentPage, nested_form_data({ 56 | 'title': '关于我们', 57 | 'body': streamfield([ 58 | ('text', 'Lorem ipsum dolor sit amet'), 59 | ]), 60 | })) 61 | ``` 62 | 63 | 请参阅 [表单数据助手](#form-data-test-helpers) 了解有关构造 POST 数据的一些有用函数。 64 | 65 | + `assertAllowedParentPageTypes(child_model, parent_models, msg=None)` 66 | 67 | 对只能在`parent_models` 创建 `child_model` 这一事实进行测试。 68 | 69 | 在父模型已经设置了 `Page.subpage_types`时, 所允许的父模型清单,可与`Page.parent_page` 中设置的不同。 70 | 71 | ```python 72 | def test_content_page_parent_pages(self): 73 | # ContentPage 只能在 HomePage 或另一个 ContentPage 下创建 74 | self.assertAllowedParentPageTypes(ContentPage, {HomePage, ContentPage}) 75 | 76 | # EventPage 只能在某个 EventIndex 下被创建 77 | self.assertAllowedParentPageTypes(EventPage, {EventIndex}) 78 | ``` 79 | 80 | + `assertAllowedSubpageTypes(parent_model, child_models, msg=None)` 81 | 82 | 对 `parent_model`下只能创建的页面类型为 `child_models` 这一事实进行测试。 83 | 84 | 在子模型已被设置为`Page.parent_page_types`时,所允许的子模型清单可与 `Page.subpage_types` 中设置的有所不同。 85 | 86 | ```python 87 | def test_content_page_subpages(self): 88 | 89 | # ContentPage 只能有其他的 ContentPage 子页面 90 | self.assertAllowedSubPageTypes(ContentPage, {ContentPage}) 91 | 92 | # HomePage 只能有 ContentPage 与 EventIndex 的子页面 93 | self.assertAllowedParentPageTypes(HomePage, {ContentPage, EventIndex}) 94 | ``` 95 | 96 | 97 | ## 表单数据助手 98 | 99 | `assertCanCreate`方法,要求有与页面编辑表单中所提交数据同样格式的页面数据传递给他。对于复杂的页面类型,就难以手动构造该数据结构;模块`wagtail.tests.utils.form_data`就提供了一套助手函数,用于帮助完成 POST 数据的构造。 100 | 101 | + `wagtail.tests.utils.form_data.nested_form_data(data)` 102 | 103 | 将某个嵌套字典的数据结构,转换为带有连字符分隔键的平面表单数据字典(a flat form data dict with hyphen-separated keys)。 104 | 105 | ```python 106 | nested_form_data({ 107 | 'foo': 'bar', 108 | 'parent': { 109 | 'child': 'field', 110 | }, 111 | }) 112 | 113 | # 将返回:{'foo': 'bar', 'parent-child': 'field'} 114 | ``` 115 | 116 | 117 | + `wagtail.tests.utils.form_data.rich_text(value, editor='default', features=None)` 118 | 119 | 将一个类似于 HTML 的富文本字符串,转换为当前活动的富文本编辑器要要求的数据格式。 120 | 121 | __参数__: 122 | 123 | + `editor` -- 一个在`WAGTAILADMIN_RICH_TEXT_EDITORS` 中所定义的替代性编辑器名称 124 | + `features` -- 在富文本内容中所允许的功能清单(参见 [对富文本字段中的功能进行限制](customisation/page_editing_interface.md#rich-text-features)) 125 | 126 | ```python 127 | self.assetCanCreate(root_page, ContentPage, nested_form_data({ 128 | 'title': '关于我们', 129 | 'body': rich_text('

    Lorem ipsum dolor sit amet

    '), 130 | })) 131 | ``` 132 | 133 | + `wagtail.tests.utils.form_data.streafield(items)` 134 | 135 | 取得一个元组的清单(`block_tyep`、`value`),并将其转换为 `StreamField`的表单数据。在某个 `nested_form_data()` 调用中使用该方法,将字段名称作为键。 136 | 137 | ```python 138 | nested_form_data({'content': streamfield([ 139 | ('text', 'Hello, world'), 140 | ])}) 141 | 142 | # 将返回: 143 | # { 144 | # 'content-count': '1', 145 | # 'content-0-type': 'text', 146 | # 'content-0-value': 'Hello, world', 147 | # 'content-0-order': '0', 148 | # 'content-0-deleted': '', 149 | # } 150 | ``` 151 | 152 | 153 | + `wagtail.tests.utils.form_data.inline_formset(items, initial=0, min=0, max=1000)` 154 | 155 | 取得一个 `InlineFormset` 的表单数据清单,并将其转换为有效的POST数据。在某个 `nested_form_data()` 调用中使用此方法,以表单集的关系名称作为键(with the formset relation name as the key)。 156 | 157 | ```python 158 | nested_form_data({'lines': inline_formset([ 159 | {'text': 'Hello'}, 160 | {'text': 'World'}, 161 | ])}) 162 | 163 | # 将返回: 164 | # { 165 | # 'lines-TOTAL_FORMS': '2', 166 | # 'lines-INITIAL_FORMS': '0', 167 | # 'lines-MIN_NUM_FORMS': '0', 168 | # 'lines-MAX_NUM_FORMS': '1000', 169 | # 'lines-0-text': 'Hello', 170 | # 'lines-0-ORDER': '0', 171 | # 'lines-0-DELETE': '', 172 | # 'lines-1-text': 'World', 173 | # 'lines-1-ORDER': '1', 174 | # 'lines-1-DELETE': '', 175 | # } 176 | ``` 177 | 178 | 179 | ## Fixtures 180 | 181 | ### 使用 `dumpdata` 182 | 183 | 通过创建出一些开发环境中的内容,并使用Django 的 [dumpdata](https://docs.djangoproject.com/en/2.0/ref/django-admin/#django-admin-dumpdata) 命令,是为测试目的而创建 一些 [fixtures](https://docs.djangoproject.com/en/stable/howto/initial-data/)的最佳方法。 184 | 185 | 请注意默认的 `dumpdata` 将以主键来表示 `content_type`;在添加/移除模型时,这可能会导致一致性问题,因为内容类型是独立于 fixtures 而单独生成的。为防止这个问题,就要使用 `--natural-foreign` 命令开关,此命令开关将使用 `["app", "model"]` 来表示内容类型。 186 | 187 | ### 手动修改 188 | 189 | 可手动修改复制的 fixtures,或设置完全手动地编写这些 fixtures。以下是一些需要留意的地方。 190 | 191 | __定制页面模型__ 192 | 193 | 在创建 fixtures 中的定制页面模型时,将需要同时添加一个 `wagtailcore.page` 的条目,以及一个定制页面模型的条目。 194 | 195 | 假设有着一个定义了`Homepage(Page)` 类的 `website` 模块。那么就可以在某个 fixture 中创建以下的一个主页: 196 | 197 | ```python 198 | [ 199 | { 200 | "model": "wagtailcore.page", 201 | "pk": 3, 202 | "fileds": { 203 | "title": "客户的主页", 204 | "content_type": ["website", "homepage"], 205 | "depth": 2 206 | } 207 | }, 208 | { 209 | "model": "website.homepage", 210 | "pk": 3, 211 | "fields": {} 212 | } 213 | ] 214 | ``` 215 | 216 | __Treebeard 字段__ 217 | 218 | 为了令到像是 `get_parent` 这类页面树的操作正确工作,因此填入 `path` / `numchild` / `depth` 字段是必要的。在某些不同寻常的情况下,若未给出值,`url_path`是另一个可导致错误的字段。 219 | 220 | [Treebeard 文档](http://django-treebeard.readthedocs.io/en/latest/mp_tree.html) 可帮助理解其工作原理。 221 | -------------------------------------------------------------------------------- /advanced_topics/embeds.md: -------------------------------------------------------------------------------- 1 | # 嵌入的内容 2 | 3 | Wagtail支持从到外部服务提供商,如Youtube或Twitter等上的内容的URLs,生成嵌入代码。默认Wagtail将直接从相关提供商站点,使用 [oEmbed协议](https://oembed.com/) 获取嵌入的代码。 4 | 5 | Wagtail有着一个内建的大多数常见服务商的清单,且该清单可通过 [一项设置](#customising-embed-providers) 进行修改。Wagtail还支持使用 [Embedly](#embedly) 与 [定制的嵌入发现器](#custom-embed-finders) 来获取嵌入代码。 6 | 7 | 8 | ## 在站点上嵌入代码 9 | 10 | 对于大多数内容提供商来说,Wagtail的内容嵌入模块应可以直接开箱即用。可使用下列的任意方式来调用此模块: 11 | 12 | ### 富文本方式 13 | 14 | Wagtail的默认富文本编辑器,有着一个允许将嵌入代码放置于富文本中的“媒体”图标。不必做任何设置来开启此特性;只要确保该富文本字段的内容,在魔板中是通过`|richtext`过滤器进行传递的即可,因为该过滤器正式对嵌入代码模块进行调用,而进行获取并将嵌入代码加以嵌入的。 15 | 16 | ### `StreamField`块的`EmbedBlock`类型 17 | 18 | 该`EmbedBlock`块类型允许将嵌入代码置于某个`StreamField`中。 19 | 20 | 比如: 21 | 22 | ```python 23 | from wagtail.embeds.blocks import EmbedBlock 24 | 25 | class MyStreamField(blocks.StreamBlock): 26 | ... 27 | 28 | embed = EmbedBlock() 29 | ``` 30 | 31 | ### `{% embed %}`标签 32 | 33 | 语法:`{% embed [max_width=] %}` 34 | 35 | 可通过传入URL与一个可选的`max_width`参数到`{% embed %}`标签,而将嵌入代码嵌套进某个魔板。 36 | 37 | 其中的`max_width`参数是在获取嵌入代码时,发送给内容提供商的: 38 | 39 | {% raw %} 40 | 41 | {% load wagtailembeds_tags %} 42 | 43 | {# 这里嵌入一个Youtube视频 #} 44 | {% embed 'https://www.youtube.com/watch?v=SJXMTtvCxRo' %} 45 | 46 | {# 此标签也可从某个变量获取URL #} 47 | {% embed page.video_url %} 48 | {% endraw %} 49 | 50 | # 从Python嵌入 51 | 52 | 还可调用内部的`get_embed`函数,该函数取一个URL字符串,返回一个`Embed`对象(参见下面的模型文档)。其也取一个可选的、在获取嵌入代码时发送到服务提供商处的`max_width`关键字参数。 53 | 54 | ```python 55 | from wagtail.embeds.embeds import get_embed 56 | from wagtail.embeds.exceptions import EmbedException 57 | 58 | try: 59 | embed = get_embed('https://www.youtube.com/watch?v=5CgB9gwXoxM') 60 | 61 | print(embed.html) 62 | 63 | except EmbedException: 64 | # 无法找到嵌入代码 65 | pass 66 | ``` 67 | 68 | 69 | ## 配置嵌入代码的“查找器” 70 | 71 | __Configuring embed "finders"__ 72 | 73 | 所谓嵌入代码查找器,是指Wagtail中负责从某个URL产生嵌入代码的那些模块。 74 | 75 | 嵌入代码查找器是通过使用`WAGTAILEMBEDS_FINDERS`设置,进行配置的。该设置是一个查找器配置的清单,米格查找器配置依次运行,直到其中之一成功返回一个嵌入代码为止: 76 | 77 | 默认配置为: 78 | 79 | ```python 80 | WAGTAILEMBEDS_FINDERS = [ 81 | { 82 | 'class': 'wagtail.embeds.finders.oembed' 83 | } 84 | ] 85 | ``` 86 | 87 | ### oEmbed(默认的) 88 | 89 | 该默认嵌入代码发现器使用oEmbed协议,直接从内容提供商处获取嵌入代码。Wagtail有着一个内建的服务提供商清单,此清单默认全部开启。可在下面的链接找到该内容提供商清单: 90 | 91 | https://github.com/wagtail/wagtail/blob/master/wagtail/embeds/oembed_providers.py 92 | 93 | __对嵌入代码提供商清单的进行定制__ 94 | 95 | 可通过指定在查找器配置中的提供商清单,来限制用到哪些提供商。 96 | 97 | 比如下面的配置,就只允许来自Vimeo与Youtube的内容才能进行嵌套。同时还加入了一个定制的服务提供商: 98 | 99 | ```python 100 | from wagtail.embeds.oembed_providers import youtube, vimeo 101 | 102 | # 加入一个定制的提供商 103 | # 为了令到嵌入代码可以工作,定制提供商必须支持oEmbed协议。应可以 104 | # 在提供商的文档中找到这些技术细节。 105 | # - `endpoint` 时 Wagtail将调用的 oEmbed 端点 106 | # - `urls` 指定和何种模式 107 | 108 | my_custom_provider = { 109 | 'endpoint': 'https://customvideosite.com/oembed', 110 | 'urls': [ 111 | '^http(?:s)?://(?:www\\.)?customvideosite\\.com/[^#?/]+/videos/.+$', 112 | ] 113 | } 114 | 115 | WAGTAILEMBEDS_FINDERS = [ 116 | { 117 | 'class': 'wagtail.embeds.finders.oembed', 118 | 'providers': [youtube, vimeo, my_custom_provider], 119 | } 120 | ] 121 | ``` 122 | 123 | __对单个的提供商进行定制__ 124 | 125 | 可将多个查找器链接在一起(multiple finders can be chained together)。此特性可用于对某个提供商的配置进行定制,而不会影响到其他提供商。 126 | 127 | 比如下面就是告诉Youtube返回HTTPS形式的视频(对于Youtube必须显式地这样做): 128 | 129 | ```python 130 | from wagtail.embeds.oembed_providers import youtube 131 | 132 | WAGTAILEMBEDS_FINDERS = [ 133 | # 获取YouTube视频,但要在调用Youtube的oEmbed端点时,将`?scheme=https`放在 `GET` 参数中 134 | 135 | { 136 | 'class': 'wagtail.embeds.finders.oembed', 137 | 'providers': [youtube], 138 | 'options': {'scheme': 'https'} 139 | }, 140 | 141 | # 以默认方式处理其他所有oEmbed提供商 142 | { 143 | 'class': 'wagtail.embeds.finders.oembed', 144 | } 145 | ] 146 | ``` 147 | 148 | __Wagtail使用多个嵌入代码查找器的方式__ 149 | 150 | 在有着多个提供商都能处理某个URL时(比如使用上面的配置时的请求的一个Youtube视频),那么最顶上的那个查找器将被选作执行本次请求。 151 | 152 | 就算选中的查找器并未返回一个嵌入内容,Wagtail也将不会尝试运行其他查找器。 153 | 154 | ### Embed.ly 155 | 156 | Embed.ly是一项并未实现oEmbed协议,但也能够提供站点嵌入内容的付费服务。 157 | 158 | 他们也提供了一些诸如赋予嵌入内容一种持久外观,与在站点允许视频在不同服务提供商处留存,且需要对这些视频实现定制控件时,一个有用常见视频回访API。 159 | 160 | Wagtail具有内建的从Embed.ly获取嵌入代码的支持。可通过将一个使用了`wagtail.embeds.finders.oembed`类,并传入了Embed.ly API秘钥的嵌入代码发现器,加入到`WAGTAILEMBEDS_FINDERS`设置,就可使用该项付费服务了: 161 | 162 | ```python 163 | WAGTAILEMBEDS_FINDERS = [ 164 | { 165 | 'class': 'wagtail.embeds.finders.embedly', 166 | 'key': '你的embed.ly秘钥' 167 | } 168 | ] 169 | ``` 170 | 171 | ### 对嵌入代码发现器类进行定制 172 | 173 | 可创建一个定制的查找器类,以实现对发现器完全的控制。 174 | 175 | 下面是一个存根发现器类,可用作其他类的框架(here is a stub finder class that could be used as a skeleton);请阅读代码中的那些 `docstrings` 来了解各个方法具体干些什么: 176 | 177 | ```python 178 | from wagtail.embeds.finders.base import EmbedFinder 179 | 180 | class ExampleFinder(EmbedFinder): 181 | def __init__(self, **options): 182 | pass 183 | 184 | def accept(self, url): 185 | 186 | """ 187 | 在本发现器知道如何获取某个URL的嵌入代码时返回 True。 188 | 这样做应不会有任何弊端(尚无到外部服务器的请求) 189 | """ 190 | pass 191 | 192 | def finder_embed(self, url, max_width=None): 193 | 194 | """ 195 | 取用一个URL 与最大宽度参数,并返回一个有个将用于嵌入到站点的内容的信息字典 196 | 197 | 此方法就是即将向外部APIs发起请求的部分。 198 | """ 199 | 200 | # TODO: 完成请求 201 | 202 | return { 203 | 'title': '内容的标题', 204 | 'author_name': '内容作者名称', 205 | 'provider_name': '提供商名称 (比如 YouTube, Vimeo, etc)', 206 | 'type': "要么是'photo', 'video', 'link', or 'rich'之一", 207 | 'thumbnail_url': '到缩略图的URL', 208 | 'width': width_in_pixels, 209 | 'height': height_in_pixels, 210 | 'html': "

    嵌入的HTML

    " 211 | } 212 | ``` 213 | 214 | 在实现了所有这些方法后,就只需将定制的查找器类添加到`WAGTAILEMBEDS_FINDERS`设置即可: 215 | 216 | ```python 217 | WAGTAILEMBEDS_FINDERS = [ 218 | { 219 | 'class': 'path.to.your.finder.class.here', 220 | # 所有其他选项,将作为提供给`__init__`方法的 `kwargs` 加以传递 221 | } 222 | ] 223 | ``` 224 | 225 | ## `Embed`模型 226 | 227 | + `class wagtail.embeds.models.Embed` 228 | 229 | 嵌入代码将只获取一次,并存储在数据库中,故其后对某个嵌入代码的访问,并不会再去调用该嵌入代码的查找器了。 230 | 231 | - `url` 232 | 233 | (文本) 234 | 此嵌入代码的原始内容的URL。 235 | 236 | - `max_width` 237 | 238 | (整数,非空值) 239 | 所请求的最大宽度。 240 | 241 | - `type` 242 | 243 | (文本) 244 | 嵌入代码的类型。该字段可以是 “video”、“photo”、“link”或“rich”。 245 | 246 | - `html` 247 | 248 | (文本) 249 | 应放置在页面商的嵌入代码的HTML内容。 250 | 251 | - `title` 252 | 253 | (文本) 254 | 所嵌入内容的标题。 255 | 256 | - `author_name` 257 | 258 | (文本) 259 | 所嵌入内容的作者名称。 260 | 261 | - `provider_name` 262 | 263 | (文本) 264 | 所嵌入内容的提供商名称。 265 | 比如: Youtube, Vimeo等 266 | 267 | - `thumbnail_url` 268 | 269 | (文本) 270 | 一个到所嵌入内容的缩略图的URL。 271 | 272 | - `width` 273 | 274 | (整数,非空值) 275 | 嵌入代码的宽度(仅适用于图片与视频)。 276 | 277 | - `height` 278 | 279 | (整数,非空值) 280 | 嵌入代码的高度(仅适用于图片与视频)。 281 | 282 | - `last_updated` 283 | 284 | (`datetime`值) 285 | 此嵌入代码上一次获取到的日期/时间。 286 | 287 | 288 | ## 删除嵌入代码 289 | 290 | 只要嵌入代码方面的配置未被破坏,那么删除`Embed`模型中的条目将很安全地完成。Wagtail将自动重新生成站点商正在使用的那些记录。 291 | 292 | 之所以要删除嵌入代码,无非是要从 oEmbed 协议转到 Embed.ly服务,或反过来,因为他们二者所生成的代码略有不同,从而导致站点上前后矛盾。 293 | -------------------------------------------------------------------------------- /topics/search/searching.md: -------------------------------------------------------------------------------- 1 | # 进行搜索 2 | 3 | ## 搜索的QuerySets 4 | 5 | Wagtail的搜索特性,是建立在Django的 [QuerySet API](https://docs.djangoproject.com/en/stable/ref/models/querysets/)之上的。可搜索所有由模型所提供的Django QuerySet,以及那些已加入到搜索索引的、进行过滤的字段(You should be able to search any Django QuerySet provided the model and the fields being filtered on have been added to the search index)。 6 | 7 | ## 对页面的搜索 8 | 9 | Wagtail提供了搜索页面的一个捷径:`.search()` `QuerySet`方法。可在所有`PageQuerySet`上调用该方法。比如: 10 | 11 | ```bash 12 | # 对未来的 EventPage进行搜索 13 | >>> from wagtail.core.models import EventPage 14 | >>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!") 15 | ``` 16 | 17 | 所有其他`PageQuerySet`的方法,都可以与`.search()`一起使用。比如: 18 | 19 | ```bash 20 | # 搜素所有活动索引下在线显示的EventPage 21 | >>> EventPage.objects.live().descendant_of(events_index).search("Event") 22 | [, ] 23 | ``` 24 | 25 | > **注意** 方法`search()`将吧`QuerySet`转换为某个Wagtail `SearchResults`类(取决于后端情况)的一个实例。这意味着在调用`search()`方法之前进行过滤。 26 | 27 | ## 对图片、文档及定制模型的搜索 28 | 29 | Wagtail的文档及图片模型,与页面模型一样,均在他们的 QuerySets 上提供了一个`search`方法: 30 | 31 | ```bash 32 | >>> from wagtail.images.models import Image 33 | 34 | >>> Image.objects.filter(uploaded_by_user=user).search("Hello") 35 | [, ] 36 | ``` 37 | 38 | 对于[定制模型](#indexing-custom-models),可通过直接在搜索后端,使用`search`方法进行搜索: 39 | 40 | ```bash 41 | >>> from myapp.models import Book 42 | >>> from wagtail.search.backends import get_search_backend 43 | 44 | # 对书籍进行搜索 45 | >>> s = get_search_backend 46 | >>> s.search("Great", Book) 47 | [, ] 48 | ``` 49 | 50 | 也可将一个 QuerySet 传递进 `search` 方法,从而实现将过滤器添加到搜索结果(you can also pass a QuerySet into the `search` method which allows you to add filters to your search results): 51 | 52 | ```bash 53 | >>> from myapp.models import Book 54 | >>> from wagtail.search.backends import get_search_backend 55 | 56 | # 对书籍进行搜索 57 | >>> s = get_search_backend 58 | >>> s.search("Great", Book.objects.filter(published_date__year__lt=1900)) 59 | [] 60 | ``` 61 | 62 | ## 指定要搜索的字段 63 | 64 | 默认Wagtail将搜索所有已使用`index.SearchField`进行索引了的字段。 65 | 66 | 可使用`fileds`关键字参数,将搜索字段限制为确切的字段集合: 67 | 68 | ```sh 69 | # 仅搜索标题字段 70 | >>> EventPage.objects.search("Event", fields=["title"]) 71 | [, ] 72 | ``` 73 | 74 | ## 分面搜索 75 | 76 | **Faceted search** 77 | 78 | Wagtail带有对分面搜索的支持,分面搜索是一种基于分类字段(诸如类别或页面类型)的过滤(Wagtail supports faceted search which is kind of filtering based on a taxonomy field(such as category or page type))。 79 | 80 | 方法`.facet(field_name)`返回的是一个`OrderedDict`。字典中的键是相关对象的IDs,这些对象则是已被该字段引用到的;字典中的值,是到各个ID的引用的数目。方法结果字典,是依引用数目降序排序的。 81 | 82 | 下面是在搜索结果中找出最常用页面类型的代码: 83 | 84 | ```sh 85 | >>> Page.objects.search("Test").facet("content_type_id") 86 | 87 | # 注意:这些键是与某个 Content_Type 对象对应的ID,值则是 88 | # 那个类型下所返回页面的数目 89 | OrderedDict([ 90 | ('2', 4), # 有4个页面有着 content_type_id == 2 91 | ('1', 2), # 有2个页面有着 content_type_id == 1 92 | ]) 93 | ``` 94 | 95 | ## 改变搜索行为 96 | 97 | **Changing search behaviour** 98 | 99 | ### 搜索运算符 100 | 101 | 搜索运算符指明了在用户输入了多个搜索词条时,搜索应如何进行。有两个可能的取值: 102 | 103 | + `or` -- 结果必须匹配至少一个词条(这是Elasticsearch默认的行为) 104 | + `and` -- 结果必须匹配所有词条(这是数据库搜索的默认行为) 105 | 106 | 二者都有利有弊。`or`运算符将返回更多的结果,但会倾向于包含很多的不相关结果。`and`运算符则仅返回那些包含了所有搜索词条的结果,但要求用户的查询更为精准。 107 | 108 | 在以相关度进行排序时,推荐使用`or`运算符,在以所有其他线索排序时使用`and`运算符(注意:当前数据库后端并不支持以相关度为依据的排序)。 109 | 110 | 下面是一个使用`operator`关键字参数的示例: 111 | 112 | ```sh 113 | # 数据库中包含了一个都有以下条目的“Thing”的模型: 114 | # - Hello world 115 | # - Hello 116 | # - World 117 | 118 | # 使用 “or” 运算符的搜索 119 | >>> s = get_search_backend() 120 | >>> s.search("Hello world", Things, operator="or") 121 | 122 | # 所有记录都被返回,因为他们都保护了“hello”或“world” 123 | [, , ] 124 | 125 | # 以“and”运算符进行搜索 126 | >>> s = get_search_backend() 127 | >>> s.search("Hello world", Things, operator="and") 128 | 129 | # 仅返回“hello world”, 因为那是仅有的包含了“hello”与“world”两个词条的数据库条目 130 | [] 131 | ``` 132 | 133 | 对于页面、图片及文档模型,在QuerySet的`search`方法上,也是支持`operator`关键字参数的: 134 | 135 | ```sh 136 | >>> Page.objects.search("Hello world", operator="or") 137 | 138 | # 所有包含了“hello”或“world”的页面都被返回 139 | [, , ] 140 | ``` 141 | 142 | ### 对排序进行定制 143 | 144 | 默认在后端支持的情况下,搜索结果是以想高度进行排序的。要保留QuerySet的既有排序,就要将`search()`方法上的`order_by_relevance`关键字参数设置为`False`。 145 | 146 | 比如: 147 | 148 | ```sh 149 | # 获取一个依日期排序的活动清单 150 | >>> EventPage.objects.order_by('date').search("Event", order_by_relevance=False) 151 | 152 | # 这就是依日期排序的活动了 153 | [, , ] 154 | ``` 155 | 156 | ### 使用分值来对结果进行批注 157 | 158 | **Annotating results with score** 159 | 160 | 对每项匹配结果,Elasticsearch会计算出一个“分值”,所谓分值,就是根据用户的查询,用于表示该项结果相关度的一个数字。结果通常是基于分值进行排序的。 161 | 162 | 在某些场合,对分值的访问是很有用的(诸如程序实现的对不同模型的两个查询结合起来)。通过调用`SearchQuerySet`上的`.annotate_score(field)`方法,可将分支加入到每次查询。 163 | 164 | 比如: 165 | 166 | ```sh 167 | >>> events = EventPage.objects.search('Event').annotate_score('_score') 168 | >>> for event in events: 169 | ... print(event.title, event._score) 170 | ... 171 | ("Easter", 2.5), 172 | ("Haloween", 1.7), 173 | ("Christmas", 1.5), 174 | ``` 175 | 176 | 请注意分值本身是随意的,且仅在对同样查询下的结果比较才有用处(note that the score itself is arbitrary and it is only useful for comparison of results for the same query)。 177 | 178 | ## 页面搜索视图的示例 179 | 180 | 下面是一个可用于将“搜索”页面加入到站点的Django视图的示例: 181 | 182 | ```python 183 | # views.py 184 | 185 | from django.shortcuts import render 186 | 187 | from wagtail.core.models import Page 188 | from wagtail.search.models import Query 189 | 190 | def search(request): 191 | 192 | # 搜索 193 | search_query = request.GET.get('query', None) 194 | if search_query: 195 | search_results = Page.objects.live().search(search_query) 196 | 197 | # 对该查询进行记录,从而令到Wagtail可以给出改进的结果建议 198 | Query.get(search_query).add_hit() 199 | 200 | else: 201 | search_results = Page.objects.none() 202 | 203 | # 对模板进行渲染 204 | return render(request, 'search_results.html', { 205 | 'search_query': search_query, 206 | 'search_results': search_results, 207 | }) 208 | ``` 209 | 210 | 以下是一个与该试图一同工作的模板: 211 | 212 | {% raw %} 213 | {% extends "base.html" %} 214 | {% load wagtailcore_tags %} 215 | 216 | {% block title %}搜索{% endblock %} 217 | 218 | {% block content %} 219 |
    220 | 221 | 222 |
    223 | 224 | {% if search_results %} 225 |
      226 | {% for result in search_results %} 227 |
    • 228 |

      {{ result }}

      229 | {% if result.search_description %} 230 | {{ result.search_description|safe }} 231 | {% endif %} 232 |
    • 233 | {% endfor %} 234 |
    235 | {% elif search_query %} 236 | 未找到结果 237 | {% else %} 238 | 请将搜索词条输入到搜索框中 239 | {% endif %} 240 | {% endblock %} 241 | {% endraw %} 242 | 243 | ## 提升后的搜索结果 244 | 245 | **Promoted search results** 246 | 247 | “提升了的搜索结果”特性,允许网站编辑显式地将相关内容,链接到搜索条目,如此结果页面就能包含干预后的内容,作为搜索引擎结果的补充。 248 | 249 | 此功能是有社区贡献模块`search_promotions`所提供了。 250 | 251 | -------------------------------------------------------------------------------- /topics/images.md: -------------------------------------------------------------------------------- 1 | # 在模板中使用图片 2 | 3 | 标签`image`将一个兼容XHTML的`img`元素,插入到页面中,并设置其`src`、`width`、`height`与`alt`属性。请参阅[对`img`标签的更多控制](#image-tag-alt)。 4 | 5 | 该标签的语法是这样的: 6 | 7 | {% image [image] [resize-rule] %} 8 | 9 | **图片与缩放规则,都必须传递给该模板标签**。 10 | 11 | 示例: 12 | 13 | {% raw %} 14 | 15 | {% load wagtailimages_tags %} 16 | ... 17 | 18 | 19 | {% image page.photo width-400 %} 20 | 21 | 22 | {% image page.photo fill-80x80 %} 23 | {% endraw %} 24 | 25 | 在上面的语法示例中,`[image]` 是一个对图片的Django对象引用。在页面模型定义了一个名为“photo”的字段时,那么`[image]`就会是`page.phoho`。其中的`[resize-rule]`定义了图片在插入到页面时,将被怎样进行缩放。支持的缩放方式有数种,分别用于不同的用例(如首页上横跨整个页面的头图,或被裁剪为固定尺寸的缩略图)。 26 | 27 | 请注意有一个空格将`[image]`与`[resize-rule]`隔开,但缩放规则却不能包含空格。缩放规则中的图片宽度,始终是在高度之前。在没有使用`fill`规则时,被缩放的图片将保持其原始宽高比,`fill`规则将导致图片中的某些像素被裁剪。 28 | 29 | 下面是可用的图片调整的一些方式: 30 | 31 | + `max` 32 | 33 | (要取两个尺寸值) 34 | 35 | {% raw %} 36 | {% iamge page.photo max-1000x500 %} 37 | {% endraw %} 38 | 39 | 40 | 调整到给定的尺寸 **里面**。 41 | 42 | 图片四个边中最长的那个边,将降低到所指定的匹配尺寸。比如一张宽为 `1000` 高为 `2000`的纵向图片,在以`max-1000x500`规则(一个横向布局)进行处理时,将生成一张被压缩的图片,那么最后的 *高度* 就是 `500` 像素,宽度为 `250` 像素了。 43 | 44 | ![图片的 `max` 过滤器](images/image_filter_max.png) 45 | 46 | *示例:图片将保持其宽高比,但将被缩放到所能提供的最大尺寸(Example: The image will keep its proportions but fit within the max(green line) dimensions provided)* 47 | 48 | 49 | + `min` 50 | 51 | (要取两个尺寸值) 52 | 53 | {% raw %} 54 | 55 | {% image page.photo min-500x200 %} 56 | {% endraw %} 57 | 58 | **覆盖** 所给定的尺寸。 59 | 60 | 此图片过滤器将导致图片些许 **大于** 所制定的尺寸。对于一个方形的宽为 `2000` 高为`2000`的图片,在以`min-500x200`规则下,将令到其高度与宽度均为 `500` 像素,也就是要匹配调整规则的 *宽度*,但要所得图片高度要大于规则中的高度。 61 | 62 | ![图片过滤器 `min`](images/image_filter_min.png) 63 | 64 | *示例:图片将保持其宽高比,但将缩放到所能提供的最小尺寸(Example: The image will keep its proportions while filling at least the min(green line) dimensions provided)* 65 | 66 | + `width` 67 | 68 | (只取一个尺寸值) 69 | 70 | {% raw %} 71 | {% image page.photo width-640 %} 72 | {% endraw %} 73 | 74 | 将图片的宽度,缩放到所指定的尺寸。 75 | 76 | + `height` 77 | 78 | (只取一个尺寸值) 79 | 80 | {% raw %} 81 | {% image page.photo height-480 %} 82 | {% endraw %} 83 | 84 | 将图片的高度,缩放到所指定的尺寸。 85 | 86 | + `scale` 87 | 88 | (取一个百分数) 89 | 90 | {% raw %} 91 | {% image page.photo scale-50 %} 92 | {% endraw %} 93 | 94 | 将图片的高度,缩放到所指定的百分比。 95 | 96 | + `fill` 97 | 98 | (取两个尺寸值,以及一个可选的`-c`参数) 99 | 100 | {% raw %} 101 | {% image page.photo fille-200x200 %} 102 | {% endraw %} 103 | 104 | 将图片进行调整及 **裁剪**,以填充到所给定的精确尺寸。 105 | 106 | 此图片过滤器在那些需要任意图片的方形缩略图的网站上,尤为有用。比如对于一张宽度为 `2000` 高度为 `1000`的图片,在以 `fill-200x200`规则进行处理是,将把其高度缩小为 `200`,随后在将其宽度(此时为`400`)缩小到 `200`。 107 | 108 | 在设置了图片焦点时,该调整规则将裁剪到图片的焦点。在没有设置焦点是,将默认裁剪到图片的中心点。 109 | 110 | ![图片过滤器`fill`](images/image_filter_fill.png) 111 | 112 | *示例:图片将先被缩放,并被裁剪(红线),以取得所提供的尺寸下的尽可能大的区域(Example: The image is scaled and also cropped(red line) to fit as much of the image as possible within the provided dimensions)* 113 | 114 | ### 对于无法进行放大的图片 115 | 116 | 有可能带有`fill`尺寸下所请求的图片在没有放大的情况下,无法支持。比如一张带有 `fill-400x400 `图片过滤器请求的宽度 `400`高度`200`图片。在这种情况下, 将匹配 *所请求的`fill`的宽高比*,但尺寸将不再匹配。因此示例的 `400x200` 图片(比例为 `2:1`),将变为 `200x200`(比例为 `1:1`,匹配了该调整规则)。 117 | 118 | ### 更接近于焦点进行裁剪 119 | 120 | 默认下,Wagtail将改变图片的宽高比,到足够匹配到所给的调整规则的比列方式,进行裁剪。 121 | 122 | 但在某些情况下(比如缩略图),就更希望以更接近与图片焦点的方式,进行裁剪,以使得图片的主题更为突出一些。 123 | 124 | 那么可通过将`-c`追加到调整规则的后面,实现此目的。比如可通过加入`-c100`,而将图片以更可能靠近其焦点的方式,进行裁剪: 125 | 126 | {% raw %} 127 | {% image page.photo fille-200x200-c100 %} 128 | {% endraw %} 129 | 130 | 这就会将图片以尽可能多的方式进行裁剪,而不会裁剪到焦点中。 131 | 132 | 如发现 `-c100` 过于靠近焦点,那么可尝试 `-c75` 或 `-c50`。`-c`可接受任何从 `0` 到 `100` 的整数。 133 | 134 | ![有焦点的图片`fill`过滤器](images/image_filter_fill_focal.png) 135 | 136 | *示例:焦点被设置为了偏离中心点,因此该图片被缩放了,同时像`fill`那样进行了裁剪,但裁剪的中心点已被防止在了靠近焦点的地方(Example: The focal point is set off centre so the image is scaled and also cropped like fill, however the center point of the crop is positioned closer the focal point)* 137 | 138 | ![带有`-c75` 的 `close` 参数的焦点图片`fill`过滤器](images/image_filter_fill_focal_close.png) 139 | 140 | *示例:在设置了 `-c75` 之后,最终裁剪将更靠近焦点(Example: With `-c75` set, the final crop will be closer to the focal point)* 141 | 142 | + `original` 143 | 144 | (不取尺寸) 145 | 146 | 147 | {% raw %} 148 | {% image page.photo original %} 149 | {% endraw %} 150 | 151 | 将图片以其原始尺寸渲染出来。 152 | 153 | > **注意** Wagtail并不允许对图片做变形或拉伸处理。图片尺寸比例将始终保持不变。Wagtail 也 *不支持图片的放大*。若强制以较大尺寸来显示较小的图片,那么将以其原生尺寸进行 “最大化(max out)”。 154 | 155 | 156 | ## 对`img`标签的更多控制 157 | 158 | Wagtail提供了两个赋予到对`img`元素更多控制的捷径: 159 | 160 | ### 1. 给 `{% image %}` 标签加上属性 161 | 162 | 可使用`attribute="value"` 语法,来制定一些额外的属性: 163 | 164 | ```html 165 | {% image page.photo width-400 class="foo" id="bar" %} 166 | ``` 167 | 168 | 通过此种方法,可设置更具相关性的 `alt` 属性,来覆写自动从图片标题生成的那个`alt`值。在必要的情况下,图片的 `src`、`width`及`height`都可以进行覆写。 169 | 170 | ### 2. 从图片生成图片模板变量,从而访问其诸多属性 171 | 172 | **Generating the image "as foo" to access individual properties** 173 | 174 | ```html 175 | {% image page.photo width-400 as tmp_photo %} 176 | 177 | {{ tmp_photo.alt }} 179 | ``` 180 | 181 | 此种语法将所采用的图片转写(the underlying image Redition, `tmp_photo`),暴露给开发者。而“转写”则包含了所请求的、使用调整规则对图片进行格式化的特定信息,也就是尺寸与图片的源URL。 182 | 183 | 在站点使用 `AstractImage` 定义了一种定制图片模型时,那么添加到某个图片的所有额外字段(比如版权所有者),都 **不会** 包含在转写中。 184 | 185 | 因此就要将一个 `author` 字段,加入到上面示例中的 `AbstractImage`,而通过`{{ page.photo.author }}`,而非`{{ tmp_photo.author }}` 来访问该字段。 186 | 187 | (由于转写图片与其父图片在数据库中的链接的原因,也 **可** 以`{{ tmp_photo.image.author }}`来访问该字段,但这样降低了代码的可读性) 188 | 189 | > **注意** 图片用于`img`元素 `src` 属性的对象属性,实际上是`image.url`,而非`image.src`。 190 | 191 | 192 | ## 关于 `attrs` 捷径 193 | 194 | **The `attrs` shortcut** 195 | 196 | 也可使用`attrs` 属性,作为一并输出`src`、`width`、`height`与`alt`等属性的快捷方式: 197 | 198 | ```html 199 | 200 | ``` 201 | 202 | ## 嵌入到富文本中的图片 203 | 204 | 以上的信息,与经由模型中定义的、特定于图片的字段有关。然而图片还可以通过页面编辑器,嵌入到富文本中的任意位置(参考[富文本(HTML)](advanced_topics/customisation/page_editing_interface.html#rich-text))。 205 | 206 | 嵌入到富文本中的图片,就不能由模板开发者所轻易控制到了。对于这样的图片,没有可供处理的图片对象,因此不能使用`{{ image }}`模板标签。而是由站点编辑,在要文本编辑框中插入图片的地方,从一些图片“格式”中选取图片。 207 | 208 | Wagtail带有三种预定义的图片格式,但开发者可以Python方式,定义更多的图片格式。这三种格式如下: 209 | 210 | + `Full width` 211 | 212 | 该格式使用`width-800`创建一个图片的撰写,并赋予了该格式下``标签 `full-width` 的CSS类 213 | 214 | + `Left-aligned` 215 | 216 | 该格式使用`width-500`创建一个图片的转写,并赋予了该格式下``标签 `left` 的CSS类 217 | 218 | + `Right-aligned` 219 | 220 | 221 | 该格式使用`width-500`创建一个图片的撰写,并赋予了该格式下``标签 `right` 的CSS类 222 | 223 | > **注意** 所添加到图片的CSS类, **并未** 相应样式表,或内联样式与其对应。比如默认 `left` 类就不会生效。开发者需要将这些类加入到他们的前端 CSS文件中,以准确地定义他们想要`left`、`right`或`full-width`意味着什么。 224 | 225 | 有关更多有关图片格式的信息,包括如何建立自己的图片格式,请参阅[富文本中的图片格式](advanced_topics/customisation/page_editing_interface.html#rich-text-image-formats)。 226 | 227 | 228 | ## 输出图片的格式 229 | 230 | 在对图片加以处理时,Wagtail可能会自动地改变某些图片的格式: 231 | 232 | + PNG与JPEG的图片不会改变格式 233 | + 不带有动画的GIF图片,被装换成PNG格式 234 | + BMP的图片,被转换成PNG格式 235 | 236 | 也可通过在调整规则之后,使用`format`过滤器,在每个图片基础上,覆写输出格式的设定。 237 | 238 | 比如使用`format-jpeg`来令到`image`标签总是将图片转换成JPEG: 239 | 240 | ```html 241 | {% image page.photo width-400 format-jpeg %} 242 | ``` 243 | 244 | 也可使用`format-png`或`format-gif`。 245 | 246 | 247 | ## 图片背景色 248 | 249 | PNG与GIF图片两种格式,都支持透明,但在打算将图片转换成JPEG格式时,透明部分就需要使用某种固定的背景色来替换。 250 | 251 | 默认下Wagtail会将背景色设为白色。但如果白色背景不能满足设计是,可使用`bgcolor`过滤器,指定某种颜色。 252 | 253 | 此过滤器仅取一个参数,为CSS的3位或6位的十六进制代码,表示想要使用的颜色: 254 | 255 | ```html 256 | {# 将图片背景色设为黑色 #} 257 | {% image page.photo width-400 bgcolor-000 format-jpeg %} 258 | ``` 259 | 260 | ## JPEG图片的质量 261 | 262 | Wagtail的JPEG图片质量默认设置为了 `85`(已经相当高了)。对此可以全局地修改或基于各个标签进行修改。 263 | 264 | ### 全局修改JPEG图片质量 265 | 266 | 在 `settings.py`中使用`WAGTAILIMAGES_JPEG_QUALITY`设置,来改变全局默认JPEG质量: 267 | 268 | ```python 269 | # settings.py 270 | 271 | # 生成低质量但更小的图片 272 | WAGTAILIMAGES_JPEG_QUALITY = 40 273 | ``` 274 | 275 | 请注意此种修改不会影响所有先前已经生成的图片,因此可能要删除所有图片的转写,从而以新的设置重新生成这些图片转写。而这可以从Django的命令行完成: 276 | 277 | ```bash 278 | # 如使用了其他定制的图片转写模型,那么就要使用该图片转写模型 279 | >>> from wagtail.images.models Rendition 280 | >>> Rendition.objects.all().delete() 281 | ``` 282 | 283 | ### 依各个标签进行修改 284 | 285 | 通过使用`jpegquality`过滤器,也可在单个标签基础上,有着不同的JPEG质量。这样总是会覆写默认设置: 286 | 287 | ```html 288 | {% image page.photo width-400 jpegquality-40 %} 289 | ``` 290 | 291 | 请注意这不会在PNG或GIF文件上生效。在想要所有文件都成为低质量,就要将该过滤器与`format-jpeg`一起使用(这强制将所有图片,输出为JPEG格式)。 292 | 293 | ```html 294 | {% image page.photo width-400 format-jpeg jpegquality-40 %} 295 | ``` 296 | 297 | ## 以Python方式生成图片的转写 298 | 299 | 上面提到所有的图片转换,都可以直接以Python代码方式使用。请参阅[以Python方式生成图片](advanced_topics/images/renditions.html#image-renditions)。 300 | -------------------------------------------------------------------------------- /advanced_topics/customisation/rich_text_internals.md: -------------------------------------------------------------------------------- 1 | # 富文本内部元素 2 | 3 | 初一看Wagtail的富文本功能,貌似给到了站点编辑对一块HTML内容的完全控制。而事实上,给予站点编辑一个相对最终的HTML输出,已移除了多个步骤的富文本内容的一种表示,是有必要的,理由如下: 4 | 5 | + 编辑界面需要将那些不期望的标签过滤掉;这些不期望的标签包括恶意脚本、从外部字处理软件粘贴来的字体样式,以及那些可能破坏站点设计的有效性或一致性的元素(比如,通常页面会保留`

    `作为页面标题,因此允许用户经由富文本而插入他们自己额外的`

    `元素就不恰当了) 6 | 7 | + 富文本字段可指定`features`参数,来进一步限制在该字段中允许使用的元素(参见 [限制富文本字段中的功能](page_editing_interface.md#rich-text-feature)) 8 | 9 | + 强制使用HTML的子集,有助于将呈现类标记符号排除在数据库之外,从而令到站点更易于维护,且令到对站点内容的重新规划更为容易(包括潜在的生成诸如 [LaTeX](https://www.latex-project.org/) 这类非HTML的输出) 10 | 11 | + 诸如页面链接及图片这样的元素,需要保留像是页面或图片ID这类元数据,这些元数据在最终HTML呈现中是不会出现的 12 | 13 | 这就要求该富文本内容经历一系列的验证与转换步骤;这些验证与转换步骤,不仅存在于编辑界面与数据库中保存的版本之间,也存在于数据库的表示与最终渲染出的HTML之间。 14 | 15 | 因为这个原因,对Wagtail的富文本处理过程进行扩展,以支持到新的元素,远非简单地讲一句“开启`
    `元素”(举个例子)那么简单,因为Wagtail的多个组件 -- 同时涉及客户端与服务端 --都需要就如何处理那个功能达成一致,包括其如何暴露在编辑界面中、在数据库中应如何表示,以及在前端渲染时应如何被翻译出来(在功能适合渲染时)。 16 | 17 | Wagtail中富文本处理过程涉及到的组件有下面这些。 18 | 19 | ## 数据格式 20 | 21 | 富文本数据(由 [`RichTextField`](page_editing_interface.md#rich-text) 进行处理,而在 [`StreamField`](https://wagtail.xfoss.com/topics/streamfield.html) 中则是由 `RichTextBlock`进行处理),是以类似于HTML,但并不完全相同的方式,存储于数据库中的。比如到某个页面的链接,可能被存储为: 22 | 23 | ```html 24 |

    联络我们 获取更多信息。

    25 | ``` 26 | 27 | 这里的`linktype`属性,表示了对该标签进行重写应使用的规则。在经由`|richtext`过滤器(参见 [富文本(过滤器)](https://wagtail.xfoss.com/topics/writing_templates.html#rich-text-filter))在某个模板上进行渲染时,这就会被转换成为一个有效的HTML: 28 | 29 | ```html 30 |

    联系我们 获取更多信息。

    31 | ``` 32 | 33 | 在`RichTextBlock`情况下,块的值为一个`RichText`对象,在作为一个字符串而渲染时,将自动完成此转换,因此过滤器`|richtext`就无必要了。 34 | 35 | 与此类似,富文本内容中的某个图片,将像下面这样存储起来: 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | 渲染时这将渲染为一个``元素: 42 | 43 | ```html 44 | A pied wagtail 45 | ``` 46 | 47 | 再次,这里的`embedtype`属性表示了重写该标签所用的规则。除了``与``之外的所有其他标签,在转换后的HTML中都将保持不变。 48 | 49 | 有着一些应用到``与``的约束,他们的作用是实现经由字符串替换,而有效地完成转换: 50 | 51 | + 标签名称与属性必须是小写的 52 | + 属性值必须用双引号括起来 53 | + `embed`元素必须使用XML的自闭标签语法(XML self-closing tag syntax, 也就是以`/>`,而非``结束) 54 | + 属性值中仅允许这些HTML实体符号:`<`、`>`、`&`与`"` 55 | 56 | ## 功能注册 57 | 58 | 项目中的所有应用,都可定义对Wagtail的富文本处理过程的扩展,比如新的`linktype`与`embedtype`规则。名为 _功能注册_ 的对象,是作为有关富文本应如何运作的核心事实来源而加以提供的(An object known as the _feature registry_ serves as a central source of truth about how rich text should behave)。该对象可通过`register_rich_text_features`钩子进行访问,而该钩子是在启动时进行调用的,调用时会收集与富文本有关的所有定义: 59 | 60 | ```python 61 | # my_app/wagtail_hooks.py 62 | 63 | from wagtail.core import hooks 64 | 65 | @hooks.register('register_rich_text_features') 66 | def register_my_feature(features): 67 | # 这里将新的定义,添加到 “features” 68 | ``` 69 | 70 | ## 关于重写处理器 71 | 72 | 重写处理器,是一些知道如何将富文本标签的内容,诸如``与``,转换为前端HTML的类。比如`PageLinkHandler`类,就知道怎样将富文本标签``,转换为HTML标签``。 73 | 74 | 重写处理器还能提供到其他一些有关富文本标签的信息。比如对于一个给定的适当标签,`PageLinkHandler`就可用于提取出将引用哪个页面的信息。这对于可能需要那些富文本中所引用对象信息的下游代码,将是有用的。 75 | 76 | 可创建对之前定制的新`linktype`与`embedtype`进行支持的重写处理器。新的处理器必须是继承自`wagtail.core.richtext.LinkHandler`或`wagtail.core.richtext.EmbedHandler`的Python类。新的类应至少对下面的部分方法进行重写(这里列出的时`LinkHandler`的方法,不过`EmbedHandler`的这些方法也有着相同的签名): 77 | 78 | [`class LinkHandler`](#LinkHandler) 79 | 80 | + `identifier` 81 | 82 | 必需的。`identifier`属性是一个表示哪个富文本标签应由该处理器进行处理的字符串。 83 | 比如`PageLinkHandler.get_identifier`返回的字符串就是`"page"`,表明所有有着``的富文本标签,都将由其加以处理。 84 | 85 | + `expand_db_attributes(attrs)` 86 | 87 | 必需的。方法`expand_db_attributes`期望从某个数据库的富文本``标签(对于`EmbedHandler`就是``标签)取得一个属性的字典,并使用该字典来生成有效的前端HTML。 88 | 比如`PageLinkHandler.expand_db_attributes`将收到`{'id': 123}`,将使用其来获取有着 ID `123`的页面,并渲染出一个到该页面URL链接,类似于``。 89 | 90 | + `get_model()` 91 | 92 | 可选的。该静态`get_model()`方法仅用在那些用来渲染与Django模型有关的内容。此方法允许处理器将那些知道如何进行处理的内容的类型暴露出来(this method allows handlers to expose the type of content that they know how to handle)。 93 | 比如`PageLinkHandler.get_model`将返回Wagtail类`Page`。 94 | 95 | 那些与Django模型无关的处理器,可将此方法留为未定义状态,此时如对其进行调用,则将引发`NotImplementedError`错误。 96 | 97 | + `get_instance(attrs)` 98 | 99 | 可选的。该静态或类方法(classmethod)`get_instance`同样只应用于那些用来渲染与Django模型有关的处理器。该方法期望获取一个来自数据库的富文本``标签(或`EmbedHandler`的``标签)的属性字典,并使用该字典来返回特定所引用的Django模型实例。 100 | 101 | 比如`PageLinkHandler.get_instance`就将收到`{'id': 123}`,并返回Wagtail的有着ID `123`的`Page` 类的实例。 102 | 103 | 在该方法未定义时,该方法的一个默认实现,将查询由使用提供的`id`属性下,`get_model`方法所返回的类上的`id`模型字段(if left undefined, a default implementation of this method will query the `id` model field on the class returned by `get_model` using the provided `id` attribute);在自己的处理器中,如打算使用其他模型字段,则可对此默认行为进行覆写。 104 | 105 | 以下是一个定制的重写处理器的示例,其中实现了加入对用户邮箱地址的富文本链接支持的上述方法。该重写处理器支持对类似``这样的富文本标签,到类似``的有效HTML的转换。此示例假定等价的前端功能已被添加,从而允许用户将这类链接插入到富文本编辑器中。 106 | 107 | ```python 108 | from django.contrib.auth import get_user_model 109 | from wagtail.core.rich_text import LinkHandler 110 | 111 | class UserLinkHandler(LinkHandler): 112 | 113 | identifier = 'user' 114 | 115 | @staticmethod 116 | def get_model(): 117 | return get_user_model() 118 | 119 | @classmethod 120 | def get_instance(cls, attrs): 121 | model = cls.get_model() 122 | return model.objects.get(username=attrs('username')) 123 | 124 | @classmethod 125 | def expand_db_attributes(cls, attrs): 126 | user = cls.get_instance(attrs) 127 | return '' % user.email 128 | ``` 129 | 130 | ## 注册重写处理器 131 | 132 | 重写处理器也必须通过`register_rich_text_features`钩子,使用功能注册而加以注册。对于链接处理器与嵌入处理器的注册,分别提供了独立的方法。 133 | 134 | + [`FeatureRegistry.register_link_type(handler)`](#FeatureRegistry.register_link_type) 135 | 136 | 该方法允许对一个派生自`wagtail.core.rich_text.LinkHandler`的定制处理器进行注册,并将其加入到富文本转换期间的链接处理器清单中。 137 | 138 | ```python 139 | # my_app/wagtail_hooks.py 140 | 141 | from wagtail.core import hooks 142 | from my_app.handlers import MyCustomLinkHandler 143 | 144 | @hooks.register('register_rich_text_features') 145 | def register_link_handler(features): 146 | features.register_link_type(MyCustomLinkHandler) 147 | ``` 148 | 149 | 虽然Wagtail内建的`external`与`email`链接并没有预定义的`linktype`,但为他们定义链接重写处理器也是可能的。比如在出于搜索引擎优化目的,而想要外部链接有着一个`rel="nofollow"`属性时: 150 | 151 | ```python 152 | from django.utils.html import escape 153 | from wagtail.core import hooks 154 | from wagtail.core.rich_text import LinkHandler 155 | 156 | class NoFollowExternalLinkHandler(LinkHandler): 157 | identifier = 'external' 158 | 159 | @classmethod 160 | def expand_db_attributes(cls, attrs): 161 | 162 | href = attrs["href"] 163 | return '' % escape(href) 164 | 165 | @hooks.register('register_rich_text_features') 166 | def register_external_link(features): 167 | features.register_link_type(NoFollowExternalLinkHandler) 168 | ``` 169 | 170 | 与此类似,可使用`email`的链接类型,来加入一个定制的用于邮箱链接的重写处理器(比如用来对富文本中的电邮进行混淆)。 171 | 172 | 173 | + [`FeatureRegistry.register_embed_type(handler)`](#FeatureRegistry_register_embed_type) 174 | 175 | 此方法允许注册一个派生自`wagtail.core.rich_text.EmbedHandler`的定制处理器,并将其添加到富文本装换期间可用的嵌入处理器清单。 176 | 177 | ```python 178 | # my_app/wagtail_hooks.py 179 | 180 | from wagtail.core import hooks 181 | from my_app.handlers import MyCustomEmbedHandler 182 | 183 | @hooks.register('register_rich_text_features') 184 | def register_embed_handler(features): 185 | features.register_embed_type(MyCustomEmbedHandler) 186 | ``` 187 | 188 | _版本 2.5 中的新功能_:在之前的版本中,`register_link_type`与`register_embed_type`都接受两个参数:一个时链接或嵌入类型标识符,以及一个用于执行重写的函数(与`expand_db_attributes`方法等价)。 189 | 190 | ## 编辑器小部件 191 | 192 | 用于富文本字段的编辑器界面,可以`WAGTAILADMIN_RICH_TEXT_EDITORS`设置项而加以配置。Wagtail提供了两个编辑器实现:`wagtail.admin.rich_text.DraftailRichTextArea`(也就是基于 [Draft.js](https://draftjs.org/) 的 [Draftail](https://www.draftail.org/) 编辑器),以及`wagtail.admin.rich_text.HalloRichTextArea`(已放弃,基于 [Hallo.js](http://hallojs.org/))。 193 | 194 | 创建自己的富文本编辑器实现是可能的。在最小状态,一个富文本编辑器就是一个Django的`Widget`子类,其构造器接受一个`options`关键字参数(一个特定于编辑器的配置选项的字典,其来源为`WAGTAILADMIN_RICH_TEXT_OPTIONS`中的`OPTIONS`字段),同时其要消费与制造上面所讲到的类HTML格式中的字符串数据(and which consumes and produces string data in the HTML-like format described above)。 195 | 196 | 通常一个富文本部件也会接收一个自`RichTextField`/`RichTextBlock`或`WAGTAILADMIN_RICH_TEXT_EDITORS`的`features`选项,传递过来的`features`的清单,该清单定义了在该编辑器的那个实例中可用的功能(参见 [对富文本字段中的功能进行限制](page_editing_interface.md#rich-text-features))。要开启功能支持,就对小部件类上的`accepts_features = True`属性做设置;随后小部件构造器将以关键字参数`features`接收到功能清单。 197 | 198 | 在 [限制富文本字段中的功能](page_editing_interface.md#rich-text-features) 下列出的一套标准的可识别功能标识符,但这并不是一个最终清单;功能标识符并不仅是依照惯例进行定义的,还取决于各个编辑器小部件而确定那些功能将为这些小部件所识别,并据此适应其行为。单个编辑器部件可能实现相较于默认功能集更少或更多的功能,既可作为内建功能形式,或在编辑器部件有着某种插件机制时,以插件的方式实现。 199 | 200 | 比如某个第三方的Wagtail扩展要引入`table`作为一种新的富文本元素,并提供了Draftail与Hallo编辑器(二者都提供了插件机制)的实现。在此情况下,该第三方扩展将对定制的编辑器部件一无所知,因此该部件就不知道如何处理`table`功能标识符。编辑器部件将静默地忽略所有他们不能识别的功能标识符。 201 | 202 | 功能注册表中的`default_features`属性,是一个在`RichTextField`/`RichTextBlock`或`WAGTAILADMIN_RICH_TEXT_EDITORS`中没有给出显式的功能清单时,使用到的功能标识符清单。该清单可在`register_rich_text_features`钩子中加以修改,以令到新的功能得以默认开启,并通过调用`get_default_features()`而获取到。 203 | 204 | ```python 205 | @hooks.register('register_rich_text_features') 206 | def make_h1_default(features): 207 | features.default_features.append('h1') 208 | ``` 209 | 210 | 在`register_rich_text_features`钩子之外 -- 比如在某个部件类中 -- 该功能注册表可作为 `wagtail.core.rich_text.features` 对象而加以导入。下面是带有功能支持的富文本编辑器的可行起点: 211 | 212 | ```python 213 | from django.forms import widgets 214 | from wagtail.core.rich_text import features 215 | 216 | class CustomRichTextArea(widgets.TextArea): 217 | accepts_features = True 218 | 219 | def __init__(self, *args, **kwargs): 220 | self.options = kwargs.pop('options', None) 221 | 222 | self.features = kwargs.pop('features', None) 223 | if self.features is None: 224 | self.features = features.get_default_features() 225 | 226 | super().__init__(*args, **kwargs) 227 | ``` 228 | 229 | ## 编辑器插件 230 | 231 | 232 | + `FeatureRegistry.register_editor_plugin(editor_name, feature_name, plugin_definition)` 233 | 234 | 富文本编辑器通常提供了某种插件机制,以允许对编辑器扩展出新的功能。`register_editor_plugin`方法就为`register_rich_text_features`钩子提供了一种标准化的方法,用于在某个富文本功能开启时将拉入到编辑器的插件的定义。 235 | 236 | `register_editor_plugin`所传入的编辑器名称(用于对编辑器部件进行标识的一个唯一字符串 -- Wagtail使用`draftail`与`hallo`作为其内建的编辑器)、功能标识符及插件定义对象。该对象时特定于编辑器部件的,并可以为任意值,通常会包含一个引用了该插件的JavaScript代码的 [Django的表单媒体](https://docs.djangoproject.com/en/stable/topics/forms/media/)定义,JavaScript代码随后就将被融入到编辑器部件自己的媒体定义中 -- 以及在编辑器实例化时要进行传递的全部相关配置选项。 237 | 238 | 239 | + `FeatureRegistry.get_editor_plugin(editor_name, feature_name)` 240 | 241 | 在编辑器部件内,某个给定功能的插件定义可通过`get_editor_plugin`方法获取到,传入的是编辑器自身的标识符与功能的标识符。在没有已注册的匹配插件时,其将返回`None`。 242 | 243 | 有关Wagtail内建编辑器插件格式的详细信息,请参见 [对Draftail编辑器进行扩展](extending_draftail.md) 与 [对Hallo编辑器进行扩展](extending_hallo.md)。 244 | 245 | ## 关于格式转换器 246 | 247 | 编辑器部件通常无法直接工作于Wagtail的富文本格式上,从而需要转换为其自己的原生格式。对于Draftail,原生格式为一种基于JSON的、叫做 `ContentState`(参见 [Draft.js是如何表示富文本数据的](https://medium.com/@rajaraodv/how-draft-js-represents-rich-text-data-eeabb5f25cf2))的格式。Hallo.js与其他编辑器则是基于HTML 的`contentEditable`机制,此机制要求有效的HTML,因此Wagtail使用了一种被称为“编辑器HTML”的约定,在这种约定中,链接及嵌入元素所需要的额外数据,保存在`data-`属性中,比如: 248 | 249 | ```html 250 | 联系我们 251 | ``` 252 | 253 | Wagtail提供了两个工具类,`wagtail.admin.rich_text.converters.contentstate.ContentstateConverter`与`wagtail.admin.rich_text.converters.editor_html.EditorHTMLConverter`,用于完成富文本格式与编辑器原生格式之间的转换。这些类是独立于各个编辑器部件的,且在将富文本渲染到模板上时,也是根据所发生的重写过程而有所区别。 254 | 255 | 两个类都接受一个`features`清单,作为他们的构造器的参数,并实现以下两个方法:方法`from_database_format(data)`将Wagtail的富文本数据转换到编辑器的格式,方法`to_database_format(data)`,将编辑器数据转换到Wagtail的富文本格式。 256 | 257 | 对于编辑器的插件,转换器类的行为可根据传递给他的功能清单,而有所不同。尤其是他可以执行白名单规则,来确保输出仅包含那些对应于当前活动功能集的HTML元素。功能注册表提供了`register_converter_rule`方法,来允许`register_rich_text_features`钩子,定义出在开启了给定功能时将要激活的转换规则。 258 | 259 | 260 | + `FeatureRegistry.register_converter_rule(converter_name, feature_name, rule_definition)` 261 | 262 | 传递给`register_converter_plugin`的是一个转换器名称(对转换器类进行唯一标识的一个字符串 -- Wagtail使用了`contentstate`与`editorhtml`两个标识符)、一个功能标识符与一个规则定义对象。该对象是特定于转换器的,且可以是任意值。 263 | 264 | 有关`contentstate`与`editorhtml`两个转换器的规则定义格式的详细信息,请分别参阅 [对Draftail编辑器进行扩展](extending_draftail.md) 与 [对Hallo编辑器进行扩展](extending_hallo.md)。 265 | 266 | 267 | + `FeatureRegistry.get_converter_rule(converter_name, feature_name)` 268 | 269 | 在某个转换器类的内部,可通过`get_converter_rule`方法,获取到给定特性的规则定义,传递给该方法的参数为转换器自身的标识符字符串与功能标识符。在匹配的规则未有注册时,该方法将返回`None`。 270 | -------------------------------------------------------------------------------- /advanced_topics/customisation/extending_draftail.md: -------------------------------------------------------------------------------- 1 | # 对Draftail编辑器进行扩展 2 | 3 | Wagtail的富文本编辑器是利用 [Draftail](https://www.draftail.org/) 构建的,同时其功能可通过插件加以扩展。 4 | 5 | 插件有三种类型: 6 | 7 | + 内联样式 -- 用于对行的一部分进行格式化,比如`bold`、`italic`、`monospace` 8 | + 块 -- 用于表示内容的结构,比如`blockquote`、`ol` 9 | + 实体 -- 用于输入一些附加数据/元数据,比如`link`(带有URL)、`image`(带有文件) 10 | 11 | 所有这些插件,都是在一个类似基线上创建出来的,下面就以一个最简单的示例 -- 一个用于内联样式`mark`的定制功能 --来演示插件的创建。将下面的代码放入任意已安装应用中的`wagtail_hooks.py`中: 12 | 13 | ```python 14 | import wagtail.admin.rich_text.editors.draftail.features as draftail_features 15 | from wagtail.admin.rich_text.converters.html_to_constentstate import InlineStyleElementHandler 16 | from wagtail.core import hooks 17 | 18 | # 1. 使用 register_rich_text_features 钩子 19 | @hooks.register('register_rich_text_features') 20 | def register_makr_feature(features): 21 | """ 22 | 对`mark`功能进行注册,该功能使用了 Draft.js 的 `MAKR` 内联样式类型 23 | 并是作为HTML的 `` 标签进行存储的 24 | """ 25 | 26 | feature_name = 'mark' 27 | type_ = 'MARK' 28 | tag = 'mark' 29 | 30 | # 2. 就Draftail如何在其工具栏中处理该功能,而对其进行配置 31 | control = { 32 | 'type': type_, 33 | 'label': '☆', 34 | 'description': 'Mark', 35 | # 下面的这个属性实际上并不需要 -- Draftail已经有着预定义MARK样式 36 | # 'style': {'textDecoration': 'line-through'}, 37 | } 38 | 39 | # 3. 调用 register_editor_plugin 来对 Draftail的这些配置进行注册 40 | features.register_editor_plugin( 41 | 'draftail', feature_name, dratail_features.InlineStyleFeature(control) 42 | ) 43 | 44 | # 4. 对从数据库到编辑器及反过来的内容装换进行配置 45 | db_conversion = { 46 | 'from_database_format': {tag: InlineStyleElementHandler(type_)}, 47 | 'to_database_format': {'style_map': {type_: tag}}, 48 | } 49 | 50 | # 5. 调用 register_converter_rule,来对内容转换进行注册 51 | features.register_converter_rule('contentstate', feature_name, db_conversion) 52 | 53 | # 6. (可选的)将该功能加入到默认功能清单,以令到其在那些没有指定 54 | # 显式“功能”清单的富文本字段上可用 55 | features.default_features.append('mark') 56 | ``` 57 | 58 | 对于所有Draftail插件,这些步骤都是一样的。以下是一些要点: 59 | 60 | + 在各个地方,都要始终使用该功能的Draft.js类型或Wagtail的功能名称(Consistently use the feature's Draftail.js type or Wagtail feature names where approciate)。 61 | + 要给予到Draftail足够信息,其才知道如何产生出该功能的按钮,以及对其进行渲染的方式(有关这个问题后面会有更详尽的说明)。 62 | + 对转换方式加以配置,以使用正确的HTML元素(因为他们是存储在数据库中的)。 63 | 64 | 有关培训选项的详细信息,可前往 [Draftail文档](https://www.draftail.org/docs/formatting-options),查看所有的细节。以下是一些有关控件的要点: 65 | 66 | + `type`是唯一强制要求的信息。 67 | + `icon`、`label`与`description`一起,用于在工具栏中显示该控件 68 | + 控件的`icon`可以是使用带有CSS类的某种图标字体的字符串,比如说`'icon': 'fas fa-user',`。也可以是使用SVG路径或SVG符号引用时的字符串数组,比如`'icon': ['M100 100 H 900 V 900 H 100 Z'],`。SVG的路径需要设置为一个 `1024x1024` 的视框。 69 | 70 | ## 新内联样式的创建 71 | 72 | 除了此前的示例外,内联样式还要取得一个`style`属性,用于定义何种CSS规则将应用到编辑器中的文本。一定要阅读一下 [Draftail文档](https://www.draftail.org/docs/formatting-options)中有关内联样式的内容。 73 | 74 | 最后,数据库的导入/导出转换,使用了一个`InlineStyleElementHandler`,将给定的标签(上面示例中的`mark`)映射到某种Draftail类型,而逆向的映射则是使用 `style_map`的[Draft.js的输出器配置](https://github.com/springload/draftjs_exporter)完成的。 75 | 76 | ## 建立新块 77 | 78 | 块与内联样式一样简单: 79 | 80 | ```python 81 | from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler 82 | 83 | @hooks.register('register_rich_text_features') 84 | def register_help_text_feature(features): 85 | """ 86 | 对 `help-text` 功能进行注册,该功能使用了 Draft.js 的 `help-text` 块类型, 87 | 并是作为 `
    ` 标签进行存储的。 88 | """ 89 | 90 | feature_name = 'help-text' 91 | type_ = 'help-text' 92 | 93 | control = { 94 | 'type': type_, 95 | 'label': '?', 96 | 'description': '帮助文本', 97 | # 可选的,这里可以告诉Draftail在编辑器中显示这些块时,使用何种元素 98 | 'element': 'div', 99 | } 100 | 101 | features.register_editor_plugin( 102 | 'draftail', feature_name, draftail_features.BlockFeature(control, css={'all': ['help-text.css']}) 103 | ) 104 | 105 | features.register_converter_rule('contentstate', feature_name, { 106 | 'from_database_format': {'div.help-text': BlockElementHandler(type_)}, 107 | 'to_database_format': {'block_map': {type_: {'element': 'div', 'props': {'class': 'help-text'}}}}, 108 | }) 109 | ``` 110 | 111 | 以下时主要的不同: 112 | 113 | + 这里可配置一个`element`,来告诉Draftail如何在编辑器中渲染这些块。 114 | + 这里使用的是`BlockFeature`来注册插件。 115 | + 这里使用了`BlockElementHandler`与`block_map`来设置转换。 116 | 117 | 118 | 作为可选项,也可使用CSS类`Draftail-block--help-text`(`Draftail-block--`),来定义块的样式。 119 | 120 | 就是这样了!其余的复杂性,就在于需要编写CSS来赋予编辑器中块的样式了。 121 | 122 | 123 | ## 新实体的创建 124 | 125 | > __警告__ 这是一项高级特性。请再三考虑是否真的需要此特性。 126 | 127 | 实体就不再简单地是一些工具栏中的格式化按钮了。他们通常需要多得多的技能,需要与APIs进行通信或请求更多的用户输入(they(entities) usually need to be much more versatile, communicating to APIs or requesting further user input)。如此等等, 128 | 129 | + 有极大可能需要编写大量的JavaScript代码,其中一些还需要React(you will most likely need to wirte a __hefty dose of JavaScript__, some of it with React)。 130 | + API是非常底层的。因此有极大可能需要一些 Draft.js 的知识(the API is very __low-level__. You will most likely need some __Draftail.js knowledge__)。 131 | + 对富文本中的UIs进行定制非常艰难。要做好在不同浏览器中进行测试而花大量时间的准备(custom UIs in rich text can be brittle. Be ready to spend time __testing in multiple browsers__)。 132 | 133 | 好消息是有了这样的底层API后,就令到第三方Wagtail插件可以在富文本功能上进行创新,带来新的体验种类。但与此同时,请考虑经由 `StreamField` 特性来实现UI,因为对于Django开发者来说,他有着经历了实战考验的API。 134 | 135 | 以下为创建新实体特性的一些主要要求: 136 | 137 | + 与内联样式及块一样,要注册一个编辑器插件。 138 | + 编辑器插件必须注册一个`source`:一个负责在编辑器中创建新实体实例的、使用Draft.js API 的React组件。 139 | + 编辑器插件还需要一个`decorator`(用于内联实体)或`block`(用于块实体):一个负责在编辑器中显示出实体实例的React组件。 140 | + 与内联样式与块类似,要设置好到数据库及从数据的转换。 141 | + 该转换占了更大比重,因为实体包含了需要被序列化为HTML的数据。 142 | 143 | 关于React组件的编写,Wagtail将其自带的React、Draft.js与Draftail的依赖,作为全局变量加以暴露。有关此方面的详细信息,请参阅 [对客户端组件进行扩展](amdin_templates.md#extending-clientside-components)。而更深入的讨论,则请移步 [Draftail文档](https://www.draftail.org/docs/formatting-options) 以及 [Draftail导出器文档](https://www.draftail.org/docs/formatting-options)。 144 | 145 | 下面是一个详细示例,用来演示这些工具在Wagtail环境下的使用方式。为此示例目的,这里可设想某份金融报刊的新闻团队的情形。他们打算撰写有关股票市场的报道,在他们内容的任意位置对特定股票进行引用(比如在某个句子中引用 "$TSLA"),随后他们的报道中将自动完善该股票的信息(链接、数字、迷你图表等)。 146 | 147 | 编辑器工具栏将包含一个显示了可选股票清单的“股票选择器”,最后将用户的选择作为一个文本式代币进行插入。比如下面将仅随机选取一支股票: 148 | 149 | ![new-entity-in-draftail](images/draftail_entity_stock_source.gif) 150 | 151 | 在发布时,这些代币将保存在富文本中。而在网站上该新闻报道被显示出来时,则将会把来自某个API的实时的市场数据,插入到各个代币旁边: 152 | 153 | ![dratail-股票实体的渲染](images/draftail_entity_strock_rendering.png) 154 | 155 | 为了实现此特性,就要像内联样式与块那样,首先对此富文本特性进行注册: 156 | 157 | ```python 158 | @hooks.register('register_rich_text_features') 159 | def register_stock_feature(features): 160 | features.default_features.append('stock') 161 | 162 | """ 163 | 对该 `stock` 功能进行注册,这里使用了 `STOCK` 的 Draft.js 实体类型,同时 164 | 是以一个 ``的标签,作为HTML进行存储的。 165 | """ 166 | 167 | feature_name = 'stock' 168 | type_ = 'STOKC' 169 | 170 | control = { 171 | 'type': type_, 172 | 'lable': '$', 173 | 'description': '股票', 174 | } 175 | 176 | features.register_editor_plugin( 177 | 'draftail', feature_name, draftail_features.EntityFeature( 178 | control, 179 | js=['stock.js'], 180 | css={'all': ['stock.css']} 181 | ) 182 | ) 183 | 184 | features.register_converter_rule('contentstate', feature_name, { 185 | # 这里请注意,数据库转换要比内联样式及块更为复杂。 186 | 'from_database_format': {'span[data-stock]': StockEntityElementHandler(type_)}, 187 | 'to_database_format': {'entity_decorators': {type_, stock_entity_decorator}}, 188 | }) 189 | ``` 190 | 191 | `EntityFeature`上的 `js` 与 `css` 关键字,可用于指定额外的、于此功能出于活动状态时进行加载的JS与CSS文件。二者都是可选的。他们的值被添加到一个`Media`对象,有关这些对象的更多文档,在 [Django表单资源文档](https://docs.djangoproject.com/en/stable/topics/forms/media/) 中可以找到。 192 | 193 | 因为实体保有数据,因此到数据库及从数据库的转换格式,就要更为复杂。这里就必须创建这两个转换处理器: 194 | 195 | ```python 196 | from draftjs_exporter.dom import DOM 197 | from wagtail.admin.rich_text.converters.html_to_contentstate import InlineEntityElementHandler 198 | 199 | def stock_entity_decorator(props): 200 | """ 201 | Draft.js 的 ContentState 格式到数据库的HTML。 202 | 将 STOCK 实体,转换为一个 span 标签。 203 | """ 204 | 205 | return DOM.create_elment('span', { 206 | 'data-stock': props['stock'], 207 | }, props['children']) 208 | 209 | 210 | class StockEntityElementHandler(InlineEntityElementHandler): 211 | """ 212 | 数据库的HTML到Draft.js的ContentState格式。 213 | 将该 span 标签,以正确的数据,转换为一个 STOCK 实体。 214 | """ 215 | 216 | mutability = 'IMMUTABLE' 217 | 218 | def get_attribute_data(self, attrs): 219 | """ 220 | 从 "data-stock" HTML属性取得 `'stock'` 的值。 221 | """ 222 | 223 | return { 224 | 'stock': attrs['data-stock'], 225 | } 226 | ``` 227 | 228 | 注意这里他们是如何完成类似的转换,使用的却是不同的APIs. `to_database_format`是使用 [Draft.js 的导出器](https://github.com/springload/draftjs_exporter) 组件的 API 构建的,而 `from_database_format` 则使用了一个 Wagtail 的 API. 229 | 230 | 下一步就是将 JavaScript 进行添加,来定义出创建实体的方式(也就是 `source`)与其显示的方式(`decorator`)。在 `stock.js` 中,定义了数据源组件: 231 | 232 | ```javascript 233 | const React = window.React; 234 | const Modifier = window.DraftJS.Modifier; 235 | const EditorState = window.DraftJS.EditorState; 236 | 237 | const DEMO_STOCKS = ['AMD', 'AAPL', 'TWTR', 'TSLA', 'BTC']; 238 | 239 | // 这并非一个真正的 React 组件 -- 在实体被渲染时,尽可能快地创建出实体。 240 | class StockSorce extends React.Component { 241 | componentDidMount() { 242 | const { editorState, entityType, onComplete } = this.props; 243 | 244 | const content = editorState.getCurrentContent(); 245 | const selection = editorState.getSelection(); 246 | 247 | const randomStock = DEMO_STOCKS[Math.floor(Math.random() * DEMO_STOCKS.length)]; 248 | 249 | // 使用 Draft.js 的API、以正确的数据来创建一个新的实体。 250 | const contentWithEntity = content.createEntity(entityType.type, 'IMMUTABLE', { 251 | stock: randomStock, 252 | }); 253 | const entityKey = contentWithEntity.getLastCreatedEntityKey(); 254 | 255 | // 这里还要添加一些将要激活的实体的一些文本。 256 | const text = `${randomStock}`; 257 | 258 | const newContent = Modifier.replaceText(content, selection, text, null, entityKey); 259 | const nextState = EditorState.push(editorState, newContent, 'insert-characters'); 260 | 261 | onComplete(nextState); 262 | } 263 | 264 | render() { 265 | return null; 266 | } 267 | } 268 | ``` 269 | 270 | 此源数据组件,使用了由 [Draftail](https://www.draftail.org/docs/api) 所提供的数据与回调。其还使用了来自全局变量的依赖 -- 请参阅 [对客户端组件进行扩展](admin_templates.md#extending-clientside-components)。 271 | 272 | 随后创建出装饰器组件: 273 | 274 | ```javascript 275 | const Stock = (props) => { 276 | const { entityKey, contentState } = props; 277 | const data = contentState.getEntity(entityKey).getData(); 278 | 279 | return React.createElement('a', { 280 | role: 'button', 281 | onMouseUp: () => { 282 | window.open(`https://finance.yahoo.com/quote/${data.stock}`); 283 | }, 284 | }, props.children); 285 | }; 286 | ``` 287 | 288 | 这是一直白的 React 组件了。其并未使用 JSX, 因为这里不打算非得要为这些JavaScript代码而使用一个构建步骤。其使用了 ES6 的语法 -- 在Internet Explorer 11 中无法运行,除非使用一个构建步骤转换到 ES5 的语法。 289 | 290 | 最后将这些 JS组件注册到插件: 291 | 292 | ```javascript 293 | window.draftail.registerPlugin({ 294 | type: 'STOCK', 295 | source: StockSource, 296 | decorator: Stock, 297 | }); 298 | ``` 299 | 300 | 就是这样了!该项设置的所有代码,将在站点前端上最终生成以下的HTML: 301 | 302 | ```html 303 |

    304 | Anyone following Elon Mask's $TSLA should also look into $BTC. 305 |

    306 | ``` 307 | 308 | 为了最终完成该示例,这里可将一点 JavaScript 代码加入到前端,从而给这些代币装饰上一些链接与迷你图表: 309 | 310 | ```javascript 311 | [].slice.call(document.querySelectionAll('data-stock')).forEach((elt) => { 312 | const link = document.createElement('a'); 313 | link.ref = `https://finance.yahoo.com/quote${elt.dataset.stock}`; 314 | link.innerHTML = `${elt.innerHTML}`; 315 | 316 | elt.innerHTML = ''; 317 | elt.appendChild(link); 318 | }); 319 | ``` 320 | 321 | 也可以创建定制块(请参阅单独的 [Draftail 文档](https://www.draftail.org/docs/blocks)),但关于定制块的创建,这里不再赘述,因为在 Wagtail 中, [StreamField](https://wagtail.xfoss.com/topics/streamfield.html#streamfield) 是创建块级别富文本的最佳方式。 322 | 323 | 324 | ## Draftail小部件的集成 325 | 326 | 为更进一步对 Draftail 小部件集成到UI的方式加以定制,有着以下的 CSS与JS的额外扩展点: 327 | 328 | + 在 JavaScript中,使用 `[data-draftail-input]` 属性选择器,来选定带有数据的输入,并使用对封装了编辑器的元素,使用 `[data-draftail-editor-wrapper]` 属性(in JavaScript, use the `[data-dratail-input]` attributes selector to target the input which contains the data, and `[data-dratail-editor-wrapper]` for the element which wraps the editor)。 329 | + 编辑器实例是绑定在某个必将访问的输入字段上的。使用`document.querySelector('[data-draftail-input]').draftailEditor`(the editor instance is bound on the input field for imperative access. Use `document.querySelector('[data-draftail-input]').draftailEditor`)。 330 | + 在 CSS中,使用以`Draftail-`作为前缀的类(in CSS, use the classes prefixed with `Draftail-`)。 331 | -------------------------------------------------------------------------------- /topics/pages.md: -------------------------------------------------------------------------------- 1 | # 页面模型 2 | 3 | **Page models** 4 | 5 | Wagtail中的每种页面类型(又名“内容类型”),都是由一个Django的模型所表示的(each page type(a.k.a. content type) in Wagtail is represented by a Django model)。所有页面模型,都必须从`wagtail.core.models.Page`类继承。 6 | 7 | > **注** Django模型,就是一个类的数据结构,对应着数据库中的一个表。Python类与数据库之间存在一个对象关系模型的抽象,对页面的修改,经由 migrate 命令,最终反映到数据库中。 8 | 9 | 因为所有页面类型都是Django模型,那么就可以使用Django所提供的所有字段类型。请参阅[模型字段参考](https://docs.djangoproject.com/en/stable/ref/models/fields/),了解所有可用字段清单。此外,Wagtail还提供了一个`RichTextField`字段,该字段提供了一个可用于编辑富文本内容的所见即所得的编辑器。 10 | 11 | **关于Django的模型** 12 | 13 | 如对Django模型尚不熟悉,那么请快速浏览以下链接,以获得基本的掌握: 14 | 15 | + [创建模型](https://docs.djangoproject.com/en/stable/intro/tutorial02/#creating-models) 16 | + [模型的语法](https://docs.djangoproject.com/en/stable/topics/db/models/) 17 | 18 | 19 | ## Wagtail页面模型示例 20 | 21 | 下面的示例表示一个典型的博客文章: 22 | 23 | ```python 24 | from django.db import models 25 | 26 | from modelcluster.fields import ParentalKey 27 | 28 | from wagtail.core.models import Page, Orderable 29 | from wagtail.core.fields import RichTextField 30 | from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel 31 | from wagtail.images.edit_handlers import ImageChooserPanel 32 | from wagtail.search import index 33 | 34 | class BlogPost(Page): 35 | 36 | # 数据库字段 37 | 38 | body = RichTextField() 39 | date = models.DateField("发布日期") 40 | feed_image = models.ForeignKey( 41 | 'wagtailimages.Image', 42 | null=True, 43 | blank=True, 44 | on_delete=models.SET_NULL, 45 | related_name="+" 46 | ) 47 | 48 | # 搜索功能索引的配置 49 | 50 | search_fields = Page.search_fields + [ 51 | index.SearchField('body'), 52 | index.FilterField('date'), 53 | ] 54 | 55 | # 编辑器面板的配置 56 | 57 | content_panels = Page.content_panels + [ 58 | FieldPanel('date'), 59 | FieldPanel('body', classname="full"), 60 | InlinePanel('related_links', label="相关链接"), 61 | ] 62 | 63 | promote_panels = [ 64 | MultiFieldPanel(Page.promote_panels, "一般性页面配置"), 65 | ImageChooserPanel('feed_image'), 66 | ] 67 | 68 | # 父页面/子页面类型规则 69 | 70 | parent_page_types = ['blog.BlogIndex'] 71 | subpage_types = [] 72 | 73 | class BlogPageRelatedLink(Orderable): 74 | page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='related_links') 75 | name = models.CharField(max_length=255) 76 | url = models.URLField() 77 | 78 | panels = [ 79 | FieldPanel('name'), 80 | FieldPanel('url'), 81 | ] 82 | ``` 83 | 84 | > **要点** 请确保字段名称与类的名称保持不同。那会因为Django处理对象关系的机制([了解更多有关此方面的信息](https://github.com/wagtail/wagtail/issues/503)),而导致出错。在上面的示例中已经通过将`Page`追加到各个模型名称之后,避免了这个问题。 85 | 86 | 87 | ## 编写页面模型 88 | 89 | 这里将对上面的实力中的各个部分进行逐一讲解,以帮助建立自己的页面模型。 90 | 91 | 92 | ### 数据库字段 93 | 94 | 每种Wagtail页面类型,都是一个Django模型,在数据库中表现为单个表。 95 | 96 | 每个页面类型,都可以有他自己的一套字段。比如一篇新闻报道可能具有文章主题文本与发布日期,而某项活动则可能需要单独的活动地点及开始/结束时间字段。 97 | 98 | 在Wagtail中,可以使用所有的Django字段类。同时由第三方应用所提供的绝大多数字段类同样可以工作。 99 | 100 | Wagtail还提供了一些他自身的字段类: 101 | 102 | + `RichTextField` -- 用于富文本内容 103 | + `StreamField` -- 一种基于块的内容字段(参见:[使用`StreamField`的自由格式页面内容](topics/streamfield.md)) 104 | 105 | 对于标签化特性,Wagtail完全支持[django-taggit](https://django-taggit.readthedocs.org/en/latest/),因此建议使用该现成的库。 106 | 107 | 108 | ## 搜索功能 109 | 110 | 页面模型中的`search_fields`属性,定义了哪些字段要被加入到搜索索引,以及他们如何被索引。 111 | 112 | 该属性影视一个`SearchField`与`FilterField`对象的清单。其中`SearchField`将某个字段加入到全文搜索。`FilterField`则是将某个字段加入到结果过滤中(`FilterField` adds a field for filtering the results)。字段可被`SearchField`与`FilterField`同时索引(但只能每个索引只能有一个实例)。 113 | 114 | 在上面的示例中,将`body`字段进行了全文搜索索引,将`date`字段做了过滤索引。 115 | 116 | 这些字段类型所接受的参数,在[对额外字段进行索引](topics/search/indexing.md#wagtailsearch-indexing-fields)中进行了文档说明。 117 | 118 | ### 编辑器面板 119 | 120 | 对于页面的这些字段如何在页面编辑器界面的安排,有着几个属性: 121 | 122 | + `content_panels` -- 用于内容,比如文章主题文本 123 | + `promote_panels` -- 用于元数据,比如标签、缩略图及搜索引擎优化的标题等 124 | + `settings_panels` -- 用于相关设置,比如发布日期等 125 | 126 | 每个这些属性,都被设置为了一个`EditHandler`对象的清单,该清单定义了哪些字段将出现在哪个分页上,以及这些字段在各个分页上的结构方式。 127 | 128 | 下面是这些Wagtail自带的`EditHandler`类的简介。请参阅[可用的面板类型](reference/pages/panels.md),了解他们的完整介绍。 129 | 130 | ### 基本面板(Basic) 131 | 132 | 基本面板实现模型字段的编辑。`FieldPanel`类会基于字段类型,来选择适当的小部件,而`StreamField`字段则就需要使用特殊面板类。 133 | 134 | + `FieldPanel` 135 | + `StreamFieldPanel` 136 | 137 | ### 结构化面板(Structural) 138 | 139 | 这类面板用于对界面中的字段进行结构组织。 140 | 141 | + `MultiFieldPanel` -- 用于将相似字段分组到一起 142 | + `InlinePanel` -- 用于将子模型置于行内化(For inlining child models) 143 | + `FieldRowPanel` -- 用于将多个字段组织为单个的行 144 | 145 | ### 选择器面板(Chooser) 146 | 147 | 到某些确切模型的`ForeignKey`字段,可以使用以下的那些 `ChooserPanel` 类之一。这些面板把美观的模态选择器对话框界面加入进来,同时图片/文档选择器还允许在不离开页面编辑器的情况下,进行新文件的上传。 148 | 149 | + `PageChooserPanel` 150 | + `ImageChooserPanel` 151 | + `DocumentChooserPanel` 152 | + `SinippetChooserPanel` 153 | 154 | > **注意** 要使用这些选择器,字段所链接到的模型,就必须是某个页面、图片、文档或内容块。 155 | > 对于链接到其他模型类型的情况,就应使用`FieldPanel`了,这时将创建出一个下拉选择框。 156 | 157 | ## 对页面编辑器界面进行定制 158 | 159 | 页面编辑器可被深度定制,请参阅 [定制编辑接口](advanced_topics/customisation/page_editing_interface.md)。 160 | 161 | 162 | ## 父页面/子页面的类型规则 163 | 164 | **Parent page / subpage type rules** 165 | 166 | 下面的两个属性,可实现在站点中何处使用何种页面类型的控制。允许定义出如同“博客条目只能创建于博客首页下”这样的规则。 167 | 168 | 两条属性都取的是模型类或模型名称的清单。模型名称的格式为 `app_label.ModelName`。在省略了 `app_label`时,假定就是同样的应用。 169 | 170 | + `parent_page_types` 限制本类型可在哪些页面类型之下创建 171 | + `subpage_types` 限制在本类型下可以创建哪些页面类型 172 | 173 | 默认情况下,在所有页面类型之下,可以创建任意的页面类型,而如果要的就是这种效果的话,就没有必要设置这些属性。 174 | 175 | 将`parent_page_types`设置为空的清单,是一种阻止在编辑器界面创建出某个特定页面类型的好做法。 176 | 177 | 178 | ## 页面模型的URL模式的定制 179 | 180 | 通常不会直接调用`Page.get_url_parts(request)`,但为了给某个给定的页面模型定义定制的URL路由,是可以覆写该方法的(the `Page.get_url_parts(request)` method will not typically be called directly, but may be overridden to define custion URL routing for a given page model)。该方法会返回一个`(site_id, root_url, page_path)`的元组,该元组为`get_url`与`get_full_url`(下面可以看到)用来构造他和给定的页面URL类型。 181 | 182 | 在覆写`get_url_parts()`方法时,需接受`*args, **kwargs`: 183 | 184 | ```python 185 | def get_url_parts(self, *args, **kwargs): 186 | ``` 187 | 188 | 并在调用`super`(如有 `super`)上的`get_url_parts`方法处,将`*args, **kwargs`传递过去,比如: 189 | 190 | ```python 191 | super().get_url_parts(*args, **kwargs) 192 | ``` 193 | 194 | 虽然可以只传递`request`关键字参数,但将所有这些参数按原样进行传递,可确保在以后这些方法签名发生改变时保持兼容性。 195 | 196 | 有关更多此方面的信息,请参阅 [`wagtail.core.models.Page.get_url_parts()`](reference/pages/model_reference.md#wagtail.core.models.Page.get_url_parts)。 197 | 198 | ### 获取页面实例的URLs 199 | 200 | 在需要某个页面的URL时,可调用`Page.get_url(request)`方法。在该方法能够探测到页面位于当前站点(通过 `request.site`)的情况下,其默认返回的是本地URLs(不包含协议或域名);否则将返回一个包含了协议与域名的完整URL。为了开启站点级别的URL信息的每次请求缓存,以及为了实现本地URLs的生成,应尽可能将可选参数`request`包含进去(whenever possible, the optional `request` argument should be included to enable per-request caching of site-level URL information and facilitate the generation of local URLs)。 201 | 202 | `get_url(request)`的一个常见用例,就是在项目中包含了用于生成导航菜单的所有定制模板标签中。在编写这样的定制模板标签时,要确保这些标签包含有`takes_context=True`,及确保他们都用到了`context.get('request')`,从而安全地传递上请求,或在上下文中不存在请求时,要传递`None`(A common use case for `get_url(request)` is in any custom template tag your project may include for generating navigation menus. When writing such a custom template tag, ensure that it includes `takes_context=True` and use `context.get('request')` to safely pass the request or `None` if no request exists in the context)。 203 | 204 | 有关此方面的更多信息,请参阅[wagtail.core.models.Page.get_url()](reference/pages/model_reference.md#wagtail.core.models.Page.get_url)。 205 | 206 | 在需要完整的URL(包含协议与域名)时,可使用`Page.get_full_url(request)`方法。同样应尽可能包含`request`参数,为的是启用站点级别的URL信息的历次请求的缓存。有关此方面的更多信息,请参阅[wagtail.core.models.Page.get_full_url()](reference/pages/model_reference.md#wagta il.core.models.Page.get_full_url) 207 | 208 | ## 模板的渲染 209 | 210 | 可以给予每个页面模型一个HTML的模板,在用户浏览到站点前端的某个页面时,模板将被渲染出来。这就是将Wagtail内容给到终端用户的最简单也是最常见方式(这不是唯一的方式哦)。 211 | 212 | ### 将某个页面模型的模板添加进来 213 | 214 | Wagtail将基于应用级别与模型类的名称,自动选择模板的名称。 215 | 216 | 格式为:`/.html` 217 | 218 | > **注** 蛇形字母写法,snake cased,参见[Snake case, wikipedia](https://en.wikipedia.org/wiki/Snake_case) 219 | 220 | 比如上面的博客页面的模板将是: `blog/blog_page.html` 221 | 222 | 只需在某个可以此名称访问到的地方,创建一个模板即可。 223 | 224 | ### 模板上下文 225 | 226 | **Template context** 227 | 228 | Wagtail 将以绑定到正在渲染的页面实力上的`page`变量,来进行模板的渲染。使用该变量来访问页面的内容。比如要获得当前页面的标题,就使用`{{ page.title }}`。所有由 [上下文处理器](https://docs.djangoproject.com/en/stable/ref/templates/api/#subclassing-context-requestcontext) 提供的变量,也都是可用的。 229 | 230 | ** 模板上下文的定制 ** 231 | 232 | 所有页面都有一个`get_context()`方法,在渲染模板时,都会调用到该方法,其返回值为绑定到模板变量的一个字典。 233 | 234 | 可通过覆写该方法,来讲更多变量加入到模板上下文: 235 | 236 | ```python 237 | class BlogIndexPage(Page): 238 | ... 239 | 240 | def get_context(self, request): 241 | context = super().get_context(request) 242 | 243 | # 加入额外变量,并返回更新后的上下文 244 | context['blog_entries'] = BlogPage.objects.child_of(self).live() 245 | # 注意上面就是一个 QuerySet 246 | return context 247 | ``` 248 | 249 | 此时这些变量就可以在模板中加以使用了: 250 | 251 | ```python 252 | {{ page.title }} 253 | 254 | {% for enry in blog_entries %} 255 | {{ entry.titlte }} 256 | {% endfor %} 257 | ``` 258 | 259 | ### 修改模板 260 | 261 | 通过在类上设置 `template` 属性,来使用一个不同的模板文件: 262 | 263 | ```python 264 | class BlogPage(Page): 265 | ... 266 | 267 | template = 'other_tempalte.html' 268 | ``` 269 | 270 | **动态地选择模板** 271 | 272 | 通过在页面类上定义`get_template()`方法,可以给每个实例指定不同的模板。在每次页面被渲染时,都会调用该方法: 273 | 274 | ```python 275 | class BlogPage(Page): 276 | ... 277 | 278 | use_other_template = models.BooleanField() 279 | 280 | def get_template(self, request): 281 | if self.use_other_template: 282 | return 'blog/other_blog_page.html' 283 | 284 | return 'blog/blog_page.html' 285 | ``` 286 | 287 | 在本示例中,那些设置了`use_other_template`字段的页面,将使用`blog/other_blog_page.html`模板。所有其他页面都将使用默认的`blog/blog_page.html`。 288 | 289 | ### 在页面渲染上的更多控制 290 | 291 | 所有页面类,都有一个`serve()`方法,该方法内部调用了`get_context`与`get_template`方法,进而对模板进行渲染。此方法与Django的视图函数类似,取的是一个Django `Request` 对象,返回的是一个 Django `Response` 对象。 292 | 293 | 为实现对页面渲染的完全掌控,此方法可被重写。 294 | 295 | 比如下面就是一种让页面以一个表示其自身的JSON方式进行响应的方法: 296 | 297 | ```python 298 | from django.http import JsonResponse 299 | 300 | class BlogPage(Page): 301 | ... 302 | 303 | def serve(self, request): 304 | return JsonResponse({ 305 | 'title': self.title, 306 | 'body': self.body, 307 | 'date': self.date, 308 | 309 | # 将图片缩放到宽度为 300px 并取得到图片的URL 310 | 'feed_image': self.feed_image.get_rendition('width-200').url, 311 | }) 312 | ``` 313 | 314 | ## 内联模型 315 | 316 | **Inline models** 317 | 318 | Wagtail可在页面中嵌套其他模型的内容。对于创建那些诸如相关链接,或要以旋转木马方式显示的重复字段,此特性是有用的。内联模型的内容,与其他页面内容一样,保存在版本变更中。 319 | 320 | 每个内联模型,都有以下要求: 321 | 322 | + 必须继承自`wagtail.core.models.Orderable` 323 | + 必须要有一个到父模型的`ParentalKey` 324 | 325 | > **注意** `django-modelcluster`与`ParentalKey`的区别 326 | > 模型的内联特性,是由[`django-modelcluster`](https://github.com/torchbox/django-modelcluster)所提供的,同时必须要从这里将`ParentalKey`字段类型导入进来: 327 | > `from modelcluster.fields import ParentalKey` 328 | > `ParentalKey` 是 Django 的`ForeignKey`的一个子类,并取的是同样的参数。 329 | 330 | 331 | 比如下面的内联模型就可用于将相关链接(一个名称-url对的的列表)加入到`BlogPage`模型: 332 | 333 | ```python 334 | from django.db import models 335 | from modelcluster.fields import ParentalKey 336 | from wagtail.core.models import Orderable 337 | 338 | class BlogPageRelatedLink(Orderable): 339 | page = ParentalKey(BlogPage, on_delete=models.CASCADE, realted_name='related_links') 340 | name = models.CharField(max_length=250) 341 | url = models.URLField() 342 | 343 | panels = [ 344 | FieldPanel('name'), 345 | FieldPanel('url'), 346 | ] 347 | ``` 348 | 349 | 接着使用`InlinePanel`编辑面板类,将其加入到管理界面: 350 | 351 | ```python 352 | content_panels = [ 353 | ... 354 | 355 | InlinePanel('related_links', label="相关链接"), 356 | ] 357 | ``` 358 | 359 | `InlinePanel`类构造函数的第一个参数,必须与`ParentalKey`的`related_name`属性一致。 360 | 361 | ## 处理多个页面 362 | 363 | **Working with pages** 364 | 365 | Wagtail使用了Django的 [多表继承](https://docs.djangoproject.com/en/stable/topics/db/models/#multi-table-inheritance) 特性,来实现在相同的树中使用多个页面模型。 366 | 367 | 每个页面都会被同时加入到Wagtail内建的`Page`模型,以及其用户定义的模型(比如早先所创建的`BlogPage`模型)。 368 | 369 | 相应地,页面可以两种形式的Python代码存在,`Page`基类的示例,或页面模型的示例。 370 | 371 | 在一并处理多个页面类型时,通常使用的是Wagtail的 `Page` 模型,其不会给予到对特定于这些页面类型的那些字段的访问。 372 | 373 | ```bash 374 | # 获取数据库中的所有页面 375 | >>> from wagtail.core.models import Page 376 | >>> Page.objects.all() 377 | [, , , , ] 378 | ``` 379 | 380 | > **注** 通过 `python manage.py shell` 即可进入到上面的命令行界面 381 | 382 | 在处理单个的页面类型时,就可以对用户定义的模型实例进行处理了。这样的模型实例,给予到对`Page`中所有可用字段的访问,以及对该特定类型的用户定义字段的访问。 383 | 384 | ```bash 385 | # 获取数据库中所有的博客条目 386 | >>> from blog.models import BlogPage 387 | >>> BlogPage.objects.all() 388 | [, ] 389 | ``` 390 | 391 | 使用`.specfific`属性,就可以将某个`Page`对象,转换为其更为具体的用户定义的等价实体。然而这会引发一次额外的数据库查询。 392 | 393 | ```bash 394 | >>> page = Page.objects.get(title='A Blog Post') 395 | >>> page 396 | 397 | 398 | # 注意:该博客文章是 Page 的一个实例,因此不能访问到文章主体、日期或首页图片 399 | 400 | >>> page.specific 401 | 402 | ``` 403 | 404 | ## 技巧 405 | 406 | ### 友好的模型名称 407 | 408 | 运用带有 `verbose_name` 属性的Django的内部`Meta`类,可令到模型名称对用户更为友好,比如: 409 | 410 | ```python 411 | class HomePage(Page): 412 | 413 | ... 414 | 415 | class Meta: 416 | verbose_name = "homepage" 417 | ``` 418 | 419 | 默认情况下,在用户选择创建要创建的页面时,页面类型清单是通过将模型名称按照大写字符拆分,而生成的。那么`HomePage`模型就将被命名为“Home Page”,显然这个名字有点笨拙。在如同上面的示例中那样定义了 `verbose_name`后,就会将名称修改为“Homepage”,这样就更为习以为常一点。 420 | 421 | > verbose: 详细 422 | 423 | ### 页面QuerySet的排序 424 | 425 | 由基类`Page`所派生的模型,是 *无法* 通过使用标准的加入 `ordering` 属性到模型内部的`Meta`类上,这种标准的Django 方法,而赋予给他一种默认排序的。 426 | 427 | ```python 428 | Class NewsItemPage(Page): 429 | 430 | publication_date = models.DateField() 431 | ... 432 | 433 | class Meta: 434 | ordering = ('-publication_date', ) # 不会生效 435 | ``` 436 | 437 | 这是因为`Page`是强制以路径对QuerySet进行排序的。 因此在构造一个QuerySet时,就必须显示地进行排序: 438 | 439 | ```python 440 | news_items = NewsItemPage.objects.live().order_by('-publication_date') 441 | ``` 442 | 443 | ### 对页面管理器进行定制 444 | 445 | 可将定制的`Manager`类,添加到`Page`类。所有定制管理器,都应继承自`wagtail.core.models.PageManager`: 446 | 447 | ```python 448 | from django.db import models 449 | 450 | from wagtail.core.models import Page, PageManager 451 | 452 | class EventPageManager(PageManager): 453 | """ 活动页面的定制管理器 """ 454 | 455 | class EventPage(Page): 456 | 457 | start_date = models.DateField() 458 | 459 | objects = EventPageManager() 460 | ``` 461 | 462 | 而在仅需加入额外的 `QuerySet` 方法时,还可以从`wagtail.core.models.PageQuerySet` 继承, 并调用`from_queryset()`来构建一个定制的 `Manager` 类: 463 | 464 | ```python 465 | from django.db import models 466 | from django.utils import timezone 467 | 468 | from wagtail.core.models import Page, PageManager, PageQuerySet 469 | 470 | class EventPageQuerySet(PageQuerySet): 471 | 472 | def future(self): 473 | today = timezone.localtime(timezone.now()).date() 474 | return self.filter(start_date__gte=today) 475 | 476 | EventPageManager = PageManager.from_queryset(EventPageQuerySet) 477 | 478 | class EventPage(Page): 479 | 480 | start_date = models.DateField() 481 | 482 | objects = EventPageManager() 483 | ``` 484 | -------------------------------------------------------------------------------- /getting_started/tutorial.md: -------------------------------------------------------------------------------- 1 | # 第一个Wagtail站点 2 | 3 | > **注意** 此教程讲的是有关建立一个全新Wagtail项目的内容。如要将Wagtail加入到某个既有Django项目,请参考[将Wagtail集成到Django项目](integrating_into_django.html)。 4 | 5 | 1. 安装 Wagtail 与其依赖: 6 | 7 | ```sh 8 | $ pip install wagtail 9 | ``` 10 | 11 | 2. 开始你的站点: 12 | 13 | ```sh 14 | $ wagtail start mysite 15 | $ cd mysite 16 | ``` 17 | 18 | Wagtail提供了与`django-admin.py startproject` 类似的 `start` 命令。在项目中运行 `wagtail start mysite`将生成一个新的、有着几个特定于 Wagtail 的附加文件的 `mysite` 文件夹,这些文件包括了: 19 | 20 | + 所需的项目设置 21 | + 一个有着空白的 `HomePage` 模型的“主页”应用 22 | + 一些基础模板 23 | + 一个简单的“搜索”应用。 24 | 25 | 3. 安装项目依赖 26 | 27 | ```sh 28 | $ pip install -r requirements.txt 29 | ``` 30 | 31 | 此步骤确保刚创建的项目具有相关版本的Django 32 | 33 | 4. 创建数据库 34 | 35 | ```sh 36 | $ ./manage.py migrate 37 | ``` 38 | 39 | 在没有更新项目设置时,数据库将是项目目录中的一个 SQLite 数据库文件。 40 | 41 | 5. 创建出一个管理员用户 42 | 43 | ```sh 44 | $ ./manage.py createsuperuser 45 | ``` 46 | 47 | 6. 启动服务器 48 | 49 | ```sh 50 | $ ./manage.py runserver 51 | ``` 52 | 53 | 如没有什么错误的话,访问 [http://127.0.0.1:8000]() 就可以看到一个欢迎页面了: 54 | 55 | ![Wagtail欢迎页面](images/tutorial_1.png) 56 | 57 | 可在 [http://127.0.0.1:8000/admin]() 处访问到管理区 58 | 59 | ![Wagtail管理区](images/tutorial_2.png) 60 | 61 | ## 对`HomePage`模块进行扩展 62 | 63 | 在此开箱即用的情况下,“主页”应用在`models.py`文件中,定义了一个空白的`HomePage`模型,以及与该空白模型一起的数据库迁移,由他们二者一起,创建出了一个主页,并将Wagtail配置为使用该主页。 64 | 65 | 按照下面这样对`home/models.py`进行编辑,将一个`body`字段加入到该模型中: 66 | 67 | ```python 68 | from django.db import models 69 | 70 | from wagtail.core.models import Page 71 | from wagtail.core.fields import RichTextField 72 | from wagtail.admin.edit_handlers import FieldPanel 73 | 74 | class HomePage(Page): 75 | body = RichTextField(blank=True) 76 | 77 | content_panels = Page.content_panels + [ 78 | FieldPanel('body', classname="full"), 79 | ] 80 | ``` 81 | 82 | `body` 被定义为 `RichTextField`,一种特殊的 Wagtail 字段。当然也可以使用任意的 [Django 核心字段](https://docs.djangoproject.com/en/stable/ref/models/fields/)。`content_panels` 定义了功能及编辑接口的布局(`content_panels` define the capatibilities and the layout of the editing interface)。请参考更多有关 [创建页面模型](topics/pages.html)。 83 | 84 | 此时运行 `./manage.py makemigrations`,接着 `./manage.py migrate` 命令,来用模型改变对数据库作出更新。在每次修改了模型定义时,都 **必须** 运行这两个命令。 85 | 86 | 现在就可以在 Wagtail 管理区(前往 “页面”、“主页”,然后点击“编辑”)对该主页进行编辑了。在`body`字段输入一些文字,并发布该页面。 87 | 88 | 现在就需要将对应的页面模板加以更新,以反映对模型作出的改变。Wagtail使用一般Django模块,来渲染各种页面类型。默认他将查找一个由应用与模型名称组成、以下划线表示大写字母分开的模板文件名(比如,“主页”应用中的`HomePage`模型,就成为了`home/home_page.html`)。该模板文件可存在于由 [Django的模板规则](https://docs.djangoproject.com/en/stable/intro/tutorial03/#write-views-that-actually-do-something) 所识别的任何位置;通常他是放在一个应用内的 `templates`文件夹下的。 89 | 90 | > **注** 可以看出 Wagtail, 以至其基础Django,采用的是 “模型-视图” 编程模型。 91 | 92 | 将 `home/templates/home/home_page.html`编辑为包含以下内容: 93 | 94 | ```html 95 | {% extends "base.html" %} 96 | 97 | {% load wagtailcore_tags %} 98 | 99 | {% block body_class %}template-homepage{% endblock %} 100 | 101 | {% block content %} 102 | {{ page.body|richtext }} 103 | {% endblock %} 104 | ``` 105 | 106 | ![修改了模型与模板后的Wagtail主页](images/tutorial_3.png) 107 | 108 | 109 | ## 关于 Wagtail 模板的标签 110 | 111 | **Wagtail template tags** 112 | 113 | Wagtail提供了一些 [模板标签与过滤器](topics/writing_templates.html#template_tags_and_filters),通过在模板文件顶部包含 `{% load wagtailcore_tags %}`,装入这些标签与过滤器。 114 | 115 | 在本教程中,将用到 `richtext` 过滤器,来将某个 `RichTextField` 字段中的内容进行转写与打印出来(to escape and print the contents of a `RichTextField`)。 116 | 117 | ```html 118 | {% load wagtailcore_tags %} 119 | {{ page.body|richtext }} 120 | ``` 121 | 122 | 这段代码将产生出: 123 | 124 | ```html 125 |
    126 |

    127 | Welcome to our new site! 128 |

    129 |
    130 | ``` 131 | 132 | **注意:** 对于用到Wagtail自带标签的所有模板,都需要包含 `{% load wagtailcore_tags %}`。如没有装入这些标签,那么Django将会抛出一个 `TemplateSyntaxError`错误。 133 | 134 | ## 一个简单的博客系统 135 | 136 | 现在已做好建立一个博客的准备了。运行`./manage.py startapp blog`命令,来在Wagtail站点中创建一个新的应用。 137 | 138 | 将此新的 `blog` 应用加入到 `mysite/settings/base.py` 文件的 `INSTALLED_APPS` 变量中。 139 | 140 | ### 博客目录与文章 141 | 142 | **Blog Index and Posts** 143 | 144 | 这里以一个简单的博客目录页面开始。在`blog/models.py`中: 145 | 146 | ```python 147 | from wagtail.core.models import Page 148 | from wagtail.core.fields import RichTextField 149 | from wagtail.admin.edit_handlers import FieldPanel 150 | 151 | class BlogIndexPage(Page): 152 | intro = RichTextField(blank=True) 153 | 154 | content_panels = Page.content_panels + [ 155 | FieldPanel('intro', classname="full") 156 | ] 157 | ``` 158 | 159 | 然后运行 `./manage.py makemigrations` 与 `./manage.py migrate`命令。 160 | 161 | 因为该模型被命名为`BlogIndexPage`,因此默认的模板名称(在没有覆盖的情况下)将是`blog/template/blog/blog_index_page.html`。使用以下内容创建出该文件: 162 | 163 | ```html 164 | {% extends "base.html" %} 165 | 166 | {% load wagtailcore_tags %} 167 | 168 | {% block body_class %}template-blogindexpage{% endblock %} 169 | 170 | {% block content %} 171 |

    {{ page.title }}

    172 |
    {{ page.intro|richtext }}
    173 | 174 | {% for post in page.get_children %} 175 |

    {{ post.title }}

    176 | {{ post.specific.intro }} 177 | {{ post.specific.body|richtext }} 178 | {% endfor %} 179 | {% endblock %} 180 | ``` 181 | 182 | 该模板中大部分都是熟悉的,但稍后要对`get_children`做一下解释。请注意`pageurl`这个标签,那与Django的`url`表情类似,不过`pageurl`带有一个Wagtail页面对象作为参数。 183 | 184 | 在Wagtail管理界面,创建一个`BlogIndexPage`,作为主页的子页面,并确保其在`效果提升(Promote)`分页中有着“blog”的一个slug。那么现在就应该可以在站点上访问到`/blog`这个URL了(请 **留意** 该`Promote`分页上的 slug `blog` 是如何定义页面URL的)。 185 | 186 | 现在需要一个博客文章的模型与模板了。在文件`blog/models.py`中: 187 | 188 | ```python 189 | from django.db import models 190 | 191 | from wagtail.core.models import Page 192 | from wagtail.core.fields import RichTextField 193 | from wagtail.admin.edit_handlers import FieldPanel 194 | from wagtail.search import index 195 | 196 | class BlogIndexPage(Page): 197 | intro = RichTextField(blank=True) 198 | 199 | content_panels = Page.content_panels + [ 200 | FieldPanel('intro', classname="full") 201 | ] 202 | 203 | # 保留 BlogIndexPage的定义,并加入: 204 | 205 | class BlogPage(Page): 206 | date = models.DateField("发布日期") 207 | intro = models.CharField(max_length=250) 208 | body = RichTextField(blank=True) 209 | 210 | search_fields = Page.search_fields + [ 211 | index.SearchField('intro'), 212 | index.SearchField('body'), 213 | ] 214 | 215 | content_panels = Page.content_panels + [ 216 | FieldPanel('date'), 217 | FieldPanel('intro'), 218 | FieldPanel('body', classname="full"), 219 | ] 220 | ``` 221 | 222 | 现在运行 `python manage.py makemigrations`与`python manage.py migrate`命令。 223 | 224 | 在 `blog/templates/blog/blog_page.html`创建一个模板: 225 | 226 | ```html 227 | {% extents "base.html" %} 228 | 229 | {% load wagtailcore_tags %} 230 | 231 | {% block body_class %}template-blogpage{% endblock %} 232 | 233 | {% block content %} 234 |

    {{ page.title }}

    235 |

    {{ page.date }}

    236 |
    {{ page.intro }}
    237 | 238 | {{ page.body|richtext }} 239 | 240 |

    返回博客首页

    241 | {% endblock %} 242 | ``` 243 | 244 | 请注意这里使用了Wagtail的内建`get_parent()`方法,来获取此文章所对应博客首页的URL。 245 | 246 | 现在创建一些作为`BlogIndexPage`的子页面的博客文章出来。在建立这些博客文章是一定要选择`Blog Page`类型。 247 | 248 | ![创建一个类型为BlogPage的博客文章页面](images/tutorial_4a.png) 249 | 250 | 251 | ![创建一个类型为BlogPage的博客文章页面](images/tutorial_4b.png) 252 | 253 | Wagtail将给予你对不同父内容类型下,可建立何种内容的完全掌控的能力(Wagtail gives you full control over what kinds of content can be created under various parent content types)。默认所有页面类型,都可以是任意其他页面类型的子页面。 254 | 255 | ![创建一个类型为BlogPage的博客文章页面](images/tutorial_5.png) 256 | 257 | 此时就有了一个可初步工作的博客系统了。在`/blog`URL处访问该博客,将看到如下页面: 258 | 259 | ![一个初步工作的Wagtail博客系统](images/tutorial_7.png) 260 | 261 | 文章标题应是链接到文章页面的,同时在每个文章页面的底部,都应有一个返回到博客主页的链接。 262 | 263 | ## 关于父页面与子页面 264 | 265 | **Parents and Children** 266 | 267 | 在Wagtail中进行的大部分工作,都是围绕由众多节点与叶子所构成的“树”结构的层次概念开展的(参见[理论](reference/pages/theory.html),Much of the work you'll be doing in Wagtail revolves around the concept of hierarchical "tree" structures consisting of nodes and leaves)。在本例中,`BlogIndexPage`是一个“节点”,同时单个的`BlogPage`实例,就是“叶子”了。 268 | 269 | 这里再来从另一个角度看看`blog_index_page.html`的代码: 270 | 271 | ```html 272 | {% for post in page.get_children %} 273 |

    {{ post.title }}

    274 | {{ post.specific.intro }} 275 | {{ post.specific.body|richtext }} 276 | {% endfor %} 277 | ``` 278 | 279 | 在Wagtail中的每个“页面”,都可以从他在这个层次体系中的位置,呼出他的父页面或所有子页面(Every "page" in Wagtail can call out to its parent or children from its own position in the hierarchy)。但这里又为何要指定`post.specific.into`,而不是`post.intro`呢?这就必须要从定义模型的方式说起了: 280 | 281 | ```python 282 | class BlogPage(Page): 283 | ``` 284 | 285 | 方法`get_children()`给出了一个`Page`基类的实例清单。而在打算引用这些继承了该基类的实例属性时,Wagtail提供了`specific`方法,来获取到真实的`BlogPage`记录(the `get_children()` method gets us a list of instances of the `Page` base class. When we want ot reference properties of the instances that inherit from the base class, Wagtail provides the `specific` method that retrieves the actual `BlogPage` record)。尽管`title`字段在基类`Page`模块上是存在的,但`intro`字段却只存在与`BlogPage`模型上,因此就需要`.specific`方法,来访问该字段。 286 | 287 | 这里可使用Django的`with`标签,来讲模板代码加以优化: 288 | 289 | ```html 290 | {% for post in page.get_children %} 291 | {% with post=post.specific %} 292 |

    {{ post.title }}

    293 | {{ post.intro }} 294 | {{ post.body|richtext }} 295 | {% endwith %} 296 | {% endfor %} 297 | ``` 298 | 299 | 在后期编写更为定制化的Wagtail代码时,将发现一整套的`QuerySet`修饰符(a whole set of QuerySet modifiers),来帮助对层次结构进行导航。 300 | 301 | ```python 302 | # 给定一个页面对象`somepage`: 303 | MyModel.objects.descendant_of(somepage) 304 | child_of(somepage) / not_child_of(somepage) 305 | ancestor_of(somepage) / not_ancestor_of(somepage) 306 | parent_of(somepage) / not_parent_of(somepage) 307 | sibling_of(somepage) / not_sibling_of(somepage) 308 | 309 | # ... and ... 310 | somepage.get_children() 311 | somepage.get_ancestors() 312 | somepage.get_descenants() 313 | somepage.get_siblings() 314 | ``` 315 | 316 | 有关此方面的更多信息,请参阅:[页面的QuerySet参考](reference/pages/queryset_reference.html) 317 | 318 | 319 | ## 覆写上下文 320 | 321 | **Overriding Context** 322 | 323 | 在上面的博客首页视图中存在一些问题: 324 | 325 | 1. 博客应该以 *相反* 的时间顺序显示的 326 | 2. 要确保只显示那些已发布的内容 327 | 328 | 要实现这两个目的,就要不光是在模板中抓取博客目录页面的子页面了。而要对模型定义中的`QuerySet`进行修改。Wagtail通过覆写`get_context()`方法,而令到这一点成为可能。像下面这样修改`BlogIndexPage`模型: 329 | 330 | ```python 331 | class BlogIndexPage(Page): 332 | intro = RichTextField(blank=True) 333 | 334 | def get_context(self, request): 335 | # 将上下文更新为仅包含发布了的博客文章,并以 时间逆序 进行排序 336 | context = super().get_context(request) 337 | blogpages = self.get_children().live().order_by('-first_publised_at') 338 | context['blogpages'] = blogpages 339 | return context 340 | ``` 341 | 342 | 这里所完成的所有工作,就是先获取原始上下文,然后创建一个定制的`QuerySet`,将其加入到获取的上下文中,最后将修改后的上下文返回给视图。为此还需要对`blog_index_page.html`模板稍作改变。做以下修改: 343 | 344 | 将 `{% for post in page.get_children %}` 修改为:`{% for post in blogpages %}` 345 | 346 | 现在尝试加入一篇未发布的文章 -- 他将不会在博客目录页面出现。同时原有的文章将一最近发布在前的方式进行排序了。 347 | 348 | ## 图片 349 | 350 | 下面将把图片集附加到博客文章这一功能加入进来。尽管可以通过简单地将图片插入到`body`富文本字段中,但通过将图片集作为一种新的专用对象类型,在数据库中建立出来,然后有诸多优势 -- 以这种方式的话,就可以完全控制到这些图片在模板中的布局与样式,而不是必须在富文本字段中以特定方式对他们进行布置了。同时这样做也可以在独立于博客文本的其他地方,比如在博客目录页面显示一个缩略图的方式,使用这些图片。 351 | 352 | 将一个新的`BlogPageGalleryImage`模型,加入到`models.py`文件中: 353 | 354 | ```python 355 | from django.db import models 356 | 357 | # 新加入了 ParentalKey、Orderable、InlinePanel与ImageChooserPanel 的导入 358 | from modelcluster.fields import ParentalKey 359 | 360 | from wagtail.core.models import Page, Orderable 361 | from wagtail.core.fields import RichTextField 362 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel 363 | from wagtail.images.edit_handlers import ImageChooserPanel 364 | from wagtail.search import index 365 | 366 | class BlogIndexPage(Page): 367 | intro = RichTextField(blank=True) 368 | 369 | def get_context(self, request): 370 | # 将上下文更新为仅包含发布了的博客文章,并以 时间逆序 进行排序 371 | context = super().get_context(request) 372 | blogpages = self.get_children().live().order_by('-first_published_at') 373 | context['blogpages'] = blogpages 374 | return context 375 | 376 | content_panels = Page.content_panels + [ 377 | FieldPanel('intro', classname="full") 378 | ] 379 | 380 | # 保留 BlogIndexPage的定义,并加入: 381 | 382 | class BlogPage(Page): 383 | date = models.DateField("发布日期") 384 | intro = models.CharField(max_length=250) 385 | body = RichTextField(blank=True) 386 | 387 | search_fields = Page.search_fields + [ 388 | index.SearchField('intro'), 389 | index.SearchField('body'), 390 | ] 391 | 392 | content_panels = Page.content_panels + [ 393 | FieldPanel('date'), 394 | FieldPanel('intro'), 395 | FieldPanel('body', classname="full"), 396 | InlinePanel('gallery_images', label="图片"), 397 | ] 398 | 399 | class BlogPageGalleryImage(Orderable): 400 | page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name="gallery_images") 401 | image = models.ForeignKey( 402 | 'wagtailimages.Image', on_delete=models.CASCADE, related_name="+" 403 | ) 404 | caption = models.CharField(blank=True, max_length=250) 405 | 406 | panels = [ 407 | ImageChooserPanel('image'), 408 | FieldPanel('caption'), 409 | ] 410 | ``` 411 | 412 | 此时运行 `python manage.py makemigrations` 与 `python manage.py migratte`。 413 | 414 | 上面的代码中涉及到一些新的概念,下面就一起来看看他们: 415 | 416 | `BlogPageGalleryImage`模型继承自`Orderable`,从而将字段`sort_order`加入到模型中了,以对图片集中的图片顺序进行跟踪。 417 | 418 | 到`BlogPage`模型的`ParentalKey`,则是将这些图片附加到某个特定页面。`ParentalKey`的工作方式与`ForeignKey`类似,不过同时将`BlogPageGalleryImage`定义为`BlogPage`模型的“子”模型,因此他就成为了页面的一个基础部分,可以对其进行修改提交与修订历史追踪等操作(A `ParentalKey` works similarly to a `ForeignKey`, but also defines `BlogPageGalleryImage` as a "child" of the `BlogPage` model, so that it's treated as a fundamental part of the page in operations like submitting for moderation, and tracking revision history)。 419 | 420 | `image`是到Wagtail内建的`Image`模型的一个`FoerignKey`, 图片本身是在`Image`模型中存储的。同时`Image`模型有着自己的专用面板类型(a dedicated panel type),`ImageChooserPanel`,该面板类型提供了一个用于选取某个既有图片或上传一个新图片的弹出界面。依此方式,就允许某个图片可以存在于多个图片集中 -- 从而有效地创建了一直页面与图片之间的多对多关系。 421 | 422 | 在该外键上指定`on_delete=models.CASCADE`,就意味着当某个图片从系统中删除时,其所在图片集也会被删除。(但在某些情况下,可能让该条目留存下来更好 -- 比如在某个“our staff”页面包含了一个有着头像的人员清单,而其中一张头像被删除了,那么就宁愿将那个人在没有头像图片的情况下保留下来。在次情况下,就要把此外键设置为`blank=True, null=True, on_delete=models.SET_NULL`)。 423 | 424 | 最后,将`InlinePanel`加入到`BlogPage.content_panels`中,从而领导该图片集在`BlogPage`的编辑界面上可用。 425 | 426 | 对博客页面进行调整,以包含这些图片: 427 | 428 | ```html 429 | {% extends "base.html" %} 430 | 431 | {% load wagtailcore_tags wagtailimages_tags %} 432 | 433 | {% block body_class %}template-blogpage{% endblock %} 434 | 435 | {% block content %} 436 |

    {{ page.title }}

    437 |

    {{ page.date }}

    438 | 439 |
    {{ page.intro }}
    440 | 441 | {{ page.body|richtext }} 442 | 443 |
    444 | {% for item in page.gallery_images.all %} 445 |
    446 | {% image item.image fill-320x240 %} 447 |

    {{ item.caption }}

    448 |
    449 | {% endfor %} 450 |
    451 | 452 |

    返回博客首页

    453 | {% endblock %} 454 | ``` 455 | 456 | 这里使用 `{% image %}` 标签(此标签存在于`wagtailimages_tags`库中,在该模板顶部有导入该库),来将某个``元素,以`file-320x240`为参数而表明该图片需要缩放及裁剪,以填充到一个`320x240`的矩形中,而进行插入。有关在模板中图片的使用的更多信息,请参阅[文档](topics/images.html)。 457 | 458 | ![插入了图片集的博客文章页面](images/tutorial_6.jpg) 459 | 460 | 因为这里的图片集图片,都是有着其自身地位的数据库对象,所以可以对其进行查询以及独立于博客文章主体的重复使用(since our gallery images are database objects in their own right, we can query and re-use them independently of the blog post body)。下面定义了一个`main_image`方法,将返回图片集的第一个条目(或在没有没有图片集时返回`None`): 461 | 462 | ```python 463 | class BlogPage(Page): 464 | date = models.DateField("发布日期") 465 | intro = models.CharField(max_length=250) 466 | body = RichTextField(blank=True) 467 | 468 | def main_image(self): 469 | gallery_item = self.gallery_images.first() 470 | if gallery_item: 471 | return gallery_item.image 472 | else: 473 | return None 474 | 475 | search_fields = Page.search_fields + [ 476 | index.SearchField('intro'), 477 | index.SearchField('body'), 478 | ] 479 | 480 | content_panels = Page.content_panels + [ 481 | FieldPanel('date'), 482 | FieldPanel('intro'), 483 | FieldPanel('body', classname="full"), 484 | InlinePanel('gallery_images', label="图片"), 485 | ] 486 | ``` 487 | 488 | 此方法现在已对模板可用了。现在对`blog_index_page.html`进行更新,以将博客文章主图作为每篇文章旁边的一个缩略图,而包含进来: 489 | 490 | ```html 491 | {% extends "base.html" %} 492 | 493 | {% load wagtailcore_tags wagtailimages_tags %} 494 | 495 | {% block body_class %}template-blogindexpage{% endblock %} 496 | 497 | {% block content %} 498 |

    {{ page.title }}

    499 |
    {{ page.intro|richtext }}
    500 | 501 | {% for post in blogpages %} 502 | {% with post=post.specific %} 503 |

    {{ post.title }}

    504 | 505 | {% with post.main_image as main_image %} 506 | {% if main_image %} 507 | {% image main_image fill-160x100 %} 508 | {% endif %} 509 | {% endwith %} 510 | 511 | {{ post.intro }} 512 | {% endwith %} 513 | {% endfor %} 514 | {% endblock %} 515 | ``` 516 | 517 | ## 将文章打上标签 518 | 519 | **Tagging Posts** 520 | 521 | 现在要加入一项可以让文章编辑给他们的文章“打上标签”的功能,如此读者就可以查看到比如“自行车”相关的所有内容。要实现次特性,就需要调用与Wagtail打包在一起的标签系统(the tagging system bundled with Wagtail),将该标签系统附加到模型`BlogPage`与内容面板,并在博客文章模板上渲染出带有链接的标签。不言而喻,这里同样需要一个能用的特定于标签URL视图(Of course, we'll need a working tag-specific URL view as well)。 522 | 523 | 首先,再一次对`models.py`进行修改: 524 | 525 | ```python 526 | from django.db import models 527 | 528 | # 新加入了 ParentalKey、Orderable、InlinePanel与ImageChooserPanel 的导入 529 | # 新加入了 ClusterTaggableManager、TaggedItemBase与MultiFieldPanel 530 | from modelcluster.fields import ParentalKey 531 | from modelcluster.contrib.taggit import ClusterTaggableManager 532 | from taggit.models import TaggedItemBase 533 | 534 | from wagtail.core.models import Page, Orderable 535 | from wagtail.core.fields import RichTextField 536 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel 537 | from wagtail.images.edit_handlers import ImageChooserPanel 538 | from wagtail.search import index 539 | 540 | # ...(保持BlogIndexPage的定义不变) 541 | class BlogIndexPage(Page): 542 | intro = RichTextField(blank=True) 543 | 544 | def get_context(self, request): 545 | # 将上下文更新为仅包含发布了的博客文章,并以 时间逆序 进行排序 546 | context = super().get_context(request) 547 | blogpages = self.get_children().live().order_by('-first_published_at') 548 | context['blogpages'] = blogpages 549 | return context 550 | 551 | content_panels = Page.content_panels + [ 552 | FieldPanel('intro', classname="full") 553 | ] 554 | 555 | class BlogPageTag(TaggedItemBase): 556 | content_object = ParentalKey( 557 | 'BlogPage', 558 | related_name = 'tagged_items', 559 | on_delete = models.CASCADE 560 | ) 561 | 562 | # 保留 BlogIndexPage的定义,并加入: 563 | 564 | class BlogPage(Page): 565 | date = models.DateField("发布日期") 566 | intro = models.CharField(max_length=250) 567 | body = RichTextField(blank=True) 568 | tags = ClusterTaggableManager(through=BlogPageTag, blank=True) 569 | 570 | # ... (保留 main_image 与 search_fields 的定义) 571 | def main_image(self): 572 | gallery_item = self.gallery_images.first() 573 | if gallery_item: 574 | return gallery_item.image 575 | else: 576 | return None 577 | 578 | search_fields = Page.search_fields + [ 579 | index.SearchField('intro'), 580 | index.SearchField('body'), 581 | ] 582 | 583 | content_panels = Page.content_panels + [ 584 | MultiFieldPanel([ 585 | FieldPanel('date'), 586 | FieldPanel('tags'), 587 | ], heading="文章信息"), 588 | FieldPanel('intro'), 589 | FieldPanel('body', classname="full"), 590 | InlinePanel('gallery_images', label="图片"), 591 | ] 592 | 593 | class BlogPageGalleryImage(Orderable): 594 | page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name="gallery_images") 595 | image = models.ForeignKey( 596 | 'wagtailimages.Image', on_delete=models.CASCADE, related_name="+" 597 | ) 598 | caption = models.CharField(blank=True, max_length=250) 599 | 600 | panels = [ 601 | ImageChooserPanel('image'), 602 | FieldPanel('caption'), 603 | ] 604 | ``` 605 | 606 | 此时运行 `python manage.py makemigrations` 与 `python manage.py migrate`。 607 | 608 | 请注意这里新的`modelcluster`与`taggit`导入,及新的`BlogPageTag`模型的加入,以及在`BlogPage`模型上加入的`tags`字段。这里还利用了`BlogPage`模型上`conent_panels`中的`MultiFieldPanel`,来把日期与标签字段放在一起,从而提升了可读性。 609 | 610 | 对`BlogPage`实例之一进行编辑,那么现在就可以对文章打标签了: 611 | 612 | ![已经可以给文章打标签](images/tutorial_8.png) 613 | 614 | 而要在`BlogPage`上渲染出标签,就要将下面的代码加入到`blog_page.html`: 615 | 616 | ```html 617 |
    618 | {% if page.tags.all.count %} 619 |
    620 |

    标签:

    621 | {% for tag in page.tags.all %} 622 | 623 | {% endfor %} 624 |
    625 | {% endif %} 626 |
    627 | ``` 628 | 629 | 请注意这里所链接到的页面,使用的是内建的`slugurl`而非早先使用的`pageurl`。二者的区别在于,`slugurl`取的是某个页面的别名(slug,来自“Promote”分页)作为参数。而`pageurl`则更为常用,因为他更为明确,且避免了额外的数据库查询。但在该循环的具体情况下,页面对象并不是已可用的,因此这里倒退使用了更少用到的 `slugurl` 标签(the Page object isn't readily available, so we fall back on the less-preferred `slugurl` tag)。 630 | 631 | 现在访问某个带有标签词的博客文章,就会看到在页面底部有了一些带有链接的按钮了 -- 每个按钮对应了一个标签词。但在点击某个按钮是将给出 `404` 错误,这是因为尚未定义一个“tags”视图的原因。将下面的代码加入到 `models.py`: 632 | 633 | ```python 634 | class BlogTagIndexPage(Page): 635 | 636 | def get_context(self, request): 637 | 638 | # 以标签词进行过滤 639 | tag = request.GET.get('tag') 640 | blogpages = BlogPage.objects.filter(tags__name=tag) 641 | 642 | # 更新模板上下文 643 | context = super().get_context(request) 644 | context['blogpages'] = blogpages 645 | return context 646 | ``` 647 | 648 | 请注意此基于页面的模型,并没有定义他自身的字段。就算没有字段,其对`Page`基类的子类化,也令到其成为Wagtail生态的一部分了。因此就可以在管理界面给他一个标题与URL,同时也就可以通过从其`get_context()`方法返回一个`QuerySet`,而对其内容进行操作(note that this Page-based model defines no fields of its own. Even without fields, subclassing `Page` makes it a part of the Wagtail ecosystem, so that you can give it a title and URL in the admin, and so that you can manipulate its contents by returning a QuerySet from its `get_context()` method)。 649 | 650 | 将此改变提交到数据库,然后在管理界面创建一个新的`BlogTagIndexPage`。差不多要将此新的页面/试图,作为站点主页的一个子页面,而与博客首页并排进行创建。在`Promote`分栏给他一个`tags`的别名(slug)。 651 | 652 | 现在去访问`/tags`的话,Django就会告诉你自己已然知道的东西:你需要创建一个`blog/blog_tag_index_page.html`的模板: 653 | 654 | > **注** 实际仍然要放在 `blog/templates/blog/`目录下。 655 | 656 | ```txt 657 | TemplateDoesNotExist at /tags/ 658 | blog/blog_tag_index_page.html 659 | Request Method: GET 660 | Request URL: http://localhost:8000/tags/ 661 | Django Version: 2.1.8 662 | Exception Type: TemplateDoesNotExist 663 | Exception Value: blog/blog_tag_index_page.html 664 | Exception Location: /home/peng/.venv/lib/python3.6/site-packages/django/template/loader.py in get_template, line 19 665 | Python Executable: /home/peng/.venv/bin/python 666 | Python Version: 3.6.7 667 | Python Path: ['/home/peng/wagtail-demo/demo', 668 | '/usr/lib/python36.zip', 669 | '/usr/lib/python3.6', 670 | '/usr/lib/python3.6/lib-dynload', 671 | '/home/peng/.venv/lib/python3.6/site-packages'] 672 | Server time: Wed, 10 Apr 2019 00:43:10 +0000 673 | ``` 674 | 675 | ```html 676 | {% extends "base.html" %} 677 | {% load wagtailcore_tags %} 678 | 679 | {% block content %} 680 | {% if request.GET.tag|length %} 681 |

    显示标签为 “{{ request.GET.tag }}” 页面

    682 | {% endif %} 683 | 684 | {% for blogpage in blogpages %} 685 |

    686 | {{ blogpage.title }}
    687 | 修订于:{{ blogpage.latest_revision_created_at }}
    688 | {% if blogpage.author %} 689 |

    作者: {{ blogpage.author.profile }}

    690 | {% endif %} 691 |

    692 | 693 | {% empty %} 694 | 未发现带有该标签词的文章。 695 | {% endfor %} 696 | {% endblock %} 697 | ``` 698 | 699 | > **注** 管理界面创建的页面 `BlogTagIndexPage` 为什么不是一个新的、如同`blog`一样的应用?为什么`BlogTagIndexPage`对应的模板仍然要放在 `blog/templates/blog`目录下?这是因为 `BlogTagIndexPage`是定义在 `blog/models.py`中的,因此只能将其视为应用`blog`的一部分,而非一个新的应用,同时其模板/视图也应位于 `blog/templates/blog`目录下。 700 | 701 | 这里调用了`Page`模型上内建的 `latest_revision_created_at` 字段 -- 要知道这总是可用的。 702 | 703 | 目前尚未给`BlogPage`模型加上`author`字段,也还没有博客文章作者的个人资料模型 -- 这些将留给读者作为练习。 704 | 705 | 现在点击某个博客文章底部的标签词按钮,就能够渲染出类似于下面的页面了: 706 | 707 | ![标签词首页页面](images/tutorial_9.png) 708 | 709 | ## 分类 710 | 711 | **Categories** 712 | 713 | 下面类给这里的博客加上分类系统(a category system)。与标签特性中某篇文章的作者可以简单地通过在页面上使用某个标签词,而将该标签词引入到页面既有标签词中有所不同,分类特性将会是一个由站点所有者经由管理界面的某个单独区域管理的固定清单(categories will be a fixed list, managed by the site owner through a separate area of the admin interface)。 714 | 715 | 那么首先就要定义一个 `BlogCategory` 模型。某个类别不是有着自身地位的页面,因此要将其定义为标准的Django `models.Model`,而非从`Page`基类加以继承。Wagtail引入了“内容片(Snippets)”这一概念,专门用于那些需要通过管理界面进行管理,但又并不是作为页面树本身的组成部分而存在的,可重用小块内容;那么这类模型就可以通过`@register_snippet`装饰器,而作为内容片进行注册。到目前位置在页面上用到的所有字段类型,都可以用在内容片上 -- 下面将给予每个类别一个图标和名称。将下面的代码加入到 `blog/models.py`: 716 | 717 | ```python 718 | # 新加入 Wagtail 的 @register_snippet 装饰器,有关 Python 装饰器的更多信息,请 719 | # 参考:[使用装饰器](https://github.com/gnu4cn/python-advance-book/blob/master/01-effective-and-fine-python-code.md#%E4%BD%BF%E7%94%A8%E8%A3%85%E9%A5%B0%E5%99%A8) 720 | 721 | from wagtail.snippets.models import register_snippet 722 | 723 | @register_snippet 724 | class BlogCategory(model.Models): 725 | name = models.CharField(max_length=250) 726 | icon = models.ForeignKey( 727 | 'wagtailimages.Image', null=True, blank=True, 728 | on_delete=models.SET_NULL, related_name="+" 729 | ) 730 | 731 | panels = [ 732 | FieldPanel('name'), 733 | ImageChooserPanel('icon'), 734 | ] 735 | 736 | def __str__(self): 737 | return self.name 738 | 739 | class Meta: 740 | verbose_name_plural = '博客类别' 741 | ``` 742 | 743 | > **注意** 请注意这里使用了`panels`而非`content_panels` -- 因为内容块一般不需要诸如别名(slug)或发布日期这类字段,所以他们的编辑界面就不会划分为标准的单独 `conent` / `promote` / `settings` 这样的分页了,且因此就不需要区分`content panels` 与 `promote panels`了 744 | 745 | 将此修改提交到数据库,并经由已经在管理菜单的“Snippets(内容块)”区,创建一些分类。 746 | 747 | 现在就可以将类别作为一个多对多关系字段,加入到 `BlogPage` 模型了。在此字段上使用的字段类型,就是`ParentalManyToManyField` -- 该字段类型,是标准的 Django `ManyToManyField` 字段类型的一个变种,Django的`ManyToManyField`确保所选的对象,与修订历史中的页面记录相对,已在数据库中正确存储,这与一对多关系中使用`ParentalKey`替换`ForeignKey`很类似。 748 | 749 | ```python 750 | # 新加入了 `forms` 与 `ParentalManyToManyField` 751 | from django.db import models 752 | from django import forms 753 | 754 | # 新加入了 ParentalKey、Orderable、InlinePanel与ImageChooserPanel 的导入 755 | # 新加入了 ClusterTaggableManager、TaggedItemBase与MultiFieldPanel 756 | from modelcluster.fields import ParentalKey, ParentalManyToManyField 757 | from modelcluster.contrib.taggit import ClusterTaggableManager 758 | from taggit.models import TaggedItemBase 759 | 760 | # ... 761 | 762 | class BlogPage(Page): 763 | date = models.DateField("发布日期") 764 | intro = models.CharField(max_length=250) 765 | body = RichTextField(blank=True) 766 | tags = ClusterTaggableManager(through=BlogPageTag, blank=True) 767 | categories = ParentalManyToManyField('blog.BlogCategory', blank=True) 768 | 769 | # ... (保留 main_image 与 search_fields 的定义) 770 | def main_image(self): 771 | gallery_item = self.gallery_images.first() 772 | if gallery_item: 773 | return gallery_item.image 774 | else: 775 | return None 776 | 777 | search_fields = Page.search_fields + [ 778 | index.SearchField('intro'), 779 | index.SearchField('body'), 780 | ] 781 | 782 | content_panels = Page.content_panels + [ 783 | MultiFieldPanel([ 784 | FieldPanel('date'), 785 | FieldPanel('tags'), 786 | FieldPanel('categories', widget=forms.CheckboxSelectMultiple), 787 | ], heading="文章信息"), 788 | FieldPanel('intro'), 789 | FieldPanel('body', classname="full"), 790 | InlinePanel('gallery_images', label="图片"), 791 | ] 792 | ``` 793 | 794 | 这里在`FieldPanel` 定义上利用了 `widget` 关键字,用来指定一个基于复选框的小部件,而不是默认的多选框,因为小部件通常被认为更为用户友好。 795 | 796 | 最后,对`blog_page.html`模板加以更新,让他显示出类别: 797 | 798 | ```html 799 | {% with categories=page.categories.all %} 800 | {% if categories %} 801 |

    发表在:

    802 |
      803 | {% for category in categories %} 804 |
    • 805 | {% image category.icon fill-32x32 style="vertical-align: middle" %} 806 | {{ category.name }} 807 |
    • 808 | {% endfor %} 809 |
    810 | {% endif %} 811 | {% endwith %} 812 | ``` 813 | 814 | ![带有类别的博客文章](images/tutorial_10.jpg) 815 | 816 | ## 下一步 817 | 818 | + 阅读Wagtail [使用手册](topics/index.html) 以及 [参考](reference/index.html) 文档 819 | + 学习如何使用 [StreamField](topics/streamfield.html) 来创建自由格式的页面内容 820 | + 浏览 [高级特性](advanced_topics/index.html) 部分并阅读 [第三方教程](advanced_topics/third-party_tutorials.html) 821 | --------------------------------------------------------------------------------