Project

General

Profile

0001-backoffice-move-management-of-submitted-forms-to-a-s.patch

Thomas Noël, 07 May 2015 04:33 PM

Download (111 KB)

View differences:

Subject: [PATCH 1/2] backoffice: move management of submitted forms to a
 subdirectory (#7151)

 tests/test_backoffice_pages.py |   24 +-
 tests/test_formdata.py         |    2 +-
 wcs/api.py                     |    2 +-
 wcs/backoffice/management.py   | 1244 ++++++++++++++++++++++++++++++++++++++++
 wcs/backoffice/root.py         | 1223 +--------------------------------------
 wcs/formdef.py                 |    2 +-
 6 files changed, 1272 insertions(+), 1225 deletions(-)
 create mode 100644 wcs/backoffice/management.py
tests/test_backoffice_pages.py
163 163
    create_superuser(pub)
164 164
    create_environment()
165 165
    app = login(get_app(pub))
166
    resp = app.get('/backoffice/form-title/')
166
    resp = app.get('/backoffice/management/form-title/')
167 167
    assert resp.body.count('data-link') == 17
168 168

  
169 169
    # check status filter <select>
170
    resp = app.get('/backoffice/form-title/')
170
    resp = app.get('/backoffice/management/form-title/')
171 171
    resp.forms[0]['filter'] = 'all'
172 172
    resp = resp.forms[0].submit()
173 173
    if getattr(pub, 'pgconn', None):
......
177 177
        assert resp.body.count('data-link') == 50
178 178

  
179 179
    # check status filter <select>
180
    resp = app.get('/backoffice/form-title/')
180
    resp = app.get('/backoffice/management/form-title/')
181 181
    resp.forms[0]['filter'] = 'done'
182 182
    resp = resp.forms[0].submit()
183 183
    if getattr(pub, 'pgconn', None):
......
191 191
    create_superuser(pub)
192 192
    create_environment()
193 193
    app = login(get_app(pub))
194
    resp = app.get('/backoffice/form-title/')
194
    resp = app.get('/backoffice/management/form-title/')
195 195
    assert resp.body.count('</th>') == 6 # five columns
196 196
    resp.forms[0]['1'].checked = False
197 197
    resp = resp.forms[0].submit()
......
203 203
    create_superuser(pub)
204 204
    create_environment()
205 205
    app = login(get_app(pub))
206
    resp = app.get('/backoffice/form-title/')
206
    resp = app.get('/backoffice/management/form-title/')
207 207
    assert resp.forms[0]['filter-status'].checked == True
208 208
    resp.forms[0]['filter-status'].checked = False
209 209
    resp.forms[0]['filter-2'].checked = True
......
229 229
    create_superuser(pub)
230 230
    create_environment()
231 231
    app = login(get_app(pub))
232
    resp = app.get('/backoffice/form-title/')
232
    resp = app.get('/backoffice/management/form-title/')
233 233
    resp = resp.click('CSV Export')
234 234
    assert resp.headers['content-type'].startswith('text/')
235 235
    assert len(resp.body.splitlines()) == 18 # 17 + header line
236 236

  
237
    resp = app.get('/backoffice/form-title/')
237
    resp = app.get('/backoffice/management/form-title/')
238 238
    resp.forms[0]['filter'] = 'all'
239 239
    resp = resp.forms[0].submit()
240 240
    resp = resp.click('CSV Export')
......
244 244
    create_superuser(pub)
245 245
    create_environment()
246 246
    app = login(get_app(pub))
247
    resp = app.get('/backoffice/form-title/')
247
    resp = app.get('/backoffice/management/form-title/')
248 248
    resp = resp.click('Open Document Format Export')
249 249
    assert resp.headers['content-type'] == 'application/vnd.oasis.opendocument.spreadsheet'
250 250
    assert 'filename=form-title.ods' in resp.headers['content-disposition']
......
254 254
    create_superuser(pub)
255 255
    create_environment()
256 256
    app = login(get_app(pub))
257
    resp = app.get('/backoffice/form-title/')
257
    resp = app.get('/backoffice/management/form-title/')
258 258
    resp = resp.click('Statistics')
259 259
    assert 'Total number of records: 50' in resp.body
260 260
    assert 'New: 17' in resp.body
......
272 272
    create_superuser(pub)
273 273
    create_environment()
274 274
    app = login(get_app(pub))
275
    resp = app.get('/backoffice/form-title/')
275
    resp = app.get('/backoffice/management/form-title/')
276 276
    resp = resp.click('Statistics')
277 277
    assert 'filter' not in resp.forms[0].fields # status is not displayed by default
278 278

  
......
302 302
    create_superuser(pub)
303 303
    create_environment()
304 304
    app = login(get_app(pub))
305
    resp = app.get('/backoffice/form-title/')
305
    resp = app.get('/backoffice/management/form-title/')
306 306
    resp = resp.click('Statistics')
307 307

  
308 308
    resp.forms[0]['filter-2'].checked = True
......
325 325
    form_class = FormDef.get_by_urlname('form-title').data_class()
326 326
    number31 = [x for x in form_class.select() if x.data['1'] == 'FOO BAR 30'][0].id
327 327
    app = login(get_app(pub))
328
    resp = app.get('/backoffice/form-title/')
328
    resp = app.get('/backoffice/management/form-title/')
329 329
    resp = resp.click(href='%s/' % number31)
330 330
    assert (' with the number %s.' % number31) in resp.body
331 331
    resp.forms[0]['comment'] = 'HELLO WORLD'
tests/test_formdata.py
56 56
    assert substvars.get('form_number') == '1'
57 57
    assert substvars.get('form_number_raw') == '1'
58 58
    assert substvars.get('form_url').endswith('/foobar/1/')
59
    assert substvars.get('form_url_backoffice').endswith('/backoffice/foobar/1/')
59
    assert substvars.get('form_url_backoffice').endswith('/backoffice/management/foobar/1/')
60 60
    assert substvars.get('form_status_url').endswith('/foobar/1/status')
61 61

  
62 62
def test_display_id(pub):
wcs/api.py
120 120

  
121 121
# import backoffice.root.FormPage after get_user_from_api_query_string
122 122
# to avoid circular dependencies
123
from backoffice.root import FormPage as BackofficeFormPage
123
from backoffice.management import FormPage as BackofficeFormPage
124 124

  
125 125

  
126 126
class ApiFormPage(BackofficeFormPage):
wcs/backoffice/management.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2015  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import csv
18
import cStringIO
19
import datetime
20
import json
21
import time
22

  
23
try:
24
    import xlwt
25
except ImportError:
26
    xlwt = None
27

  
28
from quixote import get_session, get_publisher, get_request, get_response, redirect
29
from quixote.directory import Directory
30
from quixote.html import TemplateIO, htmltext
31

  
32
from qommon.backoffice.menu import html_top
33
from qommon import misc, get_logger
34
from qommon.afterjobs import AfterJob
35
from qommon import errors
36
from qommon import ods
37
from qommon.form import *
38
from qommon.storage import Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or
39

  
40
from wcs.forms.backoffice import FormDefUI
41
from wcs.forms.common import FormStatusPage
42

  
43
from wcs.categories import Category
44
from wcs.formdef import FormDef
45

  
46

  
47
class ManagementDirectory(Directory):
48
    _q_exports = ['', 'statistics']
49

  
50
    def _q_traverse(self, path):
51
        get_response().breadcrumb.append(('management/', _('Management')))
52
        return super(ManagementDirectory, self)._q_traverse(path)
53

  
54
    def _q_index(self):
55
        html_top('management', _('Management'))
56
        get_response().filter['sidebar'] = self.get_sidebar()
57
        r = TemplateIO(html=True)
58

  
59
        user = get_request().user
60

  
61
        forms_without_pending_stuff = []
62
        forms_with_pending_stuff = []
63

  
64
        def append_form_entry(formdef):
65
            formdef_data_class = formdef.data_class()
66
            count_forms = formdef_data_class.count() - len(formdef_data_class.get_ids_with_indexed_value('status', 'draft'))
67
            not_endpoint_status = formdef.workflow.get_not_endpoint_status()
68
            not_endpoint_status_ids = ['wf-%s' % x.id for x in not_endpoint_status]
69
            pending_forms = []
70
            for status in not_endpoint_status_ids:
71
                pending_forms.extend(formdef_data_class.get_ids_with_indexed_value(
72
                                        'status', status))
73

  
74
            if formdef.acl_read != 'all' and pending_forms:
75
                concerned_ids = set()
76
                formdata_class = formdef.data_class()
77
                user_roles = set(user.roles or [])
78
                for role in user_roles:
79
                    concerned_ids |= set(formdata_class.get_ids_with_indexed_value(
80
                        'concerned_roles', str(role)))
81
                pending_forms = set(pending_forms).intersection(concerned_ids)
82

  
83
            if len(pending_forms) == 0:
84
                forms_without_pending_stuff.append((formdef, len(pending_forms), count_forms))
85
            else:
86
                forms_with_pending_stuff.append((formdef, len(pending_forms), count_forms))
87

  
88
        if user:
89
            for formdef in FormDef.select(order_by='name', ignore_errors=True):
90
                if formdef.disabled:
91
                    continue
92
                if user.is_admin or formdef.is_of_concern_for_user(user):
93
                    append_form_entry(formdef)
94

  
95
        if forms_with_pending_stuff:
96
            r += htmltext('<div class="bo-block" id="forms-in-your-care">')
97
            r += htmltext('<h2>%s</h2>') % _('Forms in your care')
98
            r += self.display_forms(forms_with_pending_stuff)
99
            r += htmltext('</div>')
100

  
101
        if forms_without_pending_stuff:
102
            r += htmltext('<div class="bo-block" id="other-forms">')
103
            r += htmltext('<h2>%s</h2>') % _('Other Forms')
104
            r += self.display_forms(forms_without_pending_stuff)
105
            r += htmltext('</div>')
106

  
107
        return r.getvalue()
108

  
109
    def get_sidebar(self):
110
        r = TemplateIO(html=True)
111
        r += htmltext('<div class="bo-block">')
112
        r += htmltext('<ul id="sidebar-actions">')
113
        r += htmltext('<li><a href="statistics">%s</a></li>') % _('Global statistics')
114
        r += htmltext('</ul>')
115
        r += htmltext('</div>')
116
        return r.getvalue()
117

  
118
    def get_stats_sidebar(self):
119
        get_response().add_javascript(['jquery.js'])
120
        DateWidget.prepare_javascript()
121
        form = Form(use_tokens=False)
122
        form.add(DateWidget, 'start', title=_('Start Date'))
123
        form.add(DateWidget, 'end', title=_('End Date'))
124
        form.add_submit('submit', _('Submit'))
125

  
126
        r = TemplateIO(html=True)
127
        r += htmltext('<h3>%s</h3>') % _('Period')
128
        r += form.render()
129

  
130
        r += htmltext('<h3>%s</h3>') % _('Shortcuts')
131
        r += htmltext('<ul>') # presets
132
        current_month_start = datetime.datetime.now().replace(day=1)
133
        start = current_month_start.strftime(misc.date_format())
134
        r += htmltext(' <li><a href="?start=%s">%s</a>') % (start, _('Current Month'))
135
        previous_month_start = current_month_start - datetime.timedelta(days=2)
136
        previous_month_start = previous_month_start.replace(day=1)
137
        start = previous_month_start.strftime(misc.date_format())
138
        end = current_month_start.strftime(misc.date_format())
139
        r += htmltext(' <li><a href="?start=%s&end=%s">%s</a>') % (
140
                start, end, _('Previous Month'))
141

  
142
        current_year_start = datetime.datetime.now().replace(month=1, day=1)
143
        start = current_year_start.strftime(misc.date_format())
144
        r += htmltext(' <li><a href="?start=%s">%s</a>') % (start, _('Current Year'))
145
        previous_year_start = current_year_start.replace(year=current_year_start.year-1)
146
        start = previous_year_start.strftime(misc.date_format())
147
        end = current_year_start.strftime(misc.date_format())
148
        r += htmltext(' <li><a href="?start=%s&end=%s">%s</a>') % (
149
                start, end, _('Previous Year'))
150

  
151
        return r.getvalue()
152

  
153
    def statistics(self):
154
        html_top('management', _('Global statistics'))
155
        get_response().breadcrumb.append(('statistics', _('Global statistics')))
156
        get_response().filter['sidebar'] = self.get_stats_sidebar()
157
        r = TemplateIO(html=True)
158
        r += htmltext('<h2>%s</h2>') % _('Global statistics')
159

  
160
        formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
161
                order_by='name', ignore_errors=True)
162

  
163
        counts = {}
164
        parsed_values = {}
165
        criterias = get_stats_criteria(get_request(), parsed_values)
166
        for formdef in formdefs:
167
            values = formdef.data_class().select(criterias)
168
            counts[formdef.id] = len(values)
169

  
170
        do_graphs = False
171
        if get_publisher().is_using_postgresql() and \
172
                get_publisher().get_site_option('postgresql_views') != 'false':
173
            do_graphs = True
174

  
175
        r += htmltext('<p>%s %s</p>') % (_('Total count:'), sum(counts.values()))
176

  
177
        if do_graphs:
178
            r += htmltext('<div class="splitcontent-left">')
179
        cats = Category.select()
180
        for cat in cats:
181
            category_formdefs = [x for x in formdefs if x.category_id == cat.id]
182
            r += self.category_global_stats(cat.name, category_formdefs, counts)
183

  
184
        category_formdefs = [x for x in formdefs if x.category_id is None]
185
        r += self.category_global_stats(_('Misc'), category_formdefs, counts)
186

  
187
        if do_graphs:
188
            r += htmltext('</div>')
189
            r += htmltext('<div class="splitcontent-right">')
