django-treebeard 物化路径节点的动态排序

Dynamic ordering of django-treebeard materialized path nodes

我有一个基于 django-oscar(和 django-cms)的项目,它使用 django.contrib.sites 模块在具有不同 SITE_ID 的多个域上运行。该项目已经富有成效,我不能再更改类别 slugs - 我希望避免将整个代码切换到嵌套集树或相邻树 - 为了您更好地理解:初始要求不需要不同的类别排序对于每个域,所以我只使用 Oscars 默认类别实现。

但是由于 Oscars 类别 model/manager 是基于 django-treebeards materialized path tree 实现的,我必须考虑一些与更改默认排序的常用 Django 方式的不同之处。

给定模型中的这两个额外字段(继承自 django-oscars AbstractCategory)

class Category(AbstractCategory):
    # ...
    order_site_1 = models.IntegerField(
        null=False,
        default=0
    )
    order_site_2 = models.IntegerField(
        null=False,
        default=0
    )

我不能简单地将排序添加到元 class 中,例如:

class Category(AbstractCategory):
    class Meta:
        ordering = ['depth', '-order_site_{}'.format(Site.objects.get_current().id), 'name']

首先,它将忽略此指令,因为 treebeards MP_NodeManager.get_queryset() 不遵守自定义排序 - 对于 MP_Tree 逻辑,它必须依赖插入时生成的排序(存储在 path) 中,因此这种方法违背了 MP_Tree 本身的目的。

我也看过 node_order_by - 但正如 docs 中所述:

Incorrect ordering of nodes when node_order_by is enabled. Ordering is enforced on node insertion, so if an attribute in node_order_by is modified after the node is inserted, the tree ordering will be inconsistent.

但是对于具有不同类别排序的多个域,这同样没有用。我能想到的唯一方法是覆盖 MP_NodeManager class:

class CustomOrderQuerySet(MP_NodeManager):
    def get_queryset(self):
        sort_key = '-order_site_{}'.format(Site.objects.get_current().id)
        return MP_NodeQuerySet(self.model).order_by('depth', sort_key, 'name')

但这种方法显然是错误的,因为 MP_Tree 依赖于 path 排序来生成合适的树——我想的是遍历所有条目,比较 path,忽略最后一部分并根据 order_site_{id} 排序并将其重新转换为 QuerySet.

我什至不确定这是否可能,我想避免去那里,所以我的问题是:有没有办法在 ORM 语句链中反映这种逻辑,以便我可以继续使用本地人 QuerySet?

所以这个问题一开始让我很头疼 - 我尝试了不同的方法,但没有一个是令人满意的 - 尝试动态排序 MP_Tree 是我现在非常不鼓励的事情,它太乱了。最好坚持在插入时创建的顺序,并在该阶段处理所有变量。我还要感谢@solarissmoke 在评论中提供的帮助。

模特

class Product(AbstractProduct):
    # ...
    @property
    def site_categories(self):
        return self.categories.filter(django_site_id=settings.SITE_ID)

    @property
    def first_category(self):
        if not self.site_categories:
            return None
        return self.site_categories[0]

class Category(AbstractCategory):
    # ...
    django_site = models.ForeignKey(Site, null=False)
    site_rank = models.IntegerField(_('Site Rank'), null=False, default=0)
    node_order_by = ['django_site_id', 'site_rank', 'name']

模板标签

复制templatetags/category_tags#get_annotated_list到项目中,稍作修改:

# ...
start_depth, prev_depth = (None, None)
if parent:
    categories = parent.get_descendants()
    if max_depth is not None:
        max_depth += parent.get_depth()
else:
    categories = Category.get_tree()

# Just add this line
categories = categories.filter(django_site_id=settings.SITE_ID)
# ...

模板

对于 templates/catalogue/detail.html:将 {% with category=product.categories.all.0 %} 替换为 {% with category=product.first_category %}

管理员

您需要对 apps.dashboard 中的视图、表单和表单集进行更改,以处理新添加的 django_sitesite_rank - 我将其遗漏了,因为在我的案例中所有类别都是通过定期导入的 CSV 定义的。完成这应该没什么大不了的 - 也许以后我会这样做并更新这个答案。

正在创建 MP 节点

无论何时添加根节点、子节点或兄弟节点,现在都需要传入 django_site_id 参数(以及 node_order_by 中包含的所有其他必需值),例如:

parent_category.add_child(
    django_site_id=settings.SITE_ID, site_rank=10, name='Child 1')
Category.add_root(
    django_site_id=settings.SITE_ID, site_rank=1, name='Root 1')