combo/combo/data/models.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2585 lines
92 KiB
Python
Raw Normal View History

2014-12-07 15:34:30 +01:00
# combo - content management system
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
import datetime
2015-08-24 14:32:06 +02:00
import hashlib
import html
2015-02-18 14:15:31 +01:00
import json
import logging
import os
2018-01-18 09:11:31 +01:00
import re
import subprocess
import urllib.parse
2023-01-06 14:33:17 +01:00
import uuid
2015-02-18 14:15:31 +01:00
2015-08-24 14:32:06 +02:00
import feedparser
import requests
from django import forms, template
from django.apps import apps
2014-12-13 11:25:53 +01:00
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import Group
2020-02-17 10:07:57 +01:00
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
2015-02-18 14:15:31 +01:00
from django.core import serializers
2015-08-24 14:32:06 +02:00
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.db import models, transaction
from django.db.models import JSONField, Max, Q
from django.db.models.base import ModelBase
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
2014-12-13 11:25:53 +01:00
from django.forms import models as model_forms
from django.forms.widgets import MediaDefiningClass
from django.template import (
RequestContext,
Template,
TemplateDoesNotExist,
TemplateSyntaxError,
VariableDoesNotExist,
engines,
)
from django.test.client import RequestFactory
from django.urls import reverse
from django.utils import timezone
from django.utils.encoding import force_str, smart_bytes
2015-06-06 16:16:19 +02:00
from django.utils.html import strip_tags
2014-12-13 11:25:53 +01:00
from django.utils.safestring import mark_safe
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from combo import utils
from combo.apps.wcs.utils import get_wcs_matching_card_model, is_wcs_enabled
2015-08-24 14:32:06 +02:00
from combo.utils import NothingInCacheException
from .fields import RichTextField, TemplatableURLField
from .library import get_cell_class, get_cell_classes, register_cell_class
from .widgets import FlexSize
2021-04-06 21:31:24 +02:00
2014-12-07 15:34:30 +01:00
class PostException(Exception):
pass
def element_is_visible(element, user=None, ignore_superuser=False):
if user is not None and user.is_superuser and not ignore_superuser:
return True
if element.public:
if getattr(element, 'restricted_to_unlogged', None) is True:
return user is None or user.is_anonymous
return True
if user is None or user.is_anonymous:
return False
page_groups = element.groups.all()
if not page_groups:
groups_ok = True
else:
groups_ok = len(set(page_groups).intersection(user.groups.all())) > 0
if getattr(element, 'restricted_to_unlogged', None) is True:
return not (groups_ok)
return groups_ok
def django_template_validator(value):
try:
engines['django'].from_string(value)
except TemplateSyntaxError as e:
raise ValidationError(_('syntax error: %s') % e)
def format_sub_slug(sub_slug):
mapping = {}
if 'P<' not in sub_slug:
# simple sub_slug without regex
sub_slug = '(?P<%s>[a-z0-9]+)' % sub_slug
# search all named-groups in sub_slug
for i, m in enumerate(re.finditer(r'P<[\w_-]+>', sub_slug)):
# extract original name
original_group = m.group()[2:-1]
# rename it to remove all bad characters
new_group = 'g%i' % i
# update sub_slug
sub_slug = sub_slug.replace('<%s>' % original_group, '<%s>' % new_group)
# keep a mapping
mapping[new_group] = original_group
return sub_slug, mapping
def compile_sub_slug(sub_slug):
sub_slug = format_sub_slug(sub_slug)[0]
# will raise re.error if wrong regexp
re.compile(sub_slug)
def extract_context_from_sub_slug(sub_slug, sub_url):
sub_slug, mapping = format_sub_slug(sub_slug)
# match url
match = re.match('^' + sub_slug + '$', sub_url)
if match is None:
return
# return a dict with original group names
context = {mapping[k]: v for k, v in match.groupdict().items()}
# format also key to replace - by _
context.update({mapping[k].replace('-', '_'): v for k, v in match.groupdict().items()})
return context
class Placeholder:
def __init__(
self,
key,
name=None,
acquired=False,
optional=False,
render=True,
cell=None,
force_synchronous=False,
outer_tag=None,
):
self.key = key
self.name = name
self.acquired = acquired
self.optional = optional
self.render = render
self.cell = cell
self.force_synchronous = force_synchronous
if outer_tag is True:
outer_tag = 'div'
self.outer_tag = outer_tag
def get_name(self):
if self.cell:
return '%s / %s' % (self.cell.get_label(), self.name)
return self.name
class PageManager(models.Manager):
2018-04-01 15:23:01 +02:00
snapshots = False
def __init__(self, *args, **kwargs):
self.snapshots = kwargs.pop('snapshots', False)
super().__init__(*args, **kwargs)
def get_by_natural_key(self, uuid):
return self.get(uuid=uuid)
2018-04-01 15:23:01 +02:00
def get_queryset(self):
queryset = super().get_queryset()
if self.snapshots:
return queryset.filter(snapshot__isnull=False)
else:
return queryset.filter(snapshot__isnull=True)
2014-12-07 15:34:30 +01:00
class Page(models.Model):
objects = PageManager()
2018-04-01 15:23:01 +02:00
snapshots = PageManager(snapshots=True)
2023-01-06 14:33:17 +01:00
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
title = models.CharField(_('Title'), max_length=150)
slug = models.SlugField(_('Slug'))
sub_slug = models.CharField(
_('Sub Slug'),
max_length=150,
blank=True,
help_text=_(
2021-06-25 16:33:07 +02:00
'Context variable assigned to the path component, for example parking_id. '
'It is also possible to define this using a regular expression, '
'in that case variables will be provided according to named groups, '
'for example (?P&lt;year&gt;[0-9]{4}).'
2021-02-15 18:01:46 +01:00
),
)
2018-04-19 10:08:51 +02:00
description = models.TextField(_('Description'), blank=True)
template_name = models.CharField(_('Page Template'), max_length=50)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
order = models.PositiveIntegerField()
exclude_from_navigation = models.BooleanField(_('Exclude from navigation'), default=True)
redirect_url = models.CharField(_('Redirect URL'), max_length=200, blank=True)
2022-01-03 16:02:21 +01:00
extra_variables = JSONField(blank=True, default=dict)
placeholder_options = JSONField(blank=True, default=dict)
public = models.BooleanField(_('Public'), default=True)
groups = models.ManyToManyField(Group, verbose_name=_('Groups'), blank=True)
creation_timestamp = models.DateTimeField(default=now)
last_update_timestamp = models.DateTimeField(auto_now=True)
picture = models.ImageField(_('Picture'), upload_to='page-pictures/', null=True)
edit_role = models.ForeignKey(
Group,
blank=True,
null=True,
default=None,
related_name='+',
verbose_name=_('Edit Role'),
on_delete=models.SET_NULL,
)
subpages_edit_role = models.ForeignKey(
Group,
blank=True,
null=True,
default=None,
related_name='+',
verbose_name=_('Subpages Edit Role'),
on_delete=models.SET_NULL,
)
2018-04-01 15:23:01 +02:00
# mark temporarily restored snapshots, it is required to save objects
# (pages and cells) for real for viewing past snapshots as many cells are
# asynchronously loaded and must refer to a real Page object.
snapshot = models.ForeignKey(
'PageSnapshot', on_delete=models.CASCADE, null=True, related_name='temporary_page'
)
# keep a cached list of cell types that are used in the page.
related_cells = JSONField(blank=True, default=dict)
_level = None
class Meta:
ordering = ['order']
2014-12-13 13:19:34 +01:00
def __str__(self):
return str(self.title)
2014-12-07 15:34:30 +01:00
def natural_key(self):
return (str(self.uuid),)
@classmethod
@utils.cache_during_request
def get_page_ids_by_uuids(cls):
return {str(page.uuid): page.pk for page in cls.objects.only('pk', 'uuid')}
def picture_extension(self):
if not self.picture:
return None
return os.path.splitext(self.picture.name)[-1]
def get_sub_slug_details(self):
if not self.sub_slug:
return
if not is_wcs_enabled(None):
return self.sub_slug, None
result = get_wcs_matching_card_model(self.sub_slug)
if not result:
return self.sub_slug, None
return self.sub_slug.replace('-', '_'), result[1]
def save(self, *args, **kwargs):
if 'update_fields' in kwargs:
return super().save(*args, **kwargs)
if not self.id:
self.related_cells = {'cell_types': []}
if self.order is None:
max_order = Page.objects.all().aggregate(Max('order')).get('order__max') or 0
self.order = max_order + 1
if not self.slug:
if not Page.objects.exists():
slug = 'index'
else:
base_slug = slugify(self.title)[:40]
slug = base_slug.strip('-')
i = 1
while Page.objects.filter(slug=slug, parent_id=self.parent_id).exists():
i += 1
slug = '%s-%s' % (base_slug, i)
self.slug = slug
if not self.template_name:
self.template_name = settings.COMBO_DEFAULT_PUBLIC_TEMPLATE
return super().save(*args, **kwargs)
def get_parents_and_self(self):
pages = [self]
page = self
while page.parent_id:
page = page._parent if hasattr(page, '_parent') else page.parent
pages.append(page)
return list(reversed(pages))
def get_online_url(self, follow_redirection=True):
if (
follow_redirection
and self.redirect_url
and not (utils.is_templated_url(self.redirect_url) or self.redirect_url.startswith('.'))
):
return self.redirect_url
parts = [x.slug for x in self.get_parents_and_self()]
if parts[0] == 'index':
parts = parts[1:]
if not parts:
2015-03-17 16:53:22 +01:00
return '/'
return '/' + '/'.join(parts) + '/'
def get_full_path_titles(self):
parts = [x.title for x in self.get_parents_and_self()]
return ' / '.join(parts)
2015-01-02 20:56:09 +01:00
def get_page_of_level(self, level):
'''Return page of given level in the page hierarchy.'''
parts = self.get_parents_and_self()
2015-01-02 20:56:09 +01:00
try:
return parts[level]
except IndexError:
return None
def get_siblings(self):
if hasattr(self, '_parent'):
if self._parent:
return self._parent._children
return Page.objects.filter(parent_id=self.parent_id)
2015-01-02 20:56:09 +01:00
def get_children(self):
if hasattr(self, '_children'):
return self._children
return Page.objects.filter(parent_id=self.id)
def has_children(self):
if hasattr(self, '_children'):
return bool(self._children)
return Page.objects.filter(parent_id=self.id).exists()
def get_descendants(self, include_myself=False):
def get_descendant_pages(page, include_page=True):
if include_page:
descendants = [page]
else:
descendants = []
for item in page.get_children():
descendants.extend(get_descendant_pages(item))
return descendants
2021-02-15 18:01:46 +01:00
return Page.objects.filter(
id__in=[x.id for x in get_descendant_pages(self, include_page=include_myself)]
)
def get_descendants_and_me(self):
return self.get_descendants(include_myself=True)
def get_template_display_name(self):
try:
return settings.COMBO_PUBLIC_TEMPLATES[self.template_name]['name']
except KeyError:
return _('Unknown (%s)') % self.template_name
def missing_template(self):
template_name = settings.COMBO_PUBLIC_TEMPLATES.get(self.template_name, {}).get('template')
if not template_name:
return True
try:
template.loader.select_template([template_name])
except TemplateDoesNotExist:
return True
return False
def is_editable(self, user):
if user.has_perm('data.change_page'):
return True
group_ids = [x.id for x in user.groups.all()]
if self.edit_role_id in group_ids:
return True
hierarchy = self.get_parents_and_self()
for page in hierarchy:
if page.subpages_edit_role_id in group_ids:
return True
return False
def get_placeholders(self, request, traverse_cells=False, template_name=None):
placeholders = []
page_template = settings.COMBO_PUBLIC_TEMPLATES.get(template_name or self.template_name, {})
if page_template.get('placeholders'):
# manual declaration
for key, options in page_template['placeholders'].items():
placeholders.append(Placeholder(key=key, **options))
return placeholders
template_names = []
if page_template.get('template'):
template_names.append(page_template['template'])
template_names.append('combo/page_template.html')
tmpl = template.loader.select_template(template_names)
request = RequestFactory(SERVER_NAME=request.get_host()).get(self.get_online_url())
request.user = None
context = {
'page': self,
'request': request,
'synchronous': True,
'placeholder_search_mode': True,
'placeholders': placeholders,
'traverse_cells': traverse_cells,
}
tmpl.render(context, request)
return placeholders
def get_next_page(self, user=None, check_visibility=True, **kwargs):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all(), **kwargs)
this_page = [x for x in pages if x.id == self.id][0]
pages = pages[pages.index(this_page) + 1 :]
for page in pages:
if not check_visibility or page.is_visible(user):
return page
return None
def get_previous_page(self, user=None, check_visibility=True, **kwargs):
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all(), **kwargs)
pages.reverse()
this_page = [x for x in pages if x.id == self.id][0]
pages = pages[pages.index(this_page) + 1 :]
for page in pages:
if not check_visibility or page.is_visible(user):
return page
return None
@classmethod
def get_as_reordered_flat_hierarchy(cls, object_list, root_page=None, follow_user_perms=None):
# create a list of [(page.order, page.id, page), (subpage.order, subpage.id, subpage),
# (subsubpage.order, subpage.id, subsubpage)] and sort it to get the page hierarchy
# as a flat list.
# follow_user_perms can be None or a User object, in that case only pages that are
# editable by user will be returned.
all_pages = {}
for page in object_list:
all_pages[page.id] = page
pages_hierarchy = []
for page in object_list:
page_hierarchy = [(page.order, page.id, page)]
parent_id = page.parent_id
while parent_id and parent_id in all_pages:
parent_page = all_pages[parent_id]
page_hierarchy.append((parent_page.order, parent_page.id, parent_page))
parent_id = parent_page.parent_id
page_hierarchy.reverse()
page.level = len(page_hierarchy) - 1
pages_hierarchy.append(page_hierarchy)
group_ids = None # None = do not pay attention to groups
if follow_user_perms and not follow_user_perms.has_perm('data.change_page'):
group_ids = [x.id for x in follow_user_perms.groups.all()]
pages_hierarchy.sort()
if group_ids is not None:
# remove pages the user cannot see/edit
pages_hierarchy = [
x
for x in pages_hierarchy
if x[-1][-1].edit_role_id in group_ids
or any(y[-1].subpages_edit_role_id in group_ids for y in x[:-1])
]
# adjust levels to have shallowest level at 0
seen_pages = {} # page_id -> page_level
for page_hierarchy in pages_hierarchy:
_, page_id, page = page_hierarchy[-1]
if page.parent_id in seen_pages:
# parent page is displayed, adjust level according to it
page.level = seen_pages[page.parent_id] + 1
else:
# page with no parent displayed, set it at root level
page.level = 0
seen_pages[page_id] = page.level
return [x[-1][-1] for x in pages_hierarchy]
@staticmethod
@utils.cache_during_request
def get_with_hierarchy_attributes():
pages = Page.objects.all()
pages_by_id = {}
for page in pages:
pages_by_id[page.id] = page
page._parent = None
page._children = []
for page in pages:
page._parent = pages_by_id[page.parent_id] if page.parent_id else None
if page._parent:
page._parent._children.append(page)
for page in pages:
page._children.sort(key=lambda x: x.order)
return pages_by_id
def visibility(self):
if self.public:
return _('Public')
groups = self.groups.all()
groupnames = ', '.join([x.name for x in groups]) if groups else _('logged users')
return _('Private (%s)') % groupnames
def is_visible(self, user=None, ignore_superuser=False):
return element_is_visible(self, user=user, ignore_superuser=ignore_superuser)
def extra_labels(self):
extra_labels = []
if not self.exclude_from_navigation:
extra_labels.append(_('navigation'))
if self.redirect_url:
extra_labels.append(_('redirection'))
return extra_labels
2015-06-06 16:16:19 +02:00
def get_cells(self):
return CellBase.get_cells(page=self)
def build_cell_cache(self):
cell_classes = get_cell_classes()
cell_types = set()
for klass in cell_classes:
if klass is None:
continue
if klass.objects.filter(page=self).exists():
cell_types.add(klass.get_cell_type_str())
if cell_types != set(self.related_cells.get('cell_types', [])):
self.related_cells['cell_types'] = list(cell_types)
self.save(update_fields=['related_cells', 'last_update_timestamp'])
2015-06-06 16:16:19 +02:00
2015-02-18 14:15:31 +01:00
def get_serialized_page(self):
cells = [x for x in self.get_cells() if x.placeholder and not x.placeholder.startswith('_')]
serialized_page = json.loads(
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
2021-02-15 18:01:46 +01:00
)
)[0]
2015-02-18 14:15:31 +01:00
del serialized_page['model']
2018-04-01 15:23:01 +02:00
if 'snapshot' in serialized_page:
del serialized_page['snapshot']
if 'related_cells' in serialized_page['fields']:
del serialized_page['fields']['related_cells']
serialized_page['cells'] = CellBase.get_serialized_cells(cells)
serialized_page['fields']['groups'] = [x[0] for x in serialized_page['fields']['groups']]
2015-02-18 14:15:31 +01:00
for cell in serialized_page['cells']:
del cell['pk']
del cell['fields']['page']
cell['fields']['groups'] = [x[0] for x in cell['fields']['groups']]
for key in list(cell['fields'].keys()):
if key.startswith('cached_'):
del cell['fields'][key]
2015-02-18 14:15:31 +01:00
return serialized_page
2015-02-18 15:48:44 +01:00
@classmethod
def load_serialized_page(cls, json_page, page=None, snapshot=None, request=None):
2015-02-18 15:48:44 +01:00
json_page['model'] = 'data.page'
json_page['fields']['groups'] = [[x] for x in json_page['fields']['groups'] if isinstance(x, str)]
created = None
if page is None:
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take uuid from snapshot: it has to be unique !
else:
qs_kwargs = {'uuid': json_page['fields']['uuid']}
page, created = Page.objects.get_or_create(**qs_kwargs)
2015-02-18 15:48:44 +01:00
json_page['pk'] = page.id
parent_uuid = json_page['fields'].get('parent') or []
if parent_uuid and not Page.objects.filter(uuid=parent_uuid[0]).exists():
# parent not found, remove it and exclude page from navigation
json_page['fields'].pop('parent')
json_page['fields']['exclude_from_navigation'] = True
if request:
messages.warning(
request,
_(
'Unknown parent for page "%s"; parent has been reset and page was excluded from navigation.'
)
% json_page['fields']['title'],
2021-02-15 18:01:46 +01:00
)
page_uuid = page.uuid
page = next(serializers.deserialize('json', json.dumps([json_page]), ignorenonexistent=True))
2018-04-01 15:23:01 +02:00
page.object.snapshot = snapshot
if snapshot:
# keep the generated uuid
page.object.uuid = page_uuid
2015-02-18 15:48:44 +01:00
page.save()
for cell in json_page.get('cells'):
cell['fields']['groups'] = [[x] for x in cell['fields']['groups'] if isinstance(x, str)]
cell['fields']['page'] = page.object.id
2015-02-18 15:48:44 +01:00
# if there were cells, remove them
# if page was created, do nothing
if created is False:
for klass in get_cell_classes():
if klass is None:
continue
klass.objects.filter(page=page.object).delete()
2018-04-01 15:23:01 +02:00
return page.object # get page out of deserialization object
2015-02-18 15:48:44 +01:00
@classmethod
def load_serialized_cells(cls, cells):
2015-02-18 15:48:44 +01:00
# load new cells
for cell_data in cells:
model = apps.get_model(cell_data['model'])
cell_data = model.prepare_serialized_data(cell_data)
cell = list(serializers.deserialize('json', json.dumps([cell_data]), ignorenonexistent=True))[0]
2015-02-18 15:48:44 +01:00
cell.save()
# will populate cached_* attributes
cell.object.save()
cell.object.import_subobjects(cell_data)
2015-02-18 15:48:44 +01:00
@classmethod
def load_serialized_pages(cls, json_site, request=None):
cells_to_load = []
to_load = []
imported_pages = []
try:
post_save.disconnect(cell_maintain_page_cell_cache)
post_delete.disconnect(cell_maintain_page_cell_cache)
for json_page in json_site:
# pre-create pages
page, created = Page.objects.get_or_create(uuid=json_page['fields']['uuid'])
to_load.append((page, created, json_page))
# delete cells of already existing pages
to_clean = [p for p, created, j in to_load if not created]
for klass in get_cell_classes():
if klass is None:
continue
klass.objects.filter(page__in=to_clean).delete()
# now load pages
for (page, created, json_page) in to_load:
imported_pages.append(cls.load_serialized_page(json_page, page=page, request=request))
cells_to_load.extend(json_page.get('cells'))
# and cells
cls.load_serialized_cells(cells_to_load)
finally:
post_save.connect(cell_maintain_page_cell_cache)
post_delete.connect(cell_maintain_page_cell_cache)
# build cache
for page in imported_pages:
page.build_cell_cache()
2015-02-18 15:48:44 +01:00
return imported_pages
@classmethod
def export_all_for_json(cls):
ordered_pages = Page.get_as_reordered_flat_hierarchy(cls.objects.all())
return [x.get_serialized_page() for x in ordered_pages]
def get_redirect_url(self, context=None):
return utils.get_templated_url(self.redirect_url, context=context)
def get_last_update_time(self):
return self.last_update_timestamp
2022-01-03 16:02:21 +01:00
def get_extra_variables(self, request, original_context):
result = {}
context = RequestContext(request)
context.push(original_context)
for key, tplt in (self.extra_variables or {}).items():
try:
result[key] = Template(tplt).render(context)
except (TemplateSyntaxError, VariableDoesNotExist):
2022-01-03 16:02:21 +01:00
continue
return result
def get_extra_variables_keys(self):
return sorted((self.extra_variables or {}).keys())
def is_new(self):
return self.creation_timestamp > timezone.now() - datetime.timedelta(days=7)
def duplicate(self, title=None, parent=False):
2019-10-22 14:58:02 +02:00
# clone current page
new_page = copy.deepcopy(self)
new_page.pk = None
# set title
new_page.title = title or _('Copy of %s') % self.title
# reset slug
new_page.slug = None
# reset uuid
new_page.uuid = uuid.uuid4()
2019-10-22 14:58:02 +02:00
# reset snapshot
new_page.snapshot = None
# set order
new_page.order = self.order + 1
# exclude from navigation
new_page.exclude_from_navigation = True
# set parent
if parent is not False:
# it can be None
new_page.parent = parent
2019-10-22 14:58:02 +02:00
# store new page
new_page.save()
# set groups
new_page.groups.set(self.groups.all())
for cell in self.get_cells():
if cell.placeholder and cell.placeholder.startswith('_'):
continue
2019-10-22 14:58:02 +02:00
cell.duplicate(page_target=new_page)
return new_page
2015-02-18 15:48:44 +01:00
2018-04-01 15:23:01 +02:00
class PageSnapshot(models.Model):
page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
2018-04-01 15:23:01 +02:00
comment = models.TextField(blank=True, null=True)
serialization = JSONField(blank=True, default=dict)
label = models.CharField(_('Label'), max_length=150, blank=True)
2018-04-01 15:23:01 +02:00
class Meta:
ordering = ('-timestamp',)
@classmethod
def take(cls, page, request=None, comment=None, deletion=False, label=None):
snapshot = cls(page=page, comment=comment, label=label or '')
if request and not request.user.is_anonymous:
2018-04-01 15:23:01 +02:00
snapshot.user = request.user
if not deletion:
snapshot.serialization = page.get_serialized_page()
else:
snapshot.serialization = {}
snapshot.comment = comment or _('deletion')
snapshot.save()
def get_page(self):
try:
# try reusing existing page
return Page.snapshots.get(snapshot=self)
except Page.DoesNotExist:
return self.load_page(self.serialization, snapshot=self)
2018-04-01 15:23:01 +02:00
def restore(self):
json_page = self.serialization
# keep current page uuid
json_page['fields']['uuid'] = str(self.page.uuid)
# keep current page order
json_page['fields']['order'] = self.page.order
# and current parent
json_page['fields']['parent'] = self.page.parent.natural_key() if self.page.parent else None
# and current exclude_from_navigation value
json_page['fields']['exclude_from_navigation'] = self.page.exclude_from_navigation
# restore snapshot
with transaction.atomic():
return self.load_page(json_page)
def load_page(self, json_page, snapshot=None):
try:
post_save.disconnect(cell_maintain_page_cell_cache)
post_delete.disconnect(cell_maintain_page_cell_cache)
page = Page.load_serialized_page(json_page, snapshot=snapshot)
page.load_serialized_cells(json_page['cells'])
finally:
post_save.connect(cell_maintain_page_cell_cache)
post_delete.connect(cell_maintain_page_cell_cache)
page.build_cell_cache()
return page
2018-04-01 15:23:01 +02:00
class Redirect(models.Model):
old_url = models.CharField(max_length=512)
page = models.ForeignKey(Page, on_delete=models.CASCADE)
creation_timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('creation_timestamp',)
2020-02-17 10:07:57 +01:00
class ValidityInfo(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
invalid_reason_code = models.CharField(max_length=100, blank=True, null=True, editable=False)
invalid_since = models.DateTimeField(blank=True, null=True, editable=False)
class Meta:
unique_together = [('content_type', 'object_id')]
@property
def invalid_datetime(self):
if not self.invalid_since:
return
return self.invalid_since + datetime.timedelta(days=2)
2020-02-17 10:07:57 +01:00
class CellMeta(MediaDefiningClass, ModelBase):
pass
2014-12-07 15:34:30 +01:00
class CellBase(models.Model, metaclass=CellMeta):
# noqa pylint: disable=too-many-public-methods
page = models.ForeignKey(Page, on_delete=models.CASCADE)
placeholder = models.CharField(max_length=20)
2014-12-07 15:34:30 +01:00
order = models.PositiveIntegerField()
slug = models.SlugField(_('Slug'), blank=True)
extra_css_class = models.CharField(_('Extra classes for CSS styling'), max_length=100, blank=True)
template_name = models.CharField(_('Cell Template'), max_length=50, blank=True, null=True)
condition = models.CharField(_('Display condition'), max_length=1000, blank=True, null=True)
2014-12-07 15:34:30 +01:00
public = models.BooleanField(_('Public'), default=True)
# restricted_to_unlogged is actually an invert switch, it is used for mark
# a cell as only visibile to unlogged users but also, when groups are set,
# to mark the cell as visible to all but those groups.
restricted_to_unlogged = models.BooleanField(_('Restrict to unlogged users'), default=False)
groups = models.ManyToManyField(Group, verbose_name=_('Groups'), blank=True)
last_update_timestamp = models.DateTimeField(auto_now=True)
2020-02-17 10:07:57 +01:00
validity_info = GenericRelation(ValidityInfo)
invalid_reason_codes = {}
2020-02-17 10:07:57 +01:00
2014-12-13 11:25:53 +01:00
default_form_class = None
manager_form_factory_kwargs = {}
manager_form_template = 'combo/cell_form.html'
manager_visibility_template = 'combo/cell_visibility.html'
manager_appearance_template = 'combo/cell_appearance.html'
children_placeholder_prefix = None
visible = True
user_dependant = False
default_template_name = None
max_one_by_page = False
session_required = False
2014-12-13 11:25:53 +01:00
# get_badge(self, context); set to None so cell types can be skipped easily
get_badge = None
# message displayed when the cell is loaded asynchronously
loading_message = _('Loading...')
# modify_global_context(self, context, request=None)
# Apply changes to the template context that must visible to all cells in the page
modify_global_context = None
# if set, automatically refresh cell every n seconds
ajax_refresh = None
class Meta:
abstract = True
def __str__(self):
label = self.get_verbose_name()
2015-01-17 17:06:07 +01:00
additional_label = self.get_additional_label()
if label and additional_label:
return '%s (%s)' % (label, re.sub(r'\r?\n', ' ', force_str(additional_label)))
2015-01-17 17:06:07 +01:00
else:
return force_str(label)
2015-01-17 17:06:07 +01:00
2014-12-08 17:06:57 +01:00
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name
2015-01-17 17:06:07 +01:00
def get_additional_label(self):
return ''
def get_ajax_url(self):
return reverse(
'combo-public-ajax-page-cell',
kwargs={'page_pk': self.page_id, 'cell_reference': self.get_reference()},
)
def get_manager_visibility_css_class(self):
if self.public:
return 'visibility-all' if not self.restricted_to_unlogged else ''
elif self.restricted_to_unlogged:
return 'visibility-off'
return ''
def get_manager_visibility_content(self):
if self.public and not self.restricted_to_unlogged:
return ''
group_names = ', '.join([x.name for x in self.groups.all()])
if group_names:
return group_names
return _('unlogged users') if self.restricted_to_unlogged else _('logged users')
@property
def legacy_class_name(self):
# legacy class name used in some themes
return self.__class__.__name__.lower()
@property
def class_name(self):
# convert CamelCase Python class name into a lower and dashed version
# appropriate for CSS
return re.sub('([A-Z]+)', r'-\1', self.__class__.__name__).lower()[1:]
@property
def css_class_names(self):
return ' '.join(
[
self.class_name,
self.legacy_class_name,
self.get_template_extra_css_classes(),
self.extra_css_class,
]
)
@property
def cleaned_extra_css_class(self):
return ' '.join([x for x in self.extra_css_class.split() if not x.startswith('size--')])
@classmethod
@utils.cache_during_request
def get_assets_by_key(cls):
from combo.apps.assets.models import Asset
return {a.key: a for a in Asset.objects.all()}
@property
def asset_css_classes(self):
if not hasattr(self, '_asset_keys'):
self._asset_keys = self.get_asset_slot_keys()
if not hasattr(self, '_assets'):
all_assets = CellBase.get_assets_by_key()
self._assets = {key: all_assets.get(key) for key in self._asset_keys.keys()}
self._assets = {k: v for k, v in self._assets.items() if v}
if not self._asset_keys or not self._assets:
return ''
# add has-asset-<slug> for each asset found
css = sorted(f'has-asset-{v}' for k, v in self._asset_keys.items() if k in self._assets)
# add has-any-asset if at least one asset defined
if self._assets:
css.append('has-any-asset')
# add has-all-assets if all assets are defined
if self._assets.keys() == self._asset_keys.keys():
css.append('has-all-assets')
return ' '.join(css)
def can_have_assets(self):
return self.get_slug_for_asset() and self.get_asset_slot_templates()
def get_slug_for_asset(self):
return self.slug
def get_label_for_asset(self):
return _('%(cell_label)s on page %(page_name)s (%(page_slug)s)') % {
'cell_label': str(self),
'page_name': str(self.page),
'page_slug': self.page.slug,
}
def get_asset_slot_key(self, key):
return 'cell:%s:%s:%s' % (self.get_cell_type_str(), key, self.get_slug_for_asset())
def get_asset_slot_templates(self):
return settings.COMBO_CELL_ASSET_SLOTS.get(self.get_cell_type_str()) or {}
def get_asset_slot_keys(self):
if not self.can_have_assets():
return {}
slot_templates = self.get_asset_slot_templates()
return {self.get_asset_slot_key(key): key for key in slot_templates.keys()}
def get_asset_slots(self):
if not self.can_have_assets():
return {}
slot_templates = self.get_asset_slot_templates()
slots = {}
for slot_template_key, slot_template_data in slot_templates.items():
suffix = ''
if slot_template_data.get('suffix'):
suffix = ' (%s)' % slot_template_data['suffix']
slot_key = self.get_asset_slot_key(slot_template_key)
label = '%(prefix)s%(label)s%(suffix)s' % {
'prefix': slot_template_data['prefix'],
'label': self.get_label_for_asset(),
'suffix': suffix,
}
short_label = '%(prefix)s%(suffix)s' % {'prefix': slot_template_data['prefix'], 'suffix': suffix}
slots[slot_key] = {
'label': label,
'short_label': short_label,
}
slots[slot_key].update(slot_template_data)
return slots
@classmethod
def get_cell_classes(cls, class_filter=lambda x: True):
for klass in get_cell_classes():
if not class_filter(klass):
continue
if not klass.is_enabled():
continue
if klass.visible is False:
continue
yield klass
@classmethod
def get_all_cell_types(cls, *args, **kwargs):
cell_types = []
for klass in cls.get_cell_classes(*args, **kwargs):
cell_types.extend(klass.get_cell_types())
return cell_types
@classmethod
def get_cells(
cls,
cell_filter=None,
skip_cell_cache=False,
prefetch_validity_info=False,
prefetch_groups=False,
select_related=None,
load_contenttypes=False,
cells_exclude=None,
get_all_objects=False,
**kwargs,
):
"""Returns the list of cells of various classes matching **kwargs"""
cells = []
pages = []
2020-02-18 13:55:18 +01:00
select_related = select_related or {}
if 'page' in kwargs:
pages = [kwargs['page']]
elif 'page__in' in kwargs:
pages = kwargs['page__in']
else:
# if there are not explicit page, limit to non-snapshot pages
kwargs['page__snapshot__isnull'] = True
cell_classes = get_cell_classes()
if pages and not skip_cell_cache:
# if there's a request for some specific pages, limit cell types
# to those that are actually in use in those pages.
cell_types = set()
for page in pages:
if page.related_cells and 'cell_types' in page.related_cells:
cell_types |= set(page.related_cells['cell_types'])
else:
break
else:
cell_classes = [get_cell_class(x) for x in cell_types]
if load_contenttypes:
# populate ContentType cache
ContentType.objects.get_for_models(*cell_classes)
extra_filter = kwargs.pop('extra_filter', None)
for klass in cell_classes:
if klass is None:
continue
if cell_filter and not cell_filter(klass):
continue
manager = klass.objects
if get_all_objects and hasattr(klass, 'all_objects'):
manager = klass.all_objects
cells_queryset = manager.filter(**kwargs)
if cells_exclude:
cells_queryset = cells_queryset.exclude(cells_exclude)
if extra_filter:
cells_queryset = cells_queryset.filter(extra_filter)
if prefetch_groups:
cells_queryset = cells_queryset.prefetch_related('groups')
2020-02-18 13:55:18 +01:00
if select_related:
cells_queryset = cells_queryset.select_related(
*select_related.get('__all__', []), *select_related.get(klass.get_cell_type_str(), [])
2021-02-15 18:01:46 +01:00
)
2020-02-18 13:55:18 +01:00
cells.extend(cells_queryset)
if prefetch_validity_info:
validity_info_list = list(ValidityInfo.objects.select_related('content_type'))
for cell in cells:
cell.prefetched_validity_info = [
v
for v in validity_info_list
if v.object_id == cell.pk and v.content_type.model_class() == cell.__class__
]
cells.sort(key=lambda x: x.order)
return cells
def get_reference(self):
"Returns a string that can serve as a unique reference to a cell" ""
return str('%s-%s' % (self.get_cell_type_str(), self.id))
@classmethod
def get_cell(cls, reference, **kwargs):
"""Returns the cell matching reference, and eventual **kwargs"""
content_id, cell_id = reference.split('-')
klass = get_cell_class(content_id)
if klass is None:
raise ObjectDoesNotExist()
return klass.objects.get(id=cell_id, **kwargs)
2014-12-13 11:25:53 +01:00
@classmethod
def get_cell_type_str(cls):
return '%s_%s' % (cls._meta.app_label, cls._meta.model_name)
@classmethod
def get_cell_type_group(cls):
return apps.get_app_config(cls._meta.app_label).verbose_name
@classmethod
def get_cell_types(cls):
2014-12-13 11:25:53 +01:00
return [
{
'name': cls.get_verbose_name(),
'cell_type_str': cls.get_cell_type_str(),
'group': cls.get_cell_type_group(),
2014-12-13 11:25:53 +01:00
'variant': 'default',
'order': 0,
'max_one_by_page': cls.max_one_by_page,
2014-12-13 11:25:53 +01:00
}
]
@classmethod
def is_enabled(cls):
"""Defines if the cell type is enabled for the given site; this is used
to selectively enable cells from extension modules."""
return True
2014-12-13 11:25:53 +01:00
def set_variant(self, variant):
pass
def get_label(self):
return self.get_verbose_name()
def get_manager_tabs(self):
from combo.manager.forms import CellVisibilityForm
tabs = []
form_class = self.get_default_form_class()
if form_class:
tabs.append(
{
'slug': 'general',
'name': _('General'),
'template': self.manager_form_template,
'form': form_class,
}
)
tabs.append(
{
'slug': 'visibility',
'name': _('Visibility'),
'template': self.manager_visibility_template,
'form': CellVisibilityForm,
}
)
tabs.append(
{
'slug': 'appearance',
'name': _('Appearance'),
'template': self.manager_appearance_template,
'form': self.get_appearance_form_class(),
}
)
return tabs
def get_default_form_fields(self):
return [
x.name
for x in self._meta.local_concrete_fields
if x.name
not in (
'id',
'page',
'placeholder',
'order',
'public',
'groups',
'slug',
'extra_css_class',
'last_update_timestamp',
'restricted_to_unlogged',
'template_name',
'condition',
2021-02-15 18:01:46 +01:00
)
+ tuple(self.get_appearance_fields())
]
def get_default_form_class(self, fields=None):
if self.default_form_class:
return self.default_form_class
2014-12-13 11:25:53 +01:00
if not fields:
fields = self.get_default_form_fields()
if not fields:
return None
2014-12-13 11:25:53 +01:00
return model_forms.modelform_factory(
self.__class__, fields=fields, **self.manager_form_factory_kwargs
)
2014-12-13 11:25:53 +01:00
def get_appearance_fields(self):
return ['title', 'custom_title']
2022-08-11 21:28:06 +02:00
def get_appearance_form_class(self, base_options_form_class=None):
model_fields = {field.name for field in self._meta.local_concrete_fields}
fields = [field for field in self.get_appearance_fields() if field in model_fields] + [
'slug',
'extra_css_class',
]
widgets = None
extra_templates = settings.COMBO_CELL_TEMPLATES.get(self.get_cell_type_str())
if extra_templates:
fields = ['template_name'] + fields
template_names = [('', _('Default Value'))] + [
(k, v['label']) for k, v in extra_templates.items()
]
widgets = {'template_name': forms.Select(choices=template_names)}
page = self.page
cell = self
2022-08-11 21:28:06 +02:00
class OptionsForm(base_options_form_class or model_forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if page.placeholder_options.get(cell.placeholder, {}).get('fx_grid_layout'):
# add a size field that takes/stores its value in the extra_css_class
# char field.
self.fields['fx_size'] = forms.ChoiceField(
label=_('Size'),
choices=[
('', ''),
('size--1-1', '1'),
('size--t1-2', '½'),
('size--t1-3', ''),
('size--t1-4', '¼'),
('size--t1-5', ''),
('size--t1-6', ''),
('size--t2-3', ''),
('size--t2-5', ''),
('size--t3-4', '¾'),
('size--t3-5', ''),
('size--t4-5', ''),
('size--t5-6', ''),
],
required=False,
widget=FlexSize,
)
# move extra_css_class field to be last
field_order = list(self.fields.keys())
field_order.remove('extra_css_class')
field_order.append('extra_css_class')
self.order_fields(field_order)
extra_css_class = self.initial['extra_css_class'].split()
for css_class, _dummy in self.fields['fx_size'].choices:
if css_class in extra_css_class:
extra_css_class.remove(css_class)
self.initial['extra_css_class'] = ' '.join(extra_css_class)
self.initial['fx_size'] = css_class
break
def save(self, *args, **kwargs):
if self.cleaned_data.get('fx_size'):
self.instance.extra_css_class = (
'%s %s'
% (self.instance.extra_css_class or '', self.cleaned_data.get('fx_size') or '')
).strip()
return super().save(*args, **kwargs)
return model_forms.modelform_factory(self.__class__, form=OptionsForm, fields=fields, widgets=widgets)
def get_extra_manager_context(self):
return {}
def mark_as_invalid(self, reason_code, force=True):
validity_info, created = ValidityInfo.objects.get_or_create(
content_type=ContentType.objects.get_for_model(self),
object_id=self.pk,
defaults={
'invalid_reason_code': reason_code,
'invalid_since': now(),
},
)
if created:
return
if not force and validity_info.invalid_since is not None:
# don't overwrite invalid reason already set
return
if validity_info.invalid_reason_code == reason_code:
# don't overwrite invalid_since if same reason already set
return
validity_info.invalid_reason_code = reason_code
validity_info.invalid_since = now()
validity_info.save()
def mark_as_valid(self):
validity_info = self.get_validity_info()
if validity_info is None:
return
validity_info.delete()
def get_validity_info(self):
if hasattr(self, 'prefetched_validity_info'):
if not self.prefetched_validity_info:
return
return self.prefetched_validity_info[0]
return self.validity_info.all().first()
def set_validity_from_url(self, resp, not_found_code='url_not_found', invalid_code='url_invalid'):
if resp is None:
# can not retrieve data, don't report cell as invalid
self.mark_as_valid()
elif resp.status_code == 404:
self.mark_as_invalid(not_found_code)
elif 400 <= resp.status_code < 500:
# 4xx error, cell is invalid
self.mark_as_invalid(invalid_code)
else:
# 2xx or 3xx: cell is valid
# 5xx error: can not retrieve data, don't report cell as invalid
self.mark_as_valid()
def get_invalid_reason(self):
validity_info = self.get_validity_info()
if validity_info is None:
return
if not validity_info.invalid_since:
return
return self.invalid_reason_codes.get(
validity_info.invalid_reason_code, validity_info.invalid_reason_code
)
def is_placeholder_active(self):
if not self.placeholder:
return False
if self.placeholder.startswith('_'):
return True
request = RequestFactory().get('/')
if not hasattr(self.page, '_placeholders'):
self.page._placeholders = self.page.get_placeholders(request, traverse_cells=True)
for placeholder in self.page._placeholders:
if placeholder.key == self.placeholder:
return True
return False
def is_hidden_because_invalid(self):
validity_info = self.get_validity_info()
return (
validity_info is not None
and validity_info.invalid_datetime
and validity_info.invalid_datetime <= now()
)
def compute_condition(self, request, original_context):
condition = self.condition
if not condition:
return True
context = RequestContext(request)
context.push(original_context)
try:
return Template('{%% if %s %%}OK{%% endif %%}' % condition).render(context) == 'OK'
except (AttributeError, TemplateSyntaxError, VariableDoesNotExist):
return False
def is_visible(self, request, context=None, check_validity_info=True):
if context:
condition = self.compute_condition(request=request, original_context=context)
if not condition:
return False
if check_validity_info and self.is_hidden_because_invalid():
return False
return element_is_visible(self, user=getattr(request, 'user', None))
def is_relevant(self, context):
"""Return whether it's relevant to render this cell in the page
context."""
return True
def is_user_dependant(self, context=None):
'''Return whether the cell content varies from user to user.'''
return self.user_dependant
def get_concerned_user(self, context):
'''Return user from UserSearch cell, or connected user.'''
return context.get('selected_user') or getattr(context.get('request'), 'user', None)
def get_cell_extra_context(self, context):
return {'cell': self}
def get_template_label(self):
cell_templates = settings.COMBO_CELL_TEMPLATES.get(self.get_cell_type_str()) or {}
selected_template_infos = cell_templates.get(self.template_name) or {}
return selected_template_infos.get('label')
def get_template_extra_css_classes(self):
cell_templates = settings.COMBO_CELL_TEMPLATES.get(self.get_cell_type_str()) or {}
selected_template_infos = cell_templates.get(self.template_name) or {}
return selected_template_infos.get('extra-css-classes') or ''
def render(self, context):
context.update(self.get_cell_extra_context(context))
template_names = ['combo/' + self._meta.model_name + '.html']
base_template_name = self._meta.model_name + '.html'
if self.default_template_name:
base_template_name = os.path.basename(self.default_template_name)
template_names.append(self.default_template_name)
if self.template_name:
cell_templates = settings.COMBO_CELL_TEMPLATES.get(self.get_cell_type_str()) or {}
selected_template_infos = cell_templates.get(self.template_name) or {}
if 'template' in selected_template_infos:
template_names.append(selected_template_infos.get('template'))
if self.slug:
template_names.append('combo/cells/%s/%s' % (self.slug, base_template_name))
template_names.reverse()
tmpl = template.loader.select_template(template_names)
return tmpl.render(context, context.get('request'))
2015-06-06 16:16:19 +02:00
def render_for_search(self):
if not self.is_enabled():
return ''
if self.is_user_dependant():
return ''
request = RequestFactory().get(self.page.get_online_url())
request.user = None # compat
context = {
'page': self.page,
'render_skeleton': False,
'request': request,
'site_base': request.build_absolute_uri('/')[:-1],
'synchronous': True,
'user': None, # compat
'absolute_uri': request.build_absolute_uri,
}
2015-06-06 16:16:19 +02:00
if not self.is_relevant(context):
return ''
return html.unescape(strip_tags(self.render(context)))
2015-06-06 16:16:19 +02:00
def get_external_links_data(self):
return []
@classmethod
def get_serialized_cells(cls, cells):
return [c.get_serialized_cell() for c in cells]
def get_serialized_cell(self):
serialized_cell = json.loads(
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
)
)[0]
serialized_cell.update(self.export_subobjects())
return serialized_cell
@classmethod
def prepare_serialized_data(cls, cell_data):
return cell_data
def export_subobjects(self):
return {}
def import_subobjects(self, cell_json):
pass
2014-12-07 15:28:18 +01:00
2021-02-26 10:13:23 +01:00
def duplicate(self, page_target=None, placeholder=None, reset_slug=False, set_order=False):
2019-10-22 14:58:02 +02:00
# clone current cell
new_cell = copy.deepcopy(self)
new_cell.pk = None
# set page
new_cell.page = page_target or self.page
# set placeholder
new_cell.placeholder = placeholder or new_cell.placeholder
# reset slug if requested and if duplicate on the same page
if reset_slug and new_cell.page == self.page:
2021-02-26 10:13:23 +01:00
new_cell.slug = ''
# set order if requested
if set_order:
if new_cell.page == self.page:
order = self.order + 1
else:
page_cells = CellBase.get_cells(page_id=new_cell.page.pk)
orders = [x.order for x in page_cells]
if orders:
order = max(orders) + 1
else:
order = 1
new_cell.order = order
2019-10-22 14:58:02 +02:00
# store new cell
new_cell.save()
# set groups
new_cell.groups.set(self.groups.all())
if hasattr(self, 'duplicate_m2m'):
self.duplicate_m2m(new_cell)
return new_cell
@register_cell_class
2014-12-07 15:34:30 +01:00
class TextCell(CellBase):
title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
text = RichTextField(_('Text'), blank=True, null=True)
2014-12-07 17:22:49 +01:00
default_template_name = 'combo/text-cell.html'
2014-12-08 17:06:57 +01:00
class Meta:
verbose_name = _('Text')
def is_relevant(self, context):
return bool(self.text or self.title)
2014-12-07 21:24:27 +01:00
def get_additional_label(self):
if not (self.title or self.text):
return None
return utils.ellipsize(self.title or self.text)
@classmethod
def get_cell_types(cls):
d = super().get_cell_types()
d[0]['order'] = -1
return d
def get_cell_extra_context(self, context):
extra_context = super().get_cell_extra_context(context)
text = self.text or ''
force_absolute_url = context.get('force_absolute_url') or context.get('render_skeleton')
def sub_variadic_url(match):
attribute = match.group(1)
url = match.group(2)
try:
url = utils.get_templated_url(url, context=context)
except utils.TemplateError as e:
logger = logging.getLogger(__name__)
logger.warning('error in templated URL (%s): %s', url, e)
return '%s="%s"' % (attribute, url)
2021-02-15 18:01:46 +01:00
text = re.sub(r'(href|src)="(.*?)"', sub_variadic_url, text)
if force_absolute_url:
request = context.get('request')
2021-02-15 18:01:46 +01:00
def sub_src(match):
url = request.build_absolute_uri(match.group(1))
return 'src="%s"' % url
2021-02-15 18:01:46 +01:00
def sub_href(match):
url = match.group(1)
if url.startswith('#') or url.startswith('{'):
# do not make URI of anchor or generated template (this is useful so
# we can get {{registration_url}} exported verbatim in a page to serve
# as custom authentic login page.
pass
else:
url = request.build_absolute_uri(url)
return 'href="%s"' % url
2021-02-15 18:01:46 +01:00
text = re.sub(r'src="(.*?)"', sub_src, text)
text = re.sub(r'href="(.*?)"', sub_href, text)
extra_context["text"] = mark_safe(text)
extra_context["title"] = mark_safe(self.title) if self.title else None
return extra_context
2014-12-07 17:22:49 +01:00
@register_cell_class
2014-12-07 17:22:49 +01:00
class FortuneCell(CellBase):
ajax_refresh = 30
2014-12-08 17:06:57 +01:00
class Meta:
verbose_name = _('Fortune')
2014-12-13 11:25:53 +01:00
def render(self, context):
2014-12-07 21:24:27 +01:00
return subprocess.check_output(['fortune'])
2014-12-13 11:25:53 +01:00
@classmethod
def is_enabled(cls):
try:
subprocess.check_output(['fortune'])
except OSError:
return False
return settings.DEBUG
2014-12-13 11:25:53 +01:00
@register_cell_class
class UnlockMarkerCell(CellBase):
# XXX: this is kept to smooth transitions, it should be removed once all
# sites # have been migrated to ParentContentCell
"""Marks an 'acquired' placeholder as unlocked."""
visible = False
class Meta:
verbose_name = _('Unlock Marker')
def render(self, context):
return ''
@register_cell_class
class MenuCell(CellBase):
depth = models.PositiveIntegerField(
_('Depth'), choices=[(i, i) for i in range(1, 3)], default=1, null=False
)
initial_level = models.IntegerField(
_('Initial Level'),
choices=[(-1, _('Same as page'))] + [(i, i) for i in range(1, 3)],
default=-1,
null=False,
)
root_page = models.ForeignKey(
Page,
on_delete=models.CASCADE,
related_name='root_page',
null=True,
blank=True,
verbose_name=_('Root Page'),
)
default_template_name = 'combo/menu-cell.html'
exclude_from_search = True
class Meta:
verbose_name = _('Menu')
def get_default_form_class(self):
from .forms import MenuCellForm
2021-02-15 18:01:46 +01:00
return MenuCellForm
def get_cell_extra_context(self, context):
from combo.public.menu import render_menu
2021-02-15 18:01:46 +01:00
ctx = super().get_cell_extra_context(context)
ctx['menu'] = render_menu(
context,
level=self.initial_level,
root_page=self.root_page,
depth=self.depth,
ignore_visibility=False,
)
return ctx
@register_cell_class
class LinkCell(CellBase):
title = models.CharField(_('Label'), max_length=150, blank=True)
url = models.CharField(_('URL'), max_length=2000, blank=True, validators=[django_template_validator])
link_page = models.ForeignKey(
'data.Page',
on_delete=models.CASCADE,
related_name='link_cell',
blank=True,
null=True,
verbose_name=_('Internal link'),
)
anchor = models.CharField(_('Anchor'), max_length=150, blank=True)
bypass_url_validity_check = models.BooleanField(_('No URL validity check'), default=False)
default_template_name = 'combo/link-cell.html'
add_as_link_label = _('add a link')
add_link_label = _('New link')
edit_link_label = _('Edit link')
add_as_link_code = 'link'
invalid_reason_codes = {
'data_url_not_defined': _('No link set'),
'data_url_not_found': _('URL seems to unexist'),
'data_url_invalid': _('URL seems to be invalid'),
}
class Meta:
verbose_name = _('Link')
def save(self, *args, **kwargs):
if 'update_fields' in kwargs:
# don't check validity
return super().save(*args, **kwargs)
result = super().save(*args, **kwargs)
# check validity
self.check_validity()
return result
@classmethod
def prepare_serialized_data(cls, cell_data):
if cell_data['fields'].get('link_page'):
if cell_data['fields']['link_page'][0] not in Page.get_page_ids_by_uuids():
del cell_data['fields']['link_page']
return cell_data
def get_additional_label(self):
title = self.title
if not title and self.link_page:
title = self.link_page.title
if not title:
return None
return utils.ellipsize(title)
def get_slug_for_asset(self):
if self.placeholder and self.placeholder.startswith('_'):
return
if self.link_page:
return self.link_page.slug
return self.slug
def get_label_for_asset(self):
if self.link_page:
return str(self)
return super().get_label_for_asset()
def get_url(self, context=None):
context = context or {}
if self.link_page:
url = self.link_page.get_online_url()
else:
url = utils.get_templated_url(self.url, context=context)
if self.anchor:
url += '#' + self.anchor
return url
def get_cell_extra_context(self, context):
force_absolute_url = context.get('force_absolute_url') or context.get('render_skeleton')
request = context.get('request')
extra_context = super().get_cell_extra_context(context)
if self.link_page:
extra_context['title'] = self.title or self.link_page.title
extra_context['description'] = self.link_page.description
else:
extra_context['title'] = self.title or self.url
url = self.get_url(context)
if force_absolute_url and not urllib.parse.urlparse(url).netloc:
# create full URL when used in a skeleton
url = request.build_absolute_uri(url)
extra_context['url'] = url
return extra_context
2015-08-24 14:32:06 +02:00
def get_appearance_fields(self):
# keep title/label on main tab
return []
def get_default_form_class(self):
from .forms import LinkCellForm
2021-02-15 18:01:46 +01:00
return LinkCellForm
def get_form_class_for_link_list_cell(self):
from .forms import LinkCellForLinkListCellForm
return LinkCellForLinkListCellForm
def get_manager_tabs(self):
tabs = super().get_manager_tabs()
tabs.insert(
1,
{
'slug': 'advanced',
'name': _('Advanced'),
'fields': ['bypass_url_validity_check'],
},
)
return tabs
def render_for_search(self):
return ''
def get_external_links_data(self):
if not self.url:
return []
if re.search(r'{{.*(request\.|cards\||forms\|)', self.url):
# skip URL templates that would reference request or the cards or forms
# context processor variables.
return []
link_data = self.get_cell_extra_context({})
if link_data.get('title') and link_data.get('url'):
return [link_data]
return []
def check_validity(self):
if self.bypass_url_validity_check:
self.mark_as_valid()
return
if self.link_page_id:
self.mark_as_valid()
return
if not self.url:
if self.anchor:
# link to anchor on same page, ok.
self.mark_as_valid()
return
self.mark_as_invalid('data_url_not_defined')
return
if self.url.count('{{') > 1:
self.mark_as_valid()
return
resp = None
try:
resp = requests.get(
self.get_url(),
timeout=settings.REQUESTS_TIMEOUT,
headers={
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0 Publik/0'
},
)
resp.raise_for_status()
except (requests.exceptions.RequestException):
pass
self.set_validity_from_url(resp, not_found_code='data_url_not_found', invalid_code='data_url_invalid')
2015-08-24 14:32:06 +02:00
@register_cell_class
class LinkListCell(CellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
limit = models.PositiveSmallIntegerField(_('Limit'), null=True, blank=True)
default_template_name = 'combo/link-list-cell.html'
manager_form_template = 'combo/manager/link-list-cell-form.html'
children_placeholder_prefix = '_linkslist:'
invalid_reason_codes = {
'data_link_invalid': _('Invalid link'),
}
class Meta:
verbose_name = _('List of links')
@property
def link_placeholder(self):
return self.children_placeholder_prefix + str(self.pk)
def get_items(self, prefetch_validity_info=False):
return CellBase.get_cells(
page=self.page,
placeholder=self.link_placeholder,
cell_filter=lambda x: hasattr(x, 'add_as_link_label'),
prefetch_validity_info=prefetch_validity_info,
)
def get_items_with_prefetch(self):
if not hasattr(self, '_items_with_prefetch_cache'):
self._items_with_prefetch_cache = self.get_items(prefetch_validity_info=True)
return self._items_with_prefetch_cache
def get_additional_label(self):
title = self.title
if not title:
return None
return utils.ellipsize(title)
def get_cell_extra_context(self, context):
extra_context = super().get_cell_extra_context(context)
links = []
for cell in context.get('page_cells', []):
if not hasattr(cell, 'add_as_link_label'):
continue
if not cell.placeholder == self.link_placeholder:
continue
links.append(cell.get_cell_extra_context(context))
extra_context['links'] = links
extra_context['more_links'] = []
if self.limit:
extra_context['more_links'] = extra_context['links'][self.limit :]
extra_context['links'] = extra_context['links'][: self.limit]
extra_context['title'] = self.title
return extra_context
def get_link_cell_classes(self):
return CellBase.get_cell_classes(lambda x: hasattr(x, 'add_as_link_label'))
def get_default_form_class(self):
from .forms import LinkListCellForm
2021-02-15 18:01:46 +01:00
return LinkListCellForm
def export_subobjects(self):
links = json.loads(
serializers.serialize(
'json', self.get_items(), use_natural_foreign_keys=True, use_natural_primary_keys=True
2021-02-15 18:01:46 +01:00
)
)
for link in links:
del link['pk']
del link['fields']['placeholder']
del link['fields']['page']
return {'links': links}
def import_subobjects(self, cell_json):
for link in cell_json['links']:
link['fields']['placeholder'] = self.link_placeholder
link['fields']['page'] = self.page_id
links = serializers.deserialize('json', json.dumps(cell_json['links']), ignorenonexistent=True)
for link in links:
link.save()
def duplicate_m2m(self, new_cell):
# duplicate also link items
for link in self.get_items():
link.duplicate(page_target=new_cell.page, placeholder=new_cell.link_placeholder)
def is_visible(self, request, check_validity_info=True, **kwargs):
# cell is visible even if items are invalid
return super().is_visible(request, check_validity_info=False, **kwargs)
def check_validity(self):
for link in self.get_items(prefetch_validity_info=True):
validity_info = link.get_validity_info()
if validity_info is not None:
self.mark_as_invalid('data_link_invalid')
return
self.mark_as_valid()
def render_for_search(self):
return ''
def get_external_links_data(self):
for link in self.get_items():
yield from link.get_external_links_data()
2015-08-24 14:32:06 +02:00
@register_cell_class
class FeedCell(CellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
url = models.CharField(_('URL'), blank=True, max_length=200)
limit = models.PositiveSmallIntegerField(_('Maximum number of entries'), null=True, blank=True)
2015-08-24 14:32:06 +02:00
manager_form_factory_kwargs = {'field_classes': {'url': TemplatableURLField}}
default_template_name = 'combo/feed-cell.html'
2015-08-24 14:32:06 +02:00
invalid_reason_codes = {
'data_url_not_defined': _('No URL set'),
'data_url_not_found': _('URL seems to unexist'),
'data_url_invalid': _('URL seems to be invalid'),
}
2015-08-24 14:32:06 +02:00
class Meta:
verbose_name = _('RSS/Atom Feed')
def is_visible(self, *args, **kwargs):
return bool(self.url) and super().is_visible(*args, **kwargs)
def save(self, *args, **kwargs):
result = super().save(*args, **kwargs)
if 'update_fields' not in kwargs:
# always mark cell as valid when it is saved, it will be checked
# for real when it is rendered
self.mark_as_valid()
return result
def get_cell_extra_context(self, context):
extra_context = super().get_cell_extra_context(context)
if not self.url:
self.mark_as_invalid('data_url_not_defined')
return extra_context
if context.get('placeholder_search_mode'):
# don't call webservices when we're just looking for placeholders
return extra_context
cache_key = hashlib.md5(smart_bytes(self.url)).hexdigest()
2015-08-24 14:32:06 +02:00
feed_content = cache.get(cache_key)
if not feed_content:
feed_response = None
try:
feed_response = requests.get(
utils.get_templated_url(self.url), timeout=settings.REQUESTS_TIMEOUT
)
feed_response.raise_for_status()
except (requests.exceptions.RequestException):
pass
else:
if feed_response.status_code == 200:
feed_content = feed_response.content
cache.set(cache_key, feed_content, 600)
self.set_validity_from_url(
feed_response, not_found_code='data_url_not_found', invalid_code='data_url_invalid'
)
2015-08-24 14:32:06 +02:00
if feed_content:
extra_context['feed'] = feedparser.parse(feed_content)
if self.limit:
extra_context['feed']['entries'] = extra_context['feed']['entries'][: self.limit]
if not self.title:
try:
self.title = extra_context['feed']['feed'].title
except (KeyError, AttributeError):
pass
return extra_context
def render(self, context):
cache_key = hashlib.md5(smart_bytes(self.url)).hexdigest()
feed_content = cache.get(cache_key)
if not context.get('synchronous') and feed_content is None:
raise NothingInCacheException()
2015-08-24 14:32:06 +02:00
return super().render(context)
@register_cell_class
class ParentContentCell(CellBase):
class Meta:
verbose_name = _('Same as parent')
def get_parents_cells(self, hierarchy, leaf):
try:
pages = [Page.objects.get(slug='index', parent=None)]
except Page.DoesNotExist:
pages = []
pages.extend(hierarchy)
if not pages:
return []
if len(pages) > 1 and pages[0].id == pages[1].id:
# don't duplicate index cells for real children of the index page.
pages = pages[1:]
cells_by_page = {}
for page in pages:
cells_by_page[page.id] = []
# get cells from placeholder + cells in private placeholders that may
# be used by actual cells.
placeholder_filter = Q(placeholder=self.placeholder)
for klass in CellBase.get_cell_classes(lambda x: bool(x.children_placeholder_prefix)):
placeholder_filter |= Q(placeholder__startswith=klass.children_placeholder_prefix)
for cell in CellBase.get_cells(page__in=pages, extra_filter=placeholder_filter):
cells_by_page[cell.page_id].append(cell)
cells = cells_by_page[pages[-1].id]
if not leaf.placeholder_options.get(self.placeholder):
for page in reversed(pages):
# if there's no placeholder options then we copy placeholder
# options from parent page.
if page.placeholder_options.get(self.placeholder):
leaf.placeholder_options[self.placeholder] = page.placeholder_options.get(
self.placeholder
)
break
for page in reversed(pages[:-1]):
for i, cell in enumerate(cells):
if isinstance(cell, ParentContentCell):
cells[i : i + 1] = cells_by_page[page.id]
break
else:
# no more ParentContentCell, stop folloing the parent page chain
break
return cells
def render(self, context):
return ''
class JsonCellBase(CellBase):
url = None
cache_duration = 60
template_string = None
varnames = None
force_async = False
log_errors = True
timeout = None
make_global = False
actions = {}
additional_data = None
session_required = True
# [
# {'key': ...,
# 'url': ...,
# 'cache_duration': ... (optional)
# },
# ...
# ]
first_data_key = 'json'
_json_content = None
invalid_reason_codes = {
'data_url_not_found': _('URL seems to unexist'),
'data_url_invalid': _('URL seems to be invalid'),
}
class Meta:
abstract = True
def save(self, *args, **kwargs):
result = super().save(*args, **kwargs)
if 'update_fields' not in kwargs:
# always mark cell as valid when it is saved, it will be checked
# for real when it is rendered
self.mark_as_valid()
return result
def is_visible(self, *args, **kwargs):
return bool(self.url) and super().is_visible(*args, **kwargs)
def is_user_dependant(self, context=None):
urls = [self.url] + [x['url'] for x in self.additional_data or []]
for url in urls:
if url and ('user_nameid' in url or 'user_email' in url):
return True
return False
def get_cell_parameters_context(self):
return {}
def get_cell_extra_context(self, context, invalidate_cache=False):
extra_context = super().get_cell_extra_context(context)
if context.get('placeholder_search_mode'):
# don't call webservices when we're just looking for placeholders
return extra_context
if self.varnames and context.get('request'):
for varname in self.varnames:
if varname in context['request'].GET:
context[varname] = context['request'].GET[varname]
self._json_content = None
extra_context.update(self.get_cell_parameters_context())
context.update(self.get_cell_parameters_context())
context['concerned_user'] = self.get_concerned_user(context)
data_urls = [
2021-02-15 18:01:46 +01:00
{
'key': self.first_data_key,
'url': self.url,
'cache_duration': self.cache_duration,
'log_errors': self.log_errors,
'timeout': self.timeout,
2021-02-15 18:01:46 +01:00
}
]
data_urls.extend(self.additional_data or [])
for data_url_dict in data_urls:
extra_context[data_url_dict['key']] = None
for data_url_dict in data_urls:
data_key = data_url_dict['key']
log_errors = data_url_dict.get('log_errors', self.log_errors)
try:
url = utils.get_templated_url(data_url_dict['url'], context)
except utils.TemplateError as e:
logger = logging.getLogger(__name__)
if log_errors and e.msg.startswith('syntax error'):
logger.error('error in templated URL (%s): %s', data_url_dict['url'], e)
else:
logger.debug('error in templated URL (%s): %s', data_url_dict['url'], e)
continue
extra_context[data_key + '_url'] = url
if not url:
continue
try:
json_response = utils.requests.get(
url,
headers={'Accept': 'application/json'},
remote_service='auto',
cache_duration=data_url_dict.get('cache_duration', self.cache_duration),
without_user=True,
raise_if_not_cached=not (context.get('synchronous')),
invalidate_cache=invalidate_cache,
log_errors=log_errors,
timeout=data_url_dict.get('timeout', self.timeout),
django_request=context.get('request'),
)
except requests.RequestException as e:
extra_context[data_key + '_status'] = -1
extra_context[data_key + '_error'] = force_str(e)
extra_context[data_key + '_exception'] = e
logger = logging.getLogger(__name__)
if log_errors:
logger.warning('error on request %r: %s', url, force_str(e))
else:
logger.debug('error on request %r: %s', url, force_str(e))
continue
extra_context[data_key + '_status'] = json_response.status_code
if json_response.status_code // 100 == 2:
if json_response.status_code != 204: # 204 = No Content
try:
extra_context[data_key] = json_response.json()
except ValueError:
extra_context[data_key + '_error'] = 'invalid_json'
logger = logging.getLogger(__name__)
if log_errors:
logger.error('invalid json content (%s)', url)
else:
logger.debug('invalid json content (%s)', url)
continue
elif json_response.headers.get('content-type') == 'application/json':
try:
extra_context[data_key + '_error'] = json_response.json()
except ValueError:
extra_context[data_key + '_error'] = 'invalid_json'
# update context with data key so it can be used in future
# templated URLs
context[data_key] = extra_context[data_key]
if not self._meta.abstract:
returns = []
for data_url in data_urls:
if data_url['url'].count('{{') > 1:
# ignore returns of url with more than one variable
continue
returns.append(extra_context.get(data_url['key'] + '_status'))
returns = {s for s in returns if s is not None}
if returns and 200 not in returns: # not a single valid answer
if 404 in returns:
self.mark_as_invalid('data_url_not_found')
elif any([400 <= r < 500 for r in returns]):
# at least 4xx errors, report the cell as invalid
self.mark_as_invalid('data_url_invalid')
else:
# 2xx or 3xx: cell is valid
# 5xx error: can not retrieve data, don't report cell as invalid
self.mark_as_valid()
else:
self.mark_as_valid()
# keep cache of first response as it may be used to find the
# appropriate template.
self._json_content = extra_context[self.first_data_key]
return extra_context
def modify_global_context(self, context, request):
if self.make_global:
synchronous = context.get('synchronous')
context['synchronous'] = True
try:
preloaded_data = self.get_cell_extra_context(context)
context[self.make_global] = preloaded_data.get(self.first_data_key)
finally:
context['synchronous'] = synchronous
@property
def default_template_name(self):
json_content = self._json_content
if json_content is None:
return 'combo/json-error-cell.html'
if isinstance(json_content, dict) and json_content.get('data'):
if isinstance(json_content['data'], list):
first_element = json_content['data'][0]
if isinstance(first_element, dict):
if 'url' in first_element and 'text' in first_element:
return 'combo/json-list-cell.html'
return 'combo/json-cell.html'
def post(self, request):
if not 'action' in request.POST:
raise PermissionDenied()
action = request.POST['action']
if not action in self.actions:
raise PermissionDenied()
error_message = self.actions[action].get('error-message')
timeout = self.actions[action].get('timeout', self.timeout)
method = self.actions[action].get('method', 'POST')
logger = logging.getLogger(__name__)
content = {}
for key, value in request.POST.items():
if key == 'action':
continue
if key.endswith('[]'): # array
content[key[:-2]] = request.POST.getlist(key)
else:
content[key] = value
context = copy.copy(content)
context.update(self.get_cell_parameters_context())
context['request'] = request
context['synchronous'] = True
try:
url = utils.get_templated_url(self.actions[action]['url'], context)
except utils.TemplateError as e:
logger.warning('error in templated URL (%s): %s', self.actions[action]['url'], e)
raise PostException(error_message)
json_response = utils.requests.request(
method,
url,
headers={'Accept': 'application/json'},
remote_service='auto',
json=content,
without_user=True,
django_request=request,
timeout=timeout,
)
if json_response.status_code // 100 != 2: # 2xx
logger.error('error POSTing data to URL (%s)', url)
raise PostException(error_message)
if self.cache_duration:
self.get_cell_extra_context(context, invalidate_cache=True)
if self.actions[action].get('response', 'cell') == 'raw':
# response: raw in the config will get the response directly sent
# to the client.
return json_response
# default behaviour, the cell will render itself.
return None
def render(self, context):
if self.force_async and not context.get('synchronous'):
raise NothingInCacheException()
if self.template_string:
tmpl = engines['django'].from_string(self.template_string)
context.update(self.get_cell_extra_context(context))
return tmpl.render(context, context.get('request'))
return super().render(context)
@register_cell_class
class JsonCell(JsonCellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
url = models.CharField(_('URL'), blank=True, max_length=500)
template_string = models.TextField(
_('Display Template'), blank=True, null=True, validators=[django_template_validator]
)
cache_duration = models.PositiveIntegerField(_('Cache duration'), default=60, help_text=_('In seconds.'))
force_async = models.BooleanField(_('Force asynchronous mode'), default=JsonCellBase.force_async)
varnames_str = models.CharField(
_('Variable names'),
max_length=200,
blank=True,
2022-06-01 19:00:46 +02:00
help_text=_('Comma separated list of query-string variables to be copied in template context'),
)
timeout = models.PositiveIntegerField(
_('Request timeout'), default=0, help_text=_('In seconds. Use 0 for default system timeout')
2021-02-15 18:01:46 +01:00
)
manager_form_factory_kwargs = {'field_classes': {'url': TemplatableURLField}}
class Meta:
verbose_name = _('JSON Prototype')
@property
def varnames(self):
return [vn.strip() for vn in self.varnames_str.split(',') if vn.strip()]
class ConfigJsonCellManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(key__in=settings.JSON_CELL_TYPES.keys())
@register_cell_class
class ConfigJsonCell(JsonCellBase):
objects = ConfigJsonCellManager()
all_objects = models.Manager()
key = models.CharField(max_length=50)
parameters = JSONField(blank=True, default=dict)
invalid_reason_codes = {
'settings_not_found': _('Cell not found in settings'),
}
def __str__(self):
return force_str(_('%s (JSON Cell)') % self.get_label())
@classmethod
def get_cell_types(cls):
l = []
for key, definition in settings.JSON_CELL_TYPES.items():
l.append(
{
'name': definition['name'],
'variant': key,
'group': _('Extra'),
'cell_type_str': cls.get_cell_type_str(),
}
)
l.sort(key=lambda x: x.get('name'))
return l
def get_label(self):
if self.key not in settings.JSON_CELL_TYPES:
return _('Unknown ConfigJsonCell %s') % self.key
return settings.JSON_CELL_TYPES[self.key]['name']
def set_variant(self, variant):
self.key = variant
@property
def css_class_names(self):
return super().css_class_names + ' ' + self.key
@property
def ajax_refresh(self):
return settings.JSON_CELL_TYPES[self.key].get('auto_refresh', None)
@property
def make_global(self):
return settings.JSON_CELL_TYPES[self.key].get('make_global', False)
def get_repeat_template(self, context):
return settings.JSON_CELL_TYPES[self.key].get('repeat')
@property
def url(self):
return settings.JSON_CELL_TYPES[self.key]['url']
@property
def cache_duration(self):
return settings.JSON_CELL_TYPES[self.key].get('cache_duration', JsonCellBase.cache_duration)
@property
def varnames(self):
return settings.JSON_CELL_TYPES[self.key].get('varnames')
@property
def force_async(self):
return settings.JSON_CELL_TYPES[self.key].get('force_async', JsonCellBase.force_async)
@property
def log_errors(self):
return settings.JSON_CELL_TYPES[self.key].get('log_errors', JsonCellBase.log_errors)
2021-02-15 18:01:46 +01:00
@property
def timeout(self):
return settings.JSON_CELL_TYPES[self.key].get('timeout', JsonCellBase.timeout)
@property
def actions(self):
return settings.JSON_CELL_TYPES[self.key].get('actions', JsonCellBase.actions)
@property
def additional_data(self):
return settings.JSON_CELL_TYPES[self.key].get('additional-data')
@property
def default_template_name(self):
return settings.JSON_CELL_TYPES[self.key].get('template-name', 'combo/json/%s.html' % self.key)
@property
def loading_message(self):
return settings.JSON_CELL_TYPES[self.key].get('loading-message', CellBase.loading_message)
def get_default_form_class(self):
formdef = settings.JSON_CELL_TYPES[self.key].get('form')
if not formdef:
return None
from .forms import ConfigJsonForm
2021-02-15 18:01:46 +01:00
# create a subclass of ConfigJsonForm with 'formdef' (the list of
# fields) as an attribute.
config_form_class = type(str('%sConfigClass' % self.key), (ConfigJsonForm,), {'formdef': formdef})
return config_form_class
def get_cell_parameters_context(self):
context = copy.copy(self.parameters or {})
context['parameters'] = self.parameters
return context
def render(self, context):
settings_varnames = [f.get('varname') for f in settings.JSON_CELL_TYPES[self.key].get('form') or {}]
if 'template_string' in self.parameters and 'template_string' in settings_varnames:
self.template_string = self.parameters['template_string']
return super().render(context)
def check_validity(self):
if self.key not in settings.JSON_CELL_TYPES:
self.mark_as_invalid('settings_not_found')
return
validity_info = self.get_validity_info()
if validity_info is None:
return
if validity_info.invalid_reason_code != 'settings_not_found':
# don't overwrite other invalid reasons
return
self.mark_as_valid()
@receiver(pre_save, sender=Page)
def create_redirects(sender, instance, raw, **kwargs):
if raw or not instance.id or instance.snapshot_id:
return
if kwargs.get('update_fields') and kwargs['update_fields'] == frozenset({'related_cells'}):
return
try:
old_page = Page.objects.get(id=instance.id)
except Page.DoesNotExist:
return
if old_page.slug == instance.slug and old_page.parent_id == instance.parent_id:
return
affected_pages = level_pages = [old_page]
while True:
level_pages = Page.objects.filter(parent_id__in=[x.id for x in level_pages]).select_related('parent')
if len(level_pages) == 0:
break
affected_pages.extend(level_pages)
for page in affected_pages:
Redirect(page=page, old_url=page.get_online_url()).save()
2021-02-15 18:01:46 +01:00
@receiver(post_save)
@receiver(post_delete)
def cell_maintain_page_cell_cache(sender, instance=None, **kwargs):
if not issubclass(sender, CellBase):
return
if not instance.page_id:
return
page = instance.page
if kwargs.get('created') is False:
# don't build the cache on update, but update page's last_update_timestamp
page.save(update_fields=['last_update_timestamp'])
return
page.build_cell_cache()
class SiteSettings(models.Model):
welcome_page = models.ForeignKey(
to=Page,
verbose_name=_('Welcome page'),
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+',
help_text=_('Page to redirect to on the first visit, to suggest user to log in.'),
)
welcome_page_path = models.CharField(
verbose_name='',
help_text=_('Path or full URL.'),
max_length=100,
blank=True,
)
initial_login_page = models.ForeignKey(
to=Page,
verbose_name=_('Initial login page'),
help_text=_('Page to redirect to the first time user logs in.'),
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+',
)
initial_login_page_path = models.CharField(
verbose_name='',
help_text=_('Path or full URL.'),
max_length=100,
blank=True,
)
@classmethod
def export_json(cls):
settings = cls.get_singleton()
return {
'initial_login_page_path': settings.initial_login_page_path,
'welcome_page_path': settings.welcome_page_path,
}
@classmethod
def import_json(cls, data):
SiteSettings.objects.update(**data)
@classmethod
def get_singleton(cls):
try:
return cls.objects.get()
except cls.DoesNotExist:
return cls()