snippet_models.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. """
  2. Snippets are for content that is re-usable in nature.
  3. """
  4. from django.db import models
  5. from django.utils.text import slugify
  6. from django.utils.translation import gettext_lazy as _
  7. from modelcluster.fields import ParentalKey
  8. from modelcluster.models import ClusterableModel
  9. from wagtail.admin.panels import (
  10. FieldPanel,
  11. InlinePanel,
  12. MultiFieldPanel,
  13. )
  14. from wagtail.models import Orderable
  15. from wagtail.snippets.models import register_snippet
  16. from wagtail.images import get_image_model_string
  17. from wagtailcrx.blocks import HTML_STREAMBLOCKS, LAYOUT_STREAMBLOCKS, NAVIGATION_STREAMBLOCKS
  18. from wagtailcrx.fields import CoderedStreamField
  19. from wagtailcrx.settings import crx_settings
  20. @register_snippet
  21. class Carousel(ClusterableModel):
  22. """
  23. Model that represents a Carousel. Can be modified through the snippets UI.
  24. Selected through Page StreamField bodies by the CarouselSnippetChooser in
  25. snippet_choosers.py
  26. """
  27. class Meta:
  28. verbose_name = _('Carousel')
  29. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  30. db_table = "coderedcms_carousel"
  31. name = models.CharField(
  32. max_length=255,
  33. verbose_name=_('Name'),
  34. )
  35. show_controls = models.BooleanField(
  36. default=True,
  37. verbose_name=_('Show controls'),
  38. help_text=_('Shows arrows on the left and right of the carousel to advance next or previous slides.'), # noqa
  39. )
  40. show_indicators = models.BooleanField(
  41. default=True,
  42. verbose_name=_('Show indicators'),
  43. help_text=_('Shows small indicators at the bottom of the carousel based on the number of slides.'), # noqa
  44. )
  45. animation = models.CharField(
  46. blank=True,
  47. max_length=20,
  48. choices=None,
  49. default='',
  50. verbose_name=_('Animation'),
  51. help_text=_('The animation when transitioning between slides.'),
  52. )
  53. panels = (
  54. [
  55. MultiFieldPanel(
  56. heading=_('Slider'),
  57. children=[
  58. FieldPanel('name'),
  59. FieldPanel('show_controls'),
  60. FieldPanel('show_indicators'),
  61. FieldPanel('animation'),
  62. ]
  63. ),
  64. InlinePanel('carousel_slides', label=_('Slides'))
  65. ]
  66. )
  67. def __str__(self):
  68. return self.name
  69. def __init__(self, *args, **kwargs):
  70. """
  71. Inject custom choices and defaults into the form fields
  72. to enable customization of settings without causing migration issues.
  73. """
  74. super().__init__(*args, **kwargs)
  75. # Set choices dynamically.
  76. self._meta.get_field('animation').choices = (
  77. crx_settings.CRX_FRONTEND_CAROUSEL_FX_CHOICES
  78. )
  79. # Set default dynamically.
  80. if not self.id:
  81. self.animation = crx_settings.CRX_FRONTEND_CAROUSEL_FX_DEFAULT
  82. class CarouselSlide(Orderable, models.Model):
  83. """
  84. Represents a slide for the Carousel model. Can be modified through the
  85. snippets UI.
  86. """
  87. class Meta(Orderable.Meta):
  88. verbose_name = _('Carousel Slide')
  89. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  90. db_table = "coderedcms_carouselslide"
  91. carousel = ParentalKey(
  92. Carousel,
  93. related_name='carousel_slides',
  94. verbose_name=_('Carousel'),
  95. )
  96. image = models.ForeignKey(
  97. get_image_model_string(),
  98. null=True,
  99. blank=True,
  100. on_delete=models.SET_NULL,
  101. related_name='+',
  102. verbose_name=_('Image'),
  103. )
  104. background_color = models.CharField(
  105. max_length=255,
  106. blank=True,
  107. verbose_name=_('Background color'),
  108. help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
  109. )
  110. custom_css_class = models.CharField(
  111. max_length=255,
  112. blank=True,
  113. verbose_name=_('Custom CSS class'),
  114. )
  115. custom_id = models.CharField(
  116. max_length=255,
  117. blank=True,
  118. verbose_name=_('Custom ID'),
  119. )
  120. content = CoderedStreamField(
  121. HTML_STREAMBLOCKS,
  122. blank=True,
  123. use_json_field=True,
  124. )
  125. panels = (
  126. [
  127. FieldPanel('image'),
  128. FieldPanel('background_color'),
  129. FieldPanel('custom_css_class'),
  130. FieldPanel('custom_id'),
  131. FieldPanel('content'),
  132. ]
  133. )
  134. @register_snippet
  135. class Classifier(ClusterableModel):
  136. """
  137. Simple and generic model to organize/categorize/group pages.
  138. """
  139. class Meta:
  140. verbose_name = _('Classifier')
  141. verbose_name_plural = _('Classifiers')
  142. ordering = ['name']
  143. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  144. db_table = "coderedcms_classifier"
  145. slug = models.SlugField(
  146. allow_unicode=True,
  147. unique=True,
  148. verbose_name=_('Slug'),
  149. )
  150. name = models.CharField(
  151. max_length=255,
  152. verbose_name=_('Name'),
  153. )
  154. panels = [
  155. FieldPanel('name'),
  156. InlinePanel('terms', label=_('Classifier Terms'))
  157. ]
  158. def save(self, *args, **kwargs):
  159. if not self.slug:
  160. # Make a slug and suffix a number if it already exists to ensure uniqueness
  161. newslug = slugify(self.name, allow_unicode=True)
  162. tmpslug = newslug
  163. suffix = 1
  164. while True:
  165. if not Classifier.objects.filter(slug=tmpslug).exists():
  166. self.slug = tmpslug
  167. break
  168. tmpslug = newslug + "-" + str(suffix)
  169. suffix += 1
  170. return super().save(*args, **kwargs)
  171. def __str__(self):
  172. return self.name
  173. class ClassifierTerm(Orderable, models.Model):
  174. """
  175. Term used to categorize a page.
  176. """
  177. class Meta(Orderable.Meta):
  178. verbose_name = _('Classifier Term')
  179. verbose_name_plural = _('Classifier Terms')
  180. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  181. db_table = "coderedcms_classifierterm"
  182. classifier = ParentalKey(
  183. Classifier,
  184. related_name='terms',
  185. verbose_name=_('Classifier'),
  186. )
  187. slug = models.SlugField(
  188. allow_unicode=True,
  189. unique=True,
  190. verbose_name=_('Slug'),
  191. )
  192. name = models.CharField(
  193. max_length=255,
  194. verbose_name=_('Name'),
  195. )
  196. panels = [
  197. FieldPanel('name'),
  198. ]
  199. def save(self, *args, **kwargs):
  200. if not self.slug:
  201. # Make a slug and suffix a number if it already exists to ensure uniqueness
  202. newslug = slugify(self.name, allow_unicode=True)
  203. tmpslug = newslug
  204. suffix = 1
  205. while True:
  206. if not ClassifierTerm.objects.filter(slug=tmpslug).exists():
  207. self.slug = tmpslug
  208. break
  209. tmpslug = newslug + "-" + str(suffix)
  210. suffix += 1
  211. return super().save(*args, **kwargs)
  212. def __str__(self):
  213. return "{0} > {1}".format(self.classifier.name, self.name)
  214. @register_snippet
  215. class Navbar(models.Model):
  216. """
  217. Snippet for site navigation bars (header, main menu, etc.)
  218. """
  219. class Meta:
  220. verbose_name = _('Navigation Bar')
  221. name = models.CharField(
  222. max_length=255,
  223. verbose_name=_('Name'),
  224. )
  225. custom_css_class = models.CharField(
  226. max_length=255,
  227. blank=True,
  228. verbose_name=_('Custom CSS Class'),
  229. )
  230. custom_id = models.CharField(
  231. max_length=255,
  232. blank=True,
  233. verbose_name=_('Custom ID'),
  234. )
  235. menu_items = CoderedStreamField(
  236. NAVIGATION_STREAMBLOCKS,
  237. verbose_name=_('Navigation links'),
  238. blank=True,
  239. use_json_field=True,
  240. )
  241. panels = [
  242. FieldPanel('name'),
  243. MultiFieldPanel(
  244. [
  245. FieldPanel('custom_css_class'),
  246. FieldPanel('custom_id'),
  247. ],
  248. heading=_('Attributes')
  249. ),
  250. FieldPanel('menu_items')
  251. ]
  252. def __str__(self):
  253. return self.name
  254. @register_snippet
  255. class Footer(models.Model):
  256. """
  257. Snippet for website footer content.
  258. """
  259. class Meta:
  260. verbose_name = _('Footer')
  261. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  262. db_table = "coderedcms_footer"
  263. name = models.CharField(
  264. max_length=255,
  265. verbose_name=_('Name'),
  266. )
  267. custom_css_class = models.CharField(
  268. max_length=255,
  269. blank=True,
  270. verbose_name=_('Custom CSS Class'),
  271. )
  272. custom_id = models.CharField(
  273. max_length=255,
  274. blank=True,
  275. verbose_name=_('Custom ID'),
  276. )
  277. content = CoderedStreamField(
  278. LAYOUT_STREAMBLOCKS,
  279. verbose_name=_('Content'),
  280. blank=True,
  281. use_json_field=True,
  282. )
  283. panels = [
  284. FieldPanel('name'),
  285. MultiFieldPanel(
  286. [
  287. FieldPanel('custom_css_class'),
  288. FieldPanel('custom_id'),
  289. ],
  290. heading=_('Attributes')
  291. ),
  292. FieldPanel('content')
  293. ]
  294. def __str__(self):
  295. return self.name
  296. @register_snippet
  297. class ReusableContent(models.Model):
  298. """
  299. Snippet for resusable content in streamfields.
  300. """
  301. class Meta:
  302. verbose_name = _('Reusable Content')
  303. verbose_name_plural = _('Reusable Content')
  304. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  305. db_table = "coderedcms_reusablecontent"
  306. name = models.CharField(
  307. max_length=255,
  308. verbose_name=_('Name'),
  309. )
  310. content = CoderedStreamField(
  311. LAYOUT_STREAMBLOCKS,
  312. verbose_name=_('content'),
  313. blank=True,
  314. use_json_field=True,
  315. )
  316. panels = [
  317. FieldPanel('name'),
  318. FieldPanel('content')
  319. ]
  320. def __str__(self):
  321. return self.name
  322. @register_snippet
  323. class ContentWall(models.Model):
  324. """
  325. Snippet that restricts access to a page with a modal.
  326. """
  327. class Meta:
  328. verbose_name = _('Content Wall')
  329. if crx_settings.CRX_DB_TABLE_CODEREDCMS:
  330. db_table = "coderedcms_contentwall"
  331. name = models.CharField(
  332. max_length=255,
  333. verbose_name=_('Name'),
  334. )
  335. content = CoderedStreamField(
  336. LAYOUT_STREAMBLOCKS,
  337. verbose_name=_('Content'),
  338. blank=True,
  339. use_json_field=True,
  340. )
  341. is_dismissible = models.BooleanField(
  342. default=True,
  343. verbose_name=_('Dismissible'),
  344. )
  345. show_once = models.BooleanField(
  346. default=True,
  347. verbose_name=_('Show once'),
  348. help_text=_('Do not show the content wall to the same user again after it has been closed.')
  349. )
  350. panels = [
  351. MultiFieldPanel(
  352. [
  353. FieldPanel('name'),
  354. FieldPanel('is_dismissible'),
  355. FieldPanel('show_once'),
  356. ],
  357. heading=_('Content Wall')
  358. ),
  359. FieldPanel('content'),
  360. ]
  361. def __str__(self):
  362. return self.name
  363. class CoderedEmail(ClusterableModel):
  364. """
  365. General purpose abstract clusterable model used for holding email information.
  366. Most likely this should be subclassed with addition of a ParentalKey.
  367. """
  368. class Meta:
  369. abstract = True
  370. verbose_name = _('CodeRed Email')
  371. to_address = models.CharField(
  372. max_length=255,
  373. blank=True,
  374. verbose_name=_('To Addresses'),
  375. help_text=_('Separate multiple email addresses with commas.')
  376. )
  377. from_address = models.CharField(
  378. max_length=255,
  379. blank=True,
  380. verbose_name=_('From Address'),
  381. help_text=_('For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes).') # noqa
  382. )
  383. reply_address = models.CharField(
  384. max_length=255,
  385. blank=True,
  386. verbose_name=_('Reply-To Address'),
  387. help_text=_('Separate multiple email addresses with commas.')
  388. )
  389. cc_address = models.CharField(
  390. max_length=255,
  391. blank=True,
  392. verbose_name=_('CC'),
  393. help_text=_('Separate multiple email addresses with commas.')
  394. )
  395. bcc_address = models.CharField(
  396. max_length=255,
  397. blank=True,
  398. verbose_name=_('BCC'),
  399. help_text=_('Separate multiple email addresses with commas.')
  400. )
  401. subject = models.CharField(max_length=255, blank=True, verbose_name=_('Subject'))
  402. body = models.TextField(blank=True, verbose_name=_('Body'))
  403. panels = (
  404. [
  405. MultiFieldPanel(
  406. [
  407. FieldPanel('to_address'),
  408. FieldPanel('from_address'),
  409. FieldPanel('cc_address'),
  410. FieldPanel('bcc_address'),
  411. FieldPanel('subject'),
  412. FieldPanel('body'),
  413. ],
  414. _('Email Message')
  415. ),
  416. ])
  417. def __str__(self):
  418. return self.subject