base_blocks.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. """
  2. Bases, mixins, and utilites for blocks.
  3. """
  4. from django import forms
  5. from django.template.loader import render_to_string
  6. from django.utils.encoding import force_text
  7. from django.utils.functional import cached_property
  8. from django.utils.safestring import mark_safe
  9. from django.utils.translation import ugettext_lazy as _
  10. from wagtail.core import blocks
  11. from wagtail.core.models import Collection
  12. from wagtail.core.utils import resolve_model_string
  13. from wagtail.documents.blocks import DocumentChooserBlock
  14. from coderedcms.settings import cr_settings
  15. class MultiSelectBlock(blocks.FieldBlock):
  16. """
  17. Renders as MultipleChoiceField, used for adding checkboxes,
  18. radios, or multiselect inputs in the streamfield.
  19. """
  20. def __init__(self, required=True, help_text=None, choices=None, widget=None, **kwargs):
  21. self.field = forms.MultipleChoiceField(
  22. required=required,
  23. help_text=help_text,
  24. choices=choices,
  25. widget=widget,
  26. )
  27. super().__init__(**kwargs)
  28. def get_searchable_content(self, value):
  29. return [force_text(value)]
  30. class ClassifierTermChooserBlock(blocks.FieldBlock):
  31. """
  32. Enables choosing a ClassifierTerm in the streamfield.
  33. Lazy loads the target_model from the string to avoid recursive imports.
  34. """
  35. widget = forms.Select
  36. def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
  37. self._required=required
  38. self._help_text=help_text
  39. self._label=label
  40. super().__init__(*args, **kwargs)
  41. @cached_property
  42. def target_model(self):
  43. return resolve_model_string('coderedcms.ClassifierTerm')
  44. @cached_property
  45. def field(self):
  46. return forms.ModelChoiceField(
  47. queryset=self.target_model.objects.all().order_by('classifier__name', 'name'),
  48. widget=self.widget,
  49. required=self._required,
  50. label=self._label,
  51. help_text=self._help_text,
  52. )
  53. def to_python(self, value):
  54. """
  55. Convert the serialized value back into a python object.
  56. """
  57. if isinstance(value, int):
  58. return self.target_model.objects.get(pk=value)
  59. return value
  60. def get_prep_value(self, value):
  61. """
  62. Serialize the model in a form suitable for wagtail's JSON-ish streamfield
  63. """
  64. if isinstance(value, self.target_model):
  65. return value.pk
  66. return value
  67. class CollectionChooserBlock(blocks.FieldBlock):
  68. """
  69. Enables choosing a wagtail Collection in the streamfield.
  70. """
  71. target_model = Collection
  72. widget = forms.Select
  73. def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
  74. self._required=required
  75. self._help_text=help_text
  76. self._label=label
  77. super().__init__(*args, **kwargs)
  78. @cached_property
  79. def field(self):
  80. return forms.ModelChoiceField(
  81. queryset=self.target_model.objects.all().order_by('name'),
  82. widget=self.widget,
  83. required=self._required,
  84. label=self._label,
  85. help_text=self._help_text,
  86. )
  87. def to_python(self, value):
  88. """
  89. Convert the serialized value back into a python object.
  90. """
  91. if isinstance(value, int):
  92. return self.target_model.objects.get(pk=value)
  93. return value
  94. def get_prep_value(self, value):
  95. """
  96. Serialize the model in a form suitable for wagtail's JSON-ish streamfield
  97. """
  98. if isinstance(value, self.target_model):
  99. return value.pk
  100. return value
  101. class ButtonMixin(blocks.StructBlock):
  102. """
  103. Standard style and size options for buttons.
  104. """
  105. button_title = blocks.CharBlock(
  106. max_length=255,
  107. required=True,
  108. label=_('Button Title'),
  109. )
  110. button_style = blocks.ChoiceBlock(
  111. choices=cr_settings['FRONTEND_BTN_STYLE_CHOICES'],
  112. default=cr_settings['FRONTEND_BTN_STYLE_DEFAULT'],
  113. required=False,
  114. label=_('Button Style'),
  115. )
  116. button_size = blocks.ChoiceBlock(
  117. choices=cr_settings['FRONTEND_BTN_SIZE_CHOICES'],
  118. default=cr_settings['FRONTEND_BTN_SIZE_DEFAULT'],
  119. required=False,
  120. label=_('Button Size'),
  121. )
  122. class CoderedAdvSettings(blocks.StructBlock):
  123. """
  124. Common fields each block should have,
  125. which are hidden under the block's "Advanced Settings" dropdown.
  126. """
  127. # placeholder, real value get set in __init__()
  128. custom_template = blocks.Block()
  129. custom_css_class = blocks.CharBlock(
  130. required=False,
  131. max_length=255,
  132. label=_('Custom CSS Class'),
  133. )
  134. custom_id = blocks.CharBlock(
  135. required=False,
  136. max_length=255,
  137. label=_('Custom ID'),
  138. )
  139. class Meta:
  140. form_template = 'wagtailadmin/block_forms/base_block_settings_struct.html'
  141. label = _('Advanced Settings')
  142. def __init__(self, local_blocks=None, template_choices=None, **kwargs):
  143. if not local_blocks:
  144. local_blocks = ()
  145. local_blocks += (
  146. (
  147. 'custom_template',
  148. blocks.ChoiceBlock(
  149. choices=template_choices,
  150. default=None,
  151. required=False,
  152. label=_('Template'))
  153. ),
  154. )
  155. super().__init__(local_blocks, **kwargs)
  156. class CoderedAdvTrackingSettings(CoderedAdvSettings):
  157. """
  158. CoderedAdvSettings plus additional tracking fields.
  159. """
  160. ga_tracking_event_category = blocks.CharBlock(
  161. required=False,
  162. max_length=255,
  163. label=_('Tracking Event Category'),
  164. )
  165. ga_tracking_event_label = blocks.CharBlock(
  166. required=False,
  167. max_length=255,
  168. label=_('Tracking Event Label'),
  169. )
  170. class CoderedAdvColumnSettings(CoderedAdvSettings):
  171. """
  172. BaseBlockSettings plus additional column fields.
  173. """
  174. column_breakpoint = blocks.ChoiceBlock(
  175. choices=cr_settings['FRONTEND_COL_BREAK_CHOICES'],
  176. default=cr_settings['FRONTEND_COL_BREAK_DEFAULT'],
  177. required=False,
  178. verbose_name=_('Column Breakpoint'),
  179. help_text=_('Screen size at which the column will expand horizontally or stack vertically.'),
  180. )
  181. class BaseBlock(blocks.StructBlock):
  182. """
  183. Common attributes for all blocks used in CodeRed CMS.
  184. """
  185. # subclasses can override this to determine the advanced settings class
  186. advsettings_class = CoderedAdvSettings
  187. # placeholder, real value get set in __init__() from advsettings_class
  188. settings = blocks.Block()
  189. def __init__(self, local_blocks=None, **kwargs):
  190. """
  191. Construct and inject settings block, then initialize normally.
  192. """
  193. klassname = self.__class__.__name__.lower()
  194. choices = cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get('*', ()) + \
  195. cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get(klassname, ())
  196. if not local_blocks:
  197. local_blocks = ()
  198. local_blocks += (('settings', self.advsettings_class(template_choices=choices)),)
  199. super().__init__(local_blocks, **kwargs)
  200. def render(self, value, context=None):
  201. template = value['settings']['custom_template']
  202. if not template:
  203. template = self.get_template(context=context)
  204. if not template:
  205. return self.render_basic(value, context=context)
  206. if context is None:
  207. new_context = self.get_context(value)
  208. else:
  209. new_context = self.get_context(value, parent_context=dict(context))
  210. return mark_safe(render_to_string(template, new_context))
  211. class BaseLayoutBlock(BaseBlock):
  212. """
  213. Common attributes for all blocks used in CodeRed CMS.
  214. """
  215. # Subclasses can override this to provide a default list of blocks for the content.
  216. content_streamblocks = []
  217. def __init__(self, local_blocks=None, **kwargs):
  218. if not local_blocks and self.content_streamblocks:
  219. local_blocks = self.content_streamblocks
  220. if local_blocks:
  221. local_blocks = (('content', blocks.StreamBlock(local_blocks, label=_('Content'))),)
  222. super().__init__(local_blocks, **kwargs)
  223. class LinkStructValue(blocks.StructValue):
  224. """
  225. Generates a URL for blocks with multiple link choices.
  226. """
  227. @property
  228. def url(self):
  229. page = self.get('page_link')
  230. doc = self.get('doc_link')
  231. ext = self.get('other_link')
  232. if page:
  233. return page.url
  234. elif doc:
  235. return doc.url
  236. else:
  237. return ext
  238. class BaseLinkBlock(BaseBlock):
  239. """
  240. Common attributes for creating a link within the CMS.
  241. """
  242. page_link = blocks.PageChooserBlock(
  243. required=False,
  244. label=_('Page link'),
  245. )
  246. doc_link = DocumentChooserBlock(
  247. required=False,
  248. label=_('Document link'),
  249. )
  250. other_link = blocks.CharBlock(
  251. required=False,
  252. max_length=255,
  253. label=_('Other link'),
  254. )
  255. advsettings_class = CoderedAdvTrackingSettings
  256. class Meta:
  257. value_class = LinkStructValue