190
            period_start = parsed_values.get('period_start')
191
            period_end = parsed_values.get('period_end')
192
            r += do_graphs_section(period_start, period_end)
193
            r += htmltext('</div>')
194

  
195
        return r.getvalue()
196

  
197
    def category_global_stats(self, title, category_formdefs, counts):
198
        r = TemplateIO(html=True)
199
        category_formdefs_ids = [x.id for x in category_formdefs]
200
        if not category_formdefs:
201
            return
202
        cat_counts = dict([(x, y) for x, y in counts.items() if x in
203
            category_formdefs_ids])
204
        if sum(cat_counts.values()) == 0:
205
            return
206
        r += htmltext('<div class="bo-block">')
207
        r += htmltext('<h3>%s</h3>') % title
208
        r += htmltext('<p>%s %s</p>') % (_('Count:'), sum(cat_counts.values()))
209
        r += htmltext('<ul>')
210
        for category_formdef in category_formdefs:
211
            if not counts.get(category_formdef.id):
212
                continue
213
            r += htmltext('<li>%s %s</li>') % (
214
                    _('%s:') % category_formdef.name,
215
                    counts.get(category_formdef.id))
216
        r += htmltext('</ul>')
217
        r += htmltext('</div>')
218
        return r.getvalue()
219

  
220
    def display_forms(self, forms_list):
221
        r = TemplateIO(html=True)
222
        r += htmltext('<ul>')
223
        cats = Category.select(order_by = 'name')
224
        for c in cats + [None]:
225
            if c is None:
226
                l2 = [x for x in forms_list if not x[0].category_id]
227
                cat_name = _('Misc')
228
            else:
229
                l2 = [x for x in forms_list if x[0].category_id == c.id]
230
                cat_name = c.name
231
            if not l2:
232
                continue
233
            r += htmltext('<li>%s</li>') % cat_name
234
            r += htmltext('<ul>')
235
            for formdef, no_pending, no_total in l2:
236
                r += htmltext('<li><a href="%s/">%s</a>') % (formdef.url_name, formdef.name)
237
                if no_pending:
238
                    r += _(': %(pending)s open on %(total)s') % {'pending': no_pending,
239
                                                        'total': no_total}
240
                else:
241
                    r += _(': %(total)s items') % {'total': no_total}
242
                r += htmltext('</li>')
243
            r += htmltext('</ul>')
244
        r += htmltext('</ul>')
245
        return r.getvalue()
246

  
247
    def _q_lookup(self, component):
248
        return FormPage(component)
249

  
250

  
251
class FormPage(Directory):
252
    _q_exports = ['', 'csv', 'stats', 'xls', 'ods', 'json', 'pending', 'export']
253

  
254
    def __init__(self, component):
255
        try:
256
            self.formdef = FormDef.get_by_urlname(component)
257
        except KeyError:
258
            raise errors.TraversalError()
259

  
260
        session = get_session()
261
        user = get_request().user
262
        if user is None and get_publisher().user_class.count() == 0:
263
            user = get_publisher().user_class()
264
            user.is_admin = True
265
        if not user:
266
            raise errors.AccessUnauthorizedError()
267
        if not user.is_admin and not self.formdef.is_of_concern_for_user(user):
268
            if session.user:
269
                raise errors.AccessForbiddenError()
270
            else:
271
                raise errors.AccessUnauthorizedError()
272
        get_response().breadcrumb.append( (component + '/', self.formdef.name) )
273

  
274
    def get_formdata_sidebar(self, qs=''):
275
        r = TemplateIO(html=True)
276
        r += htmltext('<ul id="sidebar-actions">')
277
        #' <li><a href="list%s">%s</a></li>' % (qs, _('List of results'))
278
        r += htmltext(' <li><a data-base-href="ods" href="ods%s">%s</a></li>') % (
279
                qs, _('Open Document Format Export'))
280
        r += htmltext(' <li><a data-base-href="csv" href="csv%s">%s</a></li>') % (
281
                qs, _('CSV Export'))
282
        if xlwt:
283
            r += htmltext('<li><a data-base-href="xls" href="xls%s">%s</a></li>') % (
284
                    qs, _('Excel Export'))
285
        r += htmltext(' <li><a href="stats">%s</a></li>') % _('Statistics')
286
        r += htmltext('</ul>')
287
        return r.getvalue()
288

  
289
    def get_filter_sidebar(self, selected_filter=None, mode='listing'):
290
        r = TemplateIO(html=True)
291

  
292
        waitpoint_status = self.formdef.workflow.get_waitpoint_status()
293
        period_fake_fields = [
294
            FakeField('start', 'period-date', _('Start')),
295
            FakeField('end', 'period-date', _('End')),
296
        ]
297
        filter_fields = []
298
        for field in period_fake_fields + self.get_formdef_fields():
299
            field.enabled = False
300
            if field.type not in ('item', 'period-date', 'status'):
301
                continue
302
            if field.type == 'status' and not waitpoint_status:
303
                continue
304
            filter_fields.append(field)
305

  
306
            if get_request().form:
307
                field.enabled = 'filter-%s' % field.id in get_request().form
308
            else:
309
                if mode == 'listing':
310
                    # enable status filter by default
311
                    field.enabled = (field.id in ('status',))
312
                elif mode == 'stats':
313
                    # enable period filters by default
314
                    field.enabled = (field.id in ('start', 'end'))
315

  
316
        r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % (
317
            _('Filters'), _('change')))
318

  
319
        for filter_field in filter_fields:
320
            if not filter_field.enabled:
321
                continue
322

  
323
            filter_field_key = 'filter-%s-value' % filter_field.id
324
            filter_field_value = get_request().form.get(filter_field_key)
325

  
326
            if filter_field.type == 'status':
327
                r += htmltext('<div class="widget">')
328
                r += htmltext('<div class="title">%s</div>') % _('Status to display')
329
                r += htmltext('<div class="content">')
330
                r += htmltext('<select name="filter">')
331
                filters = [('all', _('All'), None),
332
                        ('pending', _('Pending'), None),
333
                        ('done', _('Done'), None)]
334
                for status in waitpoint_status:
335
                    filters.append((status.id, status.name, status.colour))
336
                for filter_id, filter_label, filter_colour in filters:
337
                    if filter_id == selected_filter:
338
                        selected = ' selected="selected"'
339
                    else:
340
                        selected = ''
341
                    style = ''
342
                    if filter_colour and filter_colour != 'FFFFFF':
343
                        fg_colour = misc.get_foreground_colour(filter_colour)
344
                        style = 'style="background: #%s; color: %s;"' % (
345
                                filter_colour, fg_colour)
346
                    r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style))
347
                    r += htmltext('%s</option>') % filter_label
348
                r += htmltext('</select>')
349
                r += htmltext('</div>')
350
                r += htmltext('</div>')
351

  
352
            elif filter_field.type == 'period-date':
353
                r += DateWidget(filter_field_key, title=filter_field.label,
354
                        value=filter_field_value, render_br=False).render()
355

  
356
            elif filter_field.type == 'item':
357
                filter_field.required = False
358
                options = filter_field.get_options()
359
                if options:
360
                    r += SingleSelectWidget(filter_field_key, title=filter_field.label,
361
                            options=options, value=filter_field_value,
362
                            render_br=False).render()
363
                else:
364
                    # There may be no options because the field is using
365
                    # a jsonp data source, or a json source using a
366
                    # parametrized URL depending on unavailable variables.
367
                    #
368
                    # In that case fall back on a string widget.
369
                    r += StringWidget(filter_field_key, title=filter_field.label,
370
                            value=filter_field_value, render_br=False).render()
371

  
372
        # field filter dialog content
373
        r += htmltext('<div style="display: none;">')
374
        r += htmltext('<ul id="field-filter">')
375
        for field in filter_fields:
376
            r += htmltext('<li><input type="checkbox" name="filter-%s"') % field.id
377
            if field.enabled:
378
                r += htmltext(' checked="checked"')
379
            r += htmltext(' id="fields-filter-%s"') % field.id
380
            r += htmltext('/>')
381
            r += htmltext('<label for="fields-filter-%s">%s</label>') % (
382
                    field.id, misc.ellipsize(field.label, 70))
383
            r += htmltext('</li>')
384
        r += htmltext('</ul>')
385
        r += htmltext('</div>')
386

  
387
        return r.getvalue()
388

  
389
    def get_fields_sidebar(self, selected_filter, fields, offset=None,
390
            limit=None, order_by=None):
391
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
392
        get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
393

  
394
        r = TemplateIO(html=True)
395
        r += htmltext('<form id="listing-settings" action=".">')
396
        if offset or limit:
397
            if not offset:
398
                offset = 0
399
            r += htmltext('<input type="hidden" name="offset" value="%s"/>') % offset
400
        if limit:
401
            r += htmltext('<input type="hidden" name="limit" value="%s"/>') % limit
402

  
403
        if get_publisher().is_using_postgresql():
404
            if order_by is None:
405
                order_by = ''
406
            r += htmltext('<input type="hidden" name="order_by" value="%s"/>') % order_by
407

  
408
        if get_publisher().is_using_postgresql():
409
            r += htmltext('<h3>%s</h3>') % _('Search')
410
            if get_request().form.get('q'):
411
                q = get_request().form.get('q')
412
                if type(q) is not unicode:
413
                    q = unicode(q, get_publisher().site_charset)
414
                r += htmltext('<input name="q" value="%s">') % q.encode(get_publisher().site_charset)
415
            else:
416
                r += htmltext('<input name="q">')
417
            r += htmltext('<input type="submit" value="%s"/>') % _('Search')
418

  
419
        r += self.get_filter_sidebar(selected_filter=selected_filter)
420

  
421
        r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
422

  
423
        r += htmltext('<button id="columns-settings">%s</button>') % _('Columns Settings')
424

  
425
        # column settings dialog content
426
        r += htmltext('<div style="display: none;">')
427
        r += htmltext('<ul id="columns-filter">')
428
        for field in self.get_formdef_fields():
429
            if not hasattr(field, str('get_view_value')):
430
                continue
431
            r += htmltext('<li><input type="checkbox" name="%s"') % field.id
432
            if field.id in [x.id for x in fields]:
433
                r += htmltext(' checked="checked"')
434
            r += htmltext(' id="fields-column-%s"') % field.id
435
            r += htmltext('/>')
436
            r += htmltext('<label for="fields-column-%s">%s</label>') % (
437
                    field.id, misc.ellipsize(field.label, 70))
438
            r += htmltext('</li>')
439
        r += htmltext('</ul>')
440
        r += htmltext('</div>')
441
        r += htmltext('</form>')
442
        return r.getvalue()
443

  
444
    def get_formdef_fields(self):
445
        fields = []
446
        fields.append(FakeField('id', 'id', _('Identifier')))
447
        fields.append(FakeField('time', 'time', _('Time')))
448
        fields.append(FakeField('user-label', 'user-label', _('User Label')))
449
        fields.extend(self.formdef.fields)
450
        fields.append(FakeField('status', 'status', _('Status')))
451
        fields.append(FakeField('anonymised', 'anonymised', _('Anonymised')))
452

  
453
        return fields
454

  
455
    def get_fields_from_query(self):
456
        field_ids = [x for x in get_request().form.keys()]
457
        if not field_ids:
458
            field_ids = ['id', 'time', 'user-label']
459
            for field in self.formdef.fields:
460
                if hasattr(field, str('get_view_value')) and field.in_listing:
461
                    field_ids.append(field.id)
462
            field_ids.append('status')
463

  
464
        fields = []
465
        for field in self.get_formdef_fields():
466
            if field.id in field_ids:
467
                fields.append(field)
468

  
469
        return fields
470

  
471
    def get_filter_from_query(self, default='pending'):
472
        if 'filter' in get_request().form:
473
            return get_request().form['filter']
474
        if self.formdef.workflow.possible_status:
475
            return default
476
        return 'all'
477

  
478
    def get_criterias_from_query(self):
479
        period_fake_fields = [
480
            FakeField('start', 'period-date', _('Start')),
481
            FakeField('end', 'period-date', _('End')),
482
        ]
483
        filter_fields = []
484
        criterias = []
485
        format_string = misc.date_format()
486
        for filter_field in period_fake_fields + self.get_formdef_fields():
487
            if filter_field.type not in ('item', 'period-date'):
488
                continue
489

  
490
            filter_field_key = None
491

  
492
            if filter_field.varname:
493
                # if this is a field with a varname and filter-%(varname)s is
494
                # present in the query string, enable this filter.
495
                if get_request().form.get('filter-%s' % filter_field.varname):
496
                    filter_field_key = 'filter-%s' % filter_field.varname
497

  
498
            if get_request().form.get('filter-%s' % filter_field.id):
499
                # if there's a filter-%(id)s, it is used to enable the actual
500
                # filter, and the value will be found in filter-%s-value.
501
                filter_field_key = 'filter-%s-value' % filter_field.id
502

  
503
            if not filter_field_key:
504
                # if there's not known filter key, skip.
505
                continue
506

  
507
            filter_field_value = get_request().form.get(filter_field_key)
508
            if not filter_field_value:
509
                continue
510

  
511
            if filter_field.id == 'start':
512
                period_start = time.strptime(filter_field_value, format_string)
513
                criterias.append(GreaterOrEqual('receipt_time', period_start))
514
            elif filter_field.id == 'end':
515
                period_end = time.strptime(filter_field_value, format_string)
516
                criterias.append(LessOrEqual('receipt_time', period_end))
517
            elif filter_field.type == 'item' and filter_field_value not in (None, 'None'):
518
                criterias.append(Equal('f%s' % filter_field.id, filter_field_value))
519

  
520
        return criterias
521

  
522

  
523
    def _q_index(self):
524
        get_logger().info('backoffice - form %s - listing' % self.formdef.name)
