snippet_models.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  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 coderedcms.blocks import (
  18. HTML_STREAMBLOCKS,
  19. LAYOUT_STREAMBLOCKS,
  20. NAVIGATION_STREAMBLOCKS,
  21. )
  22. from coderedcms.fields import CoderedStreamField
  23. @register_snippet
  24. class Carousel(ClusterableModel):
  25. """
  26. Model that represents a Carousel. Can be modified through the snippets UI.
  27. Selected through Page StreamField bodies by the CarouselSnippetChooser in
  28. snippet_choosers.py
  29. """
  30. class Meta:
  31. verbose_name = _("Carousel")
  32. name = models.CharField(
  33. max_length=255,
  34. verbose_name=_("Name"),
  35. )
  36. show_controls = models.BooleanField(
  37. default=True,
  38. verbose_name=_("Show controls"),
  39. help_text=_(
  40. "Shows arrows on the left and right of the carousel to advance "
  41. "next or previous slides."
  42. ),
  43. )
  44. show_indicators = models.BooleanField(
  45. default=True,
  46. verbose_name=_("Show indicators"),
  47. help_text=_(
  48. "Shows small indicators at the bottom of the carousel based on the "
  49. "number of slides."
  50. ),
  51. )
  52. panels = [
  53. MultiFieldPanel(
  54. heading=_("Slider"),
  55. children=[
  56. FieldPanel("name"),
  57. FieldPanel("show_controls"),
  58. FieldPanel("show_indicators"),
  59. ],
  60. ),
  61. InlinePanel("carousel_slides", label=_("Slides")),
  62. ]
  63. def __str__(self):
  64. return self.name
  65. class CarouselSlide(Orderable, models.Model):
  66. """
  67. Represents a slide for the Carousel model. Can be modified through the
  68. snippets UI.
  69. """
  70. class Meta(Orderable.Meta):
  71. verbose_name = _("Carousel Slide")
  72. carousel = ParentalKey(
  73. Carousel,
  74. related_name="carousel_slides",
  75. verbose_name=_("Carousel"),
  76. )
  77. image = models.ForeignKey(
  78. get_image_model_string(),
  79. null=True,
  80. blank=True,
  81. on_delete=models.SET_NULL,
  82. related_name="+",
  83. verbose_name=_("Image"),
  84. )
  85. background_color = models.CharField(
  86. max_length=255,
  87. blank=True,
  88. verbose_name=_("Background color"),
  89. help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
  90. )
  91. custom_css_class = models.CharField(
  92. max_length=255,
  93. blank=True,
  94. verbose_name=_("Custom CSS class"),
  95. )
  96. custom_id = models.CharField(
  97. max_length=255,
  98. blank=True,
  99. verbose_name=_("Custom ID"),
  100. )
  101. content = CoderedStreamField(
  102. HTML_STREAMBLOCKS,
  103. blank=True,
  104. use_json_field=True,
  105. )
  106. panels = [
  107. FieldPanel("image"),
  108. FieldPanel("background_color"),
  109. FieldPanel("custom_css_class"),
  110. FieldPanel("custom_id"),
  111. FieldPanel("content"),
  112. ]
  113. @register_snippet
  114. class Classifier(ClusterableModel):
  115. """
  116. Simple and generic model to organize/categorize/group pages.
  117. """
  118. class Meta:
  119. verbose_name = _("Classifier")
  120. verbose_name_plural = _("Classifiers")
  121. ordering = ["name"]
  122. slug = models.SlugField(
  123. allow_unicode=True,
  124. unique=True,
  125. verbose_name=_("Slug"),
  126. max_length=255,
  127. )
  128. name = models.CharField(
  129. max_length=255,
  130. verbose_name=_("Name"),
  131. )
  132. panels = [
  133. FieldPanel("name"),
  134. InlinePanel("terms", label=_("Classifier Terms")),
  135. ]
  136. def save(self, *args, **kwargs):
  137. if not self.slug:
  138. # Make a slug and suffix a number if it already exists to ensure uniqueness
  139. newslug = slugify(self.name, allow_unicode=True)
  140. tmpslug = newslug
  141. suffix = 1
  142. while True:
  143. if not Classifier.objects.filter(slug=tmpslug).exists():
  144. self.slug = tmpslug
  145. break
  146. tmpslug = newslug + "-" + str(suffix)
  147. suffix += 1
  148. return super().save(*args, **kwargs)
  149. def __str__(self):
  150. return self.name
  151. class ClassifierTerm(Orderable, models.Model):
  152. """
  153. Term used to categorize a page.
  154. """
  155. class Meta(Orderable.Meta):
  156. verbose_name = _("Classifier Term")
  157. verbose_name_plural = _("Classifier Terms")
  158. classifier = ParentalKey(
  159. Classifier,
  160. related_name="terms",
  161. verbose_name=_("Classifier"),
  162. )
  163. slug = models.SlugField(
  164. allow_unicode=True,
  165. unique=True,
  166. verbose_name=_("Slug"),
  167. )
  168. name = models.CharField(
  169. max_length=255,
  170. verbose_name=_("Name"),
  171. )
  172. panels = [
  173. FieldPanel("name"),
  174. ]
  175. def save(self, *args, **kwargs):
  176. if not self.slug:
  177. # Make a slug and suffix a number if it already exists to ensure uniqueness
  178. newslug = slugify(self.name, allow_unicode=True)
  179. tmpslug = newslug
  180. suffix = 1
  181. while True:
  182. if not ClassifierTerm.objects.filter(slug=tmpslug).exists():
  183. self.slug = tmpslug
  184. break
  185. tmpslug = newslug + "-" + str(suffix)
  186. suffix += 1
  187. return super().save(*args, **kwargs)
  188. def __str__(self):
  189. return "{0} > {1}".format(self.classifier.name, self.name)
  190. @register_snippet
  191. class FilmStrip(ClusterableModel):
  192. class Meta:
  193. verbose_name = _("Film Strip")
  194. name = models.CharField(
  195. max_length=255,
  196. verbose_name=_("Name"),
  197. )
  198. panels = [
  199. FieldPanel("name"),
  200. InlinePanel("film_panels", label=_("Panels")),
  201. ]
  202. def __str__(self):
  203. return self.name
  204. class FilmPanel(Orderable, models.Model):
  205. class Meta:
  206. verbose_name = _("Film Panel")
  207. film_strip = ParentalKey(
  208. FilmStrip,
  209. related_name="film_panels",
  210. verbose_name=_("Film Panel"),
  211. )
  212. background_image = models.ForeignKey(
  213. get_image_model_string(),
  214. null=True,
  215. blank=True,
  216. on_delete=models.SET_NULL,
  217. related_name="+",
  218. verbose_name=_("Background image"),
  219. )
  220. background_color = models.CharField(
  221. max_length=255,
  222. blank=True,
  223. verbose_name=_("Background color"),
  224. help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
  225. )
  226. foreground_color = models.CharField(
  227. max_length=255,
  228. blank=True,
  229. verbose_name=_("Text color"),
  230. help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
  231. )
  232. custom_css_class = models.CharField(
  233. max_length=255,
  234. blank=True,
  235. verbose_name=_("Custom CSS class"),
  236. )
  237. custom_id = models.CharField(
  238. max_length=255,
  239. blank=True,
  240. verbose_name=_("Custom ID"),
  241. )
  242. content = CoderedStreamField(
  243. HTML_STREAMBLOCKS,
  244. blank=True,
  245. use_json_field=True,
  246. )
  247. panels = [
  248. FieldPanel("background_image"),
  249. FieldPanel("background_color"),
  250. FieldPanel("foreground_color"),
  251. FieldPanel("custom_css_class"),
  252. FieldPanel("custom_id"),
  253. FieldPanel("content"),
  254. ]
  255. @register_snippet
  256. class Navbar(models.Model):
  257. """
  258. Snippet for site navigation bars (header, main menu, etc.)
  259. """
  260. class Meta:
  261. verbose_name = _("Navigation Bar")
  262. name = models.CharField(
  263. max_length=255,
  264. verbose_name=_("Name"),
  265. )
  266. custom_css_class = models.CharField(
  267. max_length=255,
  268. blank=True,
  269. verbose_name=_("Custom CSS Class"),
  270. )
  271. custom_id = models.CharField(
  272. max_length=255,
  273. blank=True,
  274. verbose_name=_("Custom ID"),
  275. )
  276. menu_items = CoderedStreamField(
  277. NAVIGATION_STREAMBLOCKS,
  278. verbose_name=_("Navigation links"),
  279. blank=True,
  280. use_json_field=True,
  281. )
  282. panels = [
  283. FieldPanel("name"),
  284. MultiFieldPanel(
  285. [
  286. FieldPanel("custom_css_class"),
  287. FieldPanel("custom_id"),
  288. ],
  289. heading=_("Attributes"),
  290. ),
  291. FieldPanel("menu_items"),
  292. ]
  293. def __str__(self):
  294. return self.name
  295. @register_snippet
  296. class Footer(models.Model):
  297. """
  298. Snippet for website footer content.
  299. """
  300. class Meta:
  301. verbose_name = _("Footer")
  302. name = models.CharField(
  303. max_length=255,
  304. verbose_name=_("Name"),
  305. )
  306. custom_css_class = models.CharField(
  307. max_length=255,
  308. blank=True,
  309. verbose_name=_("Custom CSS Class"),
  310. )
  311. custom_id = models.CharField(
  312. max_length=255,
  313. blank=True,
  314. verbose_name=_("Custom ID"),
  315. )
  316. content = CoderedStreamField(
  317. LAYOUT_STREAMBLOCKS,
  318. verbose_name=_("Content"),
  319. blank=True,
  320. use_json_field=True,
  321. )
  322. panels = [
  323. FieldPanel("name"),
  324. MultiFieldPanel(
  325. [
  326. FieldPanel("custom_css_class"),
  327. FieldPanel("custom_id"),
  328. ],
  329. heading=_("Attributes"),
  330. ),
  331. FieldPanel("content"),
  332. ]
  333. def __str__(self):
  334. return self.name
  335. @register_snippet
  336. class ReusableContent(models.Model):
  337. """
  338. Snippet for resusable content in streamfields.
  339. """
  340. class Meta:
  341. verbose_name = _("Reusable Content")
  342. verbose_name_plural = _("Reusable Content")
  343. name = models.CharField(
  344. max_length=255,
  345. verbose_name=_("Name"),
  346. )
  347. content = CoderedStreamField(
  348. LAYOUT_STREAMBLOCKS,
  349. verbose_name=_("content"),
  350. blank=True,
  351. use_json_field=True,
  352. )
  353. panels = [FieldPanel("name"), FieldPanel("content")]
  354. def __str__(self):
  355. return self.name
  356. @register_snippet
  357. class Accordion(ClusterableModel):
  358. """Class for reusable content in a collapsible block."""
  359. class Meta:
  360. verbose_name = _("Accordion")
  361. verbose_name_plural = _("Accordions")
  362. name = models.CharField(
  363. max_length=255,
  364. verbose_name=_("Name"),
  365. )
  366. panels = [
  367. MultiFieldPanel(
  368. heading=_("Accordion"),
  369. children=[
  370. FieldPanel("name"),
  371. ],
  372. ),
  373. InlinePanel("accordion_panels", label=_("Panels")),
  374. ]
  375. def __str__(self):
  376. return self.name
  377. class AccordionPanel(Orderable, models.Model):
  378. """A panel for a collapsible accordion"""
  379. accordion = ParentalKey(
  380. Accordion,
  381. related_name="accordion_panels",
  382. verbose_name=_("Accordion"),
  383. )
  384. name = models.CharField(
  385. max_length=255,
  386. verbose_name=_("Name"),
  387. )
  388. content = CoderedStreamField(
  389. HTML_STREAMBLOCKS,
  390. blank=True,
  391. use_json_field=True,
  392. )
  393. custom_css_class = models.CharField(
  394. max_length=255,
  395. blank=True,
  396. verbose_name=_("Custom CSS class"),
  397. )
  398. custom_id = models.CharField(
  399. max_length=255,
  400. blank=True,
  401. verbose_name=_("Custom ID"),
  402. )
  403. panels = [
  404. FieldPanel("custom_css_class"),
  405. FieldPanel("custom_id"),
  406. FieldPanel("name"),
  407. FieldPanel("content"),
  408. ]
  409. @register_snippet
  410. class ContentWall(models.Model):
  411. """
  412. Snippet that restricts access to a page with a modal.
  413. """
  414. class Meta:
  415. verbose_name = _("Content Wall")
  416. name = models.CharField(
  417. max_length=255,
  418. verbose_name=_("Name"),
  419. )
  420. content = CoderedStreamField(
  421. LAYOUT_STREAMBLOCKS,
  422. verbose_name=_("Content"),
  423. blank=True,
  424. use_json_field=True,
  425. )
  426. is_dismissible = models.BooleanField(
  427. default=True,
  428. verbose_name=_("Dismissible"),
  429. )
  430. show_once = models.BooleanField(
  431. default=True,
  432. verbose_name=_("Show once"),
  433. help_text=_(
  434. "Do not show the content wall to the same user again after it has been closed."
  435. ),
  436. )
  437. panels = [
  438. MultiFieldPanel(
  439. [
  440. FieldPanel("name"),
  441. FieldPanel("is_dismissible"),
  442. FieldPanel("show_once"),
  443. ],
  444. heading=_("Content Wall"),
  445. ),
  446. FieldPanel("content"),
  447. ]
  448. def __str__(self):
  449. return self.name
  450. class CoderedEmail(ClusterableModel):
  451. """
  452. General purpose abstract clusterable model used for holding email information.
  453. Most likely this should be subclassed with addition of a ParentalKey.
  454. """
  455. class Meta:
  456. abstract = True
  457. verbose_name = _("CodeRed Email")
  458. to_address = models.CharField(
  459. max_length=255,
  460. blank=True,
  461. verbose_name=_("To Addresses"),
  462. help_text=_("Separate multiple email addresses with commas."),
  463. )
  464. from_address = models.CharField(
  465. max_length=255,
  466. blank=True,
  467. verbose_name=_("From Address"),
  468. help_text=_(
  469. 'For example: "sender@example.com" or '
  470. '"Sender Name <sender@example.com>" (without quotes).'
  471. ),
  472. )
  473. reply_address = models.CharField(
  474. max_length=255,
  475. blank=True,
  476. verbose_name=_("Reply-To Address"),
  477. help_text=_("Separate multiple email addresses with commas."),
  478. )
  479. cc_address = models.CharField(
  480. max_length=255,
  481. blank=True,
  482. verbose_name=_("CC"),
  483. help_text=_("Separate multiple email addresses with commas."),
  484. )
  485. bcc_address = models.CharField(
  486. max_length=255,
  487. blank=True,
  488. verbose_name=_("BCC"),
  489. help_text=_("Separate multiple email addresses with commas."),
  490. )
  491. subject = models.CharField(
  492. max_length=255, blank=True, verbose_name=_("Subject")
  493. )
  494. body = models.TextField(blank=True, verbose_name=_("Body"))
  495. panels = [
  496. MultiFieldPanel(
  497. [
  498. FieldPanel("to_address"),
  499. FieldPanel("from_address"),
  500. FieldPanel("cc_address"),
  501. FieldPanel("bcc_address"),
  502. FieldPanel("subject"),
  503. FieldPanel("body"),
  504. ],
  505. _("Email Message"),
  506. ),
  507. ]
  508. def __str__(self):
  509. return self.subject