misc: move filter fields ("FakeField") to their own module (#57779)

This commit is contained in:
Frédéric Péters 2024-04-10 07:38:31 +02:00
parent e905fd8f2c
commit f7ec9ad128
9 changed files with 363 additions and 200 deletions

View File

@ -149,14 +149,14 @@ def test_backoffice_submission_agent_column(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
assert 'submission_agent' not in resp.forms['listing-settings'].fields
assert 'submission-agent' not in resp.forms['listing-settings'].fields
formdef.backoffice_submission_roles = [role]
formdef.store()
resp = app.get('/backoffice/management/form-title/')
assert resp.text.count('</th>') == 6 # four columns
resp.forms['listing-settings']['submission_agent'].checked = True
resp.forms['listing-settings']['submission-agent'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('</th>') == 7 # five columns
assert resp.text.count('data-link') == 1 # 1 row
@ -172,7 +172,7 @@ def test_backoffice_submission_agent_column(pub):
formdata.store()
resp = app.get('/backoffice/management/form-title/')
resp.forms['listing-settings']['submission_agent'].checked = True
resp.forms['listing-settings']['submission-agent'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('>agent<') == 1

View File

@ -1426,7 +1426,7 @@ def test_backoffice_submission_agent_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/?limit=100')
# enable submission-agent column
resp.forms['listing-settings']['submission_agent'].checked = True
resp.forms['listing-settings']['submission-agent'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('>userA<') > 0
assert resp.text.count('>userB<') > 0

View File

@ -516,7 +516,7 @@ def test_criteria_repr():
def test_related_field_repr():
from wcs.backoffice.management import RelatedField
from wcs.backoffice.filter_fields import RelatedField
related_field = RelatedField(None, field=StringField(label='foo'), parent_field=StringField(label='bar'))
assert 'foo' in repr(related_field)

View File

@ -0,0 +1,298 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2024 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 datetime
from django.utils.encoding import force_str
from wcs.qommon import _, misc
class FilterField:
can_include_in_listing = True
id = None
key = None
label = None
addable = True
include_in_statistics = False
geojson_label = None
store_display_value = None
store_structured_value = None
def __init__(self):
self.varname = self.id.replace('-', '_')
self.contextual_id = self.id
self.contextual_varname = self.varname
self.geojson_label = force_str(self.geojson_label or self.label)
def get_view_value(self, value):
# just here to quack like a duck
return None
def get_csv_heading(self):
return [self.label]
def get_csv_value(self, element, **kwargs):
return [element]
@property
def has_relations(self):
False
return bool(self.id == 'user-label')
class RelatedField:
is_related_field = True
key = 'related-field'
varname = None
related_field = None
can_include_in_listing = True
def __init__(self, carddef, field, parent_field):
self.carddef = carddef
self.related_field = field
self.parent_field = parent_field
self.parent_field_id = parent_field.id
@property
def id(self):
return '%s$%s' % (self.parent_field_id, self.related_field.id)
@property
def contextual_id(self):
return self.id
@property
def label(self):
return '%s - %s' % (self.parent_field.label, self.related_field.label)
def __repr__(self):
return '<%s (card: %r, parent: %r, related: %r)>' % (
self.__class__.__name__,
self.carddef,
self.parent_field.label,
self.related_field.label,
)
@property
def store_display_value(self):
return self.related_field.store_display_value
@property
def store_structured_value(self):
return self.related_field.store_structured_value
def get_view_value(self, value, **kwargs):
if value is None:
return ''
if isinstance(value, bool):
return _('Yes') if value else _('No')
if isinstance(value, datetime.date):
return misc.strftime(misc.date_format(), value)
return value
def get_view_short_value(self, value, max_len=30, **kwargs):
return self.get_view_value(value)
def get_csv_heading(self):
if self.related_field:
return self.related_field.get_csv_heading()
return [self.label]
def get_csv_value(self, value, **kwargs):
if self.related_field:
return self.related_field.get_csv_value(value, **kwargs)
return [self.get_view_value(value)]
def get_column_field_id(self):
from wcs.sql import get_field_id
return get_field_id(self.related_field)
class UserRelatedField(RelatedField):
# it is named 'user-label' and not 'user' for compatibility with existing
# listings, as the 'classic' user column is named 'user-label'.
parent_field_id = 'user-label'
store_display_value = None
store_structured_value = None
def __init__(self, field):
self.related_field = field
def __repr__(self):
return '<%s (field: %r)>' % (
self.__class__.__name__,
self.related_field.label,
)
@property
def label(self):
return _('%s of User') % self.related_field.label
class UserLabelRelatedField(UserRelatedField):
# custom user-label column, targetting the "name" (= full name) column
# of the users table
id = 'user-label'
key = 'user-label'
varname = 'user_label'
has_relations = True
def __init__(self):
pass
def __repr__(self):
return '<UserLabelRelatedField>'
def get_column_field_id(self):
return 'name'
@property
def label(self):
return _('User Label')
class DisplayNameFilterField(FilterField):
id = 'name'
key = 'display_name'
label = _('Name')
class StatusFilterField(FilterField):
id = 'status'
key = 'status'
label = _('Status')
include_in_statistics = True
class UserVisibleStatusField(FilterField):
id = 'user-visible-status'
key = 'user-visible-status'
label = _('Status (for user)')
geolabel_status = _('Status')
class InternalIdFilterField(FilterField):
id = 'internal-id'
key = 'internal-id'
label = _('Identifier')
class PeriodStartFilterField(FilterField):
id = 'start'
key = 'period-date'
label = _('Start')
class PeriodEndFilterField(FilterField):
id = 'end'
key = 'period-date'
label = _('End')
class PeriodStartUpdateTimeFilterField(FilterField):
id = 'start-mtime'
key = 'period-date'
label = _('Start (modification time)')
class PeriodEndUpdateTimeFilterField(FilterField):
id = 'end-mtime'
key = 'period-date'
label = _('End (modification time)')
class UserIdFilterField(FilterField):
id = 'user'
key = 'user-id'
label = _('User')
class UserFunctionFilterField(FilterField):
id = 'user-function'
key = 'user-function'
label = _('Current User Function')
class SubmissionAgentFilterField(FilterField):
id = 'submission-agent'
key = 'submission-agent'
label = _('Submission Agent')
addable = False
class SubmissionChannelFilterField(FilterField):
id = 'submission_channel'
key = 'submission_channel'
label = _('Channel')
class CriticalityLevelFilterFiled(FilterField):
id = 'criticality-level'
key = 'criticality-level'
label = _('Criticality Level')
class DigestFilterField(FilterField):
id = 'digest'
key = 'digest'
label = _('Digest')
class IdFilterField(FilterField):
id = 'id'
key = 'id'
def __init__(self, label):
super().__init__()
self.label = label
class TimeFilterField(FilterField):
id = 'time'
key = 'time'
label = _('Created')
class LastUpdateFilterField(FilterField):
id = 'last_update_time'
key = 'last_update_time'
label = _('Last Modified')
class AnonymisedFilterField(FilterField):
id = 'anonymised'
key = 'anonymised'
label = _('Anonymised')
class NumberFilterField(FilterField):
id = 'number'
key = 'number'
label = _('Number')
class IdentifierFilterField(FilterField):
id = 'identifier'
key = 'identifier'
label = _('Identifier')
class DistanceFilterField(FilterField):
id = 'distance'
key = 'distance'
label = _('Distance')

View File

@ -34,6 +34,7 @@ from quixote.http_request import parse_query
from wcs.api_access import ApiAccess
from wcs.api_utils import get_query_flag, get_user_from_api_query_string
from wcs.backoffice import filter_fields
from wcs.backoffice.pagination import pagination_links
from wcs.carddef import CardDef
from wcs.categories import Category
@ -761,8 +762,8 @@ class ManagementDirectory(Directory):
criterias = self.get_global_listing_criterias()
formdatas = sql.AnyFormData.select(criterias + [NotNull('geoloc_base_x'), Null('anonymised')])
fields = [
FakeField('name', 'display_name', _('Name')),
FakeField('status', 'status', _('Status')),
filter_fields.DisplayNameFilterField(),
filter_fields.StatusFilterField(),
]
get_response().set_content_type('application/json')
return json.dumps(geojson_formdatas(formdatas, fields=fields), cls=misc.JSONEncoder)
@ -1147,7 +1148,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
'period-date',
'user-id',
'user-function',
'submission-agent-id',
'submission-agent',
'date',
'distance',
'criticality-level',
@ -1166,25 +1167,24 @@ class FormPage(Directory, TempfileDirectoryMixin):
waitpoint_status = self.formdef.workflow.get_waitpoint_status()
fake_fields = [
FakeField('internal-id', 'internal-id', _('Identifier')),
FakeField('start', 'period-date', _('Start')),
FakeField('end', 'period-date', _('End')),
FakeField('user', 'user-id', _('User')),
FakeField('user-function', 'user-function', _('Current User Function')),
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent'), addable=False),
filter_fields.InternalIdFilterField(),
filter_fields.PeriodStartFilterField(),
filter_fields.PeriodEndFilterField(),
filter_fields.UserIdFilterField(),
filter_fields.UserFunctionFilterField(),
]
if self.formdef.workflow.criticality_levels:
fake_fields.append(FakeField('criticality-level', 'criticality-level', _('Criticality Level')))
fake_fields.append(filter_fields.CriticalityLevelFilterFiled())
default_filters = self.get_default_filters(mode)
filter_fields = []
available_fields = []
for field in fake_fields + list(self.get_formdef_fields()):
field.enabled = False
if field.key not in self.get_filterable_field_types() + ['status']:
continue
if field.key == 'status' and not waitpoint_status:
continue
filter_fields.append(field)
available_fields.append(field)
if getattr(field, 'block_field', None):
field.label = '%s / %s' % (field.block_field.label, field.label)
@ -1263,7 +1263,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
result += htmltext('</div>')
return result
for filter_field in filter_fields:
for filter_field in available_fields:
if not filter_field.enabled:
continue
@ -1346,7 +1346,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
)
r += render_widget(widget, operators=[])
elif filter_field.key == 'submission-agent-id':
elif filter_field.key == 'submission-agent':
r += HiddenWidget(filter_field_key, value=filter_field_value).render()
if filter_field_value:
filtered_user = get_publisher().user_class.get(filter_field_value, ignore_errors=True)
@ -1485,7 +1485,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
# field filter dialog content
r += htmltext('<div style="display: none;">')
r += htmltext('<ul id="field-filter" class="objects-list">')
for field in filter_fields:
for field in available_fields:
addable = getattr(field, 'addable', True)
r += htmltext('<li %s>') % ('' if addable else 'hidden')
r += htmltext('<label for="fields-filter-%s">') % field.contextual_id
@ -1591,7 +1591,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
classnames = 'has-relations-field'
attrs = 'data-field-id="%s"' % field.id
seen_parents.add(field.id)
elif isinstance(field, RelatedField):
elif isinstance(field, filter_fields.RelatedField):
classnames = 'related-field'
if field.parent_field_id in seen_parents:
classnames += ' collapsed'
@ -1874,24 +1874,24 @@ class FormPage(Directory, TempfileDirectoryMixin):
return redirect('..')
def get_formdef_fields(self, include_block_items_fields=False):
yield FakeField('id', 'id', _('Identifier') if self.formdef.id_template else _('Number'))
yield filter_fields.IdFilterField(label=_('Identifier') if self.formdef.id_template else _('Number'))
if self.formdef.default_digest_template:
yield FakeField('digest', 'digest', _('Digest'))
yield FakeField('submission_channel', 'submission_channel', _('Channel'))
yield filter_fields.DigestFilterField()
yield filter_fields.SubmissionChannelFilterField()
if self.formdef.backoffice_submission_roles:
yield FakeField('submission_agent', 'submission_agent', _('Submission By'))
yield FakeField('time', 'time', _('Created'))
yield FakeField('last_update_time', 'last_update_time', _('Last Modified'))
yield filter_fields.SubmissionAgentFilterField()
yield filter_fields.TimeFilterField()
yield filter_fields.LastUpdateFilterField()
# user fields
# user-label field but as a custom field, to get full name of user
# using a sql join clause.
yield UserLabelRelatedField()
yield filter_fields.UserLabelRelatedField()
for field in get_publisher().user_class.get_fields():
if not field.can_include_in_listing:
continue
field.has_relations = True
yield UserRelatedField(field)
yield filter_fields.UserRelatedField(field)
for field in self.formdef.iter_fields(include_block_fields=True):
if getattr(field, 'block_field', None):
@ -1917,17 +1917,12 @@ class FormPage(Directory, TempfileDirectoryMixin):
if not card_field.can_include_in_listing:
continue
field.has_relations = True
yield RelatedField(carddef, card_field, field)
yield filter_fields.RelatedField(carddef, card_field, field)
yield FakeField('status', 'status', _('Status'), include_in_statistics=True)
yield filter_fields.StatusFilterField()
if any(x.get_visibility_mode() != 'all' for x in self.formdef.workflow.possible_status):
yield FakeField(
'user-visible-status',
'user-visible-status',
_('Status (for user)'),
geojson_label=_('Status'),
)
yield FakeField('anonymised', 'anonymised', _('Anonymised'))
yield filter_fields.UserVisibleStatusField()
yield filter_fields.AnonymisedFilterField()
def get_default_columns(self):
if self.view:
@ -2004,18 +1999,18 @@ class FormPage(Directory, TempfileDirectoryMixin):
statistics_fields_only=False,
):
fake_fields = [
FakeField('internal-id', 'internal-id', _('Identifier')),
FakeField('number', 'number', _('Number')),
FakeField('identifier', 'identifier', _('Identifier')),
FakeField('start', 'period-date', _('Start')),
FakeField('end', 'period-date', _('End')),
FakeField('start-mtime', 'period-date', _('Start (modification time)')),
FakeField('end-mtime', 'period-date', _('End (modification time)')),
FakeField('user', 'user-id', _('User')),
FakeField('user-function', 'user-function', _('Current User Function')),
FakeField('submission-agent', 'submission-agent-id', _('Submission Agent')),
FakeField('distance', 'distance', _('Distance')),
FakeField('criticality-level', 'criticality-level', _('Criticality Level')),
filter_fields.InternalIdFilterField(),
filter_fields.NumberFilterField(),
filter_fields.IdentifierFilterField(),
filter_fields.PeriodStartFilterField(),
filter_fields.PeriodEndFilterField(),
filter_fields.PeriodStartUpdateTimeFilterField(),
filter_fields.PeriodEndUpdateTimeFilterField(),
filter_fields.UserIdFilterField(),
filter_fields.UserFunctionFilterField(),
filter_fields.SubmissionAgentFilterField(),
filter_fields.DistanceFilterField(),
filter_fields.CriticalityLevelFilterFiled(),
]
criterias = []
@ -2108,7 +2103,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
# allow for short form, with a single query parameter
filters_dict['filter-user-function-value'] = filters_dict.get('filter-user-function')
if filter_field.key == 'submission-agent-id':
if filter_field.key == 'submission-agent':
# convert uuid based filter into local id filter
name_id = filters_dict.get('filter-submission-agent-uuid')
if name_id:
@ -2301,7 +2296,7 @@ class FormPage(Directory, TempfileDirectoryMixin):
criterias.append(Nothing())
else:
criterias.append(Equal('user_id', filter_field_value))
elif filter_field.key == 'submission-agent-id':
elif filter_field.key == 'submission-agent':
criterias.append(Equal('submission_agent_id', filter_field_value))
elif filter_field.key == 'user-function':
user_object = None
@ -4208,148 +4203,6 @@ class FormBackOfficeStatusPage(FormStatusPage):
return self.test_tool_result()
class FakeField:
can_include_in_listing = True
def __init__(self, id, type_key, label, addable=True, include_in_statistics=False, geojson_label=None):
self.id = id
self.contextual_id = self.id
self.key = type_key
self.label = force_str(label)
self.fake = True
self.varname = id.replace('-', '_')
self.contextual_varname = self.varname
self.store_display_value = None
self.store_structured_value = None
self.addable = addable
self.include_in_statistics = include_in_statistics
self.geojson_label = force_str(geojson_label or self.label)
def get_view_value(self, value):
# just here to quack like a duck
return None
def get_csv_heading(self):
return [self.label]
def get_csv_value(self, element, **kwargs):
return [element]
@property
def has_relations(self):
return bool(self.id == 'user-label')
class RelatedField:
is_related_field = True
key = 'related-field'
varname = None
related_field = None
can_include_in_listing = True
def __init__(self, carddef, field, parent_field):
self.carddef = carddef
self.related_field = field
self.parent_field = parent_field
self.parent_field_id = parent_field.id
@property
def id(self):
return '%s$%s' % (self.parent_field_id, self.related_field.id)
@property
def contextual_id(self):
return self.id
@property
def label(self):
return '%s - %s' % (self.parent_field.label, self.related_field.label)
def __repr__(self):
return '<%s (card: %r, parent: %r, related: %r)>' % (
self.__class__.__name__,
self.carddef,
self.parent_field.label,
self.related_field.label,
)
@property
def store_display_value(self):
return self.related_field.store_display_value
@property
def store_structured_value(self):
return self.related_field.store_structured_value
def get_view_value(self, value, **kwargs):
if value is None:
return ''
if isinstance(value, bool):
return _('Yes') if value else _('No')
if isinstance(value, datetime.date):
return misc.strftime(misc.date_format(), value)
return value
def get_view_short_value(self, value, max_len=30, **kwargs):
return self.get_view_value(value)
def get_csv_heading(self):
if self.related_field:
return self.related_field.get_csv_heading()
return [self.label]
def get_csv_value(self, value, **kwargs):
if self.related_field:
return self.related_field.get_csv_value(value, **kwargs)
return [self.get_view_value(value)]
def get_column_field_id(self):
return get_field_id(self.related_field)
class UserRelatedField(RelatedField):
# it is named 'user-label' and not 'user' for compatibility with existing
# listings, as the 'classic' user column is named 'user-label'.
parent_field_id = 'user-label'
store_display_value = None
store_structured_value = None
def __init__(self, field):
self.related_field = field
def __repr__(self):
return '<%s (field: %r)>' % (
self.__class__.__name__,
self.related_field.label,
)
@property
def label(self):
return _('%s of User') % self.related_field.label
class UserLabelRelatedField(UserRelatedField):
# custom user-label column, targetting the "name" (= full name) column
# of the users table
id = 'user-label'
key = 'user-label'
varname = 'user_label'
has_relations = True
def __init__(self):
pass
def __repr__(self):
return '<UserLabelRelatedField>'
def get_column_field_id(self):
return 'name'
@property
def label(self):
return _('User Label')
def do_graphs_section(period_start=None, period_end=None, criterias=None):
from wcs import sql

View File

@ -47,6 +47,17 @@ class CustomView(StorableObject):
xml_root_node = 'custom_view'
def migrate(self):
changed = False
# 2024-04-10
if self.columns and 'submission_agent' in [x['id'] for x in self.columns['list']]:
self.columns['list'] = [
{'id': x['id'].replace('submission_agent', 'submission-agent')} for x in self.columns['list']
]
changed = True
if changed:
self.store()
@property
def user(self):
return get_publisher().user_class.get(self.user_id)

View File

@ -990,7 +990,7 @@ class FormData(StorableObject):
return StatusFieldValue(self.get_visible_status(user=None))
if field.key == 'submission_channel':
return self.get_submission_channel_label()
if field.key == 'submission_agent':
if field.key == 'submission-agent':
try:
agent_user = self.submission_agent_id
return get_publisher().user_class.get(agent_user).display_name

View File

@ -19,6 +19,7 @@ import urllib.parse
from quixote import get_publisher, get_request, get_session, redirect
from quixote.html import TemplateIO, htmltext
from wcs.backoffice.filter_fields import FilterField
from wcs.backoffice.pagination import pagination_links
from wcs.roles import logged_users_role
from wcs.sql_criterias import Contains, FtsMatch, Intersects, Not, NotContains, Null, StrictNotEqual
@ -145,11 +146,11 @@ class FormDefUI:
return htmltext('<span title="%s">%s</span>') % (label, misc.ellipsize(label, 20))
for f in fields:
if getattr(f, 'fake', False):
if isinstance(f, FilterField):
field_sort_key = f.id
if f.id == 'time':
field_sort_key = 'receipt_time'
elif f.id in ('user-label', 'submission_agent'):
elif f.id in ('user-label', 'submission-agent'):
field_sort_key = None
elif getattr(f, 'is_related_field', False):
field_sort_key = None
@ -390,7 +391,7 @@ class FormDefUI:
'user-label': 'cell-user',
'status': 'cell-status',
'anonymised': 'cell-anonymised',
'submission_agent': 'cell-submission-agent',
'submission-agent': 'cell-submission-agent',
}.get(f.key)
if css_class:
r += htmltext('<td class="%s">' % css_class)

View File

@ -233,9 +233,9 @@ class LazyFormDefObjectsManager:
return qs
def filter_by_internal_id(self, value, op='eq'):
from wcs.backoffice.management import FakeField
from wcs.backoffice.filter_fields import InternalIdFilterField
field = FakeField('internal-id', 'internal-id', 'fake')
field = InternalIdFilterField()
operators = self.get_field_allowed_operators(field)
if op not in [o[0] for o in operators]:
self.report_error(