525

  
526
        fields = self.get_fields_from_query()
527
        selected_filter = self.get_filter_from_query()
528
        criterias = self.get_criterias_from_query()
529

  
530
        if get_publisher().is_using_postgresql():
531
            # only enable pagination in SQL mode, as we do not have sorting in
532
            # the other case.
533
            limit = get_request().form.get('limit', 20)
534
        else:
535
            limit = get_request().form.get('limit', 0)
536
        offset = get_request().form.get('offset', 0)
537
        order_by = get_request().form.get('order_by', None)
538
        query = get_request().form.get('q')
539

  
540
        qs = ''
541
        if get_request().get_query():
542
            qs = '?' + get_request().get_query()
543

  
544
        table = FormDefUI(self.formdef).listing(fields=fields,
545
                        selected_filter=selected_filter, include_form=True,
546
                        limit=int(limit), offset=int(offset), query=query,
547
                        order_by=order_by, criterias=criterias)
548

  
549
        if get_request().form.get('ajax') == 'true':
550
            get_response().filter = None
551
            return table
552

  
553
        html_top('management', '%s - %s' % (_('Listing'), self.formdef.name))
554
        r = TemplateIO(html=True)
555
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Listing'))
556
        r += table
557

  
558
        get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
559
                self.get_fields_sidebar(selected_filter, fields, limit=limit,
560
                        offset=offset, order_by=order_by)
561

  
562
        return r.getvalue()
563

  
564
    def pending(self):
565
        get_logger().info('backoffice - form %s - pending' % self.formdef.name)
566
        get_response().breadcrumb.append( ('pending', _('Pending Forms')) )
567
        html_top('management', '%s - %s' % (_('Pending Forms'), self.formdef.name))
568
        r = TemplateIO(html=True)
569
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Pending Forms'))
570

  
571
        not_endpoint_status = [('wf-%s' % x.id, x.name) for x in \
572
                                  self.formdef.workflow.get_not_endpoint_status()]
573

  
574
        nb_status = len(not_endpoint_status)
575
        column2 = nb_status/2
576
        r += htmltext('<div class="splitcontent-left">')
577
        for i, (status_id, status_label) in enumerate(not_endpoint_status):
578
            if i > 0 and i == column2:
579
                r += htmltext('</div>')
580
                r += htmltext('<div class="splitcontent-right">')
581
            status_forms = self.formdef.data_class().get_with_indexed_value(
582
                        str('status'), status_id)
583
            if not status_forms:
584
                continue
585
            status_forms.sort(lambda x, y: cmp(getattr(x, str('receipt_time')),
586
                                    getattr(y, str('receipt_time'))))
587
            status_forms.reverse()
588
            r += htmltext('<div class="bo-block">')
589
            r += htmltext('<h3>%s</h3>') % _('Forms with status "%s"') % status_label
590

  
591
            r += htmltext('<ul>')
592
            for f in status_forms:
593
                try:
594
                    u = get_publisher().user_class.get(f.user_id)
595
                    userlabel = u.display_name
596
                except KeyError:
597
                    userlabel = _('unknown user')
598
                r += htmltext('<li><a href="%s/">%s, %s</a></li>') % (
599
                    f.id,
600
                    misc.localstrftime(f.receipt_time),
601
                    userlabel)
602
            r += htmltext('</ul>')
603
            r += htmltext('</div>')
604
        r += htmltext('</div>')
605

  
606
        r += htmltext('<p class="clear"><a href=".">%s</a></p>') % _('Back')
607
        return r.getvalue()
608

  
609
    def csv_tuple_heading(self, fields):
610
        heading_fields = [] # '#id', _('time'), _('userlabel'), _('status')]
611
        for field in fields:
612
            heading_fields.extend(field.get_csv_heading())
613
        return heading_fields
614

  
615
    def csv_tuple(self, fields, data, hint=None):
616
        elements = []
617
        for field in fields:
618
            if field.type == 'id':
619
                element = str(data.id)
620
            elif field.type == 'time':
621
                element = misc.localstrftime(data.receipt_time)
622
            elif field.type == 'user-label':
623
                try:
624
                    element = get_publisher().user_class.get(data.user_id).display_name
625
                except:
626
                    element = '-'
627
            elif field.type == 'status':
628
                element = data.get_status_label()
629
            else:
630
                element = data.data.get(field.id, '') or ''
631
            elements.extend(field.get_csv_value(element, hint=hint))
632
        return elements
633

  
634
    def csv(self):
635
        fields = self.get_fields_from_query()
636
        selected_filter = self.get_filter_from_query()
637
        user = get_request().user
638
        query = get_request().form.get('q')
639

  
640
        class Exporter(object):
641
            def __init__(self, formpage, formdef, fields, selected_filter):
642
                self.formpage = formpage
643
                self.formdef = formdef
644
                self.fields = fields
645
                self.selected_filter = selected_filter
646

  
647
            def export(self, job=None):
648
                self.output = cStringIO.StringIO()
649
                csv_output = csv.writer(self.output)
650

  
651
                csv_output.writerow(self.formpage.csv_tuple_heading(self.fields))
652

  
653
                items, total_count = FormDefUI(self.formdef).get_listing_items(
654
                        self.selected_filter, user=user, query=query)
655

  
656
                for filled in items:
657
                    csv_output.writerow(self.formpage.csv_tuple(self.fields, filled))
658

  
659
                if job:
660
                    job.file_content = self.output.getvalue()
661
                    job.content_type = 'text/csv'
662
                    job.store()
663

  
664
        get_logger().info('backoffice - form %s - listing csv' % self.formdef.name)
665

  
666
        count = self.formdef.data_class().count()
667
        exporter = Exporter(self, self.formdef, fields, selected_filter)
668
        if count > 100: # Arbitrary threshold
669
            job = get_response().add_after_job(
670
                str(N_('Exporting forms in CSV')),
671
                exporter.export)
672
            job.file_name = '%s.csv' % self.formdef.url_name
673
            job.store()
674
            return redirect('export?job=%s' % job.id)
675
        else:
676
            exporter.export()
677

  
678
            response = get_response()
679
            response.set_content_type('text/plain')
680
            #response.set_header('content-disposition', 'attachment; filename=%s.csv' % self.formdef.url_name)
681
            return exporter.output.getvalue()
682

  
683
    def export(self):
684
        if get_request().form.get('download'):
685
            return self.export_download()
686

  
687
        try:
688
            job = AfterJob.get(get_request().form.get('job'))
689
        except KeyError:
690
            return redirect('.')
691

  
692
        html_top('management', title=_('Exporting'))
693
        r = TemplateIO(html=True)
694
        r += get_session().display_message()
695
        get_response().add_javascript(['jquery.js', 'afterjob.js'])
696
        r += htmltext('<dl class="job-status">')
697
        r += htmltext('<dt>')
698
        r += _(job.label)
699
        r += htmltext('</dt>')
700
        r += htmltext('<dd>')
701
        r += htmltext('<span class="afterjob" id="%s">') % job.id
702
        r += _(job.status)
703
        r += htmltext('</span>')
704
        r += htmltext('</dd>')
705
        r += htmltext('</dl>')
706

  
707
        r += htmltext('<div class="done">')
708
        r += htmltext('<a download="%s" href="export?download=%s">%s</a>') % (
709
                job.file_name, job.id, _('Download Export'))
710
        r += htmltext('</div>')
711
        return r.getvalue()
712

  
713
    def export_download(self):
714
        try:
715
            job = AfterJob.get(get_request().form.get('download'))
716
        except KeyError:
717
            return redirect('.')
718

  
719
        if not job.status == 'completed':
720
            raise errors.TraversalError()
721
        response = get_response()
722
        response.set_content_type(job.content_type)
723
        response.set_header('content-disposition',
724
            'attachment; filename=%s' % job.file_name)
725
        return job.file_content
726

  
727
    def xls(self):
728
        if xlwt is None:
729
            raise errors.TraversalError()
730

  
731
        fields = self.get_fields_from_query()
732
        selected_filter = self.get_filter_from_query()
733
        user = get_request().user
734
        query = get_request().form.get('q')
735

  
736
        class Exporter(object):
737
            def __init__(self, formpage, formdef, fields, selected_filter):
738
                self.formpage = formpage
739
                self.formdef = formdef
740
                self.fields = fields
741
                self.selected_filter = selected_filter
742

  
743
            def export(self, job=None):
744
                w = xlwt.Workbook(encoding=get_publisher().site_charset)
745
                ws = w.add_sheet('1')
746

  
747
                for i, f in enumerate(self.formpage.csv_tuple_heading(self.fields)):
748
                    ws.write(0, i, f)
749

  
750
                items, total_count = FormDefUI(self.formdef).get_listing_items(
751
                        self.selected_filter, user=user, query=query)
752

  
753
                for i, filled in enumerate(items):
754
                    for j, elem in enumerate(self.formpage.csv_tuple(fields, filled)):
755
                        if elem and len(elem) > 32767:
756
                            # xls cells have a limit of 32767 characters, cut
757
                            # it down.
758
                            elem = elem[:32760] + ' [...]'
759
                        ws.write(i+1, j, elem)
760

  
761
                self.output = cStringIO.StringIO()
762
                w.save(self.output)
763

  
764
                if job:
765
                    job.file_content = self.output.getvalue()
766
                    job.content_type = 'application/vnd.ms-excel'
767
                    job.store()
768

  
769
        get_logger().info('backoffice - form %s - as excel' % self.formdef.name)
770

  
771
        count = self.formdef.data_class().count()
772
        exporter = Exporter(self, self.formdef, fields, selected_filter)
773
        if count > 100: # Arbitrary threshold
774
            job = get_response().add_after_job(
775
                str(N_('Exporting forms in Excel format')),
776
                exporter.export)
777
            job.file_name = '%s.xls' % self.formdef.url_name
778
            job.store()
779
            return redirect('export?job=%s' % job.id)
780
        else:
781
            exporter.export()
782

  
783
            response = get_response()
784
            response.set_content_type('application/vnd.ms-excel')
785
            response.set_header('content-disposition', 'attachment; filename=%s.xls' % self.formdef.url_name)
786
            return exporter.output.getvalue()
787

  
788
    def ods(self):
789
        fields = self.get_fields_from_query()
790
        selected_filter = self.get_filter_from_query()
791
        user = get_request().user
792
        query = get_request().form.get('q')
793

  
794
        class Exporter(object):
795
            def __init__(self, formpage, formdef, fields, selected_filter):
796
                self.formpage = formpage
797
                self.formdef = formdef
798
                self.fields = fields
799
                self.selected_filter = selected_filter
800

  
801
            def export(self, job=None):
802
                w = ods.Workbook(encoding=get_publisher().site_charset)
803
                ws = w.add_sheet('1')
804

  
805
                for i, f in enumerate(self.formpage.csv_tuple_heading(self.fields)):
806
                    ws.write(0, i, f)
807

  
808
                items, total_count = FormDefUI(self.formdef).get_listing_items(
809
                        self.selected_filter, user=user, query=query)
810

  
811
                for i, filled in enumerate(items):
812
                    for j, elem in enumerate(self.formpage.csv_tuple(fields, filled, hint='ods')):
813
                        if type(elem) is str and '[download]' in elem:
814
                            elem = elem.replace('[download]', filled.get_url(backoffice=True))
815
                            ws.write(i+1, j, elem, hint='uri')
816
                        else:
817
                            ws.write(i+1, j, elem)
818

  
819
                self.output = cStringIO.StringIO()
820
                w.save(self.output)
821

  
822
                if job:
823
                    job.file_content = self.output.getvalue()
824
                    job.content_type = 'application/vnd.oasis.opendocument.spreadsheet'
825
                    job.store()
826

  
827
        get_logger().info('backoffice - form %s - as ods' % self.formdef.name)
828

  
829
        count = self.formdef.data_class().count()
830
        exporter = Exporter(self, self.formdef, fields, selected_filter)
831
        if count > 100: # Arbitrary threshold
832
            job = get_response().add_after_job(
833
                str(N_('Exporting forms in Open Document format')),
834
                exporter.export)
835
            job.file_name = '%s.ods' % self.formdef.url_name
836
            job.store()
837
            return redirect('export?job=%s' % job.id)
838
        else:
839
            exporter.export()
840

  
841
            response = get_response()
842
            response.set_content_type('application/vnd.oasis.opendocument.spreadsheet')
843
            response.set_header('content-disposition', 'attachment; filename=%s.ods' % self.formdef.url_name)
844
            return exporter.output.getvalue()
845

  
846
    def json(self):
847
        get_response().set_content_type('application/json')
848
        from wcs.api import get_user_from_api_query_string
849
        user = get_user_from_api_query_string() or get_request().user
850
        selected_filter = self.get_filter_from_query(default='all')
851
        criterias = self.get_criterias_from_query()
852
        order_by = get_request().form.get('order_by', None)
853
        query = get_request().form.get('q')
854
        items, total_count = FormDefUI(self.formdef).get_listing_items(
855
            selected_filter, user=user, query=query, criterias=criterias,
856
            order_by=order_by)
857
        if get_request().form.get('full') == 'on':
858
            output = [json.loads(filled.export_to_json()) for filled in items]
859
        else:
860
            output = [{'id': filled.id,
861
                'url': filled.get_url(),
862
                'receipt_time': filled.receipt_time,
863
                'last_update_time': filled.last_update_time} for filled in items]
864
        return json.dumps(output,
865
                cls=misc.JSONEncoder,
866
                encoding=get_publisher().site_charset)
867

  
868
    def get_stats_sidebar(self, selected_filter):
869
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
870
        get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
871
        r = TemplateIO(html=True)
872
        r += htmltext('<form id="listing-settings" action="stats">')
