From 00bd0aeebabc1d54750d486ac9a399bf86d780c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 1 Apr 2015 20:15:00 +0200 Subject: [PATCH] backoffice: filter listings by time period and closed list values (#4505) --- tests/test_backoffice_pages.py | 29 +++++++ wcs/backoffice/root.py | 160 +++++++++++++++++++++++++++++------- wcs/fields.py | 34 ++++---- wcs/formdata.py | 9 ++ wcs/forms/backoffice.py | 11 ++- wcs/qommon/static/css/dc2/admin.css | 4 + wcs/qommon/static/js/wcs.listing.js | 40 ++++++++- 7 files changed, 239 insertions(+), 48 deletions(-) diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py index 1f64040..72f7c5e 100644 --- a/tests/test_backoffice_pages.py +++ b/tests/test_backoffice_pages.py @@ -72,10 +72,13 @@ def create_environment(set_receiver=True): formdata.data = {'1': 'FOO BAR %d' % i} if i%4 == 0: formdata.data['2'] = 'foo' + formdata.data['2_display'] = 'foo' elif i%4 == 1: formdata.data['2'] = 'bar' + formdata.data['2_display'] = 'bar' else: formdata.data['2'] = 'baz' + formdata.data['2_display'] = 'baz' if i%3 == 0: formdata.jump_status('new') else: @@ -167,6 +170,32 @@ def test_backoffice_columns(pub): assert resp.body.count('data-link') == 17 # 17 rows assert resp.body.count('FOO BAR') == 0 # no field 1 column +def test_backoffice_filter(pub): + create_superuser(pub) + create_environment() + app = login(get_app(pub)) + resp = app.get('/backoffice/form-title/') + assert resp.forms[0]['filter-status'].checked == True + resp.forms[0]['filter-status'].checked = False + resp.forms[0]['filter-2'].checked = True + resp = resp.forms[0].submit() + assert '') + filters = [('all', _('All'), None), + ('pending', _('Pending'), None), + ('done', _('Done'), None)] + for status in waitpoint_status: + filters.append((status.id, status.name, status.colour)) + for filter_id, filter_label, filter_colour in filters: + if filter_id == selected_filter: + selected = ' selected="selected"' + else: + selected = '' + style = '' + if filter_colour and filter_colour != 'FFFFFF': + fg_colour = misc.get_foreground_colour(filter_colour) + style = 'style="background: #%s; color: %s;"' % ( + filter_colour, fg_colour) + r += htmltext('') % filter_label + r += htmltext('') + r += htmltext('') + r += htmltext('') + + elif filter_field.type == 'period-date': + r += DateWidget(filter_field_key, title=filter_field.label, + value=filter_field_value, render_br=False).render() + + elif filter_field.type == 'item': + filter_field.required = False + options = filter_field.get_options() + r += SingleSelectWidget(filter_field_key, title=filter_field.label, + options=options, value=filter_field_value, + render_br=False).render() + + # field filter dialog content + r += htmltext('
') + r += htmltext('') + r += htmltext('
') + + return r.getvalue() + def get_fields_sidebar(self, selected_filter, fields, offset=None, limit=None, order_by=None): get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js']) get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css') r = TemplateIO(html=True) - r += htmltext('
') + r += htmltext('') if offset or limit: if not offset: offset = 0 @@ -553,7 +641,6 @@ class FormPage(Directory): order_by = '' r += htmltext('') % order_by - waitpoint_status = self.formdef.workflow.get_waitpoint_status() if get_publisher().is_using_postgresql(): r += htmltext('

%s

') % _('Search') if get_request().form.get('q'): @@ -565,31 +652,12 @@ class FormPage(Directory): r += htmltext('') r += htmltext('') % _('Search') - if waitpoint_status: - r += htmltext('

%s

') % _('Status to display') - r += htmltext('') + r += self.get_filter_sidebar(selected_filter=selected_filter) r += htmltext('') % _('Refresh') - r += htmltext('') % _('Columns Settings') + + # column settings dialog content r += htmltext('
') r += htmltext('
    ') for field in self.get_formdef_fields(): @@ -598,12 +666,14 @@ class FormPage(Directory): r += htmltext('
  • ') - r += htmltext('') % (field.id, misc.ellipsize(field.label, 70)) + r += htmltext('') % ( + field.id, misc.ellipsize(field.label, 70)) r += htmltext('
  • ') r += htmltext('
