from __future__ import unicode_literals from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils.translation import gettext as _ from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from wagtail.admin.panels import ( FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, PublishingPanel, ) from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField from wagtail.contrib.settings.models import ( BaseGenericSetting, BaseSiteSetting, register_setting, ) from wagtail.fields import RichTextField, StreamField from wagtail.models import ( Collection, DraftStateMixin, LockableMixin, Page, PreviewableMixin, RevisionMixin, Task, TaskState, TranslatableMixin, WorkflowMixin, ) from wagtail.search import index from .blocks import BaseStreamBlock class Person( WorkflowMixin, DraftStateMixin, LockableMixin, RevisionMixin, PreviewableMixin, index.Indexed, ClusterableModel, ): """ A Django model to store Person objects. It is registered using `register_snippet` as a function in wagtail_hooks.py to allow it to have a menu item within a custom menu item group. `Person` uses the `ClusterableModel`, which allows the relationship with another model to be stored locally to the 'parent' model (e.g. a PageModel) until the parent is explicitly saved. This allows the editor to use the 'Preview' button, to preview the content, without saving the relationships to the database. https://github.com/wagtail/django-modelcluster """ first_name = models.CharField("First name", max_length=254) last_name = models.CharField("Last name", max_length=254) job_title = models.CharField("Job title", max_length=254) image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) workflow_states = GenericRelation( "wagtailcore.WorkflowState", content_type_field="base_content_type", object_id_field="object_id", related_query_name="person", for_concrete_model=False, ) revisions = GenericRelation( "wagtailcore.Revision", content_type_field="base_content_type", object_id_field="object_id", related_query_name="person", for_concrete_model=False, ) panels = [ MultiFieldPanel( [ FieldRowPanel( [ FieldPanel("first_name"), FieldPanel("last_name"), ] ) ], "Name", ), FieldPanel("job_title"), FieldPanel("image"), PublishingPanel(), ] search_fields = [ index.SearchField("first_name"), index.SearchField("last_name"), index.FilterField("job_title"), index.AutocompleteField("first_name"), index.AutocompleteField("last_name"), ] @property def thumb_image(self): # Returns an empty string if there is no profile pic or the rendition # file can't be found. try: return self.image.get_rendition("fill-50x50").img_tag() except: # noqa: E722 FIXME: remove bare 'except:' return "" @property def preview_modes(self): return PreviewableMixin.DEFAULT_PREVIEW_MODES + [("blog_post", _("Blog post"))] def __str__(self): return "{} {}".format(self.first_name, self.last_name) def get_preview_template(self, request, mode_name): from bakerydemo.blog.models import BlogPage if mode_name == "blog_post": return BlogPage.template return "base/preview/person.html" def get_preview_context(self, request, mode_name): from bakerydemo.blog.models import BlogPage context = super().get_preview_context(request, mode_name) if mode_name == self.default_preview_mode: return context page = BlogPage.objects.filter(blog_person_relationship__person=self).first() if page: # Use the page authored by this person if available, # and replace the instance from the database with the edited instance page.authors = [ self if author.pk == self.pk else author for author in page.authors() ] # The authors() method only shows live authors, so make sure the instance # is included even if it's not live as this is just a preview if not self.live: page.authors.append(self) else: # Otherwise, get the first page and simulate the person as the author page = BlogPage.objects.first() page.authors = [self] context["page"] = page return context class Meta: verbose_name = "Person" verbose_name_plural = "People" class FooterText( DraftStateMixin, RevisionMixin, PreviewableMixin, TranslatableMixin, models.Model, ): """ This provides editable text for the site footer. Again it is registered using `register_snippet` as a function in wagtail_hooks.py to be grouped together with the Person model inside the same main menu item. It is made accessible on the template via a template tag defined in base/templatetags/ navigation_tags.py """ body = RichTextField() revisions = GenericRelation( "wagtailcore.Revision", content_type_field="base_content_type", object_id_field="object_id", related_query_name="footer_text", for_concrete_model=False, ) panels = [ FieldPanel("body"), PublishingPanel(), ] def __str__(self): return "Footer text" def get_preview_template(self, request, mode_name): return "base.html" def get_preview_context(self, request, mode_name): return {"footer_text": self.body} class Meta(TranslatableMixin.Meta): verbose_name = "footer text" verbose_name_plural = "footer text" class StandardPage(Page): """ A generic content page. On this demo site we use it for an about page but it could be used for any type of page content that only needs a title, image, introduction and body field """ introduction = models.TextField(help_text="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="Landscape mode only; horizontal width between 1000px and 3000px.", ) body = StreamField( BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True ) content_panels = Page.content_panels + [ FieldPanel("introduction"), FieldPanel("body"), FieldPanel("image"), ] class HomePage(Page): """ The Home Page. This looks slightly more complicated than it is. You can see if you visit your site and edit the homepage that it is split between a: - Hero area - Body area - A promotional area - Moveable featured site sections """ # Hero section of HomePage image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Homepage image", ) hero_text = models.CharField( max_length=255, help_text="Write an introduction for the bakery" ) hero_cta = models.CharField( verbose_name="Hero CTA", max_length=255, help_text="Text to display on Call to Action", ) hero_cta_link = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", verbose_name="Hero CTA link", help_text="Choose a page to link to for the Call to Action", ) # Body section of the HomePage body = StreamField( BaseStreamBlock(), verbose_name="Home content block", blank=True, use_json_field=True, ) # Promo section of the HomePage promo_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Promo image", ) promo_title = models.CharField( blank=True, max_length=255, help_text="Title to display above the promo copy" ) promo_text = RichTextField( null=True, blank=True, max_length=1000, help_text="Write some promotional copy" ) # Featured sections on the HomePage # You will see on templates/base/home_page.html that these are treated # in different ways, and displayed in different areas of the page. # Each list their children items that we access via the children function # that we define on the individual Page models e.g. BlogIndexPage featured_section_1_title = models.CharField( blank=True, max_length=255, help_text="Title to display above the promo copy" ) featured_section_1 = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="First featured section for the homepage. Will display up to " "three child items.", verbose_name="Featured section 1", ) featured_section_2_title = models.CharField( blank=True, max_length=255, help_text="Title to display above the promo copy" ) featured_section_2 = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Second featured section for the homepage. Will display up to " "three child items.", verbose_name="Featured section 2", ) featured_section_3_title = models.CharField( blank=True, max_length=255, help_text="Title to display above the promo copy" ) featured_section_3 = models.ForeignKey( "wagtailcore.Page", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Third featured section for the homepage. Will display up to " "six child items.", verbose_name="Featured section 3", ) content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("image"), FieldPanel("hero_text"), MultiFieldPanel( [ FieldPanel("hero_cta"), FieldPanel("hero_cta_link"), ] ), ], heading="Hero section", ), MultiFieldPanel( [ FieldPanel("promo_image"), FieldPanel("promo_title"), FieldPanel("promo_text"), ], heading="Promo section", ), FieldPanel("body"), MultiFieldPanel( [ MultiFieldPanel( [ FieldPanel("featured_section_1_title"), FieldPanel("featured_section_1"), ] ), MultiFieldPanel( [ FieldPanel("featured_section_2_title"), FieldPanel("featured_section_2"), ] ), MultiFieldPanel( [ FieldPanel("featured_section_3_title"), FieldPanel("featured_section_3"), ] ), ], heading="Featured homepage sections", ), ] def __str__(self): return self.title class GalleryPage(Page): """ This is a page to list locations from the selected Collection. We use a Q object to list any Collection created (/admin/collections/) even if they contain no items. In this demo we use it for a GalleryPage, and is intended to show the extensibility of this aspect of Wagtail """ introduction = models.TextField(help_text="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="Landscape mode only; horizontal width between 1000px and " "3000px.", ) body = StreamField( BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True ) collection = models.ForeignKey( Collection, limit_choices_to=~models.Q(name__in=["Root"]), null=True, blank=True, on_delete=models.SET_NULL, help_text="Select the image collection for this gallery.", ) content_panels = Page.content_panels + [ FieldPanel("introduction"), FieldPanel("body"), FieldPanel("image"), FieldPanel("collection"), ] # Defining what content type can sit under the parent. Since it's a blank # array no subpage can be added subpage_types = [] class FormField(AbstractFormField): """ Wagtailforms is a module to introduce simple forms on a Wagtail site. It isn't intended as a replacement to Django's form support but as a quick way to generate a general purpose data-collection form or contact form without having to write code. We use it on the site for a contact form. You can read more about Wagtail forms at: https://docs.wagtail.org/en/stable/reference/contrib/forms/index.html """ page = ParentalKey("FormPage", related_name="form_fields", on_delete=models.CASCADE) class FormPage(AbstractEmailForm): image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) body = StreamField(BaseStreamBlock(), use_json_field=True) thank_you_text = RichTextField(blank=True) # Note how we include the FormField object via an InlinePanel using the # related_name value content_panels = AbstractEmailForm.content_panels + [ FieldPanel("image"), FieldPanel("body"), InlinePanel("form_fields", heading="Form fields", label="Field"), FieldPanel("thank_you_text"), MultiFieldPanel( [ FieldRowPanel( [ FieldPanel("from_address"), FieldPanel("to_address"), ] ), FieldPanel("subject"), ], "Email", ), ] @register_setting(icon="cog") class GenericSettings(ClusterableModel, BaseGenericSetting): mastodon_url = models.URLField(verbose_name="Mastodon URL", blank=True) github_url = models.URLField(verbose_name="GitHub URL", blank=True) organisation_url = models.URLField(verbose_name="Organisation URL", blank=True) panels = [ MultiFieldPanel( [ FieldPanel("github_url"), FieldPanel("mastodon_url"), FieldPanel("organisation_url"), ], "Social settings", ) ] @register_setting(icon="site") class SiteSettings(BaseSiteSetting): title_suffix = models.CharField( verbose_name="Title suffix", max_length=255, help_text="The suffix for the title meta tag e.g. ' | The Wagtail Bakery'", default="The Wagtail Bakery", ) panels = [ FieldPanel("title_suffix"), ] class UserApprovalTaskState(TaskState): pass class UserApprovalTask(Task): """ Based on https://docs.wagtail.org/en/stable/extending/custom_tasks.html. """ user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False ) admin_form_fields = Task.admin_form_fields + ["user"] task_state_class = UserApprovalTaskState # prevent editing of `user` after the task is created # by default, this attribute contains the 'name' field to prevent tasks from being renamed admin_form_readonly_on_edit_fields = Task.admin_form_readonly_on_edit_fields + [ "user" ] def user_can_access_editor(self, page, user): return user == self.user def page_locked_for_user(self, page, user): return user != self.user def get_actions(self, page, user): if user == self.user: return [ ("approve", "Approve", False), ("reject", "Reject", False), ("cancel", "Cancel", False), ] else: return [] def on_action(self, task_state, user, action_name, **kwargs): if action_name == "cancel": return task_state.workflow_state.cancel(user=user) else: return super().on_action(task_state, user, action_name, **kwargs) def get_task_states_user_can_moderate(self, user, **kwargs): if user == self.user: # get all task states linked to the (base class of) current task return TaskState.objects.filter( status=TaskState.STATUS_IN_PROGRESS, task=self.task_ptr ) else: return TaskState.objects.none() @classmethod def get_description(cls): return _("Only a specific user can approve this task")