873
        r += self.get_filter_sidebar(selected_filter=selected_filter, mode='stats')
874
        r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
875
        if misc.can_decorate_as_pdf():
876
            r += htmltext('<button class="pdf">%s</button>') % _('Download PDF')
877
        r += htmltext('</form>')
878
        return r.getvalue()
879

  
880
    def stats(self):
881
        get_logger().info('backoffice - form %s - stats' % self.formdef.name)
882
        html_top('management', '%s - %s' % (_('Form'), self.formdef.name))
883
        r = TemplateIO(html=True)
884
        get_response().breadcrumb.append( ('stats', _('Statistics')) )
885

  
886
        selected_filter = self.get_filter_from_query(default='all')
887
        criterias = self.get_criterias_from_query()
888
        get_response().filter['sidebar'] = self.get_formdata_sidebar() + \
889
                self.get_stats_sidebar(selected_filter)
890
        do_graphs = get_publisher().is_using_postgresql()
891

  
892
        if selected_filter and selected_filter != 'all':
893
            if selected_filter == 'pending':
894
                applied_filters = ['wf-%s' % x.id for x in
895
                              self.formdef.workflow.get_not_endpoint_status()]
896
            elif selected_filter == 'done':
897
                applied_filters = ['wf-%s' % x.id for x in
898
                              self.formdef.workflow.get_endpoint_status()]
899
            else:
900
                applied_filters = ['wf-%s' % selected_filter]
901
            criterias.append(Or([Equal('status', x) for x in applied_filters]))
902

  
903
        values = self.formdef.data_class().select(criterias)
904
        if get_publisher().is_using_postgresql():
905
            # load all evolutions in a single batch, to avoid as many query as
906
            # there are formdata when computing resolution times statistics.
907
            self.formdef.data_class().load_all_evolutions(values)
908

  
909
        r += htmltext('<div id="statistics">')
910
        if do_graphs:
911
            r += htmltext('<div class="splitcontent-left">')
912

  
913
        no_forms = len(values)
914
        r += htmltext('<div class="bo-block">')
915
        r += htmltext('<p>%s %d</p>') % (_('Total number of records:'), no_forms)
916

  
917
        if self.formdef.workflow:
918
            r += htmltext('<ul>')
919
            for status in self.formdef.workflow.possible_status:
920
                r += htmltext('<li>%s: %d</li>') % (status.name,
921
                        len([x for x in values if x.status == 'wf-%s' % status.id]))
922
            r += htmltext('</ul>')
923
        r += htmltext('</div>')
924

  
925
        excluded_fields = []
926
        for criteria in criterias:
927
            if not isinstance(criteria, Equal):
928
                continue
929
            excluded_fields.append(criteria.attribute[1:])
930

  
931
        stats_for_fields = self.stats_fields(values,
932
                excluded_fields=excluded_fields)
933
        if stats_for_fields:
934
            r += htmltext('<div class="bo-block">')
935
            r += stats_for_fields
936
            r += htmltext('</div>')
937

  
938
        stats_times = self.stats_resolution_time(values)
939
        if stats_times:
940
            r += htmltext('<div class="bo-block">')
941
            r += stats_times
942
            r += htmltext('</div>')
943

  
944
        if do_graphs:
945
            r += htmltext('</div>')
946
            r += htmltext('<div class="splitcontent-right">')
947
            criterias.append(Equal('formdef_id', int(self.formdef.id)))
948
            r += do_graphs_section(criterias=criterias)
949
            r += htmltext('</div>')
950

  
951
        r += htmltext('</div>') # id="statistics"
952

  
953
        if get_request().form.get('ajax') == 'true':
954
            get_response().filter = None
955
            return r.getvalue()
956

  
957
        page = TemplateIO(html=True)
958
        page += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Statistics'))
959
        page += htmltext(r)
960
        page += htmltext('<a class="back" href=".">%s</a>') % _('Back')
961

  
962
        if 'pdf' in get_request().form:
963
            pdf_content = misc.decorate_as_pdf(page.getvalue())
964
            response = get_response()
965
            response.set_content_type('application/pdf')
966
            return pdf_content
967

  
968
        return page.getvalue()
969

  
970
    def stats_fields(self, values, excluded_fields=None):
971
        r = TemplateIO(html=True)
972
        had_page = False
973
        last_page = None
974
        last_title = None
975
        for f in self.formdef.fields:
976
            if excluded_fields and f.id in excluded_fields:
977
                continue
978
            if f.type == 'page':
979
                last_page = f.label
980
                last_title = None
981
                continue
982
            if f.type == 'title':
983
                last_title = f.label
984
                continue
985
            if not f.stats:
986
                continue
987
            t = f.stats(values)
988
            if not t:
989
                continue
990
            if last_page:
991
                if had_page:
992
                    r += htmltext('</div>')
993
                r += htmltext('<div class="page">')
994
                r += htmltext('<h3>%s</h3>') % last_page
995
                had_page = True
996
                last_page = None
997
            if last_title:
998
                r += htmltext('<h3>%s</h3>') % last_title
999
                last_title = None
1000
            r += t
1001

  
1002
        if had_page:
1003
            r += htmltext('</div>')
1004

  
1005
        return r.getvalue()
1006

  
1007
    def stats_resolution_time(self, values):
1008
        possible_status = [('wf-%s' % x.id, x.id) for x in self.formdef.workflow.possible_status]
1009

  
1010
        if len(possible_status) < 2:
1011
            return
1012

  
1013
        r = TemplateIO(html=True)
1014
        r += htmltext('<h2>%s</h2>') % _('Resolution time')
1015

  
1016
        for status, status_id in possible_status:
1017
            res_time_forms = [
1018
                    (time.mktime(x.evolution[-1].time) - time.mktime(x.receipt_time)) \
1019
                    for x in values if x.status == status and x.evolution]
1020
            if not res_time_forms:
1021
                continue
1022
            res_time_forms.sort()
1023
            sum_times = sum(res_time_forms)
1024
            len_times = len(res_time_forms)
1025
            r += htmltext('<h3>%s</h3>') % (_('To Status "%s"') % self.formdef.workflow.get_status(status_id).name)
1026
            r += htmltext('<ul>')
1027
            r += htmltext(' <li>%s %s</li>') % (_('Minimum Time:'), format_time(min(res_time_forms)))
1028
            r += htmltext(' <li>%s %s</li>') % (_('Maximum Time:'), format_time(max(res_time_forms)))
1029
            r += htmltext(' <li>%s %s</li>') % (_('Range:'), format_time(max(res_time_forms)-min(res_time_forms)))
1030
            mean = sum_times/len_times
1031
            r += htmltext(' <li>%s %s</li>') % (_('Mean:'), format_time(mean))
1032
            if len_times % 2:
1033
                median = res_time_forms[len_times/2]
1034
            else:
1035
                midpt = len_times/2
1036
                median = (res_time_forms[midpt-1]+res_time_forms[midpt])/2
1037
            r += htmltext(' <li>%s %s</li>') % (_('Median:'), format_time(median))
1038

  
1039
            # variance...
1040
            x = 0
1041
            for t in res_time_forms:
1042
                x += (t - mean)**2.0
1043
            try:
1044
                variance = x/(len_times+1)
1045
            except:
1046
                variance = 0
1047
            # not displayed since in square seconds which is not easy to grasp
1048

  
1049
            from math import sqrt
1050
            # and standard deviation
1051
            std_dev = sqrt(variance)
1052
            r += htmltext(' <li>%s %s</li>') % (_('Standard Deviation:'), format_time(std_dev))
1053

  
1054
            r += htmltext('</ul>')
1055

  
1056
        return r.getvalue()
1057

  
1058
    def _q_lookup(self, component):
1059
        try:
1060
            filled = self.formdef.data_class().get(component)
1061
        except KeyError:
1062
            raise errors.TraversalError()
1063

  
1064
        return FormBackOfficeStatusPage(self.formdef, filled)
1065

  
1066

  
1067
class FormBackOfficeStatusPage(FormStatusPage):
1068
    def html_top(self, title = None):
1069
        return html_top('management', title)
1070

  
1071
    def _q_index(self):
1072
        get_response().add_javascript(['jquery.js', 'qommon.admin.js'])
1073
        return self.status()
1074

  
1075

  
1076
class FakeField(object):
1077
    def __init__(self, id, type_, label):
1078
        self.id = id
1079
        self.type = type_
1080
        self.label = label
1081
        self.fake = True
1082
        self.varname = None
1083

  
1084
    def get_view_value(self, value):
1085
        # just here to quack like a duck
1086
        return None
1087

  
1088
    def get_csv_heading(self):
1089
        return [self.label]
1090

  
1091
    def get_csv_value(self, element, hint=None):
1092
        return [element]
1093

  
1094

  
1095
def do_graphs_section(period_start=None, period_end=None, criterias=None):
1096
    from wcs import sql
1097
    r = TemplateIO(html=True)
1098
    monthly_totals = sql.get_monthly_totals(period_start, period_end, criterias)[-12:]
1099
    yearly_totals = sql.get_yearly_totals(period_start, period_end, criterias)[-10:]
1100

  
1101
    if not monthly_totals:
1102
        monthly_totals = [('%s-%s' % datetime.date.today().timetuple()[:2], 0)]
1103
    if not yearly_totals:
1104
        yearly_totals = [(datetime.date.today().year, 0)]
1105

  
1106
    weekday_totals = sql.get_weekday_totals(period_start, period_end, criterias)
1107
    weekday_line = []
