0001-backoffice-move-management-of-submitted-forms-to-a-s.patch
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 |
- |