Alan Hou的个人博客

Django 3网页开发指南第4版 第8章 层级结构

完整目录请见:Django 3网页开发指南 – 第4版

本章中包含如下小节:

引言

无论你是在构建自己的博客、评论插件或分类系统,总会碰到需要在数据库中进行层级结构保存的问题。虽然在关联型数据库(如MySQL和PostgreSQL)中数据表是平级的,却有一种快速有效存储层级结构的方式。称为预排序遍历树(MPTT)。MPTT让我们可以不用对数据库进行递归调用就可读取树状结构。

首先,我们来熟悉一下树状结构的用词。树状数据结构是一种节点的嵌套集合,从根节点开始并对子节点进行引用。有很多限制:例如,节点不应引用回去产生循环引用,也不应重复引用。以下是其它应当知道的用词:

下面来讲解下MPTT的原理。设置一下横向平铺树结构,根节点放在顶端。树中的每个节点有左值和右值。把它们想象为节点左右侧的左右把手。然后你逆时针从根节点开始行走(遍历),交对左值和右值进行数字标记:1、2、3,依此类推。类似下面的图表这样:

在层级结构的数据表中,每个节点拥有有标题、左值和右值。

此时,如果希望获取左值为2、右值为11的B节点的子树,需要选取所有左值位于2和11之间的节点。即为 C、D、E和F。

要获取左值为5、右值为10的 D 节点的祖先节点,需要选取所有左值小于5、右值大于10的节点。即B和A。

获取一个节点的后代节点数量,可以使用如下公式:

$$descendants = (right – left – 1) / 2$$

因此B节点的后代节点数量可以通过如下公式来进行计算:

$$(11 – 2 – 1) / 2 = 4$$

如果想要将E节点添加到C节点,我们需要更新其第一个共有祖先节点 B下节点的左右值。这样 C 节点的左值依然是3;E 节点的左值为4、右值为5;C 节点的右值变成了6;D 节点的左值变成了7;F 节点的左值依然是8;其它均保持不变。

类似地在MPTT中还有树相关的运算。在项目中靠自己管理所有层级结构会极为复杂。所幸有一个名为django-mptt 的Django应用长期用于处理这些算法并提供有直接的 API 供处理树状结构来使用。另一个应用django-treebeard也在django CMS 3.1中替换了MPTT之后被广泛使用和测试,成为一个有力的替代应用。本章中,我们将学习如何使用这些帮助应用。

技术要求

运行本章的代码要求安装最新稳定版的Python 3、MySQL或PostgreSQL数据库以及通过虚拟环境创建的Django项目。

可在GitHub仓库的Chapter08目录中查看本章的代码。

使用django-mptt创建分类层级

为讲解如何处理MPTT,我们对第3章 表单和视图中的ideas应用做进一步构建。我们会将分类修改为层级Category模型并更新Idea模型来建立与分类的多对多关联。你也可以从头创建该应用,仅使用这里所展示的内容从头实现一个非常基础版本的Idea模型。

准备工作

执行如下步骤:

  1. 使用如下命令在虚拟环境中安装django-mptt:
  2. 如尚未创建请创建categories和ideas应用。将这些应用以及mptt添加到配置文件的INSTALLED_APPS中,如下所示:

如何实现…

我们将创建一个层级Category模型并将其绑定到Idea模型,即与分类形成多对多关联,如下:

  1. 打开categories应用中的models.py文件并添加继承mptt.models.MPTTModel 和第2章 模型和数据库结构中所定义的CreationModificationDateBase。除mixin中的字段外,Category需要有一个TreeForeignKey类型的parent字段和title字段:
  2. 更新Idea模型来包含TreeManyToManyField类型的categories字段:
  3. 通过迁移和运行来更新数据库:

实现原理…

MPTTModel mixin会对Category模型添加tree_id、lft、rght和level字段:

通过MPTT中的元信息选项order_insertion_by,我们保证了在新增分类时会按标题的字母排序。

