from django import forms from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db import models from modelcluster.fields import ParentalManyToManyField from wagtail.admin.panels import FieldPanel, MultiFieldPanel from wagtail.fields import StreamField from wagtail.models import Page from wagtail.search import index from wagtail.snippets.models import register_snippet from wagtail_editable_help.models import HelpText from bakerydemo.base.blocks import BaseStreamBlock @register_snippet class Country(models.Model): """ A Django model to store set of countries of origin. It uses the `@register_snippet` decorator to allow it to be accessible via the Snippets UI (e.g. /admin/snippets/breads/country/) In the BreadPage model you'll see we use a ForeignKey to create the relationship between Country and BreadPage. This allows a single relationship (e.g only one Country can be added) that is one-way (e.g. Country will have no way to access related BreadPage objects). """ title = models.CharField(max_length=100) def __str__(self): return self.title class Meta: verbose_name_plural = "Countries of Origin" @register_snippet class BreadIngredient(models.Model): """ Standard Django model that is displayed as a snippet within the admin due to the `@register_snippet` decorator. We use a new piece of functionality available to Wagtail called the ParentalManyToManyField on the BreadPage model to display this. The Wagtail Docs give a slightly more detailed example https://docs.wagtail.org/en/stable/getting_started/tutorial.html#categories """ name = models.CharField(max_length=255) panels = [ FieldPanel("name"), ] def __str__(self): return self.name class Meta: verbose_name_plural = "Bread ingredients" @register_snippet class BreadType(models.Model): """ A Django model to define the bread type It uses the `@register_snippet` decorator to allow it to be accessible via the Snippets UI. In the BreadPage model you'll see we use a ForeignKey to create the relationship between BreadType and BreadPage. This allows a single relationship (e.g only one BreadType can be added) that is one-way (e.g. BreadType will have no way to access related BreadPage objects) """ title = models.CharField(max_length=255) panels = [ FieldPanel("title"), ] def __str__(self): return self.title class Meta: verbose_name_plural = "Bread types" class BreadPage(Page): """ Detail view for a specific bread """ introduction = models.TextField( help_text=HelpText( "Bread page", "introduction", default="Text to describe the page" ), blank=True, ) image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text=HelpText( "Common", "hero image", default="Landscape mode only; horizontal width between 1000px and 3000px.", ), ) body = StreamField( BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True ) origin = models.ForeignKey( Country, on_delete=models.SET_NULL, null=True, blank=True, ) # We include related_name='+' to avoid name collisions on relationships. # e.g. there are two FooPage models in two different apps, # and they both have a FK to bread_type, they'll both try to create a # relationship called `foopage_objects` that will throw a valueError on # collision. bread_type = models.ForeignKey( "breads.BreadType", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) ingredients = ParentalManyToManyField("BreadIngredient", blank=True) content_panels = Page.content_panels + [ FieldPanel("introduction", classname="full"), FieldPanel("image"), FieldPanel("body"), FieldPanel("origin"), FieldPanel("bread_type"), MultiFieldPanel( [ FieldPanel( "ingredients", widget=forms.CheckboxSelectMultiple, ), ], heading="Additional Metadata", classname="collapsible collapsed", ), ] search_fields = Page.search_fields + [ index.SearchField("body"), ] parent_page_types = ["BreadsIndexPage"] class BreadsIndexPage(Page): """ Index page for breads. This is more complex than other index pages on the bakery demo site as we've included pagination. We've separated the different aspects of the index page to be discrete functions to make it easier to follow """ introduction = models.TextField( help_text=HelpText( "Breads index page", "introduction", default="Text to describe the page" ), blank=True, ) image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text=HelpText( "Common", "hero image", default="Landscape mode only; horizontal width between 1000px and 3000px.", ), ) content_panels = Page.content_panels + [ FieldPanel("introduction", classname="full"), FieldPanel("image"), ] # Can only have BreadPage children subpage_types = ["BreadPage"] # Returns a queryset of BreadPage objects that are live, that are direct # descendants of this index page with most recent first def get_breads(self): return ( BreadPage.objects.live().descendant_of(self).order_by("-first_published_at") ) # Allows child objects (e.g. BreadPage objects) to be accessible via the # template. We use this on the HomePage to display child items of featured # content def children(self): return self.get_children().specific().live() # Pagination for the index page. We use the `django.core.paginator` as any # standard Django app would, but the difference here being we have it as a # method on the model rather than within a view function def paginate(self, request, *args): page = request.GET.get("page") paginator = Paginator(self.get_breads(), 12) try: pages = paginator.page(page) except PageNotAnInteger: pages = paginator.page(1) except EmptyPage: pages = paginator.page(paginator.num_pages) return pages # Returns the above to the get_context method that is used to populate the # template def get_context(self, request): context = super(BreadsIndexPage, self).get_context(request) # BreadPage objects (get_breads) are passed through pagination breads = self.paginate(request, self.get_breads()) context["breads"] = breads return context