2
0

models.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935
  1. import hashlib
  2. import os
  3. import uuid
  4. from django import forms
  5. from django.conf import settings
  6. from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.core.exceptions import ValidationError
  9. from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
  10. from django.db import models
  11. from django.shortcuts import redirect
  12. from django.template.response import TemplateResponse
  13. from django.utils.translation import gettext_lazy as _
  14. from modelcluster.contrib.taggit import ClusterTaggableManager
  15. from modelcluster.fields import ParentalKey, ParentalManyToManyField
  16. from modelcluster.models import ClusterableModel
  17. from taggit.managers import TaggableManager
  18. from taggit.models import ItemBase, TagBase, TaggedItemBase
  19. from wagtail.admin.forms import WagtailAdminPageForm
  20. from wagtail.admin.mail import send_mail
  21. from wagtail.admin.panels import (
  22. FieldPanel,
  23. HelpPanel,
  24. InlinePanel,
  25. MultiFieldPanel,
  26. ObjectList,
  27. TabbedInterface,
  28. )
  29. from wagtail.blocks import (
  30. CharBlock,
  31. FieldBlock,
  32. RawHTMLBlock,
  33. RichTextBlock,
  34. StreamBlock,
  35. StructBlock,
  36. )
  37. from wagtail.contrib.forms.forms import FormBuilder
  38. from wagtail.contrib.forms.models import (
  39. FORM_FIELD_CHOICES,
  40. AbstractEmailForm,
  41. AbstractFormField,
  42. AbstractFormSubmission,
  43. )
  44. from wagtail.contrib.forms.views import SubmissionsListView
  45. from wagtail.contrib.settings.models import (
  46. BaseGenericSetting,
  47. BaseSiteSetting,
  48. register_setting,
  49. )
  50. from wagtail.contrib.sitemaps import Sitemap
  51. from wagtail.contrib.table_block.blocks import TableBlock
  52. from wagtail.documents import get_document_model
  53. from wagtail.documents.models import AbstractDocument, Document
  54. from wagtail.fields import RichTextField, StreamField
  55. from wagtail.images import get_image_model
  56. from wagtail.images.blocks import ImageChooserBlock
  57. from wagtail.images.models import AbstractImage, AbstractRendition, Image
  58. from wagtail.models import (
  59. DraftStateMixin,
  60. Orderable,
  61. Page,
  62. PageManager,
  63. PageQuerySet,
  64. PreviewableMixin,
  65. RevisionMixin,
  66. Task,
  67. TranslatableMixin,
  68. )
  69. from wagtail.search import index
  70. from wagtail.snippets.models import register_snippet
  71. from .forms import FormClassAdditionalFieldPageForm, ValidatedPageForm
  72. EVENT_AUDIENCE_CHOICES = (
  73. ("public", "Public"),
  74. ("private", "Private"),
  75. )
  76. COMMON_PANELS = (
  77. FieldPanel("slug"),
  78. FieldPanel("seo_title"),
  79. FieldPanel("show_in_menus"),
  80. FieldPanel("search_description"),
  81. )
  82. # Link fields
  83. class LinkFields(models.Model):
  84. link_external = models.URLField("External link", blank=True)
  85. link_page = models.ForeignKey(
  86. "wagtailcore.Page",
  87. null=True,
  88. blank=True,
  89. related_name="+",
  90. on_delete=models.CASCADE,
  91. )
  92. link_document = models.ForeignKey(
  93. "wagtaildocs.Document",
  94. null=True,
  95. blank=True,
  96. related_name="+",
  97. on_delete=models.CASCADE,
  98. )
  99. @property
  100. def link(self):
  101. if self.link_page:
  102. return self.link_page.url
  103. elif self.link_document:
  104. return self.link_document.url
  105. else:
  106. return self.link_external
  107. panels = [
  108. FieldPanel("link_external"),
  109. FieldPanel("link_page"),
  110. FieldPanel("link_document"),
  111. ]
  112. class Meta:
  113. abstract = True
  114. # Carousel items
  115. class CarouselItem(LinkFields):
  116. image = models.ForeignKey(
  117. "wagtailimages.Image",
  118. null=True,
  119. blank=True,
  120. on_delete=models.SET_NULL,
  121. related_name="+",
  122. )
  123. embed_url = models.URLField("Embed URL", blank=True)
  124. caption = models.CharField(max_length=255, blank=True)
  125. panels = [
  126. FieldPanel("image"),
  127. FieldPanel("embed_url"),
  128. FieldPanel("caption"),
  129. MultiFieldPanel(LinkFields.panels, "Link"),
  130. ]
  131. class Meta:
  132. abstract = True
  133. # Related links
  134. class RelatedLink(LinkFields):
  135. title = models.CharField(max_length=255, help_text="Link title")
  136. panels = [
  137. FieldPanel("title"),
  138. MultiFieldPanel(LinkFields.panels, "Link"),
  139. ]
  140. class Meta:
  141. abstract = True
  142. # Simple page
  143. class SimplePage(Page):
  144. content = models.TextField()
  145. page_description = "A simple page description"
  146. content_panels = [
  147. FieldPanel("title", classname="full title"),
  148. FieldPanel("content"),
  149. ]
  150. def get_admin_display_title(self):
  151. return "%s (simple page)" % super().get_admin_display_title()
  152. class MultiPreviewModesPage(Page):
  153. template = "tests/simple_page.html"
  154. @property
  155. def preview_modes(self):
  156. return [("original", "Original"), ("alt#1", "Alternate")]
  157. @property
  158. def default_preview_mode(self):
  159. return "alt#1"
  160. def get_preview_template(self, request, mode_name, *args, **kwargs):
  161. if mode_name == "alt#1":
  162. return "tests/simple_page_alt.html"
  163. return super().get_preview_template(request, *args, **kwargs)
  164. # Page with Excluded Fields when copied
  165. class PageWithExcludedCopyField(Page):
  166. content = models.TextField()
  167. # Exclude this field from being copied
  168. special_field = models.CharField(blank=True, max_length=255, default="Very Special")
  169. exclude_fields_in_copy = ["special_field"]
  170. content_panels = [
  171. FieldPanel("title", classname="full title"),
  172. FieldPanel("special_field"),
  173. FieldPanel("content"),
  174. ]
  175. class RelatedGenericRelation(models.Model):
  176. content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
  177. object_id = models.PositiveBigIntegerField()
  178. content_object = GenericForeignKey("content_type", "object_id")
  179. class PageWithGenericRelation(Page):
  180. generic_relation = GenericRelation("tests.RelatedGenericRelation")
  181. class PageWithOldStyleRouteMethod(Page):
  182. """
  183. Prior to Wagtail 0.4, the route() method on Page returned an HttpResponse
  184. rather than a Page instance. As subclasses of Page may override route,
  185. we need to continue accepting this convention (albeit as a deprecated API).
  186. """
  187. content = models.TextField()
  188. template = "tests/simple_page.html"
  189. def route(self, request, path_components):
  190. return self.serve(request)
  191. # File page
  192. class FilePage(Page):
  193. file_field = models.FileField()
  194. content_panels = [
  195. FieldPanel("title", classname="full title"),
  196. HelpPanel("remember to check for viruses"),
  197. FieldPanel("file_field"),
  198. ]
  199. # Event page
  200. class EventPageCarouselItem(TranslatableMixin, Orderable, CarouselItem):
  201. page = ParentalKey(
  202. "tests.EventPage", related_name="carousel_items", on_delete=models.CASCADE
  203. )
  204. class Meta(TranslatableMixin.Meta, Orderable.Meta):
  205. pass
  206. class EventPageRelatedLink(TranslatableMixin, Orderable, RelatedLink):
  207. page = ParentalKey(
  208. "tests.EventPage", related_name="related_links", on_delete=models.CASCADE
  209. )
  210. class Meta(TranslatableMixin.Meta, Orderable.Meta):
  211. pass
  212. class EventPageSpeakerAward(TranslatableMixin, Orderable, models.Model):
  213. speaker = ParentalKey(
  214. "tests.EventPageSpeaker", related_name="awards", on_delete=models.CASCADE
  215. )
  216. name = models.CharField("Award name", max_length=255)
  217. date_awarded = models.DateField(null=True, blank=True)
  218. panels = [
  219. FieldPanel("name"),
  220. FieldPanel("date_awarded"),
  221. ]
  222. class Meta(TranslatableMixin.Meta, Orderable.Meta):
  223. pass
  224. class EventPageSpeaker(TranslatableMixin, Orderable, LinkFields, ClusterableModel):
  225. page = ParentalKey(
  226. "tests.EventPage",
  227. related_name="speakers",
  228. related_query_name="speaker",
  229. on_delete=models.CASCADE,
  230. )
  231. first_name = models.CharField("Name", max_length=255, blank=True)
  232. last_name = models.CharField("Surname", max_length=255, blank=True)
  233. image = models.ForeignKey(
  234. "wagtailimages.Image",
  235. null=True,
  236. blank=True,
  237. on_delete=models.SET_NULL,
  238. related_name="+",
  239. )
  240. @property
  241. def name_display(self):
  242. return self.first_name + " " + self.last_name
  243. panels = [
  244. FieldPanel("first_name"),
  245. FieldPanel("last_name"),
  246. FieldPanel("image"),
  247. MultiFieldPanel(LinkFields.panels, "Link"),
  248. InlinePanel("awards", label="Awards"),
  249. ]
  250. class Meta(TranslatableMixin.Meta, Orderable.Meta):
  251. pass
  252. class EventCategory(TranslatableMixin, models.Model):
  253. name = models.CharField("Name", max_length=255)
  254. def __str__(self):
  255. return self.name
  256. # Override the standard WagtailAdminPageForm to add validation on start/end dates
  257. # that appears as a non-field error
  258. class EventPageForm(WagtailAdminPageForm):
  259. def clean(self):
  260. cleaned_data = super().clean()
  261. # Make sure that the event starts before it ends
  262. start_date = cleaned_data["date_from"]
  263. end_date = cleaned_data["date_to"]
  264. if start_date and end_date and start_date > end_date:
  265. raise ValidationError("The end date must be after the start date")
  266. return cleaned_data
  267. class EventPage(Page):
  268. date_from = models.DateField("Start date", null=True)
  269. date_to = models.DateField(
  270. "End date",
  271. null=True,
  272. blank=True,
  273. help_text="Not required if event is on a single day",
  274. )
  275. time_from = models.TimeField("Start time", null=True, blank=True)
  276. time_to = models.TimeField("End time", null=True, blank=True)
  277. audience = models.CharField(max_length=255, choices=EVENT_AUDIENCE_CHOICES)
  278. location = models.CharField(max_length=255)
  279. body = RichTextField(blank=True)
  280. cost = models.CharField(max_length=255)
  281. signup_link = models.URLField(blank=True)
  282. feed_image = models.ForeignKey(
  283. "wagtailimages.Image",
  284. null=True,
  285. blank=True,
  286. on_delete=models.SET_NULL,
  287. related_name="+",
  288. )
  289. categories = ParentalManyToManyField(EventCategory, blank=True)
  290. search_fields = [
  291. index.SearchField("get_audience_display"),
  292. index.SearchField("location"),
  293. index.SearchField("body"),
  294. index.FilterField("url_path"),
  295. ]
  296. password_required_template = "tests/event_page_password_required.html"
  297. base_form_class = EventPageForm
  298. content_panels = [
  299. FieldPanel("title", classname="full title"),
  300. FieldPanel("date_from"),
  301. FieldPanel("date_to"),
  302. FieldPanel("time_from"),
  303. FieldPanel("time_to"),
  304. FieldPanel("location"),
  305. FieldPanel("audience"),
  306. FieldPanel("cost"),
  307. FieldPanel("signup_link"),
  308. InlinePanel("carousel_items", label="Carousel items"),
  309. FieldPanel("body"),
  310. InlinePanel("speakers", label="Speakers", heading="Speaker lineup"),
  311. InlinePanel("related_links", label="Related links"),
  312. FieldPanel("categories"),
  313. # InlinePanel related model uses `pk` not `id`
  314. InlinePanel("head_counts", label="Head Counts"),
  315. ]
  316. promote_panels = [
  317. MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
  318. FieldPanel("feed_image"),
  319. ]
  320. class HeadCountRelatedModelUsingPK(models.Model):
  321. """Related model that uses a custom primary key (pk) not id"""
  322. custom_id = models.AutoField(primary_key=True)
  323. event_page = ParentalKey(
  324. EventPage, on_delete=models.CASCADE, related_name="head_counts"
  325. )
  326. head_count = models.IntegerField()
  327. panels = [FieldPanel("head_count")]
  328. # Override the standard WagtailAdminPageForm to add field that is not in model
  329. # so that we can test additional potential issues like comparing versions
  330. class FormClassAdditionalFieldPage(Page):
  331. location = models.CharField(max_length=255)
  332. body = RichTextField(blank=True)
  333. content_panels = [
  334. FieldPanel("title", classname="full title"),
  335. FieldPanel("location"),
  336. FieldPanel("body"),
  337. FieldPanel("code"), # not in model, see set base_form_class
  338. ]
  339. base_form_class = FormClassAdditionalFieldPageForm
  340. # Just to be able to test multi table inheritance
  341. class SingleEventPage(EventPage):
  342. excerpt = models.TextField(
  343. max_length=255,
  344. blank=True,
  345. null=True,
  346. help_text="Short text to describe what is this action about",
  347. )
  348. # Give this page model a custom URL routing scheme
  349. def get_url_parts(self, request=None):
  350. url_parts = super().get_url_parts(request=request)
  351. if url_parts is None:
  352. return None
  353. else:
  354. site_id, root_url, page_path = url_parts
  355. return (site_id, root_url, page_path + "pointless-suffix/")
  356. def route(self, request, path_components):
  357. if path_components == ["pointless-suffix"]:
  358. # treat this as equivalent to a request for this page
  359. return super().route(request, [])
  360. else:
  361. # fall back to default routing rules
  362. return super().route(request, path_components)
  363. def get_admin_display_title(self):
  364. return "%s (single event)" % super().get_admin_display_title()
  365. content_panels = [FieldPanel("excerpt")] + EventPage.content_panels
  366. # "custom" sitemap object
  367. class EventSitemap(Sitemap):
  368. pass
  369. # Event index (has a separate AJAX template, and a custom template context)
  370. class EventIndex(Page):
  371. intro = RichTextField(blank=True, max_length=50)
  372. ajax_template = "tests/includes/event_listing.html"
  373. def get_events(self):
  374. return self.get_children().live().type(EventPage)
  375. def get_paginator(self):
  376. return Paginator(self.get_events(), 4)
  377. def get_context(self, request, page=1):
  378. # Pagination
  379. paginator = self.get_paginator()
  380. try:
  381. events = paginator.page(page)
  382. except PageNotAnInteger:
  383. events = paginator.page(1)
  384. except EmptyPage:
  385. events = paginator.page(paginator.num_pages)
  386. # Update context
  387. context = super().get_context(request)
  388. context["events"] = events
  389. return context
  390. def route(self, request, path_components):
  391. if self.live and len(path_components) == 1:
  392. try:
  393. return self.serve(request, page=int(path_components[0]))
  394. except (TypeError, ValueError):
  395. pass
  396. return super().route(request, path_components)
  397. def get_static_site_paths(self):
  398. # Get page count
  399. page_count = self.get_paginator().num_pages
  400. # Yield a path for each page
  401. for page in range(page_count):
  402. yield "/%d/" % (page + 1)
  403. # Yield from superclass
  404. for path in super().get_static_site_paths():
  405. yield path
  406. def get_sitemap_urls(self, request=None):
  407. # Add past events url to sitemap
  408. return super().get_sitemap_urls(request=request) + [
  409. {
  410. "location": self.full_url + "past/",
  411. "lastmod": self.latest_revision_created_at,
  412. }
  413. ]
  414. def get_cached_paths(self):
  415. return super().get_cached_paths() + ["/past/"]
  416. content_panels = [
  417. FieldPanel("title", classname="full title"),
  418. FieldPanel("intro"),
  419. ]
  420. class FormField(AbstractFormField):
  421. page = ParentalKey("FormPage", related_name="form_fields", on_delete=models.CASCADE)
  422. class FormPage(AbstractEmailForm):
  423. def get_context(self, request):
  424. context = super().get_context(request)
  425. context["greeting"] = "hello world"
  426. return context
  427. # This is redundant (SubmissionsListView is the default view class), but importing
  428. # SubmissionsListView in this models.py helps us to confirm that this recipe
  429. # https://docs.wagtail.org/en/stable/reference/contrib/forms/customisation.html#customise-form-submissions-listing-in-wagtail-admin
  430. # works without triggering circular dependency issues -
  431. # see https://github.com/wagtail/wagtail/issues/6265
  432. submissions_list_view_class = SubmissionsListView
  433. content_panels = [
  434. FieldPanel("title", classname="full title"),
  435. InlinePanel("form_fields", label="Form fields"),
  436. MultiFieldPanel(
  437. [
  438. FieldPanel("to_address"),
  439. FieldPanel("from_address"),
  440. FieldPanel("subject"),
  441. ],
  442. "Email",
  443. ),
  444. ]
  445. # FormPage with a non-HTML extension
  446. class JadeFormField(AbstractFormField):
  447. page = ParentalKey(
  448. "JadeFormPage", related_name="form_fields", on_delete=models.CASCADE
  449. )
  450. class JadeFormPage(AbstractEmailForm):
  451. template = "tests/form_page.jade"
  452. content_panels = [
  453. FieldPanel("title", classname="full title"),
  454. InlinePanel("form_fields", label="Form fields"),
  455. MultiFieldPanel(
  456. [
  457. FieldPanel("to_address"),
  458. FieldPanel("from_address"),
  459. FieldPanel("subject"),
  460. ],
  461. "Email",
  462. ),
  463. ]
  464. # Form page that redirects to a different page
  465. class RedirectFormField(AbstractFormField):
  466. page = ParentalKey(
  467. "FormPageWithRedirect", related_name="form_fields", on_delete=models.CASCADE
  468. )
  469. class FormPageWithRedirect(AbstractEmailForm):
  470. thank_you_redirect_page = models.ForeignKey(
  471. "wagtailcore.Page",
  472. null=True,
  473. blank=True,
  474. on_delete=models.SET_NULL,
  475. related_name="+",
  476. )
  477. def get_context(self, request):
  478. context = super(FormPageWithRedirect, self).get_context(request)
  479. context["greeting"] = "hello world"
  480. return context
  481. def render_landing_page(self, request, form_submission=None, *args, **kwargs):
  482. """
  483. Renders the landing page OR if a receipt_page_redirect is chosen redirects to this page.
  484. """
  485. if self.thank_you_redirect_page:
  486. return redirect(self.thank_you_redirect_page.url, permanent=False)
  487. return super(FormPageWithRedirect, self).render_landing_page(
  488. request, form_submission, *args, **kwargs
  489. )
  490. content_panels = [
  491. FieldPanel("title", classname="full title"),
  492. FieldPanel("thank_you_redirect_page"),
  493. InlinePanel("form_fields", label="Form fields"),
  494. MultiFieldPanel(
  495. [
  496. FieldPanel("to_address"),
  497. FieldPanel("from_address"),
  498. FieldPanel("subject"),
  499. ],
  500. "Email",
  501. ),
  502. ]
  503. # FormPage with a custom FormSubmission
  504. class FormPageWithCustomSubmission(AbstractEmailForm):
  505. """
  506. This Form page:
  507. * Have custom submission model
  508. * Have custom related_name (see `FormFieldWithCustomSubmission.page`)
  509. * Saves reference to a user
  510. * Doesn't render html form, if submission for current user is present
  511. """
  512. intro = RichTextField(blank=True)
  513. thank_you_text = RichTextField(blank=True)
  514. def get_context(self, request, *args, **kwargs):
  515. context = super().get_context(request)
  516. context["greeting"] = "hello world"
  517. return context
  518. def get_form_fields(self):
  519. return self.custom_form_fields.all()
  520. def get_data_fields(self):
  521. data_fields = [
  522. ("useremail", "User email"),
  523. ]
  524. data_fields += super().get_data_fields()
  525. return data_fields
  526. def get_submission_class(self):
  527. return CustomFormPageSubmission
  528. def process_form_submission(self, form):
  529. form_submission = self.get_submission_class().objects.create(
  530. form_data=form.cleaned_data,
  531. page=self,
  532. user=form.user,
  533. )
  534. if self.to_address:
  535. addresses = [x.strip() for x in self.to_address.split(",")]
  536. content = "\n".join(
  537. [
  538. x[1].label + ": " + str(form.data.get(x[0]))
  539. for x in form.fields.items()
  540. ]
  541. )
  542. send_mail(
  543. self.subject,
  544. content,
  545. addresses,
  546. self.from_address,
  547. )
  548. # process_form_submission should now return the created form_submission
  549. return form_submission
  550. def serve(self, request, *args, **kwargs):
  551. if (
  552. self.get_submission_class()
  553. .objects.filter(page=self, user__pk=request.user.pk)
  554. .exists()
  555. ):
  556. return TemplateResponse(request, self.template, self.get_context(request))
  557. return super().serve(request, *args, **kwargs)
  558. content_panels = [
  559. FieldPanel("title", classname="full title"),
  560. FieldPanel("intro"),
  561. InlinePanel("custom_form_fields", label="Form fields"),
  562. FieldPanel("thank_you_text"),
  563. MultiFieldPanel(
  564. [
  565. FieldPanel("to_address"),
  566. FieldPanel("from_address"),
  567. FieldPanel("subject"),
  568. ],
  569. "Email",
  570. ),
  571. ]
  572. class FormFieldWithCustomSubmission(AbstractFormField):
  573. page = ParentalKey(
  574. FormPageWithCustomSubmission,
  575. on_delete=models.CASCADE,
  576. related_name="custom_form_fields",
  577. )
  578. class CustomFormPageSubmission(AbstractFormSubmission):
  579. user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  580. def get_data(self):
  581. form_data = super().get_data()
  582. form_data.update(
  583. {
  584. "useremail": self.user.email,
  585. }
  586. )
  587. return form_data
  588. # Custom form page with custom submission listing view and form submission
  589. class FormFieldForCustomListViewPage(AbstractFormField):
  590. page = ParentalKey(
  591. "FormPageWithCustomSubmissionListView",
  592. related_name="form_fields",
  593. on_delete=models.CASCADE,
  594. )
  595. class FormPageWithCustomSubmissionListView(AbstractEmailForm):
  596. """Form Page with customised submissions listing view"""
  597. intro = RichTextField(blank=True)
  598. thank_you_text = RichTextField(blank=True)
  599. def get_submissions_list_view_class(self):
  600. from .views import CustomSubmissionsListView
  601. return CustomSubmissionsListView
  602. def get_submission_class(self):
  603. return CustomFormPageSubmission
  604. def get_data_fields(self):
  605. data_fields = [
  606. ("useremail", "User email"),
  607. ]
  608. data_fields += super().get_data_fields()
  609. return data_fields
  610. content_panels = [
  611. FieldPanel("title", classname="full title"),
  612. FieldPanel("intro"),
  613. InlinePanel("form_fields", label="Form fields"),
  614. FieldPanel("thank_you_text"),
  615. MultiFieldPanel(
  616. [
  617. FieldPanel("to_address"),
  618. FieldPanel("from_address"),
  619. FieldPanel("subject"),
  620. ],
  621. "Email",
  622. ),
  623. ]
  624. # FormPage with custom FormBuilder
  625. EXTENDED_CHOICES = FORM_FIELD_CHOICES + (("ipaddress", "IP Address"),)
  626. class ExtendedFormField(AbstractFormField):
  627. """
  628. Override the field_type field with extended choices
  629. and a custom clean_name override.
  630. """
  631. page = ParentalKey(
  632. "FormPageWithCustomFormBuilder",
  633. related_name="form_fields",
  634. on_delete=models.CASCADE,
  635. )
  636. field_type = models.CharField(
  637. verbose_name="field type", max_length=16, choices=EXTENDED_CHOICES
  638. )
  639. def get_field_clean_name(self):
  640. clean_name = super().get_field_clean_name()
  641. # scoping to field type to easily test behaviour in isolation
  642. if self.field_type == "number":
  643. return f"number_field--{clean_name}"
  644. # scoping to field label to easily test duplicate behaviour in isolation
  645. if "duplicate" in self.label:
  646. return "test duplicate"
  647. return clean_name
  648. class CustomFormBuilder(FormBuilder):
  649. """
  650. A custom FormBuilder that has an 'ipaddress' field with
  651. customised create_singleline_field with shorter max_length
  652. """
  653. def create_singleline_field(self, field, options):
  654. options["max_length"] = 120 # usual default is 255
  655. return forms.CharField(**options)
  656. def create_ipaddress_field(self, field, options):
  657. return forms.GenericIPAddressField(**options)
  658. class FormPageWithCustomFormBuilder(AbstractEmailForm):
  659. """
  660. A Form page that has a custom form builder and uses a custom
  661. form field model with additional field_type choices.
  662. """
  663. form_builder = CustomFormBuilder
  664. content_panels = [
  665. FieldPanel("title", classname="full title"),
  666. InlinePanel("form_fields", label="Form fields"),
  667. MultiFieldPanel(
  668. [
  669. FieldPanel("to_address"),
  670. FieldPanel("from_address"),
  671. FieldPanel("subject"),
  672. ],
  673. "Email",
  674. ),
  675. ]
  676. # Snippets
  677. class AdvertPlacement(models.Model):
  678. page = ParentalKey(
  679. "wagtailcore.Page", related_name="advert_placements", on_delete=models.CASCADE
  680. )
  681. advert = models.ForeignKey(
  682. "tests.Advert", related_name="+", on_delete=models.CASCADE
  683. )
  684. colour = models.CharField(max_length=255)
  685. class AdvertTag(TaggedItemBase):
  686. content_object = ParentalKey(
  687. "Advert", related_name="tagged_items", on_delete=models.CASCADE
  688. )
  689. class Advert(ClusterableModel):
  690. url = models.URLField(null=True, blank=True)
  691. text = models.CharField(max_length=255)
  692. tags = TaggableManager(through=AdvertTag, blank=True)
  693. panels = [
  694. FieldPanel("url"),
  695. FieldPanel("text"),
  696. FieldPanel("tags"),
  697. ]
  698. def __str__(self):
  699. return self.text
  700. register_snippet(Advert)
  701. class AdvertWithCustomPrimaryKey(ClusterableModel):
  702. advert_id = models.CharField(max_length=255, primary_key=True)
  703. url = models.URLField(null=True, blank=True)
  704. text = models.CharField(max_length=255)
  705. panels = [
  706. FieldPanel("url"),
  707. FieldPanel("text"),
  708. ]
  709. def __str__(self):
  710. return self.text
  711. register_snippet(AdvertWithCustomPrimaryKey)
  712. class AdvertWithCustomUUIDPrimaryKey(ClusterableModel):
  713. advert_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  714. url = models.URLField(null=True, blank=True)
  715. text = models.CharField(max_length=255)
  716. panels = [
  717. FieldPanel("url"),
  718. FieldPanel("text"),
  719. ]
  720. def __str__(self):
  721. return self.text
  722. register_snippet(AdvertWithCustomUUIDPrimaryKey)
  723. class AdvertWithTabbedInterface(models.Model):
  724. url = models.URLField(null=True, blank=True)
  725. text = models.CharField(max_length=255)
  726. something_else = models.CharField(max_length=255)
  727. advert_panels = [
  728. FieldPanel("url"),
  729. FieldPanel("text"),
  730. ]
  731. other_panels = [
  732. FieldPanel("something_else"),
  733. ]
  734. edit_handler = TabbedInterface(
  735. [
  736. ObjectList(advert_panels, heading="Advert"),
  737. ObjectList(other_panels, heading="Other"),
  738. ]
  739. )
  740. def __str__(self):
  741. return self.text
  742. class Meta:
  743. ordering = ("text",)
  744. register_snippet(AdvertWithTabbedInterface)
  745. # Models with RevisionMixin
  746. class RevisableModel(RevisionMixin, models.Model):
  747. text = models.TextField()
  748. register_snippet(RevisableModel)
  749. class RevisableChildModel(RevisableModel):
  750. secret_text = models.TextField(blank=True, default="")
  751. panels = [
  752. FieldPanel("text"),
  753. FieldPanel("secret_text", permission="superuser"),
  754. ]
  755. register_snippet(RevisableChildModel)
  756. class RevisableGrandChildModel(RevisableChildModel):
  757. pass
  758. # Models with DraftStateMixin
  759. class DraftStateModel(DraftStateMixin, RevisionMixin, models.Model):
  760. text = models.TextField()
  761. panels = [
  762. FieldPanel("text"),
  763. ]
  764. def __str__(self):
  765. return self.text
  766. register_snippet(DraftStateModel)
  767. # Models with PreviewableMixin
  768. class PreviewableModel(PreviewableMixin, ClusterableModel):
  769. text = models.TextField()
  770. categories = ParentalManyToManyField(EventCategory, blank=True)
  771. def __str__(self):
  772. return self.text
  773. def get_preview_template(self, request, mode_name, *args, **kwargs):
  774. return "tests/previewable_model.html"
  775. register_snippet(PreviewableModel)
  776. class MultiPreviewModesModel(PreviewableMixin, RevisionMixin, models.Model):
  777. text = models.TextField()
  778. def __str__(self):
  779. return self.text
  780. @property
  781. def preview_modes(self):
  782. return [("", "Normal"), ("alt#1", "Alternate")]
  783. @property
  784. def default_preview_mode(self):
  785. return "alt#1"
  786. def get_preview_template(self, request, mode_name, *args, **kwargs):
  787. templates = {
  788. "": "tests/previewable_model.html",
  789. "alt#1": "tests/previewable_model_alt.html",
  790. }
  791. return templates.get(mode_name, templates[""])
  792. register_snippet(MultiPreviewModesModel)
  793. class NonPreviewableModel(PreviewableMixin, RevisionMixin, models.Model):
  794. text = models.TextField()
  795. def __str__(self):
  796. return self.text
  797. preview_modes = []
  798. register_snippet(NonPreviewableModel)
  799. class StandardIndex(Page):
  800. """Index for the site"""
  801. parent_page_types = [Page]
  802. # A custom panel setup where all Promote fields are placed in the Content tab instead;
  803. # we use this to test that the 'promote' tab is left out of the output when empty
  804. content_panels = [
  805. FieldPanel("title", classname="full title"),
  806. FieldPanel("seo_title"),
  807. FieldPanel("slug"),
  808. InlinePanel("advert_placements", label="Adverts"),
  809. ]
  810. promote_panels = []
  811. class StandardChild(Page):
  812. pass
  813. # Test overriding edit_handler with a custom one
  814. StandardChild.edit_handler = TabbedInterface(
  815. [
  816. ObjectList(StandardChild.content_panels, heading="Content"),
  817. ObjectList(StandardChild.promote_panels, heading="Promote"),
  818. ObjectList(
  819. StandardChild.settings_panels, heading="Settings", classname="settings"
  820. ),
  821. ObjectList(
  822. [
  823. HelpPanel("Watch out for asteroids"),
  824. ],
  825. heading="Dinosaurs",
  826. ),
  827. ],
  828. base_form_class=WagtailAdminPageForm,
  829. )
  830. class BusinessIndex(Page):
  831. """Can be placed anywhere, can only have Business children"""
  832. subpage_types = ["tests.BusinessChild", "tests.BusinessSubIndex"]
  833. class BusinessSubIndex(Page):
  834. """Can be placed under BusinessIndex, and have BusinessChild children"""
  835. # BusinessNowherePage is 'incorrectly' added here as a possible child.
  836. # The rules on BusinessNowherePage prevent it from being a child here though.
  837. subpage_types = ["tests.BusinessChild", "tests.BusinessNowherePage"]
  838. parent_page_types = ["tests.BusinessIndex", "tests.BusinessChild"]
  839. class BusinessChild(Page):
  840. """Can only be placed under Business indexes, no children allowed"""
  841. subpage_types = []
  842. parent_page_types = ["tests.BusinessIndex", BusinessSubIndex]
  843. page_description = _("A lazy business child page description")
  844. class BusinessNowherePage(Page):
  845. """Not allowed to be placed anywhere"""
  846. parent_page_types = []
  847. class TaggedPageTag(TaggedItemBase):
  848. content_object = ParentalKey(
  849. "tests.TaggedPage", related_name="tagged_items", on_delete=models.CASCADE
  850. )
  851. class TaggedPage(Page):
  852. tags = ClusterTaggableManager(through=TaggedPageTag, blank=True)
  853. content_panels = [
  854. FieldPanel("title", classname="full title"),
  855. FieldPanel("tags"),
  856. ]
  857. class TaggedChildPage(TaggedPage):
  858. pass
  859. class TaggedGrandchildPage(TaggedChildPage):
  860. pass
  861. class SingletonPage(Page):
  862. @classmethod
  863. def can_create_at(cls, parent):
  864. # You can only create one of these!
  865. return (
  866. super(SingletonPage, cls).can_create_at(parent) and not cls.objects.exists()
  867. )
  868. class SingletonPageViaMaxCount(Page):
  869. max_count = 1
  870. class PageChooserModel(models.Model):
  871. page = models.ForeignKey(
  872. "wagtailcore.Page", help_text="help text", on_delete=models.CASCADE
  873. )
  874. class EventPageChooserModel(models.Model):
  875. page = models.ForeignKey(
  876. "tests.EventPage", help_text="more help text", on_delete=models.CASCADE
  877. )
  878. class SnippetChooserModel(models.Model):
  879. advert = models.ForeignKey(Advert, help_text="help text", on_delete=models.CASCADE)
  880. panels = [
  881. FieldPanel("advert"),
  882. ]
  883. class SnippetChooserModelWithCustomPrimaryKey(models.Model):
  884. advertwithcustomprimarykey = models.ForeignKey(
  885. AdvertWithCustomPrimaryKey, help_text="help text", on_delete=models.CASCADE
  886. )
  887. panels = [
  888. FieldPanel("advertwithcustomprimarykey"),
  889. ]
  890. class CustomImage(AbstractImage):
  891. caption = models.CharField(max_length=255, blank=True)
  892. fancy_caption = RichTextField(blank=True)
  893. not_editable_field = models.CharField(max_length=255, blank=True)
  894. admin_form_fields = Image.admin_form_fields + (
  895. "caption",
  896. "fancy_caption",
  897. )
  898. class Meta:
  899. unique_together = [("title", "collection")]
  900. class CustomRendition(AbstractRendition):
  901. image = models.ForeignKey(
  902. CustomImage, related_name="renditions", on_delete=models.CASCADE
  903. )
  904. class Meta:
  905. unique_together = (("image", "filter_spec", "focal_point_key"),)
  906. # Custom image model with a required field
  907. class CustomImageWithAuthor(AbstractImage):
  908. author = models.CharField(max_length=255)
  909. admin_form_fields = Image.admin_form_fields + ("author",)
  910. class CustomRenditionWithAuthor(AbstractRendition):
  911. image = models.ForeignKey(
  912. CustomImageWithAuthor, related_name="renditions", on_delete=models.CASCADE
  913. )
  914. class Meta:
  915. unique_together = (("image", "filter_spec", "focal_point_key"),)
  916. class CustomDocument(AbstractDocument):
  917. description = models.TextField(blank=True)
  918. fancy_description = RichTextField(blank=True)
  919. admin_form_fields = Document.admin_form_fields + (
  920. "description",
  921. "fancy_description",
  922. )
  923. class Meta:
  924. unique_together = [("title", "collection")]
  925. # Custom document model with a required field
  926. class CustomDocumentWithAuthor(AbstractDocument):
  927. author = models.CharField(max_length=255)
  928. admin_form_fields = Document.admin_form_fields + ("author",)
  929. class StreamModel(models.Model):
  930. body = StreamField(
  931. [
  932. ("text", CharBlock()),
  933. ("rich_text", RichTextBlock()),
  934. ("image", ImageChooserBlock()),
  935. ],
  936. use_json_field=False,
  937. )
  938. class JSONStreamModel(models.Model):
  939. body = StreamField(
  940. [
  941. ("text", CharBlock()),
  942. ("rich_text", RichTextBlock()),
  943. ("image", ImageChooserBlock()),
  944. ],
  945. use_json_field=True,
  946. )
  947. class MinMaxCountStreamModel(models.Model):
  948. body = StreamField(
  949. [
  950. ("text", CharBlock()),
  951. ("rich_text", RichTextBlock()),
  952. ("image", ImageChooserBlock()),
  953. ],
  954. min_num=2,
  955. max_num=5,
  956. use_json_field=False,
  957. )
  958. class JSONMinMaxCountStreamModel(models.Model):
  959. body = StreamField(
  960. [
  961. ("text", CharBlock()),
  962. ("rich_text", RichTextBlock()),
  963. ("image", ImageChooserBlock()),
  964. ],
  965. min_num=2,
  966. max_num=5,
  967. use_json_field=True,
  968. )
  969. class BlockCountsStreamModel(models.Model):
  970. body = StreamField(
  971. [
  972. ("text", CharBlock()),
  973. ("rich_text", RichTextBlock()),
  974. ("image", ImageChooserBlock()),
  975. ],
  976. block_counts={
  977. "text": {"min_num": 1},
  978. "rich_text": {"max_num": 1},
  979. "image": {"min_num": 1, "max_num": 1},
  980. },
  981. use_json_field=False,
  982. )
  983. class JSONBlockCountsStreamModel(models.Model):
  984. body = StreamField(
  985. [
  986. ("text", CharBlock()),
  987. ("rich_text", RichTextBlock()),
  988. ("image", ImageChooserBlock()),
  989. ],
  990. block_counts={
  991. "text": {"min_num": 1},
  992. "rich_text": {"max_num": 1},
  993. "image": {"min_num": 1, "max_num": 1},
  994. },
  995. use_json_field=True,
  996. )
  997. class ExtendedImageChooserBlock(ImageChooserBlock):
  998. """
  999. Example of Block with custom get_api_representation method.
  1000. If the request has an 'extended' query param, it returns a dict of id and title,
  1001. otherwise, it returns the default value.
  1002. """
  1003. def get_api_representation(self, value, context=None):
  1004. image_id = super().get_api_representation(value, context=context)
  1005. if "request" in context and context["request"].query_params.get(
  1006. "extended", False
  1007. ):
  1008. return {"id": image_id, "title": value.title}
  1009. return image_id
  1010. class StreamPage(Page):
  1011. body = StreamField(
  1012. [
  1013. ("text", CharBlock()),
  1014. ("rich_text", RichTextBlock()),
  1015. ("image", ExtendedImageChooserBlock()),
  1016. (
  1017. "product",
  1018. StructBlock(
  1019. [
  1020. ("name", CharBlock()),
  1021. ("price", CharBlock()),
  1022. ]
  1023. ),
  1024. ),
  1025. ("raw_html", RawHTMLBlock()),
  1026. (
  1027. "books",
  1028. StreamBlock(
  1029. [
  1030. ("title", CharBlock()),
  1031. ("author", CharBlock()),
  1032. ]
  1033. ),
  1034. ),
  1035. ],
  1036. use_json_field=False,
  1037. )
  1038. api_fields = ("body",)
  1039. content_panels = [
  1040. FieldPanel("title"),
  1041. FieldPanel("body"),
  1042. ]
  1043. preview_modes = []
  1044. class DefaultStreamPage(Page):
  1045. body = StreamField(
  1046. [
  1047. ("text", CharBlock()),
  1048. ("rich_text", RichTextBlock()),
  1049. ("image", ImageChooserBlock()),
  1050. ],
  1051. default="",
  1052. use_json_field=False,
  1053. )
  1054. content_panels = [
  1055. FieldPanel("title"),
  1056. FieldPanel("body"),
  1057. ]
  1058. class MTIBasePage(Page):
  1059. is_creatable = False
  1060. class Meta:
  1061. verbose_name = "MTI Base page"
  1062. class MTIChildPage(MTIBasePage):
  1063. # Should be creatable by default, no need to set anything
  1064. pass
  1065. class AbstractPage(Page):
  1066. class Meta:
  1067. abstract = True
  1068. @register_setting
  1069. class TestSiteSetting(BaseSiteSetting):
  1070. title = models.CharField(max_length=100)
  1071. email = models.EmailField(max_length=50)
  1072. @register_setting
  1073. class TestGenericSetting(BaseGenericSetting):
  1074. title = models.CharField(max_length=100)
  1075. email = models.EmailField(max_length=50)
  1076. @register_setting
  1077. class ImportantPagesSiteSetting(BaseSiteSetting):
  1078. sign_up_page = models.ForeignKey(
  1079. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1080. )
  1081. general_terms_page = models.ForeignKey(
  1082. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1083. )
  1084. privacy_policy_page = models.ForeignKey(
  1085. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1086. )
  1087. @register_setting
  1088. class ImportantPagesGenericSetting(BaseGenericSetting):
  1089. sign_up_page = models.ForeignKey(
  1090. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1091. )
  1092. general_terms_page = models.ForeignKey(
  1093. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1094. )
  1095. privacy_policy_page = models.ForeignKey(
  1096. "wagtailcore.Page", related_name="+", null=True, on_delete=models.SET_NULL
  1097. )
  1098. @register_setting(icon="icon-setting-tag")
  1099. class IconSiteSetting(BaseSiteSetting):
  1100. pass
  1101. @register_setting(icon="icon-setting-tag")
  1102. class IconGenericSetting(BaseGenericSetting):
  1103. pass
  1104. class NotYetRegisteredSiteSetting(BaseSiteSetting):
  1105. pass
  1106. class NotYetRegisteredGenericSetting(BaseGenericSetting):
  1107. pass
  1108. @register_setting
  1109. class FileSiteSetting(BaseSiteSetting):
  1110. file = models.FileField()
  1111. @register_setting
  1112. class FileGenericSetting(BaseGenericSetting):
  1113. file = models.FileField()
  1114. class BlogCategory(models.Model):
  1115. name = models.CharField(unique=True, max_length=80)
  1116. class BlogCategoryBlogPage(models.Model):
  1117. category = models.ForeignKey(
  1118. BlogCategory, related_name="+", on_delete=models.CASCADE
  1119. )
  1120. page = ParentalKey(
  1121. "ManyToManyBlogPage", related_name="categories", on_delete=models.CASCADE
  1122. )
  1123. panels = [
  1124. FieldPanel("category"),
  1125. ]
  1126. class ManyToManyBlogPage(Page):
  1127. """
  1128. A page type with two different kinds of M2M relation.
  1129. We don't formally support these, but we don't want them to cause
  1130. hard breakages either.
  1131. """
  1132. body = RichTextField(blank=True)
  1133. adverts = models.ManyToManyField(Advert, blank=True)
  1134. blog_categories = models.ManyToManyField(
  1135. BlogCategory, through=BlogCategoryBlogPage, blank=True
  1136. )
  1137. # make first_published_at editable on this page model
  1138. settings_panels = Page.settings_panels + [
  1139. FieldPanel("first_published_at"),
  1140. ]
  1141. class OneToOnePage(Page):
  1142. """
  1143. A Page containing a O2O relation.
  1144. """
  1145. body = RichTextBlock(blank=True)
  1146. page_ptr = models.OneToOneField(
  1147. Page, parent_link=True, related_name="+", on_delete=models.CASCADE
  1148. )
  1149. class GenericSnippetPage(Page):
  1150. """
  1151. A page containing a reference to an arbitrary snippet (or any model for that matter)
  1152. linked by a GenericForeignKey
  1153. """
  1154. snippet_content_type = models.ForeignKey(
  1155. ContentType, on_delete=models.SET_NULL, null=True
  1156. )
  1157. snippet_object_id = models.PositiveIntegerField(null=True)
  1158. snippet_content_object = GenericForeignKey(
  1159. "snippet_content_type", "snippet_object_id"
  1160. )
  1161. class CustomImageFilePath(AbstractImage):
  1162. def get_upload_to(self, filename):
  1163. """Create a path that's file-system friendly.
  1164. By hashing the file's contents we guarantee an equal distribution
  1165. of files within our root directories. This also gives us a
  1166. better chance of uploading images with the same filename, but
  1167. different contents - this isn't guaranteed as we're only using
  1168. the first three characters of the checksum.
  1169. """
  1170. original_filepath = super().get_upload_to(filename)
  1171. folder_name, filename = original_filepath.split(os.path.sep)
  1172. # Ensure that we consume the entire file, we can't guarantee that
  1173. # the stream has not be partially (or entirely) consumed by
  1174. # another process
  1175. original_position = self.file.tell()
  1176. self.file.seek(0)
  1177. hash256 = hashlib.sha256()
  1178. while True:
  1179. data = self.file.read(256)
  1180. if not data:
  1181. break
  1182. hash256.update(data)
  1183. checksum = hash256.hexdigest()
  1184. self.file.seek(original_position)
  1185. return os.path.join(folder_name, checksum[:3], filename)
  1186. class CustomPageQuerySet(PageQuerySet):
  1187. def about_spam(self):
  1188. return self.filter(title__contains="spam")
  1189. CustomManager = PageManager.from_queryset(CustomPageQuerySet)
  1190. class CustomManagerPage(Page):
  1191. objects = CustomManager()
  1192. class MyBasePage(Page):
  1193. """
  1194. A base Page model, used to set site-wide defaults and overrides.
  1195. """
  1196. objects = CustomManager()
  1197. class Meta:
  1198. abstract = True
  1199. class MyCustomPage(MyBasePage):
  1200. pass
  1201. class ValidatedPage(Page):
  1202. foo = models.CharField(max_length=255)
  1203. base_form_class = ValidatedPageForm
  1204. content_panels = Page.content_panels + [
  1205. FieldPanel("foo"),
  1206. ]
  1207. class DefaultRichTextFieldPage(Page):
  1208. body = RichTextField()
  1209. content_panels = [
  1210. FieldPanel("title", classname="full title"),
  1211. FieldPanel("body"),
  1212. ]
  1213. class DefaultRichBlockFieldPage(Page):
  1214. body = StreamField(
  1215. [
  1216. ("rich_text", RichTextBlock()),
  1217. ],
  1218. use_json_field=False,
  1219. )
  1220. content_panels = Page.content_panels + [FieldPanel("body")]
  1221. class CustomRichTextFieldPage(Page):
  1222. body = RichTextField(editor="custom")
  1223. content_panels = [
  1224. FieldPanel("title", classname="full title"),
  1225. FieldPanel("body"),
  1226. ]
  1227. class CustomRichBlockFieldPage(Page):
  1228. body = StreamField(
  1229. [
  1230. ("rich_text", RichTextBlock(editor="custom")),
  1231. ],
  1232. use_json_field=False,
  1233. )
  1234. content_panels = [
  1235. FieldPanel("title", classname="full title"),
  1236. FieldPanel("body"),
  1237. ]
  1238. class RichTextFieldWithFeaturesPage(Page):
  1239. body = RichTextField(features=["quotation", "embed", "made-up-feature"])
  1240. content_panels = [
  1241. FieldPanel("title", classname="full title"),
  1242. FieldPanel("body"),
  1243. ]
  1244. # a page that only contains RichTextField within an InlinePanel,
  1245. # to test that the inline child's form media gets pulled through
  1246. class SectionedRichTextPageSection(Orderable):
  1247. page = ParentalKey(
  1248. "tests.SectionedRichTextPage", related_name="sections", on_delete=models.CASCADE
  1249. )
  1250. body = RichTextField()
  1251. panels = [FieldPanel("body")]
  1252. class SectionedRichTextPage(Page):
  1253. content_panels = [
  1254. FieldPanel("title", classname="full title"),
  1255. InlinePanel("sections"),
  1256. ]
  1257. class InlineStreamPageSection(Orderable):
  1258. page = ParentalKey(
  1259. "tests.InlineStreamPage", related_name="sections", on_delete=models.CASCADE
  1260. )
  1261. body = StreamField(
  1262. [
  1263. ("text", CharBlock()),
  1264. ("rich_text", RichTextBlock()),
  1265. ("image", ImageChooserBlock()),
  1266. ],
  1267. use_json_field=False,
  1268. )
  1269. panels = [FieldPanel("body")]
  1270. class InlineStreamPage(Page):
  1271. content_panels = [
  1272. FieldPanel("title", classname="full title"),
  1273. InlinePanel("sections"),
  1274. ]
  1275. class TableBlockStreamPage(Page):
  1276. table = StreamField([("table", TableBlock())], use_json_field=False)
  1277. content_panels = [FieldPanel("table")]
  1278. class UserProfile(models.Model):
  1279. # Wagtail's schema must be able to coexist alongside a custom UserProfile model
  1280. user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  1281. favourite_colour = models.CharField(max_length=255)
  1282. class PanelSiteSettings(TestSiteSetting):
  1283. panels = [FieldPanel("title")]
  1284. class PanelGenericSettings(TestGenericSetting):
  1285. panels = [FieldPanel("title")]
  1286. class TabbedSiteSettings(TestSiteSetting):
  1287. edit_handler = TabbedInterface(
  1288. [
  1289. ObjectList([FieldPanel("title")], heading="First tab"),
  1290. ObjectList([FieldPanel("email")], heading="Second tab"),
  1291. ]
  1292. )
  1293. class TabbedGenericSettings(TestGenericSetting):
  1294. edit_handler = TabbedInterface(
  1295. [
  1296. ObjectList([FieldPanel("title")], heading="First tab"),
  1297. ObjectList([FieldPanel("email")], heading="Second tab"),
  1298. ]
  1299. )
  1300. class AlwaysShowInMenusPage(Page):
  1301. show_in_menus_default = True
  1302. # test for AddField migrations on StreamFields using various default values
  1303. class AddedStreamFieldWithoutDefaultPage(Page):
  1304. body = StreamField([("title", CharBlock())], use_json_field=False)
  1305. class AddedStreamFieldWithEmptyStringDefaultPage(Page):
  1306. body = StreamField([("title", CharBlock())], default="", use_json_field=False)
  1307. class AddedStreamFieldWithEmptyListDefaultPage(Page):
  1308. body = StreamField([("title", CharBlock())], default=[], use_json_field=False)
  1309. class SecretPage(Page):
  1310. boring_data = models.TextField()
  1311. secret_data = models.TextField()
  1312. content_panels = Page.content_panels + [
  1313. FieldPanel("boring_data"),
  1314. FieldPanel("secret_data", permission="superuser"),
  1315. ]
  1316. class SimpleParentPage(Page):
  1317. subpage_types = ["tests.SimpleChildPage"]
  1318. class SimpleChildPage(Page):
  1319. parent_page_types = ["tests.SimpleParentPage"]
  1320. max_count_per_parent = 1
  1321. class PersonPage(Page):
  1322. first_name = models.CharField(
  1323. max_length=255,
  1324. verbose_name="First Name",
  1325. )
  1326. last_name = models.CharField(
  1327. max_length=255,
  1328. verbose_name="Last Name",
  1329. )
  1330. content_panels = Page.content_panels + [
  1331. MultiFieldPanel(
  1332. [
  1333. FieldPanel("first_name"),
  1334. FieldPanel("last_name"),
  1335. ],
  1336. "Person",
  1337. ),
  1338. InlinePanel("addresses", label="Address"),
  1339. ]
  1340. class Meta:
  1341. verbose_name = "Person"
  1342. verbose_name_plural = "Persons"
  1343. class Address(index.Indexed, ClusterableModel, Orderable):
  1344. address = models.CharField(
  1345. max_length=255,
  1346. verbose_name="Address",
  1347. )
  1348. tags = ClusterTaggableManager(
  1349. through="tests.AddressTag",
  1350. blank=True,
  1351. )
  1352. person = ParentalKey(
  1353. to="tests.PersonPage", related_name="addresses", verbose_name="Person"
  1354. )
  1355. panels = [
  1356. FieldPanel("address"),
  1357. FieldPanel("tags"),
  1358. ]
  1359. class Meta:
  1360. verbose_name = "Address"
  1361. verbose_name_plural = "Addresses"
  1362. class AddressTag(TaggedItemBase):
  1363. content_object = ParentalKey(
  1364. to="tests.Address", on_delete=models.CASCADE, related_name="tagged_items"
  1365. )
  1366. class RestaurantPage(Page):
  1367. tags = ClusterTaggableManager(through="tests.TaggedRestaurant", blank=True)
  1368. content_panels = Page.content_panels + [
  1369. FieldPanel("tags"),
  1370. ]
  1371. class RestaurantTag(TagBase):
  1372. free_tagging = False
  1373. class Meta:
  1374. verbose_name = "Tag"
  1375. verbose_name_plural = "Tags"
  1376. class TaggedRestaurant(ItemBase):
  1377. tag = models.ForeignKey(
  1378. RestaurantTag, related_name="tagged_restaurants", on_delete=models.CASCADE
  1379. )
  1380. content_object = ParentalKey(
  1381. to="tests.RestaurantPage", on_delete=models.CASCADE, related_name="tagged_items"
  1382. )
  1383. class SimpleTask(Task):
  1384. pass
  1385. # StreamField media definitions must not be evaluated at startup (e.g. during system checks) -
  1386. # these may fail if e.g. ManifestStaticFilesStorage is in use and collectstatic has not been run.
  1387. # Check this with a media definition that deliberately errors; if media handling is not set up
  1388. # correctly, then the mere presence of this model definition will cause startup to fail.
  1389. class DeadlyTextInput(forms.TextInput):
  1390. @property
  1391. def media(self):
  1392. raise Exception("BOOM! Attempted to evaluate DeadlyTextInput.media")
  1393. class DeadlyCharBlock(FieldBlock):
  1394. def __init__(self, *args, **kwargs):
  1395. self.field = forms.CharField(widget=DeadlyTextInput())
  1396. super().__init__(*args, **kwargs)
  1397. class DeadlyStreamPage(Page):
  1398. body = StreamField(
  1399. [
  1400. ("title", DeadlyCharBlock()),
  1401. ],
  1402. use_json_field=False,
  1403. )
  1404. content_panels = Page.content_panels + [
  1405. FieldPanel("body"),
  1406. ]
  1407. # Check that get_image_model and get_document_model work at import time
  1408. # (so that it's possible to use them in foreign key definitions, for example)
  1409. ReimportedImageModel = get_image_model()
  1410. ReimportedDocumentModel = get_document_model()
  1411. # Custom document model with a custom tag field
  1412. class TaggedRestaurantDocument(ItemBase):
  1413. tag = models.ForeignKey(
  1414. RestaurantTag, related_name="tagged_documents", on_delete=models.CASCADE
  1415. )
  1416. content_object = models.ForeignKey(
  1417. to="tests.CustomRestaurantDocument",
  1418. on_delete=models.CASCADE,
  1419. related_name="tagged_items",
  1420. )
  1421. class CustomRestaurantDocument(AbstractDocument):
  1422. tags = TaggableManager(
  1423. help_text=None,
  1424. blank=True,
  1425. verbose_name="tags",
  1426. through=TaggedRestaurantDocument,
  1427. )
  1428. admin_form_fields = Document.admin_form_fields
  1429. # Custom image model with a custom tag field
  1430. class TaggedRestaurantImage(ItemBase):
  1431. tag = models.ForeignKey(
  1432. RestaurantTag, related_name="tagged_images", on_delete=models.CASCADE
  1433. )
  1434. content_object = models.ForeignKey(
  1435. to="tests.CustomRestaurantImage",
  1436. on_delete=models.CASCADE,
  1437. related_name="tagged_items",
  1438. )
  1439. class CustomRestaurantImage(AbstractImage):
  1440. tags = TaggableManager(
  1441. help_text=None, blank=True, verbose_name="tags", through=TaggedRestaurantImage
  1442. )
  1443. admin_form_fields = Image.admin_form_fields