2017-10-28 169 views
1

我一直在確保自己已經嘗試了各種可能的途徑,然後纔來到這裏尋求建議。W Multi的多級分類

這就是說,這是我目前正在掙扎的;創建多級/嵌套類別。順便說一句,如果w core核心開發人員可以實現創建多級別類別的簡單方法,而不需要爲它編寫一些vanilla-django破解程序,那將會很不錯。

我一直在這個應用程序工作了幾個星期,一切運行順利,除了現在,有一個嵌套類別的業務決策實施。

我的初始M.O是創建一個ServiceCategoryIndex頁面,一個ServiceCategoryPage然後使ServiceIndex頁面作爲ServiceCategoryPage的ServiceCategoryIndex頁面的後代或可訂購,這只是不正確。

經過幾次迭代後,我回到了我的默認模型,然後嘗試使用視圖和url類似vanilla-django的類別的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'), 

]

+0

您需要多少層次的嵌套?另外,您是否需要選擇「組」類別還是僅選擇最低級別的「類別」? –

+0

理想情況下,嵌套的級別應儘可能多,但2將會很好。並且是類別組。 –

回答

0

我一直在一個類似的問題 - 除了我們呼喚他們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

這確保了在前面的代碼的TopicAdmin在管理員鶺使用註冊的ModelAdmin功能。你會知道它的工作原理,因爲它會出現在左側管理員側欄modeladmin register docs

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


modeladmin_register(TopicAdmin) 

3 - 遷移&創建第一個話題

現在將是一個很好的時間,使遷移和運行遷移,請記住,node_order_by是不容易改變您已經構建了模型之後。如果你想添加自定義的孩子排序例如。重新排序容量或按其他字段排序,請在遷移之前執行此操作。

然後進入管理並創建第一個根節點。

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 - 改進的餘地

  • 主題字符串中總是包含了破折號,以顯示其「深度」,當看到其他地方,這是一個有點難看。最好使用擴展字段類型,並僅在需要時才構建此表示形式。
  • 如前所述,無法手動重新排序子節點,您可以在模型管理員中創建自定義按鈕,以便可以添加按鈕以向上/向下移動並以此方式工作。
  • 代碼示例,所以可能有一些粗糙的邊緣,但應該足以讓你開始。我已經在演示應用程序的W 1. 1.13上測試了它,並且它可以工作。
+0

不錯,會讓我的怪胎,並試試看。謝謝... –