1108
    weekday_names = [_('Sunday'), _('Monday'), _('Tuesday'),
1109
            _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
1110
    for weekday, total in weekday_totals:
1111
        label = weekday_names[weekday]
1112
        weekday_line.append((label, total))
1113
    # move Sunday to the last place
1114
    weekday_line = weekday_line[1:] + [weekday_line[0]]
1115

  
1116
    hour_totals = sql.get_hour_totals(period_start, period_end, criterias)
1117

  
1118
    r += htmltext('''<script>
1119
var weekday_line = %(weekday_line)s;
1120
var hour_line = %(hour_line)s;
1121
var month_line = %(month_line)s;
1122
var year_line = %(year_line)s;
1123
</script>''' % {
1124
        'weekday_line': json.dumps(weekday_line),
1125
        'hour_line': json.dumps(hour_totals),
1126
        'month_line': json.dumps(monthly_totals),
1127
        'year_line': json.dumps(yearly_totals),
1128
        })
1129

  
1130
    if len(yearly_totals) > 1:
1131
        r += htmltext('<h3>%s</h3>') % _('Submissions by year')
1132
        r += htmltext('<div id="chart_years" style="height:160px; width:100%;"></div>')
1133

  
1134
    r += htmltext('<h3>%s</h3>') % _('Submissions by month')
1135
    r += htmltext('<div id="chart_months" style="height:160px; width:100%;"></div>')
1136
    r += htmltext('<h3>%s</h3>') % _('Submissions by weekday')
1137
    r += htmltext('<div id="chart_weekdays" style="height:160px; width:100%;"></div>')
1138
    r += htmltext('<h3>%s</h3>') % _('Submissions by hour')
1139
    r += htmltext('<div id="chart_hours" style="height:160px; width:100%;"></div>')
1140

  
1141

  
1142
    get_response().add_javascript(['jquery.js', 'jqplot/jquery.jqplot.min.js',
1143
        'jqplot/plugins/jqplot.canvasTextRenderer.min.js',
1144
        'jqplot/plugins/jqplot.canvasAxisLabelRenderer.min.js',
1145
        'jqplot/plugins/jqplot.canvasAxisTickRenderer.min.js',
1146
        'jqplot/plugins/jqplot.categoryAxisRenderer.min.js',
1147
        'jqplot/plugins/jqplot.barRenderer.min.js',
1148
        ])
1149

  
1150
    get_response().add_javascript_code('''
1151
function wcs_draw_graphs() {
1152
$.jqplot ('chart_weekdays', [weekday_line], {
1153
series:[{renderer:$.jqplot.BarRenderer}],
1154
axesDefaults: {
1155
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
1156
tickOptions: { angle: -30, }
1157
},
1158
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer } }
1159
});
1160

  
1161
$.jqplot ('chart_hours', [hour_line], {
1162
axesDefaults: {
1163
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
1164
tickOptions: { angle: -30, }
1165
},
1166
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
1167
});
1168

  
1169
$.jqplot ('chart_months', [month_line], {
1170
axesDefaults: {
1171
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
1172
tickOptions: { angle: -30, }
1173
},
1174
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
1175
});
1176

  
1177
if ($('#chart_years').length) {
1178
$.jqplot ('chart_years', [year_line], {
1179
series:[{renderer:$.jqplot.BarRenderer}],
1180
axesDefaults: {
1181
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
1182
tickOptions: { angle: -30, }
1183
},
1184
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
1185
});
1186
}
1187
}
1188

  
1189
$(document).ready(function(){
1190
  wcs_draw_graphs();
1191
});
1192
    ''')
1193
    return r.getvalue()
1194

  
1195

  
1196
def get_stats_criteria(request, parsed_values=None):
1197
    """
1198
    Parses the request query string and returns a list of criterias suitable
1199
    for select() usage. The parsed_values parameter can be given a dictionary,
1200
    to be filled with the parsed values.
1201
    """
1202
    format_string = misc.date_format()
1203
    criterias = [NotEqual('status', 'draft')]
1204
    try:
1205
        period_start = time.strptime(request.form.get('start'), format_string)
1206
        criterias.append(GreaterOrEqual('receipt_time', period_start))
1207
        if parsed_values is not None:
1208
            parsed_values['period_start'] = datetime.datetime.fromtimestamp(time.mktime(period_start))
1209
    except (ValueError, TypeError):
1210
        pass
1211

  
1212
    try:
1213
        period_end = time.strptime(request.form.get('end'), format_string)
1214
        criterias.append(LessOrEqual('receipt_time', period_end))
1215
        if parsed_values is not None:
1216
            parsed_values['period_end'] = datetime.datetime.fromtimestamp(time.mktime(period_end))
1217
    except (ValueError, TypeError):
1218
        pass
1219

  
1220
    return criterias
1221

  
1222
def format_time(t, units = 2):
1223
    days = int(t/86400)
1224
    hours = int((t-days*86400)/3600)
1225
    minutes = int((t-days*86400-hours*3600)/60)
1226
    seconds = t % 60
1227
    if units == 1:
1228
        if days:
1229
            return _('%d day(s)') % days
1230
        if hours:
1231
            return _('%d hour(s)') % hours
1232
        if minutes:
1233
            return _('%d minute(s)') % minutes
1234
    elif units == 2:
1235
        if days:
1236
            return _('%(days)d day(s) and %(hours)d hour(s)') % {
1237
                    'days': days, 'hours': hours}
1238
        if hours:
1239
            return _('%(hours)d hour(s) and %(minutes)d minute(s)') % {
1240
                    'hours': hours, 'minutes': minutes}
1241
        if minutes:
1242
            return _('%(minutes)d minute(s) and %(seconds)d seconds') % {
1243
                    'minutes': minutes, 'seconds': seconds}
1244
    return _('%d seconds') % seconds
wcs/backoffice/root.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import csv
18
import cStringIO
19
import datetime
20
import json
21 17
import time
22 18

  
23 19
from quixote import get_session, get_publisher, get_request, get_response, redirect
......
28 24
from qommon.backoffice.menu import html_top
29 25

  
30 26
from qommon import misc, get_logger, get_cfg
31
from qommon.afterjobs import AfterJob
32 27
from qommon import errors
33
from qommon import ods
34 28
from qommon.form import *
35
from qommon.storage import Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or
36 29

  
37
from wcs.forms.common import FormStatusPage
38

  
39
from wcs.categories import Category
40 30
from wcs.formdef import FormDef
41 31
from wcs.roles import Role
42 32

  
43
from wcs.forms.backoffice import FormDefUI
44

  
45 33
import wcs.admin.bounces
46 34
import wcs.admin.categories
47 35
import wcs.admin.forms
......
51 39
import wcs.admin.users
52 40
import wcs.admin.workflows
53 41

  
54
from wcs import data_sources
55

  
56

  
57

  
58
try:
59
    import xlwt
60
except ImportError:
61
    xlwt = None
62

  
63
def format_time(t, units = 2):
64
    days = int(t/86400)
65
    hours = int((t-days*86400)/3600)
66
    minutes = int((t-days*86400-hours*3600)/60)
67
    seconds = t % 60
68
    if units == 1:
69
        if days:
70
            return _('%d day(s)') % days
71
        if hours:
72
            return _('%d hour(s)') % hours
73
        if minutes:
74
            return _('%d minute(s)') % minutes
75
    elif units == 2:
76
        if days:
77
            return _('%(days)d day(s) and %(hours)d hour(s)') % {
78
                    'days': days, 'hours': hours}
79
        if hours:
80
            return _('%(hours)d hour(s) and %(minutes)d minute(s)') % {
81
                    'hours': hours, 'minutes': minutes}
82
        if minutes:
83
            return _('%(minutes)d minute(s) and %(seconds)d seconds') % {
84
                    'minutes': minutes, 'seconds': seconds}
85
    return _('%d seconds') % seconds
86

  
87

  
88
def get_stats_criteria(request, parsed_values=None):
89
    """
90
    Parses the request query string and returns a list of criterias suitable
91
    for select() usage. The parsed_values parameter can be given a dictionary,
92
    to be filled with the parsed values.
93
    """
94
    format_string = misc.date_format()
95
    criterias = [NotEqual('status', 'draft')]
96
    try:
97
        period_start = time.strptime(request.form.get('start'), format_string)
98
        criterias.append(GreaterOrEqual('receipt_time', period_start))
99
        if parsed_values is not None:
100
            parsed_values['period_start'] = datetime.datetime.fromtimestamp(time.mktime(period_start))
101
    except (ValueError, TypeError):
102
        pass
103

  
104
    try:
105
        period_end = time.strptime(request.form.get('end'), format_string)
106
        criterias.append(LessOrEqual('receipt_time', period_end))
107
        if parsed_values is not None:
108
            parsed_values['period_end'] = datetime.datetime.fromtimestamp(time.mktime(period_end))
109
    except (ValueError, TypeError):
110
        pass
111

  
112
    return criterias
113

  
114

  
115
def do_graphs_section(period_start=None, period_end=None, criterias=None):
116
    from wcs import sql
117
    r = TemplateIO(html=True)
118
    monthly_totals = sql.get_monthly_totals(period_start, period_end, criterias)[-12:]
119
    yearly_totals = sql.get_yearly_totals(period_start, period_end, criterias)[-10:]
120

  
121
    if not monthly_totals:
122
        monthly_totals = [('%s-%s' % datetime.date.today().timetuple()[:2], 0)]
123
    if not yearly_totals:
124
        yearly_totals = [(datetime.date.today().year, 0)]
125

  
126
    weekday_totals = sql.get_weekday_totals(period_start, period_end, criterias)
127
    weekday_line = []
128
    weekday_names = [_('Sunday'), _('Monday'), _('Tuesday'),
129
            _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
130
    for weekday, total in weekday_totals:
131
        label = weekday_names[weekday]
132
        weekday_line.append((label, total))
133
    # move Sunday to the last place
134
    weekday_line = weekday_line[1:] + [weekday_line[0]]
135

  
136
    hour_totals = sql.get_hour_totals(period_start, period_end, criterias)
137

  
138
    r += htmltext('''<script>
139
var weekday_line = %(weekday_line)s;
140
var hour_line = %(hour_line)s;
141
var month_line = %(month_line)s;
142
var year_line = %(year_line)s;
143
</script>''' % {
144
        'weekday_line': json.dumps(weekday_line),
145
        'hour_line': json.dumps(hour_totals),
146
        'month_line': json.dumps(monthly_totals),
147
        'year_line': json.dumps(yearly_totals),
148
        })
149

  
150
    if len(yearly_totals) > 1:
151
        r += htmltext('<h3>%s</h3>') % _('Submissions by year')
152
        r += htmltext('<div id="chart_years" style="height:160px; width:100%;"></div>')
153

  
154
    r += htmltext('<h3>%s</h3>') % _('Submissions by month')
155
    r += htmltext('<div id="chart_months" style="height:160px; width:100%;"></div>')
156
    r += htmltext('<h3>%s</h3>') % _('Submissions by weekday')
157
    r += htmltext('<div id="chart_weekdays" style="height:160px; width:100%;"></div>')
158
    r += htmltext('<h3>%s</h3>') % _('Submissions by hour')
159
    r += htmltext('<div id="chart_hours" style="height:160px; width:100%;"></div>')
160

  
161

  
162
    get_response().add_javascript(['jquery.js', 'jqplot/jquery.jqplot.min.js',
163
        'jqplot/plugins/jqplot.canvasTextRenderer.min.js',
164
        'jqplot/plugins/jqplot.canvasAxisLabelRenderer.min.js',
165
        'jqplot/plugins/jqplot.canvasAxisTickRenderer.min.js',
166
        'jqplot/plugins/jqplot.categoryAxisRenderer.min.js',
167
        'jqplot/plugins/jqplot.barRenderer.min.js',
168
        ])
169

  
170
    get_response().add_javascript_code('''
171
function wcs_draw_graphs() {
172
$.jqplot ('chart_weekdays', [weekday_line], {
173
series:[{renderer:$.jqplot.BarRenderer}],
174
axesDefaults: {
175
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
176
tickOptions: { angle: -30, }
177
},
178
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer } }
179
});
180

  
181
$.jqplot ('chart_hours', [hour_line], {
182
axesDefaults: {
183
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
184
tickOptions: { angle: -30, }
185
},
186
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
187
});
188

  
189
$.jqplot ('chart_months', [month_line], {
190
axesDefaults: {
191
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
192
tickOptions: { angle: -30, }
193
},
194
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
195
});
196

  
197
if ($('#chart_years').length) {
198
$.jqplot ('chart_years', [year_line], {
199
series:[{renderer:$.jqplot.BarRenderer}],
200
axesDefaults: {
201
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
202
tickOptions: { angle: -30, }
203
},
204
axes: { xaxis: { renderer: $.jqplot.CategoryAxisRenderer }, yaxis: {min: 0} }
205
});
206
}
207
}
208

  
209
$(document).ready(function(){
210
  wcs_draw_graphs();
211
});
212
    ''')
213
    return r.getvalue()
42
from . import management
214 43

  
215 44

  
216 45
class RootDirectory(BackofficeRootDirectory):
217
    _q_exports = ['', 'management', 'pending', 'statistics']
46
    _q_exports = ['', 'pending', 'statistics']
218 47

  
219 48
    bounces = wcs.admin.bounces.BouncesDirectory()
220 49
    categories = wcs.admin.categories.CategoriesDirectory()
......
224 53
    settings = wcs.admin.settings.SettingsDirectory()
225 54
    users = wcs.admin.users.UsersDirectory()
226 55
    workflows = wcs.admin.workflows.WorkflowsDirectory()
56
    management = management.ManagementDirectory()
227 57

  
228 58
    menu_items = [
229
        ('management', N_('Management')),
59
        ('management/', N_('Management')),
230 60
        ('forms/', N_('Forms Workshop')),
231 61
        ('workflows/', N_('Workflows Workshop')),
232 62
        ('users/', N_('Users')),
......
235 65
        ('logger/', N_('Logs'), logger.is_visible),
236 66
        ('bounces/', N_('Bounces'), bounces.is_visible),
237 67
        ('settings/', N_('Settings')),
238
        ('/', N_('WCS Form Server'))
239 68
    ]
240 69

  
241 70
    def _q_traverse(self, path):
......
329 158

  
330 159
        return r.getvalue()
331 160

  
332

  
333 161
    def get_sidebar(self):
334 162
        from qommon.admin.menu import get_vc_version
335 163
        from wcs.admin.root import gpl
......
337 165

  
338 166
        r += htmltext('<div class="bo-block">')
339 167
        r += htmltext('<ul id="sidebar-actions">')
340
        r += htmltext('<li><a href="statistics">%s</a></li>') % _('Global statistics')
168
        r += htmltext('<li><a href="management/statistics">%s</a></li>') % _('Global statistics')
341 169
        r += htmltext('</ul>')
342 170
        r += htmltext('</div>')
343 171

  
......
355 183

  
356 184
        return r.getvalue()
357 185

  
358
    def management(self):
359
        get_response().breadcrumb.append(('management', _('Management')))
360
        html_top('management', _('Management'))
361
        get_response().filter['sidebar'] = self.get_sidebar()
362
        r = TemplateIO(html=True)
363

  
364
        user = get_request().user
365

  
366
        forms_without_pending_stuff = []
367
        forms_with_pending_stuff = []
368

  
369
        def append_form_entry(formdef):
370
            formdef_data_class = formdef.data_class()
371
            count_forms = formdef_data_class.count() - len(formdef_data_class.get_ids_with_indexed_value('status', 'draft'))
372
            not_endpoint_status = formdef.workflow.get_not_endpoint_status()
373
            not_endpoint_status_ids = ['wf-%s' % x.id for x in not_endpoint_status]
374
            pending_forms = []
375
            for status in not_endpoint_status_ids:
376
                pending_forms.extend(formdef_data_class.get_ids_with_indexed_value(
377
                                        'status', status))
378

  
379
            if formdef.acl_read != 'all' and pending_forms:
380
                concerned_ids = set()
381
                formdata_class = formdef.data_class()
382
                user_roles = set(user.roles or [])
383
                for role in user_roles:
384
                    concerned_ids |= set(formdata_class.get_ids_with_indexed_value(
385
                        'concerned_roles', str(role)))
386
                pending_forms = set(pending_forms).intersection(concerned_ids)
387

  
388
            if len(pending_forms) == 0:
389
                forms_without_pending_stuff.append((formdef, len(pending_forms), count_forms))
390
            else:
391
                forms_with_pending_stuff.append((formdef, len(pending_forms), count_forms))
392

  
393
        if user:
394
            for formdef in FormDef.select(order_by='name', ignore_errors=True):
395
                if formdef.disabled:
396
                    continue
397
                if user.is_admin or formdef.is_of_concern_for_user(user):
398
                    append_form_entry(formdef)
399

  
400
        if forms_with_pending_stuff:
401
            r += htmltext('<div class="bo-block" id="forms-in-your-care">')
402
            r += htmltext('<h2>%s</h2>') % _('Forms in your care')
403
            r += self.display_forms(forms_with_pending_stuff)
404
            r += htmltext('</div>')
405

  
406
        if forms_without_pending_stuff:
407
            r += htmltext('<div class="bo-block" id="other-forms">')
408
            r += htmltext('<h2>%s</h2>') % _('Other Forms')
409
            r += self.display_forms(forms_without_pending_stuff)
410
            r += htmltext('</div>')
411

  
412
        return r.getvalue()
413

  
414

  
415
    def get_stats_sidebar(self):
416
        get_response().add_javascript(['jquery.js'])
417
        DateWidget.prepare_javascript()
418
        form = Form(use_tokens=False)
419
        form.add(DateWidget, 'start', title=_('Start Date'))
420
        form.add(DateWidget, 'end', title=_('End Date'))
421
        form.add_submit('submit', _('Submit'))
422

  
423
        r = TemplateIO(html=True)
424
        r += htmltext('<h3>%s</h3>') % _('Period')
425
        r += form.render()
426

  
427
        r += htmltext('<h3>%s</h3>') % _('Shortcuts')
428
        r += htmltext('<ul>') # presets
429
        current_month_start = datetime.datetime.now().replace(day=1)
430
        start = current_month_start.strftime(misc.date_format())
431
        r += htmltext(' <li><a href="?start=%s">%s</a>') % (start, _('Current Month'))
432
        previous_month_start = current_month_start - datetime.timedelta(days=2)
433
        previous_month_start = previous_month_start.replace(day=1)
434
        start = previous_month_start.strftime(misc.date_format())
435
        end = current_month_start.strftime(misc.date_format())
436
        r += htmltext(' <li><a href="?start=%s&end=%s">%s</a>') % (
437
                start, end, _('Previous Month'))
438

  
439
        current_year_start = datetime.datetime.now().replace(month=1, day=1)
440
        start = current_year_start.strftime(misc.date_format())
441
        r += htmltext(' <li><a href="?start=%s">%s</a>') % (start, _('Current Year'))
442
        previous_year_start = current_year_start.replace(year=current_year_start.year-1)
443
        start = previous_year_start.strftime(misc.date_format())
444
        end = current_year_start.strftime(misc.date_format())
445
        r += htmltext(' <li><a href="?start=%s&end=%s">%s</a>') % (
446
                start, end, _('Previous Year'))
447

  
448
        return r.getvalue()
449

  
450

  
451
    def statistics(self):
452
        html_top('management', _('Global statistics'))
453
        get_response().breadcrumb.append(('statistics', _('Global statistics')))
454
        get_response().filter['sidebar'] = self.get_stats_sidebar()
455
        r = TemplateIO(html=True)
456
        r += htmltext('<h2>%s</h2>') % _('Global statistics')
457

  
458
        formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
459
                order_by='name', ignore_errors=True)
460

  
461
        counts = {}
462
        parsed_values = {}
463
        criterias = get_stats_criteria(get_request(), parsed_values)
464
        for formdef in formdefs:
465
            values = formdef.data_class().select(criterias)
466
            counts[formdef.id] = len(values)
467

  
468
        do_graphs = False
469
        if get_publisher().is_using_postgresql() and \
470
                get_publisher().get_site_option('postgresql_views') != 'false':
471
            do_graphs = True
472

  
473
        r += htmltext('<p>%s %s</p>') % (_('Total count:'), sum(counts.values()))
474

  
475
        if do_graphs:
476
            r += htmltext('<div class="splitcontent-left">')
477
        cats = Category.select()
478
        for cat in cats:
479
            category_formdefs = [x for x in formdefs if x.category_id == cat.id]
480
            r += self.category_global_stats(cat.name, category_formdefs, counts)
481

  
482
        category_formdefs = [x for x in formdefs if x.category_id is None]
483
        r += self.category_global_stats(_('Misc'), category_formdefs, counts)
484

  
485
        if do_graphs:
486
            r += htmltext('</div>')
487
            r += htmltext('<div class="splitcontent-right">')
488
            period_start = parsed_values.get('period_start')
489
            period_end = parsed_values.get('period_end')
490
            r += do_graphs_section(period_start, period_end)
491
            r += htmltext('</div>')
492

  
493
        return r.getvalue()
494

  
495

  
496
    def category_global_stats(self, title, category_formdefs, counts):
497
        r = TemplateIO(html=True)
498
        category_formdefs_ids = [x.id for x in category_formdefs]
499
        if not category_formdefs:
500
            return
501
        cat_counts = dict([(x, y) for x, y in counts.items() if x in
502
            category_formdefs_ids])
503
        if sum(cat_counts.values()) == 0:
504
            return
505
        r += htmltext('<div class="bo-block">')
506
        r += htmltext('<h3>%s</h3>') % title
507
        r += htmltext('<p>%s %s</p>') % (_('Count:'), sum(cat_counts.values()))
508
        r += htmltext('<ul>')
509
        for category_formdef in category_formdefs:
510
            if not counts.get(category_formdef.id):
511
                continue
512
            r += htmltext('<li>%s %s</li>') % (
513
                    _('%s:') % category_formdef.name,
514
                    counts.get(category_formdef.id))
515
        r += htmltext('</ul>')
516
        r += htmltext('</div>')
517
        return r.getvalue()
518

  
519

  
520
    def display_forms(self, forms_list):
521
        r = TemplateIO(html=True)
522
        r += htmltext('<ul>')
523
        cats = Category.select(order_by = 'name')
524
        for c in cats + [None]:
525
            if c is None:
526
                l2 = [x for x in forms_list if not x[0].category_id]
527
                cat_name = _('Misc')
528
            else:
529
                l2 = [x for x in forms_list if x[0].category_id == c.id]
530
                cat_name = c.name
531
            if not l2:
532
                continue
533
            r += htmltext('<li>%s</li>') % cat_name
534
            r += htmltext('<ul>')
535
            for formdef, no_pending, no_total in l2:
536
                r += htmltext('<li><a href="%s/">%s</a>') % (formdef.url_name, formdef.name)
537
                if no_pending:
538
                    r += _(': %(pending)s open on %(total)s') % {'pending': no_pending,
539
                                                        'total': no_total}
540
                else:
541
                    r += _(': %(total)s items') % {'total': no_total}
542
                r += htmltext('</li>')
543
            r += htmltext('</ul>')
544
        r += htmltext('</ul>')
545
        return r.getvalue()
546

  
547 186
    def pending(self):
548 187
        # kept as a redirection for compatibility with possible bookmarks
549 188
        return redirect('.')
550 189

  
190
    def statistics(self):
191
        return redirect('management/statistics')
192

  
551 193
    def _q_lookup(self, component):
552 194
        if component in [str(x[0]).strip('/') for x in self.menu_items]:
553 195
            if not self.is_accessible(component):
554 196
                raise errors.AccessForbiddenError()
555 197
            return getattr(self, component)
556
        return FormPage(component)
198
        if FormDef.has_key(component):
199
            # keep compatibility with previous versions, redirect from legacy
200
            # URL to new ones under management/
201
            return redirect('management/%s/' % component)
202
        return super(RootDirectory, self)._q_lookup(component)
557 203

  
558 204
    def get_menu_items(self):
559 205
        if not get_request().user:
......
590 236
                    'categories', 'settings', 'management'):
