page_models.py 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633
  1. """
  2. Base and abstract pages used in CodeRed CMS.
  3. """
  4. import json
  5. import logging
  6. import os
  7. import geocoder
  8. from django import forms
  9. from django.conf import settings
  10. from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
  11. from django.core.files.storage import FileSystemStorage
  12. from django.core.mail import EmailMessage
  13. from django.core.paginator import Paginator
  14. from django.core.serializers.json import DjangoJSONEncoder
  15. from django.core.validators import MaxValueValidator, MinValueValidator
  16. from django.db import models
  17. from django.http import JsonResponse
  18. from django.shortcuts import render, redirect
  19. from django.template import Context, Template
  20. from django.template.loader import render_to_string
  21. from django.utils import timezone
  22. from django.utils.html import strip_tags
  23. from django.utils.safestring import mark_safe
  24. from django.utils.translation import ugettext_lazy as _
  25. from eventtools.models import BaseEvent, BaseOccurrence
  26. from icalendar import Event as ICalEvent
  27. from modelcluster.fields import ParentalKey, ParentalManyToManyField
  28. from modelcluster.tags import ClusterTaggableManager
  29. from taggit.models import TaggedItemBase
  30. from wagtail.admin.edit_handlers import (
  31. HelpPanel,
  32. FieldPanel,
  33. FieldRowPanel,
  34. InlinePanel,
  35. MultiFieldPanel,
  36. ObjectList,
  37. PageChooserPanel,
  38. StreamFieldPanel,
  39. TabbedInterface)
  40. from wagtail.core import hooks
  41. from wagtail.core.fields import StreamField
  42. from wagtail.core.models import Orderable, PageBase, Page, Site
  43. from wagtail.core.utils import resolve_model_string
  44. from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
  45. from wagtail.contrib.forms.forms import WagtailAdminFormPageForm
  46. from wagtail.images.edit_handlers import ImageChooserPanel
  47. from wagtail.contrib.forms.models import FormSubmission
  48. from wagtail.search import index
  49. from wagtailcache.cache import WagtailCacheMixin
  50. from coderedcms import schema, utils
  51. from coderedcms.blocks import (
  52. CONTENT_STREAMBLOCKS,
  53. LAYOUT_STREAMBLOCKS,
  54. ContentWallBlock,
  55. OpenHoursBlock,
  56. StructuredDataActionBlock)
  57. from coderedcms.fields import ColorField
  58. from coderedcms.forms import CoderedFormBuilder, CoderedSubmissionsListView
  59. from coderedcms.models.snippet_models import ClassifierTerm
  60. from coderedcms.models.wagtailsettings_models import GeneralSettings, LayoutSettings, SeoSettings, GoogleApiSettings
  61. from coderedcms.settings import cr_settings
  62. from coderedcms.widgets import ClassifierSelectWidget
  63. logger = logging.getLogger('coderedcms')
  64. CODERED_PAGE_MODELS = []
  65. def get_page_models():
  66. return CODERED_PAGE_MODELS
  67. class CoderedPageMeta(PageBase):
  68. def __init__(cls, name, bases, dct):
  69. super().__init__(name, bases, dct)
  70. if 'amp_template' not in dct:
  71. cls.amp_template = None
  72. if 'search_db_include' not in dct:
  73. cls.search_db_include = False
  74. if 'search_db_boost' not in dct:
  75. cls.search_db_boost = 0
  76. if 'search_filterable' not in dct:
  77. cls.search_filterable = False
  78. if 'search_name' not in dct:
  79. cls.search_name = cls._meta.verbose_name
  80. if 'search_name_plural' not in dct:
  81. cls.search_name_plural = cls._meta.verbose_name_plural
  82. if 'search_template' not in dct:
  83. cls.search_template = 'coderedcms/pages/search_result.html'
  84. if not cls._meta.abstract:
  85. CODERED_PAGE_MODELS.append(cls)
  86. class CoderedTag(TaggedItemBase):
  87. class Meta:
  88. verbose_name = _('CodeRed Tag')
  89. content_object = ParentalKey('coderedcms.CoderedPage', related_name='tagged_items')
  90. class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
  91. """
  92. General use page with caching, templating, and SEO functionality.
  93. All pages should inherit from this.
  94. """
  95. class Meta:
  96. verbose_name = _('CodeRed Page')
  97. # Do not allow this page type to be created in wagtail admin
  98. is_creatable = False
  99. # Templates
  100. # The page will render the following templates under certain conditions:
  101. #
  102. # template = ''
  103. # amp_template = ''
  104. # ajax_template = ''
  105. # search_template = ''
  106. ###############
  107. # Content fields
  108. ###############
  109. cover_image = models.ForeignKey(
  110. 'wagtailimages.Image',
  111. null=True,
  112. blank=True,
  113. on_delete=models.SET_NULL,
  114. related_name='+',
  115. verbose_name=_('Cover image'),
  116. )
  117. ###############
  118. # Index fields
  119. ###############
  120. # Subclasses can override this to enabled index features by default.
  121. index_show_subpages_default = False
  122. # Subclasses can override this to query on a specific
  123. # page model, rather than the default wagtail Page.
  124. index_query_pagemodel = 'coderedcms.CoderedPage'
  125. # Subclasses can override these fields to enable custom
  126. # ordering based on specific subpage fields.
  127. index_order_by_default = ''
  128. index_order_by_choices = (
  129. ('', _('Default Ordering')),
  130. ('-first_published_at', _('Date first published, newest to oldest')),
  131. ('first_published_at', _('Date first published, oldest to newest')),
  132. ('-last_published_at', _('Date updated, newest to oldest')),
  133. ('last_published_at', _('Date updated, oldest to newest')),
  134. ('title', _('Title, alphabetical')),
  135. ('-title', _('Title, reverse alphabetical')),
  136. )
  137. index_show_subpages = models.BooleanField(
  138. default=index_show_subpages_default,
  139. verbose_name=_('Show list of child pages')
  140. )
  141. index_order_by = models.CharField(
  142. max_length=255,
  143. choices=index_order_by_choices,
  144. default=index_order_by_default,
  145. blank=True,
  146. verbose_name=_('Order child pages by'),
  147. )
  148. index_num_per_page = models.PositiveIntegerField(
  149. default=10,
  150. verbose_name=_('Number per page'),
  151. )
  152. index_classifiers = ParentalManyToManyField(
  153. 'coderedcms.Classifier',
  154. blank=True,
  155. verbose_name=_('Filter child pages by'),
  156. help_text=_('Enable filtering child pages by these classifiers.'),
  157. )
  158. ###############
  159. # Layout fields
  160. ###############
  161. custom_template = models.CharField(
  162. blank=True,
  163. max_length=255,
  164. choices=None,
  165. verbose_name=_('Template')
  166. )
  167. ###############
  168. # SEO fields
  169. ###############
  170. og_image = models.ForeignKey(
  171. 'wagtailimages.Image',
  172. null=True,
  173. blank=True,
  174. on_delete=models.SET_NULL,
  175. related_name='+',
  176. verbose_name=_('Open Graph preview image'),
  177. help_text=_('The image shown when linking to this page on social media. If blank, defaults to article cover image, or logo in Settings > Layout > Logo')
  178. )
  179. struct_org_type = models.CharField(
  180. default='',
  181. blank=True,
  182. max_length=255,
  183. choices=schema.SCHEMA_ORG_CHOICES,
  184. verbose_name=_('Organization type'),
  185. help_text=_('If blank, no structured data will be used on this page.')
  186. )
  187. struct_org_name = models.CharField(
  188. default='',
  189. blank=True,
  190. max_length=255,
  191. verbose_name=_('Organization name'),
  192. help_text=_('Leave blank to use the site name in Settings > Sites')
  193. )
  194. struct_org_logo = models.ForeignKey(
  195. 'wagtailimages.Image',
  196. null=True,
  197. blank=True,
  198. on_delete=models.SET_NULL,
  199. related_name='+',
  200. verbose_name=_('Organization logo'),
  201. help_text=_('Leave blank to use the logo in Settings > Layout > Logo')
  202. )
  203. struct_org_image = models.ForeignKey(
  204. 'wagtailimages.Image',
  205. null=True,
  206. blank=True,
  207. on_delete=models.SET_NULL,
  208. related_name='+',
  209. verbose_name=_('Photo of Organization'),
  210. help_text=_('A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.')
  211. )
  212. struct_org_phone = models.CharField(
  213. blank=True,
  214. max_length=255,
  215. verbose_name=_('Telephone number'),
  216. help_text=_('Include country code for best results. For example: +1-216-555-8000')
  217. )
  218. struct_org_address_street = models.CharField(
  219. blank=True,
  220. max_length=255,
  221. verbose_name=_('Street address'),
  222. help_text=_('House number and street. For example, 55 Public Square Suite 1710')
  223. )
  224. struct_org_address_locality = models.CharField(
  225. blank=True,
  226. max_length=255,
  227. verbose_name=_('City'),
  228. help_text=_('City or locality. For example, Cleveland')
  229. )
  230. struct_org_address_region = models.CharField(
  231. blank=True,
  232. max_length=255,
  233. verbose_name=_('State'),
  234. help_text=_('State, province, county, or region. For example, OH')
  235. )
  236. struct_org_address_postal = models.CharField(
  237. blank=True,
  238. max_length=255,
  239. verbose_name=_('Postal code'),
  240. help_text=_('Zip or postal code. For example, 44113')
  241. )
  242. struct_org_address_country = models.CharField(
  243. blank=True,
  244. max_length=255,
  245. verbose_name=_('Country'),
  246. help_text=_('For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptible https://en.wikipedia.org/wiki/ISO_3166-1')
  247. )
  248. struct_org_geo_lat = models.DecimalField(
  249. blank=True,
  250. null=True,
  251. max_digits=10,
  252. decimal_places=8,
  253. verbose_name=_('Geographic latitude')
  254. )
  255. struct_org_geo_lng = models.DecimalField(
  256. blank=True,
  257. null=True,
  258. max_digits=10,
  259. decimal_places=8,
  260. verbose_name=_('Geographic longitude')
  261. )
  262. struct_org_hours = StreamField(
  263. [
  264. ('hours', OpenHoursBlock()),
  265. ],
  266. blank=True,
  267. verbose_name=_('Hours of operation')
  268. )
  269. struct_org_actions = StreamField(
  270. [
  271. ('actions', StructuredDataActionBlock())
  272. ],
  273. blank=True,
  274. verbose_name=_('Actions')
  275. )
  276. struct_org_extra_json = models.TextField(
  277. blank=True,
  278. verbose_name=_('Additional Organization markup'),
  279. help_text=_('Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.')
  280. )
  281. ###############
  282. # Classify
  283. ###############
  284. classifier_terms = ParentalManyToManyField(
  285. 'coderedcms.ClassifierTerm',
  286. blank=True,
  287. verbose_name=_('Classifiers'),
  288. help_text=_('Categorize and group pages together with classifiers. Used to organize and filter pages across the site.'),
  289. )
  290. tags = ClusterTaggableManager(
  291. through=CoderedTag,
  292. blank=True,
  293. verbose_name=_('Tags'),
  294. help_text=_('Used to organize pages across the site.'),
  295. )
  296. ###############
  297. # Settings
  298. ###############
  299. content_walls = StreamField(
  300. [
  301. ('content_wall', ContentWallBlock())
  302. ],
  303. blank=True,
  304. verbose_name=_('Content Walls')
  305. )
  306. ###############
  307. # Search
  308. ###############
  309. search_fields = [
  310. index.SearchField('title', partial_match=True, boost=3),
  311. index.SearchField('seo_title', partial_match=True, boost=3),
  312. index.SearchField('search_description', boost=2),
  313. index.FilterField('title'),
  314. index.FilterField('id'),
  315. index.FilterField('live'),
  316. index.FilterField('owner'),
  317. index.FilterField('content_type'),
  318. index.FilterField('path'),
  319. index.FilterField('depth'),
  320. index.FilterField('locked'),
  321. index.FilterField('first_published_at'),
  322. index.FilterField('last_published_at'),
  323. index.FilterField('latest_revision_created_at'),
  324. index.FilterField('index_show_subpages'),
  325. index.FilterField('index_order_by'),
  326. index.FilterField('custom_template'),
  327. index.FilterField('classifier_terms'),
  328. ]
  329. ###############
  330. # Panels
  331. ###############
  332. content_panels = Page.content_panels + [
  333. ImageChooserPanel('cover_image'),
  334. ]
  335. body_content_panels = []
  336. bottom_content_panels = []
  337. classify_panels = [
  338. FieldPanel('classifier_terms', widget=ClassifierSelectWidget()),
  339. FieldPanel('tags'),
  340. ]
  341. layout_panels = [
  342. MultiFieldPanel(
  343. [
  344. FieldPanel('custom_template')
  345. ],
  346. heading=_('Visual Design')
  347. ),
  348. MultiFieldPanel(
  349. [
  350. FieldPanel('index_show_subpages'),
  351. FieldPanel('index_num_per_page'),
  352. FieldPanel('index_order_by'),
  353. FieldPanel('index_classifiers', widget=forms.CheckboxSelectMultiple()),
  354. ],
  355. heading=_('Show Child Pages')
  356. )
  357. ]
  358. promote_panels = [
  359. MultiFieldPanel(
  360. [
  361. FieldPanel('slug'),
  362. FieldPanel('seo_title'),
  363. FieldPanel('search_description'),
  364. ImageChooserPanel('og_image'),
  365. ],
  366. _('Page Meta Data')
  367. ),
  368. MultiFieldPanel(
  369. [
  370. HelpPanel(
  371. heading=_('About Organization Structured Data'),
  372. content=_("""The fields below help define brand, contact, and storefront
  373. information to search engines. This information should be filled out on
  374. the site’s root page (Home Page). If your organization has multiple locations,
  375. then also fill this info out on each location page using that particular
  376. location’s info."""),
  377. ),
  378. FieldPanel('struct_org_type'),
  379. FieldPanel('struct_org_name'),
  380. ImageChooserPanel('struct_org_logo'),
  381. ImageChooserPanel('struct_org_image'),
  382. FieldPanel('struct_org_phone'),
  383. FieldPanel('struct_org_address_street'),
  384. FieldPanel('struct_org_address_locality'),
  385. FieldPanel('struct_org_address_region'),
  386. FieldPanel('struct_org_address_postal'),
  387. FieldPanel('struct_org_address_country'),
  388. FieldPanel('struct_org_geo_lat'),
  389. FieldPanel('struct_org_geo_lng'),
  390. StreamFieldPanel('struct_org_hours'),
  391. StreamFieldPanel('struct_org_actions'),
  392. FieldPanel('struct_org_extra_json'),
  393. ],
  394. _('Structured Data - Organization')
  395. ),
  396. ]
  397. settings_panels = Page.settings_panels + [
  398. StreamFieldPanel('content_walls'),
  399. ]
  400. integration_panels = []
  401. def __init__(self, *args, **kwargs):
  402. """
  403. Inject custom choices and defalts into the form fields
  404. to enable customization by subclasses.
  405. """
  406. super().__init__(*args, **kwargs)
  407. klassname = self.__class__.__name__.lower()
  408. template_choices = cr_settings['FRONTEND_TEMPLATES_PAGES'].get('*', ()) + \
  409. cr_settings['FRONTEND_TEMPLATES_PAGES'].get(klassname, ())
  410. self._meta.get_field('index_order_by').choices = self.index_order_by_choices
  411. self._meta.get_field('custom_template').choices = template_choices
  412. if not self.id:
  413. self.index_order_by = self.index_order_by_default
  414. self.index_show_subpages = self.index_show_subpages_default
  415. @classmethod
  416. def get_edit_handler(cls):
  417. """
  418. Override to "lazy load" the panels overriden by subclasses.
  419. """
  420. panels = [
  421. ObjectList(cls.content_panels + cls.body_content_panels + cls.bottom_content_panels, heading=_('Content')),
  422. ObjectList(cls.classify_panels, heading=_('Classify')),
  423. ObjectList(cls.layout_panels, heading=_('Layout')),
  424. ObjectList(cls.promote_panels, heading=_('SEO'), classname="seo"),
  425. ObjectList(cls.settings_panels, heading=_('Settings'), classname="settings"),
  426. ]
  427. if cls.integration_panels:
  428. panels.append(ObjectList(cls.integration_panels, heading='Integrations', classname='integrations'))
  429. return TabbedInterface(panels).bind_to_model(cls)
  430. def get_struct_org_name(self):
  431. """
  432. Gets org name for sturctured data using a fallback.
  433. """
  434. if self.struct_org_name:
  435. return self.struct_org_name
  436. return self.get_site().site_name
  437. def get_struct_org_logo(self):
  438. """
  439. Gets logo for structured data using a fallback.
  440. """
  441. if self.struct_org_logo:
  442. return self.struct_org_logo
  443. else:
  444. layout_settings = LayoutSettings.for_site(self.get_site())
  445. if layout_settings.logo:
  446. return layout_settings.logo
  447. return None
  448. def get_template(self, request, *args, **kwargs):
  449. """
  450. Override parent to serve different templates based on querystring.
  451. """
  452. if 'amp' in request.GET and hasattr(self, 'amp_template'):
  453. seo_settings = SeoSettings.for_site(request.site)
  454. if seo_settings.amp_pages:
  455. if request.is_ajax():
  456. return self.ajax_template or self.amp_template
  457. return self.amp_template
  458. if self.custom_template:
  459. return self.custom_template
  460. return super(CoderedPage, self).get_template(request, args, kwargs)
  461. def get_index_children(self):
  462. """
  463. Returns query of subpages as defined by `index_` variables.
  464. """
  465. if self.index_query_pagemodel:
  466. querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
  467. query = querymodel.objects.child_of(self).live()
  468. else:
  469. query = self.get_children().live()
  470. if self.index_order_by:
  471. return query.order_by(self.index_order_by)
  472. return query
  473. def get_content_walls(self, check_child_setting=True):
  474. current_content_walls = []
  475. if check_child_setting:
  476. for wall in self.content_walls:
  477. if wall.value['show_content_wall_on_children']:
  478. current_content_walls.append(wall.value)
  479. else:
  480. current_content_walls = self.content_walls
  481. try:
  482. return list(current_content_walls) + self.get_parent().specific.get_content_walls()
  483. except AttributeError:
  484. return list(current_content_walls)
  485. def get_context(self, request, *args, **kwargs):
  486. """
  487. Add child pages and paginated child pages to context.
  488. """
  489. context = super().get_context(request)
  490. if self.index_show_subpages:
  491. # Get child pages
  492. all_children = self.get_index_children()
  493. # Filter by classifier terms if applicable
  494. if len(request.GET) > 0 and self.index_classifiers.exists():
  495. # Look up comma separated ClassifierTerm slugs i.e. `/?c=term1-slug,term2-slug`
  496. terms = []
  497. get_c = request.GET.get('c', None)
  498. if get_c:
  499. terms = get_c.split(',')
  500. # Else look up individual querystrings i.e. `/?classifier-slug=term1-slug`
  501. else:
  502. for classifier in self.index_classifiers.all().only('slug'):
  503. get_term = request.GET.get(classifier.slug, None)
  504. if get_term:
  505. terms.append(get_term)
  506. if len(terms) > 0:
  507. selected_terms = ClassifierTerm.objects.filter(slug__in=terms)
  508. context['selected_terms'] = selected_terms
  509. if len(selected_terms) > 0:
  510. try:
  511. for term in selected_terms:
  512. all_children = all_children.filter(classifier_terms=term)
  513. except:
  514. logger.warning("Tried to filter by ClassifierTerm, but <%s.%s ('%s')>.get_index_children() did not return a queryset or is not a queryset of CoderedPage models.", self._meta.app_label, self.__class__.__name__, self.title)
  515. paginator = Paginator(all_children, self.index_num_per_page)
  516. pagenum = request.GET.get('p', 1)
  517. try:
  518. paged_children = paginator.page(pagenum)
  519. except:
  520. paged_children = paginator.page(1)
  521. context['index_paginated'] = paged_children
  522. context['index_children'] = all_children
  523. context['content_walls'] = self.get_content_walls(check_child_setting=False)
  524. return context
  525. ###############################################################################
  526. # Abstract pages providing pre-built common website functionality, suitable for subclassing.
  527. # These are abstract so subclasses can override fields if desired.
  528. ###############################################################################
  529. class CoderedWebPage(CoderedPage):
  530. """
  531. Provides a body and body-related functionality.
  532. This is abstract so that subclasses can override the body StreamField.
  533. """
  534. class Meta:
  535. verbose_name = _('CodeRed Web Page')
  536. abstract = True
  537. template = 'coderedcms/pages/web_page.html'
  538. # Child pages should override based on what blocks they want in the body.
  539. # Default is LAYOUT_STREAMBLOCKS which is the fullest editor experience.
  540. body = StreamField(LAYOUT_STREAMBLOCKS, null=True, blank=True)
  541. # Search fields
  542. search_fields = (
  543. CoderedPage.search_fields +
  544. [index.SearchField('body')]
  545. )
  546. # Panels
  547. body_content_panels = [
  548. StreamFieldPanel('body'),
  549. ]
  550. @property
  551. def body_preview(self):
  552. """
  553. A shortened version of the body without HTML tags.
  554. """
  555. # add spaces between tags for legibility
  556. body = str(self.body).replace('>', '> ')
  557. # strip tags
  558. body = strip_tags(body)
  559. # truncate and add ellipses
  560. preview = body[:200] + "..." if len(body) > 200 else body
  561. return mark_safe(preview)
  562. @property
  563. def page_ptr(self):
  564. """
  565. Overwrite of `page_ptr` to make it compatible with wagtailimportexport.
  566. """
  567. return self.base_page_ptr
  568. @page_ptr.setter
  569. def page_ptr(self, value):
  570. self.base_page_ptr = value
  571. class CoderedArticlePage(CoderedWebPage):
  572. """
  573. Article, suitable for news or blog content.
  574. """
  575. class Meta:
  576. verbose_name = _('CodeRed Article')
  577. abstract = True
  578. template = 'coderedcms/pages/article_page.html'
  579. amp_template = 'coderedcms/pages/article_page.amp.html'
  580. # Override body to provide simpler content
  581. body = StreamField(CONTENT_STREAMBLOCKS, null=True, blank=True)
  582. caption = models.CharField(
  583. max_length=255,
  584. blank=True,
  585. verbose_name=_('Caption'),
  586. )
  587. author = models.ForeignKey(
  588. settings.AUTH_USER_MODEL,
  589. null=True,
  590. blank=True,
  591. editable=True,
  592. on_delete=models.SET_NULL,
  593. verbose_name=_('Author'),
  594. )
  595. author_display = models.CharField(
  596. max_length=255,
  597. blank=True,
  598. verbose_name=_('Display author as'),
  599. help_text=_('Override how the author’s name displays on this article.'),
  600. )
  601. date_display = models.DateField(
  602. null=True,
  603. blank=True,
  604. verbose_name=_('Display publish date'),
  605. )
  606. def get_author_name(self):
  607. """
  608. Gets author name using a fallback.
  609. """
  610. if self.author_display:
  611. return self.author_display
  612. if self.author:
  613. return self.author.get_full_name()
  614. return ''
  615. def get_pub_date(self):
  616. """
  617. Gets published date.
  618. """
  619. if self.date_display:
  620. return self.date_display
  621. return ''
  622. def get_description(self):
  623. """
  624. Gets the description using a fallback.
  625. """
  626. if self.search_description:
  627. return self.search_description
  628. if self.caption:
  629. return self.caption
  630. if self.body_preview:
  631. return self.body_preview
  632. return ''
  633. search_fields = (
  634. CoderedWebPage.search_fields +
  635. [
  636. index.SearchField('caption', boost=2),
  637. index.FilterField('author'),
  638. index.FilterField('author_display'),
  639. index.FilterField('date_display'),
  640. ]
  641. )
  642. content_panels = CoderedWebPage.content_panels + [
  643. FieldPanel('caption'),
  644. MultiFieldPanel(
  645. [
  646. FieldPanel('author'),
  647. FieldPanel('author_display'),
  648. FieldPanel('date_display'),
  649. ],
  650. _('Publication Info')
  651. )
  652. ]
  653. class CoderedArticleIndexPage(CoderedWebPage):
  654. """
  655. Shows a list of article sub-pages.
  656. """
  657. class Meta:
  658. verbose_name = _('CodeRed Article Index Page')
  659. abstract = True
  660. template = 'coderedcms/pages/article_index_page.html'
  661. index_show_subpages_default = True
  662. index_order_by_default = '-date_display'
  663. index_order_by_choices = (('-date_display', 'Display publish date, newest first'),) + \
  664. CoderedWebPage.index_order_by_choices
  665. show_images = models.BooleanField(
  666. default=True,
  667. verbose_name=_('Show images'),
  668. )
  669. show_captions = models.BooleanField(
  670. default=True,
  671. )
  672. show_meta = models.BooleanField(
  673. default=True,
  674. verbose_name=_('Show author and date info'),
  675. )
  676. show_preview_text = models.BooleanField(
  677. default=True,
  678. verbose_name=_('Show preview text'),
  679. )
  680. layout_panels = CoderedWebPage.layout_panels + [
  681. MultiFieldPanel(
  682. [
  683. FieldPanel('show_images'),
  684. FieldPanel('show_captions'),
  685. FieldPanel('show_meta'),
  686. FieldPanel('show_preview_text'),
  687. ],
  688. heading=_('Child page display')
  689. ),
  690. ]
  691. class CoderedEventPage(CoderedWebPage, BaseEvent):
  692. class Meta:
  693. verbose_name = _('CodeRed Event')
  694. abstract = True
  695. calendar_color = ColorField(
  696. blank=True,
  697. help_text=_('The color that the event will use when displayed on a calendar.'),
  698. )
  699. address = models.TextField(
  700. blank=True,
  701. verbose_name=_("Address")
  702. )
  703. content_panels = CoderedWebPage.content_panels + [
  704. MultiFieldPanel(
  705. [
  706. FieldPanel('calendar_color'),
  707. FieldPanel('address'),
  708. ],
  709. heading=_('Event information')
  710. ),
  711. InlinePanel(
  712. 'occurrences',
  713. min_num=1,
  714. heading=_("Dates and times"),
  715. ),
  716. ]
  717. @property
  718. def upcoming_occurrences(self):
  719. """
  720. Returns the next x occurrences for this event.
  721. By default, it returns 10.
  722. """
  723. return self.query_occurrences(num_of_instances_to_return=10)
  724. @property
  725. def most_recent_occurrence(self):
  726. """
  727. Gets the next upcoming, or last occurrence if the event has no more occurrences.
  728. """
  729. try:
  730. noc = self.next_occurrence()
  731. if noc:
  732. return noc
  733. aoc = []
  734. for occurrence in self.occurrences.all():
  735. aoc += [instance for instance in occurrence.all_occurrences()]
  736. if len(aoc) > 0:
  737. return aoc[-1] # last one in the list
  738. except AttributeError:
  739. # Triggers when a preview is initiated on an EventPage because it uses a FakeQuerySet object.
  740. # Here we manually compute the next_occurrence
  741. occurrences = [e.next_occurrence() for e in self.occurrences.all()]
  742. if occurrences:
  743. return sorted(occurrences, key=lambda tup: tup[0])[0]
  744. def query_occurrences(self, num_of_instances_to_return=None, **kwargs):
  745. """
  746. Returns a list of all upcoming event instances for the specified query.
  747. For more information on what you can query with, visit
  748. https://github.com/gregplaysguitar/django-eventtools
  749. """
  750. event_instances = []
  751. occurrence_kwargs = {
  752. 'from_date': kwargs.get('from_date', timezone.now().date())
  753. }
  754. if 'limit' in kwargs:
  755. if kwargs['limit'] != None:
  756. # Limit the number of event instances that will be generated per occurrence rule to 10, if not otherwise specified.
  757. occurrence_kwargs['limit'] = kwargs.get('limit', 10)
  758. # For each occurrence rule in all of the occurrence rules for this event.
  759. for occurrence in self.occurrences.all():
  760. # Add the qualifying generated event instances to the list.
  761. event_instances += [instance for instance in occurrence.all_occurrences(**occurrence_kwargs)]
  762. # Sort all the events by the date that they start
  763. event_instances.sort(key=lambda d: d[0])
  764. # Return the event instances, possibly spliced if num_instances_to_return is set.
  765. return event_instances[:num_of_instances_to_return] if num_of_instances_to_return else event_instances
  766. def convert_to_ical_format(self, dt_start=None, dt_end=None, occurrence=None):
  767. ical_event = ICalEvent()
  768. ical_event.add('summary', self.title)
  769. if self.address:
  770. ical_event.add('location', self.address)
  771. if dt_start:
  772. ical_event.add('dtstart', dt_start)
  773. if dt_end:
  774. ical_event.add('dtend', dt_end)
  775. if occurrence:
  776. freq = occurrence.repeat.split(":")[1] if occurrence.repeat else None
  777. repeat_until = occurrence.repeat_until.strftime("%Y%m%dT000000Z") if occurrence.repeat_until else None
  778. ical_event.add('dtstart', occurrence.start)
  779. if occurrence.end:
  780. ical_event.add('dtend', occurrence.end)
  781. if freq:
  782. ical_event.add('RRULE', freq, encode=False)
  783. if repeat_until:
  784. ical_event.add('until', repeat_until)
  785. return ical_event
  786. def create_single_ical(self, dt_start, dt_end=None):
  787. return self.convert_to_ical_format(dt_start=dt_start, dt_end=dt_end)
  788. def create_recurring_ical(self):
  789. events = []
  790. for occurrence in self.occurrences.all():
  791. events.append(self.convert_to_ical_format(occurrence=occurrence))
  792. return events
  793. class DefaultCalendarViewChoices():
  794. MONTH = 'month'
  795. AGENDA_WEEK = 'agendaWeek'
  796. AGENDA_DAY = 'agendaDay'
  797. LIST_MONTH = 'listMonth'
  798. CHOICES = (
  799. ('', _('No calendar')),
  800. (MONTH, _('Monthly Calendar')),
  801. (AGENDA_WEEK, _('Weekly Calendar')),
  802. (AGENDA_DAY, _('Daily Calendar')),
  803. (LIST_MONTH, _('Calendar List View')),
  804. )
  805. class CoderedEventIndexPage(CoderedWebPage):
  806. """
  807. Shows a list of event sub-pages.
  808. """
  809. class Meta:
  810. verbose_name = _('CodeRed Event Index Page')
  811. abstract = True
  812. template = 'coderedcms/pages/event_index_page.html'
  813. index_show_subpages_default = True
  814. index_order_by_default = 'next_occurrence'
  815. index_order_by_choices = (
  816. ('next_occurrence', 'Display next occurrence, soonest first'),
  817. ) + CoderedWebPage.index_order_by_choices
  818. default_calendar_view = models.CharField(
  819. blank=True,
  820. choices=DefaultCalendarViewChoices.CHOICES,
  821. max_length=255,
  822. verbose_name=_('Calendar Style'),
  823. help_text=_('The default look of the calendar on this page.')
  824. )
  825. layout_panels = CoderedWebPage.layout_panels + [
  826. FieldPanel('default_calendar_view'),
  827. ]
  828. def get_index_children(self):
  829. if self.index_query_pagemodel and self.index_order_by == 'next_occurrence':
  830. querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
  831. qs = querymodel.objects.child_of(self).live()
  832. # filter out events that don't have a next_occurrence
  833. upcoming = []
  834. for event in qs.all():
  835. if event.next_occurrence():
  836. upcoming.append(event)
  837. # sort the events by next_occurrence
  838. return sorted(upcoming, key=lambda e: e.next_occurrence())
  839. return super().get_index_children()
  840. def get_calendar_events(self, start, end):
  841. # start with all child events, regardless of get_index_children rules.
  842. querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
  843. qs = querymodel.objects.child_of(self).live()
  844. event_instances = []
  845. for event in qs:
  846. occurrences = event.query_occurrences(limit=None, from_date=start, to_date=end)
  847. for occurrence in occurrences:
  848. event_data = {
  849. 'title': event.title,
  850. 'start': occurrence[0].strftime('%Y-%m-%dT%H:%M:%S'),
  851. 'end' : occurrence[1].strftime('%Y-%m-%dT%H:%M:%S') if occurrence[1] else "",
  852. 'description': "",
  853. }
  854. if event.url:
  855. event_data['url'] = event.url
  856. if event.calendar_color:
  857. event_data['backgroundColor'] = event.calendar_color
  858. event_instances.append(event_data)
  859. return event_instances
  860. class CoderedEventOccurrence(Orderable, BaseOccurrence):
  861. class Meta:
  862. verbose_name = _('CodeRed Event Occurrence')
  863. abstract = True
  864. class CoderedFormPage(CoderedWebPage):
  865. """
  866. This is basically a clone of wagtail.contrib.forms.models.AbstractForm
  867. with changes in functionality and extending CoderedWebPage vs wagtailcore.Page.
  868. """
  869. class Meta:
  870. verbose_name = _('CodeRed Form Page')
  871. abstract = True
  872. template = 'coderedcms/pages/form_page.html'
  873. landing_page_template = 'coderedcms/pages/form_page_landing.html'
  874. base_form_class = WagtailAdminFormPageForm
  875. form_builder = CoderedFormBuilder
  876. submissions_list_view_class = CoderedSubmissionsListView
  877. ### Custom codered fields
  878. to_address = models.CharField(
  879. max_length=255,
  880. blank=True,
  881. verbose_name=_('Email form submissions to'),
  882. help_text=_('Optional - email form submissions to this address. Separate multiple addresses by comma.')
  883. )
  884. reply_address = models.CharField(
  885. max_length=255,
  886. blank=True,
  887. verbose_name=_('Reply-to address'),
  888. help_text=_('Optional - to reply to the submitter, specify the email field here. For example, if a form field above is labeled "Your Email", enter: {{ your_email }}')
  889. )
  890. subject = models.CharField(
  891. max_length=255,
  892. blank=True,
  893. verbose_name=_('Subject'),
  894. )
  895. save_to_database = models.BooleanField(
  896. default=True,
  897. verbose_name=_('Save form submissions'),
  898. help_text=_('Submissions are saved to database and can be exported at any time.')
  899. )
  900. thank_you_page = models.ForeignKey(
  901. 'wagtailcore.Page',
  902. null=True,
  903. blank=True,
  904. on_delete=models.SET_NULL,
  905. related_name='+',
  906. verbose_name=_('Thank you page'),
  907. help_text=_('The page users are redirected to after submitting the form.'),
  908. )
  909. button_text = models.CharField(
  910. max_length=255,
  911. default=_('Submit'),
  912. verbose_name=_('Button text'),
  913. )
  914. button_style = models.CharField(
  915. blank=True,
  916. choices=cr_settings['FRONTEND_BTN_STYLE_CHOICES'],
  917. default=cr_settings["FRONTEND_BTN_STYLE_DEFAULT"],
  918. max_length=255,
  919. verbose_name=_('Button style'),
  920. )
  921. button_size = models.CharField(
  922. blank=True,
  923. choices=cr_settings['FRONTEND_BTN_SIZE_CHOICES'],
  924. default=cr_settings["FRONTEND_BTN_SIZE_DEFAULT"],
  925. max_length=255,
  926. verbose_name=_('Button Size'),
  927. )
  928. button_css_class = models.CharField(
  929. max_length=255,
  930. blank=True,
  931. verbose_name=_('Button CSS class'),
  932. help_text=_('Custom CSS class applied to the submit button.'),
  933. )
  934. form_css_class = models.CharField(
  935. max_length=255,
  936. blank=True,
  937. verbose_name=_('Form CSS Class'),
  938. help_text=_('Custom CSS class applied to <form> element.'),
  939. )
  940. form_id = models.CharField(
  941. max_length=255,
  942. blank=True,
  943. verbose_name=_('Form ID'),
  944. help_text=_('Custom ID applied to <form> element.'),
  945. )
  946. form_golive_at = models.DateTimeField(
  947. blank=True,
  948. null=True,
  949. verbose_name=_('Form go live date/time'),
  950. help_text=_('Date and time when the FORM goes live on the page.'),
  951. )
  952. form_expire_at = models.DateTimeField(
  953. blank=True,
  954. null=True,
  955. verbose_name=_('Form expiry date/time'),
  956. help_text=_('Date and time when the FORM will no longer be available on the page.'),
  957. )
  958. body_content_panels = CoderedWebPage.body_content_panels + [
  959. FormSubmissionsPanel(),
  960. InlinePanel('form_fields', label="Form fields"),
  961. MultiFieldPanel(
  962. [
  963. PageChooserPanel('thank_you_page'),
  964. FieldPanel('button_text'),
  965. FieldPanel('button_style'),
  966. FieldPanel('button_size'),
  967. FieldPanel('button_css_class'),
  968. FieldPanel('form_css_class'),
  969. FieldPanel('form_id'),
  970. ],
  971. _('Form Settings')
  972. ),
  973. MultiFieldPanel(
  974. [
  975. FieldPanel('save_to_database'),
  976. FieldPanel('to_address'),
  977. FieldPanel('reply_address'),
  978. FieldPanel('subject'),
  979. ],
  980. _('Form Submissions')
  981. ),
  982. InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
  983. ]
  984. settings_panels = CoderedPage.settings_panels + [
  985. MultiFieldPanel(
  986. [
  987. FieldRowPanel(
  988. [
  989. FieldPanel('form_golive_at'),
  990. FieldPanel('form_expire_at'),
  991. ],
  992. classname='label-above',
  993. ),
  994. ],
  995. _('Form Scheduled Publishing'),
  996. )
  997. ]
  998. @property
  999. def form_live(self):
  1000. """
  1001. A boolean on whether or not the <form> element should be shown on the page.
  1002. """
  1003. return (self.form_golive_at is None or self.form_golive_at <= timezone.now()) and \
  1004. (self.form_expire_at is None or self.form_expire_at >= timezone.now())
  1005. def __init__(self, *args, **kwargs):
  1006. super().__init__(*args, **kwargs)
  1007. if not hasattr(self, 'landing_page_template'):
  1008. name, ext = os.path.splitext(self.template)
  1009. self.landing_page_template = name + '_landing' + ext
  1010. def get_form_fields(self):
  1011. """
  1012. Form page expects `form_fields` to be declared.
  1013. If you want to change backwards relation name,
  1014. you need to override this method.
  1015. """
  1016. return self.form_fields.all()
  1017. def get_data_fields(self):
  1018. """
  1019. Returns a list of tuples with (field_name, field_label).
  1020. """
  1021. data_fields = [
  1022. ('submit_time', _('Submission date')),
  1023. ]
  1024. data_fields += [
  1025. (field.clean_name, field.label)
  1026. for field in self.get_form_fields()
  1027. ]
  1028. return data_fields
  1029. def get_form_class(self):
  1030. fb = self.form_builder(self.get_form_fields())
  1031. return fb.get_form_class()
  1032. def get_form_parameters(self):
  1033. return {}
  1034. def get_form(self, *args, **kwargs):
  1035. form_class = self.get_form_class()
  1036. form_params = self.get_form_parameters()
  1037. form_params.update(kwargs)
  1038. return form_class(*args, **form_params)
  1039. def get_landing_page_template(self, request, *args, **kwargs):
  1040. return self.landing_page_template
  1041. def get_submission_class(self):
  1042. """
  1043. Returns submission class.
  1044. You can override this method to provide custom submission class.
  1045. Your class must be inherited from AbstractFormSubmission.
  1046. """
  1047. return FormSubmission
  1048. def process_form_submission(self, request, form):
  1049. """
  1050. Accepts form instance with submitted data, user and page.
  1051. Creates submission instance.
  1052. You can override this method if you want to have custom creation logic.
  1053. For example, if you want to save reference to a user.
  1054. """
  1055. processed_data = {}
  1056. # Handle file uploads
  1057. for key, val in form.cleaned_data.items():
  1058. if type(val) == InMemoryUploadedFile or type(val) == TemporaryUploadedFile:
  1059. # Save the file and get its URL
  1060. file_system = FileSystemStorage(
  1061. location=cr_settings['PROTECTED_MEDIA_ROOT'],
  1062. base_url=cr_settings['PROTECTED_MEDIA_URL']
  1063. )
  1064. filename = file_system.save(file_system.get_valid_name(val.name), val)
  1065. processed_data[key] = file_system.url(filename)
  1066. else:
  1067. processed_data[key] = val
  1068. # Get submission
  1069. form_submission = self.get_submission_class()(
  1070. form_data=json.dumps(processed_data, cls=DjangoJSONEncoder),
  1071. page=self,
  1072. )
  1073. # Save to database
  1074. if self.save_to_database:
  1075. form_submission.save()
  1076. # Send the mails
  1077. if self.to_address:
  1078. self.send_summary_mail(request, form, processed_data)
  1079. if self.confirmation_emails:
  1080. # Convert form data into a context.
  1081. context = Context(self.data_to_dict(processed_data))
  1082. # Render emails as if they are django templates.
  1083. for email in self.confirmation_emails.all():
  1084. # Build email message parameters.
  1085. message_args = {}
  1086. # From
  1087. if email.from_address:
  1088. template_from_email = Template(email.from_address)
  1089. message_args['from_email'] = template_from_email.render(context)
  1090. else:
  1091. genemail = GeneralSettings.for_site(request.site).from_email_address
  1092. if genemail:
  1093. message_args['from_email'] = genemail
  1094. # Reply-to
  1095. if email.reply_address:
  1096. template_reply_to = Template(email.reply_address)
  1097. message_args['reply_to'] = template_reply_to.render(context).split(',')
  1098. # CC
  1099. if email.cc_address:
  1100. template_cc = Template(email.cc_address)
  1101. message_args['cc'] = template_cc.render(context).split(',')
  1102. # BCC
  1103. if email.bcc_address:
  1104. template_bcc = Template(email.bcc_address)
  1105. message_args['bcc'] = template_bcc.render(context).split(',')
  1106. # Subject
  1107. if email.subject:
  1108. template_subject = Template(email.subject)
  1109. message_args['subject'] = template_subject.render(context)
  1110. else:
  1111. message_args['subject'] = self.title
  1112. # Body
  1113. template_body = Template(email.body)
  1114. message_args['body'] = template_body.render(context)
  1115. # To
  1116. template_to = Template(email.to_address)
  1117. message_args['to'] = template_to.render(context).split(',')
  1118. # Send email
  1119. message = EmailMessage(**message_args)
  1120. message.content_subtype = 'html'
  1121. message.send()
  1122. for fn in hooks.get_hooks('form_page_submit'):
  1123. fn(instance=self, form_submission=form_submission)
  1124. return form_submission
  1125. def send_summary_mail(self, request, form, processed_data):
  1126. """
  1127. Sends a form submission summary email.
  1128. """
  1129. addresses = [x.strip() for x in self.to_address.split(',')]
  1130. content = []
  1131. for field in form:
  1132. value = processed_data[field.name]
  1133. # Convert lists into human readable comma separated strings.
  1134. if isinstance(value, list):
  1135. value = ', '.join(value)
  1136. content.append('{0}: {1}'.format(
  1137. field.label,
  1138. utils.attempt_protected_media_value_conversion(request, value)
  1139. ))
  1140. content = '\n\n'.join(content)
  1141. # Build email message parameters
  1142. message_args = {
  1143. 'body': content,
  1144. 'to': addresses,
  1145. }
  1146. if self.subject:
  1147. message_args['subject'] = self.subject
  1148. else:
  1149. message_args['subject'] = self.title
  1150. genemail = GeneralSettings.for_site(request.site).from_email_address
  1151. if genemail:
  1152. message_args['from_email'] = genemail
  1153. if self.reply_address:
  1154. # Render reply-to field using form submission as context.
  1155. context = Context(self.data_to_dict(processed_data))
  1156. template_reply_to = Template(self.reply_address)
  1157. message_args['reply_to'] = template_reply_to.render(context).split(',')
  1158. # Send email
  1159. message = EmailMessage(**message_args)
  1160. message.send()
  1161. def data_to_dict(self, processed_data):
  1162. """
  1163. Converts processed form data into a dictionary suitable
  1164. for rendering in a context.
  1165. """
  1166. dictionary = {}
  1167. for key, value in processed_data.items():
  1168. dictionary[key.replace('-', '_')] = value
  1169. if isinstance(value, list):
  1170. dictionary[key] = ', '.join(value)
  1171. return dictionary
  1172. def render_landing_page(self, request, form_submission=None, *args, **kwargs):
  1173. """
  1174. Renders the landing page.
  1175. You can override this method to return a different HttpResponse as
  1176. landing page. E.g. you could return a redirect to a separate page.
  1177. """
  1178. if self.thank_you_page:
  1179. return redirect(self.thank_you_page.url)
  1180. context = self.get_context(request)
  1181. context['form_submission'] = form_submission
  1182. response = render(
  1183. request,
  1184. self.get_landing_page_template(request),
  1185. context
  1186. )
  1187. return response
  1188. def serve_submissions_list_view(self, request, *args, **kwargs):
  1189. """
  1190. Returns list submissions view for admin.
  1191. `list_submissions_view_class` can bse set to provide custom view class.
  1192. Your class must be inherited from SubmissionsListView.
  1193. """
  1194. view = self.submissions_list_view_class.as_view()
  1195. return view(request, form_page=self, *args, **kwargs)
  1196. def serve(self, request, *args, **kwargs):
  1197. if request.method == 'POST':
  1198. form = self.get_form(request.POST, request.FILES, page=self, user=request.user)
  1199. if form.is_valid():
  1200. form_submission = self.process_form_submission(request, form)
  1201. return self.render_landing_page(request, form_submission, *args, **kwargs)
  1202. else:
  1203. form = self.get_form(page=self, user=request.user)
  1204. context = self.get_context(request)
  1205. context['form'] = form
  1206. response = render(
  1207. request,
  1208. self.get_template(request),
  1209. context
  1210. )
  1211. return response
  1212. preview_modes = [
  1213. ('form', _('Form')),
  1214. ('landing', _('Thank you page')),
  1215. ]
  1216. def serve_preview(self, request, mode):
  1217. if mode == 'landing':
  1218. request.is_preview = True
  1219. return self.render_landing_page(request)
  1220. return super().serve_preview(request, mode)
  1221. class CoderedLocationPage(CoderedWebPage):
  1222. """
  1223. Location, suitable for store locations or help centers.
  1224. """
  1225. class Meta:
  1226. verbose_name = _('CodeRed Location')
  1227. abstract = True
  1228. template = 'coderedcms/pages/location_page.html'
  1229. # Override body to provide simpler content
  1230. body = StreamField(CONTENT_STREAMBLOCKS, null=True, blank=True)
  1231. address = models.TextField(
  1232. blank=True,
  1233. verbose_name=_("Address")
  1234. )
  1235. latitude = models.FloatField(
  1236. blank=True,
  1237. null=True,
  1238. verbose_name=_("Latitude")
  1239. )
  1240. longitude = models.FloatField(
  1241. blank=True,
  1242. null=True,
  1243. verbose_name=_("Longitude")
  1244. )
  1245. auto_update_latlng = models.BooleanField(
  1246. default=True,
  1247. verbose_name=_("Auto Update Latitude and Longitude"),
  1248. help_text=_("If checked, automatically update the latitude and longitude when the address is updated.")
  1249. )
  1250. map_title = models.CharField(
  1251. blank=True,
  1252. max_length=255,
  1253. verbose_name=_("Map Title"),
  1254. help_text=_("If this is filled out, this is the title that will be used on the map.")
  1255. )
  1256. map_description = models.CharField(
  1257. blank=True,
  1258. max_length=255,
  1259. verbose_name=_("Map Description"),
  1260. help_text=_("If this is filled out, this is the description that will be used on the map.")
  1261. )
  1262. website = models.TextField(
  1263. blank=True,
  1264. verbose_name=_("Website")
  1265. )
  1266. phone_number = models.CharField(
  1267. blank=True,
  1268. max_length=255,
  1269. verbose_name=_("Phone Number")
  1270. )
  1271. content_panels = CoderedWebPage.content_panels + [
  1272. FieldPanel('address'),
  1273. FieldPanel('website'),
  1274. FieldPanel('phone_number'),
  1275. ]
  1276. layout_panels = CoderedWebPage.layout_panels + [
  1277. MultiFieldPanel(
  1278. [
  1279. FieldPanel('map_title'),
  1280. FieldPanel('map_description'),
  1281. ],
  1282. heading=_('Map Layout')
  1283. ),
  1284. ]
  1285. settings_panels = CoderedWebPage.settings_panels + [
  1286. MultiFieldPanel(
  1287. [
  1288. FieldPanel('auto_update_latlng'),
  1289. FieldPanel('latitude'),
  1290. FieldPanel('longitude'),
  1291. ],
  1292. heading=_("Location Settings")
  1293. ),
  1294. ]
  1295. @property
  1296. def geojson_name(self):
  1297. return self.map_title or self.title
  1298. @property
  1299. def geojson_description(self):
  1300. return self.map_description
  1301. @property
  1302. def render_pin_description(self):
  1303. return render_to_string(
  1304. 'coderedcms/includes/map_pin_description.html',
  1305. {
  1306. 'page': self
  1307. }
  1308. )
  1309. @property
  1310. def render_list_description(self):
  1311. return render_to_string(
  1312. 'coderedcms/includes/map_list_description.html',
  1313. {
  1314. 'page': self
  1315. }
  1316. )
  1317. def to_geojson(self):
  1318. return {
  1319. "type": "Feature",
  1320. "geometry":{
  1321. "type": "Point",
  1322. "coordinates": [self.longitude, self.latitude]
  1323. },
  1324. "properties":{
  1325. "list_description": self.render_list_description,
  1326. "pin_description": self.render_pin_description
  1327. }
  1328. }
  1329. def save(self, *args, **kwargs):
  1330. if self.auto_update_latlng and GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key:
  1331. try:
  1332. g = geocoder.google(self.address, key=GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key)
  1333. self.latitude = g.latlng[0]
  1334. self.longitude = g.latlng[1]
  1335. except TypeError:
  1336. # Raised if google denied the request
  1337. pass
  1338. return super(CoderedLocationPage, self).save(*args, **kwargs)
  1339. def get_context(self, request, *args, **kwargs):
  1340. context = super().get_context(request)
  1341. context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
  1342. return context
  1343. class CoderedLocationIndexPage(CoderedWebPage):
  1344. """
  1345. Shows a map view of the children CoderedLocationPage.
  1346. """
  1347. class Meta:
  1348. verbose_name = _('CodeRed Location Index Page')
  1349. abstract = True
  1350. template = 'coderedcms/pages/location_index_page.html'
  1351. index_show_subpages_default = True
  1352. center_latitude = models.FloatField(
  1353. null=True,
  1354. blank=True,
  1355. help_text=_('The default latitude you want the map set to.'),
  1356. default=0
  1357. )
  1358. center_longitude = models.FloatField(
  1359. null=True,
  1360. blank=True,
  1361. help_text=_('The default longitude you want the map set to.'),
  1362. default=0
  1363. )
  1364. zoom = models.IntegerField(
  1365. default=8,
  1366. validators=[
  1367. MaxValueValidator(20),
  1368. MinValueValidator(1),
  1369. ],
  1370. help_text=_('Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings')
  1371. )
  1372. layout_panels = CoderedWebPage.layout_panels + [
  1373. MultiFieldPanel(
  1374. [
  1375. FieldPanel('center_latitude'),
  1376. FieldPanel('center_longitude'),
  1377. FieldPanel('zoom'),
  1378. ],
  1379. heading=_('Map Display')
  1380. ),
  1381. ]
  1382. def geojson_data(self, viewport=None):
  1383. """
  1384. function that will return all locations under this index as geoJSON compliant data.
  1385. It is filtered by a latitude/longitude viewport if given.
  1386. viewport is a string in the format of :
  1387. 'southwest.latitude,southwest.longitude|northeast.latitude,northeast.longitude'
  1388. An example viewport that covers Cleveland, OH would look like this:
  1389. '41.354912150983964,-81.95331736661791|41.663427748126935,-81.45206614591478'
  1390. """
  1391. qs = self.get_index_children().live()
  1392. if viewport:
  1393. southwest, northeast = viewport.split('|')
  1394. southwest = [float(x) for x in southwest.split(',')]
  1395. northeast = [float(x) for x in northeast.split(',')]
  1396. qs = qs.filter(latitude__gte=southwest[0], latitude__lte=northeast[0], longitude__gte=southwest[1], longitude__lte=northeast[1])
  1397. return {
  1398. "type": "FeatureCollection",
  1399. "features": [
  1400. location.to_geojson() for location in qs
  1401. ]
  1402. }
  1403. def serve(self, request, *args, **kwargs):
  1404. data_format = request.GET.get('data-format', None)
  1405. if data_format == 'geojson':
  1406. return self.serve_geojson(request, *args, **kwargs)
  1407. return super().serve(request, *args, **kwargs)
  1408. def serve_geojson(self, request, *args, **kwargs):
  1409. viewport = request.GET.get('viewport', None)
  1410. return JsonResponse(self.geojson_data(viewport=viewport))
  1411. def get_context(self, request, *args, **kwargs):
  1412. context = super().get_context(request)
  1413. context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
  1414. return context