2017-10-28 22 views
1

私はアドバイスのためにここに来る前にすべての可能な道を必ず試みたことを確認します。Wagtailの複数レベルのカテゴリ

これは、私が現在苦労していることです。マルチレベル/ネストされたカテゴリを作成します。さて、wagtailのコア開発者が多レベルのカテゴリ作成のための簡単な方法を実装できれば、バニラとジャンゴのハックを書く必要はありません。

私はこのアプリケーションを数週間使っていますが、すべてがスムーズに実行されますが、ネストされたカテゴリを実装するビジネス上の決定があります。

私の最初のM.Oは、ServiceCategoryPageページを作成し、次にServiceCategoryPageを作成して、ServiceCategoryPageとしてのServiceCategoryIndexページの子孫または注文可能にしました。

2つの繰り返しの後、私はデフォルトモデルに戻り、ビューとURLを使ってカテゴリのURLを試してみました。問題はバニラ - ジャンゴのようなものです。私は外部キーにテンプレート上の関係を介して、私はまだリストのクエリセットとしてレンダリングされるサービスページの内容を取得することはできません。

ここに私のモデルコードはありますが、これに関するいかなる提案や回避策も絶対に役立ちます。 P.S:私はほぼバニラ・ジャンゴでプロジェクト全体を書き直す時点にあり、数日以内に解決策を見つけることはできません。

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'), 

]私は同様の問題に取り組んでいます

+0

いくつのレベルのネスティングが必要ですか?また、カテゴリ 'グループ'または最低レベルの 'カテゴリ'を選択する必要がありますか? –

+0

理想的には、ネスティングのレベルは可能な限り大きくする必要がありますが、2は良いでしょう。そして、うん、カテゴリグループ。 –

答えて

0

Collection hierarchyについては進行中の作業があることに注意する価値はあるかもしれませんが、これはImages/Documentsの方が多くなります。

2.0リリースでフラグが設定されています。

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

0

- 私たちはそれらを呼び出している以外は、Topic代わりのCategoryうまくいけば、これはあなたを助けます。

ソリューション

の概要
  • あなたの木を管理するためにDjango-Treebeard libraryを使用して、彼らは深い63のレベルまでネストすることができ、get_childrenまたはis_rootのようなもののためにあなたのAPIへのフルアクセスを提供します。
  • ノードを作成して移動するには、いくつかの動作を無効にする必要があります。これはbase_form_class overrideで行うのが最適です。
  • 私はこのためにModelAdminを使用しましたが、スニペットの場合はうまく動作しますが、複雑な編集を追加する場合は、ModelAdminを使用して将来の管理を強化できます。
  • 最後に、これらのトピック/カテゴリを、ForeignKeyやその他の関連リンクを含むページにリンクすることができます。
  • 警告:この例では、アルファベット以外の子ノードの並べ替えはできませんが、追加することはできますが、UIを必要とするため少し複雑です。したがってModelAdminを使用します。また、ユーザーがルートを削除しないようにする必要があります。すべてのノードが削除されます。
  • Django Treebeard Caveats - 私は、専用のTopicsアプリを持っていますが、あなたはどのmodels.pyでこれを置くことができるモデル

    の構築 -

1を読む価値。コード全体を説明するコメントを参照してください。

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

これは、前のコードでTopicAdminはセキレイ管理に使用されることを確実にModelAdminの機能を登録します。左側の管理サイドバーmodeladmin register docsに表示されるように動作することがわかります。

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


modeladmin_register(TopicAdmin) 

3 - &は、まずトピック

を作成して移行

今すぐ移行を作り、あなたの移行を実行し、node_order_byはあなたのモデルを構築した後に変更することは容易ではないことを覚えておくことは良い時間になるでしょう。子供のカスタム注文を追加する場合は、容量を並べ替えるか、他のフィールドで注文する場合は、移行する前にこれを行ってください。

次に、adminに移動して最初のルートノードを作成します。

4 - リンクあなたのページにここで

あなたは空想何もせずにページに1つのトピックにリンクさせる、迅速かつ厄介な例です。ここでは選択肢を制限していますが、これはトピックに設定したフィールドに基づいてより複雑な制限を行うように拡張できます。

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でこれをテストしています。
+0

ニース、私のオタクを取得し、試してみましょう。ありがとう... –

関連する問題