python - 鹡鸰中的多级分类

标签 python django content-management-system wagtail

在来这里寻求建议之前,我总是确保我已经尝试了所有可能的途径。

也就是说,这就是我目前正在努力解决的问题;创建多级/嵌套类别。顺便说一句,如果 wagtail 核心开发人员能够实现一种创建多级类别的简单方法,而无需我们为其编写一些 vanilla-django hack,那就太好了。

我已经在这个应用程序上工作了几个星期,一切运行顺利,除了现在,有一个要实现嵌套类别的业务决策。

我最初的 M.O 是创建一个 ServiceCategoryIndex 页面,一个 ServiceCategoryPage,然后使 ServiceIndex 页面成为 ServiceCategoryIndex 页面的后代或可订购的 ServiceCategoryPage,这似乎不对。

经过几次迭代,我回到我的默认模型,然后尝试使用 View 和 url 的类别 url,如 vanilla-django,问题是,我无法通过直通查询外键模板上的关系,所以我仍然无法将服务页面的内容作为列表查询集呈现出来。

下面是我的模型代码,对此的任何建议或解决方法都绝对有帮助。 P.S:我几乎要用 vanilla-django 重写整个项目,因为我在接下来的几天内找不到解决方案。

def get_service_context(context):
    context['all_categories'] = ServiceCategory.objects.all()
    context['root_categories'] = ServiceCategory.objects.filter(
    parent=None,
    ).prefetch_related(
    'children',
    ).annotate(
    service_count=Count('servicepage'),
    )
    return context

class ServiceIndexPage(Page):
    header_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    heading = models.CharField(max_length=500, null=True, blank=True)
    sub_heading = models.CharField(max_length=500, null=True, blank=True)
    body = RichTextField(null=True, blank=True)

    def get_context(self, request, category=None, *args, **kwargs):
        context = super(ServiceIndexPage, self).get_context(request, *args, **kwargs)

        services = ServicePage.objects.child_of(self).live().order_by('-first_published_at').prefetch_related('categories', 'categories__category')
        if category is None:
            if request.GET.get('category'):
                category = get_object_or_404(ServiceCategory, slug=request.GET.get('category'))
        if category:
            if not request.GET.get('category'):
                category = get_object_or_404(ServiceCategory, slug=category)
            services = services.filter(categories__category__name=category)

        # Pagination
        page = request.GET.get('page')
        page_size = 10
        if hasattr(settings, 'SERVICE_PAGINATION_PER_PAGE'):
            page_size = settings.SERVICE_PAGINATION_PER_PAGE

        if page_size is not None:
            paginator = Paginator(services, page_size)  # Show 10 services per page
            try:
                services = paginator.page(page)
            except PageNotAnInteger:
                services = paginator.page(1)
            except EmptyPage:
                services = paginator.page(paginator.num_pages)


        context['services'] = services
        context['category'] = category
        context = get_service_context(context)

        return context


@register_snippet
class ServiceCategory(models.Model):
    name = models.CharField(max_length=250, unique=True, verbose_name=_('Category Name'))
    slug = models.SlugField(unique=True, max_length=250)
    parent = models.ForeignKey('self', blank=True, null=True, related_name="children")
    date = models.DateField(auto_now_add=True, auto_now=False, null=True, blank=True)
    description = RichTextField(blank=True)

    class Meta:
        ordering = ['-date']
        verbose_name = _("Service Category")
        verbose_name_plural = _("Service Categories")

    panels = [
        FieldPanel('name'),
        FieldPanel('parent'),
        FieldPanel('description'),
    ]

    def __str__(self):
        return self.name

    def clean(self):
        if self.parent:
            parent = self.parent
            if self.parent == self:
                raise ValidationError('Parent category cannot be self.')
            if parent.parent and parent.parent == self:
                raise ValidationError('Cannot have circular Parents.')

    def save(self, *args, **kwargs):
        if not self.slug:
            slug = slugify(self.name)
            count = ServiceCategory.objects.filter(slug=slug).count()
            if count > 0:
                slug = '{}-{}'.format(slug, count)
            self.slug = slug
        return super(ServiceCategory, self).save(*args, **kwargs)

class ServiceCategoryServicePage(models.Model):
    category = models.ForeignKey(ServiceCategory, related_name="+", verbose_name=_('Category'))
    page = ParentalKey('ServicePage', related_name='categories')
    panels = [
        FieldPanel('category'),
    ]



