123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935 |
- import hashlib
- import os
- import uuid
- from django import forms
- from django.conf import settings
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.core.exceptions import ValidationError
- from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
- from django.db import models
- from django.shortcuts import redirect
- from django.template.response import TemplateResponse
- from django.utils.translation import gettext_lazy as _
- from modelcluster.contrib.taggit import ClusterTaggableManager
- from modelcluster.fields import ParentalKey, ParentalManyToManyField
- from modelcluster.models import ClusterableModel
- from taggit.managers import TaggableManager
- from taggit.models import ItemBase, TagBase, TaggedItemBase
- from wagtail.admin.forms import WagtailAdminPageForm
- from wagtail.admin.mail import send_mail
- from wagtail.admin.panels import (
- FieldPanel,
- HelpPanel,
- InlinePanel,
- MultiFieldPanel,
- ObjectList,
- TabbedInterface,
- )
- from wagtail.blocks import (
- CharBlock,
- FieldBlock,
- RawHTMLBlock,
- RichTextBlock,
- StreamBlock,
- StructBlock,
- )
- from wagtail.contrib.forms.forms import FormBuilder
- from wagtail.contrib.forms.models import (
- FORM_FIELD_CHOICES,
- AbstractEmailForm,
- AbstractFormField,
- AbstractFormSubmission,
- )
- from wagtail.contrib.forms.views import SubmissionsListView
- from wagtail.contrib.settings.models import (
- BaseGenericSetting,
- BaseSiteSetting,
- register_setting,
- )
- from wagtail.contrib.sitemaps import Sitemap
- from wagtail.contrib.table_block.blocks import TableBlock
- from wagtail.documents import get_document_model
- from wagtail.documents.models import AbstractDocument, Document
- from wagtail.fields import RichTextField, StreamField
- from wagtail.images import get_image_model
- from wagtail.images.blocks import ImageChooserBlock
- from wagtail.images.models import AbstractImage, AbstractRendition, Image
- from wagtail.models import (
- DraftStateMixin,
- Orderable,
- Page,
- PageManager,
- PageQuerySet,
- PreviewableMixin,
- RevisionMixin,
- Task,
- TranslatableMixin,
- )
- from wagtail.search import index
- from wagtail.snippets.models import register_snippet
- from .forms import FormClassAdditionalFieldPageForm, ValidatedPageForm
- EVENT_AUDIENCE_CHOICES = (
- ("public", "Public"),
- ("private", "Private"),
- )
- COMMON_PANELS = (
- FieldPanel("slug"),
- FieldPanel("seo_title"),
- FieldPanel("show_in_menus"),
- FieldPanel("search_description"),
- )
- # Link fields
- class LinkFields(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
- panels = [
- FieldPanel("link_external"),
- FieldPanel("link_page"),
- FieldPanel("link_document"),
- ]
- class Meta:
- abstract = True
- # Carousel items
- class CarouselItem(LinkFields):
- 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)
- panels = [
- FieldPanel("image"),
- FieldPanel("embed_url"),
- FieldPanel("caption"),
- MultiFieldPanel(LinkFields.panels, "Link"),
- ]
- class Meta:
- abstract = True
- # Related links
- class RelatedLink(LinkFields):
- title = models.CharField(max_length=255, help_text="Link title")
- panels = [
- FieldPanel("title"),
- MultiFieldPanel(LinkFields.panels, "Link"),
- ]
- class Meta:
- abstract = True
- # Simple page
- class SimplePage(Page):
- content = models.TextField()
- page_description = "A simple page description"
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("content"),
- ]
- def get_admin_display_title(self):
- return "%s (simple page)" % super().get_admin_display_title()
- class MultiPreviewModesPage(Page):
- template = "tests/simple_page.html"
- @property
- def preview_modes(self):
- return [("original", "Original"), ("alt#1", "Alternate")]
- @property
- def default_preview_mode(self):
- return "alt#1"
- def get_preview_template(self, request, mode_name, *args, **kwargs):
- if mode_name == "alt#1":
- return "tests/simple_page_alt.html"
- return super().get_preview_template(request, *args, **kwargs)
- # Page with Excluded Fields when copied
- class PageWithExcludedCopyField(Page):
- content = models.TextField()
- # Exclude this field from being copied
- special_field = models.CharField(blank=True, max_length=255, default="Very Special")
- exclude_fields_in_copy = ["special_field"]
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("special_field"),
- FieldPanel("content"),
- ]
- class RelatedGenericRelation(models.Model):
- content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
- object_id = models.PositiveBigIntegerField()
- content_object = GenericForeignKey("content_type", "object_id")
- class PageWithGenericRelation(Page):
- generic_relation = GenericRelation("tests.RelatedGenericRelation")
- class PageWithOldStyleRouteMethod(Page):
- """
- Prior to Wagtail 0.4, the route() method on Page returned an HttpResponse
- rather than a Page instance. As subclasses of Page may override route,
- we need to continue accepting this convention (albeit as a deprecated API).
- """
- content = models.TextField()
- template = "tests/simple_page.html"
- def route(self, request, path_components):
- return self.serve(request)
- # File page
- class FilePage(Page):
- file_field = models.FileField()
- content_panels = [
- FieldPanel("title", classname="full title"),
- HelpPanel("remember to check for viruses"),
- FieldPanel("file_field"),
- ]
- # Event page
- class EventPageCarouselItem(TranslatableMixin, Orderable, CarouselItem):
- page = ParentalKey(
- "tests.EventPage", related_name="carousel_items", on_delete=models.CASCADE
- )
- class Meta(TranslatableMixin.Meta, Orderable.Meta):
- pass
- class EventPageRelatedLink(TranslatableMixin, Orderable, RelatedLink):
- page = ParentalKey(
- "tests.EventPage", related_name="related_links", on_delete=models.CASCADE
- )
- class Meta(TranslatableMixin.Meta, Orderable.Meta):
- pass
- class EventPageSpeakerAward(TranslatableMixin, Orderable, models.Model):
- speaker = ParentalKey(
- "tests.EventPageSpeaker", related_name="awards", on_delete=models.CASCADE
- )
- name = models.CharField("Award name", max_length=255)
- date_awarded = models.DateField(null=True, blank=True)
- panels = [
- FieldPanel("name"),
- FieldPanel("date_awarded"),
- ]
- class Meta(TranslatableMixin.Meta, Orderable.Meta):
- pass
- class EventPageSpeaker(TranslatableMixin, Orderable, LinkFields, ClusterableModel):
- page = ParentalKey(
- "tests.EventPage",
- related_name="speakers",
- related_query_name="speaker",
- 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="+",
- )
- @property
- def name_display(self):
- return self.first_name + " " + self.last_name
- panels = [
- FieldPanel("first_name"),
- FieldPanel("last_name"),
- FieldPanel("image"),
- MultiFieldPanel(LinkFields.panels, "Link"),
- InlinePanel("awards", label="Awards"),
- ]
- class Meta(TranslatableMixin.Meta, Orderable.Meta):
- pass
- class EventCategory(TranslatableMixin, models.Model):
- name = models.CharField("Name", max_length=255)
- def __str__(self):
- return self.name
- # Override the standard WagtailAdminPageForm to add validation on start/end dates
- # that appears as a non-field error
- class EventPageForm(WagtailAdminPageForm):
- def clean(self):
- cleaned_data = super().clean()
- # Make sure that the event starts before it ends
- start_date = cleaned_data["date_from"]
- end_date = cleaned_data["date_to"]
- if start_date and end_date and start_date > end_date:
- raise ValidationError("The end date must be after the start date")
- return cleaned_data
- class EventPage(Page):
- date_from = models.DateField("Start date", null=True)
- 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=EVENT_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="+",
- )
- categories = ParentalManyToManyField(EventCategory, blank=True)
- search_fields = [
- index.SearchField("get_audience_display"),
- index.SearchField("location"),
- index.SearchField("body"),
- index.FilterField("url_path"),
- ]
- password_required_template = "tests/event_page_password_required.html"
- base_form_class = EventPageForm
- content_panels = [
- FieldPanel("title", classname="full title"),
- 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", heading="Speaker lineup"),
- InlinePanel("related_links", label="Related links"),
- FieldPanel("categories"),
- # InlinePanel related model uses `pk` not `id`
- InlinePanel("head_counts", label="Head Counts"),
- ]
- promote_panels = [
- MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
- FieldPanel("feed_image"),
- ]
- class HeadCountRelatedModelUsingPK(models.Model):
- """Related model that uses a custom primary key (pk) not id"""
- custom_id = models.AutoField(primary_key=True)
- event_page = ParentalKey(
- EventPage, on_delete=models.CASCADE, related_name="head_counts"
- )
- head_count = models.IntegerField()
- panels = [FieldPanel("head_count")]
- # Override the standard WagtailAdminPageForm to add field that is not in model
- # so that we can test additional potential issues like comparing versions
- class FormClassAdditionalFieldPage(Page):
- location = models.CharField(max_length=255)
- body = RichTextField(blank=True)
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("location"),
- FieldPanel("body"),
- FieldPanel("code"), # not in model, see set base_form_class
- ]
- base_form_class = FormClassAdditionalFieldPageForm
- # Just to be able to test multi table inheritance
- class SingleEventPage(EventPage):
- excerpt = models.TextField(
- max_length=255,
- blank=True,
- null=True,
- help_text="Short text to describe what is this action about",
- )
- # Give this page model a custom URL routing scheme
- def get_url_parts(self, request=None):
- url_parts = super().get_url_parts(request=request)
- if url_parts is None:
- return None
- else:
- site_id, root_url, page_path = url_parts
- return (site_id, root_url, page_path + "pointless-suffix/")
- def route(self, request, path_components):
- if path_components == ["pointless-suffix"]:
- # treat this as equivalent to a request for this page
- return super().route(request, [])
- else:
- # fall back to default routing rules
- return super().route(request, path_components)
- def get_admin_display_title(self):
- return "%s (single event)" % super().get_admin_display_title()
- content_panels = [FieldPanel("excerpt")] + EventPage.content_panels
- # "custom" sitemap object
- class EventSitemap(Sitemap):
- pass
- # Event index (has a separate AJAX template, and a custom template context)
- class EventIndex(Page):
- intro = RichTextField(blank=True, max_length=50)
- ajax_template = "tests/includes/event_listing.html"
- def get_events(self):
- return self.get_children().live().type(EventPage)
- def get_paginator(self):
- return Paginator(self.get_events(), 4)
- def get_context(self, request, page=1):
- # Pagination
- paginator = self.get_paginator()
- try:
- events = paginator.page(page)
- except PageNotAnInteger:
- events = paginator.page(1)
- except EmptyPage:
- events = paginator.page(paginator.num_pages)
- # Update context
- context = super().get_context(request)
- context["events"] = events
- return context
- def route(self, request, path_components):
- if self.live and len(path_components) == 1:
- try:
- return self.serve(request, page=int(path_components[0]))
- except (TypeError, ValueError):
- pass
- return super().route(request, path_components)
- def get_static_site_paths(self):
- # Get page count
- page_count = self.get_paginator().num_pages
- # Yield a path for each page
- for page in range(page_count):
- yield "/%d/" % (page + 1)
- # Yield from superclass
- for path in super().get_static_site_paths():
- yield path
- def get_sitemap_urls(self, request=None):
- # Add past events url to sitemap
- return super().get_sitemap_urls(request=request) + [
- {
- "location": self.full_url + "past/",
- "lastmod": self.latest_revision_created_at,
- }
- ]
- def get_cached_paths(self):
- return super().get_cached_paths() + ["/past/"]
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("intro"),
- ]
- class FormField(AbstractFormField):
- page = ParentalKey("FormPage", related_name="form_fields", on_delete=models.CASCADE)
- class FormPage(AbstractEmailForm):
- def get_context(self, request):
- context = super().get_context(request)
- context["greeting"] = "hello world"
- return context
- # This is redundant (SubmissionsListView is the default view class), but importing
- # SubmissionsListView in this models.py helps us to confirm that this recipe
- # https://docs.wagtail.org/en/stable/reference/contrib/forms/customisation.html#customise-form-submissions-listing-in-wagtail-admin
- # works without triggering circular dependency issues -
- # see https://github.com/wagtail/wagtail/issues/6265
- submissions_list_view_class = SubmissionsListView
- content_panels = [
- FieldPanel("title", classname="full title"),
- InlinePanel("form_fields", label="Form fields"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- # FormPage with a non-HTML extension
- class JadeFormField(AbstractFormField):
- page = ParentalKey(
- "JadeFormPage", related_name="form_fields", on_delete=models.CASCADE
- )
- class JadeFormPage(AbstractEmailForm):
- template = "tests/form_page.jade"
- content_panels = [
- FieldPanel("title", classname="full title"),
- InlinePanel("form_fields", label="Form fields"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- # Form page that redirects to a different page
- class RedirectFormField(AbstractFormField):
- page = ParentalKey(
- "FormPageWithRedirect", related_name="form_fields", on_delete=models.CASCADE
- )
- class FormPageWithRedirect(AbstractEmailForm):
- thank_you_redirect_page = models.ForeignKey(
- "wagtailcore.Page",
- null=True,
- blank=True,
- on_delete=models.SET_NULL,
- related_name="+",
- )
- def get_context(self, request):
- context = super(FormPageWithRedirect, self).get_context(request)
- context["greeting"] = "hello world"
- return context
- def render_landing_page(self, request, form_submission=None, *args, **kwargs):
- """
- Renders the landing page OR if a receipt_page_redirect is chosen redirects to this page.
- """
- if self.thank_you_redirect_page:
- return redirect(self.thank_you_redirect_page.url, permanent=False)
- return super(FormPageWithRedirect, self).render_landing_page(
- request, form_submission, *args, **kwargs
- )
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("thank_you_redirect_page"),
- InlinePanel("form_fields", label="Form fields"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- # FormPage with a custom FormSubmission
- class FormPageWithCustomSubmission(AbstractEmailForm):
- """
- This Form page:
- * Have custom submission model
- * Have custom related_name (see `FormFieldWithCustomSubmission.page`)
- * Saves reference to a user
- * Doesn't render html form, if submission for current user is present
- """
- intro = RichTextField(blank=True)
- thank_you_text = RichTextField(blank=True)
- def get_context(self, request, *args, **kwargs):
- context = super().get_context(request)
- context["greeting"] = "hello world"
- return context
- def get_form_fields(self):
- return self.custom_form_fields.all()
- def get_data_fields(self):
- data_fields = [
- ("useremail", "User email"),
- ]
- data_fields += super().get_data_fields()
- return data_fields
- def get_submission_class(self):
- return CustomFormPageSubmission
- def process_form_submission(self, form):
- form_submission = self.get_submission_class().objects.create(
- form_data=form.cleaned_data,
- page=self,
- user=form.user,
- )
- if self.to_address:
- addresses = [x.strip() for x in self.to_address.split(",")]
- content = "\n".join(
- [
- x[1].label + ": " + str(form.data.get(x[0]))
- for x in form.fields.items()
- ]
- )
- send_mail(
- self.subject,
- content,
- addresses,
- self.from_address,
- )
- # process_form_submission should now return the created form_submission
- return form_submission
- def serve(self, request, *args, **kwargs):
- if (
- self.get_submission_class()
- .objects.filter(page=self, user__pk=request.user.pk)
- .exists()
- ):
- return TemplateResponse(request, self.template, self.get_context(request))
- return super().serve(request, *args, **kwargs)
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("intro"),
- InlinePanel("custom_form_fields", label="Form fields"),
- FieldPanel("thank_you_text"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- class FormFieldWithCustomSubmission(AbstractFormField):
- page = ParentalKey(
- FormPageWithCustomSubmission,
- on_delete=models.CASCADE,
- related_name="custom_form_fields",
- )
- class CustomFormPageSubmission(AbstractFormSubmission):
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- def get_data(self):
- form_data = super().get_data()
- form_data.update(
- {
- "useremail": self.user.email,
- }
- )
- return form_data
- # Custom form page with custom submission listing view and form submission
- class FormFieldForCustomListViewPage(AbstractFormField):
- page = ParentalKey(
- "FormPageWithCustomSubmissionListView",
- related_name="form_fields",
- on_delete=models.CASCADE,
- )
- class FormPageWithCustomSubmissionListView(AbstractEmailForm):
- """Form Page with customised submissions listing view"""
- intro = RichTextField(blank=True)
- thank_you_text = RichTextField(blank=True)
- def get_submissions_list_view_class(self):
- from .views import CustomSubmissionsListView
- return CustomSubmissionsListView
- def get_submission_class(self):
- return CustomFormPageSubmission
- def get_data_fields(self):
- data_fields = [
- ("useremail", "User email"),
- ]
- data_fields += super().get_data_fields()
- return data_fields
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("intro"),
- InlinePanel("form_fields", label="Form fields"),
- FieldPanel("thank_you_text"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- # FormPage with custom FormBuilder
- EXTENDED_CHOICES = FORM_FIELD_CHOICES + (("ipaddress", "IP Address"),)
- class ExtendedFormField(AbstractFormField):
- """
- Override the field_type field with extended choices
- and a custom clean_name override.
- """
- page = ParentalKey(
- "FormPageWithCustomFormBuilder",
- related_name="form_fields",
- on_delete=models.CASCADE,
- )
- field_type = models.CharField(
- verbose_name="field type", max_length=16, choices=EXTENDED_CHOICES
- )
- def get_field_clean_name(self):
- clean_name = super().get_field_clean_name()
- # scoping to field type to easily test behaviour in isolation
- if self.field_type == "number":
- return f"number_field--{clean_name}"
- # scoping to field label to easily test duplicate behaviour in isolation
- if "duplicate" in self.label:
- return "test duplicate"
- return clean_name
- class CustomFormBuilder(FormBuilder):
- """
- A custom FormBuilder that has an 'ipaddress' field with
- customised create_singleline_field with shorter max_length
- """
- def create_singleline_field(self, field, options):
- options["max_length"] = 120 # usual default is 255
- return forms.CharField(**options)
- def create_ipaddress_field(self, field, options):
- return forms.GenericIPAddressField(**options)
- class FormPageWithCustomFormBuilder(AbstractEmailForm):
- """
- A Form page that has a custom form builder and uses a custom
- form field model with additional field_type choices.
- """
- form_builder = CustomFormBuilder
- content_panels = [
- FieldPanel("title", classname="full title"),
- InlinePanel("form_fields", label="Form fields"),
- MultiFieldPanel(
- [
- FieldPanel("to_address"),
- FieldPanel("from_address"),
- FieldPanel("subject"),
- ],
- "Email",
- ),
- ]
- # Snippets
- class AdvertPlacement(models.Model):
- page = ParentalKey(
- "wagtailcore.Page", related_name="advert_placements", on_delete=models.CASCADE
- )
- advert = models.ForeignKey(
- "tests.Advert", related_name="+", on_delete=models.CASCADE
- )
- colour = models.CharField(max_length=255)
- class AdvertTag(TaggedItemBase):
- content_object = ParentalKey(
- "Advert", related_name="tagged_items", on_delete=models.CASCADE
- )
- class Advert(ClusterableModel):
- url = models.URLField(null=True, blank=True)
- text = models.CharField(max_length=255)
- tags = TaggableManager(through=AdvertTag, blank=True)
- panels = [
- FieldPanel("url"),
- FieldPanel("text"),
- FieldPanel("tags"),
- ]
- def __str__(self):
- return self.text
- register_snippet(Advert)
- class AdvertWithCustomPrimaryKey(ClusterableModel):
- advert_id = models.CharField(max_length=255, primary_key=True)
- url = models.URLField(null=True, blank=True)
- text = models.CharField(max_length=255)
- panels = [
- FieldPanel("url"),
- FieldPanel("text"),
- ]
- def __str__(self):
- return self.text
- register_snippet(AdvertWithCustomPrimaryKey)
- class AdvertWithCustomUUIDPrimaryKey(ClusterableModel):
- advert_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- url = models.URLField(null=True, blank=True)
- text = models.CharField(max_length=255)
- panels = [
- FieldPanel("url"),
- FieldPanel("text"),
- ]
- def __str__(self):
- return self.text
- register_snippet(AdvertWithCustomUUIDPrimaryKey)
- class AdvertWithTabbedInterface(models.Model):
- url = models.URLField(null=True, blank=True)
- text = models.CharField(max_length=255)
- something_else = models.CharField(max_length=255)
- advert_panels = [
- FieldPanel("url"),
- FieldPanel("text"),
- ]
- other_panels = [
- FieldPanel("something_else"),
- ]
- edit_handler = TabbedInterface(
- [
- ObjectList(advert_panels, heading="Advert"),
- ObjectList(other_panels, heading="Other"),
- ]
- )
- def __str__(self):
- return self.text
- class Meta:
- ordering = ("text",)
- register_snippet(AdvertWithTabbedInterface)
- # Models with RevisionMixin
- class RevisableModel(RevisionMixin, models.Model):
- text = models.TextField()
- register_snippet(RevisableModel)
- class RevisableChildModel(RevisableModel):
- secret_text = models.TextField(blank=True, default="")
- panels = [
- FieldPanel("text"),
- FieldPanel("secret_text", permission="superuser"),
- ]
- register_snippet(RevisableChildModel)
- class RevisableGrandChildModel(RevisableChildModel):
- pass
- # Models with DraftStateMixin
- class DraftStateModel(DraftStateMixin, RevisionMixin, models.Model):
- text = models.TextField()
- panels = [
- FieldPanel("text"),
- ]
- def __str__(self):
- return self.text
- register_snippet(DraftStateModel)
- # Models with PreviewableMixin
- class PreviewableModel(PreviewableMixin, ClusterableModel):
- text = models.TextField()
- categories = ParentalManyToManyField(EventCategory, blank=True)
- def __str__(self):
- return self.text
- def get_preview_template(self, request, mode_name, *args, **kwargs):
- return "tests/previewable_model.html"
- register_snippet(PreviewableModel)
- class MultiPreviewModesModel(PreviewableMixin, RevisionMixin, models.Model):
- text = models.TextField()
- def __str__(self):
- return self.text
- @property
- def preview_modes(self):
- return [("", "Normal"), ("alt#1", "Alternate")]
- @property
- def default_preview_mode(self):
- return "alt#1"
- def get_preview_template(self, request, mode_name, *args, **kwargs):
- templates = {
- "": "tests/previewable_model.html",
- "alt#1": "tests/previewable_model_alt.html",
- }
- return templates.get(mode_name, templates[""])
- register_snippet(MultiPreviewModesModel)
- class NonPreviewableModel(PreviewableMixin, RevisionMixin, models.Model):
- text = models.TextField()
- def __str__(self):
- return self.text
- preview_modes = []
- register_snippet(NonPreviewableModel)
- class StandardIndex(Page):
- """Index for the site"""
- parent_page_types = [Page]
- # A custom panel setup where all Promote fields are placed in the Content tab instead;
- # we use this to test that the 'promote' tab is left out of the output when empty
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("seo_title"),
- FieldPanel("slug"),
- InlinePanel("advert_placements", label="Adverts"),
- ]
- promote_panels = []
- class StandardChild(Page):
- pass
- # Test overriding edit_handler with a custom one
- StandardChild.edit_handler = TabbedInterface(
- [
- ObjectList(StandardChild.content_panels, heading="Content"),
- ObjectList(StandardChild.promote_panels, heading="Promote"),
- ObjectList(
- StandardChild.settings_panels, heading="Settings", classname="settings"
- ),
- ObjectList(
- [
- HelpPanel("Watch out for asteroids"),
- ],
- heading="Dinosaurs",
- ),
- ],
- base_form_class=WagtailAdminPageForm,
- )
- class BusinessIndex(Page):
- """Can be placed anywhere, can only have Business children"""
- subpage_types = ["tests.BusinessChild", "tests.BusinessSubIndex"]
- class BusinessSubIndex(Page):
- """Can be placed under BusinessIndex, and have BusinessChild children"""
- # BusinessNowherePage is 'incorrectly' added here as a possible child.
- # The rules on BusinessNowherePage prevent it from being a child here though.
- subpage_types = ["tests.BusinessChild", "tests.BusinessNowherePage"]
- parent_page_types = ["tests.BusinessIndex", "tests.BusinessChild"]
- class BusinessChild(Page):
- """Can only be placed under Business indexes, no children allowed"""
- subpage_types = []
- parent_page_types = ["tests.BusinessIndex", BusinessSubIndex]
- page_description = _("A lazy business child page description")
- class BusinessNowherePage(Page):
- """Not allowed to be placed anywhere"""
- parent_page_types = []
- class TaggedPageTag(TaggedItemBase):
- content_object = ParentalKey(
- "tests.TaggedPage", related_name="tagged_items", on_delete=models.CASCADE
- )
- class TaggedPage(Page):
- tags = ClusterTaggableManager(through=TaggedPageTag, blank=True)
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("tags"),
- ]
- class TaggedChildPage(TaggedPage):
- pass
- class TaggedGrandchildPage(TaggedChildPage):
- pass
- class SingletonPage(Page):
- @classmethod
- def can_create_at(cls, parent):
- # You can only create one of these!
- return (
- super(SingletonPage, cls).can_create_at(parent) and not cls.objects.exists()
- )
- class SingletonPageViaMaxCount(Page):
- max_count = 1
- class PageChooserModel(models.Model):
- page = models.ForeignKey(
- "wagtailcore.Page", help_text="help text", on_delete=models.CASCADE
- )
- class EventPageChooserModel(models.Model):
- page = models.ForeignKey(
- "tests.EventPage", help_text="more help text", on_delete=models.CASCADE
- )
- class SnippetChooserModel(models.Model):
- advert = models.ForeignKey(Advert, help_text="help text", on_delete=models.CASCADE)
- panels = [
- FieldPanel("advert"),
- ]
- class SnippetChooserModelWithCustomPrimaryKey(models.Model):
- advertwithcustomprimarykey = models.ForeignKey(
- AdvertWithCustomPrimaryKey, help_text="help text", on_delete=models.CASCADE
- )
- panels = [
- FieldPanel("advertwithcustomprimarykey"),
- ]
- class CustomImage(AbstractImage):
- caption = models.CharField(max_length=255, blank=True)
- fancy_caption = RichTextField(blank=True)
- not_editable_field = models.CharField(max_length=255, blank=True)
- admin_form_fields = Image.admin_form_fields + (
- "caption",
- "fancy_caption",
- )
- class Meta:
- unique_together = [("title", "collection")]
- class CustomRendition(AbstractRendition):
- image = models.ForeignKey(
- CustomImage, related_name="renditions", on_delete=models.CASCADE
- )
- class Meta:
- unique_together = (("image", "filter_spec", "focal_point_key"),)
- # Custom image model with a required field
- class CustomImageWithAuthor(AbstractImage):
- author = models.CharField(max_length=255)
- admin_form_fields = Image.admin_form_fields + ("author",)
- class CustomRenditionWithAuthor(AbstractRendition):
- image = models.ForeignKey(
- CustomImageWithAuthor, related_name="renditions", on_delete=models.CASCADE
- )
- class Meta:
- unique_together = (("image", "filter_spec", "focal_point_key"),)
- class CustomDocument(AbstractDocument):
- description = models.TextField(blank=True)
- fancy_description = RichTextField(blank=True)
- admin_form_fields = Document.admin_form_fields + (
- "description",
- "fancy_description",
- )
- class Meta:
- unique_together = [("title", "collection")]
- # Custom document model with a required field
- class CustomDocumentWithAuthor(AbstractDocument):
- author = models.CharField(max_length=255)
- admin_form_fields = Document.admin_form_fields + ("author",)
- class StreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- use_json_field=False,
- )
- class JSONStreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- use_json_field=True,
- )
- class MinMaxCountStreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- min_num=2,
- max_num=5,
- use_json_field=False,
- )
- class JSONMinMaxCountStreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- min_num=2,
- max_num=5,
- use_json_field=True,
- )
- class BlockCountsStreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- block_counts={
- "text": {"min_num": 1},
- "rich_text": {"max_num": 1},
- "image": {"min_num": 1, "max_num": 1},
- },
- use_json_field=False,
- )
- class JSONBlockCountsStreamModel(models.Model):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- block_counts={
- "text": {"min_num": 1},
- "rich_text": {"max_num": 1},
- "image": {"min_num": 1, "max_num": 1},
- },
- use_json_field=True,
- )
- class ExtendedImageChooserBlock(ImageChooserBlock):
- """
- Example of Block with custom get_api_representation method.
- If the request has an 'extended' query param, it returns a dict of id and title,
- otherwise, it returns the default value.
- """
- def get_api_representation(self, value, context=None):
- image_id = super().get_api_representation(value, context=context)
- if "request" in context and context["request"].query_params.get(
- "extended", False
- ):
- return {"id": image_id, "title": value.title}
- return image_id
- class StreamPage(Page):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ExtendedImageChooserBlock()),
- (
- "product",
- StructBlock(
- [
- ("name", CharBlock()),
- ("price", CharBlock()),
- ]
- ),
- ),
- ("raw_html", RawHTMLBlock()),
- (
- "books",
- StreamBlock(
- [
- ("title", CharBlock()),
- ("author", CharBlock()),
- ]
- ),
- ),
- ],
- use_json_field=False,
- )
- api_fields = ("body",)
- content_panels = [
- FieldPanel("title"),
- FieldPanel("body"),
- ]
- preview_modes = []
- class DefaultStreamPage(Page):
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- default="",
- use_json_field=False,
- )
- content_panels = [
- FieldPanel("title"),
- FieldPanel("body"),
- ]
- class MTIBasePage(Page):
- is_creatable = False
- class Meta:
- verbose_name = "MTI Base page"
- class MTIChildPage(MTIBasePage):
- # Should be creatable by default, no need to set anything
- pass
- class AbstractPage(Page):
- class Meta:
- abstract = True
- @register_setting
- class TestSiteSetting(BaseSiteSetting):
- title = models.CharField(max_length=100)
- email = models.EmailField(max_length=50)
- @register_setting
- class TestGenericSetting(BaseGenericSetting):
- title = models.CharField(max_length=100)
- email = models.EmailField(max_length=50)
- @register_setting
- class ImportantPagesSiteSetting(BaseSiteSetting):
- sign_up_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- general_terms_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- privacy_policy_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- @register_setting
- class ImportantPagesGenericSetting(BaseGenericSetting):
- sign_up_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- general_terms_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- privacy_policy_page = models.ForeignKey(
- "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
- )
- @register_setting(icon="icon-setting-tag")
- class IconSiteSetting(BaseSiteSetting):
- pass
- @register_setting(icon="icon-setting-tag")
- class IconGenericSetting(BaseGenericSetting):
- pass
- class NotYetRegisteredSiteSetting(BaseSiteSetting):
- pass
- class NotYetRegisteredGenericSetting(BaseGenericSetting):
- pass
- @register_setting
- class FileSiteSetting(BaseSiteSetting):
- file = models.FileField()
- @register_setting
- class FileGenericSetting(BaseGenericSetting):
- file = models.FileField()
- class BlogCategory(models.Model):
- name = models.CharField(unique=True, max_length=80)
- class BlogCategoryBlogPage(models.Model):
- category = models.ForeignKey(
- BlogCategory, related_name="+", on_delete=models.CASCADE
- )
- page = ParentalKey(
- "ManyToManyBlogPage", related_name="categories", on_delete=models.CASCADE
- )
- panels = [
- FieldPanel("category"),
- ]
- class ManyToManyBlogPage(Page):
- """
- A page type with two different kinds of M2M relation.
- We don't formally support these, but we don't want them to cause
- hard breakages either.
- """
- body = RichTextField(blank=True)
- adverts = models.ManyToManyField(Advert, blank=True)
- blog_categories = models.ManyToManyField(
- BlogCategory, through=BlogCategoryBlogPage, blank=True
- )
- # make first_published_at editable on this page model
- settings_panels = Page.settings_panels + [
- FieldPanel("first_published_at"),
- ]
- class OneToOnePage(Page):
- """
- A Page containing a O2O relation.
- """
- body = RichTextBlock(blank=True)
- page_ptr = models.OneToOneField(
- Page, parent_link=True, related_name="+", on_delete=models.CASCADE
- )
- class GenericSnippetPage(Page):
- """
- A page containing a reference to an arbitrary snippet (or any model for that matter)
- linked by a GenericForeignKey
- """
- snippet_content_type = models.ForeignKey(
- ContentType, on_delete=models.SET_NULL, null=True
- )
- snippet_object_id = models.PositiveIntegerField(null=True)
- snippet_content_object = GenericForeignKey(
- "snippet_content_type", "snippet_object_id"
- )
- class CustomImageFilePath(AbstractImage):
- def get_upload_to(self, filename):
- """Create a path that's file-system friendly.
- By hashing the file's contents we guarantee an equal distribution
- of files within our root directories. This also gives us a
- better chance of uploading images with the same filename, but
- different contents - this isn't guaranteed as we're only using
- the first three characters of the checksum.
- """
- original_filepath = super().get_upload_to(filename)
- folder_name, filename = original_filepath.split(os.path.sep)
- # Ensure that we consume the entire file, we can't guarantee that
- # the stream has not be partially (or entirely) consumed by
- # another process
- original_position = self.file.tell()
- self.file.seek(0)
- hash256 = hashlib.sha256()
- while True:
- data = self.file.read(256)
- if not data:
- break
- hash256.update(data)
- checksum = hash256.hexdigest()
- self.file.seek(original_position)
- return os.path.join(folder_name, checksum[:3], filename)
- class CustomPageQuerySet(PageQuerySet):
- def about_spam(self):
- return self.filter(title__contains="spam")
- CustomManager = PageManager.from_queryset(CustomPageQuerySet)
- class CustomManagerPage(Page):
- objects = CustomManager()
- class MyBasePage(Page):
- """
- A base Page model, used to set site-wide defaults and overrides.
- """
- objects = CustomManager()
- class Meta:
- abstract = True
- class MyCustomPage(MyBasePage):
- pass
- class ValidatedPage(Page):
- foo = models.CharField(max_length=255)
- base_form_class = ValidatedPageForm
- content_panels = Page.content_panels + [
- FieldPanel("foo"),
- ]
- class DefaultRichTextFieldPage(Page):
- body = RichTextField()
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("body"),
- ]
- class DefaultRichBlockFieldPage(Page):
- body = StreamField(
- [
- ("rich_text", RichTextBlock()),
- ],
- use_json_field=False,
- )
- content_panels = Page.content_panels + [FieldPanel("body")]
- class CustomRichTextFieldPage(Page):
- body = RichTextField(editor="custom")
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("body"),
- ]
- class CustomRichBlockFieldPage(Page):
- body = StreamField(
- [
- ("rich_text", RichTextBlock(editor="custom")),
- ],
- use_json_field=False,
- )
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("body"),
- ]
- class RichTextFieldWithFeaturesPage(Page):
- body = RichTextField(features=["quotation", "embed", "made-up-feature"])
- content_panels = [
- FieldPanel("title", classname="full title"),
- FieldPanel("body"),
- ]
- # a page that only contains RichTextField within an InlinePanel,
- # to test that the inline child's form media gets pulled through
- class SectionedRichTextPageSection(Orderable):
- page = ParentalKey(
- "tests.SectionedRichTextPage", related_name="sections", on_delete=models.CASCADE
- )
- body = RichTextField()
- panels = [FieldPanel("body")]
- class SectionedRichTextPage(Page):
- content_panels = [
- FieldPanel("title", classname="full title"),
- InlinePanel("sections"),
- ]
- class InlineStreamPageSection(Orderable):
- page = ParentalKey(
- "tests.InlineStreamPage", related_name="sections", on_delete=models.CASCADE
- )
- body = StreamField(
- [
- ("text", CharBlock()),
- ("rich_text", RichTextBlock()),
- ("image", ImageChooserBlock()),
- ],
- use_json_field=False,
- )
- panels = [FieldPanel("body")]
- class InlineStreamPage(Page):
- content_panels = [
- FieldPanel("title", classname="full title"),
- InlinePanel("sections"),
- ]
- class TableBlockStreamPage(Page):
- table = StreamField([("table", TableBlock())], use_json_field=False)
- content_panels = [FieldPanel("table")]
- class UserProfile(models.Model):
- # Wagtail's schema must be able to coexist alongside a custom UserProfile model
- user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- favourite_colour = models.CharField(max_length=255)
- class PanelSiteSettings(TestSiteSetting):
- panels = [FieldPanel("title")]
- class PanelGenericSettings(TestGenericSetting):
- panels = [FieldPanel("title")]
- class TabbedSiteSettings(TestSiteSetting):
- edit_handler = TabbedInterface(
- [
- ObjectList([FieldPanel("title")], heading="First tab"),
- ObjectList([FieldPanel("email")], heading="Second tab"),
- ]
- )
- class TabbedGenericSettings(TestGenericSetting):
- edit_handler = TabbedInterface(
- [
- ObjectList([FieldPanel("title")], heading="First tab"),
- ObjectList([FieldPanel("email")], heading="Second tab"),
- ]
- )
- class AlwaysShowInMenusPage(Page):
- show_in_menus_default = True
- # test for AddField migrations on StreamFields using various default values
- class AddedStreamFieldWithoutDefaultPage(Page):
- body = StreamField([("title", CharBlock())], use_json_field=False)
- class AddedStreamFieldWithEmptyStringDefaultPage(Page):
- body = StreamField([("title", CharBlock())], default="", use_json_field=False)
- class AddedStreamFieldWithEmptyListDefaultPage(Page):
- body = StreamField([("title", CharBlock())], default=[], use_json_field=False)
- class SecretPage(Page):
- boring_data = models.TextField()
- secret_data = models.TextField()
- content_panels = Page.content_panels + [
- FieldPanel("boring_data"),
- FieldPanel("secret_data", permission="superuser"),
- ]
- class SimpleParentPage(Page):
- subpage_types = ["tests.SimpleChildPage"]
- class SimpleChildPage(Page):
- parent_page_types = ["tests.SimpleParentPage"]
- max_count_per_parent = 1
- class PersonPage(Page):
- first_name = models.CharField(
- max_length=255,
- verbose_name="First Name",
- )
- last_name = models.CharField(
- max_length=255,
- verbose_name="Last Name",
- )
- content_panels = Page.content_panels + [
- MultiFieldPanel(
- [
- FieldPanel("first_name"),
- FieldPanel("last_name"),
- ],
- "Person",
- ),
- InlinePanel("addresses", label="Address"),
- ]
- class Meta:
- verbose_name = "Person"
- verbose_name_plural = "Persons"
- class Address(index.Indexed, ClusterableModel, Orderable):
- address = models.CharField(
- max_length=255,
- verbose_name="Address",
- )
- tags = ClusterTaggableManager(
- through="tests.AddressTag",
- blank=True,
- )
- person = ParentalKey(
- to="tests.PersonPage", related_name="addresses", verbose_name="Person"
- )
- panels = [
- FieldPanel("address"),
- FieldPanel("tags"),
- ]
- class Meta:
- verbose_name = "Address"
- verbose_name_plural = "Addresses"
- class AddressTag(TaggedItemBase):
- content_object = ParentalKey(
- to="tests.Address", on_delete=models.CASCADE, related_name="tagged_items"
- )
- class RestaurantPage(Page):
- tags = ClusterTaggableManager(through="tests.TaggedRestaurant", blank=True)
- content_panels = Page.content_panels + [
- FieldPanel("tags"),
- ]
- class RestaurantTag(TagBase):
- free_tagging = False
- class Meta:
- verbose_name = "Tag"
- verbose_name_plural = "Tags"
- class TaggedRestaurant(ItemBase):
- tag = models.ForeignKey(
- RestaurantTag, related_name="tagged_restaurants", on_delete=models.CASCADE
- )
- content_object = ParentalKey(
- to="tests.RestaurantPage", on_delete=models.CASCADE, related_name="tagged_items"
- )
- class SimpleTask(Task):
- pass
- # StreamField media definitions must not be evaluated at startup (e.g. during system checks) -
- # these may fail if e.g. ManifestStaticFilesStorage is in use and collectstatic has not been run.
- # Check this with a media definition that deliberately errors; if media handling is not set up
- # correctly, then the mere presence of this model definition will cause startup to fail.
- class DeadlyTextInput(forms.TextInput):
- @property
- def media(self):
- raise Exception("BOOM! Attempted to evaluate DeadlyTextInput.media")
- class DeadlyCharBlock(FieldBlock):
- def __init__(self, *args, **kwargs):
- self.field = forms.CharField(widget=DeadlyTextInput())
- super().__init__(*args, **kwargs)
- class DeadlyStreamPage(Page):
- body = StreamField(
- [
- ("title", DeadlyCharBlock()),
- ],
- use_json_field=False,
- )
- content_panels = Page.content_panels + [
- FieldPanel("body"),
- ]
- # Check that get_image_model and get_document_model work at import time
- # (so that it's possible to use them in foreign key definitions, for example)
- ReimportedImageModel = get_image_model()
- ReimportedDocumentModel = get_document_model()
- # Custom document model with a custom tag field
- class TaggedRestaurantDocument(ItemBase):
- tag = models.ForeignKey(
- RestaurantTag, related_name="tagged_documents", on_delete=models.CASCADE
- )
- content_object = models.ForeignKey(
- to="tests.CustomRestaurantDocument",
- on_delete=models.CASCADE,
- related_name="tagged_items",
- )
- class CustomRestaurantDocument(AbstractDocument):
- tags = TaggableManager(
- help_text=None,
- blank=True,
- verbose_name="tags",
- through=TaggedRestaurantDocument,
- )
- admin_form_fields = Document.admin_form_fields
- # Custom image model with a custom tag field
- class TaggedRestaurantImage(ItemBase):
- tag = models.ForeignKey(
- RestaurantTag, related_name="tagged_images", on_delete=models.CASCADE
- )
- content_object = models.ForeignKey(
- to="tests.CustomRestaurantImage",
- on_delete=models.CASCADE,
- related_name="tagged_items",
- )
- class CustomRestaurantImage(AbstractImage):
- tags = TaggableManager(
- help_text=None, blank=True, verbose_name="tags", through=TaggedRestaurantImage
- )
- admin_form_fields = Image.admin_form_fields
|