591 237
                menu_items[-1]['icon'] = k.strip('/')
592 238
        return menu_items
593

  
594
class FakeField(object):
595
    def __init__(self, id, type_, label):
596
        self.id = id
597
        self.type = type_
598
        self.label = label
599
        self.fake = True
600
        self.varname = None
601

  
602
    def get_view_value(self, value):
603
        # just here to quack like a duck
604
        return None
605

  
606
    def get_csv_heading(self):
607
        return [self.label]
608

  
609
    def get_csv_value(self, element, hint=None):
610
        return [element]
611

  
612
class FormPage(Directory):
613
    _q_exports = ['', 'csv', 'stats', 'xls', 'ods', 'json', 'pending', 'export']
614

  
615
    def __init__(self, component):
616
        try:
617
            self.formdef = FormDef.get_by_urlname(component)
618
        except KeyError:
619
            raise errors.TraversalError()
620

  
621
        session = get_session()
622
        user = get_request().user
623
        if user is None and get_publisher().user_class.count() == 0:
624
            user = get_publisher().user_class()
625
            user.is_admin = True
626
        if not user:
627
            raise errors.AccessUnauthorizedError()
628
        if not user.is_admin and not self.formdef.is_of_concern_for_user(user):
629
            if session.user:
630
                raise errors.AccessForbiddenError()
631
            else:
632
                raise errors.AccessUnauthorizedError()
633
        get_response().breadcrumb.append( (component + '/', self.formdef.name) )
634

  
635
    def get_formdata_sidebar(self, qs=''):
636
        r = TemplateIO(html=True)
637
        r += htmltext('<ul id="sidebar-actions">')
638
        #' <li><a href="list%s">%s</a></li>' % (qs, _('List of results'))
639
        r += htmltext(' <li><a data-base-href="ods" href="ods%s">%s</a></li>') % (
640
                qs, _('Open Document Format Export'))
641
        r += htmltext(' <li><a data-base-href="csv" href="csv%s">%s</a></li>') % (
642
                qs, _('CSV Export'))
643
        if xlwt:
644
            r += htmltext('<li><a data-base-href="xls" href="xls%s">%s</a></li>') % (
645
                    qs, _('Excel Export'))
646
        r += htmltext(' <li><a href="stats">%s</a></li>') % _('Statistics')
647
        r += htmltext('</ul>')
648
        return r.getvalue()
649

  
650
    def get_filter_sidebar(self, selected_filter=None, mode='listing'):
651
        r = TemplateIO(html=True)
652

  
653
        waitpoint_status = self.formdef.workflow.get_waitpoint_status()
654
        period_fake_fields = [
655
            FakeField('start', 'period-date', _('Start')),
656
            FakeField('end', 'period-date', _('End')),
657
        ]
658
        filter_fields = []
659
        for field in period_fake_fields + self.get_formdef_fields():
660
            field.enabled = False
661
            if field.type not in ('item', 'period-date', 'status'):
662
                continue
663
            if field.type == 'status' and not waitpoint_status:
664
                continue
665
            filter_fields.append(field)
666

  
667
            if get_request().form:
668
                field.enabled = 'filter-%s' % field.id in get_request().form
669
            else:
670
                if mode == 'listing':
671
                    # enable status filter by default
672
                    field.enabled = (field.id in ('status',))
673
                elif mode == 'stats':
674
                    # enable period filters by default
675
                    field.enabled = (field.id in ('start', 'end'))
676

  
677
        r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % (
678
            _('Filters'), _('change')))
679

  
680
        for filter_field in filter_fields:
681
            if not filter_field.enabled:
682
                continue
683

  
684
            filter_field_key = 'filter-%s-value' % filter_field.id
685
            filter_field_value = get_request().form.get(filter_field_key)
686

  
687
            if filter_field.type == 'status':
688
                r += htmltext('<div class="widget">')
689
                r += htmltext('<div class="title">%s</div>') % _('Status to display')
690
                r += htmltext('<div class="content">')
691
                r += htmltext('<select name="filter">')
692
                filters = [('all', _('All'), None),
693
                        ('pending', _('Pending'), None),
694
                        ('done', _('Done'), None)]
695
                for status in waitpoint_status:
696
                    filters.append((status.id, status.name, status.colour))
697
                for filter_id, filter_label, filter_colour in filters:
698
                    if filter_id == selected_filter:
699
                        selected = ' selected="selected"'
700
                    else:
701
                        selected = ''
702
                    style = ''
703
                    if filter_colour and filter_colour != 'FFFFFF':
704
                        fg_colour = misc.get_foreground_colour(filter_colour)
705
                        style = 'style="background: #%s; color: %s;"' % (
706
                                filter_colour, fg_colour)
707
                    r += htmltext('<option value="%s"%s %s>' % (filter_id, selected, style))
708
                    r += htmltext('%s</option>') % filter_label
709
                r += htmltext('</select>')
710
                r += htmltext('</div>')
711
                r += htmltext('</div>')
712

  
713
            elif filter_field.type == 'period-date':
714
                r += DateWidget(filter_field_key, title=filter_field.label,
715
                        value=filter_field_value, render_br=False).render()
716

  
717
            elif filter_field.type == 'item':
718
                filter_field.required = False
719
                options = filter_field.get_options()
720
                if options:
721
                    r += SingleSelectWidget(filter_field_key, title=filter_field.label,
722
                            options=options, value=filter_field_value,
723
                            render_br=False).render()
724
                else:
725
                    # There may be no options because the field is using
726
                    # a jsonp data source, or a json source using a
727
                    # parametrized URL depending on unavailable variables.
728
                    #
729
                    # In that case fall back on a string widget.
730
                    r += StringWidget(filter_field_key, title=filter_field.label,
731
                            value=filter_field_value, render_br=False).render()
732

  
733
        # field filter dialog content
734
        r += htmltext('<div style="display: none;">')
735
        r += htmltext('<ul id="field-filter">')
736
        for field in filter_fields:
737
            r += htmltext('<li><input type="checkbox" name="filter-%s"') % field.id
738
            if field.enabled:
739
                r += htmltext(' checked="checked"')
740
            r += htmltext(' id="fields-filter-%s"') % field.id
741
            r += htmltext('/>')
742
            r += htmltext('<label for="fields-filter-%s">%s</label>') % (
743
                    field.id, misc.ellipsize(field.label, 70))
744
            r += htmltext('</li>')
745
        r += htmltext('</ul>')
746
        r += htmltext('</div>')
747

  
748
        return r.getvalue()
749

  
750
    def get_fields_sidebar(self, selected_filter, fields, offset=None,
751
            limit=None, order_by=None):
752
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
753
        get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
754

  
755
        r = TemplateIO(html=True)
756
        r += htmltext('<form id="listing-settings" action=".">')
757
        if offset or limit:
758
            if not offset:
759
                offset = 0
760
            r += htmltext('<input type="hidden" name="offset" value="%s"/>') % offset
761
        if limit:
762
            r += htmltext('<input type="hidden" name="limit" value="%s"/>') % limit
763

  
764
        if get_publisher().is_using_postgresql():
765
            if order_by is None:
766
                order_by = ''
767
            r += htmltext('<input type="hidden" name="order_by" value="%s"/>') % order_by
768

  
769
        if get_publisher().is_using_postgresql():
770
            r += htmltext('<h3>%s</h3>') % _('Search')
771
            if get_request().form.get('q'):
772
                q = get_request().form.get('q')
773
                if type(q) is not unicode:
774
                    q = unicode(q, get_publisher().site_charset)
775
                r += htmltext('<input name="q" value="%s">') % q.encode(get_publisher().site_charset)
776
            else:
777
                r += htmltext('<input name="q">')
