import flask
from bson import ObjectId
from flask import render_template_string
from flask_babel import lazy_gettext as _
from xmm.util.mongo import KeyValidator
from .base import BaseCMSType, CMSFieldsMixin
from .. import fields, SignalizedMetaclass
from ..dynamicfields import DynamicFieldsMixin
from ..embedded import BaseEmbeddedDocument
[docs]class BlockType(BaseCMSType, metaclass=SignalizedMetaclass):
"""
A single block inside a page type template.
A block type defines the expected content defined
by the implementing block.
BlockTypes specify fields with a defined type
that will be defined by the implementing Block.
ex: BlockType Product defines a sub-template
that displays a product's details, contains
only one field to select a product, which will
be chosen on the Block instance.
(This could also make for dynamic blocks, that fill the
given blocktype's fields from, say, a URL parameter for
automatic browsing of models maybe).
"""
_elastic_index = 'system'
_elastic_doc_type = 'blocktype'
meta = {
'collection': 'system_blocktype',
'verbose_name': _('Blocktyp'),
'verbose_name_plural': _('Blocktypen'),
'indexes': [
{
'name': 'key',
'fields': ['key'],
'unique': True
},
{
'name': 'attribute_key',
'fields': ['key', 'attributes.key'],
'unique': True
},
],
}
key = fields.StringField(label=_('Schlüssel'), required=True, validation=KeyValidator(), isSortable=True)
label = fields.MultilingualField(fields.StringField(label=_('Bezeichnung'), isSortable=True))
label_plural = fields.MultilingualField(fields.StringField())
description = fields.MultilingualField(fields.TextField(label=_('Beschreibung'), width=400, flexGrow=1))
identifier = fields.TextField(label=_('Block Preview Template'))
valid_blocktypes = fields.ListField(fields.ReferenceField('BlockType'))
default_blocktype = fields.ReferenceField('BlockType')
dict_fields = [
'key',
'label',
'description',
]
columns_definitions = dict_fields
def __str__(self):
return self.label() or self.key
@classmethod
def get_impl_class(cls):
from .page import Page
return Page
@classmethod
def post_save(cls, sender, document, **kwargs):
super().post_save(sender, document, **kwargs)
from .page import Page
Page.elastic_put_mapping()
@classmethod
def verify_deletions(cls, objects):
from .page import Page
for block_type in objects:
if Page.objects(blocks__blocktype=block_type).count():
flask.flash(_('Blocktyp kann nicht gelöscht werden, da er von Blöcken verwendet wird.'), 'warning')
return False
return True
def impl_objects(self, **kwargs):
"""
Return a filtered cursor that queries only this block type.
.. warning:: This method returns a cursor on :class:`Page` objects!
:param kwargs: Passed to the ``blocks__`` filter query.
:returns: A query on pages!
"""
from .page import Page
kwargs['blocktype'] = self
query = {
'blocks__{}'.format(key): value
for key, value in kwargs.items()
}
return Page.objects(**query)
def identify_block(self, block, depth=0):
"""
Get the preview text for a block.
:param Block block: The block to describe
:param depth: Depth of current identification call.
"""
if self.identifier:
context = {
'block': block,
'blocktype': self,
'request': flask.request or None,
}
return render_template_string(self.identifier, **context)
# No identifier template on block!
return block.id
def update_from_json(self, json, compare_timestamps=True):
if 'valid_blocktypes' in json:
self.valid_blocktypes = list(map(BlockType.from_id, json.pop('valid_blocktypes') or []))
return super().update_from_json(json, compare_timestamps)
[docs]class Block(CMSFieldsMixin, DynamicFieldsMixin, BaseEmbeddedDocument):
"""
A block that implements a single blocktype's expected contents.
Will show a field to be filled out for every defined
property in the block's type.
This can be anything from freetext, free HTML, a number
or a number of models from a defined list.
Could also possibly be dynamically calculated by the current
page's parameters and status, such as URL and user login status.
"""
meta = {
'verbose_name': _('Block'),
'verbose_name_plural': _('Blöcke'),
}
id = fields.ObjectIdField(required=True, default=ObjectId)
type_field = 'blocktype'
blocktype = fields.ReferenceField('BlockType', required=True)
area = fields.StringField(help_text='Page area key')
pos = fields.IntField(default=0)
blocks = fields.EmbeddedDocumentListField('Block')
def __str__(self):
return str(self.id)
def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
@property
def key(self):
return self.id
@classmethod
def create_from_json(cls, json):
blocktype = BlockType.objects.get(id=json.pop('blocktypeId'))
return cls._create_from_json(blocktype, json)
def identify(self, depth=0):
"""Get a preview of this block's contents."""
if self.blocktype is not None:
return self.blocktype.identify_block(self, depth=0)
return _('Blocktyp zu diesem Block existiert nicht mehr!')
def update_from_json(self, json):
self.set_validity_fields(json)
if 'blocktypeId' in json:
self.blocktype = BlockType.objects.get_or_404(id=ObjectId(json.pop('blocktypeId')))
# to move a block around, simply change these two values
if 'area' in json:
self.area = json.pop('area')
if 'pos' in json:
self.pos = json.pop('pos')
return super().update_from_json(json)
def sort_subblocks(self):
for i, subblock in enumerate(sorted(self.blocks, key=lambda block: block.pos if block.pos else float('inf'))):
subblock.pos = i + 1
subblock.sort_subblocks()
def remove_id_recursively(self):
"""Set id to None so id will be autogenerated."""
self.id = None
for block in self.blocks:
block.remove_id_recursively()