2074 lines
81 KiB
Python
2074 lines
81 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 difflib
|
|
import io
|
|
import xml.etree.ElementTree as ET
|
|
from collections import defaultdict
|
|
|
|
from quixote import get_publisher, get_request, get_response, get_session, redirect
|
|
from quixote.directory import AccessControlled, Directory
|
|
from quixote.html import TemplateIO, htmlescape, htmltext
|
|
|
|
from wcs.backoffice.applications import ApplicationsDirectory
|
|
from wcs.backoffice.deprecations import DeprecationsDirectory
|
|
from wcs.backoffice.snapshots import SnapshotsDirectory
|
|
from wcs.carddef import CardDef
|
|
from wcs.categories import Category
|
|
from wcs.formdef import (
|
|
DRAFTS_DEFAULT_LIFESPAN,
|
|
FormDef,
|
|
FormdefImportError,
|
|
FormdefImportRecoverableError,
|
|
UpdateDigestAfterJob,
|
|
)
|
|
from wcs.forms.common import TempfileDirectoryMixin
|
|
from wcs.forms.root import qrcode
|
|
from wcs.qommon import _, force_str, misc, pgettext_lazy, template
|
|
from wcs.qommon.afterjobs import AfterJob
|
|
from wcs.qommon.errors import AccessForbiddenError, TraversalError
|
|
from wcs.qommon.form import (
|
|
CheckboxesWidget,
|
|
CheckboxWidget,
|
|
DateTimeWidget,
|
|
DateWidget,
|
|
FileWidget,
|
|
Form,
|
|
HtmlWidget,
|
|
OptGroup,
|
|
SingleSelectWidget,
|
|
SlugWidget,
|
|
StringWidget,
|
|
UrlWidget,
|
|
WcsExtraStringWidget,
|
|
WidgetList,
|
|
WysiwygTextWidget,
|
|
)
|
|
from wcs.qommon.misc import localstrftime
|
|
from wcs.roles import get_user_roles, logged_users_role
|
|
from wcs.sql_criterias import Equal, Null, StrictNotEqual
|
|
from wcs.workflows import Workflow
|
|
|
|
from . import utils
|
|
from .blocks import BlocksDirectory
|
|
from .categories import CategoriesDirectory, get_categories
|
|
from .data_sources import NamedDataSourcesDirectory
|
|
from .fields import FieldDefPage, FieldsDirectory
|
|
from .logged_errors import LoggedErrorsDirectory
|
|
|
|
|
|
def is_global_accessible(section):
|
|
return get_publisher().get_backoffice_root().is_global_accessible(section)
|
|
|
|
|
|
class FormDefUI:
|
|
formdef_class = FormDef
|
|
category_class = Category
|
|
section = 'forms'
|
|
|
|
def __init__(self, formdef):
|
|
self.formdef = formdef
|
|
|
|
def get_categories(self):
|
|
global_access = is_global_accessible(self.section)
|
|
user_roles = set(get_request().user.get_roles())
|
|
|
|
def filter_function(category):
|
|
if global_access:
|
|
return True
|
|
management_roles = {x.id for x in getattr(category, 'management_roles') or []}
|
|
return bool(user_roles.intersection(management_roles))
|
|
|
|
return get_categories(self.category_class, filter_function=filter_function)
|
|
|
|
@classmethod
|
|
def get_workflows(cls, formdef_category=None):
|
|
default_workflow = cls.formdef_class.get_default_workflow()
|
|
t = sorted(
|
|
(misc.simplify(x.name), x.category.name if x.category else '', x.id, x.name, x.id)
|
|
for x in Workflow.select()
|
|
if x.possible_status
|
|
)
|
|
workflows_by_category_names = defaultdict(list)
|
|
for x in t:
|
|
workflows_by_category_names[x[1]].append(x[2:])
|
|
category_names = list(workflows_by_category_names.keys())
|
|
if len(category_names) == 1 and category_names[0] == '':
|
|
# no category found
|
|
return [(None, default_workflow.name, '')] + [x[2:] for x in t]
|
|
options = []
|
|
options.append((None, default_workflow.name, ''))
|
|
# first options, workflows with the same category name as updated formdef
|
|
if formdef_category and formdef_category.name in workflows_by_category_names:
|
|
options.append(OptGroup(formdef_category.name))
|
|
options.extend(workflows_by_category_names[formdef_category.name])
|
|
# then other categories
|
|
for name in sorted(category_names):
|
|
if not name:
|
|
continue
|
|
if formdef_category and formdef_category.name == name:
|
|
continue
|
|
options.append(OptGroup(name))
|
|
options.extend(workflows_by_category_names[name])
|
|
# and workflows without category
|
|
options.append(OptGroup(_('Without category')))
|
|
options.extend(workflows_by_category_names[''])
|
|
return options
|
|
|
|
def new_form_ui(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
if self.formdef:
|
|
formdef = self.formdef
|
|
else:
|
|
formdef = self.formdef_class()
|
|
form.add(
|
|
StringWidget, 'name', title=_('Name'), required=True, size=40, value=formdef.name, maxlength=250
|
|
)
|
|
categories = self.get_categories()
|
|
if categories:
|
|
if is_global_accessible(self.section):
|
|
categories = [(None, '---', '')] + list(categories)
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'category_id',
|
|
title=_('Category'),
|
|
value=formdef.category_id,
|
|
options=categories,
|
|
)
|
|
workflows = self.get_workflows()
|
|
if len(workflows) > 1:
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'workflow_id',
|
|
title=_('Workflow'),
|
|
value=formdef.workflow_id,
|
|
options=workflows,
|
|
**{'data-autocomplete': 'true'},
|
|
)
|
|
if not formdef.is_readonly():
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
return form
|
|
|
|
def submit_form(self, form):
|
|
if self.formdef:
|
|
formdef = self.formdef
|
|
else:
|
|
formdef = self.formdef_class()
|
|
|
|
name = form.get_widget('name').parse()
|
|
formdefs_name = [
|
|
x.name
|
|
for x in self.formdef_class.select(ignore_errors=True, lightweight=True)
|
|
if x.id != formdef.id
|
|
]
|
|
if name in formdefs_name:
|
|
form.get_widget('name').set_error(_('This name is already used.'))
|
|
raise ValueError()
|
|
|
|
for f in (
|
|
'name',
|
|
'confirmation',
|
|
'category_id',
|
|
'disabled',
|
|
'enable_tracking_codes',
|
|
'workflow_id',
|
|
'disabled_redirection',
|
|
'publication_date',
|
|
'expiration_date',
|
|
):
|
|
widget = form.get_widget(f)
|
|
if widget:
|
|
setattr(formdef, f, widget.parse())
|
|
|
|
if not formdef.fields:
|
|
formdef.fields = []
|
|
|
|
formdef.store()
|
|
|
|
return formdef
|
|
|
|
|
|
class FormFieldDefPage(FieldDefPage):
|
|
section = 'forms'
|
|
deletion_extra_warning_message = _(
|
|
'Warning: this field data will be permanently deleted from existing forms.'
|
|
)
|
|
|
|
def get_deletion_extra_warning(self):
|
|
if not self.objectdef.data_class().count():
|
|
return None
|
|
return self.deletion_extra_warning_message
|
|
|
|
|
|
class FormFieldsDirectory(FieldsDirectory):
|
|
field_def_page_class = FormFieldDefPage
|
|
field_var_prefix = 'form_var_'
|
|
readonly_message = _('This form is readonly.')
|
|
|
|
def index_bottom(self):
|
|
if self.objectdef.is_readonly():
|
|
return
|
|
if hasattr(self.objectdef, 'disabled') and self.objectdef.disabled:
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<div class="warningnotice">')
|
|
r += str(_('This form is currently disabled.'))
|
|
if hasattr(self.objectdef, 'disabled_redirection') and self.objectdef.disabled_redirection:
|
|
r += htmltext(' (<a href="%s">') % self.objectdef.disabled_redirection
|
|
r += str(_('redirection'))
|
|
r += htmltext('</a>)')
|
|
r += htmltext(' <a href="../enable?back=fields">%s</a>') % _('Enable')
|
|
r += htmltext('</div>')
|
|
return r.getvalue()
|
|
|
|
|
|
class OptionsDirectory(Directory):
|
|
category_class = Category
|
|
category_empty_choice = _('Select a category for this form')
|
|
section = 'forms'
|
|
|
|
_q_exports = [
|
|
'confirmation',
|
|
'tracking_code',
|
|
'online_status',
|
|
'captcha',
|
|
'description',
|
|
'keywords',
|
|
'category',
|
|
'management',
|
|
'appearance',
|
|
'templates',
|
|
'user_support',
|
|
]
|
|
|
|
def __init__(self, formdef, formdefui):
|
|
self.formdef = formdef
|
|
self.changed = False
|
|
self.formdefui = formdefui
|
|
|
|
def confirmation(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
CheckboxWidget,
|
|
'confirmation',
|
|
title=_('Include confirmation page'),
|
|
value=self.formdef.confirmation,
|
|
)
|
|
return self.handle(form, _('Confirmation Page'))
|
|
|
|
def tracking_code(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
|
|
form.widgets.append(HtmlWidget(htmltext('<h3>%s</h3>') % _('Draft')))
|
|
widget = form.add(
|
|
WcsExtraStringWidget,
|
|
'drafts_lifespan',
|
|
title=_('Lifespan of drafts (in days)'),
|
|
value=self.formdef.drafts_lifespan,
|
|
hint=_('By default drafts are removed after %s days.') % DRAFTS_DEFAULT_LIFESPAN,
|
|
)
|
|
|
|
def check_lifespan(value):
|
|
try:
|
|
return bool(int(value) >= 2 and int(value) <= 100)
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
widget.validation_function = check_lifespan
|
|
widget.validation_function_error_message = _('Lifespan must be between 2 and 100 days.')
|
|
|
|
form.widgets.append(HtmlWidget(htmltext('<h3>%s</h3>') % _('Tracking Code')))
|
|
form.add(
|
|
CheckboxWidget,
|
|
'enable_tracking_codes',
|
|
title=_('Enable support for tracking codes'),
|
|
value=self.formdef.enable_tracking_codes,
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
)
|
|
verify_fields = [(None, '---', None)]
|
|
for field in self.formdef.fields:
|
|
if field.key in ('string', 'date', 'email', 'computed'):
|
|
verify_fields.append((field.id, field.label, field.id))
|
|
form.add(
|
|
WidgetList,
|
|
'tracking_code_verify_fields',
|
|
title=_('Fields to check after entering the tracking code'),
|
|
element_type=SingleSelectWidget,
|
|
value=self.formdef.tracking_code_verify_fields,
|
|
add_element_label=_('Add verification Field'),
|
|
element_kwargs={'render_br': False, 'options': verify_fields},
|
|
hint=_('Only text, date, email and computed fields can be used.'),
|
|
attrs={
|
|
'data-dynamic-display-child-of': 'enable_tracking_codes',
|
|
'data-dynamic-display-checked': 'true',
|
|
},
|
|
)
|
|
|
|
return self.handle(form, _('Form Tracking'))
|
|
|
|
def captcha(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
CheckboxWidget,
|
|
'has_captcha',
|
|
title=_('Prepend a CAPTCHA page for anonymous users'),
|
|
value=self.formdef.has_captcha,
|
|
)
|
|
return self.handle(form, _('CAPTCHA'))
|
|
|
|
def management(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
CheckboxWidget,
|
|
'include_download_all_button',
|
|
title=_('Include button to download all files'),
|
|
value=self.formdef.include_download_all_button,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'skip_from_360_view',
|
|
title=_('Skip from per user view'),
|
|
value=self.formdef.skip_from_360_view,
|
|
)
|
|
return self.handle(form, _('Management'))
|
|
|
|
def online_status(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(CheckboxWidget, 'disabled', title=_('Disable access to form'), value=self.formdef.disabled)
|
|
form.add(
|
|
StringWidget,
|
|
'disabled_redirection',
|
|
title=_('If disabled, redirect to this URL'),
|
|
size=40,
|
|
hint=_(
|
|
'Redirection will only be performed if the form is disabled and a URL is given. '
|
|
'Common variables are available with the {{variable}} syntax.'
|
|
),
|
|
value=self.formdef.disabled_redirection,
|
|
)
|
|
form.add(
|
|
DateTimeWidget,
|
|
'publication_date',
|
|
title=_('Publication Date'),
|
|
value=self.formdef.publication_date,
|
|
)
|
|
form.add(
|
|
DateTimeWidget, 'expiration_date', title=_('Expiration Date'), value=self.formdef.expiration_date
|
|
)
|
|
return self.handle(form, _('Online Status'))
|
|
|
|
def description(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(WysiwygTextWidget, 'description', title=_('Description'), value=self.formdef.description)
|
|
return self.handle(form, _('Description'))
|
|
|
|
def keywords(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
StringWidget,
|
|
'keywords',
|
|
title=_('Keywords'),
|
|
value=self.formdef.keywords,
|
|
size=50,
|
|
hint=_('Keywords need to be separated with commas.'),
|
|
)
|
|
return self.handle(form, _('Keywords'))
|
|
|
|
def category(self):
|
|
categories = self.formdefui.get_categories()
|
|
if is_global_accessible(self.section):
|
|
categories = [(None, '---', '')] + list(categories)
|
|
form = Form(enctype='multipart/form-data')
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % self.category_empty_choice))
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'category_id',
|
|
title=_('Category'),
|
|
value=self.formdef.category_id,
|
|
options=list(categories),
|
|
)
|
|
return self.handle(form, _('Category'))
|
|
|
|
def appearance(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
StringWidget,
|
|
'appearance_keywords',
|
|
title=_('Appearance keywords'),
|
|
value=self.formdef.appearance_keywords,
|
|
size=50,
|
|
hint=_(
|
|
'Serie of keywords to alter form appearance using CSS or '
|
|
'custom templates, separated by spaces.'
|
|
),
|
|
)
|
|
return self.handle(form, _('Appearance'))
|
|
|
|
def get_templates_form(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
StringWidget,
|
|
'digest_template',
|
|
title=_('Digest'),
|
|
value=self.formdef.default_digest_template,
|
|
size=50,
|
|
)
|
|
form.add(
|
|
WysiwygTextWidget,
|
|
'lateral_template',
|
|
title=_('Lateral Block'),
|
|
value=self.formdef.lateral_template,
|
|
)
|
|
form.add(
|
|
WysiwygTextWidget,
|
|
'submission_lateral_template',
|
|
title=_('Submission Lateral Block'),
|
|
value=self.formdef.submission_lateral_template,
|
|
)
|
|
return form
|
|
|
|
def templates(self):
|
|
form = self.get_templates_form()
|
|
result = self.handle(form, _('Templates'))
|
|
if self.changed and self.formdef.data_class().count():
|
|
get_response().add_after_job(UpdateDigestAfterJob(formdefs=[self.formdef]))
|
|
if isinstance(self.formdef, CardDef):
|
|
get_session().message = ('info', _('Existing cards will be updated in the background.'))
|
|
else:
|
|
get_session().message = ('info', _('Existing forms will be updated in the background.'))
|
|
return result
|
|
|
|
def user_support(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'user_support',
|
|
title=_('User support'),
|
|
value=self.formdef.user_support,
|
|
options=[
|
|
(None, pgettext_lazy('user_support', 'No'), ''),
|
|
('optional', pgettext_lazy('user_support', 'Optional'), 'optional'),
|
|
],
|
|
)
|
|
return self.handle(form, _('User support'))
|
|
|
|
def handle(self, form, title):
|
|
if not self.formdef.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():
|
|
attrs = [
|
|
'confirmation',
|
|
'disabled',
|
|
'enable_tracking_codes',
|
|
'tracking_code_verify_fields',
|
|
'disabled_redirection',
|
|
'publication_date',
|
|
'expiration_date',
|
|
'has_captcha',
|
|
'description',
|
|
'keywords',
|
|
'category_id',
|
|
'skip_from_360_view',
|
|
'appearance_keywords',
|
|
'include_download_all_button',
|
|
'digest_template',
|
|
'lateral_template',
|
|
'id_template',
|
|
'submission_lateral_template',
|
|
'drafts_lifespan',
|
|
'user_support',
|
|
]
|
|
for attr in attrs:
|
|
widget = form.get_widget(attr)
|
|
if widget:
|
|
if hasattr(self, 'clean_%s' % attr):
|
|
has_error = getattr(self, 'clean_%s' % attr)(form)
|
|
if has_error:
|
|
continue
|
|
new_value = widget.parse()
|
|
if attr == 'digest_template':
|
|
if self.formdef.default_digest_template != new_value:
|
|
self.changed = True
|
|
if not self.formdef.digest_templates:
|
|
self.formdef.digest_templates = {}
|
|
self.formdef.digest_templates['default'] = new_value
|
|
elif attr == 'id_template':
|
|
if self.formdef.id_template != new_value:
|
|
self.changed = True
|
|
self.formdef.id_template = new_value
|
|
else:
|
|
if getattr(self.formdef, attr, None) != new_value:
|
|
setattr(self.formdef, attr, new_value)
|
|
if not form.has_errors():
|
|
self.formdef.store(comment=_('Changed "%s" parameters') % title)
|
|
return redirect('..')
|
|
|
|
get_response().set_title(self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % title
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def clean_digest_template(self, form):
|
|
if not isinstance(self.formdef, CardDef):
|
|
return False
|
|
|
|
widget = form.get_widget('digest_template')
|
|
new_value = widget.parse()
|
|
if new_value:
|
|
return False
|
|
|
|
if any(self.formdef.usage_in_formdefs()):
|
|
widget.set_error(
|
|
_('Can not empty digest template: this card model is used as data source in some forms.')
|
|
)
|
|
return True
|
|
|
|
return False
|
|
|
|
def _q_traverse(self, path):
|
|
get_response().breadcrumb.append((path[0] + '/', self.formdef.name))
|
|
return super()._q_traverse(path)
|
|
|
|
|
|
class WorkflowRoleDirectory(Directory):
|
|
def __init__(self, formdef):
|
|
self.formdef = formdef
|
|
|
|
def _q_lookup(self, component):
|
|
if component not in self.formdef.workflow.roles:
|
|
raise TraversalError()
|
|
|
|
if not self.formdef.workflow_roles:
|
|
self.formdef.workflow_roles = {}
|
|
role_id = self.formdef.workflow_roles.get(component)
|
|
|
|
options = [(None, '---', None)]
|
|
options.extend(get_user_roles())
|
|
form = Form(enctype='multipart/form-data')
|
|
form.add(SingleSelectWidget, 'role_id', value=role_id, options=options)
|
|
if not self.formdef.is_readonly():
|
|
form.add_submit('submit', _('Submit'))
|
|
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(('role/%s' % component, _('Workflow Role')))
|
|
get_response().set_title(title=self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Role')
|
|
r += htmltext('<p>%s</p>') % self.formdef.workflow.roles.get(component)
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
self.formdef.workflow_roles[component] = form.get_widget('role_id').parse()
|
|
self.formdef.store(
|
|
comment=_('Change in function "%s"') % self.formdef.workflow.roles.get(component)
|
|
)
|
|
if self.formdef.data_class().exists():
|
|
# instruct formdef to update its security rules
|
|
def update(job=None):
|
|
self.formdef.data_class().rebuild_security()
|
|
|
|
get_response().add_after_job(_('Reindexing data after function change'), update)
|
|
return redirect('..')
|
|
|
|
|
|
class FormDefPage(Directory, TempfileDirectoryMixin):
|
|
do_not_call_in_templates = True
|
|
_q_exports = [
|
|
'',
|
|
'fields',
|
|
'delete',
|
|
'duplicate',
|
|
'export',
|
|
'anonymise',
|
|
'enable',
|
|
'workflow',
|
|
'role',
|
|
('workflow-options', 'workflow_options'),
|
|
('workflow-variables', 'workflow_variables'),
|
|
('workflow-status-remapping', 'workflow_status_remapping'),
|
|
'roles',
|
|
'title',
|
|
'options',
|
|
'overwrite',
|
|
'qrcode',
|
|
'information',
|
|
'inspect',
|
|
'tempfile',
|
|
'tests',
|
|
('public-url', 'public_url'),
|
|
('backoffice-submission-roles', 'backoffice_submission_roles'),
|
|
('logged-errors', 'logged_errors_dir'),
|
|
('history', 'snapshots_dir'),
|
|
]
|
|
|
|
formdef_class = FormDef
|
|
formdef_export_prefix = 'form'
|
|
formdef_ui_class = FormDefUI
|
|
formdef_default_workflow = '_default'
|
|
section = 'forms'
|
|
options_directory_class = OptionsDirectory
|
|
fields_directory_class = FormFieldsDirectory
|
|
|
|
delete_message = _('You are about to irrevocably delete this form.')
|
|
delete_title = _('Deleting Form:')
|
|
duplicate_title = _('Duplicate Form')
|
|
overwrite_message = _('You can replace this form by uploading a file or by pointing to a form URL.')
|
|
overwrite_success_message = _(
|
|
'The form has been successfully overwritten. '
|
|
'Do note it kept its existing address and role and workflow parameters.'
|
|
)
|
|
backoffice_submission_role_label = _('Backoffice Submission Roles')
|
|
backoffice_submission_role_description = (
|
|
_('Select the roles that will be allowed to fill out forms of this kind in the backoffice.'),
|
|
)
|
|
formdef_template_name = 'wcs/backoffice/formdef.html'
|
|
inspect_template_name = 'wcs/backoffice/formdef-inspect.html'
|
|
|
|
def __init__(self, component, instance=None):
|
|
from .tests import TestsDirectory
|
|
|
|
try:
|
|
self.formdef = instance or self.formdef_class.get(component)
|
|
except KeyError:
|
|
raise TraversalError()
|
|
self.formdefui = self.formdef_ui_class(self.formdef)
|
|
if component:
|
|
get_response().breadcrumb.append((component + '/', self.formdef.name))
|
|
self.fields = self.fields_directory_class(self.formdef)
|
|
self.role = WorkflowRoleDirectory(self.formdef)
|
|
self.options = self.options_directory_class(self.formdef, self.formdefui)
|
|
self.tests = TestsDirectory(self.formdef)
|
|
self.logged_errors_dir = LoggedErrorsDirectory(
|
|
parent_dir=self, formdef_class=self.formdef_class, formdef_id=self.formdef.id
|
|
)
|
|
self.snapshots_dir = SnapshotsDirectory(self.formdef)
|
|
|
|
def add_option_line(self, link, label, current_value, popup=True):
|
|
return htmltext(
|
|
'<li><a rel="%(popup)s" href="%(link)s">'
|
|
'<span class="label">%(label)s</span> '
|
|
'<span class="value">%(current_value)s</span>'
|
|
'</a></li>'
|
|
% {
|
|
'popup': 'popup' if popup else '',
|
|
'link': link,
|
|
'label': label,
|
|
'current_value': htmlescape(current_value),
|
|
}
|
|
)
|
|
|
|
def _q_index(self):
|
|
from wcs.applications import Application
|
|
|
|
get_response().set_title(title=self.formdef.name)
|
|
get_response().add_javascript(
|
|
['popup.js', 'widget_list.js', 'qommon.wysiwyg.js', 'qommon.fileupload.js', 'select2.js']
|
|
)
|
|
DateWidget.prepare_javascript()
|
|
|
|
user = get_request().user
|
|
if not self.formdef.is_readonly():
|
|
Application.load_for_object(self.formdef)
|
|
context = {
|
|
'test_result': self.last_test_result(),
|
|
'has_qrcode': bool(qrcode is not None),
|
|
'view': self,
|
|
'formdef': self.formdef,
|
|
'form_preview': self.get_preview(), # get media
|
|
'has_sidebar': True,
|
|
'include_management_link': bool(user.is_admin or self.formdef.is_of_concern_for_user(user)),
|
|
'options': self.get_option_lines(),
|
|
'has_captcha_option': get_publisher().has_site_option('formdef-captcha-option'),
|
|
'has_appearance_keywords': get_publisher().has_site_option('formdef-appearance-keywords'),
|
|
}
|
|
return template.QommonTemplateResponse(
|
|
templates=[self.formdef_template_name],
|
|
context=context,
|
|
is_django_native=True,
|
|
)
|
|
|
|
def snapshot_info_block(self):
|
|
return utils.snapshot_info_block(snapshot=self.formdef.snapshot_object)
|
|
|
|
def last_modification_block(self):
|
|
return utils.last_modification_block(obj=self.formdef)
|
|
|
|
def last_test_result(self):
|
|
from wcs.testdef import TestDef, TestResult
|
|
|
|
criterias = [Equal('object_type', self.formdef.get_table_name()), Equal('object_id', self.formdef.id)]
|
|
|
|
if not TestDef.count(criterias):
|
|
return
|
|
|
|
test_results = TestResult.select(criterias, order_by='-id')
|
|
|
|
if not test_results:
|
|
return
|
|
|
|
result = test_results[0]
|
|
result.formatted_timestamp = localstrftime(result.timestamp)
|
|
return result
|
|
|
|
def errors_block(self):
|
|
return LoggedErrorsDirectory.errors_block(
|
|
formdef_class=self.formdef_class, formdef_id=self.formdef.id
|
|
)
|
|
|
|
def get_option_lines(self):
|
|
options = {
|
|
'description': self.add_option_line(
|
|
'options/description',
|
|
_('Description'),
|
|
self.formdef.description
|
|
and pgettext_lazy('description', 'On')
|
|
or pgettext_lazy('description', 'None'),
|
|
),
|
|
'keywords': self.add_option_line(
|
|
'options/keywords',
|
|
_('Keywords'),
|
|
self.formdef.keywords and self.formdef.keywords or pgettext_lazy('keywords', 'None'),
|
|
),
|
|
'category': self.add_option_line(
|
|
'options/category',
|
|
_('Category'),
|
|
self.formdef.category_id
|
|
and self.formdef.category
|
|
and self.formdef.category.name
|
|
or pgettext_lazy('category', 'None'),
|
|
),
|
|
'user_roles': self.add_option_line(
|
|
'roles', _('User Roles'), self.get_roles_label_and_auth_context()
|
|
),
|
|
'backoffice_submission_roles': self.add_option_line(
|
|
'backoffice-submission-roles',
|
|
self.backoffice_submission_role_label,
|
|
self._get_roles_label('backoffice_submission_roles'),
|
|
),
|
|
'confirmation': self.add_option_line(
|
|
'options/confirmation',
|
|
_('Confirmation Page'),
|
|
self.formdef.confirmation
|
|
and pgettext_lazy('confirmation page', 'Enabled')
|
|
or pgettext_lazy('confirmation page', 'Disabled'),
|
|
),
|
|
'management': self.add_option_line(
|
|
'options/management',
|
|
_('Management'),
|
|
_('Custom')
|
|
if (self.formdef.skip_from_360_view or self.formdef.include_download_all_button)
|
|
else _('Default'),
|
|
),
|
|
'tracking_code': self.add_option_line(
|
|
'options/tracking_code',
|
|
_('Form Tracking'),
|
|
self.formdef.enable_tracking_codes
|
|
and pgettext_lazy('tracking code', 'Enabled')
|
|
or pgettext_lazy('tracking code', 'Disabled'),
|
|
),
|
|
'captcha': self.add_option_line(
|
|
'options/captcha',
|
|
_('CAPTCHA for anonymous users'),
|
|
self.formdef.has_captcha
|
|
and pgettext_lazy('captcha', 'Enabled')
|
|
or pgettext_lazy('captcha', 'Disabled'),
|
|
),
|
|
'appearance': self.add_option_line(
|
|
'options/appearance',
|
|
_('Appearance'),
|
|
self.formdef.appearance_keywords
|
|
and self.formdef.appearance_keywords
|
|
or pgettext_lazy('appearance', 'Standard'),
|
|
),
|
|
}
|
|
unknown_wf = self.formdef.workflow.id == Workflow.get_unknown_workflow().id
|
|
if get_publisher().get_backoffice_root().is_accessible('workflows') and not unknown_wf:
|
|
# custom option line to also include a link to the workflow itself.
|
|
options['workflow'] = htmltext(
|
|
'<li><a rel="popup" href="%(link)s">'
|
|
'<span class="label">%(label)s</span> '
|
|
'<span class="value offset">%(current_value)s</span>'
|
|
'</a>'
|
|
'<a class="extra-link" title="%(title)s" href="%(workflow_url)s">↗</a>'
|
|
'</li>'
|
|
% {
|
|
'link': 'workflow',
|
|
'label': _('Workflow'),
|
|
'title': _('Open workflow page'),
|
|
'workflow_url': self.formdef.workflow.get_admin_url(),
|
|
'current_value': self.formdef.workflow.name or '-',
|
|
}
|
|
)
|
|
else:
|
|
options['workflow'] = self.add_option_line(
|
|
'workflow', _('Workflow'), self.formdef.workflow and self.formdef.workflow.name or '-'
|
|
)
|
|
|
|
options['workflow_options'] = ''
|
|
if self.formdef.workflow_id:
|
|
pristine_workflow = Workflow.get(self.formdef.workflow_id, ignore_errors=True)
|
|
if pristine_workflow and pristine_workflow.variables_formdef:
|
|
options['workflow_options'] = self.add_option_line('workflow-variables', _('Options'), '')
|
|
elif self.formdef.workflow_options and get_publisher().has_site_option(
|
|
'enable-workflow-variable-parameter'
|
|
):
|
|
# there are no variables defined but there are some values
|
|
# in workflow_options, this is probably the legacy stuff.
|
|
if any(x for x in self.formdef.workflow_options if '*' in x):
|
|
options['workflow_options'] = self.add_option_line('workflow-options', _('Options'), '')
|
|
|
|
options['workflow_roles_list'] = []
|
|
if self.formdef.workflow.roles:
|
|
for wf_role_id, wf_role_label, role_label in self.get_workflow_roles_elements():
|
|
options['workflow_roles_list'].append(
|
|
self.add_option_line('role/%s' % wf_role_id, wf_role_label, role_label)
|
|
)
|
|
|
|
if (
|
|
self.formdef.default_digest_template
|
|
or self.formdef.lateral_template
|
|
or self.formdef.submission_lateral_template
|
|
or self.formdef.id_template
|
|
):
|
|
template_status = pgettext_lazy('template', 'Custom')
|
|
else:
|
|
template_status = pgettext_lazy('template', 'None')
|
|
options['templates'] = self.add_option_line(
|
|
'options/templates', _('Templates'), template_status, popup=False
|
|
)
|
|
|
|
online_status = pgettext_lazy('online status', 'Active')
|
|
if self.formdef.disabled:
|
|
# manually disabled
|
|
online_status = pgettext_lazy('online status', 'Disabled')
|
|
if self.formdef.disabled_redirection:
|
|
online_status = _('Redirected')
|
|
elif self.formdef.is_disabled():
|
|
# disabled by date
|
|
online_status = pgettext_lazy('online status', 'Inactive by date')
|
|
options['online_status'] = self.add_option_line(
|
|
'options/online_status', _('Online Status'), online_status
|
|
)
|
|
return options
|
|
|
|
def get_workflow_roles_elements(self):
|
|
if not self.formdef.workflow_roles:
|
|
self.formdef.workflow_roles = {}
|
|
workflow_roles = list((self.formdef.workflow.roles or {}).items())
|
|
workflow_roles.sort(key=lambda x: '' if x[0] == '_receiver' else misc.simplify(x[1]))
|
|
for wf_role_id, wf_role_label in workflow_roles:
|
|
role_id = self.formdef.workflow_roles.get(wf_role_id)
|
|
if role_id:
|
|
try:
|
|
role = get_publisher().role_class.get(role_id)
|
|
role_label = role.name
|
|
except KeyError:
|
|
# removed role ?
|
|
role_label = _('Unknown role (%s)') % role_id
|
|
else:
|
|
role_label = '-'
|
|
yield (wf_role_id, wf_role_label, role_label)
|
|
|
|
def _get_roles_label(self, attribute):
|
|
if getattr(self.formdef, attribute):
|
|
roles = []
|
|
for x in getattr(self.formdef, attribute):
|
|
if x == logged_users_role().id:
|
|
roles.append(logged_users_role().name)
|
|
else:
|
|
try:
|
|
roles.append(get_publisher().role_class.get(x).name)
|
|
except KeyError:
|
|
# removed role ?
|
|
roles.append(_('Unknown role (%s)') % x)
|
|
value = htmltext(', ').join([str(x) for x in roles])
|
|
else:
|
|
value = pgettext_lazy('roles', 'None')
|
|
return value
|
|
|
|
def get_roles_label_and_auth_context(self):
|
|
value = self._get_roles_label('roles')
|
|
if self.formdef.required_authentication_contexts:
|
|
auth_contexts = get_publisher().get_supported_authentication_contexts()
|
|
value += ' (%s)' % ', '.join(
|
|
[
|
|
str(auth_contexts.get(x))
|
|
for x in self.formdef.required_authentication_contexts
|
|
if auth_contexts.get(x)
|
|
]
|
|
)
|
|
return value
|
|
|
|
def public_url(self):
|
|
get_response().set_title(title=self.formdef.name)
|
|
get_response().breadcrumb.append(('public-url', _('Public URL')))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>' % _('Public URL'))
|
|
r += htmltext('<div>')
|
|
r += htmltext('<p>%s</p>') % _('The public URL of this form is:')
|
|
url = self.formdef.get_url()
|
|
r += htmltext('<a href="%s">%s</a>') % (url, url)
|
|
r += htmltext('</div>')
|
|
return r.getvalue()
|
|
|
|
def qrcode(self):
|
|
get_response().set_title(title=self.formdef.name)
|
|
get_response().breadcrumb.append(('qrcode', _('QR Code')))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>' % _('QR Code'))
|
|
r += htmltext('<div id="qrcode">')
|
|
r += htmltext('<img width="410px" height="410px" src="%sqrcode" alt=""/>' % self.formdef.get_url())
|
|
r += htmltext('<a href="%sqrcode?download">%s</a></p>') % (self.formdef.get_url(), _('Download'))
|
|
r += htmltext('</div>')
|
|
return r.getvalue()
|
|
|
|
def _roles_selection(self, title, attribute, description=None, include_logged_users_role=True):
|
|
form = Form(enctype='multipart/form-data')
|
|
options = [(None, '---', None)]
|
|
if include_logged_users_role:
|
|
options.append((logged_users_role().id, logged_users_role().name, logged_users_role().id))
|
|
options += get_user_roles()
|
|
form.add(
|
|
WidgetList,
|
|
'roles',
|
|
element_type=SingleSelectWidget,
|
|
value=getattr(self.formdef, attribute),
|
|
add_element_label=_('Add Role'),
|
|
element_kwargs={'render_br': False, 'options': options},
|
|
)
|
|
if attribute == 'roles':
|
|
# additional options
|
|
form.add(
|
|
CheckboxWidget,
|
|
'only_allow_one',
|
|
title=_('Only allow one form per user'),
|
|
hint=_(
|
|
'This option concerns logged in users only. Form access must be restricted for this to be effective.'
|
|
),
|
|
value=self.formdef.only_allow_one,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'always_advertise',
|
|
title=_('Advertise to unlogged users'),
|
|
value=self.formdef.always_advertise,
|
|
)
|
|
auth_contexts = get_publisher().get_supported_authentication_contexts()
|
|
if auth_contexts:
|
|
form.add(
|
|
CheckboxesWidget,
|
|
'required_authentication_contexts',
|
|
title=_('Required authentication contexts'),
|
|
value=self.formdef.required_authentication_contexts,
|
|
options=list(auth_contexts.items()),
|
|
)
|
|
|
|
if not self.formdef.is_readonly():
|
|
form.add_submit('submit', _('Submit'))
|
|
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(('roles', title))
|
|
get_response().set_title(title=self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % title
|
|
if description:
|
|
r += htmltext('<p>%s</p>') % description
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
roles = form.get_widget('roles').parse() or []
|
|
setattr(self.formdef, attribute, [x for x in roles if x])
|
|
for extra in ('required_authentication_contexts', 'only_allow_one', 'always_advertise'):
|
|
extra_widget = form.get_widget(extra)
|
|
if extra_widget:
|
|
old_value = getattr(self.formdef, extra, None)
|
|
setattr(self.formdef, extra, extra_widget.parse())
|
|
if old_value != getattr(self.formdef, extra, None):
|
|
self.formdef.store(comment=_('Changed "%s" parameter') % extra_widget.get_title())
|
|
if extra == 'only_allow_one' and not roles:
|
|
get_session().message = (
|
|
'warning',
|
|
_(
|
|
'The single form option concerns logged in users only, '
|
|
'however this form is accessible anonymously. '
|
|
'Consider adding a sender role.'
|
|
),
|
|
)
|
|
|
|
self.formdef.store(comment=_('Change of %s') % title)
|
|
return redirect('.')
|
|
|
|
def roles(self):
|
|
return self._roles_selection(
|
|
title=_('User Roles'),
|
|
attribute='roles',
|
|
description=_('Select the roles that can access this form.'),
|
|
)
|
|
|
|
def backoffice_submission_roles(self):
|
|
return self._roles_selection(
|
|
title=self.backoffice_submission_role_label,
|
|
attribute='backoffice_submission_roles',
|
|
include_logged_users_role=False,
|
|
description=self.backoffice_submission_role_description,
|
|
)
|
|
|
|
def title(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
kwargs = {}
|
|
if self.formdef.url_name == misc.simplify(self.formdef.name):
|
|
# if name and url name are in sync, keep them that way
|
|
kwargs['data-slug-sync'] = 'url_name'
|
|
form.add(
|
|
StringWidget,
|
|
'name',
|
|
title=_('Name'),
|
|
required=True,
|
|
size=40,
|
|
value=self.formdef.name,
|
|
maxlength=250,
|
|
**kwargs,
|
|
)
|
|
|
|
disabled_url_name = bool(self.formdef.data_class().count())
|
|
kwargs = {}
|
|
if disabled_url_name:
|
|
kwargs['readonly'] = 'readonly'
|
|
form.add(
|
|
SlugWidget,
|
|
'url_name',
|
|
title=_('Identifier in URLs'),
|
|
value=self.formdef.url_name,
|
|
**kwargs,
|
|
)
|
|
if not self.formdef.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():
|
|
new_name = form.get_widget('name').parse()
|
|
new_url_name = form.get_widget('url_name').parse()
|
|
formdefs = [
|
|
x
|
|
for x in self.formdef_class.select(ignore_errors=True, lightweight=True)
|
|
if x.id != self.formdef.id
|
|
]
|
|
if new_name in [x.name for x in formdefs]:
|
|
form.get_widget('name').set_error(_('This name is already used.'))
|
|
if new_url_name in [x.url_name for x in formdefs]:
|
|
form.get_widget('url_name').set_error(_('This identifier is already used.'))
|
|
if not form.has_errors():
|
|
self.formdef.name = new_name
|
|
self.formdef.url_name = new_url_name
|
|
self.formdef.store(comment=_('Change of title / URL'))
|
|
return redirect('.')
|
|
|
|
if disabled_url_name:
|
|
form.widgets.append(
|
|
HtmlWidget(
|
|
'<p>%s<br>'
|
|
% _('The form identifier should not be modified as there is already some data.')
|
|
)
|
|
)
|
|
form.widgets.append(
|
|
HtmlWidget(
|
|
'<a href="" class="change-nevertheless">%s</a></p>'
|
|
% _('I understand the danger, make it editable nevertheless.')
|
|
)
|
|
)
|
|
|
|
get_response().breadcrumb.append(('title', _('Title')))
|
|
get_response().set_title(title=self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Title')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def workflow(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
workflows = self.formdef_ui_class.get_workflows(formdef_category=self.formdef.category)
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'workflow_id',
|
|
value=self.formdef.workflow_id,
|
|
options=workflows,
|
|
**{'data-autocomplete': 'true'},
|
|
)
|
|
if not self.formdef.is_readonly():
|
|
form.add_submit('submit', _('Submit'))
|
|
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(('workflow', _('Workflow')))
|
|
get_response().set_title(title=self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Workflow')
|
|
r += htmltext('<p>%s</p>') % _('Select the workflow that will handle those forms.')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
workflow_id = form.get_widget('workflow_id').parse() or self.formdef_default_workflow
|
|
if self.formdef.data_class().count():
|
|
# there are existing formdata, status will have to be mapped
|
|
return redirect('workflow-status-remapping?new=%s' % workflow_id)
|
|
|
|
job = WorkflowChangeJob(
|
|
formdef=self.formdef,
|
|
new_workflow_id=workflow_id,
|
|
status_mapping={},
|
|
user_id=get_session().user,
|
|
)
|
|
job.store()
|
|
get_response().add_after_job(job)
|
|
return redirect(job.get_processing_url())
|
|
|
|
def has_remapping_jobs(self):
|
|
return bool(
|
|
[
|
|
x
|
|
for x in AfterJob.select()
|
|
if isinstance(x, WorkflowChangeJob)
|
|
and x.kwargs['formdef_class'] is self.formdef.__class__
|
|
and x.kwargs['formdef_id'] == self.formdef.id
|
|
and x.status in ('registered', 'running')
|
|
]
|
|
)
|
|
|
|
def workflow_status_remapping(self):
|
|
if self.has_remapping_jobs():
|
|
get_response().breadcrumb.append(('workflow-status-remapping', _('Workflow Status Remapping')))
|
|
return template.error_page(_('A workflow change is already running.'))
|
|
|
|
try:
|
|
new_workflow = Workflow.get(get_request().form.get('new'))
|
|
except KeyError:
|
|
get_response().breadcrumb.append(('workflow-status-remapping', _('Workflow Status Remapping')))
|
|
return template.error_page(_('Invalid target workflow.'))
|
|
|
|
if get_request().get_method() == 'GET':
|
|
get_request().form = None # do not be considered submitted already
|
|
new_workflow_status = [('', '')] + [(x.id, x.name) for x in new_workflow.possible_status]
|
|
form = Form(enctype='multipart/form-data')
|
|
for status in self.formdef.workflow.possible_status:
|
|
default = status.id
|
|
if default not in [x.id for x in new_workflow.possible_status]:
|
|
default = ''
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'mapping-%s' % status.id,
|
|
title=status.name,
|
|
value=default,
|
|
options=new_workflow_status,
|
|
required=True,
|
|
)
|
|
|
|
if self.formdef.workflow.id == '_unknown':
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'mapping',
|
|
title=_('Status'),
|
|
options=new_workflow_status,
|
|
required=True,
|
|
)
|
|
|
|
form.add_submit('submit', _('Submit'))
|
|
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(('workflow-status-remapping', _('Workflow Status Remapping')))
|
|
get_response().set_title(title=self.formdef.name)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<p>')
|
|
if self.formdef.workflow.id == '_unknown':
|
|
r += str(_('The current workflow configuration is broken; remapping will apply to all data.'))
|
|
else:
|
|
r += str(
|
|
_('From %(here)s to %(there)s')
|
|
% {
|
|
'here': self.formdef.workflow.name,
|
|
'there': new_workflow.name,
|
|
}
|
|
)
|
|
r += htmltext('</p>')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
status_mapping = {}
|
|
for status in self.formdef.workflow.possible_status:
|
|
status_mapping[status.id] = form.get_widget('mapping-%s' % status.id).parse()
|
|
|
|
if self.formdef.workflow.id == '_unknown':
|
|
status_mapping['_all'] = form.get_widget('mapping').parse()
|
|
|
|
if self.has_remapping_jobs():
|
|
# handle unlikely case of mapping job appearing concurrently
|
|
return self.workflow_status_remapping()
|
|
|
|
job = WorkflowChangeJob(
|
|
formdef=self.formdef,
|
|
new_workflow_id=new_workflow.id,
|
|
status_mapping=status_mapping,
|
|
user_id=get_session().user,
|
|
)
|
|
job.store()
|
|
get_response().add_after_job(job)
|
|
return redirect(job.get_processing_url())
|
|
|
|
def get_preview(self):
|
|
form = Form(action='#', use_tokens=False)
|
|
form.attrs['data-backoffice-preview'] = 'on'
|
|
on_page = 0
|
|
for field in self.formdef.fields:
|
|
if getattr(field, 'add_to_form', None):
|
|
try:
|
|
get_request().disable_error_notifications = True
|
|
field.add_to_form(form)
|
|
except Exception as e:
|
|
form.widgets.append(
|
|
HtmlWidget(
|
|
htmltext('<div class="errornotice"><p>%s (%s)</p></div>')
|
|
% (_('Error previewing field.'), e)
|
|
)
|
|
)
|
|
finally:
|
|
get_request().disable_error_notifications = False
|
|
else:
|
|
if field.key == 'page':
|
|
if on_page:
|
|
form.widgets.append(HtmlWidget('</fieldset>'))
|
|
form.widgets.append(HtmlWidget('<fieldset class="formpage">'))
|
|
on_page += 1
|
|
form.widgets.append(
|
|
HtmlWidget('<legend><span>%s — %s</span> ' % (_('Page #%s:') % on_page, field.label))
|
|
)
|
|
form.widgets.append(
|
|
HtmlWidget(
|
|
'<a class="pk-button" href="%s">%s</a>'
|
|
% ('fields/pages/%s/' % field.id, _('edit page fields'))
|
|
)
|
|
)
|
|
form.widgets.append(HtmlWidget('</legend>'))
|
|
|
|
if on_page:
|
|
form.widgets.append(HtmlWidget('</fieldset>'))
|
|
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<div class="form-preview">')
|
|
r += form.render()
|
|
r += htmltext('</div>')
|
|
return r.getvalue()
|
|
|
|
def duplicate(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
name_widget = form.add(StringWidget, 'name', title=_('Name'), required=True, size=30, maxlength=250)
|
|
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.formdefui.formdef.name
|
|
new_name = '%s %s' % (original_name, _('(copy)'))
|
|
names = [x.name for x in self.formdef_class.select(lightweight=True)]
|
|
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():
|
|
get_response().set_title(title=self.duplicate_title)
|
|
r = TemplateIO(html=True)
|
|
get_response().breadcrumb.append(('duplicate', _('Duplicate')))
|
|
r += htmltext('<h2>%s</h2>') % self.duplicate_title
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
return self.duplicate_submit(form)
|
|
|
|
def duplicate_submit(self, form):
|
|
from wcs.testdef import TestDef
|
|
|
|
testdefs = TestDef.select_for_objectdef(self.formdefui.formdef)
|
|
|
|
self.formdefui.formdef.name = form.get_widget('name').parse()
|
|
self.formdefui.formdef.id = None
|
|
self.formdefui.formdef.url_name = None
|
|
self.formdefui.formdef.table_name = None
|
|
self.formdefui.formdef.disabled = True
|
|
self.formdefui.formdef.store()
|
|
|
|
for testdef in testdefs:
|
|
testdef = TestDef.import_from_xml_tree(testdef.export_to_xml(), self.formdefui.formdef)
|
|
testdef.store()
|
|
|
|
return redirect('../%s/' % self.formdefui.formdef.id)
|
|
|
|
def get_check_deletion_message(self):
|
|
from wcs import sql
|
|
|
|
criterias = [
|
|
Equal('formdef_id', self.formdefui.formdef.id),
|
|
StrictNotEqual('status', 'draft'),
|
|
Equal('is_at_endpoint', False),
|
|
Null('anonymised'),
|
|
]
|
|
if sql.AnyFormData.count(criterias):
|
|
return _('Deletion is not possible as there are open forms.')
|
|
|
|
def delete(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
check_count_message = self.get_check_deletion_message()
|
|
if check_count_message:
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % check_count_message))
|
|
else:
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % self.delete_message))
|
|
criterias = [StrictNotEqual('status', 'draft'), Null('anonymised')]
|
|
if self.formdef.data_class().count(criterias):
|
|
form.widgets.append(
|
|
HtmlWidget(
|
|
htmltext('<div class="warningnotice"><p>%s</p></div>')
|
|
% _('Beware submitted forms will also be deleted.')
|
|
)
|
|
)
|
|
form.add_submit('delete', _('Delete'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
if form.get_widget('cancel').parse() or (form.is_submitted() and check_count_message):
|
|
return redirect('.')
|
|
if not form.is_submitted() or form.has_errors():
|
|
get_response().breadcrumb.append(('delete', _('Delete')))
|
|
get_response().set_title(title=self.delete_title)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s %s</h2>') % (self.delete_title, self.formdef.name)
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
criterias = [
|
|
Equal('formdef_type', self.formdef_class.xml_root_node),
|
|
Equal('formdef_id', self.formdef.id),
|
|
]
|
|
for view in get_publisher().custom_view_class.select(criterias):
|
|
view.remove_self()
|
|
self.formdef.remove_self()
|
|
return redirect('..')
|
|
|
|
def overwrite(self):
|
|
form = Form(enctype='multipart/form-data', use_tokens=False)
|
|
form.add(FileWidget, 'file', title=_('File'), required=False)
|
|
form.add(UrlWidget, 'url', title=_('Address'), required=False, size=50)
|
|
form.add_hidden('new_formdef', required=False)
|
|
form.add_hidden('force', required=False)
|
|
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():
|
|
try:
|
|
return self.overwrite_submit(form)
|
|
except ValueError:
|
|
pass
|
|
|
|
get_response().breadcrumb.append(('overwrite', _('Overwrite')))
|
|
get_response().set_title(title=_('Overwrite'))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Overwrite')
|
|
r += htmltext('<p>%s</p>') % self.overwrite_message
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def overwrite_submit(self, form):
|
|
if form.get_widget('file').parse():
|
|
fp = form.get_widget('file').parse().fp
|
|
elif form.get_widget('new_formdef').parse():
|
|
fp = io.StringIO(form.get_widget('new_formdef').parse())
|
|
elif form.get_widget('url').parse():
|
|
url = form.get_widget('url').parse()
|
|
try:
|
|
fp = misc.urlopen(url)
|
|
except misc.ConnectionError as e:
|
|
form.set_error('url', _('Error loading form (%s).') % str(e))
|
|
raise ValueError()
|
|
else:
|
|
form.set_error('file', _('You have to enter a file or a URL.'))
|
|
raise ValueError()
|
|
|
|
error, reason = False, None
|
|
try:
|
|
new_formdef = self.formdef_class.import_from_xml(fp, include_id=True)
|
|
except FormdefImportError as e:
|
|
error = True
|
|
reason = _(e.msg) % e.msg_args
|
|
if hasattr(e, 'render'):
|
|
form.add_global_errors([e.render()])
|
|
elif e.details:
|
|
reason += ' [%s]' % e.details
|
|
except ValueError:
|
|
error = True
|
|
|
|
if error:
|
|
if reason:
|
|
msg = _('Invalid File (%s)') % reason
|
|
else:
|
|
msg = _('Invalid File')
|
|
if form.get_widget('url').parse():
|
|
form.set_error('url', msg)
|
|
else:
|
|
form.set_error('file', msg)
|
|
raise ValueError()
|
|
|
|
# it's been through the summary page, or there is no data yet
|
|
if not self.formdef.data_class().count() or form.get_widget('force').parse():
|
|
# doing it!
|
|
return self.overwrite_by_formdef(new_formdef)
|
|
|
|
return self.overwrite_warning_summary(new_formdef)
|
|
|
|
def overwrite_by_formdef(self, new_formdef):
|
|
incompatible_field_ids = self.get_incompatible_field_ids(new_formdef)
|
|
if incompatible_field_ids:
|
|
# if there are incompatible field ids, remove them first
|
|
self.formdef.fields = [x for x in self.formdef.fields if x.id not in incompatible_field_ids]
|
|
self.formdef.store(comment=_('Overwritten (removal of incompatible fields)'))
|
|
|
|
# keep current formdef id, url_name, and sql table name
|
|
new_formdef.id = self.formdef.id
|
|
new_formdef.url_name = self.formdef.url_name
|
|
new_formdef.table_name = self.formdef.table_name
|
|
# keep currently assigned category and workflow
|
|
new_formdef.category_id = self.formdef.category_id
|
|
new_formdef.workflow_id = self.formdef.workflow_id
|
|
new_formdef.workflow_options = self.formdef.workflow_options
|
|
# keep currently assigned roles
|
|
new_formdef.workflow_roles = self.formdef.workflow_roles
|
|
new_formdef.backoffice_submission_roles = self.formdef.backoffice_submission_roles
|
|
new_formdef.roles = self.formdef.roles
|
|
|
|
# remove existing shared views
|
|
for view in get_publisher().custom_view_class.select():
|
|
if view.match(user=None, formdef=self.formdef):
|
|
view.remove_self()
|
|
|
|
self.formdef = new_formdef
|
|
self.formdef.store(comment=_('Overwritten'))
|
|
get_session().message = ('info', str(self.overwrite_success_message))
|
|
return redirect('.')
|
|
|
|
def get_incompatible_field_ids(self, new_formdef):
|
|
incompatible_field_ids = []
|
|
current_fields = {}
|
|
for field in self.formdef.fields:
|
|
current_fields[field.id] = field
|
|
|
|
for field in new_formdef.fields:
|
|
current_field = current_fields.get(field.id)
|
|
if current_field and current_field.key != field.key:
|
|
incompatible_field_ids.append(field.id)
|
|
|
|
return incompatible_field_ids
|
|
|
|
def overwrite_warning_summary(self, new_formdef):
|
|
get_response().set_title(title=_('Overwrite'))
|
|
get_response().breadcrumb.append(('overwrite', _('Overwrite')))
|
|
r = TemplateIO(html=True)
|
|
|
|
r += htmltext('<h2>%s - %s</h2>') % (_('Overwrite'), _('Summary of changes'))
|
|
|
|
current_fields_list = [str(x.id) for x in self.formdef.fields]
|
|
new_fields_list = [str(x.id) for x in new_formdef.fields]
|
|
|
|
current_fields = {}
|
|
new_fields = {}
|
|
for field in self.formdef.fields:
|
|
current_fields[field.id] = field
|
|
for field in new_formdef.fields:
|
|
new_fields[field.id] = field
|
|
|
|
table = TemplateIO(html=True)
|
|
table += htmltext('<table id="table-diff">')
|
|
|
|
def ellipsize_html(field):
|
|
return misc.ellipsize(field.unhtmled_label, 60)
|
|
|
|
display_warning = False
|
|
|
|
for diffinfo in difflib.ndiff(current_fields_list, new_fields_list):
|
|
if diffinfo[0] == '?':
|
|
# detail line, ignored
|
|
continue
|
|
field_id = diffinfo[2:].split()[0]
|
|
current_field = current_fields.get(field_id)
|
|
new_field = new_fields.get(field_id)
|
|
|
|
current_label = (
|
|
htmltext('%s - %s') % (ellipsize_html(current_field), current_field.get_type_label())
|
|
if current_field
|
|
else ''
|
|
)
|
|
new_label = (
|
|
htmltext('%s - %s') % (ellipsize_html(new_field), new_field.get_type_label())
|
|
if new_field
|
|
else ''
|
|
)
|
|
|
|
if diffinfo[0] == ' ':
|
|
# unchanged line
|
|
if current_field and new_field and current_field.key != new_field.key:
|
|
# different datatypes
|
|
if current_field.is_no_data_field:
|
|
# but current field doesn't hold data, not a problem
|
|
table += htmltext('<tr class="added-field"><td class="indicator">+</td>')
|
|
current_label = ''
|
|
elif new_field.is_no_data_field:
|
|
# new field won't hold data, but old data will be removed
|
|
table += htmltext('<tr class="removed-field"><td class="indicator">-</td>')
|
|
new_label = ''
|
|
display_warning = True
|
|
else:
|
|
# and real incompatibility, data will need to be wiped out.
|
|
table += htmltext('<tr class="type-change"><td class="indicator">!</td>')
|
|
display_warning = True
|
|
elif (
|
|
current_field
|
|
and new_field
|
|
and ET.tostring(current_field.export_to_xml()) != ET.tostring(new_field.export_to_xml())
|
|
):
|
|
# same type, but changes within field
|
|
table += htmltext('<tr class="modified-field"><td class="indicator">~</td>')
|
|
else:
|
|
table += htmltext('<tr><td class="indicator"></td>')
|
|
elif diffinfo[0] == '-':
|
|
# removed field
|
|
table += htmltext('<tr class="removed-field"><td class="indicator">-</td>')
|
|
display_warning = True
|
|
elif diffinfo[0] == '+':
|
|
# added field
|
|
table += htmltext('<tr class="added-field"><td class="indicator">+</td>')
|
|
table += htmltext('<td>%s</td> <td>%s</td></tr>') % (current_label, new_label)
|
|
table += htmltext('</table>')
|
|
|
|
if display_warning:
|
|
r += htmltext('<div class="errornotice"><p>%s</p></div>') % _(
|
|
'The form removes or changes fields, you should review the '
|
|
'changes carefully as some data will be lost.'
|
|
)
|
|
|
|
r += htmltext('<div class="section">')
|
|
r += htmltext('<div id="form-diff">')
|
|
r += table.getvalue()
|
|
|
|
r += htmltext('<div id="legend">')
|
|
r += htmltext('<table>')
|
|
r += htmltext('<tr class="added-field"><td class="indicator">+</td><td>%s</td></tr>') % (
|
|
_('Added field')
|
|
)
|
|
r += htmltext('</table>')
|
|
r += htmltext('<table>')
|
|
r += htmltext('<tr class="removed-field"><td class="indicator">-</td><td>%s</td></tr>') % (
|
|
_('Removed field')
|
|
)
|
|
r += htmltext('</table>')
|
|
r += htmltext('<table>')
|
|
r += htmltext('<tr class="modified-field"><td class="indicator">~</td><td>%s</td></tr>') % (
|
|
_('Modified field')
|
|
)
|
|
r += htmltext('<table>')
|
|
r += htmltext('<tr class="type-change"><td class="indicator">!</td><td>%s</td></tr>') % (
|
|
_('Incompatible field')
|
|
)
|
|
r += htmltext('</table>')
|
|
r += htmltext('</div>') # .legend
|
|
|
|
get_request().method = 'GET'
|
|
get_request().form = {}
|
|
form = Form(enctype='multipart/form-data', use_tokens=False)
|
|
if display_warning:
|
|
form.add(CheckboxWidget, 'force', title=_('Overwrite despite data loss'))
|
|
else:
|
|
form.add_hidden('force', 'ok')
|
|
form.add_hidden('new_formdef', force_str(ET.tostring(new_formdef.export_to_xml(include_id=True))))
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
r += form.render()
|
|
r += htmltext('</div>') # #form-diff
|
|
r += htmltext('</div>') # .section
|
|
|
|
return r.getvalue()
|
|
|
|
def export(self):
|
|
self.formdef._export_tests = True
|
|
return misc.xml_response(
|
|
self.formdef,
|
|
filename='%s-%s.wcs' % (self.formdef_export_prefix, self.formdef.url_name),
|
|
content_type='application/x-wcs-form',
|
|
)
|
|
|
|
def enable(self):
|
|
self.formdef.disabled = False
|
|
self.formdef.store(comment=_('Enable'))
|
|
if get_request().form.get('back') == 'fields':
|
|
return redirect('fields/')
|
|
return redirect('.')
|
|
|
|
def workflow_variables(self):
|
|
if not self.formdef.workflow.variables_formdef:
|
|
raise TraversalError()
|
|
get_response().set_title(title=_('Options'))
|
|
|
|
form = Form(enctype='multipart/form-data')
|
|
self.formdef.workflow.variables_formdef.add_fields_to_form(
|
|
form, form_data=self.formdef.get_variable_options_for_form()
|
|
)
|
|
for field in self.formdef.workflow.variables_formdef.fields:
|
|
if getattr(field, 'default_value', None) is None:
|
|
continue
|
|
form_widget = form.get_widget(f'f{field.id}')
|
|
if form_widget:
|
|
form_widget.hint = (form_widget.hint + ' ') if form_widget.hint else ''
|
|
form_widget.hint += _('Default value: %s') % field.default_value
|
|
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.formdef.set_variable_options(form)
|
|
self.formdef.store(comment=_('Change in workflow variables'))
|
|
return redirect('.')
|
|
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Options')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def workflow_options(self):
|
|
request = get_request()
|
|
if request.get_method() == 'GET' and request.form.get('file'):
|
|
value = self.formdef.workflow_options.get(request.form.get('file'))
|
|
if value:
|
|
return value.build_response()
|
|
|
|
get_response().set_title(title=_('Workflow Options'))
|
|
form = Form(enctype='multipart/form-data')
|
|
pristine_workflow = Workflow.get(self.formdef.workflow_id)
|
|
for status in self.formdef.workflow.possible_status:
|
|
had_options = False
|
|
for item in status.items:
|
|
prefix = '%s*%s*' % (status.id, item.id)
|
|
pristine_item = pristine_workflow.get_status(status.id).get_item(item.id)
|
|
parameters = [x for x in item.get_parameters() if not getattr(pristine_item, x)]
|
|
if not parameters:
|
|
continue
|
|
if not had_options:
|
|
form.widgets.append(HtmlWidget('<h3>%s</h3>' % status.name))
|
|
had_options = True
|
|
label = getattr(item, 'label', None) or _(item.description)
|
|
form.widgets.append(HtmlWidget('<h4>%s</h4>' % label))
|
|
item.add_parameters_widgets(form, parameters, prefix=prefix, formdef=self.formdef)
|
|
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.workflow_options_submit(form)
|
|
return redirect('.')
|
|
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Workflow Options')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def workflow_options_submit(self, form):
|
|
self.formdef.workflow_options = {}
|
|
for widget in form.get_all_widgets():
|
|
if widget in form.get_submit_widgets():
|
|
continue
|
|
if widget.name.startswith('_'):
|
|
continue
|
|
self.formdef.workflow_options[widget.name] = widget.parse()
|
|
self.formdef.store(comment=_('Change in workflow options'))
|
|
|
|
def inspect(self):
|
|
get_response().set_title(self.formdef.name)
|
|
get_response().breadcrumb.append(('inspect', _('Inspector')))
|
|
return self.render_inspect()
|
|
|
|
def render_inspect(self):
|
|
context = {'formdef': self.formdef, 'view': self, 'has_sidebar': self.formdef.is_readonly()}
|
|
if self.formdef.workflow.variables_formdef:
|
|
context['workflow_options'] = {}
|
|
variables_form_data = self.formdef.get_variable_options_for_form()
|
|
for field in self.formdef.workflow.variables_formdef.fields:
|
|
if not hasattr(field, 'get_view_value'): # inhert
|
|
context['workflow_options'][field.label] = '__%s__' % field.key
|
|
continue
|
|
context['workflow_options'][field.label] = htmltext('%s') % field.get_view_value(
|
|
variables_form_data.get(field.id)
|
|
)
|
|
page = None
|
|
for field in self.formdef.fields:
|
|
if field.key == 'page':
|
|
page = field
|
|
field.on_page = page
|
|
context['workflow_roles'] = list(self.get_workflow_roles_elements())
|
|
context['backoffice_submission_roles'] = self._get_roles_label('backoffice_submission_roles')
|
|
if self.formdef.tracking_code_verify_fields:
|
|
context['tracking_code_verify_fields_labels'] = ', '.join(
|
|
[
|
|
x.label
|
|
for x in self.formdef.fields
|
|
if str(x.id) in self.formdef.tracking_code_verify_fields
|
|
]
|
|
)
|
|
if hasattr(self.formdef, '_custom_views'):
|
|
# loaded from snapshot
|
|
custom_views = self.formdef._custom_views
|
|
else:
|
|
custom_views = []
|
|
for view in get_publisher().custom_view_class.select_shared_for_formdef(self.formdef):
|
|
custom_views.append(view)
|
|
for view in custom_views:
|
|
view.digest_template = (self.formdef.digest_templates or {}).get(
|
|
'custom-view:%s' % view.get_url_slug()
|
|
)
|
|
if view.visibility == 'role':
|
|
role_id = view.role_id
|
|
if role_id:
|
|
try:
|
|
role = get_publisher().role_class.get(role_id)
|
|
role_label = role.name
|
|
except KeyError:
|
|
# removed role ?
|
|
role_label = _('Unknown role (%s)') % role_id
|
|
else:
|
|
role_label = '-'
|
|
view.role = role_label
|
|
context['custom_views'] = sorted(custom_views, key=lambda x: getattr(x, 'title'))
|
|
if not hasattr(self.formdef, 'snapshot_object'):
|
|
deprecations = DeprecationsDirectory()
|
|
context['deprecations'] = deprecations.get_deprecations(
|
|
f'{self.formdef.xml_root_node}:{self.formdef.id}'
|
|
)
|
|
context['deprecation_titles'] = deprecations.titles
|
|
return template.QommonTemplateResponse(
|
|
templates=[self.inspect_template_name],
|
|
context=context,
|
|
is_django_native=True,
|
|
)
|
|
|
|
def snapshot_info_inspect_block(self):
|
|
return utils.snapshot_info_block(
|
|
snapshot=self.formdef.snapshot_object, url_name='inspect', url_prefix='../'
|
|
)
|
|
|
|
|
|
class NamedDataSourcesDirectoryInForms(NamedDataSourcesDirectory):
|
|
pass
|
|
|
|
|
|
class FormsDirectory(AccessControlled, Directory):
|
|
do_not_call_in_templates = True
|
|
|
|
_q_exports = [
|
|
'',
|
|
'new',
|
|
('import', 'p_import'),
|
|
'blocks',
|
|
'categories',
|
|
('data-sources', 'data_sources'),
|
|
('application', 'applications_dir'),
|
|
]
|
|
|
|
category_class = Category
|
|
categories = CategoriesDirectory()
|
|
blocks = BlocksDirectory()
|
|
data_sources = NamedDataSourcesDirectoryInForms()
|
|
formdef_class = FormDef
|
|
formdef_page_class = FormDefPage
|
|
formdef_ui_class = FormDefUI
|
|
|
|
section = 'forms'
|
|
top_title = _('Forms')
|
|
index_template_name = 'wcs/backoffice/forms.html'
|
|
import_title = _('Import Form')
|
|
import_submit_label = _('Import Form')
|
|
import_paragraph = _('You can install a new form by uploading a file or by pointing to the form URL.')
|
|
import_loading_error_message = _('Error loading form (%s).')
|
|
import_success_message = _('This form has been successfully imported. Do note it is disabled by default.')
|
|
import_error_message = _(
|
|
'Imported form contained errors and has been automatically fixed, '
|
|
'you should nevertheless check everything is ok. '
|
|
'Do note it is disabled by default.'
|
|
)
|
|
import_slug_change = _(
|
|
'The form identifier (%(slug)s) was already used by another form. '
|
|
'A new one has been generated (%(newslug)s).'
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.applications_dir = ApplicationsDirectory(self.formdef_class)
|
|
|
|
def _q_traverse(self, path):
|
|
get_response().breadcrumb.append(('%s/' % self.section, self.top_title))
|
|
get_response().set_backoffice_section(self.section)
|
|
return super()._q_traverse(path)
|
|
|
|
def is_accessible(self, user, traversal=False):
|
|
if is_global_accessible(self.section):
|
|
return True
|
|
|
|
# check for access to specific categories
|
|
user_roles = set(user.get_roles())
|
|
for category in self.category_class.select():
|
|
management_roles = {x.id for x in getattr(category, 'management_roles') or []}
|
|
if management_roles and user_roles.intersection(management_roles):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _q_index(self):
|
|
from wcs.applications import Application
|
|
|
|
get_response().set_title(title=self.top_title)
|
|
get_response().add_javascript(['widget_list.js', 'select2.js', 'popup.js'])
|
|
|
|
context = {
|
|
'view': self,
|
|
'has_roles': bool(get_publisher().role_class.count()),
|
|
'applications': Application.select_for_object_type(self.formdef_class.xml_root_node),
|
|
'elements_label': self.formdef_class.verbose_name_plural,
|
|
'has_sidebar': True,
|
|
}
|
|
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
|
|
Application.populate_objects(formdefs)
|
|
context.update(self.get_list_context(formdefs))
|
|
context.update(self.get_extra_index_context_data())
|
|
|
|
return template.QommonTemplateResponse(
|
|
templates=[self.index_template_name], context=context, is_django_native=True
|
|
)
|
|
|
|
def get_list_context(self, formdefs):
|
|
global_access = is_global_accessible(self.section)
|
|
|
|
categories = self.category_class.select()
|
|
self.category_class.sort_by_position(categories)
|
|
categories.append(self.category_class(_('Misc')))
|
|
|
|
has_form_with_category_set = False
|
|
for category in categories:
|
|
if not global_access:
|
|
user_roles = set(get_request().user.get_roles())
|
|
management_roles = {x.id for x in getattr(category, 'management_roles') or []}
|
|
if not user_roles.intersection(management_roles):
|
|
continue
|
|
l2 = [x for x in formdefs if str(x.category_id) == str(category.id)]
|
|
l2 = [x for x in l2 if not x.disabled or (x.disabled and x.disabled_redirection)] + [
|
|
x for x in l2 if x.disabled and not x.disabled_redirection
|
|
]
|
|
category.objects = l2
|
|
if category.objects and category.id:
|
|
has_form_with_category_set = True
|
|
|
|
if not has_form_with_category_set:
|
|
# no form with a category set, do not display "Misc" title
|
|
categories[-1].name = None
|
|
|
|
return {
|
|
'objects': formdefs,
|
|
'categories': categories,
|
|
}
|
|
|
|
def get_extra_index_context_data(self):
|
|
return {
|
|
'is_global_accessible_forms': is_global_accessible('forms'),
|
|
'is_global_accessible_categories': get_publisher()
|
|
.get_backoffice_root()
|
|
.is_accessible('categories'),
|
|
}
|
|
|
|
def new(self):
|
|
get_response().breadcrumb.append(('new', _('New')))
|
|
if not (get_publisher().role_class.exists()):
|
|
return template.error_page(self.section, _('You first have to define roles.'))
|
|
formdefui = self.formdef_ui_class(None)
|
|
form = formdefui.new_form_ui()
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('.')
|
|
|
|
if form.is_submitted() and not form.has_errors():
|
|
try:
|
|
formdef = formdefui.submit_form(form)
|
|
formdef.disabled = True
|
|
formdef.store()
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
return redirect(str(formdef.id) + '/')
|
|
|
|
get_response().set_title(title=_('New Form'))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('New Form')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def _q_lookup(self, component):
|
|
directory = self.formdef_page_class(component)
|
|
if not directory.formdef.has_admin_access(get_request().user):
|
|
raise AccessForbiddenError()
|
|
return directory
|
|
|
|
def p_import(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
|
|
form.add(FileWidget, 'file', title=_('File'), required=False)
|
|
form.add(UrlWidget, 'url', title=_('Address'), required=False, size=50)
|
|
form.add_submit('submit', self.import_submit_label)
|
|
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')))
|
|
get_response().set_title(title=self.import_title)
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % self.import_title
|
|
r += htmltext('<p>%s</p>') % self.import_paragraph
|
|
r += form.render()
|
|
return r.getvalue()
|
|
|
|
def import_submit(self, form):
|
|
self.imported_formdef = None
|
|
if form.get_widget('file').parse():
|
|
fp = form.get_widget('file').parse().fp
|
|
elif form.get_widget('url').parse():
|
|
url = form.get_widget('url').parse()
|
|
try:
|
|
fp = misc.urlopen(url)
|
|
except misc.ConnectionError as e:
|
|
form.set_error('url', self.import_loading_error_message % str(e))
|
|
raise ValueError()
|
|
else:
|
|
form.set_error('file', _('You have to enter a file or a URL.'))
|
|
raise ValueError()
|
|
|
|
error, reason = False, None
|
|
try:
|
|
try:
|
|
formdef = self.formdef_class.import_from_xml(fp)
|
|
get_session().message = ('info', str(self.import_success_message))
|
|
except FormdefImportRecoverableError:
|
|
fp.seek(0)
|
|
formdef = self.formdef_class.import_from_xml(fp, fix_on_error=True)
|
|
get_session().message = ('info', str(self.import_error_message))
|
|
except FormdefImportError as e:
|
|
error = True
|
|
reason = _(e.msg) % e.msg_args
|
|
if hasattr(e, 'render'):
|
|
form.add_global_errors([e.render()])
|
|
elif e.details:
|
|
reason += ' [%s]' % e.details
|
|
except ValueError:
|
|
error = True
|
|
|
|
if not error:
|
|
global_access = is_global_accessible(self.section)
|
|
if not global_access:
|
|
management_roles = {x.id for x in getattr(formdef.category, 'management_roles', None) or []}
|
|
user_roles = set(get_request().user.get_roles())
|
|
if not user_roles.intersection(management_roles):
|
|
error = True
|
|
reason = _('unauthorized category')
|
|
|
|
if error:
|
|
if reason:
|
|
msg = _('Invalid File (%s)') % reason
|
|
else:
|
|
msg = _('Invalid File')
|
|
if form.get_widget('url').parse():
|
|
form.set_error('url', msg)
|
|
else:
|
|
form.set_error('file', msg)
|
|
raise ValueError()
|
|
|
|
if hasattr(formdef, '_import_orig_slug'):
|
|
get_session().message = (
|
|
'warning',
|
|
'%s %s'
|
|
% (
|
|
get_session().message[1],
|
|
self.import_slug_change
|
|
% {'slug': formdef._import_orig_slug, 'newslug': formdef.url_name},
|
|
),
|
|
)
|
|
|
|
self.imported_formdef = formdef
|
|
formdef.disabled = True
|
|
formdef.store()
|
|
formdef.finish_tests_xml_import()
|
|
return redirect('%s/' % formdef.id)
|
|
|
|
|
|
class WorkflowChangeJob(AfterJob):
|
|
def __init__(self, formdef, new_workflow_id, status_mapping, user_id):
|
|
super().__init__(
|
|
label=_('Updating data for new workflow'),
|
|
formdef_class=formdef.__class__,
|
|
formdef_id=formdef.id,
|
|
new_workflow_id=new_workflow_id,
|
|
status_mapping=status_mapping,
|
|
user_id=user_id,
|
|
)
|
|
|
|
def execute(self):
|
|
formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
|
|
workflow = Workflow.get(self.kwargs['new_workflow_id'])
|
|
formdef.change_workflow(workflow, self.kwargs['status_mapping'], user_id=self.kwargs.get('user_id'))
|
|
|
|
def done_action_url(self):
|
|
formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
|
|
return formdef.get_admin_url()
|
|
|
|
def done_action_label(self):
|
|
return _('Back')
|