0001-backoffice-filter-listings-by-time-period-and-closed.patch
tests/test_backoffice_pages.py | ||
---|---|---|
72 | 72 |
formdata.data = {'1': 'FOO BAR %d' % i} |
73 | 73 |
if i%4 == 0: |
74 | 74 |
formdata.data['2'] = 'foo' |
75 |
formdata.data['2_display'] = 'foo' |
|
75 | 76 |
elif i%4 == 1: |
76 | 77 |
formdata.data['2'] = 'bar' |
78 |
formdata.data['2_display'] = 'bar' |
|
77 | 79 |
else: |
78 | 80 |
formdata.data['2'] = 'baz' |
81 |
formdata.data['2_display'] = 'baz' |
|
79 | 82 |
if i%3 == 0: |
80 | 83 |
formdata.jump_status('new') |
81 | 84 |
else: |
... | ... | |
167 | 170 |
assert resp.body.count('data-link') == 17 # 17 rows |
168 | 171 |
assert resp.body.count('FOO BAR') == 0 # no field 1 column |
169 | 172 | |
173 |
def test_backoffice_filter(pub): |
|
174 |
create_superuser(pub) |
|
175 |
create_environment() |
|
176 |
app = login(get_app(pub)) |
|
177 |
resp = app.get('/backoffice/form-title/') |
|
178 |
assert resp.forms[0]['filter-status'].checked == True |
|
179 |
resp.forms[0]['filter-status'].checked = False |
|
180 |
resp.forms[0]['filter-2'].checked = True |
|
181 |
resp = resp.forms[0].submit() |
|
182 |
assert '<select name="filter">' not in resp.body |
|
183 | ||
184 |
resp.forms[0]['filter-2-value'] = 'baz' |
|
185 |
resp = resp.forms[0].submit() |
|
186 |
assert resp.body.count('<td>baz</td>') == 8 |
|
187 |
assert resp.body.count('<td>foo</td>') == 0 |
|
188 |
assert resp.body.count('<td>bar</td>') == 0 |
|
189 | ||
190 |
resp.forms[0]['filter-start'].checked = True |
|
191 |
resp = resp.forms[0].submit() |
|
192 |
resp.forms[0]['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d') |
|
193 |
resp = resp.forms[0].submit() |
|
194 |
assert resp.body.count('<td>baz</td>') == 0 |
|
195 |
resp.forms[0]['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d') |
|
196 |
resp = resp.forms[0].submit() |
|
197 |
assert resp.body.count('<td>baz</td>') == 8 |
|
198 | ||
170 | 199 |
def test_backoffice_csv(pub): |
171 | 200 |
create_superuser(pub) |
172 | 201 |
create_environment() |
wcs/backoffice/root.py | ||
---|---|---|
44 | 44 | |
45 | 45 |
import wcs.admin.forms |
46 | 46 |
import wcs.admin.workflows |
47 |
from wcs import data_sources |
|
47 | 48 | |
48 | 49 |
from wcs.api import get_user_from_api_query_string |
49 | 50 | |
... | ... | |
534 | 535 |
r += htmltext('</ul>') |
535 | 536 |
return r.getvalue() |
536 | 537 | |
538 |
def get_filter_sidebar(self, selected_filter=None): |
|
539 |
r = TemplateIO(html=True) |
|
540 | ||
541 |
waitpoint_status = self.formdef.workflow.get_waitpoint_status() |
|
542 |
period_fake_fields = [ |
|
543 |
FakeField('start', 'period-date', _('Start')), |
|
544 |
FakeField('end', 'period-date', _('End')), |
|
545 |
] |
|
546 |
filter_fields = [] |
|
547 |
for field in period_fake_fields + self.get_formdef_fields(): |
|
548 |
field.enabled = False |
|
549 |
if field.type not in ('item', 'period-date', 'status'): |
|
550 |
continue |
|
551 |
if field.type == 'status' and not waitpoint_status: |
|
552 |
continue |
|
553 |
filter_fields.append(field) |
|
554 | ||
555 |
if get_request().form: |
|
556 |
field.enabled = 'filter-%s' % field.id in get_request().form |
|
557 |
else: |
|
558 |
# enable status filter by default |
|
559 |
field.enabled = (field.id in ('status',)) |
|
560 | ||
561 |
r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % ( |
|
562 |
_('Filters'), _('change'))) |
|
563 | ||
564 |
for filter_field in filter_fields: |
|
565 |
if not filter_field.enabled: |
|
566 |
continue |
|
567 | ||
568 |
filter_field_key = 'filter-%s-value' % filter_field.id |
|
569 |
filter_field_value = get_request().form.get(filter_field_key) |
|
570 | ||
571 |
if filter_field.type == 'status': |
|
572 |
r += htmltext('<div class="widget">') |
|
573 |
r += htmltext('<div class="title">%s</div>') % _('Status to display') |
|
574 |
r += htmltext('<div class="content">') |
|
575 |
r += htmltext('<select name="filter">') |
|
576 |
filters = [('all', _('All'), None), |
|
577 |
('pending', _('Pending'), None), |
|
578 |
('done', _('Done'), None)] |
|
579 |
for status in waitpoint_status: |
|
580 |
filters.append((status.id, status.name, status.colour)) |
|
581 |
for filter_id, filter_label, filter_colour in filters: |
|
582 |
if filter_id == selected_filter: |
|
583 |
selected = ' selected="selected"' |
|
584 |
else: |
|
585 |
selected = '' |
|
586 |
style = '' |
|
587 |
if filter_colour and filter_colour != 'FFFFFF': |
|
588 |
fg_colour = misc.get_foreground_colour(filter_colour) |
|
589 |
style = 'style="background: #%s; color: %s;"' % ( |
|
590 |
filter_colour, fg_colour) |
|
591 |
r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style)) |
|
592 |
r += htmltext('%s</option>') % filter_label |
|
593 |
r += htmltext('</select>') |
|
594 |
r += htmltext('</div>') |
|
595 |
r += htmltext('</div>') |
|
596 | ||
597 |
elif filter_field.type == 'period-date': |
|
598 |
r += DateWidget(filter_field_key, title=filter_field.label, |
|
599 |
value=filter_field_value, render_br=False).render() |
|
600 | ||
601 |
elif filter_field.type == 'item': |
|
602 |
filter_field.required = False |
|
603 |
options = filter_field.get_options() |
|
604 |
r += SingleSelectWidget(filter_field_key, title=filter_field.label, |
|
605 |
options=options, value=filter_field_value, |
|
606 |
render_br=False).render() |
|
607 | ||
608 |
# field filter dialog content |
|
609 |
r += htmltext('<div style="display: none;">') |
|
610 |
r += htmltext('<ul id="field-filter">') |
|
611 |
for field in filter_fields: |
|
612 |
r += htmltext('<li><input type="checkbox" name="filter-%s"') % field.id |
|
613 |
if field.enabled: |
|
614 |
r += htmltext(' checked="checked"') |
|
615 |
r += htmltext(' id="fields-filter-%s"') % field.id |
|
616 |
r += htmltext('/>') |
|
617 |
r += htmltext('<label for="fields-filter-%s">%s</label>') % ( |
|
618 |
field.id, misc.ellipsize(field.label, 70)) |
|
619 |
r += htmltext('</li>') |
|
620 |
r += htmltext('</ul>') |
|
621 |
r += htmltext('</div>') |
|
622 | ||
623 |
return r.getvalue() |
|
624 | ||
537 | 625 |
def get_fields_sidebar(self, selected_filter, fields, offset=None, |
538 | 626 |
limit=None, order_by=None): |
539 | 627 |
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js']) |
540 | 628 |
get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css') |
541 | 629 | |
542 | 630 |
r = TemplateIO(html=True) |
543 |
r += htmltext('<form id="listing-settings">') |
|
631 |
r += htmltext('<form id="listing-settings" action=".">')
|
|
544 | 632 |
if offset or limit: |
545 | 633 |
if not offset: |
546 | 634 |
offset = 0 |
... | ... | |
553 | 641 |
order_by = '' |
554 | 642 |
r += htmltext('<input type="hidden" name="order_by" value="%s"/>') % order_by |
555 | 643 | |
556 |
waitpoint_status = self.formdef.workflow.get_waitpoint_status() |
|
557 | 644 |
if get_publisher().is_using_postgresql(): |
558 | 645 |
r += htmltext('<h3>%s</h3>') % _('Search') |
559 | 646 |
if get_request().form.get('q'): |
... | ... | |
565 | 652 |
r += htmltext('<input name="q">') |
566 | 653 |
r += htmltext('<input type="submit" value="%s"/>') % _('Search') |
567 | 654 | |
568 |
if waitpoint_status: |
|
569 |
r += htmltext('<h3>%s</h3>') % _('Status to display') |
|
570 |
r += htmltext('<select name="filter">') |
|
571 |
filters = [('all', _('All'), None), |
|
572 |
('pending', _('Pending'), None), |
|
573 |
('done', _('Done'), None)] |
|
574 |
for status in waitpoint_status: |
|
575 |
filters.append((status.id, status.name, status.colour)) |
|
576 |
for filter_id, filter_label, filter_colour in filters: |
|
577 |
if filter_id == selected_filter: |
|
578 |
selected = ' selected="selected"' |
|
579 |
else: |
|
580 |
selected = '' |
|
581 |
style = '' |
|
582 |
if filter_colour and filter_colour != 'FFFFFF': |
|
583 |
fg_colour = misc.get_foreground_colour(filter_colour) |
|
584 |
style = 'style="background: #%s; color: %s;"' % ( |
|
585 |
filter_colour, fg_colour) |
|
586 |
r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style)) |
|
587 |
r += htmltext('%s</option>') % filter_label |
|
588 |
r += htmltext('</select>') |
|
655 |
r += self.get_filter_sidebar(selected_filter=selected_filter) |
|
589 | 656 | |
590 | 657 |
r += htmltext('<button class="refresh">%s</button>') % _('Refresh') |
591 | ||
592 | 658 |
r += htmltext('<button id="columns-settings">%s</button>') % _('Columns Settings') |
659 | ||
660 |
# column settings dialog content |
|
593 | 661 |
r += htmltext('<div style="display: none;">') |
594 | 662 |
r += htmltext('<ul id="columns-filter">') |
595 | 663 |
for field in self.get_formdef_fields(): |
... | ... | |
598 | 666 |
r += htmltext('<li><input type="checkbox" name="%s"') % field.id |
599 | 667 |
if field.id in [x.id for x in fields]: |
600 | 668 |
r += htmltext(' checked="checked"') |
601 |
r += htmltext(' id="fields-filter-%s"') % field.id
|
|
669 |
r += htmltext(' id="fields-column-%s"') % field.id
|
|
602 | 670 |
r += htmltext('/>') |
603 |
r += htmltext('<label for="fields-filter-%s">%s</label>') % (field.id, misc.ellipsize(field.label, 70)) |
|
671 |
r += htmltext('<label for="fields-column-%s">%s</label>') % ( |
|
672 |
field.id, misc.ellipsize(field.label, 70)) |
|
604 | 673 |
r += htmltext('</li>') |
605 | 674 |
r += htmltext('</ul>') |
606 |
r += htmltext('</div>') # id="columns-settings" |
|
675 |
r += htmltext('</div>') |
|
676 | ||
607 | 677 |
r += htmltext('</form>') |
608 | 678 |
return r.getvalue() |
609 | 679 | |
... | ... | |
641 | 711 |
return 'pending' |
642 | 712 |
return 'all' |
643 | 713 | |
714 |
def get_criterias_from_query(self): |
|
715 |
period_fake_fields = [ |
|
716 |
FakeField('start', 'period-date', _('Start')), |
|
717 |
FakeField('end', 'period-date', _('End')), |
|
718 |
] |
|
719 |
filter_fields = [] |
|
720 |
criterias = [] |
|
721 |
format_string = misc.date_format() |
|
722 |
for filter_field in period_fake_fields + self.get_formdef_fields(): |
|
723 |
if filter_field.type not in ('item', 'period-date'): |
|
724 |
continue |
|
725 | ||
726 |
if not get_request().form.get('filter-%s' % filter_field.id): |
|
727 |
# the field is not enabled |
|
728 |
continue |
|
729 | ||
730 |
filter_field_key = 'filter-%s-value' % filter_field.id |
|
731 |
filter_field_value = get_request().form.get(filter_field_key) |
|
732 |
if not filter_field_value: |
|
733 |
continue |
|
734 | ||
735 |
if filter_field.id == 'start': |
|
736 |
period_start = time.strptime(filter_field_value, format_string) |
|
737 |
criterias.append(GreaterOrEqual('receipt_time', period_start)) |
|
738 |
elif filter_field.id == 'end': |
|
739 |
period_end = time.strptime(filter_field_value, format_string) |
|
740 |
criterias.append(LessOrEqual('receipt_time', period_end)) |
|
741 |
elif filter_field.type == 'item' and filter_field_value not in (None, 'None'): |
|
742 |
criterias.append(Equal('f%s' % filter_field.id, filter_field_value)) |
|
743 | ||
744 |
return criterias |
|
745 | ||
746 | ||
644 | 747 |
def _q_index(self): |
645 | 748 |
get_logger().info('backoffice - form %s - listing' % self.formdef.name) |
646 | 749 | |
647 | 750 |
fields = self.get_fields_from_query() |
648 | 751 |
selected_filter = self.get_filter_from_query() |
752 |
criterias = self.get_criterias_from_query() |
|
649 | 753 | |
650 | 754 |
if get_publisher().is_using_postgresql(): |
651 | 755 |
# only enable pagination in SQL mode, as we do not have sorting in |
... | ... | |
664 | 768 |
table = FormDefUI(self.formdef).listing(fields=fields, |
665 | 769 |
selected_filter=selected_filter, include_form=True, |
666 | 770 |
limit=int(limit), offset=int(offset), query=query, |
667 |
order_by=order_by) |
|
771 |
order_by=order_by, criterias=criterias)
|
|
668 | 772 | |
669 | 773 |
if get_request().form.get('ajax') == 'true': |
670 | 774 |
get_response().filter = None |
wcs/fields.py | ||
---|---|---|
844 | 844 |
self.items = [] |
845 | 845 |
WidgetField.__init__(self, **kwargs) |
846 | 846 | |
847 |
def get_options(self): |
|
848 |
if not self.data_source: |
|
849 |
return self.items |
|
850 | ||
851 |
options = data_sources.get_items(self.data_source) |
|
852 |
if options and not self.required: |
|
853 |
if type(options[0]) is str: |
|
854 |
options[:0] = [None] |
|
855 |
elif len(options) == 2: |
|
856 |
options[:0] = [(None, '---')] |
|
857 |
elif len(options[0]) == 3: |
|
858 |
options[:0] = [(None, '---', None)] |
|
859 |
return options |
|
860 | ||
847 | 861 |
def perform_more_widget_changes(self, form, kwargs, edit = True): |
848 |
if self.data_source: |
|
849 |
if self.data_source.get('type') == 'jsonp': |
|
850 |
kwargs['url'] = self.data_source.get('value') |
|
851 |
self.widget_class = JsonpSingleSelectWidget |
|
852 |
else: |
|
853 |
kwargs['options'] = data_sources.get_items(self.data_source) |
|
854 |
if kwargs['options'] and not self.required: |
|
855 |
if type(kwargs['options'][0]) is str: |
|
856 |
kwargs['options'][:0] = [None] |
|
857 |
elif len(kwargs['options'][0]) == 2: |
|
858 |
kwargs['options'][:0] = [(None, '---')] |
|
859 |
elif len(kwargs['options'][0]) == 3: |
|
860 |
kwargs['options'][:0] = [(None, '---', None)] |
|
861 |
elif self.items: |
|
862 |
kwargs['options'] = self.items |
|
862 |
if self.data_source and self.data_source.get('type') == 'jsonp': |
|
863 |
kwargs['url'] = self.data_source.get('value') |
|
864 |
self.widget_class = JsonpSingleSelectWidget |
|
865 |
else: |
|
866 |
kwargs['options'] = self.get_options() |
|
863 | 867 |
if not kwargs.get('options'): |
864 | 868 |
kwargs['options'] = [(None, '---')] |
865 | 869 |
if self.show_as_radio: |
wcs/formdata.py | ||
---|---|---|
549 | 549 |
field.feed_session(self.data.get(field.id), |
550 | 550 |
self.data.get('%s_display' % field.id)) |
551 | 551 | |
552 |
def __getattr__(self, attr): |
|
553 |
try: |
|
554 |
return self.__dict__[attr] |
|
555 |
except KeyError: |
|
556 |
# give direct access to values from the data dictionary |
|
557 |
if attr[0] == 'f': |
|
558 |
return self.__dict__['data'][attr[1:]] |
|
559 |
raise AttributeError(attr) |
|
560 | ||
552 | 561 |
# don't pickle _formdef cache |
553 | 562 |
def __getstate__(self): |
554 | 563 |
odict = self.__dict__ |
wcs/forms/backoffice.py | ||
---|---|---|
27 | 27 | |
28 | 28 |
def listing(self, fields, selected_filter='all', url_action=None, |
29 | 29 |
include_form=False, items=None, offset=0, limit=0, |
30 |
query=None, order_by=None): |
|
30 |
query=None, order_by=None, criterias=None):
|
|
31 | 31 | |
32 | 32 |
partial_display = False |
33 | 33 | |
... | ... | |
35 | 35 |
if offset and not limit: |
36 | 36 |
limit = 20 |
37 | 37 |
items, total_count = self.get_listing_items( |
38 |
selected_filter, offset, limit, query, order_by) |
|
38 |
selected_filter, offset, limit, query, order_by, |
|
39 |
criterias=criterias) |
|
39 | 40 |
if (offset > 0) or (total_count > limit > 0): |
40 | 41 |
partial_display = True |
41 | 42 | |
... | ... | |
130 | 131 |
return r.getvalue() |
131 | 132 | |
132 | 133 |
def get_listing_items(self, selected_filter='all', offset=None, |
133 |
limit=None, query=None, order_by=None, user=None): |
|
134 |
limit=None, query=None, order_by=None, user=None, criterias=None):
|
|
134 | 135 |
formdata_class = self.formdef.data_class() |
135 | 136 |
if selected_filter == 'all': |
136 | 137 |
item_ids = [int(x) for x in formdata_class.keys()] |
... | ... | |
153 | 154 |
query_ids = formdata_class.get_ids_from_query(query) |
154 | 155 |
item_ids = list(set(item_ids).intersection(query_ids)) |
155 | 156 | |
157 |
if criterias: |
|
158 |
select_ids = [x.id for x in formdata_class.select(clause=criterias)] |
|
159 |
item_ids = list(set(item_ids).intersection(select_ids)) |
|
160 | ||
156 | 161 |
if self.formdef.acl_read != 'all' and item_ids: |
157 | 162 |
# if the formdef has some ACL defined, we don't go the full way of |
158 | 163 |
# supporting all the cases but assume that as we are in the |
wcs/qommon/static/css/dc2/admin.css | ||
---|---|---|
887 | 887 |
width: auto; |
888 | 888 |
} |
889 | 889 |
} |
890 | ||
891 |
a#filter-settings { |
|
892 |
cursor: pointer; |
|
893 |
} |
wcs/qommon/static/js/wcs.listing.js | ||
---|---|---|
85 | 85 |
} |
86 | 86 | |
87 | 87 |
$(function() { |
88 |
var must_reload_page = false; |
|
89 | ||
88 | 90 |
/* column settings */ |
89 | 91 |
$('#columns-settings').click(function() { |
90 | 92 |
var dialog = $('<form>'); |
... | ... | |
113 | 115 |
}]); |
114 | 116 |
return false; |
115 | 117 |
}); |
118 | ||
119 |
/* filter settings */ |
|
120 |
$('#filter-settings').click(function() { |
|
121 |
var dialog = $('<form>'); |
|
122 |
$('#field-filter').clone().appendTo(dialog); |
|
123 |
$(dialog).find('input').each(function(idx, elem) { |
|
124 |
$(this).attr('id', 'dlg-' + $(this).attr('id')); |
|
125 |
}); |
|
126 |
$(dialog).find('label').each(function(idx, elem) { |
|
127 |
$(this).attr('for', 'dlg-' + $(this).attr('for')); |
|
128 |
}); |
|
129 |
$(dialog).dialog({ |
|
130 |
modal: true, |
|
131 |
resizable: false, |
|
132 |
title: $('#filter-settings').parents('h3').find('span:first-child').text(), |
|
133 |
width: '30em'}); |
|
134 |
$(dialog).dialog('option', 'buttons', [ |
|
135 |
{text: $('form#listing-settings button.refresh').text(), |
|
136 |
click: function() { |
|
137 |
$(this).find('input[type="checkbox"]').each(function(idx, elem) { |
|
138 |
$('form#listing-settings input[name="' + $(elem).attr('name') + '"]').prop('checked', |
|
139 |
$(elem).prop('checked')); |
|
140 |
}); |
|
141 |
$(this).dialog('close'); |
|
142 |
must_reload_page = true; |
|
143 |
$('form#listing-settings').submit(); |
|
144 |
} |
|
145 |
}]); |
|
146 |
return false; |
|
147 |
}); |
|
148 | ||
116 | 149 |
/* possibility to toggle the sidebar */ |
117 | 150 |
$('#main-content').after($('<span id="sidebar-toggle">⁞</span>')); |
118 | 151 |
$('#sidebar-toggle').click(function() { |
... | ... | |
124 | 157 |
$('#main-content').animate({width: '95%'}); |
125 | 158 |
} |
126 | 159 |
}); |
127 |
/* automatically refresh on status change */
|
|
128 |
$('form#listing-settings select[name="filter"]').change(function() {
|
|
160 |
/* automatically refresh on filter change */
|
|
161 |
$('form#listing-settings select').change(function() { |
|
129 | 162 |
$('form#listing-settings').submit(); |
130 | 163 |
}); |
131 | 164 |
/* partial table refresh */ |
132 | 165 |
$('form#listing-settings').submit(function(event) { |
166 |
if (must_reload_page) { |
|
167 |
return true; |
|
168 |
} |
|
133 | 169 |
event.preventDefault(); |
134 | 170 |
refresh_table(); |
135 | 171 |
return false; |
136 |
- |