除新字段外,MPTTModel mixin添加了类似使用JavaScript导航DOM元素的方法来在树状结构之间导航。这些方法如下:

所有这些方法均可在视图、模板或管理命令中使用。如果想要操作树状结构,还可以使用insert_at() 和 move_to()方法。可以阅读https://django-mptt.readthedocs.io/en/stable/models.html了解更多有关这些方法及树管理方法的内容。

上例的模型中,我们使用了TreeForeignKey和TreeManyToManyField。它们类似ForeignKey和ManyToManyField,不同点在于在后台界面中会以缩进的层级来显示各选项。

同时注意看Category模型的Meta类,我们先以tree_id然后以lft值对分类进行排序,来让分类在树状结构自然地展示出来。

相关内容

使用django-mptt-admin创建分类后台界面

django-mptt应用内置一个简易模型后台mixin,让我们可以创建一个树状结构并以缩进展示。要对树进行重新排序,需要自己创建这一功能或使用第三方解决方案。django-mptt-admin可以让我们对层级模型创建一个可拖拽的后台。下面在本节进行学习。

准备工作

首先按照前面使用django-mptt创建分类层级一节中讲解的方法配置categories应用。然后需要按照如下步骤来安装django-mptt-admin应用:

  1. 使用如下命令在虚拟环境中安装该应用:
  2. 在配置的INSTALLED_APPS中添加该应用,如下:
  3. 确保可在项目中访问django-mptt-admin 的静态文件:

如何实现…

创建admin.py文件,在其中对Category模型定义后台界面。它将继承DjangoMpttAdmin而不是admin.ModelAdmin,如下:

实现原理…

该分类的后台界面有两种模式:树状视图和网格视图。树状视图类似下图:

TODO

树状视图使用jqTree jQuery库来进行节点操作。可以展开及收起分类来获取更好的总览效果。可以在列表视图中拖拽标题来重新排序或修改依赖关系。在重新排序时,用户界面(UI)类似下图:

TODO

注意任何常规列表相关的配置,如list_display或list_filter会在树状视图中被忽略掉。同时,由order_insertion_by meta所控制的排序会被手动排序所覆盖。

如果希望过滤分类、按具体字段排序或应用后台动作,可以切换为网格视图,它显示默认的分类修改列表,如下图所示:

TODO

相关内容

使用django-mptt在模板中渲染分类

在应用内创建好分类后,需要在模板中按层级显示这些分类。最简单的方式是使用MPTT树,在使用django-mptt创建分类层级一节中进行过表述,使用的是django-mptt 应用中的{% recursetree %} 模板标签。本节中将演示如何实现。

准备工作

确保已有categories和ideas应用。Idea模型和Category模型之间应该像使用django-mptt创建分类层级一节中那样具有多对多关联。在数据库中添加一些分类。

如何实现…

将层级分类的QuerySet传递到模板中,然后使用{% recursetree %} 模板标签如下:

  1. 创建加载所有分类的视图并传递至模板中:
  2. 使用如下内容创建模板来输出分类的层级:
  3. 创建URL规则来显示视图:

实现原理…

模板会被渲染为嵌套列表,如下图所示:

TODO

{% recursetree %}版块模板标签接收分类的QuerySet并使用嵌套在该标签中的模板内容渲染列表。这里使用了两个特殊变量:

扩展知识…

如果层级结构非常复杂,有20多层,推荐使用 {% full_tree_for_model %} 和 {% drilldown_tree_for_node %} 迭代标签或非递归的tree_info模板过滤器。

ℹ️有关更多如何实现的内容,请参见官方文档

相关内容

通过django-mptt使用单选项在表单中选取单个分类

如果希望在表单中显示分类选项会怎样呢?如何展示层级呢?在django-mptt中,有一个特殊的TreeNodeChoiceField表单,可用于在表单选项字段中显示层级结构。下面就来学习如何实现。

