Source code for ppa.pages.models

import bleach
from django.db import models
from django.template.defaultfilters import striptags, truncatechars_html
from django.utils.text import slugify
from wagtail.admin.panels import FieldPanel
from wagtail import blocks
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
from wagtail.documents.blocks import DocumentChooserBlock
from wagtail.embeds.blocks import EmbedBlock
from wagtail.images.blocks import ImageChooserBlock
from wagtail.images.models import Image
from wagtail.search import index
from wagtail.snippets.blocks import SnippetChooserBlock
from wagtail.snippets.models import register_snippet

from ppa.archive.models import Collection


[docs] @register_snippet class Person(index.Indexed, models.Model): """Common model for a person, currently used to document authorship for instances of :class:`ppa.editorial.models.EditorialPage`.""" #: the display name of an individual name = models.CharField( max_length=255, help_text="Full name for the person as it should appear in the author list.", ) #: Optional profile image to be associated with a person photo = models.ForeignKey( Image, null=True, blank=True, on_delete=models.CASCADE, help_text="Image to use as a profile photo for a person, " "displayed on contributor list.", ) #: identifying URI for a person (VIAF, ORCID iD, personal website, etc.) url = models.URLField( blank=True, default="", help_text="Personal website, profile page, or social media profile page " "for this person.", ) #: description (affiliation, etc.) description = RichTextField( blank=True, features=["bold", "italic"], help_text="Title & affiliation, or other relevant context.", ) #: project role project_role = models.CharField( max_length=255, blank=True, help_text="Project role, if any, for display on contributor list.", ) #: project years project_years = models.CharField( max_length=255, blank=True, help_text="Project years, if desired for display on contributor list.", ) orcid = models.URLField( "ORCID iD", max_length=255, blank=True, help_text="ORCID url, if available, to include in editorial author citation", ) panels = [ FieldPanel("name"), FieldPanel("photo"), FieldPanel("url"), FieldPanel("description"), FieldPanel("project_role"), FieldPanel("project_years"), FieldPanel("orcid"), ] search_fields = [ index.SearchField("name"), index.AutocompleteField("name"), ] def __str__(self): return self.name
[docs] class HomePage(Page): """:class:`wagtail.models.Page` model for PPA home page""" body = RichTextField(blank=True) page_preview_1 = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="First page to preview on the home page as a card", ) page_preview_2 = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Second page to preview on the home page as card", ) content_panels = Page.content_panels + [ FieldPanel("page_preview_1"), FieldPanel("page_preview_2"), FieldPanel("body", classname="full"), ] # only generic parent page allowed, so homepage can be created under # Root but not otherwise used as a child page parent_page_types = [Page] class Meta: verbose_name = "homepage"
[docs] def get_context(self, request): """Add collections with stats and previews for content pages to template context.""" context = super().get_context(request) preview_pages = [ page for page in [self.page_preview_1, self.page_preview_2] if page ] # if no preview pages are associated, look for history and prosody # by slug url (preliminary urls!) if not preview_pages: preview_pages = ContentPage.objects.filter(slug__in=["history", "prosody"]) # grab collection page for displaying collection overview collection_page = CollectionPage.objects.live().first() # include 2 random collections # along with stats for all collections context.update( { "collections": Collection.objects.order_by("?")[:2], "stats": Collection.stats(), "preview_pages": preview_pages, "collection_page": collection_page, } ) return context
#: help text for image alternative text ALT_TEXT_HELP = """Alternative text for visually impaired users to briefly communicate the intended message of the image in this context."""
[docs] class ImageWithCaption(blocks.StructBlock): """:class:`~wagtail.blocks.StructBlock` for an image with a formatted caption, so caption can be context-specific. Also allows images to be floated right, left, or take up the width of the page.""" image = ImageChooserBlock() alternative_text = blocks.TextBlock(required=True, help_text=ALT_TEXT_HELP) caption = blocks.RichTextBlock(required=False, features=["bold", "italic", "link"]) style = blocks.ChoiceBlock( required=True, default="full", choices=[ ("full", "Full Width"), ("left", "Floated Left"), ("right", "Floated Right"), ], help_text="Controls how other content flows around the image. Note \ that this will only take effect on larger screens. Float consecutive \ images in opposite directions for side-by-side display.", ) class Meta: icon = "image" template = "pages/blocks/image_caption_block.html"
[docs] class SVGImageBlock(blocks.StructBlock): """:class:`~wagtail.blocks.StructBlock` for an SVG image with alternative text and optional formatted caption. Separate from :class:`CaptionedImageBlock` because Wagtail image handling does not work with SVG.""" extended_description_help = """This text will only be read to \ non-sighted users and should describe the major insights or \ takeaways from the graphic. Multiple paragraphs are allowed.""" image = DocumentChooserBlock() alternative_text = blocks.TextBlock(required=True, help_text=ALT_TEXT_HELP) caption = blocks.RichTextBlock(features=["bold", "italic", "link"], required=False) extended_description = blocks.RichTextBlock( features=["p"], required=False, help_text=extended_description_help ) class Meta: icon = "image" label = "SVG" template = "pages/blocks/svg_image_block.html"
[docs] class LinkableSectionBlock(blocks.StructBlock): """:class:`~wagtail.blocks.StructBlock` for a rich text block and an associated `title` that will render as an <h2>. Creates an anchor (<a>) so that the section can be directly linked to using a url fragment.""" title = blocks.CharBlock() anchor_text = blocks.CharBlock(help_text="Short label for anchor link") body = blocks.RichTextBlock() panels = [ FieldPanel("title"), FieldPanel("slug"), FieldPanel("body"), ] class Meta: icon = "form" label = "Linkable Section" template = "pages/blocks/linkable_section.html"
[docs] def clean(self, value): cleaned_values = super().clean(value) # run slugify to ensure anchor text is a slug cleaned_values["anchor_text"] = slugify(cleaned_values["anchor_text"]) return cleaned_values
[docs] class BodyContentBlock(blocks.StreamBlock): """Common set of content blocks to be used on both content pages and editorial pages""" paragraph = blocks.RichTextBlock( features=[ "h2", "h3", "bold", "italic", "link", "ol", "ul", "hr", "blockquote", "superscript", "subscript", "strikethrough", "code", ] ) captioned_image = ImageWithCaption(label="image") # lavel as image svg_image = SVGImageBlock() footnotes = blocks.RichTextBlock( features=["ol", "ul", "bold", "italic", "link"], classname="footnotes" ) document = DocumentChooserBlock() linkable_section = LinkableSectionBlock() embed = EmbedBlock()
[docs] class PagePreviewDescriptionMixin(models.Model): """Page mixin with logic for page preview content. Adds an optional richtext description field, and methods to get description and plain-text description, for use in previews on the site and plain-text metadata previews.""" description = RichTextField( blank=True, help_text="Optional. Brief description for preview display. Will " + "also be used for search description (without tags), if one is not entered.", features=["bold", "italic"], ) #: maximum length for description to be displayed max_length = 250 # ('a' is omitted by subsetting and p is added to default ALLOWED_TAGS) #: allowed tags for bleach html stripping in description allowed_tags = list((set(bleach.sanitizer.ALLOWED_TAGS) | set(["p"])) - set(["a"])) class Meta: abstract = True
[docs] def get_description(self): """Get formatted description for preview. Uses description field if there is content, otherwise uses the beginning of the body content.""" description = "" # use description field if set # use striptags to check for empty paragraph) if striptags(self.description): description = self.description # if not, use beginning of body content else: # Iterate over blocks and use content from the first paragraph content for block in self.body: if block.block_type == "paragraph": description = block # break so we stop after the first instead of using last break description = bleach.clean(str(description), tags=self.allowed_tags, strip=True) # truncate either way return truncatechars_html(description, self.max_length)
[docs] def get_plaintext_description(self): """Get plain-text description for use in metadata. Uses search_description field if set; otherwise uses the result of :meth:`get_description` with tags stripped.""" if self.search_description.strip(): return self.search_description return striptags(self.get_description())
[docs] class ContentPage(Page, PagePreviewDescriptionMixin): """Basic content page model.""" body = StreamField(BodyContentBlock, use_json_field=True) content_panels = Page.content_panels + [ FieldPanel("description"), FieldPanel("body"), ]
[docs] class CollectionPage(Page): """Collection list page, with editable text content""" body = RichTextField(blank=True) content_panels = Page.content_panels + [ FieldPanel("body", classname="full"), ] # only allow creating directly under home page parent_page_types = [HomePage] # not allowed to have sub pages subpage_types = []
[docs] def get_context(self, request): """Add collections and collection stats to template context""" context = super().get_context(request) # include all collections with stats context.update( { "collections": Collection.objects.all(), "stats": Collection.stats(), } ) return context
[docs] class ContributorPage(Page, PagePreviewDescriptionMixin): """Project contributor and advisory board page.""" contributors = StreamField( [("person", SnippetChooserBlock(Person))], blank=True, help_text="Select and order people to be listed as project \ contributors.", use_json_field=True, ) board = StreamField( [("person", SnippetChooserBlock(Person))], blank=True, help_text="Select and order people to be listed as board members.", use_json_field=True, ) body = StreamField(BodyContentBlock, blank=True, use_json_field=True) content_panels = Page.content_panels + [ FieldPanel("description"), FieldPanel("contributors"), FieldPanel("board"), FieldPanel("body"), ] # only allow creating directly under home page parent_page_types = [HomePage] # not allowed to have sub pages subpage_types = []