From b0dd3a272d4437a6b3aece29c02e3a9508fbf0c9 Mon Sep 17 00:00:00 2001 From: Erik Stein Date: Mon, 12 Feb 2018 17:08:45 +0100 Subject: [PATCH] Refactorement. --- content_plugins/__init__.py | 6 + content_plugins/abstract_plugins.py | 107 ++++---- content_plugins/content_plugins.py | 371 ++++++++++++++++++++++++++++ content_plugins/fields.py | 13 +- content_plugins/mixins.py | 4 +- content_plugins/renderer.py | 5 +- 6 files changed, 452 insertions(+), 54 deletions(-) diff --git a/content_plugins/__init__.py b/content_plugins/__init__.py index e69de29..83c5d59 100644 --- a/content_plugins/__init__.py +++ b/content_plugins/__init__.py @@ -0,0 +1,6 @@ +from django.conf import settings + + +USE_TRANSLATABLE_FIELDS = getattr(settings, 'CONTENT_USE_TRANSLATABLE_FIELDS', True) + +# TODO Implement translatable AutoSlugField: USE_TRANSLATABLE_SLUG_FIELDS = getattr(settings, 'CONTENT_USE_TRANSLATABLE_SLUG_FIELDS', True) diff --git a/content_plugins/abstract_plugins.py b/content_plugins/abstract_plugins.py index 4424a3d..696551a 100644 --- a/content_plugins/abstract_plugins.py +++ b/content_plugins/abstract_plugins.py @@ -10,13 +10,14 @@ from django.utils.translation import ugettext_lazy as _ from feincms3.cleanse import CleansedRichTextField from shared.utils.fields import AutoSlugField -from shared.multilingual.utils.fields import TranslatableCharField, TranslatableTextField from .admin import ContentInlineBase, RichTextInlineBase -from .fields import TranslatableCleansedRichTextField -# FIXME Implement USE_TRANSLATABLE_FIELDS -from .mixins import USE_TRANSLATABLE_FIELDS +from . import USE_TRANSLATABLE_FIELDS + +if USE_TRANSLATABLE_FIELDS: + from shared.multilingual.utils.fields import TranslatableCharField + from .fields import TranslatableCleansedRichTextField """ @@ -31,8 +32,8 @@ TODO Class hierarchy should be class BasePlugin(models.Model): class Meta: abstract = True - verbose_name = _("Element") - verbose_name_plural = _("Elements") + verbose_name = _("plugin") + verbose_name_plural = _("plugins") def __str__(self): return "{} ({})".format(self._meta.verbose_name, self.pk) @@ -57,14 +58,16 @@ class BasePlugin(models.Model): or an instance with a "render" method (i.e. a Template instance). Default implementation is to return the result of self.get_template_names(). + + See rendering logic in feincms3.TemplateRendererPlugin. """ return self.get_template_names() def get_context_data(self, request_context, **kwargs): context = kwargs.get('context', {}) - context['parent'] = self.parent - context['request'] = request_context['request'] context['content'] = self + context['parent'] = self.parent + context['request'] = getattr(request_context, 'request', None) return context # For rendering the template's render() method is used @@ -82,19 +85,19 @@ class StringRendererPlugin(BasePlugin): raise NotImplementedError -class StyleField(models.CharField): - # Allow overriding of STYLE_CHOICES constant in subclasses - - def contribute_to_class(self, cls, name, **kwargs): - if hasattr(cls, 'STYLE_CHOICES'): - self.choices = cls.STYLE_CHOICES - super().contribute_to_class(cls, name, **kwargs) +class StyleMixin(models.Model): + class StyleField(models.CharField): + # Allow overriding of STYLE_CHOICES in subclasses + def contribute_to_class(self, cls, name, **kwargs): + if hasattr(cls, 'STYLE_CHOICES'): + self.choices = cls.STYLE_CHOICES + super().contribute_to_class(cls, name, **kwargs) -class StyleMixin(models.Model): STYLE_CHOICES = ( ('default', _("default")), ) + style = StyleField(_("style"), max_length=50, null=True, blank=True) class Meta: @@ -103,42 +106,56 @@ class StyleMixin(models.Model): def get_style_slug(self): return getattr(self, 'style', None) or 'default' + def get_template_names(self): + # Should only be called by classes using filesystem templates + template_names = super().get_template_names() or [] + template_names.extend([ + "{path_prefix}style/_{style}.html".format( + path_prefix=self.get_path_prefix(), + style=self.get_style_slug()), + ]) + return template_names -class FilesystemTemplateRendererPlugin(BasePlugin): - # TODO Join FilesystemTemplateRendererPlugin code with BaseObjectElement code +class FilesystemTemplateRendererPlugin(BasePlugin): template_name = None - path_prefix = None # Potential values: "richtext", "image" + path_prefix = 'plugins/' # TODO Rename to 'template_name_prefix' class Meta: abstract = True def get_path_prefix(self): - if self.path_prefix: - return "{}{}".format(self.path_prefix, os.path.sep) - else: - return "" + return self.path_prefix + + def prefixed_path(self, path): + return "{}{}".format(self.get_path_prefix(), path) def get_template_names(self): - # TODO Style related logic should be part of the StyleMixin: maybe get_template_names should call super() - if hasattr(self, 'style'): + # Per default look first for absolute template_name path and + # template_name path prefixed with path_prefix. + if getattr(self, 'template_name', False): template_names = [ - "curatorialcontent/elements/{path_prefix}style/_{style}.html".format( - path_prefix=self.get_path_prefix(), style=self.get_style_slug()), + self.template_name, + self.prefixed_path(self.template_name) ] else: template_names = [] + template_names.extend(super().get_template_names() or []) + return template_names + [ - "curatorialcontent/elements/{path_prefix}_default.html".format( + "{path_prefix}_default.html".format( path_prefix=self.get_path_prefix()) - ] + ([self.template_name] if getattr(self, 'template_name', None) else []) + ] class RichTextBase(StyleMixin, FilesystemTemplateRendererPlugin): - richtext = TranslatableCleansedRichTextField(_("text"), blank=True) + if USE_TRANSLATABLE_FIELDS: + richtext = TranslatableCleansedRichTextField(_("text"), blank=True) + else: + richtext = CleansedRichTextField(_("text"), blank=True) - path_prefix = 'richtext' + path_prefix = FilesystemTemplateRendererPlugin.path_prefix + 'richtext/' class Meta: abstract = True @@ -151,29 +168,29 @@ class RichTextBase(StyleMixin, FilesystemTemplateRendererPlugin): class SectionBase(StyleMixin, BasePlugin): - subheading = TranslatableCharField(_("subheading"), max_length=500) + if USE_TRANSLATABLE_FIELDS: + subheading = TranslatableCharField(_("subheading"), max_length=500) + else: + subheading = models.CharField(_("subheading"), max_length=500) slug = AutoSlugField(_("slug"), max_length=200, blank=True, populate_from='subheading', unique_slug=False) class Meta: abstract = True - verbose_name = _("Abschnittswechsel") - verbose_name_plural = _("Abschnittswechsel") + verbose_name = _("section") + verbose_name_plural = _("section") def get_template(self): return Template(""" -
-