准备工作

我们使用前面小节中定义的categories和ideas应用。本节我们需要用到django-crispy-forms。参见第3章 表单和视图通过django-crispy-forms创建表单布局一节来了解如何进行安装。

如何实现…

我们对第3章 表单和视图过滤对象列表一节所创建的过滤表单进行改进,添加一个分类过滤:

  1. 在ideas应用的forms.py文件中,创建一个带有category字段的表单,如下:
  2. 我们应该已经创建了IdeaListView,其相关联的URL规则以及显示这一表单的idea_list.html 模板。使用 {% crispy %} 模板标签在模板中渲染此过滤器表单,如下:

实现原理…

下拉菜单的分类选项效果如下所示:

TODO

TreeNodeChoiceField有些像ModelChoiceField,但它以缩进的方式显示层级选项。默认TreeNodeChoiceField在深一级的内容之前添加三个短横线—。在本例中,我们通过对字段传递level_indicator参数将层级分隔符修改为了无间断空格(  HTML实体)。为保证无间断空格不被转义,我们使用了mark_safe()函数。

相关内容

通过django-mptt使用复选框列表在表单中选取多个分类

在表单中需要选取一个或多个分类,可以使用django-mptt所提供多项选择字段TreeNodeMultipleChoiceField。但多项选择字段(如<select multiple>)从界面显示角度来说不够用户友好,因为用户需要滚动并在点选多个选择时按下control或command键。尤其是在需要从非常大量的子项中进行选项而用户又希望一次选择多个时,或者是对于残障人士,如运动感知障碍的人,那将是非常糟糕的用户体验。更好的方式是提供一个复选框列表来供用户选择分类。本节中,我们将创建一个可在表单中显示缩进的层级树状结构复选框。

准备工作

我们使用前面小节中所定义的categories和ideas应用以及项目中应该早已存在的core应用。

如何实现…

渲染带有复选框的分类缩进列表,我们将创建、使用新的MultipleChoiceTreeField表单字段并为该字段创建一个HTML模板。

具体的模板会传递给表单中的crispy_forms布局。按照如下步骤来实现:

  1. 在core应用中,添加form_fields.py文件并创建继承ModelMultipleChoiceField的MultipleChoiceTreeField表单字段,如下:
  2. 使用具有分类的新字段来在创建idea的新表单中进行选择。同时,在表单布局中,将自定义模板传递给categories字段,如下所示:
  3. 基于crispy模板创建一个Bootstrap样式的复选框列表模板,bootstrap4/layout/checkboxselectmultiple.html,如下所示:
  4. 使用刚刚创建的表单新建 一个添加idea的视图:
  5. 添加相关联的模板来显示带有{% crispy %}模板标签的表单,其使用可参见第3章 表单和视图通过django-crispy-forms创建表单布局一节进行了解:
  6. 我们还需要一个指向新视图的URL规则,如下:
  7. 在CSS文件中添加规则设置margin-left参数,使用复选框树字段模板所生成的class对标签缩进显示,如.level-0、.level-1和 .level-2。确保设置的CSS类的值充分考虑到所在上下文中最深节点的缩进,如下:

实现原理…

产生的表单如下:

TODO

不同于Django默认将字段生成硬编码到Python代码中,django-crispy-forms应用使用模板来渲染字段。可以在crispy_forms/templates/bootstrap4下进行查看,并在需要时拷贝至项目模板目录对应的路径下进行修改。

在idea的创建和编辑表单中,我们对categories字段传递了一个自定义表单,它会在包裹复选框的<label>标签中添加.level-* CSS类。正常的CheckboxSelectMultiple微件的问题是,在渲染时它只使用选项值和选项文本,而我们需要用到分类的其它属性,如深度。解决这一问题,我们还创建了一个MultipleChoiceTreeField表单字段,它继承ModelMultipleChoiceField并重写了label_from_instance() 方法来返回分类实例本身,而非其Unicode值。字段的模板看起来很复杂,但是大多中只是使用必要的Bootstrap标记对多复选框模板(crispy_forms/templates/bootstrap4/layout/checkboxselectmultiple.html)的修改。我们主要是进行了添加.level-* CSS 类的微调。

