from datetime import date from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.db import models from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey from taggit.models import TaggedItemBase from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel from wagtail.api import APIField from wagtail.contrib.forms.models import AbstractForm, AbstractFormField from wagtail.fields import RichTextField from wagtail.images.api.fields import ImageRenditionField from wagtail.models import Orderable, Page from wagtail.search import index # ABSTRACT MODELS # ============================= class AbstractLinkFields(models.Model): link_external = models.URLField("External link", blank=True) link_page = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, related_name="+", on_delete=models.CASCADE, ) link_document = models.ForeignKey( "wagtaildocs.Document", null=True, blank=True, related_name="+", on_delete=models.CASCADE, ) @property def link(self): if self.link_page: return self.link_page.url elif self.link_document: return self.link_document.url else: return self.link_external def clean(self): if ( self.link_page is None and self.link_document is None and not self.link_external ): raise ValidationError( "You must provide a related page, related document or an external URL" ) api_fields = ("link",) panels = [ FieldPanel("link_external"), FieldPanel("link_page"), FieldPanel("link_document"), ] class Meta: abstract = True class AbstractRelatedLink(AbstractLinkFields): title = models.CharField(max_length=255, help_text="Link title") api_fields = ("title",) + AbstractLinkFields.api_fields panels = [ FieldPanel("title"), MultiFieldPanel(AbstractLinkFields.panels, "Link"), ] class Meta: abstract = True class AbstractCarouselItem(AbstractLinkFields): image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) embed_url = models.URLField("Embed URL", blank=True) caption = models.CharField(max_length=255, blank=True) api_fields = ( "image", "embed_url", "caption", ) + AbstractLinkFields.api_fields panels = [ FieldPanel("image"), FieldPanel("embed_url"), FieldPanel("caption"), MultiFieldPanel(AbstractLinkFields.panels, "Link"), ] class Meta: abstract = True class ContactFieldsMixin(models.Model): telephone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) address_1 = models.CharField(max_length=255, blank=True) address_2 = models.CharField(max_length=255, blank=True) city = models.CharField(max_length=255, blank=True) country = models.CharField(max_length=255, blank=True) post_code = models.CharField(max_length=10, blank=True) api_fields = ( "telephone", "email", "address_1", "address_2", "city", "country", "post_code", ) panels = [ FieldPanel("telephone"), FieldPanel("email"), FieldPanel("address_1"), FieldPanel("address_2"), FieldPanel("city"), FieldPanel("country"), FieldPanel("post_code"), ] class Meta: abstract = True # PAGE MODELS # ============================= # Home page class HomePage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) body = RichTextField(blank=True) api_fields = ( "body", "carousel_items", "related_links", ) search_fields = Page.search_fields + [ index.SearchField("body"), ] class Meta: verbose_name = "homepage" class HomePageCarouselItem(Orderable, AbstractCarouselItem): page = ParentalKey( "HomePage", related_name="carousel_items", on_delete=models.CASCADE ) class HomePageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "HomePage", related_name="related_links", on_delete=models.CASCADE ) HomePage.content_panels = Page.content_panels + [ FieldPanel("body"), InlinePanel("carousel_items", label="Carousel items"), InlinePanel("related_links", label="Related links"), ] # Standard pages class StandardPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) intro = RichTextField(blank=True) body = RichTextField(blank=True) feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "intro", "body", "feed_image", "carousel_items", "related_links", ) search_fields = Page.search_fields + [ index.SearchField("intro"), index.SearchField("body"), ] class StandardPageCarouselItem(Orderable, AbstractCarouselItem): page = ParentalKey( "StandardPage", related_name="carousel_items", on_delete=models.CASCADE ) class StandardPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "StandardPage", related_name="related_links", on_delete=models.CASCADE ) StandardPage.content_panels = Page.content_panels + [ FieldPanel("intro"), InlinePanel("carousel_items", heading="Carousel items", label="Carousel item"), FieldPanel("body"), InlinePanel("related_links", heading="Related links", label="Related link"), ] StandardPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), ] class StandardIndexPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) intro = RichTextField(blank=True) feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "intro", "feed_image", "related_links", ) search_fields = Page.search_fields + [ index.SearchField("intro"), ] class StandardIndexPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "StandardIndexPage", related_name="related_links", on_delete=models.CASCADE ) StandardIndexPage.content_panels = Page.content_panels + [ FieldPanel("intro"), InlinePanel("related_links", label="Related links"), ] StandardIndexPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), ] # Blog pages class BlogEntryPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) body = RichTextField() tags = ClusterTaggableManager(through="BlogEntryPageTag", blank=True) date = models.DateField("Post date") feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( APIField("body"), APIField("tags"), APIField("date"), APIField("feed_image"), APIField( "feed_image_thumbnail", serializer=ImageRenditionField("fill-300x300", source="feed_image"), ), APIField("carousel_items"), APIField("related_links"), ) search_fields = Page.search_fields + [ index.SearchField("body"), ] def get_blog_index(self): # Find closest ancestor which is a blog index return BlogIndexPage.ancestor_of(self).last() class BlogEntryPageCarouselItem(Orderable, AbstractCarouselItem): page = ParentalKey( "BlogEntryPage", related_name="carousel_items", on_delete=models.CASCADE ) class BlogEntryPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "BlogEntryPage", related_name="related_links", on_delete=models.CASCADE ) class BlogEntryPageTag(TaggedItemBase): content_object = ParentalKey( "BlogEntryPage", related_name="tagged_items", on_delete=models.CASCADE ) BlogEntryPage.content_panels = Page.content_panels + [ FieldPanel("date"), FieldPanel("body"), InlinePanel("carousel_items", label="Carousel items"), InlinePanel("related_links", label="Related links"), ] BlogEntryPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), FieldPanel("tags"), ] class BlogIndexPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) intro = RichTextField(blank=True) api_fields = ( "intro", "related_links", ) search_fields = Page.search_fields + [ index.SearchField("intro"), ] def get_blog_entries(self): # Get list of live blog pages that are descendants of this page entries = BlogEntryPage.objects.descendant_of(self).live() # Order by most recent date first entries = entries.order_by("-date") return entries def get_context(self, request): # Get blog entries entries = self.get_blog_entries() # Filter by tag tag = request.GET.get("tag") if tag: entries = entries.filter(tags__name=tag) paginator = Paginator(entries, per_page=10) entries = paginator.get_page(request.GET.get("page")) # Update template context context = super().get_context(request) context["entries"] = entries return context class BlogIndexPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "BlogIndexPage", related_name="related_links", on_delete=models.CASCADE ) BlogIndexPage.content_panels = Page.content_panels + [ FieldPanel("intro"), InlinePanel("related_links", label="Related links"), ] # Events pages class EventPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) AUDIENCE_CHOICES = ( ("public", "Public"), ("private", "Private"), ) date_from = models.DateField("Start date") date_to = models.DateField( "End date", null=True, blank=True, help_text="Not required if event is on a single day", ) time_from = models.TimeField("Start time", null=True, blank=True) time_to = models.TimeField("End time", null=True, blank=True) audience = models.CharField(max_length=255, choices=AUDIENCE_CHOICES) location = models.CharField(max_length=255) body = RichTextField(blank=True) cost = models.CharField(max_length=255) signup_link = models.URLField(blank=True) feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "date_from", "date_to", "time_from", "time_to", "audience", "location", "body", "cost", "signup_link", "feed_image", "carousel_items", "related_links", "speakers", ) search_fields = Page.search_fields + [ index.SearchField("get_audience_display"), index.SearchField("location"), index.SearchField("body"), ] def get_event_index(self): # Find closest ancestor which is an event index return EventIndexPage.objects.ancestor_of(self).last() class EventPageCarouselItem(Orderable, AbstractCarouselItem): page = ParentalKey( "EventPage", related_name="carousel_items", on_delete=models.CASCADE ) class EventPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "EventPage", related_name="related_links", on_delete=models.CASCADE ) class EventPageSpeaker(Orderable, AbstractLinkFields): page = ParentalKey("EventPage", related_name="speakers", on_delete=models.CASCADE) first_name = models.CharField("Name", max_length=255, blank=True) last_name = models.CharField("Surname", max_length=255, blank=True) image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "first_name", "last_name", "image", ) panels = [ FieldPanel("first_name"), FieldPanel("last_name"), FieldPanel("image"), MultiFieldPanel(AbstractLinkFields.panels, "Link"), ] EventPage.content_panels = Page.content_panels + [ FieldPanel("date_from"), FieldPanel("date_to"), FieldPanel("time_from"), FieldPanel("time_to"), FieldPanel("location"), FieldPanel("audience"), FieldPanel("cost"), FieldPanel("signup_link"), InlinePanel("carousel_items", label="Carousel items"), FieldPanel("body"), InlinePanel("speakers", label="Speakers"), InlinePanel("related_links", label="Related links"), ] EventPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), ] class EventIndexPage(Page): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) intro = RichTextField(blank=True) api_fields = ( "intro", "related_links", ) search_fields = Page.search_fields + [ index.SearchField("intro"), ] def get_events(self): # Get list of live event pages that are descendants of this page events = EventPage.objects.descendant_of(self).live() # Filter events list to get ones that are either # running now or start in the future events = events.filter(date_from__gte=date.today()) # Order by date events = events.order_by("date_from") return events class EventIndexPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "EventIndexPage", related_name="related_links", on_delete=models.CASCADE ) EventIndexPage.content_panels = Page.content_panels + [ FieldPanel("intro"), InlinePanel("related_links", label="Related links"), ] # Person page class PersonPage(Page, ContactFieldsMixin): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) intro = RichTextField(blank=True) biography = RichTextField(blank=True) image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "first_name", "last_name", "intro", "biography", "image", "feed_image", "related_links", ) + ContactFieldsMixin.api_fields search_fields = Page.search_fields + [ index.SearchField("first_name"), index.SearchField("last_name"), index.SearchField("intro"), index.SearchField("biography"), ] class PersonPageRelatedLink(Orderable, AbstractRelatedLink): page = ParentalKey( "PersonPage", related_name="related_links", on_delete=models.CASCADE ) PersonPage.content_panels = Page.content_panels + [ FieldPanel("first_name"), FieldPanel("last_name"), FieldPanel("intro"), FieldPanel("biography"), FieldPanel("image"), MultiFieldPanel(ContactFieldsMixin.panels, "Contact"), InlinePanel("related_links", label="Related links"), ] PersonPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), ] # Contact page class ContactPage(Page, ContactFieldsMixin): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) body = RichTextField(blank=True) feed_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) api_fields = ( "body", "feed_image", ) + ContactFieldsMixin.api_fields search_fields = Page.search_fields + [ index.SearchField("body"), ] ContactPage.content_panels = Page.content_panels + [ FieldPanel("body"), MultiFieldPanel(ContactFieldsMixin.panels, "Contact"), ] ContactPage.promote_panels = [ MultiFieldPanel(Page.promote_panels, "Common page configuration"), FieldPanel("feed_image"), ] class FormField(AbstractFormField): page = ParentalKey("FormPage", related_name="form_fields", on_delete=models.CASCADE) class FormPage(AbstractForm): page_ptr = models.OneToOneField( Page, parent_link=True, related_name="+", on_delete=models.CASCADE ) api_fields = [APIField("form_fields")] content_panels = AbstractForm.content_panels + [ InlinePanel("form_fields", label="Form fields") ]