class ServicePage(Page):
     header_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Header image')
    )
    service_title = models.CharField(max_length=300, null=True, blank=True)
    body = StreamField([
        ('h1', CharBlock(icon="title", classanme="title")),
        ('h2', CharBlock(icon="title", classanme="title")),
        ('h3', CharBlock(icon="title", classanme="title")),
        ('h4', CharBlock(icon="title", classanme="title")),
        ('h5', CharBlock(icon="title", classanme="title")),
        ('h6', CharBlock(icon="title", classanme="title")),
        ('paragraph', RichTextBlock(icon="pilcrow")),
        ('aligned_image', ImageBlock(label="Aligned image", icon="image")),
        ('pullquote', PullQuoteBlock()),
        ('raw_html', RawHTMLBlock(label='Raw HTML', icon="code")),
        ('embed', EmbedBlock(icon="code")),
])
    date = models.DateField("Post date")
    service_categories = models.ManyToManyField(ServiceCategory, through=ServiceCategoryServicePage, blank=True)

    feed_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Feed image')
    )

    search_fields = Page.search_fields + [
        index.SearchField('body'),
        index.SearchField('service_title'),
        index.SearchField('title'),]


    def get_absolute_url(self):
        return self.url


    def get_service_index(self):
        # Find closest ancestor which is a service index
        return self.get_ancestors().type(ServiceIndexPage).last()


    def get_context(self, request, *args, **kwargs):
        context = super(ServicePage, self).get_context(request, *args, **kwargs)
        context['services'] = self.get_service_index().serviceindexpage
        context = get_service_context(context)
        return context

    class Meta:
        verbose_name = _('Service page')
        verbose_name_plural = _('Services pages')

    parent_page_types = ['services.ServiceIndexPage']


ServicePage.content_panels = [
    FieldPanel('title', classname="full title"),
    FieldPanel('service_title'),
    ImageChooserPanel('header_image'),
    FieldPanel('date'),
    InlinePanel('categories', label=_("Categories")),
    StreamFieldPanel('body'),
    ImageChooserPanel('feed_image'),

]

最佳答案

我一直在研究类似的问题 - 除了我们将它们称为 Topic 而不是 Category 但希望这对您有所帮助。

解决方案总结

  • 使用Django-Treebeard library为了管理您的树,它们最多可以嵌套 63 层,并让您可以完全访问 API 以获取诸如 get_childrenis_root 之类的东西。
  • 您需要覆盖一些创建和“移动”节点的行为,这最好由 base_form_class override 完成。 .
  • 我用过ModelAdmin为此,如果是代码片段,它应该也能正常工作,但是如果您想添加更复杂的编辑,ModelAdmin 可以让您对 future 有更多的控制权。
  • 最后,您可以使用 ForeignKey 或其他一些相关链接将这些主题/类别链接到您的页面。
  • 注意事项:在此示例中,除了字母顺序之外,没有对子节点进行重新排序,这可以添加,但它有点复杂,因为您需要一个 UI - 因此使用 ModelAdmin。另外,你永远不应该让用户删除根,它会删除所有节点。
  • Django Treebeard Caveats - 值得一读

1 - 构建模型

我有一个专用的 Topics 应用程序,但您可以将它放在任何 models.py 中。请参阅解释代码的注释。

from __future__ import unicode_literals

from django import forms
from django.core.exceptions import PermissionDenied
from django.db import models

from treebeard.mp_tree import MP_Node

from wagtail.contrib.modeladmin.options import ModelAdmin
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin.forms import WagtailAdminModelForm


# This is your main 'node' model, it inherits mp_node
# mp_node is short for materialized path, it means the tree has a clear path
class Topic(MP_Node):
    """
        Topics can be nested and ordered.
        Root (id 1) cannot be deleted, can be edited.
        User should not edit path, depth, numchild directly.
    """

    name = models.CharField(max_length=30)
    is_selectable = models.BooleanField(default=True)  # means selectable by pages
    # any other fields for the Topic/Category can go here
    # eg. slug, date, description

    # may need to rework node_order_by to be orderable
    # careful - cannot change after initial data is set up
    node_order_by = ['name']

    # just like any model in wagtail, you will need to set up panels for editing fields
    panels = [
        FieldPanel('parent'),  # parent is not a field on the model, it is built in the TopicForm form class
        FieldPanel('name', classname='full'),
        FieldPanel('is_selectable'),
    ]

    # this is just a convenience function to make the names appear with lines
    # eg root | - first child
    def name_with_depth(self):
        depth = '— ' * (self.get_depth() - 1)
        return depth + self.name
    name_with_depth.short_description = 'Name'

    # another convenience function/property - just for use in modeladmin index
    @property
    def parent_name(self):
        if not self.is_root():
            return self.get_parent().name
        return None

    # a bit of a hacky way to stop users from deleting root
    def delete(self):
        if self.is_root():
            raise PermissionDenied('Cannot delete root topic.')
        else:
            super(Topic, self).delete()

    # pick your python string representation
    def __unicode__(self):
        return self.name_with_depth()

    def __str__(self):
        return self.name_with_depth()

    class Meta:
        verbose_name = 'Topic'
        verbose_name_plural = 'Topics'


