backoffice: filter listings by time period and closed list values (#4505)
This commit is contained in:
parent
128978d782
commit
bfc0d859db
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">⁞</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;
|
||||
|
|
Loading…
Reference in New Issue