wcs/wcs/admin/fields.py

569 lines
23 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 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/>.
import copy
import json
from quixote import get_request, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmlescape, htmltext
from wcs import fields
from wcs.admin import utils
from wcs.fields import get_field_options
from wcs.formdef import FormDef
from wcs.qommon import _, errors, get_cfg, misc
from wcs.qommon.admin.menu import command_icon
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.form import CheckboxWidget, Form, HtmlWidget, SingleSelectWidget, StringWidget
from wcs.qommon.substitution import CompatibilityNamesDict
class FieldDefPage(Directory):
_q_exports = ['', 'delete', 'duplicate']
large = False
page_id = None
blacklisted_attributes = []
def __init__(self, objectdef, field_id):
self.objectdef = objectdef
try:
self.field = [x for x in self.objectdef.fields if x.id == field_id][0]
except IndexError:
raise errors.TraversalError()
if not self.field.label:
self.field.label = _('None')
label = misc.ellipsize(self.field.unhtmled_label, 40)
get_response().breadcrumb.append((field_id + '/', label))
def form(self):
form = Form(enctype='multipart/form-data', advanced_label=_('Additional parameters'))
self.field.fill_admin_form(form)
form.widgets = [
x for x in form.widgets if getattr(x, 'name', None) not in self.blacklisted_attributes
]
if not self.objectdef.is_readonly():
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
def _q_index(self):
form = self.form()
redo = False
if form.get_submit() == 'cancel':
return redirect('../#itemId_%s' % self.field.id)
if form.get_widget('items') and form.get_widget('items').get_widget('add_element').parse():
form.clear_errors()
redo = True
if form.is_submitted():
try:
self.field.check_admin_form(form)
except AttributeError:
# informational fields don't have that method
pass
if form.has_errors():
redo = True
if redo or not form.get_submit() == 'submit':
self.html_top(self.objectdef.name)
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % misc.ellipsize(self.field.unhtmled_label, 80)
r += form.render()
return r.getvalue()
self.submit(form)
if form.get_widget('items') is None and self.field.type == 'item':
return redirect('.')
prefill_type = self.field.prefill.get('type') if self.field.prefill else None
prefill_value = self.field.prefill.get('value') if self.field.prefill else None
users_cfg = get_cfg('users', {})
field_email = users_cfg.get('field_email') or 'email'
if self.field.key != 'email' and prefill_type == 'user' and prefill_value == field_email:
get_session().message = (
'warning',
_("\"%s\" is not an email field. Are you sure you want to prefill it with user's email?")
% self.field.label,
)
return redirect('..')
return redirect('../#itemId_%s' % self.field.id)
def submit(self, form):
for f in self.field.get_admin_attributes():
widget = form.get_widget(f)
if not widget:
continue
setattr(self.field, f.replace('-', '_'), widget.parse())
self.objectdef.store(comment=_('Modification of field "%s"') % self.field.ellipsized_label)
def get_deletion_extra_warning(self):
return _('Warning: this field data will be permanently deleted.')
def redirect_field_anchor(self, field):
anchor = '#itemId_%s' % field.id if field else ''
if self.page_id:
return redirect('../%s' % anchor)
else:
return redirect('../../fields/%s' % anchor)
def delete(self):
form = Form(enctype='multipart/form-data')
ellipsized_field_label = misc.ellipsize(self.field.unhtmled_label, 60)
if self.field.type == 'page':
remove_top_title = _('Delete Page')
remove_title = _('Deleting Page: %s') % ellipsized_field_label
remove_message = _("You are about to remove the \"%s\" page.") % ellipsized_field_label
else:
remove_top_title = _('Delete Field')
remove_title = _('Deleting Field: %s') % ellipsized_field_label
remove_message = _("You are about to remove the \"%s\" field.") % ellipsized_field_label
form.widgets.append(HtmlWidget('<p>%s</p>' % remove_message))
if self.field.type not in ('page', 'subtitle', 'title', 'comment'):
warning = self.get_deletion_extra_warning()
if warning:
form.widgets.append(HtmlWidget('<div class="warningnotice">%s</div>' % warning))
current_field_index = self.objectdef.fields.index(self.field)
to_be_deleted = []
if self.field.type == 'page':
# get fields of the page and store indexes for deletion
for index in range(current_field_index + 1, len(self.objectdef.fields)):
field = self.objectdef.fields[index]
if field.type == 'page':
# next page found; break
break
to_be_deleted.append(index)
to_be_deleted.reverse()
# add delete_fields checkbox only if the page has fields
if to_be_deleted:
form.add(CheckboxWidget, 'delete_fields', title=_('Also remove all fields from the page'))
form.add_submit('delete', _('Submit'))
form.add_submit("cancel", _("Cancel"))
if form.get_widget('cancel').parse():
return self.redirect_field_anchor(self.field)
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('delete', _('Delete')))
self.html_top(title=remove_top_title)
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % remove_title
r += form.render()
return r.getvalue()
else:
# delete page fields if requested
delete_fields = form.get_widget('delete_fields')
if delete_fields and delete_fields.parse():
for index in to_be_deleted:
del self.objectdef.fields[index]
# delete current field
del self.objectdef.fields[current_field_index]
self.objectdef.store(comment=_('Deletion of field "%s"') % self.field.ellipsized_label)
# redirect to the field that was above this one
if self.objectdef.fields:
if current_field_index == 0:
above_field = self.objectdef.fields[0]
else:
above_field = self.objectdef.fields[current_field_index - 1]
else:
above_field = None
return self.redirect_field_anchor(above_field)
def duplicate(self):
field_pos = self.objectdef.fields.index(self.field)
fields = self.objectdef.fields
new_field = copy.deepcopy(self.field)
# allocate a new id
new_field.id = self.objectdef.get_new_field_id()
fields.insert(field_pos + 1, new_field)
self.objectdef.store(comment=_('Duplication of field "%s"') % self.field.unhtmled_label)
return self.redirect_field_anchor(new_field)
class FieldsPagesDirectory(Directory):
def __init__(self, parent):
self.parent = parent
def _q_lookup(self, component):
directory = FieldsDirectory(self.parent.objectdef)
directory.field_var_prefix = self.parent.field_var_prefix
directory.html_top = self.parent.html_top
try:
directory.page_id = str(component)
except ValueError:
raise errors.TraversalError()
return directory
class FieldsDirectory(Directory):
_q_exports = ['', 'update_order', 'move_page_fields', 'new', 'pages']
field_def_page_class = FieldDefPage
blacklisted_types = []
page_id = None
field_var_prefix = '..._'
readonly_message = _('The fields are readonly.')
support_import = True
def html_top(self, title, *args, **kwargs):
html_top(self.section, title, *args, **kwargs)
def __init__(self, objectdef):
self.objectdef = objectdef
self.pages = FieldsPagesDirectory(self)
def _q_traverse(self, path):
if self.page_id:
get_response().breadcrumb.append(('pages/%s/' % self.page_id, _('Page')))
else:
get_response().breadcrumb.append(('fields/', _('Fields')))
return Directory._q_traverse(self, path)
def _q_lookup(self, component):
d = self.field_def_page_class(self.objectdef, component)
d.html_top = self.html_top
d.page_id = self.page_id
return d
def _q_index(self):
self.html_top(self.objectdef.name)
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js'])
r = TemplateIO(html=True)
r += self.index_top()
if self.objectdef.fields:
if len(self.objectdef.fields) > 500:
r += htmltext('<div class="errornotice">')
r += htmltext(
_(
'This form contains more than 500 fields. '
'It is close to the database limits and no new fields should be added.'
)
)
r += htmltext('</div>')
if [x for x in self.objectdef.fields if x.type == 'page']:
if self.objectdef.fields[0].type != 'page':
r += htmltext('<div class="errornotice">')
r += htmltext(_('In a multipage form, the first field should be of type "page".'))
r += htmltext('</div>')
r += htmltext('<p class="hint">%s</p>') % _(
'Use drag and drop with the handles to reorder items.'
)
if self.page_id is not None:
r += htmltext('<p>')
r += htmltext('<a href="../../">%s</a>') % _('Display all pages')
r += htmltext('</p>')
extra_classes = []
if [x for x in self.objectdef.fields if x.type == 'page']:
extra_classes.append('multipage')
r += htmltext(
'<ul id="fields-list" class="biglist sortable %s" data-page-no-label="%s">'
% (' '.join(extra_classes), _('Page #%s:') % '***')
)
current_page_no = 0
on_page = False
for field in self.objectdef.fields:
if field.type == 'page':
current_page_no += 1
on_page = bool(str(field.id) == self.page_id)
hidden = ''
if self.page_id and (not on_page or field.type == 'page'):
hidden = 'style="display:none;"'
r += htmltext(
'<li class="biglistitem type-%s" id="itemId_%s" %s>' % (field.type, field.id, hidden)
)
type_label = field.get_type_label()
if field.type in ('subtitle', 'title', 'comment'):
label = misc.ellipsize(field.unhtmled_label, 60)
if field.type in ('subtitle', 'title'):
r += htmltext('<strong id="label%s">%s</strong>') % (field.id, label)
else:
r += htmltext('<span id="label%s">%s</span>') % (field.id, label)
r += htmltext('<p class="details">')
r += htmltext('<span class="type">%s</span>') % _(type_label)
if getattr(field, 'condition', None):
r += htmltext(' - <span class="condition">%s</span>') % _('depending on condition')
r += htmltext('</p>')
r += htmltext('<p class="commands">')
r += command_icon('%s/' % field.id, 'edit')
else:
r += htmltext('<strong class="label" id="label%s">' % field.id)
if field.type == 'page':
r += htmltext('<span class="page-no">%s</span> ') % _('Page #%s:') % current_page_no
r += htmltext('%s</strong>') % field.label
r += htmltext('<p class="details">')
if field.type != 'page':
r += htmltext('<span class="type">%s</span>') % _(type_label)
if hasattr(field, 'required'):
if field.required:
required = ''
else:
required = ' - %s' % _('optional')
r += htmltext('<span class="optional">%s</span>') % required
if getattr(field, 'condition', None):
r += htmltext(' - <span class="condition">%s</span>') % _('depending on condition')
if getattr(field, 'varname', None) and CompatibilityNamesDict.valid_key_regex.match(
field.varname
):
r += htmltext(' - <span class="varname">{{ %s%s }}</span>') % (
self.field_var_prefix,
field.varname,
)
r += htmltext('</p>')
r += htmltext('<p class="commands">')
if field.type == 'page' and self.page_id is None:
r += command_icon(
'pages/%s/' % field.id, 'view', label=_('Limit display to this page')
)
r += command_icon('%s/' % field.id, 'edit')
if not self.objectdef.is_readonly():
r += command_icon('%s/duplicate' % field.id, 'duplicate')
r += command_icon('%s/delete' % field.id, 'remove', popup=True)
r += htmltext('</p></li>')
r += htmltext('</ul>')
if self.objectdef.is_readonly():
get_response().filter['sidebar'] = (
htmltext('<div class="infonotice"><p>%s</p></div>') % self.readonly_message
)
if hasattr(self.objectdef, 'snapshot_object'):
get_response().filter['sidebar'] += utils.snapshot_info_block(
snapshot=self.objectdef.snapshot_object
)
else:
get_response().filter['sidebar'] = str(self.get_new_field_form(self.page_id))
r += self.index_bottom()
return r.getvalue()
def get_new_field_form(self, page_id):
r = TemplateIO(html=True)
r += htmltext('<div id="new-field">')
r += htmltext('<h3>%s</h3>') % _('New Field')
get_request().form = None # ignore the eventual ?page=x
form = Form(enctype='multipart/form-data', action='new')
if page_id:
form.add_hidden('page_id', page_id)
form.add(StringWidget, 'label', title=_('Label'), required=True, size=50)
form.add(
SingleSelectWidget,
'type',
title=_('Type'),
required=True,
options=get_field_options(self.blacklisted_types),
)
form.add_submit('submit', _('Add'))
r += form.render()
if self.support_import:
form = Form(enctype='multipart/form-data', action='new')
if page_id:
form.add_hidden('page_id', page_id)
form.add(
SingleSelectWidget,
'form',
title=_('Or import fields from:'),
required=True,
options=[(None, '----', None)]
+ [
(x.id, x.name, x.id)
for x in FormDef.select(order_by='name', lightweight=True, ignore_errors=True)
],
)
form.add_submit('submit', _('Submit'))
r += form.render()
r += htmltext('</div>')
return r.getvalue()
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<h2>%s') % self.objectdef.name
if self.page_id:
current_page_no = 0
for field in self.objectdef.fields:
if field.type == 'page':
current_page_no += 1
if str(field.id) == self.page_id:
r += ' - '
r += _('page %d') % current_page_no
r += ' - '
r += field.label
r += htmltext('</h2>')
r += get_session().display_message()
if not self.objectdef.fields:
r += htmltext('<div class="infonotice">%s</div>') % _(
'There are not yet any fields for this form.'
)
return r.getvalue()
def index_bottom(self):
pass
def update_order(self):
get_response().set_content_type('application/json')
request = get_request()
if 'element' not in request.form:
return json.dumps({'success': 'ko'})
if 'order' not in request.form:
return json.dumps({'success': 'ko'})
dropped_element = request.form['element']
dropped_page_index = None
new_order = request.form['order'].strip(';').split(';')
new_fields = []
# build new ordered field list
for y in new_order:
for i, x in enumerate(self.objectdef.fields):
if x.id != y:
continue
new_fields.append(x)
# if dropped field is a page, keep it's old index
if x.id == dropped_element and x.type == 'page':
dropped_page_index = i
break
# get the list of dropped page fields from old field list
page_field_ids = []
if dropped_page_index is not None:
for field in self.objectdef.fields[dropped_page_index + 1 :]:
if field.type == 'page':
# next page found; break
break
page_field_ids.append(field.id)
# check new field list composition
if set(self.objectdef.fields) != set(new_fields):
return json.dumps({'success': 'ko'})
self.objectdef.fields = new_fields
self.objectdef.store(comment=_('Change in order of fields'))
if not page_field_ids:
return json.dumps({'success': 'ok'})
# propose to move also page fields
return json.dumps(
{
'success': 'ok',
'additional-action': {
'message': str(_('Also move the fields of the page')),
'url': 'move_page_fields?fields=%s&page=%s' % (';'.join(page_field_ids), dropped_element),
},
}
)
def move_page_fields(self):
request = get_request()
if 'fields' not in request.form:
return redirect('.')
if 'page' not in request.form:
return redirect('.')
field_ids = request.form['fields'].strip(';').split(';')
# keep all fields except page fields
new_fields = [f for f in self.objectdef.fields if f.id not in field_ids]
# find page fields
page_fields = [f for f in self.objectdef.fields if f.id in field_ids]
# find page in new fields, and insert page_fields
for i, field in enumerate(new_fields):
if field.id != request.form['page']:
continue
new_fields = new_fields[: i + 1] + page_fields + new_fields[i + 1 :]
break
# check new field list composition
if set(self.objectdef.fields) != set(new_fields):
return redirect('.')
self.objectdef.fields = new_fields
self.objectdef.store(comment=_('Change in order of fields'))
return redirect('.')
def new(self):
form = Form(enctype='multipart/form-data', action='new')
form.add(StringWidget, 'page_id')
form.add(StringWidget, 'label', title=_('Label'), size=50)
form.add(
SingleSelectWidget, 'type', title=_('Type'), options=get_field_options(self.blacklisted_types)
)
if FormDef.count():
form.add(
SingleSelectWidget,
'form',
title=_('Or import fields from:'),
options=[(x.id, x.name, x.id) for x in FormDef.select(order_by='name', ignore_errors=True)],
)
if not form.is_submitted() or form.has_errors():
get_session().message = ('error', _('Submitted form was not filled properly.'))
return redirect('.')
try:
page_id = form.get_widget('page_id').parse()
except (TypeError, ValueError):
page_id = None
redirect_url = '.'
if page_id is not None:
redirect_url = './?page=%s' % page_id
on_page = False
for i, field in enumerate(self.objectdef.fields):
if field.type == 'page':
if on_page:
break
if str(field.id) == str(page_id):
on_page = True
else:
i += 1
insertion_point = i
else:
insertion_point = len(self.objectdef.fields)
field_type = form.get_widget('type').parse()
if form.get_widget('label').parse() and field_type:
label = form.get_widget('label').parse()
if field_type == 'comment' and not label.startswith('<'):
label = '<p>%s</p>' % htmlescape(label)
field = fields.get_field_class_by_type(field_type)(
label=label, type=field_type, id=self.objectdef.get_new_field_id()
)
self.objectdef.fields.insert(insertion_point, field)
self.objectdef.store(comment=_('New field "%s"') % field.ellipsized_label)
elif form.get_widget('form') and form.get_widget('form').parse():
formdef = FormDef.get(form.get_widget('form').parse())
for j, field in enumerate(formdef.fields):
field.id = self.objectdef.get_new_field_id()
self.objectdef.fields.insert(insertion_point + j, field)
self.objectdef.store(comment=_('Import of fields from "%s"') % formdef.name)
else:
get_session().message = ('error', _('Submitted form was not filled properly.'))
return redirect(redirect_url)
return redirect(redirect_url)