# this class is the form class override for Topic
# it handles the logic to ensure that pages can be moved
# root pages need to be treated specially
# including the first created item always being the root
class TopicForm(WagtailAdminModelForm):

    # build a parent field that will show the available topics
    parent = forms.ModelChoiceField(
        required=True,
        empty_label=None,
        queryset=Topic.objects.none(),
    )

    def __init__(self, *args, **kwargs):
        super(TopicForm, self).__init__(*args, **kwargs)
        instance = kwargs['instance']
        all = Topic.objects.all()
        is_root = False

        if len(all) == 0 or instance.is_root():
            # no nodes, first created must be root or is editing root
            is_root = True

        if is_root:
            # disable the parent field, rename name label
            self.fields['parent'].empty_label = 'N/A - Root Node'
            self.fields['parent'].disabled = True
            self.fields['parent'].required = False
            self.fields['parent'].help_text = 'Root Node has no Parent'
            self.fields['name'].label += ' (Root)'
        else:
            # sets the queryset on the parent field
            # ensure that they cannot select the existing topic as parent
            self.fields['parent'].queryset = Topic.objects.exclude(
                pk=instance.pk)
            self.fields['parent'].initial = instance.get_parent()

    def save(self, commit=True):
        parent = self.cleaned_data['parent']
        instance = super(TopicForm, self).save(commit=False)
        all = Topic.objects.all()

        is_new = instance.id is None
        is_root = False
        if is_new and len(all) == 0:
            is_root = True
        elif not is_new and instance.is_root():
            is_root = True

        # saving / creating
        if is_root and is_new and commit:
            # adding the root
            instance = Topic.add_root(instance=instance)
        elif is_new and commit:
            # adding a new child under the seleced parent
            instance = parent.add_child(instance=instance)
        elif not is_new and instance.get_parent() != parent and commit:
            # moving the instance to under a new parent, editing existing node
            # must use 'sorted-child' - will base sorting on node_order_by
            instance.move(parent, pos='sorted-child')
        elif commit:
            # no moving required, just save
            instance.save()

        return instance


# tell Wagtail to use our form class override
Topic.base_form_class = TopicForm


class TopicAdmin(ModelAdmin):
    model = Topic
    menu_icon = 'radio-empty'
    menu_order = 200
    add_to_settings_menu = False
    list_display = ['name_with_depth', 'parent_name']
    search_fields = ['name']

2 - 在wagtail_hooks.py

中注册modeladmin函数

这确保了前面代码中的 TopicAdmin 在 Wagtail Admin 中使用。你会知道它是有效的,因为它会出现在左侧管理侧边栏上 modeladmin register docs .

from wagtail.contrib.modeladmin.options import modeladmin_register
from .models import TopicAdmin


modeladmin_register(TopicAdmin)

3 - 迁移并创建第一个主题

现在是进行迁移和运行迁移的好时机,请记住 node_order_by 在构建模型后不容易更改。如果您想添加 child 的自定义排序,例如。重新排序容量或按其他字段排序,请在迁移之前执行此操作。

然后进入管理并创建第一个根节点。

4 - 链接到您的页面

这是一个快速而讨厌的示例,让您可以将一个主题链接到一个页面,而不需要任何花哨的东西。请注意,我们在这里限制了选择,这可以扩展为根据您在主题中设置的字段进行更复杂的限制。

topic = models.ForeignKey(
    'topics.Topic',
    on_delete=models.SET_NULL,
    blank=True,
    null=True,
    limit_choices_to={'is_selectable': True},
    related_name='blog_page_topic',
)

5 - 改进空间

  • 主题字符串表示总是包含破折号以显示其“深度”,这在其他地方看起来有点难看。最好使用扩展字段类型并仅在需要时构建此表示。
  • 如前所述,无法手动重新排序子节点,您可以在模型管理中创建自定义按钮,这样就可以添加向上/向下移动的按钮并以这种方式工作。
  • 示例代码,可能有些粗糙,但应该足以让您入门。我已经在演示应用程序的 Wagtail 1.13 上对此进行了测试,它可以正常工作。

关于python - 鹡鸰中的多级分类,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46995540/

相关文章:

django - 如何将自定义表单添加到 django 管理页面内联

python - Wagtail 查询 - 日期范围、类型选择和限制

google-analytics - 我们可以在内部网站上使用谷歌分析吗?

java - Magnolia 自定义内容类型作为属性

php - 如何更新mySQL表中单元格值大于n的行?

python - virtualenv 中的 django 出现 apache mod_wsgi 错误

python - 如何使用 Bottle 获取请求正文?

python - DRF : Cannot POST or create a new object through admin interface

python - 如何在列表中查找特定项目并使用该项目执行操作

python - 根据先前的列集创建多个新列(更有效)