backoffice: filter listings by time period and closed list values (#4505)

This commit is contained in:
Frédéric Péters 2015-04-01 20:15:00 +02:00
parent 128978d782
commit bfc0d859db
7 changed files with 242 additions and 48 deletions

View File

@ -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 '<select name="filter">' not in resp.body
resp.forms[0]['filter-2-value'] = 'baz'
resp = resp.forms[0].submit()
assert resp.body.count('<td>baz</td>') == 8
assert resp.body.count('<td>foo</td>') == 0
assert resp.body.count('<td>bar</td>') == 0
resp.forms[0]['filter-start'].checked = True
resp = resp.forms[0].submit()
resp.forms[0]['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms[0].submit()
assert resp.body.count('<td>baz</td>') == 0
resp.forms[0]['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms[0].submit()
assert resp.body.count('<td>baz</td>') == 8
def test_backoffice_csv(pub):
create_superuser(pub)
create_environment()

View File

@ -44,6 +44,7 @@ from wcs.forms.backoffice import FormDefUI
import wcs.admin.forms
import wcs.admin.workflows
from wcs import data_sources
from wcs.api import get_user_from_api_query_string
@ -534,13 +535,100 @@ class FormPage(Directory):
r += htmltext('</ul>')
return r.getvalue()
def get_filter_sidebar(self, selected_filter=None):
r = TemplateIO(html=True)
waitpoint_status = self.formdef.workflow.get_waitpoint_status()
period_fake_fields = [
FakeField('start', 'period-date', _('Start')),
FakeField('end', 'period-date', _('End')),
]
filter_fields = []
for field in period_fake_fields + self.get_formdef_fields():
field.enabled = False
if field.type not in ('item', 'period-date', 'status'):
continue
if field.type == 'status' and not waitpoint_status:
continue
filter_fields.append(field)
if get_request().form:
field.enabled = 'filter-%s' % field.id in get_request().form
else:
# enable status filter by default
field.enabled = (field.id in ('status',))
r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % (
_('Filters'), _('change')))
for filter_field in filter_fields:
if not filter_field.enabled:
continue
filter_field_key = 'filter-%s-value' % filter_field.id
filter_field_value = get_request().form.get(filter_field_key)
if filter_field.type == 'status':
r += htmltext('<div class="widget">')
r += htmltext('<div class="title">%s</div>') % _('Status to display')
r += htmltext('<div class="content">')
r += htmltext('<select name="filter">')
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('<option value="%s"%s %s>' % (filter_id, selected, style))
r += htmltext('%s</option>') % filter_label
r += htmltext('</select>')
r += htmltext('</div>')
r += htmltext('</div>')
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('<div style="display: none;">')
r += htmltext('<ul id="field-filter">')
for field in filter_fields:
r += htmltext('<li><input type="checkbox" name="filter-%s"') % field.id
if field.enabled:
r += htmltext(' checked="checked"')
r += htmltext(' id="fields-filter-%s"') % field.id
r += htmltext('/>')
r += htmltext('<label for="fields-filter-%s">%s</label>') % (
field.id, misc.ellipsize(field.label, 70))
r += htmltext('</li>')
r += htmltext('</ul>')
r += htmltext('</div>')
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('<form id="listing-settings">')
r += htmltext('<form id="listing-settings" action=".">')
if offset or limit:
if not offset:
offset = 0
@ -553,7 +641,6 @@ class FormPage(Directory):
order_by = ''
r += htmltext('<input type="hidden" name="order_by" value="%s"/>') % order_by
waitpoint_status = self.formdef.workflow.get_waitpoint_status()
if get_publisher().is_using_postgresql():
r += htmltext('<h3>%s</h3>') % _('Search')
if get_request().form.get('q'):
@ -565,31 +652,12 @@ class FormPage(Directory):
r += htmltext('<input name="q">')
r += htmltext('<input type="submit" value="%s"/>') % _('Search')
if waitpoint_status:
r += htmltext('<h3>%s</h3>') % _('Status to display')
r += htmltext('<select name="filter">')
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('<option value="%s"%s %s>' % (filter_id, selected, style))
r += htmltext('%s</option>') % filter_label
r += htmltext('</select>')
r += self.get_filter_sidebar(selected_filter=selected_filter)
r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
r += htmltext('<button id="columns-settings">%s</button>') % _('Columns Settings')
# column settings dialog content
r += htmltext('<div style="display: none;">')
r += htmltext('<ul id="columns-filter">')
for field in self.get_formdef_fields():
@ -598,12 +666,14 @@ class FormPage(Directory):
r += htmltext('<li><input type="checkbox" name="%s"') % field.id
if field.id in [x.id for x in fields]:
r += htmltext(' checked="checked"')
r += htmltext(' id="fields-filter-%s"') % field.id
r += htmltext(' id="fields-column-%s"') % field.id
r += htmltext('/>')
r += htmltext('<label for="fields-filter-%s">%s</label>') % (field.id, misc.ellipsize(field.label, 70))
r += htmltext('<label for="fields-column-%s">%s</label>') % (
field.id, misc.ellipsize(field.label, 70))
r += htmltext('</li>')
r += htmltext('</ul>')
r += htmltext('</div>') # id="columns-settings"
r += htmltext('</div>')
r += htmltext('</form>')
return r.getvalue()
@ -641,11 +711,45 @@ class FormPage(Directory):
return default
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

View File

@ -850,22 +850,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:

View File

@ -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__

View File

@ -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

View File

@ -696,11 +696,14 @@ div.SubmitWidget input:hover, input[type=submit]:hover {
border-color: #666;
}
ul#field-filter,
ul#columns-filter {
list-style: none;
padding-left: 0;
margin-left: 0;
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
}
ul.multipage li {
@ -887,3 +890,7 @@ fieldset.form-plus.closed legend:after {
width: auto;
}
}
a#filter-settings {
cursor: pointer;
}

View File

@ -85,6 +85,8 @@ function refresh_table() {
}
$(function() {
var must_reload_page = false;
/* column settings */
$('#columns-settings').click(function() {
var dialog = $('<form>');
@ -113,6 +115,37 @@ $(function() {
}]);
return false;
});
/* filter settings */
$('#filter-settings').click(function() {
var dialog = $('<form>');
$('#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($('<span id="sidebar-toggle">&#8286;</span>'));
$('#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;