') - r += htmltext('
') # id="columns-settings" + r += htmltext('') + r += htmltext('
') return r.getvalue() @@ -641,11 +711,45 @@ class FormPage(Directory): return 'pending' return 'all' + def get_criterias_from_query(self): + period_fake_fields = [ + FakeField('start', 'period-date', _('Start')), + FakeField('end', 'period-date', _('End')), + ] + filter_fields = [] + criterias = [] + format_string = misc.date_format() + for filter_field in period_fake_fields + self.get_formdef_fields(): + if filter_field.type not in ('item', 'period-date'): + continue + + if not get_request().form.get('filter-%s' % filter_field.id): + # the field is not enabled + continue + + filter_field_key = 'filter-%s-value' % filter_field.id + filter_field_value = get_request().form.get(filter_field_key) + if not filter_field_value: + continue + + if filter_field.id == 'start': + period_start = time.strptime(filter_field_value, format_string) + criterias.append(GreaterOrEqual('receipt_time', period_start)) + elif filter_field.id == 'end': + period_end = time.strptime(filter_field_value, format_string) + criterias.append(LessOrEqual('receipt_time', period_end)) + elif filter_field.type == 'item' and filter_field_value not in (None, 'None'): + criterias.append(Equal('f%s' % filter_field.id, filter_field_value)) + + return criterias + + def _q_index(self): get_logger().info('backoffice - form %s - listing' % self.formdef.name) fields = self.get_fields_from_query() selected_filter = self.get_filter_from_query() + criterias = self.get_criterias_from_query() if get_publisher().is_using_postgresql(): # only enable pagination in SQL mode, as we do not have sorting in @@ -664,7 +768,7 @@ class FormPage(Directory): table = FormDefUI(self.formdef).listing(fields=fields, selected_filter=selected_filter, include_form=True, limit=int(limit), offset=int(offset), query=query, - order_by=order_by) + order_by=order_by, criterias=criterias) if get_request().form.get('ajax') == 'true': get_response().filter = None diff --git a/wcs/fields.py b/wcs/fields.py index df2f296..558d228 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -844,22 +844,26 @@ class ItemField(WidgetField): self.items = [] WidgetField.__init__(self, **kwargs) + def get_options(self): + if not self.data_source: + return self.items + + options = data_sources.get_items(self.data_source) + if options and not self.required: + if type(options[0]) is str: + options[:0] = [None] + elif len(options) == 2: + options[:0] = [(None, '---')] + elif len(options[0]) == 3: + options[:0] = [(None, '---', None)] + return options + def perform_more_widget_changes(self, form, kwargs, edit = True): - if self.data_source: - if self.data_source.get('type') == 'jsonp': - kwargs['url'] = self.data_source.get('value') - self.widget_class = JsonpSingleSelectWidget - else: - kwargs['options'] = data_sources.get_items(self.data_source) - if kwargs['options'] and not self.required: - if type(kwargs['options'][0]) is str: - kwargs['options'][:0] = [None] - elif len(kwargs['options'][0]) == 2: - kwargs['options'][:0] = [(None, '---')] - elif len(kwargs['options'][0]) == 3: - kwargs['options'][:0] = [(None, '---', None)] - elif self.items: - kwargs['options'] = self.items + if self.data_source and self.data_source.get('type') == 'jsonp': + kwargs['url'] = self.data_source.get('value') + self.widget_class = JsonpSingleSelectWidget + else: + kwargs['options'] = self.get_options() if not kwargs.get('options'): kwargs['options'] = [(None, '---')] if self.show_as_radio: diff --git a/wcs/formdata.py b/wcs/formdata.py index 4034676..1f7a1e8 100644 --- a/wcs/formdata.py +++ b/wcs/formdata.py @@ -549,6 +549,15 @@ class FormData(StorableObject): field.feed_session(self.data.get(field.id), self.data.get('%s_display' % field.id)) + def __getattr__(self, attr): + try: + return self.__dict__[attr] + except KeyError: + # give direct access to values from the data dictionary + if attr[0] == 'f': + return self.__dict__['data'][attr[1:]] + raise AttributeError(attr) + # don't pickle _formdef cache def __getstate__(self): odict = self.__dict__ diff --git a/wcs/forms/backoffice.py b/wcs/forms/backoffice.py index 3a14a86..35d4530 100644 --- a/wcs/forms/backoffice.py +++ b/wcs/forms/backoffice.py @@ -27,7 +27,7 @@ class FormDefUI(object): def listing(self, fields, selected_filter='all', url_action=None, include_form=False, items=None, offset=0, limit=0, - query=None, order_by=None): + query=None, order_by=None, criterias=None): partial_display = False @@ -35,7 +35,8 @@ class FormDefUI(object): if offset and not limit: limit = 20 items, total_count = self.get_listing_items( - selected_filter, offset, limit, query, order_by) + selected_filter, offset, limit, query, order_by, + criterias=criterias) if (offset > 0) or (total_count > limit > 0): partial_display = True @@ -130,7 +131,7 @@ class FormDefUI(object): return r.getvalue() def get_listing_items(self, selected_filter='all', offset=None, - limit=None, query=None, order_by=None, user=None): + limit=None, query=None, order_by=None, user=None, criterias=None): formdata_class = self.formdef.data_class() if selected_filter == 'all': item_ids = [int(x) for x in formdata_class.keys()] @@ -153,6 +154,10 @@ class FormDefUI(object): query_ids = formdata_class.get_ids_from_query(query) item_ids = list(set(item_ids).intersection(query_ids)) + if criterias: + select_ids = [x.id for x in formdata_class.select(clause=criterias)] + item_ids = list(set(item_ids).intersection(select_ids)) + if self.formdef.acl_read != 'all' and item_ids: # if the formdef has some ACL defined, we don't go the full way of # supporting all the cases but assume that as we are in the diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index be60142..3f36b9e 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -887,3 +887,7 @@ fieldset.form-plus.closed legend:after { width: auto; } } + +a#filter-settings { + cursor: pointer; +} diff --git a/wcs/qommon/static/js/wcs.listing.js b/wcs/qommon/static/js/wcs.listing.js index ab6d10f..d3d5865 100644 --- a/wcs/qommon/static/js/wcs.listing.js +++ b/wcs/qommon/static/js/wcs.listing.js @@ -85,6 +85,8 @@ function refresh_table() { } $(function() { + var must_reload_page = false; + /* column settings */ $('#columns-settings').click(function() { var dialog = $('
'); @@ -113,6 +115,37 @@ $(function() { }]); return false; }); + + /* filter settings */ + $('#filter-settings').click(function() { + var dialog = $(''); + $('#field-filter').clone().appendTo(dialog); + $(dialog).find('input').each(function(idx, elem) { + $(this).attr('id', 'dlg-' + $(this).attr('id')); + }); + $(dialog).find('label').each(function(idx, elem) { + $(this).attr('for', 'dlg-' + $(this).attr('for')); + }); + $(dialog).dialog({ + modal: true, + resizable: false, + title: $('#filter-settings').parents('h3').find('span:first-child').text(), + width: '30em'}); + $(dialog).dialog('option', 'buttons', [ + {text: $('form#listing-settings button.refresh').text(), + click: function() { + $(this).find('input[type="checkbox"]').each(function(idx, elem) { + $('form#listing-settings input[name="' + $(elem).attr('name') + '"]').prop('checked', + $(elem).prop('checked')); + }); + $(this).dialog('close'); + must_reload_page = true; + $('form#listing-settings').submit(); + } + }]); + return false; + }); + /* possibility to toggle the sidebar */ $('#main-content').after($('')); $('#sidebar-toggle').click(function() { @@ -124,12 +157,15 @@ $(function() { $('#main-content').animate({width: '95%'}); } }); - /* automatically refresh on status change */ - $('form#listing-settings select[name="filter"]').change(function() { + /* automatically refresh on filter change */ + $('form#listing-settings select').change(function() { $('form#listing-settings').submit(); }); /* partial table refresh */ $('form#listing-settings').submit(function(event) { + if (must_reload_page) { + return true; + } event.preventDefault(); refresh_table(); return false; -- 2.1.4