From 47862d759efa0868b19f0e5234e5ad20851d12b9 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 28 Aug 2024 18:26:40 +0100 Subject: [PATCH] Add pages_tabs to page sidebar options --- etna/core/models/mixins.py | 629 +++++++++--------- .../0034_alter_generalpage_page_sidebar.py | 30 + 2 files changed, 345 insertions(+), 314 deletions(-) create mode 100644 etna/generic_pages/migrations/0034_alter_generalpage_page_sidebar.py diff --git a/etna/core/models/mixins.py b/etna/core/models/mixins.py index 5344d801c..25470bf3f 100644 --- a/etna/core/models/mixins.py +++ b/etna/core/models/mixins.py @@ -1,314 +1,315 @@ -from datetime import timedelta - -from django.db import models -from django.utils import timezone -from django.utils.functional import cached_property -from django.utils.safestring import mark_safe -from django.utils.translation import gettext_lazy as _ - -from wagtail.admin.panels import FieldPanel, MultiFieldPanel -from wagtail.api import APIField -from wagtail.fields import RichTextField -from wagtail.images import get_image_model_string - -from etna.core.serializers import ( - DetailedImageSerializer, - ImageSerializer, - RichTextSerializer, -) - -from .forms import RequiredHeroImagePageForm - -__all__ = [ - "ContentWarningMixin", - "NewLabelMixin", - "HeroImageMixin", - "RequiredHeroImageMixin", - "SidebarMixin", - "SocialMixin", -] - - -class ContentWarningMixin(models.Model): - """Mixin to allow editors to toggle content warnings on and off""" - - display_content_warning = models.BooleanField( - verbose_name="display a content warning on this page", - default=False, - ) - - custom_warning_text = RichTextField( - verbose_name="custom content warning text (optional)", - features=["link"], - blank=True, - help_text=( - "If specified, will be used for the content warning. " - "Otherwise the default text will be used." - ), - ) - - api_fields = [ - APIField("display_content_warning"), - APIField("custom_warning_text", serializer=RichTextSerializer()), - ] - - class Meta: - abstract = True - - -class NewLabelMixin(models.Model): - """Mixin to allow editors to toggle 'new' label to be applied on-publish""" - - mark_new_on_next_publish = models.BooleanField( - verbose_name="mark this page as 'new' when published", - default=True, - help_text="This will set the 'new' label for 21 days", - ) - - newly_published_at = models.DateField( - editable=False, - verbose_name="Page marked as new on", - default=None, - null=True, - ) - - new_label_display_for_days = 21 - - def with_content_json(self, content): - """ - Overrides Page.with_content_json() to ensure page's `newly_published_at` - value is always preserved between revisions. - """ - obj = super().with_content_json(content) - obj.newly_published_at = self.newly_published_at - return obj - - def save(self, *args, **kwargs): - """ - Overrides Page.save() to set `newly_published_at` under the right - circumstances, and to ensure `mark_new_on_next_publish` is unset - once that wish has been fulfilled. - """ - # Set/reset newly_published_at where requested - if self.live and self.mark_new_on_next_publish: - self.newly_published_at = timezone.now().date() - self.mark_new_on_next_publish = False - - # Save page changes to the database - super().save(*args, **kwargs) - - if self.live and self.mark_new_on_next_publish and self.latest_revision: - # If `mark_new_on_next_publish` is still 'True' in the latest revision, - # The checkbox will remain checked when the page is next edited in Wagtail. - # Checking the box has had the desired effect now, so we 'uncheck' it - # in the revision content to avoid unexpected resetting. - self.latest_revision.content["mark_new_on_next_publish"] = False - self.latest_revision.save() - - @cached_property - def is_newly_published(self): - expiry_date = timezone.now().date() - timedelta( - days=self.new_label_display_for_days - ) - if self.newly_published_at: - if self.newly_published_at > expiry_date: - return True - return False - - promote_panels = [ - MultiFieldPanel( - [ - FieldPanel("mark_new_on_next_publish"), - FieldPanel("newly_published_at", read_only=True), - ], - heading="New label", - ) - ] - - class Meta: - abstract = True - - api_fields = [ - APIField("is_newly_published"), - ] - - -class HeroImageMixin(models.Model): - """Mixin to add hero_image attribute to a Page.""" - - hero_image = models.ForeignKey( - get_image_model_string(), - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - ) - - hero_image_caption = RichTextField( - verbose_name="hero image caption (optional)", - features=["bold", "italic", "link"], - blank=True, - help_text=( - "An optional caption for hero images. This could be used for image sources or for other useful metadata." - ), - ) - - class Meta: - abstract = True - - content_panels = [ - MultiFieldPanel( - [ - FieldPanel("hero_image"), - FieldPanel("hero_image_caption"), - ], - heading="Hero image", - ) - ] - - api_fields = [ - APIField("hero_image_caption", serializer=RichTextSerializer()), - APIField( - "hero_image", - serializer=DetailedImageSerializer("fill-1200x480"), - ), - APIField( - "hero_image_small", - serializer=DetailedImageSerializer("fill-600x400", source="hero_image"), - ), - ] - - -class RequiredHeroImageMixin(HeroImageMixin): - """Mixin to add hero_image attribute to a Page, and make it required.""" - - class Meta: - abstract = True - - base_form_class = RequiredHeroImagePageForm - - -class SidebarMixin(models.Model): - """Mixin to add sidebar options to a Page.""" - - page_sidebar = models.CharField( - choices=[ - ("contents", "Contents"), - ("sections", "Sections"), - ("section_tabs", "Section tabs"), - ("pages", "Pages"), - ], - help_text=mark_safe( - "Select the sidebar style for this page. For more information, see the sidebar documentation." - ), - null=True, - blank=True, - ) - - class Meta: - abstract = True - - settings_panels = [ - FieldPanel("page_sidebar"), - ] - - api_fields = [ - APIField("page_sidebar"), - ] - - -class SocialMixin(models.Model): - """Mixin to add social media sharing options to a Page.""" - - search_image = models.ForeignKey( - get_image_model_string(), - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - verbose_name=_("OpenGraph image"), - help_text=_( - "Image that will appear when this page is shared on social media. This will default to the teaser image if left blank." - ), - ) - - twitter_og_title = models.CharField( - verbose_name=_("Twitter OpenGraph title"), - max_length=255, - blank=True, - null=True, - help_text=_("If left blank, the OpenGraph title will be used."), - ) - twitter_og_description = models.TextField( - verbose_name=_("Twitter OpenGraph description"), - blank=True, - null=True, - help_text=_("If left blank, the OpenGraph description will be used."), - ) - twitter_og_image = models.ForeignKey( - get_image_model_string(), - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - verbose_name=_("Twitter OpenGraph image"), - help_text=_("If left blank, the OpenGraph image will be used."), - ) - - class Meta: - abstract = True - - promote_panels = [ - MultiFieldPanel( - [ - FieldPanel("teaser_text"), - FieldPanel("teaser_image"), - ], - heading="Internal data", - ), - MultiFieldPanel( - [ - FieldPanel( - "seo_title", - help_text=_( - "The name of the page displayed on search engine results as the clickable headline and when shared on social media." - ), - ), - FieldPanel( - "search_description", - help_text=_( - "The descriptive text displayed underneath a headline in search engine results and when shared on social media." - ), - ), - FieldPanel("search_image"), - ], - heading="Base OpenGraph data", - ), - MultiFieldPanel( - [ - FieldPanel("twitter_og_title"), - FieldPanel("twitter_og_description"), - FieldPanel("twitter_og_image"), - ], - heading="Twitter OpenGraph data", - ), - ] - - api_meta_fields = [ - APIField( - "teaser_image_square", - serializer=ImageSerializer("fill-512x512", source="teaser_image"), - ), - APIField("seo_title"), - APIField("search_description"), - APIField( - "search_image", - serializer=ImageSerializer("fill-1200x630"), - ), - APIField("twitter_og_title"), - APIField("twitter_og_description"), - APIField( - "twitter_og_image", - serializer=ImageSerializer("fill-1200x630"), - ), - ] +from datetime import timedelta + +from django.db import models +from django.utils import timezone +from django.utils.functional import cached_property +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + +from wagtail.admin.panels import FieldPanel, MultiFieldPanel +from wagtail.api import APIField +from wagtail.fields import RichTextField +from wagtail.images import get_image_model_string + +from etna.core.serializers import ( + DetailedImageSerializer, + ImageSerializer, + RichTextSerializer, +) + +from .forms import RequiredHeroImagePageForm + +__all__ = [ + "ContentWarningMixin", + "NewLabelMixin", + "HeroImageMixin", + "RequiredHeroImageMixin", + "SidebarMixin", + "SocialMixin", +] + + +class ContentWarningMixin(models.Model): + """Mixin to allow editors to toggle content warnings on and off""" + + display_content_warning = models.BooleanField( + verbose_name="display a content warning on this page", + default=False, + ) + + custom_warning_text = RichTextField( + verbose_name="custom content warning text (optional)", + features=["link"], + blank=True, + help_text=( + "If specified, will be used for the content warning. " + "Otherwise the default text will be used." + ), + ) + + api_fields = [ + APIField("display_content_warning"), + APIField("custom_warning_text", serializer=RichTextSerializer()), + ] + + class Meta: + abstract = True + + +class NewLabelMixin(models.Model): + """Mixin to allow editors to toggle 'new' label to be applied on-publish""" + + mark_new_on_next_publish = models.BooleanField( + verbose_name="mark this page as 'new' when published", + default=True, + help_text="This will set the 'new' label for 21 days", + ) + + newly_published_at = models.DateField( + editable=False, + verbose_name="Page marked as new on", + default=None, + null=True, + ) + + new_label_display_for_days = 21 + + def with_content_json(self, content): + """ + Overrides Page.with_content_json() to ensure page's `newly_published_at` + value is always preserved between revisions. + """ + obj = super().with_content_json(content) + obj.newly_published_at = self.newly_published_at + return obj + + def save(self, *args, **kwargs): + """ + Overrides Page.save() to set `newly_published_at` under the right + circumstances, and to ensure `mark_new_on_next_publish` is unset + once that wish has been fulfilled. + """ + # Set/reset newly_published_at where requested + if self.live and self.mark_new_on_next_publish: + self.newly_published_at = timezone.now().date() + self.mark_new_on_next_publish = False + + # Save page changes to the database + super().save(*args, **kwargs) + + if self.live and self.mark_new_on_next_publish and self.latest_revision: + # If `mark_new_on_next_publish` is still 'True' in the latest revision, + # The checkbox will remain checked when the page is next edited in Wagtail. + # Checking the box has had the desired effect now, so we 'uncheck' it + # in the revision content to avoid unexpected resetting. + self.latest_revision.content["mark_new_on_next_publish"] = False + self.latest_revision.save() + + @cached_property + def is_newly_published(self): + expiry_date = timezone.now().date() - timedelta( + days=self.new_label_display_for_days + ) + if self.newly_published_at: + if self.newly_published_at > expiry_date: + return True + return False + + promote_panels = [ + MultiFieldPanel( + [ + FieldPanel("mark_new_on_next_publish"), + FieldPanel("newly_published_at", read_only=True), + ], + heading="New label", + ) + ] + + class Meta: + abstract = True + + api_fields = [ + APIField("is_newly_published"), + ] + + +class HeroImageMixin(models.Model): + """Mixin to add hero_image attribute to a Page.""" + + hero_image = models.ForeignKey( + get_image_model_string(), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + + hero_image_caption = RichTextField( + verbose_name="hero image caption (optional)", + features=["bold", "italic", "link"], + blank=True, + help_text=( + "An optional caption for hero images. This could be used for image sources or for other useful metadata." + ), + ) + + class Meta: + abstract = True + + content_panels = [ + MultiFieldPanel( + [ + FieldPanel("hero_image"), + FieldPanel("hero_image_caption"), + ], + heading="Hero image", + ) + ] + + api_fields = [ + APIField("hero_image_caption", serializer=RichTextSerializer()), + APIField( + "hero_image", + serializer=DetailedImageSerializer("fill-1200x480"), + ), + APIField( + "hero_image_small", + serializer=DetailedImageSerializer("fill-600x400", source="hero_image"), + ), + ] + + +class RequiredHeroImageMixin(HeroImageMixin): + """Mixin to add hero_image attribute to a Page, and make it required.""" + + class Meta: + abstract = True + + base_form_class = RequiredHeroImagePageForm + + +class SidebarMixin(models.Model): + """Mixin to add sidebar options to a Page.""" + + page_sidebar = models.CharField( + choices=[ + ("contents", "Contents"), + ("sections", "Sections"), + ("section_tabs", "Section tabs"), + ("pages", "Pages"), + ("pages_tabs", "Pages tabs"), + ], + help_text=mark_safe( + "Select the sidebar style for this page. For more information, see the sidebar documentation." + ), + null=True, + blank=True, + ) + + class Meta: + abstract = True + + settings_panels = [ + FieldPanel("page_sidebar"), + ] + + api_fields = [ + APIField("page_sidebar"), + ] + + +class SocialMixin(models.Model): + """Mixin to add social media sharing options to a Page.""" + + search_image = models.ForeignKey( + get_image_model_string(), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + verbose_name=_("OpenGraph image"), + help_text=_( + "Image that will appear when this page is shared on social media. This will default to the teaser image if left blank." + ), + ) + + twitter_og_title = models.CharField( + verbose_name=_("Twitter OpenGraph title"), + max_length=255, + blank=True, + null=True, + help_text=_("If left blank, the OpenGraph title will be used."), + ) + twitter_og_description = models.TextField( + verbose_name=_("Twitter OpenGraph description"), + blank=True, + null=True, + help_text=_("If left blank, the OpenGraph description will be used."), + ) + twitter_og_image = models.ForeignKey( + get_image_model_string(), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + verbose_name=_("Twitter OpenGraph image"), + help_text=_("If left blank, the OpenGraph image will be used."), + ) + + class Meta: + abstract = True + + promote_panels = [ + MultiFieldPanel( + [ + FieldPanel("teaser_text"), + FieldPanel("teaser_image"), + ], + heading="Internal data", + ), + MultiFieldPanel( + [ + FieldPanel( + "seo_title", + help_text=_( + "The name of the page displayed on search engine results as the clickable headline and when shared on social media." + ), + ), + FieldPanel( + "search_description", + help_text=_( + "The descriptive text displayed underneath a headline in search engine results and when shared on social media." + ), + ), + FieldPanel("search_image"), + ], + heading="Base OpenGraph data", + ), + MultiFieldPanel( + [ + FieldPanel("twitter_og_title"), + FieldPanel("twitter_og_description"), + FieldPanel("twitter_og_image"), + ], + heading="Twitter OpenGraph data", + ), + ] + + api_meta_fields = [ + APIField( + "teaser_image_square", + serializer=ImageSerializer("fill-512x512", source="teaser_image"), + ), + APIField("seo_title"), + APIField("search_description"), + APIField( + "search_image", + serializer=ImageSerializer("fill-1200x630"), + ), + APIField("twitter_og_title"), + APIField("twitter_og_description"), + APIField( + "twitter_og_image", + serializer=ImageSerializer("fill-1200x630"), + ), + ] diff --git a/etna/generic_pages/migrations/0034_alter_generalpage_page_sidebar.py b/etna/generic_pages/migrations/0034_alter_generalpage_page_sidebar.py new file mode 100644 index 000000000..5c5153c12 --- /dev/null +++ b/etna/generic_pages/migrations/0034_alter_generalpage_page_sidebar.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.8 on 2024-08-28 17:24 +# etna:allowAlterField + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("generic_pages", "0033_alter_generalpage_page_sidebar"), + ] + + operations = [ + migrations.AlterField( + model_name="generalpage", + name="page_sidebar", + field=models.CharField( + blank=True, + choices=[ + ("contents", "Contents"), + ("sections", "Sections"), + ("section_tabs", "Section tabs"), + ("pages", "Pages"), + ("pages_tabs", "Pages tabs"), + ], + help_text="Select the sidebar style for this page. For more information, see the sidebar documentation.", + null=True, + ), + ), + ]