You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

327 lines
9.8 KiB

import os
import re
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.html import mark_safe, strip_tags
from django.utils.text import Truncator
from django.utils.translation import ugettext_lazy as _
from shared.utils.models.slugs import DowngradingSlugField
# TODO Rename ContentInlineBase to PluginInlineBase
from .admin import ContentInlineBase, RichTextInlineBase
from .plugins.mixins import StyleMixin # Make available for import # noqa
from . import USE_TRANSLATABLE_FIELDS
if USE_TRANSLATABLE_FIELDS:
from shared.multilingual.utils.fields import TranslatableCharField
from .fields import TranslatableCleansedRichTextField
else:
from feincms3.cleanse import CleansedRichTextField
TranslatableCharField = models.CharField
TranslatableCleansedRichTextField = CleansedRichTextField
class BasePlugin(models.Model):
admin_inline_baseclass = ContentInlineBase
class Meta:
abstract = True
verbose_name = _("plugin")
verbose_name_plural = _("plugins")
@classmethod
def register_with_renderer(cls, renderer):
pass
def __str__(self):
return "{} ({})".format(self._meta.verbose_name, self.pk)
@classmethod
def admin_inline(cls, base_class=None):
class Inline(base_class or cls.admin_inline_baseclass):
model = cls
regions = cls.regions
return Inline
def get_plugin_context(self, context=None, **kwargs):
"""
Returns a dict.
"""
plugin_context = {}
plugin_context['content'] = self
plugin_context['parent'] = self.parent
if 'request_context' in kwargs:
plugin_context['request'] = getattr(kwargs['request_context'], 'request', None)
return plugin_context
class StringRendererPlugin(BasePlugin):
class Meta:
abstract = True
@classmethod
def register_with_renderer(cls, renderer):
renderer.register_string_renderer(cls, cls.render)
def render(self):
raise NotImplementedError("render method must be implemented by subclass")
class TemplateRendererPlugin(BasePlugin):
class Meta:
abstract = True
@classmethod
def register_with_renderer(cls, renderer):
renderer.register_template_renderer(cls, cls.get_template, cls.get_plugin_context)
def get_template_names(self):
t = getattr(self, 'template_name', None)
if t:
return [t]
else:
return []
def get_template(self):
"""
Might return a single template name, a list of template names
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()
# For rendering the template's render() method is used
class FilesystemTemplateRendererPlugin(TemplateRendererPlugin):
# Don't define template_name_prefix here, so that a sibling class takes precedence
# template_name_prefix = ''
# template_name = ''
class Meta:
abstract = True
def get_template_name_prefix(self):
return getattr(self, 'template_name_prefix', '')
def prefixed_path(self, path):
# TODO Use posixpath
return "{}{}".format(self.get_template_name_prefix(), path)
def get_template_names(self):
"""
Look first for template_name,
second for prefixed template_name,
then super's template names,
finally prefixed _default.html.
"""
if not getattr(self, 'template_name', False):
raise ImproperlyConfigured(
"FilesystemTemplateRendererPlugin requires either a definition of "
"'template_name' or an implementation of 'get_template_names()'")
else:
return [
self.prefixed_path(self.template_name),
]
class PrepareRichtextMixin:
@property
def prepared_richtext(self):
return mark_safe(self.get_prepared_richtext(self.richtext))
def get_prepared_richtext(self, richtext):
return richtext
class RichTextBase(PrepareRichtextMixin, FilesystemTemplateRendererPlugin):
richtext = TranslatableCleansedRichTextField(_("text"), blank=True)
admin_inline_baseclass = RichTextInlineBase
template_name = 'plugins/_richtext.html'
class Meta:
abstract = True
verbose_name = _("text")
verbose_name_plural = _("texts")
def __str__(self):
return Truncator(strip_tags(self.richtext)).words(10, truncate=" ...")
class SectionBreakBase(FilesystemTemplateRendererPlugin):
subheading = TranslatableCharField(_("subheading"), null=True, blank=True, max_length=500)
slug = DowngradingSlugField(_("slug"), max_length=200, blank=True, populate_from='subheading', unique_slug=False)
template_name = 'plugins/_sectionbreak.html'
class Meta:
abstract = True
verbose_name = _("section break")
verbose_name_plural = _("section break")
def __str__(self):
return Truncator(strip_tags(self.subheading)).words(10, truncate=" ...")
# FIXME Not needed, members are accessible through {{ content.slug }} etc.
def get_plugin_context(self, context=None, **kwargs):
context = super().get_plugin_context(context=context, **kwargs)
context['slug'] = self.slug
context['subheading'] = self.subheading
return context
SectionBase = SectionBreakBase
class ObjectPluginBase(FilesystemTemplateRendererPlugin):
fk_fieldname = None
regions = None
class Meta:
abstract = True
def __str__(self):
return str(getattr(self, self.fk_fieldname, ""))
@property
def object(self):
assert self.fk_fieldname, "fk_fieldname not set."
return getattr(self, self.fk_fieldname)
def get_type_slug(self):
type = getattr(self.object, 'type', None)
if type:
return getattr(type, 'internal_slug', "")
return ""
def get_template_names(self):
""""
_<fk_fieldname>_<object.type>/_<style>.html
_<fk_fieldname>_<object.type>.html
_<fk_fieldname>/_<style>.html
_<fk_fieldname>.html
"""
assert self.fk_fieldname, "fk_fieldname not set."
template_names = []
type_slug = self.get_type_slug()
if getattr(self, 'template_name', False):
base_template_name = os.path.splitext(self.template_name)[0]
else:
base_template_name = "plugins/_{}".format(self.fk_fieldname)
if type_slug:
template_names.append(self.prefixed_path(
"{}_{}.html".format(
base_template_name, type_slug
)
))
template_names.append(
self.prefixed_path("{}.html".format(base_template_name)))
return template_names
@classmethod
def admin_inline(cls):
assert cls.fk_fieldname, "fk_fieldname not set."
inline = super().admin_inline()
if not inline.raw_id_fields:
inline.raw_id_fields = []
inline.raw_id_fields += [cls.fk_fieldname]
return inline
class SimpleImageBase(StringRendererPlugin):
image = models.ImageField(_("image"), upload_to='images/%Y/%m/')
caption = TranslatableCharField(_("caption"), max_length=500,
null=True, blank=True,
help_text=_("Optional, used instead of the caption of the image object."))
class Meta:
abstract = True
verbose_name = _("image")
verbose_name_plural = _("images")
def __str__(self):
return getattr(self.image, 'name', "")
def render(self):
template = """
<figure class="image">
<img src="{src}">
<figcaption>
{caption_text}
</figcaption>
</figure>
"""
return mark_safe(template.format(
src=self.image.url,
caption_text=mark_safe(self.caption or "")
))
class SimpleDownloadBase(StringRendererPlugin):
file = models.FileField(upload_to='downloads/%Y/%m/')
class Meta:
abstract = True
verbose_name = _("download")
verbose_name_plural = _("downloads")
def __str__(self):
return getattr(self.file, 'name', "")
def render(self):
template = """
<a href="{url}" download="{name}">{name}</a>
"""
return mark_safe(template.format(
url=self.file.url,
name=self.file.name,
))
class FootnoteBase(PrepareRichtextMixin, FilesystemTemplateRendererPlugin):
# TODO Validators: index must only contain alphanumeric characters
index = models.CharField(_("footnote index"), max_length=10)
richtext = TranslatableCleansedRichTextField(_("footnote text"), null=True, blank=True)
html_tag = getattr(settings, 'FOOTNOTE_TAG', 'div')
template_name = 'plugins/_footnote.html'
class Meta:
abstract = True
verbose_name = _("footnote")
verbose_name_plural = _("footnote")
def __str__(self):
return "[{}] {}".format(
self.index,
Truncator(strip_tags(self.richtext)).words(10, truncate=" ...")
)
class RichTextFootnoteMixin:
OO_FOOTNOTES = re.compile("<a.*?>(<sup>(.*?)</sup>)</a>")
MATCH_FOOTNOTES = re.compile("<sup>(\w+)</sup>")
def get_prepared_richtext(self, richtext):
# Find all footnotes and convert them into links
richtext = super().get_prepared_richtext(richtext)
richtext = self.OO_FOOTNOTES.subn('\g<1>', richtext)[0]
rv = self.MATCH_FOOTNOTES.subn(
'<sup id=\"back\g<1>\" class="footnote"><a href=\"#fn\g<1>\">\g<1></a></sup>',
richtext)[0]
return rv