wcs/wcs/admin/blocks.py

393 lines
15 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2020 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from quixote import get_publisher, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import BlockCategoriesDirectory, get_categories
from wcs.admin.fields import FieldDefPage, FieldsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.blocks import BlockDef, BlockdefImportError
from wcs.categories import BlockCategory
from wcs.formdef import UpdateStatisticsDataAfterJob
from wcs.qommon import _, misc, template
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.errors import AccessForbiddenError, TraversalError
from wcs.qommon.form import FileWidget, Form, HtmlWidget, SingleSelectWidget, SlugWidget, StringWidget
class BlockFieldDefPage(FieldDefPage):
def redirect_field_anchor(self, field):
anchor = '#itemId_%s' % field.id if field else ''
return redirect('../%s' % anchor)
def schedule_statistics_data_update(self):
get_response().add_after_job(
UpdateStatisticsDataAfterJob(formdefs=self.objectdef.get_usage_formdefs())
)
class BlockDirectory(FieldsDirectory):
_q_exports = [
'',
'update_order',
'new',
'delete',
'export',
'settings',
'inspect',
'duplicate',
('history', 'snapshots_dir'),
]
field_def_page_class = BlockFieldDefPage
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks', 'computed']
support_import = False
readonly_message = _('This block of fields is readonly.')
field_count_message = _('This block of fields contains %d fields.')
field_over_count_message = _('This block of fields contains more than %d fields.')
fields_count_total_soft_limit = 30
fields_count_total_hard_limit = 60
def __init__(self, section='forms', *args, **kwargs):
kwargs.pop('component', None) # snapshot
if 'instance' in kwargs:
kwargs['objectdef'] = kwargs.pop('instance')
self.section = section
super().__init__(*args, **kwargs)
self.snapshots_dir = SnapshotsDirectory(self.objectdef)
def _q_traverse(self, path):
get_response().breadcrumb.append(('%s/' % self.objectdef.id, self.objectdef.name))
return Directory._q_traverse(self, path)
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % self.objectdef.name
r += htmltext('</div>')
r += utils.last_modification_block(obj=self.objectdef)
r += get_session().display_message()
if not self.objectdef.fields:
r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.')
return r.getvalue()
def index_bottom(self):
formdefs = list(self.objectdef.get_usage_formdefs())
formdefs.sort(key=lambda x: x.name.lower())
if not formdefs:
return
r = TemplateIO(html=True)
r += htmltext('<div class="section">')
r += htmltext('<h3>%s</h3>') % _('Usage')
r += htmltext('<ul class="objects-list single-links">')
for formdef in formdefs:
r += htmltext('<li><a href="%s">' % formdef.get_admin_url())
r += htmltext('%s</a></li>') % formdef.name
r += htmltext('</ul>')
r += htmltext('</div>')
return r.getvalue()
def get_new_field_form_sidebar(self, page_id):
r = TemplateIO(html=True)
r += htmltext('<ul id="sidebar-actions">')
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
r += htmltext('<li><a href="duplicate" rel="popup">%s</a></li>') % _('Duplicate')
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
if get_publisher().snapshot_class:
r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _('Save snapshot')
r += htmltext('<li><a href="history/">%s</a></li>') % _('History')
r += htmltext('<li><a href="inspect">%s</a></li>') % _('Inspector')
r += htmltext('<li><a href="settings" rel="popup">%s</a></li>') % _('Settings')
r += htmltext('</ul>')
r += super().get_new_field_form_sidebar(page_id=page_id)
return r.getvalue()
def delete(self):
form = Form(enctype='multipart/form-data')
if not self.objectdef.is_used():
form.widgets.append(
HtmlWidget('<p>%s</p>' % _('You are about to irrevocably delete this block.'))
)
form.add_submit('delete', _('Submit'))
else:
form.widgets.append(
HtmlWidget('<p>%s</p>' % _('This block is still used, it cannot be deleted.'))
)
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('..')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('delete', _('Delete')))
html_top(self.section, title=_('Delete Block'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s %s</h2>') % (_('Deleting Block:'), self.objectdef.name)
r += form.render()
return r.getvalue()
else:
self.objectdef.remove_self()
return redirect('..')
def duplicate(self):
form = Form(enctype='multipart/form-data')
name_widget = form.add(StringWidget, 'name', title=_('Name'), required=True, size=30)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if not form.is_submitted():
original_name = self.objectdef.name
new_name = '%s %s' % (original_name, _('(copy)'))
names = [x.name for x in BlockDef.select()]
no = 2
while new_name in names:
new_name = _('%(name)s (copy %(no)d)') % {'name': original_name, 'no': no}
no += 1
name_widget.set_value(new_name)
if not form.is_submitted() or form.has_errors():
html_top(self.section, title=_('Duplicate Fields Block'))
r = TemplateIO(html=True)
get_response().breadcrumb.append(('duplicate', _('Duplicate')))
r += htmltext('<h2>%s</h2>') % _('Duplicate Fields Block')
r += form.render()
return r.getvalue()
self.objectdef.id = None
self.objectdef.slug = None
self.objectdef.name = form.get_widget('name').parse()
self.objectdef.store()
return redirect('../%s/' % self.objectdef.id)
def export(self):
return misc.xml_response(
self.objectdef,
filename='block-%s.wcs' % self.objectdef.slug,
content_type='application/x-wcs-form',
)
def settings(self):
get_response().breadcrumb.append(('settings', _('Settings')))
form = Form()
form.add(StringWidget, 'name', title=_('Name'), value=self.objectdef.name, size=50)
disabled_slug = bool(self.objectdef.is_used())
widget = form.add(
SlugWidget,
'slug',
value=self.objectdef.slug,
readonly=disabled_slug,
)
if disabled_slug:
widget.hint = _('The identifier can not be modified as the block is in use.')
category_options = get_categories(BlockCategory)
if category_options:
category_options = [(None, '---', '')] + list(category_options)
form.add(
SingleSelectWidget,
'category_id',
title=_('Category'),
options=category_options,
value=self.objectdef.category_id,
)
form.add(
StringWidget,
'digest_template',
title=_('Digest Template'),
value=self.objectdef.digest_template,
size=50,
hint=_('Use block_var_... to refer to fields.'),
)
if not self.objectdef.is_readonly():
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
self.objectdef.name = form.get_widget('name').parse()
if form.get_widget('slug'):
self.objectdef.slug = form.get_widget('slug').parse()
# check there's no other block with the new slug
block_with_slug = BlockDef.get_by_slug(self.objectdef.slug)
if block_with_slug and block_with_slug.id != self.objectdef.id:
form.get_widget('slug').set_error(_('This identifier is already used.'))
if form.get_widget('category_id'):
self.objectdef.category_id = form.get_widget('category_id').parse()
widget_template = form.get_widget('digest_template')
if widget_template.parse() and 'form_var_' in widget_template.parse():
widget_template.set_error(
_('Wrong variable "form_var_…" detected. Please replace it by "block_var_…".')
)
if not form.has_errors():
self.objectdef.digest_template = widget_template.parse()
self.objectdef.store()
return redirect('.')
html_top(self.section, title=_('Settings'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Settings')
r += form.render()
return r.getvalue()
def inspect(self):
self.html_top(self.objectdef.name)
get_response().breadcrumb.append(('inspect', _('Inspector')))
return self.render_inspect()
def render_inspect(self):
context = {'blockdef': self.objectdef, 'view': self}
return template.QommonTemplateResponse(
templates=['wcs/backoffice/block-inspect.html'], context=context
)
class BlocksDirectory(Directory):
_q_exports = ['', 'new', 'categories', ('import', 'p_import')]
do_not_call_in_templates = True
categories = BlockCategoriesDirectory()
def __init__(self, section):
super().__init__()
self.section = section
def _q_traverse(self, path):
if not get_publisher().get_backoffice_root().is_global_accessible('forms'):
raise AccessForbiddenError()
get_response().breadcrumb.append(('blocks/', _('Fields Blocks')))
return super()._q_traverse(path)
def _q_lookup(self, component):
try:
block = BlockDef.get(component)
except KeyError:
raise TraversalError()
return BlockDirectory(self.section, block)
def _q_index(self):
html_top(self.section, title=_('Fields Blocks'))
categories = BlockCategory.select()
BlockCategory.sort_by_position(categories)
blocks = BlockDef.select(order_by='name')
if categories:
categories.append(BlockCategory(_('Misc')))
for category in categories:
category.blocks = [x for x in blocks if x.category_id == category.id]
return template.QommonTemplateResponse(
templates=['wcs/backoffice/blocks.html'],
context={'view': self, 'blocks': blocks, 'categories': categories},
)
def new(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
category_options = get_categories(BlockCategory)
if category_options:
category_options = [(None, '---', '')] + list(category_options)
form.add(
SingleSelectWidget,
'category_id',
title=_('Category'),
options=category_options,
)
form.add_submit('submit', _('Add'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
block = BlockDef(name=form.get_widget('name').parse())
if form.get_widget('category_id'):
block.category_id = form.get_widget('category_id').parse()
block.store()
return redirect('%s/' % block.id)
get_response().breadcrumb.append(('new', _('New Fields Block')))
html_top(self.section, title=_('New Fields Block'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('New Fields Block')
r += form.render()
return r.getvalue()
def p_import(self):
form = Form(enctype='multipart/form-data')
form.add(FileWidget, 'file', title=_('File'), required=True)
form.add_submit('submit', _('Import Fields Block'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
try:
return self.import_submit(form)
except ValueError:
pass
get_response().breadcrumb.append(('import', _('Import')))
html_top(self.section, title=_('Import Fields Block'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Import Fields Block')
r += htmltext('<p>%s</p>') % _('You can install a new fields block by uploading a file.')
r += form.render()
return r.getvalue()
def import_submit(self, form):
fp = form.get_widget('file').parse().fp
error, reason = False, None
try:
blockdef = BlockDef.import_from_xml(fp)
except BlockdefImportError as e:
error = True
reason = _(e.msg)
if e.details:
reason += ' [%s]' % e.details
except ValueError:
error = True
if error:
if reason:
msg = _('Invalid File (%s)') % reason
else:
msg = _('Invalid File')
form.set_error('file', msg)
raise ValueError()
initial_blockdef_name = blockdef.name
blockdef_names = [x.name for x in BlockDef.select()]
copy_no = 1
while blockdef.name in blockdef_names:
if copy_no == 1:
blockdef.name = _('Copy of %s') % initial_blockdef_name
else:
blockdef.name = _('Copy of %(name)s (%(no)d)') % {
'name': initial_blockdef_name,
'no': copy_no,
}
copy_no += 1
blockdef.store()
get_session().message = ('info', _('This fields block has been successfully imported.'))
return redirect('%s/' % blockdef.id)