相关内容

通过django-treebeard创建层级分类

树状算法有多种算法,各有各的优点。django CMS所使用的名为django-treebeard的应用,是django-mptt的一个替代,它提供3种树状表单:

为演示它支持所有这3种算法,我们将使用django-treebeard及其对应的API。我们对第3章 表单和视图中的categories应用进行扩展。在修改中,我们通过所支持的树算法来改善Category模型对层级的支持。

准备工作

执行如下步骤:

  1. 使用如下命令在虚拟环境中安装django-treebeard:
  2. 如尚未创建请创建categories和ideas应用。在配置文件的INSTALLED_APPS中添加categories应用及treebeard,如下:

如何实现…

我们使用物化路径算法来改进Category模型,如下:

  1. 打开models.py文件、更新Category模型来继承 treebeard.mp_tree.MP_Node模型,而非标准的Django模型。它还应当继承第2章 模型和数据库结构中所定义的CreationModificationDateMixin。除mixin中的字段外,Category模型中还需要有一个title字段:
  2. 这要求对数据库进行更新,因此接下来我们将对categories应用进行迁移:
  3. 通过使用抽象模型继承,treebeard树节点可以使用标准关联与其它模型建立关联。因此Idea模型可以继续与Category之间存在简单的ManyToManyField关联:

实现原理…

MP_Node抽象模型提供有path、depth和numchild字段,以及steplen、alphabet和node_order_by属性,供Category模型构建树所需:

path、depth和numchild应被看作只读。同时在将第一对象保存到树中之后不应再修改steplen、alphabet和node_order_by的值,否则数据会崩溃。

除新字段和属性外,MP_Node抽象类通过树状结构添加了用于导航的方法。这些方法的一些重要示例列举如下:

扩展知识…

本节只涉及到强大的django-treebeard及其物化路径树很浅的部分。用于导航和构建树还存在很多其它方法。此外,物化路径树的API与内嵌集合树和相邻列表树大多相同,只需要将MP_Node换成NS_Node或AL_Node抽象类来分别进行使用即可使用。

ℹ️阅读django-treebeard API文档查看每个树实现的完整可用属性和方法。

相关内容

通过django-treebeard创建基本分类后台界面

django-treebeard应用自带有TreeAdmin,继承自标准的ModelAdmin。这让我们可以在后台界面中查看树状节点层级,并按照所使用的树算法展示界面功能。下面就在本节中进行学习。

准备工作

首先按照本章前面通过django-treebeard创建层级分类一节所讲解的配置categories应用和django-treebeard。同时要确保在项目中可使用django-treebeard的静态文件:

如何实现…

通过继承treebeard.admin.TreeAdmin(替换默认的admin.ModelAdmin)并使用自定义表单工厂创建categories应用Category模型的后台界面,如下:

实现原理…

根据所使用的实现树后台界面中分类存在两种模式。对物化路径和嵌套集合树提供了一个高级UI,如下所示:

TODO

这一高级视图让我们可以按更好的总览展开或收起分类。可以通过拖拽标题来重新排序或改变依赖关系。在重新排序时,用户界面类似下图:

TODO

如果通过具体字段对分类应用过滤或排序,高级功能会无法使用,但美观的页面和高级页面仍会保留。可以看下面的中间页面,仅显示过去7天创建的分类:

TODO

但如若使用了相邻列表算法,会出现基本版UI,呈现的美观度略差,并且高级UI中所具有的切换和排序功能都没有了。

ℹ️有关django-treebeard后台的更多详情,包括基于界面的截图,请参见官方文档

相关内容

退出移动版