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
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
|
|
|