Projet

Général

Profil

0001-backoffice-filter-listings-by-time-period-and-closed.patch

Frédéric Péters, 01 avril 2015 20:26

Télécharger (20,2 ko)

Voir les différences:

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(-)
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">&#8286;</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
-