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 feincms3.cleanse import CleansedRichTextField
from shared.utils.models.slugs import DowngradingSlugField
# TODO Rename ContentInlineBase to PluginInlineBase
from .admin import ContentInlineBase, RichTextInlineBase
from .plugins.mixins import StyleMixin
from . import USE_TRANSLATABLE_FIELDS
if USE_TRANSLATABLE_FIELDS:
from shared.multilingual.utils.fields import TranslatableCharField
from .fields import TranslatableCleansedRichTextField
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):
# TODO Cache inline
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):
template_name_prefix = None
template_name = None
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 self.template_name is None:
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):
if USE_TRANSLATABLE_FIELDS:
richtext = TranslatableCleansedRichTextField(_("text"), blank=True)
else:
richtext = CleansedRichTextField(_("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=" ...")
# TODO Rename to SectionBreakBase
class SectionBase(StyleMixin, FilesystemTemplateRendererPlugin):
if USE_TRANSLATABLE_FIELDS:
subheading = TranslatableCharField(_("subheading"), null=True, blank=True, max_length=500)
else:
subheading = models.CharField(_("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")
verbose_name_plural = _("section")
def __str__(self):
return Truncator(strip_tags(self.subheading)).words(10, truncate=" ...")
# FIXME Not need, 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
# ImageBase can't live here because image managment is different from project to project
# class ImageBase(StyleMixin, BasePlugin):
# image = models.ForeignKey(Image, on_delete=models.PROTECT)
# alt_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 render(self):
# template = """
#
#
#
#
# {caption_text}
#
#
# """
# # TOOD Assemble caption from image's captions if empty
# return mark_safe(template.format(
# src=self.image.figure_image.url,
# caption_text=mark_safe(self.alt_caption or "")
# ))
# TODO See comments on ImageBase; remove DownloadBase
class DownloadBase(StyleMixin, 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 = """
{name}
"""
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)
if USE_TRANSLATABLE_FIELDS:
richtext = TranslatableCleansedRichTextField(_("footnote text"), null=True, blank=True)
else:
richtext = CleansedRichTextField(_("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("((.*?))")
MATCH_FOOTNOTES = re.compile("(\w+)")
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(
'\" class="footnote">\">\g<1>',
richtext)[0]
return rv