wcs/wcs/forms/backoffice.py

367 lines
14 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 urllib.parse
from quixote import get_publisher, get_request, get_session, redirect
from quixote.html import TemplateIO, htmltext
from wcs.qommon.storage import Contains, FtsMatch, Intersects, Not, NotContains, Null, StrictNotEqual
from wcs.roles import logged_users_role
from ..qommon import _, misc
from ..qommon.backoffice.listing import pagination_links
class FormDefUI:
def __init__(self, formdef):
self.formdef = formdef
def listing(
self,
fields,
selected_filter='all',
selected_filter_operator='eq',
url_action=None,
items=None,
offset=0,
limit=0,
query=None,
order_by=None,
criterias=None,
include_checkboxes=False,
):
# noqa pylint: disable=too-many-arguments
if not items:
if offset and not limit:
limit = int(get_publisher().get_site_option('default-page-size') or 20)
if not criterias:
criterias = []
criterias.append(Null('anonymised'))
items, total_count = self.get_listing_items(
fields,
selected_filter,
selected_filter_operator,
offset,
limit,
query,
order_by,
criterias=criterias,
)
if offset > total_count:
get_request().form['offset'] = '0'
return redirect('?' + urllib.parse.urlencode(get_request().form))
r = TemplateIO(html=True)
if self.formdef.workflow:
colours = []
for status in self.formdef.workflow.possible_status:
if status.colour and status.colour != 'FFFFFF':
fg_colour = misc.get_foreground_colour(status.colour)
colours.append((status.id, status.colour, fg_colour))
if colours:
r += htmltext('<style>')
for status_id, bg_colour, fg_colour in colours:
r += htmltext(
'tr.status-%s-wf-%s td.cell-status { '
'background-color: #%s !important; color: %s !important; }\n'
% (self.formdef.workflow.id, status_id, bg_colour, fg_colour)
)
r += htmltext('</style>')
r += htmltext('<table id="listing" class="main compact">')
r += htmltext('<colgroup>')
if include_checkboxes:
r += htmltext('<col/>') # checkbox
r += htmltext('<col/>') # lock
r += htmltext('<col/>')
r += htmltext('<col/>')
for f in fields:
r += htmltext('<col />')
r += htmltext('</colgroup>')
r += htmltext('<thead><tr>')
if self.formdef.workflow.criticality_levels:
r += htmltext(
'<th class="criticality-level-cell" data-field-sort-key="criticality_level"><span></span></th>'
)
else:
r += htmltext('<th></th>') # lock
if include_checkboxes:
r += htmltext('<th class="select"><input type="checkbox" id="top-select"/>')
r += htmltext(
' <span id="info-all-rows"><label><input type="checkbox" name="select[]" value="_all"/> %s</label></span></th>'
) % _('Run selected action on all pages')
for f in fields:
if getattr(f, 'fake', False):
field_sort_key = f.id
if f.id == 'time':
field_sort_key = 'receipt_time'
elif f.id in ('user-label', 'submission_agent'):
field_sort_key = None
elif getattr(f, 'is_related_field', False):
field_sort_key = None
elif getattr(f, 'block_field', None) and f.block_field.max_items != 1:
# allow sorting on a field of block field if there is one item only
field_sort_key = None
else:
field_sort_key = 'f%s' % f.contextual_id
if field_sort_key:
r += htmltext('<th data-field-sort-key="%s">') % field_sort_key
else:
r += htmltext('<th>')
if getattr(f, 'block_field', None):
f.label = '%s / %s' % (f.block_field.label, f.label)
if len(f.label) < 20:
r += htmltext('<span>%s</span>') % f.label
else:
r += htmltext('<span title="%s">%s</span>') % (f.label, misc.ellipsize(f.label, 20))
r += htmltext('</th>')
r += htmltext('</tr></thead>')
r += htmltext('<tbody>')
r += htmltext(self.tbody(fields, items, url_action, include_checkboxes=include_checkboxes))
r += htmltext('</tbody>')
r += htmltext('</table>')
# add links to paginate
r += pagination_links(offset, limit, total_count)
return r.getvalue()
def get_listing_item_ids(
self,
selected_filter='all',
selected_filter_operator='eq',
query=None,
order_by=None,
user=None,
criterias=None,
anonymise=False,
):
formdata_class = self.formdef.data_class()
criterias = [] or criterias[:]
criterias.append(StrictNotEqual('status', 'draft'))
if selected_filter == 'all':
if selected_filter_operator == 'ne':
# nothing
return []
elif selected_filter == 'waiting':
user_roles = [logged_users_role().id] + user.get_roles()
actionable_criteria = formdata_class.get_actionable_ids_criteria(user_roles)
if selected_filter_operator == 'ne':
criterias.append(Not(actionable_criteria))
else:
criterias.append(actionable_criteria)
else:
# build selected status list
applied_filters = []
if selected_filter == 'pending':
applied_filters = ['wf-%s' % x.id for x in self.formdef.workflow.get_not_endpoint_status()]
elif selected_filter == 'done':
applied_filters = ['wf-%s' % x.id for x in self.formdef.workflow.get_endpoint_status()]
else:
applied_filters = ['wf-%s' % selected_filter]
if selected_filter_operator == 'ne':
# exclude selected status list
criterias.append(NotContains('status', applied_filters))
else:
# only selected status list
criterias.append(Contains('status', applied_filters))
if query:
criterias.append(FtsMatch(query))
if not anonymise:
# as we are in the backoffice, we don't have to care about the
# situation where the user is the submitter, and we limit ourselves
# to consider treating roles.
if not user.is_admin:
user_roles = [str(x) for x in user.get_roles()]
criterias.append(Intersects('concerned_roles_array', user_roles))
if order_by and not anonymise:
direction = ''
if order_by.startswith('-'):
order_by = order_by[1:]
direction = '-'
for field in self.formdef.iter_fields(include_block_fields=True):
if getattr(field, 'block_field', None):
if field.key == 'items':
# not yet
continue
if order_by not in [field.contextual_varname, 'f%s' % field.contextual_id]:
continue
if field.contextual_varname == order_by:
order_by = "f%s" % field.contextual_id
if getattr(field, 'block_field', None) and 'f%s' % field.contextual_id == order_by:
# field of block field, sort on the first element
order_by = "f%s->'data'->0->>'%s%s'" % (
field.block_field.id,
field.id,
"_display" if field.store_display_value else "",
)
elif field.store_display_value:
order_by += "_display"
break
order_by = '%s%s' % (direction, order_by)
elif not anonymise and query:
order_by = 'rank'
else:
order_by = '-id'
return list(formdata_class.get_sorted_ids(order_by, criterias))
def get_listing_items(
self,
fields=None,
selected_filter='all',
selected_filter_operator='eq',
offset=None,
limit=None,
query=None,
order_by=None,
user=None,
criterias=None,
anonymise=False,
):
# noqa pylint: disable=too-many-arguments
user = user or get_request().user
formdata_class = self.formdef.data_class()
if order_by and not hasattr(formdata_class, 'get_sorted_ids'):
# get_sorted_ids is only implemented in the SQL backend
order_by = None
item_ids = self.get_listing_item_ids(
selected_filter=selected_filter,
selected_filter_operator=selected_filter_operator,
query=query,
order_by=order_by,
user=user,
criterias=criterias,
anonymise=anonymise,
)
total_count = len(item_ids)
if not offset:
offset = 0
kwargs = {}
kwargs['fields'] = fields
if limit:
items = formdata_class.get_ids(item_ids[offset : offset + limit], keep_order=True, **kwargs)
else:
items = formdata_class.get_ids(item_ids, keep_order=True, **kwargs)
return (items, total_count)
def tbody(self, fields=None, items=None, url_action=None, include_checkboxes=False):
r = TemplateIO(html=True)
if url_action:
pass
# url_action = '/' + url_action
else:
url_action = ''
user = get_request().user
user_roles = set(user.get_roles())
visited_objects = get_session().get_visited_objects(exclude_user=user.id)
include_criticality_level = bool(self.formdef.workflow.criticality_levels)
for i, filled in enumerate(items):
classes = ['status-%s-%s' % (filled.formdef.workflow.id, filled.status)]
if i % 2:
classes.append('even')
else:
classes.append('odd')
if filled.get_object_key() in visited_objects:
classes.append('advisory-lock')
if filled.backoffice_submission:
classes.append('backoffice-submission')
style = ''
if include_criticality_level:
try:
level = filled.get_criticality_level_object()
except IndexError:
style = ''
else:
classes.append('criticality-level')
style = ' style="border-left-color: #%s;"' % level.colour
link = str(filled.id) + '/'
data = ' data-link="%s"' % link
if filled.anonymised:
data += ' data-anonymised="true"'
r += htmltext('<tr class="%s"%s>' % (' '.join(classes), data))
if include_criticality_level:
r += htmltext('<td class="criticality-level-cell" %s></td>' % style) # criticality_level
else:
r += htmltext('<td class="lock-cell"></td>') # lock
if include_checkboxes:
r += htmltext('<td class="select"><input type="checkbox" name="select[]" ')
r += htmltext('value="%s"') % filled.id
workflow_roles = {}
if self.formdef.workflow_roles:
workflow_roles.update(self.formdef.workflow_roles)
if filled.workflow_roles:
workflow_roles.update(filled.workflow_roles)
for function_key, function_value in workflow_roles.items():
if isinstance(function_value, (str, int)):
# single role, defined at formdef level
# (int are for compatibility with very old forms)
function_values = {str(function_value)}
else:
# list of roles (or none), defined at formdata level
function_values = set(function_value or [])
if user_roles.intersection(function_values):
# dashes are replaced by underscores to prevent HTML5
# normalization to CamelCase.
r += htmltext(' data-is_%s="true" ' % function_key.replace('-', '_'))
r += htmltext('/></td>')
for i, f in enumerate(fields):
field_value = filled.get_field_view_value(f, max_length=30)
if f.type == 'id':
r += htmltext('<td class="cell-id"><a href="%s%s">%s</a></td>') % (
link,
url_action,
field_value,
)
continue
css_class = {
'time': 'cell-time',
'last_update_time': 'cell-time',
'user-label': 'cell-user',
'status': 'cell-status',
'anonymised': 'cell-anonymised',
'submission_agent': 'cell-submission-agent',
}.get(f.type)
if css_class:
r += htmltext('<td class="%s">' % css_class)
else:
r += htmltext('<td>')
r += field_value.replace('[download]', str('%sdownload' % link))
r += htmltext('</td>')
r += htmltext('</tr>\n')
return r.getvalue()