778
            r += htmltext('<input type="submit" value="%s"/>') % _('Search')
779

  
780
        r += self.get_filter_sidebar(selected_filter=selected_filter)
781

  
782
        r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
783

  
784
        r += htmltext('<button id="columns-settings">%s</button>') % _('Columns Settings')
785

  
786
        # column settings dialog content
787
        r += htmltext('<div style="display: none;">')
788
        r += htmltext('<ul id="columns-filter">')
789
        for field in self.get_formdef_fields():
790
            if not hasattr(field, str('get_view_value')):
791
                continue
792
            r += htmltext('<li><input type="checkbox" name="%s"') % field.id
793
            if field.id in [x.id for x in fields]:
794
                r += htmltext(' checked="checked"')
795
            r += htmltext(' id="fields-column-%s"') % field.id
796
            r += htmltext('/>')
797
            r += htmltext('<label for="fields-column-%s">%s</label>') % (
798
                    field.id, misc.ellipsize(field.label, 70))
799
            r += htmltext('</li>')
800
        r += htmltext('</ul>')
801
        r += htmltext('</div>')
802
        r += htmltext('</form>')
803
        return r.getvalue()
804

  
805
    def get_formdef_fields(self):
806
        fields = []
807
        fields.append(FakeField('id', 'id', _('Identifier')))
808
        fields.append(FakeField('time', 'time', _('Time')))
809
        fields.append(FakeField('user-label', 'user-label', _('User Label')))
810
        fields.extend(self.formdef.fields)
811
        fields.append(FakeField('status', 'status', _('Status')))
812
        fields.append(FakeField('anonymised', 'anonymised', _('Anonymised')))
813

  
814
        return fields
815

  
816
    def get_fields_from_query(self):
817
        field_ids = [x for x in get_request().form.keys()]
818
        if not field_ids:
819
            field_ids = ['id', 'time', 'user-label']
820
            for field in self.formdef.fields:
821
                if hasattr(field, str('get_view_value')) and field.in_listing:
822
                    field_ids.append(field.id)
823
            field_ids.append('status')
824

  
825
        fields = []
826
        for field in self.get_formdef_fields():
827
            if field.id in field_ids:
828
                fields.append(field)
829

  
830
        return fields
831

  
832
    def get_filter_from_query(self, default='pending'):
833
        if 'filter' in get_request().form:
834
            return get_request().form['filter']
835
        if self.formdef.workflow.possible_status:
836
            return default
837
        return 'all'
838

  
839
    def get_criterias_from_query(self):
840
        period_fake_fields = [
841
            FakeField('start', 'period-date', _('Start')),
842
            FakeField('end', 'period-date', _('End')),
843
        ]
844
        filter_fields = []
845
        criterias = []
846
        format_string = misc.date_format()
847
        for filter_field in period_fake_fields + self.get_formdef_fields():
848
            if filter_field.type not in ('item', 'period-date'):
849
                continue
850

  
851
            filter_field_key = None
852

  
853
            if filter_field.varname:
854
                # if this is a field with a varname and filter-%(varname)s is
855
                # present in the query string, enable this filter.
856
                if get_request().form.get('filter-%s' % filter_field.varname):
857
                    filter_field_key = 'filter-%s' % filter_field.varname
858

  
859
            if get_request().form.get('filter-%s' % filter_field.id):
860
                # if there's a filter-%(id)s, it is used to enable the actual
861
                # filter, and the value will be found in filter-%s-value.
862
                filter_field_key = 'filter-%s-value' % filter_field.id
863

  
864
            if not filter_field_key:
865
                # if there's not known filter key, skip.
866
                continue
867

  
868
            filter_field_value = get_request().form.get(filter_field_key)
869
            if not filter_field_value:
870
                continue
871

  
872
            if filter_field.id == 'start':
873
                period_start = time.strptime(filter_field_value, format_string)
874
                criterias.append(GreaterOrEqual('receipt_time', period_start))
875
            elif filter_field.id == 'end':
876
                period_end = time.strptime(filter_field_value, format_string)
877
                criterias.append(LessOrEqual('receipt_time', period_end))
878
            elif filter_field.type == 'item' and filter_field_value not in (None, 'None'):
879
                criterias.append(Equal('f%s' % filter_field.id, filter_field_value))
880

  
881
        return criterias
882

  
883

  
884
    def _q_index(self):
885
        get_logger().info('backoffice - form %s - listing' % self.formdef.name)
886

  
887
        fields = self.get_fields_from_query()
888
        selected_filter = self.get_filter_from_query()
889
        criterias = self.get_criterias_from_query()
890

  
891
        if get_publisher().is_using_postgresql():
892
            # only enable pagination in SQL mode, as we do not have sorting in
893
            # the other case.
894
            limit = get_request().form.get('limit', 20)
895
        else:
896
            limit = get_request().form.get('limit', 0)
897
        offset = get_request().form.get('offset', 0)
898
        order_by = get_request().form.get('order_by', None)
899
        query = get_request().form.get('q')
900

  
901
        qs = ''
902
        if get_request().get_query():
903
            qs = '?' + get_request().get_query()
904

  
905
        table = FormDefUI(self.formdef).listing(fields=fields,
906
                        selected_filter=selected_filter, include_form=True,
907
                        limit=int(limit), offset=int(offset), query=query,
908
                        order_by=order_by, criterias=criterias)
909

  
910
        if get_request().form.get('ajax') == 'true':
911
            get_response().filter = None
912
            return table
913

  
914
        html_top('management', '%s - %s' % (_('Listing'), self.formdef.name))
915
        r = TemplateIO(html=True)
916
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Listing'))
917
        r += table
918

  
919
        get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
920
                self.get_fields_sidebar(selected_filter, fields, limit=limit,
921
                        offset=offset, order_by=order_by)
922

  
923
        return r.getvalue()
924

  
925
    def pending(self):
926
        get_logger().info('backoffice - form %s - pending' % self.formdef.name)
927
        get_response().breadcrumb.append( ('pending', _('Pending Forms')) )
928
        html_top('management', '%s - %s' % (_('Pending Forms'), self.formdef.name))
929
        r = TemplateIO(html=True)
930
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Pending Forms'))
931

  
932
        not_endpoint_status = [('wf-%s' % x.id, x.name) for x in \
933
                                  self.formdef.workflow.get_not_endpoint_status()]
934

  
935
        nb_status = len(not_endpoint_status)
936
        column2 = nb_status/2
937
        r += htmltext('<div class="splitcontent-left">')
938
        for i, (status_id, status_label) in enumerate(not_endpoint_status):
939
            if i > 0 and i == column2:
940
                r += htmltext('</div>')
941
                r += htmltext('<div class="splitcontent-right">')
942
            status_forms = self.formdef.data_class().get_with_indexed_value(
943
                        str('status'), status_id)
944
            if not status_forms:
945
                continue
946
            status_forms.sort(lambda x, y: cmp(getattr(x, str('receipt_time')),
947
                                    getattr(y, str('receipt_time'))))
948
            status_forms.reverse()
949
            r += htmltext('<div class="bo-block">')
950
            r += htmltext('<h3>%s</h3>') % _('Forms with status "%s"') % status_label
951

  
952
            r += htmltext('<ul>')
953
            for f in status_forms:
954
                try:
955
                    u = get_publisher().user_class.get(f.user_id)
956
                    userlabel = u.display_name
957
                except KeyError:
958
                    userlabel = _('unknown user')
959
                r += htmltext('<li><a href="%s/">%s, %s</a></li>') % (
960
                    f.id,
961
                    misc.localstrftime(f.receipt_time),
962
                    userlabel)
963
            r += htmltext('</ul>')
964
            r += htmltext('</div>')
965
        r += htmltext('</div>')
966

  
967
        r += htmltext('<p class="clear"><a href=".">%s</a></p>') % _('Back')
968
        return r.getvalue()
969

  
970
    def csv_tuple_heading(self, fields):
971
        heading_fields = [] # '#id', _('time'), _('userlabel'), _('status')]
972
        for field in fields:
973
            heading_fields.extend(field.get_csv_heading())
974
        return heading_fields
975

  
976
    def csv_tuple(self, fields, data, hint=None):
977
        elements = []
978
        for field in fields:
979
            if field.type == 'id':
980
                element = str(data.id)
981
            elif field.type == 'time':
982
                element = misc.localstrftime(data.receipt_time)
983
            elif field.type == 'user-label':
984
                try:
985
                    element = get_publisher().user_class.get(data.user_id).display_name
986
                except:
987
                    element = '-'
988
            elif field.type == 'status':
989
                element = data.get_status_label()
990
            else:
991
                element = data.data.get(field.id, '') or ''
992
            elements.extend(field.get_csv_value(element, hint=hint))
993
        return elements
994

  
995
    def csv(self):
996
        fields = self.get_fields_from_query()
997
        selected_filter = self.get_filter_from_query()
998
        user = get_request().user
999
        query = get_request().form.get('q')
1000

  
1001
        class Exporter(object):
1002
            def __init__(self, formpage, formdef, fields, selected_filter):
1003
                self.formpage = formpage
1004
                self.formdef = formdef
1005
                self.fields = fields
1006
                self.selected_filter = selected_filter
1007

  
1008
            def export(self, job=None):
1009
                self.output = cStringIO.StringIO()
1010
                csv_output = csv.writer(self.output)
1011

  
1012
                csv_output.writerow(self.formpage.csv_tuple_heading(self.fields))
1013

  
1014
                items, total_count = FormDefUI(self.formdef).get_listing_items(
1015
                        self.selected_filter, user=user, query=query)
1016

  
1017
                for filled in items:
1018
                    csv_output.writerow(self.formpage.csv_tuple(self.fields, filled))
1019

  
1020
                if job:
1021
                    job.file_content = self.output.getvalue()
1022
                    job.content_type = 'text/csv'
1023
                    job.store()
1024

  
1025
        get_logger().info('backoffice - form %s - listing csv' % self.formdef.name)
1026

  
1027
        count = self.formdef.data_class().count()
1028
        exporter = Exporter(self, self.formdef, fields, selected_filter)
1029
        if count > 100: # Arbitrary threshold
1030
            job = get_response().add_after_job(
1031
                str(N_('Exporting forms in CSV')),
1032
                exporter.export)
1033
            job.file_name = '%s.csv' % self.formdef.url_name
1034
            job.store()
1035
            return redirect('export?job=%s' % job.id)
1036
        else:
1037
            exporter.export()
1038

  
1039
            response = get_response()
1040
            response.set_content_type('text/plain')
1041
            #response.set_header('content-disposition', 'attachment; filename=%s.csv' % self.formdef.url_name)
1042
            return exporter.output.getvalue()
1043

  
1044
    def export(self):
1045
        if get_request().form.get('download'):
1046
            return self.export_download()
1047

  
1048
        try:
1049
            job = AfterJob.get(get_request().form.get('job'))
1050
        except KeyError:
1051
            return redirect('.')
1052

  
1053
        html_top('management', title=_('Exporting'))
1054
        r = TemplateIO(html=True)
1055
        r += get_session().display_message()
1056
        get_response().add_javascript(['jquery.js', 'afterjob.js'])
1057
        r += htmltext('<dl class="job-status">')
1058
        r += htmltext('<dt>')
1059
        r += _(job.label)
1060
        r += htmltext('</dt>')
1061
        r += htmltext('<dd>')
1062
        r += htmltext('<span class="afterjob" id="%s">') % job.id
1063
        r += _(job.status)
1064
        r += htmltext('</span>')
1065
        r += htmltext('</dd>')
1066
        r += htmltext('</dl>')
1067

  
1068
        r += htmltext('<div class="done">')
1069
        r += htmltext('<a download="%s" href="export?download=%s">%s</a>') % (
1070
                job.file_name, job.id, _('Download Export'))
1071
        r += htmltext('</div>')
1072
        return r.getvalue()
1073

  
1074
    def export_download(self):
1075
        try:
1076
            job = AfterJob.get(get_request().form.get('download'))
1077
        except KeyError:
1078
            return redirect('.')
1079

  
1080
        if not job.status == 'completed':
1081
            raise errors.TraversalError()
1082
        response = get_response()
1083
        response.set_content_type(job.content_type)
1084
        response.set_header('content-disposition',
1085
            'attachment; filename=%s' % job.file_name)
1086
        return job.file_content
1087

  
1088
    def xls(self):
1089
        if xlwt is None:
1090
            raise errors.TraversalError()
1091

  
1092
        fields = self.get_fields_from_query()
1093
        selected_filter = self.get_filter_from_query()
1094
        user = get_request().user
1095
        query = get_request().form.get('q')
1096

  
1097
        class Exporter(object):
1098
            def __init__(self, formpage, formdef, fields, selected_filter):
1099
                self.formpage = formpage
1100
                self.formdef = formdef
1101
                self.fields = fields
1102
                self.selected_filter = selected_filter
1103

  
1104
            def export(self, job=None):
1105
                w = xlwt.Workbook(encoding=get_publisher().site_charset)
1106
                ws = w.add_sheet('1')
1107

  
1108
                for i, f in enumerate(self.formpage.csv_tuple_heading(self.fields)):
1109
                    ws.write(0, i, f)
1110

  
1111
                items, total_count = FormDefUI(self.formdef).get_listing_items(
1112
                        self.selected_filter, user=user, query=query)
1113

  
1114
                for i, filled in enumerate(items):
1115
                    for j, elem in enumerate(self.formpage.csv_tuple(fields, filled)):
1116
                        if elem and len(elem) > 32767:
1117
                            # xls cells have a limit of 32767 characters, cut
1118
                            # it down.
1119
                            elem = elem[:32760] + ' [...]'
1120
                        ws.write(i+1, j, elem)
1121

  
1122
                self.output = cStringIO.StringIO()
