501 lines
16 KiB
Python
501 lines
16 KiB
Python
# 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 feedparser
|
|
import hashlib
|
|
import json
|
|
import requests
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import Group
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core import serializers
|
|
from django.db import models
|
|
from django.db.models import Max
|
|
from django.forms import models as model_forms
|
|
from django import template
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from ckeditor.fields import RichTextField
|
|
import cmsplugin_blurp.utils
|
|
|
|
from .library import register_cell_class, get_cell_classes, get_cell_class
|
|
|
|
from combo import utils
|
|
from combo.utils import NothingInCacheException
|
|
|
|
|
|
def element_is_visible(element, user=None):
|
|
if element.public:
|
|
return True
|
|
if user is None:
|
|
return False
|
|
if user.is_anonymous():
|
|
return False
|
|
page_groups = element.groups.all()
|
|
if not page_groups:
|
|
# user is logged in, no group restriction in place
|
|
return True
|
|
return len(set(page_groups).intersection(user.groups.all())) > 0
|
|
|
|
|
|
class Page(models.Model):
|
|
title = models.CharField(_('Title'), max_length=50)
|
|
slug = models.SlugField(_('Slug'))
|
|
template_name = models.CharField(_('Template'), max_length=50)
|
|
parent = models.ForeignKey('self', null=True, blank=True)
|
|
order = models.PositiveIntegerField()
|
|
exclude_from_navigation = models.BooleanField(_('Exclude from navigation'), default=False)
|
|
redirect_url = models.CharField(_('Redirect URL'), max_length=100, blank=True)
|
|
|
|
public = models.BooleanField(_('Public'), default=True)
|
|
groups = models.ManyToManyField(Group, verbose_name=_('Groups'), blank=True)
|
|
|
|
_level = None
|
|
_children = None
|
|
|
|
class Meta:
|
|
ordering = ['order']
|
|
|
|
def __unicode__(self):
|
|
return self.title
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.order:
|
|
max_order = Page.objects.all().aggregate(Max('order')).get('order__max') or 0
|
|
self.order = max_order + 1
|
|
return super(Page, self).save(*args, **kwargs)
|
|
|
|
def get_parents_and_self(self):
|
|
pages = [self]
|
|
page = self
|
|
while page.parent_id:
|
|
page = page.parent
|
|
pages.append(page)
|
|
return reversed(pages)
|
|
|
|
def get_online_url(self):
|
|
parts = [x.slug for x in self.get_parents_and_self()]
|
|
if parts == ['index']:
|
|
return '/'
|
|
return '/' + '/'.join(parts)
|
|
|
|
def get_page_of_level(self, level):
|
|
'''Return page of given level in the page hierarchy.'''
|
|
parts = [self]
|
|
page = self
|
|
while page.parent_id:
|
|
page = page.parent
|
|
parts.append(page)
|
|
parts.reverse()
|
|
try:
|
|
return parts[level]
|
|
except IndexError:
|
|
return None
|
|
|
|
def get_siblings(self):
|
|
return Page.objects.filter(parent=self.parent)
|
|
|
|
def get_children(self):
|
|
return Page.objects.filter(parent_id=self.id)
|
|
|
|
def has_children(self):
|
|
return Page.objects.filter(parent_id=self.id).exists()
|
|
|
|
@classmethod
|
|
def get_as_reordered_flat_hierarchy(cls, object_list):
|
|
reordered = []
|
|
def fill_list(object_sublist, level=0, parent=None):
|
|
for page in object_sublist:
|
|
page._children = [x for x in object_list if x.parent_id == page.id]
|
|
if page.parent == parent:
|
|
page.level = level
|
|
reordered.append(page)
|
|
fill_list(object_sublist, level=level+1, parent=page)
|
|
fill_list(object_list)
|
|
return reordered
|
|
|
|
def get_unlocked_placeholders(self, cells=None):
|
|
combo_template = settings.COMBO_PUBLIC_TEMPLATES.get(self.template_name)
|
|
if self.slug == 'index':
|
|
# on the site index page, there are no unlocked placeholder.
|
|
return combo_template['placeholders'].keys()
|
|
|
|
if cells is None:
|
|
cells = CellBase.get_cells(page_id=self.id)
|
|
|
|
# on the other page sites, look for unlock markers
|
|
unlocked_placeholders = []
|
|
for cell in cells:
|
|
if not isinstance(cell, UnlockMarkerCell):
|
|
continue
|
|
if cell.page_id == self.id:
|
|
unlocked_placeholders.append(cell.placeholder)
|
|
return unlocked_placeholders
|
|
|
|
def get_locked_placeholders(self, cells=None):
|
|
combo_template = settings.COMBO_PUBLIC_TEMPLATES.get(self.template_name)
|
|
lockable_placeholders = [x for x in combo_template['placeholders'] if (
|
|
combo_template['placeholders'].get(x).get('acquired'))]
|
|
unlocked_placeholders = self.get_unlocked_placeholders(cells)
|
|
locked_placeholders = set(lockable_placeholders) - set(unlocked_placeholders)
|
|
return list(locked_placeholders)
|
|
|
|
def visibility(self):
|
|
if self.public:
|
|
return _('Public')
|
|
return _('Private (%s)') % ', '.join([x.name for x in self.groups.all()])
|
|
|
|
def is_visible(self, user=None):
|
|
return element_is_visible(self, user=user)
|
|
|
|
def get_serialized_page(self):
|
|
cells = CellBase.get_cells(page_id=self.id)
|
|
serialized_page = json.loads(serializers.serialize('json', [self]))[0]
|
|
del serialized_page['pk']
|
|
del serialized_page['model']
|
|
del serialized_page['fields']['parent']
|
|
if self.parent_id:
|
|
serialized_page['parent_slug'] = self.parent.slug
|
|
serialized_page['cells'] = json.loads(serializers.serialize('json', cells))
|
|
for cell in serialized_page['cells']:
|
|
del cell['pk']
|
|
del cell['fields']['page']
|
|
return serialized_page
|
|
|
|
@classmethod
|
|
def load_serialized_page(cls, json_page):
|
|
json_page['model'] = 'data.page'
|
|
page, created = Page.objects.get_or_create(slug=json_page['fields']['slug'])
|
|
json_page['pk'] = page.id
|
|
page = [x for x in serializers.deserialize('json', json.dumps([json_page]))][0]
|
|
page.save()
|
|
for cell in json_page.get('cells'):
|
|
cell['fields']['page'] = page.object.id
|
|
|
|
# if there were cells, remove them
|
|
for cell in CellBase.get_cells(page_id=page.object.id):
|
|
cell.delete()
|
|
|
|
# load new cells
|
|
for cell in serializers.deserialize('json', json.dumps(json_page.get('cells'))):
|
|
cell.save()
|
|
|
|
@classmethod
|
|
def load_serialized_pages(cls, json_site):
|
|
for json_page in json_site:
|
|
cls.load_serialized_page(json_page)
|
|
|
|
# 2nd pass to set parents
|
|
for json_page in json_site:
|
|
if json_page.get('parent_slug'):
|
|
page = Page.objects.get(slug=json_page['fields']['slug'])
|
|
page.parent = Page.objects.get(slug=json_page.get('parent_slug'))
|
|
page.save()
|
|
|
|
|
|
|
|
class CellBase(models.Model):
|
|
page = models.ForeignKey(Page)
|
|
placeholder = models.CharField(max_length=20)
|
|
order = models.PositiveIntegerField()
|
|
slug = models.SlugField(_('Slug'), blank=True)
|
|
|
|
public = models.BooleanField(_('Public'), default=True)
|
|
groups = models.ManyToManyField(Group, verbose_name=_('Groups'), blank=True)
|
|
|
|
default_form_class = None
|
|
visible = True
|
|
user_dependant = False
|
|
manager_form_template = 'combo/cell_form.html'
|
|
template_name = None
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def __unicode__(self):
|
|
label = unicode(self.get_verbose_name())
|
|
additional_label = self.get_additional_label()
|
|
if label and additional_label:
|
|
return '%s (%s)' % (label, additional_label)
|
|
else:
|
|
return label
|
|
|
|
@classmethod
|
|
def get_verbose_name(cls):
|
|
return cls._meta.verbose_name
|
|
|
|
def get_additional_label(self):
|
|
return ''
|
|
|
|
@property
|
|
def css_class_name(self):
|
|
return self.__class__.__name__.lower()
|
|
|
|
@classmethod
|
|
def get_cell_content_types(cls):
|
|
content_types = []
|
|
for klass in get_cell_classes():
|
|
if not klass.is_enabled():
|
|
continue
|
|
if klass.visible is False:
|
|
continue
|
|
content_types.extend(klass.get_content_types())
|
|
return content_types
|
|
|
|
@classmethod
|
|
def get_cells(cls, **kwargs):
|
|
"""Returns the list of cells of various classes matching **kwargs"""
|
|
cells = []
|
|
for klass in get_cell_classes():
|
|
cells.extend(klass.objects.filter(**kwargs))
|
|
cells.sort(lambda x, y: cmp(x.order, y.order))
|
|
return cells
|
|
|
|
def get_reference(self):
|
|
"Returns a string that can serve as a unique reference to a cell"""
|
|
content_type = ContentType.objects.get_for_model(self)
|
|
content_type_str = '%s_%s' % (content_type.app_label, content_type.model)
|
|
return str('%s-%s' % (content_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('-')
|
|
try:
|
|
klass = get_cell_class(content_id)
|
|
except KeyError:
|
|
raise ObjectDoesNotExist()
|
|
return klass.objects.get(id=cell_id, **kwargs)
|
|
|
|
@classmethod
|
|
def get_content_types(cls):
|
|
return [{
|
|
'name': cls.get_verbose_name(),
|
|
'content_type': ContentType.objects.get_for_model(cls),
|
|
'variant': 'default',
|
|
}]
|
|
|
|
@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
|
|
|
|
def set_variant(self, variant):
|
|
pass
|
|
|
|
def get_label(self):
|
|
return self.get_verbose_name()
|
|
|
|
def get_default_form_class(self):
|
|
if self.default_form_class:
|
|
return self.default_form_class
|
|
|
|
fields = [x.name for x in self._meta.local_concrete_fields
|
|
if x.name not in ('id', 'page', 'placeholder', 'order',
|
|
'public', 'groups', 'slug')]
|
|
if not fields:
|
|
return None
|
|
|
|
return model_forms.modelform_factory(self.__class__, fields=fields)
|
|
|
|
def get_options_form_class(self):
|
|
return model_forms.modelform_factory(self.__class__,
|
|
fields=['slug'])
|
|
|
|
def get_visibility_form_class(self):
|
|
return model_forms.modelform_factory(self.__class__,
|
|
fields=['public', 'groups'])
|
|
|
|
def get_extra_manager_context(self):
|
|
return {}
|
|
|
|
def is_visible(self, user=None):
|
|
return element_is_visible(self, user=user)
|
|
|
|
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):
|
|
'''Return whether the cell content varies from user to user.'''
|
|
return self.user_dependant
|
|
|
|
def get_cell_extra_context(self):
|
|
return {'cell': self}
|
|
|
|
def render(self, context):
|
|
context.update(self.get_cell_extra_context())
|
|
tmpl = template.loader.get_template(self.template_name)
|
|
return tmpl.render(context)
|
|
|
|
|
|
@register_cell_class
|
|
class TextCell(CellBase):
|
|
text = RichTextField(_('Text'), blank=True, null=True)
|
|
|
|
class Meta:
|
|
verbose_name = _('Text')
|
|
|
|
def render(self, context):
|
|
return mark_safe(self.text or '')
|
|
|
|
def get_additional_label(self):
|
|
if not self.text:
|
|
return None
|
|
return utils.ellipsize(self.text)
|
|
|
|
|
|
@register_cell_class
|
|
class FortuneCell(CellBase):
|
|
ajax_refresh = 30
|
|
|
|
class Meta:
|
|
verbose_name = _('Fortune')
|
|
|
|
def render(self, context):
|
|
import subprocess
|
|
return subprocess.check_output(['fortune'])
|
|
|
|
@classmethod
|
|
def is_enabled(cls):
|
|
return settings.DEBUG
|
|
|
|
|
|
@register_cell_class
|
|
class UnlockMarkerCell(CellBase):
|
|
"""Marks an 'acquired' placeholder as unlocked."""
|
|
visible = False
|
|
|
|
class Meta:
|
|
verbose_name = _('Unlock Marker')
|
|
|
|
def render(self, context):
|
|
return ''
|
|
|
|
@register_cell_class
|
|
class BlurpCell(CellBase):
|
|
blurp_key = models.CharField(max_length=50)
|
|
|
|
@classmethod
|
|
def get_content_types(cls):
|
|
try:
|
|
blurp_renderers = settings.CMS_PLUGIN_BLURP_RENDERERS
|
|
except AttributeError:
|
|
return []
|
|
l = []
|
|
base_content_type = ContentType.objects.get_for_model(cls)
|
|
for blurp_key, blurp_value in blurp_renderers.items():
|
|
if blurp_value.get('private'):
|
|
continue
|
|
l.append({
|
|
'name': blurp_value.get('name'),
|
|
'content_type': base_content_type,
|
|
'variant': blurp_key,
|
|
})
|
|
l.sort(lambda x, y: cmp(x.get('name'), y.get('name')))
|
|
return l
|
|
|
|
def get_label(self):
|
|
return settings.CMS_PLUGIN_BLURP_RENDERERS[self.blurp_key]['name']
|
|
|
|
def set_variant(self, variant):
|
|
self.blurp_key = variant
|
|
|
|
def render(self, context):
|
|
renderer = cmsplugin_blurp.utils.resolve_renderer(self.blurp_key)
|
|
template = renderer.render_template()
|
|
context = renderer.render(context)
|
|
return template.render(context)
|
|
|
|
def get_default_form_class(self):
|
|
return None
|
|
|
|
|
|
@register_cell_class
|
|
class MenuCell(CellBase):
|
|
depth = models.PositiveIntegerField(_('Depth'),
|
|
choices=[(i, i) for i in range(1, 3)], default=1, null=False)
|
|
|
|
class Meta:
|
|
verbose_name = _('Menu')
|
|
|
|
def render(self, context):
|
|
from combo.public.menu import render_menu
|
|
return render_menu(context, level=-1, depth=self.depth)
|
|
|
|
|
|
@register_cell_class
|
|
class LinkCell(CellBase):
|
|
title = models.CharField(_('Title'), max_length=150, blank=True)
|
|
url = models.URLField(_('URL'), blank=True)
|
|
link_page = models.ForeignKey('data.Page', related_name='link_cell', blank=True,
|
|
null=True, verbose_name=_('Internal link'))
|
|
anchor = models.CharField(_('Anchor'), max_length=150, blank=True)
|
|
|
|
template_name = 'combo/link-cell.html'
|
|
|
|
class Meta:
|
|
verbose_name = _('Link')
|
|
|
|
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 render(self, context):
|
|
if self.link_page:
|
|
context['url'] = self.link_page.get_online_url()
|
|
context['title'] = self.title or self.link_page.title
|
|
else:
|
|
context['url'] = self.url
|
|
context['title'] = self.title or self.url
|
|
if self.anchor:
|
|
context['url'] += '#' + self.anchor
|
|
return super(LinkCell, self).render(context)
|
|
|
|
|
|
@register_cell_class
|
|
class FeedCell(CellBase):
|
|
url = models.URLField(_('URL'), blank=True)
|
|
|
|
template_name = 'combo/feed-cell.html'
|
|
|
|
class Meta:
|
|
verbose_name = _('RSS/Atom Feed')
|
|
|
|
def render(self, context):
|
|
cache_key = hashlib.md5(self.url).hexdigest()
|
|
feed_content = cache.get(cache_key)
|
|
if not context.get('ajax') and feed_content is None:
|
|
raise NothingInCacheException()
|
|
if not feed_content:
|
|
feed_response = requests.get(self.url)
|
|
if feed_response.status_code == 200:
|
|
feed_content = feed_response.content
|
|
cache.set(cache_key, feed_content, 600)
|
|
if feed_content:
|
|
context['feed'] = feedparser.parse(feed_content)
|
|
return super(FeedCell, self).render(context)
|