backoffice: add an inspect view to forms/card models (#30381)

This commit is contained in:
Frédéric Péters 2022-04-14 21:50:32 +02:00
parent 9a21305a78
commit 1476f6ab7e
7 changed files with 464 additions and 29 deletions

View File

@ -16,7 +16,7 @@ from wcs.data_sources import NamedDataSource
from wcs.formdef import FormDef
from wcs.qommon.errors import ConnectionError
from wcs.qommon.http_request import HTTPRequest
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
from wcs.wscalls import NamedWsCall
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
@ -895,8 +895,6 @@ def test_form_workflow_variables(pub):
Workflow.wipe()
workflow = Workflow(name='Workflow One')
from wcs.workflows import WorkflowVariablesFieldsFormDef
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields.append(
fields.StringField(id='1', varname='test', label='Test', type='string')
@ -947,8 +945,6 @@ def test_form_workflow_table_variables(pub):
Workflow.wipe()
workflow = Workflow(name='Workflow One')
from wcs.workflows import WorkflowVariablesFieldsFormDef
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields.append(
fields.TableRowsField(id='1', varname='test', label='Test2', type='tablerows', columns=['a'])
@ -996,8 +992,6 @@ def test_form_workflow_invalid_file_variable(pub):
Workflow.wipe()
workflow = Workflow(name='Workflow One')
from wcs.workflows import WorkflowVariablesFieldsFormDef
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields = [
fields.StringField(id='1', varname='test', label='Test', type='string')
@ -3421,3 +3415,117 @@ def test_field_display_locations_statistics_choice(pub):
resp.form['varname'] = 'var_%s' % i
resp = resp.form.submit('submit')
assert 'statistics' in FormDef.get(formdef.id).fields[i].display_locations
def test_admin_form_inspect(pub):
create_superuser(pub)
create_role(pub)
NamedDataSource.wipe()
data_source = NamedDataSource(name='Foobar')
data_source.data_source = {'type': 'json', 'value': 'http://remote.example.net/404'}
data_source.store()
CardDef.wipe()
carddef = CardDef()
carddef.name = 'Baz'
carddef.digest_templates = {'default': 'plop'}
carddef.store()
Workflow.wipe()
workflow = Workflow(name='Workflow One')
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields.append(
fields.StringField(id='1', varname='test', label='Test', type='string')
)
workflow.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.workflow_id = workflow.id
formdef.fields = [
fields.PageField(
id='0',
label='1st page',
type='page',
post_conditions=[
{'condition': {'type': 'django', 'value': 'false'}, 'error_message': 'You shall not pass.'}
],
),
fields.StringField(
id='1', label='String field', varname='var_1', condition={'type': 'django', 'value': 'true'}
),
fields.StringField(id='2', label='String field digits', validation={'type': 'digits'}),
fields.StringField(id='3', label='String field regex', validation={'type': 'regex', 'value': r'\d+'}),
fields.ItemField(
id='4',
label='Date field',
type='date',
),
fields.ItemField(id='5', label='Item field', type='item', items=['One', 'Two', 'Three']),
fields.ItemField(
id='6', label='Item field named data source', type='item', data_source={'type': 'foobar'}
),
fields.ItemField(
id='7', label='Item field carddef data source', type='item', data_source={'type': 'carddef:baz'}
),
fields.ItemField(
id='8',
label='Item field json data source',
type='item',
data_source={'type': 'json', 'value': 'http://test'},
),
fields.ItemsField(id='9', label='Items field', type='items', items=['One', 'Two', 'Three']),
fields.StringField(id='10', label='prefill', prefill={'type': 'user', 'value': 'email'}),
fields.StringField(
id='11', label='prefill2', prefill={'type': 'string', 'value': '{{plop}}', 'locked': True}
),
fields.FileField(
id='12', label='file', automatic_image_resize=True, display_locations=['validation']
),
]
formdef.workflow_options = {'test': 'plop'}
formdef.store()
app = login(get_app(pub))
resp = app.get('/backoffice/forms/%s/inspect' % formdef.id)
assert 'Test → plop' in resp.text # workflow option
assert (
resp.pyquery('[data-field-id="0"] .parameter-post_conditions').text()
== 'Post Conditions:\nfalse - You shall not pass.'
)
assert (
resp.pyquery('[data-field-id="1"] .parameter-condition').text() == 'Display Condition: true (Django)'
)
assert resp.pyquery('[data-field-id="2"] .parameter-validation').text() == 'Validation: Digits'
assert (
resp.pyquery('[data-field-id="3"] .parameter-validation').text()
== 'Validation: Regular Expression - \\d+'
)
assert resp.pyquery('[data-field-id="5"] .parameter-items').text() == 'Choices: One, Two, Three'
assert resp.pyquery('[data-field-id="6"] .parameter-data_source').text() == 'Data source: Foobar'
assert (
resp.pyquery('[data-field-id="7"] .parameter-data_source').text() == 'Data source: card model - Baz'
)
assert (
resp.pyquery('[data-field-id="8"] .parameter-data_source').text()
== 'Data source: JSON URL - http://test'
)
assert (
resp.pyquery('[data-field-id="10"] .parameter-prefill').text()
== 'Prefill:\nType: User Field\nValue: Email (builtin)'
)
assert (
resp.pyquery('[data-field-id="11"] .parameter-prefill').text()
== 'Prefill:\nType: String / Template\nValue: {{plop}}\nLocked'
)
assert (
resp.pyquery('[data-field-id="12"] .parameter-automatic_image_resize').text()
== 'Automatically resize uploaded images: Yes'
)
assert (
resp.pyquery('[data-field-id="12"] .parameter-display_locations').text()
== 'Display Locations: Validation Page'
)

View File

@ -596,6 +596,7 @@ class WorkflowRoleDirectory(Directory):
class FormDefPage(Directory):
do_not_call_in_templates = True
_q_exports = [
'',
'fields',
@ -616,6 +617,7 @@ class FormDefPage(Directory):
'overwrite',
'qrcode',
'information',
'inspect',
('public-url', 'public_url'),
('backoffice-submission-roles', 'backoffice_submission_roles'),
('logged-errors', 'logged_errors_dir'),
@ -640,6 +642,7 @@ class FormDefPage(Directory):
)
readonly_message = _('This form is readonly.')
management_view_label = _('Management view')
inspect_template_name = 'wcs/backoffice/formdef-inspect.html'
def __init__(self, component, instance=None):
try:
@ -758,24 +761,10 @@ class FormDefPage(Directory):
r += self.add_option_line('workflow-options', _('Options'), '')
if self.formdef.workflow.roles:
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 = '-'
for (wf_role_id, wf_role_label, role_label) in self.get_workflow_roles_elements():
r += self.add_option_line('role/%s' % wf_role_id, wf_role_label, role_label)
r += self.add_option_line('roles', _('User Roles'), self._get_roles_label_and_auth_context('roles'))
r += self.add_option_line('roles', _('User Roles'), self.get_roles_label_and_auth_context())
r += self.add_option_line(
'backoffice-submission-roles',
_('Backoffice Submission Role'),
@ -880,6 +869,24 @@ class FormDefPage(Directory):
r += htmltext('</div>')
return r.getvalue()
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 = []
@ -897,8 +904,8 @@ class FormDefPage(Directory):
value = C_('roles|None')
return value
def _get_roles_label_and_auth_context(self, attribute):
value = self._get_roles_label(attribute)
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(
@ -924,6 +931,7 @@ class FormDefPage(Directory):
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')
if not get_publisher().is_using_postgresql() and self.formdef_class == FormDef:
r += htmltext('<li><a href="archive">%s</a></li>') % _('Archive')
r += htmltext('</ul>')
@ -1742,6 +1750,29 @@ class FormDefPage(Directory):
self.formdef.workflow_options[widget.name] = widget.parse()
self.formdef.store(comment=_('Change in workflow options'))
def inspect(self):
self.html_top(self.formdef.name)
get_response().breadcrumb.append(('inspect', _('Inspector')))
context = {'formdef': self.formdef, 'view': self}
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:
context['workflow_options'][field.label] = htmltext('%s') % field.get_view_value(
variables_form_data.get(field.id)
)
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
]
)
return template.QommonTemplateResponse(templates=[self.inspect_template_name], context=context)
class NamedDataSourcesDirectoryInForms(NamedDataSourcesDirectory):
pass

View File

@ -30,6 +30,7 @@ class Category(XmlStorableObject):
_names = 'categories'
xml_root_node = 'category'
backoffice_class = 'wcs.admin.categories.CategoryPage'
backoffice_base_url = 'forms/categories/'
name = None
url_name = None
@ -92,6 +93,9 @@ class Category(XmlStorableObject):
return True
return False
def get_admin_url(self):
return '%s/%s%s/' % (get_publisher().get_backoffice_url(), self.backoffice_base_url, self.id)
def store(self, *args, comment=None, snapshot_store_user=True, **kwargs):
if not self.url_name:
existing_slugs = {
@ -214,6 +218,7 @@ class CardDefCategory(Category):
_names = 'carddef_categories'
xml_root_node = 'carddef_category'
backoffice_class = 'wcs.admin.categories.CardDefCategoryPage'
backoffice_base_url = 'cards/categories/'
# declarations for serialization
XML_NODES = [
@ -236,6 +241,7 @@ class WorkflowCategory(Category):
_names = 'workflow_categories'
xml_root_node = 'workflow_category'
backoffice_class = 'wcs.admin.categories.WorkflowCategoryPage'
backoffice_base_url = 'workflows/categories/'
# declarations for serialization
XML_NODES = [
@ -257,6 +263,7 @@ class BlockCategory(Category):
_names = 'block_categories'
xml_root_node = 'block_category'
backoffice_class = 'wcs.admin.categories.BlockCategoryPage'
backoffice_base_url = 'forms/blocks/categories/'
# declarations for serialization
XML_NODES = [
@ -277,6 +284,7 @@ class MailTemplateCategory(Category):
_names = 'mail_template_categories'
xml_root_node = 'mail_template_category'
backoffice_class = 'wcs.admin.categories.MailTemplateCategoryPage'
backoffice_base_url = 'workflows/mail-templates/categories/'
# declarations for serialization
XML_NODES = [
@ -297,6 +305,7 @@ class DataSourceCategory(Category):
_names = 'data_source_categories'
xml_root_node = 'data_source_category'
backoffice_class = 'wcs.admin.categories.DataSourceCategoryPage'
backoffice_base_url = 'forms/data-sources/categories/'
# declarations for serialization
XML_NODES = [

View File

@ -48,6 +48,7 @@ from .qommon.form import (
EmailWidget,
FileSizeWidget,
FileWithPreviewWidget,
Form,
HiddenWidget,
HtmlWidget,
IntWidget,
@ -137,7 +138,7 @@ class PrefillSelectionWidget(CompositeWidget):
if not self.value or self.value.get('type') == 'none':
self.value = {}
prefill_types = collections.OrderedDict(options)
self.prefill_types = prefill_types = collections.OrderedDict(options)
self.add(
StringWidget,
'value_string',
@ -690,6 +691,129 @@ class Field:
yield NamedDataSource.get_by_slug(data_source_type, ignore_errors=True)
def get_parameters_view(self):
r = TemplateIO(html=True)
form = Form()
self.fill_admin_form(form)
parameters = [x for x in self.get_admin_attributes() if getattr(self, x, None) is not None]
r += htmltext('<ul>')
for parameter in parameters:
widget = form.get_widget(parameter)
if not widget:
continue
label = self.get_parameter_view_label(widget, parameter)
if not label:
continue
value = getattr(self, parameter, Ellipsis)
if value is None or value == getattr(self.__class__, parameter, Ellipsis):
continue
parameter_view_value = self.get_parameter_view_value(widget, parameter)
if parameter_view_value:
r += htmltext('<li class="parameter-%s">' % parameter)
r += htmltext('<span class="parameter">%s</span> ') % _('%s:') % label
r += parameter_view_value
r += htmltext('</li>')
r += htmltext('</ul>')
return r.getvalue()
def get_parameter_view_label(self, widget, parameter):
if hasattr(self, 'get_%s_parameter_view_label' % parameter):
return getattr(self, 'get_%s_parameter_view_label' % parameter)()
return widget.get_title()
def get_parameter_view_value(self, widget, parameter):
if hasattr(self, 'get_%s_parameter_view_value' % parameter):
return getattr(self, 'get_%s_parameter_view_value' % parameter)(widget)
value = getattr(self, parameter)
if isinstance(value, bool):
return str(_('Yes') if value else _('No'))
elif hasattr(widget, 'options') and value:
if not isinstance(widget, CheckboxesWidget):
value = [value]
value_labels = []
for option in widget.options:
if isinstance(option, tuple):
if option[0] in value:
value_labels.append(str(option[1]))
else:
if option in value:
value_labels.append(str(option))
return ', '.join(value_labels) if value_labels else '-'
elif isinstance(value, list):
return ', '.join(value)
else:
return str(value)
def get_prefill_parameter_view_value(self, widget):
value = getattr(self, 'prefill', None)
if not value or value.get('type') == 'none':
return
r = TemplateIO(html=True)
r += htmltext('<ul>')
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Type'),
_(':'),
widget.prefill_types.get(value.get('type')),
)
if value.get('type') in ('user', 'geolocation'):
select_widget = widget.get_widget('value_%s' % value['type'])
labels = {x[0]: x[1] for x in select_widget.options}
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Value'),
_(':'),
labels.get(value.get('value'), '-'),
)
else:
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Value'),
_(':'),
value.get('value'),
)
if value.get('locked'):
r += htmltext('<li>%s</li>') % _('Locked')
r += htmltext('</ul>')
return r.getvalue()
def get_data_source_parameter_view_value(self, widget):
value = getattr(self, 'data_source', None)
if not value or value.get('type') == 'none':
return
if value.get('type').startswith('carddef:'):
from wcs.carddef import CardDef
parts = value['type'].split(':')
try:
carddef = CardDef.get_by_urlname(parts[1])
except KeyError:
return _('deleted card model')
custom_view = CardDef.get_data_source_custom_view(value['type'], carddef=carddef)
if custom_view:
return _('card model: %s, custom view: %s') % (carddef.name, custom_view.title)
return '%s - %s' % (_('card model'), carddef.name)
data_source_types = {
'json': _('JSON URL'),
'jsonp': _('JSONP URL'),
'geojson': _('GeoJSON URL'),
'formula': _('Python Expression'),
}
if value.get('type') in data_source_types:
return '%s - %s' % (data_source_types[value.get('type')], value.get('value'))
from wcs.data_sources import NamedDataSource
data_source = NamedDataSource.get_by_slug(value['type'])
return data_source.name
def get_condition_parameter_view_value(self, widget):
if not self.condition or self.condition.get('type') == 'none':
return
return htmltext('<tt class="condition">%s</tt> <span class="condition-type">(%s)</span>') % (
self.condition['value'],
{'django': 'Django', 'python': 'Python'}.get(self.condition['type']),
)
def __repr__(self):
return '<%s %s %r>' % (self.__class__.__name__, self.id, self.label and self.label[:64])
@ -1168,6 +1292,15 @@ class StringField(WidgetField):
super().init_with_xml(elem, charset, include_id=include_id)
self.migrate()
def get_validation_parameter_view_value(self, widget):
if not self.validation:
return
validation_type = self.validation['type']
validation_types = {x: y['title'] for x, y in ValidationWidget.validation_methods.items()}
if validation_type in ('regex', 'django'):
return '%s - %s' % (validation_types.get(validation_type), self.validation['value'])
return str(validation_types.get(validation_type))
register_field_class(StringField)
@ -1905,6 +2038,15 @@ class ItemFieldMixin:
data_source_type.set_value(None)
data_source_type.transfer_form_value(get_request())
def get_items_parameter_view_label(self):
if self.data_source:
# skip field if there's a data source
return None
return str(_('Choices'))
def get_data_source_parameter_view_label(self):
return str(_('Data source'))
class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
key = 'item'
@ -2225,8 +2367,8 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
def get_admin_attributes(self):
return WidgetField.get_admin_attributes(self) + [
'items',
'display_mode',
'items',
'data_source',
'in_filters',
'anonymise',
@ -2741,6 +2883,19 @@ class PageField(Field):
for post_condition in self.post_conditions or []:
yield post_condition.get('condition')
def get_post_conditions_parameter_view_value(self, widget):
if not self.post_conditions:
return
r = TemplateIO(html=True)
r += htmltext('<ul>')
for post_condition in self.post_conditions:
r += htmltext('<li>%s - %s</li>') % (
post_condition.get('condition').get('value'),
post_condition.get('error_message'),
)
r += htmltext('</ul>')
return r.getvalue()
register_field_class(PageField)

View File

@ -873,6 +873,9 @@ class FormDef(StorableObject):
def get_page(self, page_no):
return [x for x in self.fields if x.type == 'page'][page_no]
def page_count(self):
return len([x for x in self.fields if x.type == 'page']) or 1
def create_view_form(self, dict=None, use_tokens=True, visible=True):
dict = dict or {}
form = Form(enctype='multipart/form-data', use_tokens=use_tokens)

View File

@ -1662,8 +1662,38 @@ div.expanded-statuses div.status h4::before {
font-size: 90%;
}
div.expanded-statuses div.status ul span.parameter {
color: #666;
div.inspect-form-tabs,
div.inspect-field,
div.expanded-statuses div.status {
ul span.parameter {
color: #666;
}
}
.inspect-field {
margin: 0.5em 0 1em 0;
h4 {
margin: 0.5em 0;
&:first-child {
margin-top: 0;
}
}
&.inspect-field--page:not(.inspect-field--first) {
border-top: 1px solid #ccc;
padding-top: 0.5em;
}
}
[role=tabpanel] {
> .pk-information:first-child {
margin-top: 0;
}
}
.condition-type,
.inspect-field-type {
font-weight: normal;
font-size: 90%;
}
pre.wrapping-pre {
@ -1702,6 +1732,19 @@ form div.page > div {
}
}
.inspect-tabs {
ul {
margin: 0;
padding: 0;
}
li {
margin-left: 1em;
&::marker {
color: #666;
}
}
}
ul.biglist.form-inspector {
padding-top: 0;
}

View File

@ -0,0 +1,86 @@
{% extends "wcs/backoffice/base.html" %}
{% load i18n %}
{% block appbar %}{% endblock %}
{% block content %}
<div class="pk-tabs inspect-tabs inspect-form-tabs">
<div class="pk-tabs--tab-list" role="tablist">
<button role="tab" aria-selected="true" aria-controls="inspect-infos" id="tab-infos" tabindex="0">{% trans "Information" %}</button>
<button role="tab" aria-selected="false" aria-controls="inspect-workflow" id="tab-workflow" tabindex="-1">{% trans "Workflow" %}</button>
<button role="tab" aria-selected="false" aria-controls="inspect-options" id="tab-options" tabindex="-1">{% trans "Options" %}</button>
<button role="tab" aria-selected="false" aria-controls="inspect-fields" id="tab-fields" tabindex="-1">{% trans "Fields" %}</button>
</div>
<div class="pk-tabs--container">
<div id="inspect-infos" role="tabpanel" tabindex="0" aria-labelledby="tab-info">
<ul>
<li><span class="parameter">{% trans "Title" %}{% trans ":" %}</span> {{ formdef.name|default:"-" }}</li>
<li><span class="parameter">{% trans "Description" %}{% trans ":" %}</span> {{ formdef.description|default:"-"|safe }}</li>
<li><span class="parameter">{% trans "Keywords" %}{% trans ":" %}</span> {{ formdef.keywords|default:"-" }}</li>
<li><span class="parameter">{% trans "Category" %}{% trans ":" %}</span> {% if formdef.category %}<a href="{{ formdef.category.get_admin_url }}">{{ formdef.category.name }}</a>{% else %}-{% endif %}</li>
</ul>
</div>
<div id="inspect-workflow" role="tabpanel" tabindex="0" aria-labelledby="tab-workflow" hidden>
<ul>
<li><span class="parameter">{% trans "Workflow" %}{% trans ":" %}</span> <a href="{{ formdef.workflow.get_admin_url }}">{{ formdef.workflow.name }}</a></li>
<li><span class="parameter">{% trans "Options" %}{% trans ":" %}</span> {% if not workflow_options %}-{% else %}<ul>
{% for label, value in workflow_options.items %}
<li>{{ label }} → {{ value|safe|default:"-" }}</li>
{% endfor %}
</ul>{% endif %}</li>
{% for wf_role_id, wf_role_label, role_label in workflow_roles %}
<li><span class="parameter">{{ wf_role_label }}{% trans ":" %}</span> {{ role_label|default:"-" }}</li>
{% endfor %}
<li><span class="parameter">{% trans "User Roles" %}{% trans ":" %}</span> {{ view.get_roles_label_and_auth_context|default:"-" }}</li>
<li><span class="parameter">{% trans "Backoffice Submission Role" %}{% trans ":" %}</span> {{ backoffice_submission_roles|default:"-" }}</li>
</ul>
</div>
<div id="inspect-options" role="tabpanel" tabindex="0" aria-labelledby="tab-options" hidden>
<ul>
<li><span class="parameter">{% trans "Confirmation Page" %}{% trans ":" %}</span> {{ formdef.confirmation|yesno }}</li>
<li><span class="parameter">{% trans "Limit to one form" %}{% trans ":" %}</span> {{ formdef.only_allow_one|yesno }}</li>
{% if formdef.roles %}
<li><span class="parameter">{% trans "Display to unlogged users" %}{% trans ":" %}</span> {{ formdef.always_advertise|yesno }}</li>
{% endif %}
<li><span class="parameter">{% trans "Include button to download all files" %}{% trans ":" %}</span> {{ formdef.include_download_all_button|yesno }}</li>
<li><span class="parameter">{% trans "Skip from per user view" %}{% trans ":" %}</span> {{ formdef.skip_from_360_view|yesno }}</li>
<li><span class="parameter">{% trans "Tracking codes" %}{% trans ":" %}</span> {{ formdef.enable_tracking_codes|yesno }}</li>
{% if formdef.enable_tracking_codes %}
<li><span class="parameter">{% trans "Fields to check after entering the tracking code" %}{% trans ":" %}</span> {{ tracking_code_verify_fields_labels|default:"-" }}</li>
{% endif %}
<li><span class="parameter">{% trans "Lifespan of drafts (in days)" %}{% trans ":" %}</span> {{ formdef.drafts_lifespan|default_if_none:_('default value') }}</li>
<li><span class="parameter">{% trans "Templates" %}</span>
<ul>
<li><span class="parameter">{% trans "Digest" %}{% trans ":" %}</span> {{ formdef.default_digest_template|default:"-" }}</li>
<li><span class="parameter">{% trans "Lateral Block" %}{% trans ":" %}</span> {{ formdef.lateral_template|default:"-" }}</li>
<li><span class="parameter">{% trans "Submission Lateral Block" %}{% trans ":" %}</span> {{ formdef.submission_lateral_template|default:"-" }}</li>
</ul>
</li>
<li><span class="parameter">{% trans "Disable access to form" %}{% trans ":" %}</span> {{ formdef.disabled|yesno }}</li>
<li><span class="parameter">{% trans "Redirection when disabled" %}{% trans ":" %}</span> {{ formdef.disabled_redirection|default:"-" }}</li>
<li><span class="parameter">{% trans "Publication date" %}{% trans ":" %}</span> {{ formdef.publication_date|default:"-" }}</li>
<li><span class="parameter">{% trans "Expiration date" %}{% trans ":" %}</span> {{ formdef.expiration_date|default:"-" }}</li>
</ul>
</div>
<div id="inspect-fields" role="tabpanel" tabindex="0" aria-labelledby="tab-fields" hidden>
<div class="pk-information"><p>
{% blocktrans count page_count=formdef.page_count %}{{ page_count }} page{% plural %}{{ page_count }} pages{% endblocktrans %},
{% blocktrans count fields_count=formdef.fields|count %}{{ fields_count }} field{% plural %}{{ fields_count }} fields.{% endblocktrans %}
</p></div>
{% for field in formdef.fields %}
<div class="inspect-field inspect-field--{{ field.key }} {% if forloop.first %}inspect-field--first{% endif %}" data-field-id="{{ field.id }}">
<h4><a href="fields/{{ field.id }}/">{{ field.ellipsized_label }}</a> <span class="inspect-field-type">- {{ field.get_type_label }}</span></h4>
{{ field.get_parameters_view|safe }}
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}