{{ heading }}

- -
+

{{ subheading }}

""") def get_context_data(self, request_context, **kwargs): context = super().get_context_data(request_context, **kwargs) context['slug'] = self.slug - context['heading'] = self.heading + context['subheading'] = self.subheading return context @@ -224,7 +241,10 @@ class DownloadBase(StyleMixin, BasePlugin): class FootnoteBase(BasePlugin): # TODO Validators: index might only contain alphanumeric characters index = models.CharField(_("footnote index"), max_length=10) - richtext = TranslatableCleansedRichTextField(_("footnote text")) + if USE_TRANSLATABLE_FIELDS: + richtext = TranslatableCleansedRichTextField(_("footnote text")) + else: + richtext = CleansedRichTextField(_("footnote text")) html_tag = '
  • ' @@ -233,13 +253,12 @@ class FootnoteBase(BasePlugin): verbose_name = _("footnote") verbose_name_plural = _("footnote") + # TODO Convert to Template def render(self, html_tag=None): template = """ {opening_tag} -
    - {number} - {text} -
    + {number} +
    {text}
    {closing_tag} """ context = { diff --git a/content_plugins/content_plugins.py b/content_plugins/content_plugins.py index e69de29..3171a95 100644 --- a/content_plugins/content_plugins.py +++ b/content_plugins/content_plugins.py @@ -0,0 +1,371 @@ +""" +Example instantiation of the abstract plugin base classes +""" + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from content_editor.models import create_plugin_base +from shared.utils.text import slugify_long +from shared.multilingual.utils.fields import TranslatableCharField + +from ..collection.models import ItemBundle, Event, Collection +from ..content.admin import RichTextInlineBase +from ..content import plugins +from ..content.renderer import ContentPluginRenderer +from ..media.models import ( + MediaImage, + MediaAudio, + MediaVideo, + MediaDocument, +) +from ..people.models import Person +from ..thesaurus.models import Term +from .models import Page + + +ContentPluginBase = create_plugin_base(Page) + + +renderer = ContentPluginRenderer() + + +@renderer.register() +class RichTextContentPlugin(plugins.RichTextFootnoteMixin, plugins.RichTextBase, ContentPluginBase): + regions = ['main', 'aside', 'intro'] + + +@renderer.register() +class BlockquoteContentPlugin(plugins.RichTextFootnoteMixin, plugins.RichTextBase, ContentPluginBase): + STYLE_CHOICES = plugins.StyleMixin.STYLE_CHOICES + ( + ('blockquote', _("Blockquote")), + ('pullquote', _("Pull Quote")), + ('introquote', _("Introductory Quote")), + ) + + path_prefix = 'quote' + + regions = ['main', 'intro'] + + class Meta: + verbose_name = _("quote") + verbose_name_plural = _("quotes") + + +@renderer.register() +class SectionContentPlugin(plugins.SectionBase, ContentPluginBase): + regions = ['main', 'aside'] + + +# @renderer.register() +# class ImageContentPlugin(plugins.ImageBase, ContentPluginBase): +# regions = ['main', 'aside'] + + +# @renderer.register() +# class DownloadContentPlugin(plugins.DownloadBase, ContentPluginBase): +# regions = ['main', 'aside'] + + +@renderer.register() +class FootnoteContentPlugin(plugins.FootnoteBase, ContentPluginBase): + regions = ['references', 'footnotes'] + + +class BaseObjectElement(plugins.BasePlugin, ContentPluginBase): + STYLE_CHOICES = plugins.StyleMixin.STYLE_CHOICES + ( + ('minimal', _("Minimal View")), + ('extensive', _("Extensive View")), + ) + + class Meta: + abstract = True + verbose_name = _("object view") + verbose_name_plural = _("objects view") + + fk_fieldname = None + regions = ['main', 'aside'] + + @property + def object(self): + assert self.fk_fieldname, "fk_fieldname not set." + return getattr(self, self.fk_fieldname) + + def type_slug(self): + slug = '' + type = getattr(self.object, 'type', None) + if type: + slug = getattr(type, 'internal_slug', '') + return slug + + def get_template_names(self): + assert self.fk_fieldname, "fk_fieldname not set." + + type_slug = self.type_slug() + style_slug = getattr(self, 'style', None) or 'default' + + # Append a potentially defined self.template_name + return [ + "curatorialcontent/elements/{path_prefix}/pk/_{pk}.html".format( + path_prefix=self.fk_fieldname, pk=self.object.pk), + "curatorialcontent/elements/{path_prefix}/type/_{style}/_{type}.html".format( + path_prefix=self.fk_fieldname, style=style_slug, type=type_slug), + "curatorialcontent/elements/{path_prefix}/style/_{style}.html".format( + path_prefix=self.fk_fieldname, style=style_slug), + "curatorialcontent/elements/{path_prefix}/type/_{type}.html".format( + path_prefix=self.fk_fieldname, type=type_slug), + "curatorialcontent/elements/{path_prefix}/_default.html".format( + path_prefix=self.fk_fieldname) + ] + ([self.template_name] if hasattr(self, 'template_name') else []) + + @classmethod + def admin_inline(cls, base_class=None): + assert cls.fk_fieldname, "fk_fieldname not set." + inline = super().admin_inline(base_class=RichTextInlineBase) # TODO Do we need RichTextInlineBase here? + if not inline.raw_id_fields: + inline.raw_id_fields = [] + inline.raw_id_fields += [cls.fk_fieldname] + return inline + + +@renderer.register() +class ItemBundleElement(BaseObjectElement, plugins.StyleMixin): + STYLE_CHOICES = BaseObjectElement.STYLE_CHOICES + ( + ('teaser', _("Big Top Teaser")), + ) + + itembundle = models.ForeignKey(ItemBundle, verbose_name=_("ItemBundle")) + + fk_fieldname = 'itembundle' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("item bundle view") + verbose_name_plural = _("item bundle views") + + +@renderer.register() +class PersonElement(BaseObjectElement, plugins.StyleMixin): + person = models.ForeignKey(Person, verbose_name=_("Person")) + + fk_fieldname = 'person' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("person view") + verbose_name_plural = _("person views") + + +@renderer.register() +class ContributorElement(BaseObjectElement): + person = models.ForeignKey(Person, verbose_name=_("Person")) + CONTRIBUTOR_ROLE_CHOICES = ( + ('author', _("Author")), + ('contributor', _("Contributor")), + ) + role = models.CharField(_("role"), max_length=50, + choices=CONTRIBUTOR_ROLE_CHOICES, + default=CONTRIBUTOR_ROLE_CHOICES[0][0], null=False, blank=False) + + fk_fieldname = 'person' + regions = ['contributors'] + + class Meta: + verbose_name = _("contributor") + verbose_name_plural = _("contributors") + + +@renderer.register() +class TermElement(BaseObjectElement, plugins.StyleMixin): + term = models.ForeignKey(Term, verbose_name=_("Term")) + + fk_fieldname = 'term' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("term view") + verbose_name_plural = _("term views") + + +@renderer.register() +class EventElement(BaseObjectElement, plugins.StyleMixin): + event = models.ForeignKey(Event, verbose_name=_("Event")) + + fk_fieldname = 'event' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("event view") + verbose_name_plural = _("event views") + + +@renderer.register() +class SubcollectionElement(BaseObjectElement, plugins.StyleMixin): + STYLE_CHOICES = BaseObjectElement.STYLE_CHOICES + ( + ('index-nav', _("Index Page Navigation")), + ) + + collection = models.ForeignKey(Collection, verbose_name=_("Subcollection")) + + fk_fieldname = 'collection' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("collection view") + verbose_name_plural = _("collection views") + + +@renderer.register() +class MediaImageElement(BaseObjectElement): + image = models.ForeignKey(MediaImage, verbose_name=_("Image")) + caption = TranslatableCharField(_("caption"), max_length=500, null=True, blank=True, help_text=_("image caption")) + + fk_fieldname = 'image' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("image") + verbose_name_plural = _("images") + + def type_slug(self): + return '' + + +@renderer.register() +class MediaAudioElement(BaseObjectElement): + audio = models.ForeignKey(MediaAudio, verbose_name=_("Audio")) + caption = TranslatableCharField(_("caption"), max_length=500, null=True, blank=True, help_text=_("audio caption")) + + fk_fieldname = 'audio' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("audio") + verbose_name_plural = _("audio") + + +@renderer.register() +class MediaVideoElement(BaseObjectElement): + video = models.ForeignKey(MediaVideo, verbose_name=_("Video")) + caption = TranslatableCharField(_("caption"), max_length=500, null=True, blank=True, help_text=_("video caption")) + + fk_fieldname = 'video' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("video") + verbose_name_plural = _("video") + + +@renderer.register() +class MediaDocumentElement(BaseObjectElement): + document = models.ForeignKey(MediaDocument, verbose_name=_("Document")) + caption = TranslatableCharField(_("caption"), max_length=500, null=True, blank=True, help_text=_("document caption")) + + fk_fieldname = 'document' + regions = ['main', 'aside'] + + class Meta: + verbose_name = _("document") + verbose_name_plural = _("document") + + +@renderer.register() +class SubsectionsElement(plugins.StyleMixin, plugins.FilesystemTemplateRendererPlugin, ContentPluginBase): + regions = ['main', 'aside'] + + path_prefix = 'subsections_nav' + + class Meta: + verbose_name = _("subsections navigation element") + verbose_name_plural = _("subsections navigation elements") + + +@renderer.register() +class TeamElement(plugins.StyleMixin, plugins.FilesystemTemplateRendererPlugin, ContentPluginBase): + regions = ['main', 'aside'] + + path_prefix = 'team' + + class Meta: + verbose_name = _("team navigation element") + verbose_name_plural = _("team navigation elements") + + +@renderer.register() +class ActorsElement(plugins.StyleMixin, plugins.FilesystemTemplateRendererPlugin, ContentPluginBase): + regions = ['main', 'aside'] + + path_prefix = 'actors' + + class Meta: + verbose_name = _("actors navigation element") + verbose_name_plural = _("actors navigation elements") + + +@renderer.register() +class ArticlesElement(plugins.StyleMixin, plugins.FilesystemTemplateRendererPlugin, ContentPluginBase): + regions = ['main', 'aside'] + + path_prefix = 'articles_nav' + + class Meta: + verbose_name = _("articles navigation element") + verbose_name_plural = _("articles navigation elements") + + +# +# Slideshow Elements + +class BaseSlideshowContentPlugin(plugins.StyleMixin, plugins.FilesystemTemplateRendererPlugin, ContentPluginBase): + STYLE_CHOICES = ( + ('black', _("black background")), + ('yellow', _("yellow background")), + ('white', _("white background")), + ) + + caption = plugins.TranslatableCleansedRichTextField(_("caption"), blank=True) + + path_prefix = 'slide' + + regions = ['slides'] + + class Meta: + abstract = True + + @classmethod + def admin_inline(cls, base_class=None): + return super().admin_inline(base_class=RichTextInlineBase) + + +@renderer.register() +class TextSlideshowContentPlugin(BaseSlideshowContentPlugin): + path_prefix = 'slide/text/' + + regions = ['slides'] + + class Meta: + verbose_name = _("text slide") + verbose_name_plural = _("text slides") + + +@renderer.register() +class ItembundleSlideshowContentPlugin(BaseSlideshowContentPlugin): + itembundle = models.ForeignKey(ItemBundle, verbose_name=_("ItemBundle")) + + path_prefix = 'slide/itembundle/' + + regions = ['slides'] + + class Meta: + verbose_name = _("item bundle slide") + verbose_name_plural = _("item bundle slides") + + @classmethod + def admin_inline(cls, base_class=None): + inline = super().admin_inline(base_class=RichTextInlineBase) + if not inline.raw_id_fields: + inline.raw_id_fields = [] + inline.raw_id_fields += ['itembundle'] + return inline + diff --git a/content_plugins/fields.py b/content_plugins/fields.py index 40cad40..55dab63 100644 --- a/content_plugins/fields.py +++ b/content_plugins/fields.py @@ -1,8 +1,11 @@ from feincms3.cleanse import CleansedRichTextField -from shared.multilingual.utils.fields import TranslatableFieldMixin +from . import USE_TRANSLATABLE_FIELDS -class TranslatableCleansedRichTextField(TranslatableFieldMixin, CleansedRichTextField): - base_class = CleansedRichTextField - extra_parameter_names = ['config_name', 'extra_plugins', 'external_plugin_resources'] - # TODO Implement translatable rich text widget + +if USE_TRANSLATABLE_FIELDS: + from shared.multilingual.utils.fields import TranslatableFieldMixin + + class TranslatableCleansedRichTextField(TranslatableFieldMixin, CleansedRichTextField): + base_class = CleansedRichTextField + extra_parameter_names = ['config_name', 'extra_plugins', 'external_plugin_resources'] diff --git a/content_plugins/mixins.py b/content_plugins/mixins.py index e57e1dd..a5dc004 100644 --- a/content_plugins/mixins.py +++ b/content_plugins/mixins.py @@ -9,9 +9,7 @@ from shared.utils.fields import AutoSlugField from shared.utils.functional import firstof from shared.utils.text import slimdown - -USE_TRANSLATABLE_FIELDS = getattr(settings, 'CONTENT_USE_TRANSLATABLE_FIELDS', True) -# TODO Implement translatable AutoSlugField: USE_TRANSLATABLE_SLUG_FIELDS = getattr(settings, 'CONTENT_USE_TRANSLATABLE_SLUG_FIELDS', True) +from . import USE_TRANSLATABLE_FIELDS if USE_TRANSLATABLE_FIELDS: diff --git a/content_plugins/renderer.py b/content_plugins/renderer.py index 6e2be84..8c0f22c 100644 --- a/content_plugins/renderer.py +++ b/content_plugins/renderer.py @@ -8,10 +8,11 @@ class MultilingualRegions(Regions): return '%s-%s' % (get_language(), super().cache_key(region)) -class PluginRenderer(TemplatePluginRenderer): - # Used as decorator +class ContentPluginRenderer(TemplatePluginRenderer): def register(self): """ + Used as decorator + Usage: @renderer.register() class TextPlugin(ModelPlugin):