Wagtail

时间:2017-10-28 23:37:43

标签: python django content-management-system wagtail

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

那就是说,这就是我目前正在努力的方面;创建多级/嵌套类别。顺便说一下,如果令人讨厌的核心开发人员可以实现多级别类别创建的简单方法,那将是很好的,而不必为此编写一些vanilla-django hack。

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

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

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

以下是我的型号代码,任何建议或解决方法都绝对有用。 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'),

2 个答案:

答案 0 :(得分:2)

我一直在处理类似的问题 - 除了我们称之为Topic而不是Category,但希望这可以帮助你解决问题。

解决方案摘要

  • 使用Django-Treebeard library管理您的树,它们最多可嵌套63级,并可让您完全访问api,例如get_childrenis_root
  • 您需要覆盖某些行为,以便创建和移动'节点,最好由base_form_class override完成。
  • 我已经使用了ModelAdmin,但是如果它们是片段,它也应该可以正常工作,但如果你想添加更复杂的编辑,ModelAdmin可以为你的未来提供更多控制。
  • 最后,您可以使用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函数

这可确保在Wagtail Admin中使用上一代码中的TopicAdmin。您将知道它的工作方式将显示在左侧管理侧栏modeladmin register docs

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


modeladmin_register(TopicAdmin)

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

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

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

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进行了测试,但它确实有效。

答案 1 :(得分:0)

值得注意的是,Collection hierarchy正在进行工作,但这对图像/文档来说会更多。

它已被标记为2.0版本。

https://github.com/wagtail/wagtail/pull/3407