1123
                w.save(self.output)
1124

  
1125
                if job:
1126
                    job.file_content = self.output.getvalue()
1127
                    job.content_type = 'application/vnd.ms-excel'
1128
                    job.store()
1129

  
1130
        get_logger().info('backoffice - form %s - as excel' % self.formdef.name)
1131

  
1132
        count = self.formdef.data_class().count()
1133
        exporter = Exporter(self, self.formdef, fields, selected_filter)
1134
        if count > 100: # Arbitrary threshold
1135
            job = get_response().add_after_job(
1136
                str(N_('Exporting forms in Excel format')),
1137
                exporter.export)
1138
            job.file_name = '%s.xls' % self.formdef.url_name
1139
            job.store()
1140
            return redirect('export?job=%s' % job.id)
1141
        else:
1142
            exporter.export()
1143

  
1144
            response = get_response()
1145
            response.set_content_type('application/vnd.ms-excel')
1146
            response.set_header('content-disposition', 'attachment; filename=%s.xls' % self.formdef.url_name)
1147
            return exporter.output.getvalue()
1148

  
1149
    def ods(self):
1150
        fields = self.get_fields_from_query()
1151
        selected_filter = self.get_filter_from_query()
1152
        user = get_request().user
1153
        query = get_request().form.get('q')
1154

  
1155
        class Exporter(object):
1156
            def __init__(self, formpage, formdef, fields, selected_filter):
1157
                self.formpage = formpage
1158
                self.formdef = formdef
1159
                self.fields = fields
1160
                self.selected_filter = selected_filter
1161

  
1162
            def export(self, job=None):
1163
                w = ods.Workbook(encoding=get_publisher().site_charset)
1164
                ws = w.add_sheet('1')
1165

  
1166
                for i, f in enumerate(self.formpage.csv_tuple_heading(self.fields)):
1167
                    ws.write(0, i, f)
1168

  
1169
                items, total_count = FormDefUI(self.formdef).get_listing_items(
1170
                        self.selected_filter, user=user, query=query)
1171

  
1172
                for i, filled in enumerate(items):
1173
                    for j, elem in enumerate(self.formpage.csv_tuple(fields, filled, hint='ods')):
1174
                        if type(elem) is str and '[download]' in elem:
1175
                            elem = elem.replace('[download]', filled.get_url(backoffice=True))
1176
                            ws.write(i+1, j, elem, hint='uri')
1177
                        else:
1178
                            ws.write(i+1, j, elem)
1179

  
1180
                self.output = cStringIO.StringIO()
1181
                w.save(self.output)
1182

  
1183
                if job:
1184
                    job.file_content = self.output.getvalue()
1185
                    job.content_type = 'application/vnd.oasis.opendocument.spreadsheet'
1186
                    job.store()
1187

  
1188
        get_logger().info('backoffice - form %s - as ods' % self.formdef.name)
1189

  
1190
        count = self.formdef.data_class().count()
1191
        exporter = Exporter(self, self.formdef, fields, selected_filter)
1192
        if count > 100: # Arbitrary threshold
1193
            job = get_response().add_after_job(
1194
                str(N_('Exporting forms in Open Document format')),
1195
                exporter.export)
1196
            job.file_name = '%s.ods' % self.formdef.url_name
1197
            job.store()
1198
            return redirect('export?job=%s' % job.id)
1199
        else:
1200
            exporter.export()
1201

  
1202
            response = get_response()
1203
            response.set_content_type('application/vnd.oasis.opendocument.spreadsheet')
1204
            response.set_header('content-disposition', 'attachment; filename=%s.ods' % self.formdef.url_name)
1205
            return exporter.output.getvalue()
1206

  
1207
    def json(self):
1208
        get_response().set_content_type('application/json')
1209
        from wcs.api import get_user_from_api_query_string
1210
        user = get_user_from_api_query_string() or get_request().user
1211
        selected_filter = self.get_filter_from_query(default='all')
1212
        criterias = self.get_criterias_from_query()
1213
        order_by = get_request().form.get('order_by', None)
1214
        query = get_request().form.get('q')
1215
        items, total_count = FormDefUI(self.formdef).get_listing_items(
1216
            selected_filter, user=user, query=query, criterias=criterias,
1217
            order_by=order_by)
1218
        if get_request().form.get('full') == 'on':
1219
            output = [json.loads(filled.export_to_json()) for filled in items]
1220
        else:
1221
            output = [{'id': filled.id,
1222
                'url': filled.get_url(),
1223
                'receipt_time': filled.receipt_time,
1224
                'last_update_time': filled.last_update_time} for filled in items]
1225
        return json.dumps(output,
1226
                cls=misc.JSONEncoder,
1227
                encoding=get_publisher().site_charset)
1228

  
1229
    def get_stats_sidebar(self, selected_filter):
1230
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
1231
        get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
1232
        r = TemplateIO(html=True)
1233
        r += htmltext('<form id="listing-settings" action="stats">')
1234
        r += self.get_filter_sidebar(selected_filter=selected_filter, mode='stats')
1235
        r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
1236
        if misc.can_decorate_as_pdf():
1237
            r += htmltext('<button class="pdf">%s</button>') % _('Download PDF')
1238
        r += htmltext('</form>')
1239
        return r.getvalue()
1240

  
1241
    def stats(self):
1242
        get_logger().info('backoffice - form %s - stats' % self.formdef.name)
1243
        html_top('management', '%s - %s' % (_('Form'), self.formdef.name))
1244
        r = TemplateIO(html=True)
1245
        get_response().breadcrumb.append( ('stats', _('Statistics')) )
1246

  
1247
        selected_filter = self.get_filter_from_query(default='all')
1248
        criterias = self.get_criterias_from_query()
1249
        get_response().filter['sidebar'] = self.get_formdata_sidebar() + \
1250
                self.get_stats_sidebar(selected_filter)
1251
        do_graphs = get_publisher().is_using_postgresql()
1252

  
1253
        if selected_filter and selected_filter != 'all':
1254
            if selected_filter == 'pending':
1255
                applied_filters = ['wf-%s' % x.id for x in
1256
                              self.formdef.workflow.get_not_endpoint_status()]
1257
            elif selected_filter == 'done':
1258
                applied_filters = ['wf-%s' % x.id for x in
1259
                              self.formdef.workflow.get_endpoint_status()]
1260
            else:
1261
                applied_filters = ['wf-%s' % selected_filter]
1262
            criterias.append(Or([Equal('status', x) for x in applied_filters]))
1263

  
1264
        values = self.formdef.data_class().select(criterias)
1265
        if get_publisher().is_using_postgresql():
1266
            # load all evolutions in a single batch, to avoid as many query as
1267
            # there are formdata when computing resolution times statistics.
1268
            self.formdef.data_class().load_all_evolutions(values)
1269

  
1270
        r += htmltext('<div id="statistics">')
1271
        if do_graphs:
1272
            r += htmltext('<div class="splitcontent-left">')
1273

  
1274
        no_forms = len(values)
1275
        r += htmltext('<div class="bo-block">')
1276
        r += htmltext('<p>%s %d</p>') % (_('Total number of records:'), no_forms)
1277

  
1278
        if self.formdef.workflow:
1279
            r += htmltext('<ul>')
1280
            for status in self.formdef.workflow.possible_status:
1281
                r += htmltext('<li>%s: %d</li>') % (status.name,
1282
                        len([x for x in values if x.status == 'wf-%s' % status.id]))
1283
            r += htmltext('</ul>')
1284
        r += htmltext('</div>')
1285

  
1286
        excluded_fields = []
1287
        for criteria in criterias:
1288
            if not isinstance(criteria, Equal):
1289
                continue
1290
            excluded_fields.append(criteria.attribute[1:])
1291

  
1292
        stats_for_fields = self.stats_fields(values,
1293
                excluded_fields=excluded_fields)
1294
        if stats_for_fields:
1295
            r += htmltext('<div class="bo-block">')
1296
            r += stats_for_fields
1297
            r += htmltext('</div>')
1298

  
1299
        stats_times = self.stats_resolution_time(values)
1300
        if stats_times:
1301
            r += htmltext('<div class="bo-block">')
1302
            r += stats_times
1303
            r += htmltext('</div>')
1304

  
1305
        if do_graphs:
1306
            r += htmltext('</div>')
1307
            r += htmltext('<div class="splitcontent-right">')
1308
            criterias.append(Equal('formdef_id', int(self.formdef.id)))
1309
            r += do_graphs_section(criterias=criterias)
1310
            r += htmltext('</div>')
1311

  
1312
        r += htmltext('</div>') # id="statistics"
1313

  
1314
        if get_request().form.get('ajax') == 'true':
1315
            get_response().filter = None
1316
            return r.getvalue()
1317

  
1318
        page = TemplateIO(html=True)
1319
        page += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Statistics'))
1320
        page += htmltext(r)
1321
        page += htmltext('<a class="back" href=".">%s</a>') % _('Back')
1322

  
1323
        if 'pdf' in get_request().form:
1324
            pdf_content = misc.decorate_as_pdf(page.getvalue())
1325
            response = get_response()
1326
            response.set_content_type('application/pdf')
1327
            return pdf_content
1328

  
1329
        return page.getvalue()
1330

  
1331
    def stats_fields(self, values, excluded_fields=None):
1332
        r = TemplateIO(html=True)
1333
        had_page = False
1334
        last_page = None
1335
        last_title = None
1336
        for f in self.formdef.fields:
1337
            if excluded_fields and f.id in excluded_fields:
1338
                continue
1339
            if f.type == 'page':
1340
                last_page = f.label
1341
                last_title = None
1342
                continue
1343
            if f.type == 'title':
1344
                last_title = f.label
1345
                continue
1346
            if not f.stats:
1347
                continue
1348
            t = f.stats(values)
1349
            if not t:
1350
                continue
1351
            if last_page:
1352
                if had_page:
1353
                    r += htmltext('</div>')
1354
                r += htmltext('<div class="page">')
1355
                r += htmltext('<h3>%s</h3>') % last_page
1356
                had_page = True
1357
                last_page = None
1358
            if last_title:
1359
                r += htmltext('<h3>%s</h3>') % last_title
1360
                last_title = None
1361
            r += t
1362

  
1363
        if had_page:
1364
            r += htmltext('</div>')
1365

  
1366
        return r.getvalue()
1367

  
1368
    def stats_resolution_time(self, values):
1369
        possible_status = [('wf-%s' % x.id, x.id) for x in self.formdef.workflow.possible_status]
1370

  
1371
        if len(possible_status) < 2:
1372
            return
1373

  
1374
        r = TemplateIO(html=True)
1375
        r += htmltext('<h2>%s</h2>') % _('Resolution time')
1376

  
1377
        for status, status_id in possible_status:
1378
            res_time_forms = [
1379
                    (time.mktime(x.evolution[-1].time) - time.mktime(x.receipt_time)) \
1380
                    for x in values if x.status == status and x.evolution]
1381
            if not res_time_forms:
1382
                continue
1383
            res_time_forms.sort()
1384
            sum_times = sum(res_time_forms)
1385
            len_times = len(res_time_forms)
1386
            r += htmltext('<h3>%s</h3>') % (_('To Status "%s"') % self.formdef.workflow.get_status(status_id).name)
1387
            r += htmltext('<ul>')
1388
            r += htmltext(' <li>%s %s</li>') % (_('Minimum Time:'), format_time(min(res_time_forms)))
1389
            r += htmltext(' <li>%s %s</li>') % (_('Maximum Time:'), format_time(max(res_time_forms)))
1390
            r += htmltext(' <li>%s %s</li>') % (_('Range:'), format_time(max(res_time_forms)-min(res_time_forms)))
1391
            mean = sum_times/len_times
1392
            r += htmltext(' <li>%s %s</li>') % (_('Mean:'), format_time(mean))
1393
            if len_times % 2:
1394
                median = res_time_forms[len_times/2]
1395
            else:
1396
                midpt = len_times/2
1397
                median = (res_time_forms[midpt-1]+res_time_forms[midpt])/2
1398
            r += htmltext(' <li>%s %s</li>') % (_('Median:'), format_time(median))
1399

  
1400
            # variance...
1401
            x = 0
1402
            for t in res_time_forms:
1403
                x += (t - mean)**2.0
1404
            try:
1405
                variance = x/(len_times+1)
1406
            except:
1407
                variance = 0
1408
            # not displayed since in square seconds which is not easy to grasp
1409

  
1410
            from math import sqrt
1411
            # and standard deviation
1412
            std_dev = sqrt(variance)
1413
            r += htmltext(' <li>%s %s</li>') % (_('Standard Deviation:'), format_time(std_dev))
1414

  
1415
            r += htmltext('</ul>')
1416

  
1417
        return r.getvalue()
1418

  
1419
    def _q_lookup(self, component):
1420
        try:
1421
            filled = self.formdef.data_class().get(component)
1422
        except KeyError:
1423
            raise errors.TraversalError()
1424

  
1425
        return FormBackOfficeStatusPage(self.formdef, filled)
1426

  
1427

  
1428

  
1429
class FormBackOfficeStatusPage(FormStatusPage):
1430
    def html_top(self, title = None):
1431
        return html_top('management', title)
1432

  
1433
    def _q_index(self):
1434
        get_response().add_javascript(['jquery.js', 'qommon.admin.js'])
1435
        return self.status()
wcs/formdef.py
331 331

  
332 332
    def get_url(self, backoffice = False):
333 333
        if backoffice:
334
            base_url = get_publisher().get_backoffice_url()
334
            base_url = get_publisher().get_backoffice_url() + '/management'
335 335
        else:
336 336
            base_url = get_publisher().get_frontoffice_url()
337 337
        return '%s/%s/' % (base_url, self.url_name)
338
-