0001-misc-apply-pyupgrade-isort-black-56062.patch
bijoe/engine.py | ||
---|---|---|
17 | 17 |
import collections |
18 | 18 |
import contextlib |
19 | 19 |
import datetime |
20 |
import logging |
|
21 |
import itertools |
|
22 | 20 |
import hashlib |
21 |
import itertools |
|
22 |
import logging |
|
23 | 23 | |
24 | 24 |
import psycopg2 |
25 | ||
26 |
from django.core.cache import cache |
|
27 | 25 |
from django.conf import settings |
26 |
from django.core.cache import cache |
|
28 | 27 |
from django.utils.encoding import force_bytes, force_text |
29 | 28 |
from django.utils.six import python_2_unicode_compatible |
30 | 29 |
from django.utils.translation import ugettext_lazy as _ |
... | ... | |
63 | 62 |
return _('N/A') |
64 | 63 |
else: |
65 | 64 |
try: |
66 |
return (u'%4.2f' % float(value)).replace('.', ',') + u' %'
|
|
65 |
return ('%4.2f' % float(value)).replace('.', ',') + ' %'
|
|
67 | 66 |
except TypeError: |
68 | 67 |
return _('N/A') |
69 | 68 |
elif self.measure.type == 'duration': |
70 | 69 |
if value is None: |
71 |
return u'0'
|
|
70 |
return '0' |
|
72 | 71 |
else: |
73 |
s = u''
|
|
72 |
s = '' |
|
74 | 73 |
if value.days: |
75 |
s += u'%d jour(s)' % value.days
|
|
74 |
s += '%d jour(s)' % value.days |
|
76 | 75 |
if value.seconds // 3600: |
77 |
s += u' %d heure(s)' % (value.seconds // 3600)
|
|
76 |
s += ' %d heure(s)' % (value.seconds // 3600) |
|
78 | 77 |
if not s: |
79 |
s = u'moins d\'1 heure'
|
|
78 |
s = 'moins d\'1 heure' |
|
80 | 79 |
return s |
81 | 80 |
elif self.measure.type == 'bool': |
82 | 81 |
if value is None: |
... | ... | |
99 | 98 |
def __new__(cls, dimensions=[], measures=[]): |
100 | 99 |
dimensions = list(dimensions) |
101 | 100 |
measures = list(measures) |
102 |
return super(Cells, cls).__new__(cls, dimensions, measures)
|
|
101 |
return super().__new__(cls, dimensions, measures) |
|
103 | 102 | |
104 | 103 | |
105 | 104 |
def quote(s): |
... | ... | |
126 | 125 |
Member = collections.namedtuple('Member', ['id', 'label']) |
127 | 126 | |
128 | 127 | |
129 |
class EngineDimension(object):
|
|
128 |
class EngineDimension: |
|
130 | 129 |
def __init__(self, engine, engine_cube, dimension): |
131 | 130 |
self.engine = engine |
132 | 131 |
self.engine_cube = engine_cube |
... | ... | |
148 | 147 |
cache_key = self.cache_key(filters) |
149 | 148 |
members = cache.get(cache_key) |
150 | 149 |
if members is not None: |
151 |
self.engine.log.debug('MEMBERS: (from cache) dimension %s.%s filters=%s: %s', |
|
152 |
self.engine_cube.name, self.name, filters, |
|
153 |
members) |
|
150 |
self.engine.log.debug( |
|
151 |
'MEMBERS: (from cache) dimension %s.%s filters=%s: %s', |
|
152 |
self.engine_cube.name, |
|
153 |
self.name, |
|
154 |
filters, |
|
155 |
members, |
|
156 |
) |
|
154 | 157 |
return members |
155 | 158 | |
156 | 159 |
members = [] |
... | ... | |
184 | 187 |
table_expression = '%s' % self.engine_cube.fact_table |
185 | 188 |
if joins: |
186 | 189 |
table_expression = self.engine_cube.build_table_expression( |
187 |
joins, self.engine_cube.fact_table, force_join='right') |
|
190 |
joins, self.engine_cube.fact_table, force_join='right' |
|
191 |
) |
|
188 | 192 |
sql = 'SELECT %s AS value, %s::text AS label ' % (value, value_label) |
189 | 193 |
sql += 'FROM %s ' % table_expression |
190 | 194 |
if order_by: |
... | ... | |
199 | 203 |
if order_value not in group_by: |
200 | 204 |
group_by.append(order_value) |
201 | 205 |
if conditions: |
202 |
sql += 'WHERE %s ' % (' AND '.join(conditions))
|
|
206 |
sql += 'WHERE %s ' % ' AND '.join(conditions)
|
|
203 | 207 |
sql += 'GROUP BY %s ' % ', '.join(group_by) |
204 | 208 |
sql += 'ORDER BY (%s) ' % ', '.join(order_by) |
205 | 209 |
sql = sql.format(fact_table=self.engine_cube.fact_table) |
... | ... | |
210 | 214 |
continue |
211 | 215 |
members.append(Member(id=row[0], label=force_text(row[1]))) |
212 | 216 |
cache.set(cache_key, members, 600) |
213 |
self.engine.log.debug('MEMBERS: dimension %s.%s filters=%s: %s',
|
|
214 |
self.engine_cube.name, self.name, filters,
|
|
215 |
members)
|
|
217 |
self.engine.log.debug( |
|
218 |
'MEMBERS: dimension %s.%s filters=%s: %s', self.engine_cube.name, self.name, filters, members
|
|
219 |
) |
|
216 | 220 |
return members |
217 | 221 | |
218 | 222 | |
219 | 223 |
class SchemaJSONDimension(schemas.Dimension): |
220 | 224 |
'''Generated dimensions for JSON fields keys''' |
225 | ||
221 | 226 |
filter = False |
222 | 227 |
order_by = None |
223 | 228 |
group_by = None |
... | ... | |
225 | 230 |
type = 'string' |
226 | 231 | |
227 | 232 |
def __init__(self, json_field, name): |
228 |
super(SchemaJSONDimension, self).__init__()
|
|
233 |
super().__init__() |
|
229 | 234 |
name = str(name) |
230 | 235 |
self.name = name |
231 | 236 |
self.label = name.title() |
... | ... | |
233 | 238 |
self.value_label = expr |
234 | 239 |
self.value = expr |
235 | 240 |
self.join = ['json_' + name] |
236 |
sql = ('SELECT DISTINCT {json_field}->>\'%s\' AS v, {json_field}->>\'%s\' AS v' |
|
237 |
' FROM {{fact_table}} WHERE ({json_field}->>\'%s\') IS NOT NULL ORDER BY v' % |
|
238 |
(self.name, self.name, self.name)) |
|
241 |
sql = ( |
|
242 |
'SELECT DISTINCT {json_field}->>\'%s\' AS v, {json_field}->>\'%s\' AS v' |
|
243 |
' FROM {{fact_table}} WHERE ({json_field}->>\'%s\') IS NOT NULL ORDER BY v' |
|
244 |
% (self.name, self.name, self.name) |
|
245 |
) |
|
239 | 246 |
self.members_query = sql.format(json_field=json_field) |
240 |
self.filter_expression = ('({fact_table}.id IS NULL ' |
|
241 |
'OR ({fact_table}.%s->>\'%s\') IN (%%s))' |
|
242 |
% (json_field, name)) |
|
247 |
self.filter_expression = '({fact_table}.id IS NULL OR ({fact_table}.%s->>\'%s\') IN (%%s))' % ( |
|
248 |
json_field, |
|
249 |
name, |
|
250 |
) |
|
243 | 251 |
self.filter_needs_join = False |
244 | 252 |
self.absent_label = _('None') |
245 | 253 | |
246 | 254 | |
247 | 255 |
class EngineJSONDimension(EngineDimension): |
248 | ||
249 | 256 |
def __init__(self, engine, engine_cube, name): |
250 | 257 |
self.engine = engine |
251 | 258 |
self.engine_cube = engine_cube |
252 | 259 |
self.dimension = SchemaJSONDimension(self.engine_cube.json_field, name) |
253 | 260 | |
254 | 261 |
def cache_key(self, filters): |
255 |
key = (self.engine.path + self.engine_cube.json_field |
|
256 |
+ self.engine_cube.name + self.name + repr(filters)) |
|
262 |
key = ( |
|
263 |
self.engine.path + self.engine_cube.json_field + self.engine_cube.name + self.name + repr(filters) |
|
264 |
) |
|
257 | 265 |
return hashlib.md5(force_bytes(key)).hexdigest() |
258 | 266 | |
259 | 267 |
def to_json(self): |
... | ... | |
263 | 271 |
} |
264 | 272 | |
265 | 273 | |
266 |
class EngineMeasure(object):
|
|
274 |
class EngineMeasure: |
|
267 | 275 |
def __init__(self, engine, engine_cube, measure): |
268 | 276 |
self.engine = engine |
269 | 277 |
self.engine_cube = engine_cube |
... | ... | |
273 | 281 |
return getattr(self.measure, name) |
274 | 282 | |
275 | 283 | |
276 |
class ProxyList(object):
|
|
284 |
class ProxyList: |
|
277 | 285 |
chain = None |
278 | 286 | |
279 | 287 |
def __init__(self, engine, engine_cube, attribute, cls, chain=None): |
... | ... | |
285 | 293 |
self.chain = chain(engine, engine_cube) |
286 | 294 | |
287 | 295 |
def __iter__(self): |
288 |
i = (self.cls(self.engine, self.engine_cube, o) |
|
289 |
for o in getattr(self.engine_cube.cube, self.attribute)) |
|
296 |
i = ( |
|
297 |
self.cls(self.engine, self.engine_cube, o) for o in getattr(self.engine_cube.cube, self.attribute) |
|
298 |
) |
|
290 | 299 |
if self.chain: |
291 | 300 |
i = itertools.chain(i, self.chain) |
292 | 301 |
return i |
... | ... | |
300 | 309 |
raise KeyError |
301 | 310 | |
302 | 311 | |
303 |
class JSONDimensions(object):
|
|
312 |
class JSONDimensions: |
|
304 | 313 |
__cache = None |
305 | 314 | |
306 | 315 |
def __init__(self, engine, engine_cube): |
... | ... | |
313 | 322 |
return [] |
314 | 323 |
if not self.__cache: |
315 | 324 |
with self.engine.get_cursor() as cursor: |
316 |
sql = ('select distinct jsonb_object_keys(%s) as a from %s order by a' |
|
317 |
% (self.engine_cube.json_field, self.engine_cube.fact_table)) |
|
325 |
sql = 'select distinct jsonb_object_keys(%s) as a from %s order by a' % ( |
|
326 |
self.engine_cube.json_field, |
|
327 |
self.engine_cube.fact_table, |
|
328 |
) |
|
318 | 329 |
cursor.execute(sql) |
319 | 330 |
self.__cache = [row[0] for row in cursor.fetchall()] |
320 | 331 |
return self.__cache |
... | ... | |
329 | 340 |
return EngineJSONDimension(self.engine, self.engine_cube, name) |
330 | 341 | |
331 | 342 | |
332 |
class ProxyListDescriptor(object):
|
|
343 |
class ProxyListDescriptor: |
|
333 | 344 |
def __init__(self, attribute, cls, chain=None): |
334 | 345 |
self.attribute = attribute |
335 | 346 |
self.cls = cls |
... | ... | |
342 | 353 |
return obj.__dict__[key] |
343 | 354 | |
344 | 355 | |
345 |
class EngineCube(object):
|
|
356 |
class EngineCube: |
|
346 | 357 |
dimensions = ProxyListDescriptor('all_dimensions', EngineDimension, chain=JSONDimensions) |
347 | 358 |
measures = ProxyListDescriptor('measures', EngineMeasure) |
348 | 359 | |
... | ... | |
365 | 376 |
name=name, |
366 | 377 |
table=( |
367 | 378 |
'(SELECT DISTINCT %s.%s->>\'%s\' AS value FROM %s ' |
368 |
'WHERE (%s.%s->>\'%s\') IS NOT NULL ORDER BY value)' % ( |
|
369 |
self.fact_table, self.json_field, json_key, self.fact_table, |
|
370 |
self.fact_table, self.json_field, json_key |
|
371 |
) |
|
379 |
'WHERE (%s.%s->>\'%s\') IS NOT NULL ORDER BY value)' |
|
380 |
) |
|
381 |
% ( |
|
382 |
self.fact_table, |
|
383 |
self.json_field, |
|
384 |
json_key, |
|
385 |
self.fact_table, |
|
386 |
self.fact_table, |
|
387 |
self.json_field, |
|
388 |
json_key, |
|
372 | 389 |
), |
373 | 390 |
master='%s->>\'%s\'' % (self.json_field, json_key), |
374 | 391 |
detail='value', |
... | ... | |
427 | 444 |
sql += ' GROUP BY %s' % ', '.join(group_by) |
428 | 445 |
if order_by: |
429 | 446 |
sql += ' ORDER BY %s' % ', '.join(order_by) |
430 |
sql = sql.format(fact_table=self.cube.fact_table, |
|
431 |
table_expression=table_expression, |
|
432 |
where_conditions=where_conditions) |
|
447 |
sql = sql.format( |
|
448 |
fact_table=self.cube.fact_table, |
|
449 |
table_expression=table_expression, |
|
450 |
where_conditions=where_conditions, |
|
451 |
) |
|
433 | 452 |
return sql |
434 | 453 | |
435 | 454 |
def query(self, filters, drilldown, measures, **kwargs): |
436 |
self.engine.log.debug('%s.%s query filters=%s drilldown=%s measures=%s', |
|
437 |
self.engine.warehouse.name, self.cube.name, filters, drilldown, |
|
438 |
measures) |
|
455 |
self.engine.log.debug( |
|
456 |
'%s.%s query filters=%s drilldown=%s measures=%s', |
|
457 |
self.engine.warehouse.name, |
|
458 |
self.cube.name, |
|
459 |
filters, |
|
460 |
drilldown, |
|
461 |
measures, |
|
462 |
) |
|
439 | 463 |
with self.engine.get_cursor() as cursor: |
440 | 464 |
sql = self.sql_query(filters=filters, drilldown=drilldown, measures=measures, **kwargs) |
441 | 465 |
self.engine.log.debug('SQL: %s', sql) |
... | ... | |
451 | 475 |
else: |
452 | 476 |
value_label = row[j + 1] |
453 | 477 |
j += 2 |
454 |
cells.dimensions.append(DimensionCell( |
|
455 |
dimension=dimension, |
|
456 |
value=value, |
|
457 |
value_label=value_label, |
|
458 |
)) |
|
478 |
cells.dimensions.append( |
|
479 |
DimensionCell( |
|
480 |
dimension=dimension, |
|
481 |
value=value, |
|
482 |
value_label=value_label, |
|
483 |
) |
|
484 |
) |
|
459 | 485 |
for i, measure in enumerate(measures): |
460 |
cells.measures.append(MeasureCell( |
|
461 |
measure=measure, |
|
462 |
value=row[j + i], |
|
463 |
)) |
|
486 |
cells.measures.append( |
|
487 |
MeasureCell( |
|
488 |
measure=measure, |
|
489 |
value=row[j + i], |
|
490 |
) |
|
491 |
) |
|
464 | 492 |
yield cells |
465 | 493 | |
466 | 494 |
JOIN_KINDS = { |
... | ... | |
471 | 499 |
} |
472 | 500 | |
473 | 501 |
def build_table_expression(self, joins, table_name, force_join=None): |
474 |
'''Recursively build the table expression from the join tree,
|
|
475 |
starting from the fact table'''
|
|
502 |
"""Recursively build the table expression from the join tree,
|
|
503 |
starting from the fact table"""
|
|
476 | 504 | |
477 | 505 |
join_tree = {} |
478 | 506 |
# Build join tree |
... | ... | |
495 | 523 |
for join_name, join in joins.items(): |
496 | 524 |
contain_joins = True |
497 | 525 |
sql += ' %s ' % self.JOIN_KINDS[kind] |
498 |
sql += ' ' + build_table_expression_helper(join_tree, join.table, alias=join.name, top=False) |
|
526 |
sql += ' ' + build_table_expression_helper( |
|
527 |
join_tree, join.table, alias=join.name, top=False |
|
528 |
) |
|
499 | 529 |
condition = '%s.%s = %s.%s' % ( |
500 | 530 |
alias or table_name, |
501 | 531 |
join.master.split('.')[-1], |
502 | 532 |
quote(join.name), |
503 |
join.detail) |
|
533 |
join.detail, |
|
534 |
) |
|
504 | 535 |
sql += ' ON ' + condition |
505 | 536 | |
506 | 537 |
# if the table expression contains joins and is not the full table |
... | ... | |
514 | 545 |
return build_table_expression_helper(join_tree, table_name) |
515 | 546 | |
516 | 547 | |
517 |
class Engine(object):
|
|
548 |
class Engine: |
|
518 | 549 |
def __init__(self, warehouse): |
519 | 550 |
self.warehouse = warehouse |
520 | 551 |
self.log = logging.getLogger(__name__) |
bijoe/hobo_agent/management/commands/hobo_deploy.py | ||
---|---|---|
25 | 25 |
except ImportError: |
26 | 26 |
sentry_sdk = None |
27 | 27 | |
28 |
from tenant_schemas.utils import tenant_context |
|
28 |
from django.conf import settings |
|
29 |
from django.utils.encoding import force_str |
|
29 | 30 |
from hobo.agent.common.management.commands import hobo_deploy |
30 | 31 |
from hobo.multitenant.settings_loaders import KnownServices |
31 | ||
32 |
from django.utils.encoding import force_str |
|
33 |
from django.conf import settings |
|
32 |
from tenant_schemas.utils import tenant_context |
|
34 | 33 | |
35 | 34 | |
36 | 35 |
def pg_dsn_quote(value): |
... | ... | |
47 | 46 |
else: |
48 | 47 |
# insert hash in the middle, to keep some readability |
49 | 48 |
return ( |
50 |
identifier[:(63 - hash_length) // 2] |
|
49 |
identifier[: (63 - hash_length) // 2]
|
|
51 | 50 |
+ hashlib.md5(identifier.encode('utf-8')).hexdigest()[:hash_length] |
52 |
+ identifier[-(63 - hash_length) // 2:]) |
|
51 |
+ identifier[-(63 - hash_length) // 2 :] |
|
52 |
) |
|
53 | 53 | |
54 | 54 | |
55 | 55 |
def schema_from_url(url, hash_length=6): |
... | ... | |
62 | 62 | |
63 | 63 |
class Command(hobo_deploy.Command): |
64 | 64 |
def deploy_specifics(self, hobo_environment, tenant): |
65 |
super(Command, self).deploy_specifics(hobo_environment, tenant)
|
|
65 |
super().deploy_specifics(hobo_environment, tenant) |
|
66 | 66 |
with tenant_context(tenant): |
67 | 67 |
services = hobo_environment.get('services') |
68 | 68 |
ini_file = os.path.join(tenant.get_directory(), 'wcs-olap.ini') |
... | ... | |
77 | 77 |
config.add_section('wcs-olap') |
78 | 78 |
config.set('wcs-olap', 'cubes_model_dirs', schemas_path) |
79 | 79 |
pg_dsn_parts = [] |
80 |
for pg_dsn_part in [('NAME', 'dbname'), |
|
81 |
('HOST', 'host'), |
|
82 |
('USER', 'user'), |
|
83 |
('PASSWORD', 'password'), |
|
84 |
('PORT', 'port')]: |
|
80 |
for pg_dsn_part in [ |
|
81 |
('NAME', 'dbname'), |
|
82 |
('HOST', 'host'), |
|
83 |
('USER', 'user'), |
|
84 |
('PASSWORD', 'password'), |
|
85 |
('PORT', 'port'), |
|
86 |
]: |
|
85 | 87 |
if settings.DATABASES['default'].get(pg_dsn_part[0]): |
86 |
pg_dsn_parts.append('%s=%s' % ( |
|
87 |
pg_dsn_part[1], |
|
88 |
pg_dsn_quote(settings.DATABASES['default'].get(pg_dsn_part[0])))) |
|
88 |
pg_dsn_parts.append( |
|
89 |
'%s=%s' |
|
90 |
% (pg_dsn_part[1], pg_dsn_quote(settings.DATABASES['default'].get(pg_dsn_part[0]))) |
|
91 |
) |
|
89 | 92 |
config.set('wcs-olap', 'pg_dsn', config_parser_quote(' '.join(pg_dsn_parts))) |
90 | 93 | |
91 | 94 |
for service in services: |
... | ... | |
97 | 100 |
our_key = this['secret_key'] |
98 | 101 |
for service in services: |
99 | 102 |
base_url = service.get('base_url') |
100 |
if (service.get('this') or not service.get('secret_key') |
|
101 |
or service['service-id'] != 'wcs' or not service.get('base_url')): |
|
103 |
if ( |
|
104 |
service.get('this') |
|
105 |
or not service.get('secret_key') |
|
106 |
or service['service-id'] != 'wcs' |
|
107 |
or not service.get('base_url') |
|
108 |
): |
|
102 | 109 |
continue |
103 | 110 |
elif service.get('secondary') and not config.has_section(base_url): |
104 | 111 |
# skip secondary instances unless they were already added, |
bijoe/management/commands/export_site.py | ||
---|---|---|
27 | 27 | |
28 | 28 |
def add_arguments(self, parser): |
29 | 29 |
parser.add_argument( |
30 |
'--output', metavar='FILE', default=None,
|
|
31 |
help='name of a file to write output to')
|
|
30 |
'--output', metavar='FILE', default=None, help='name of a file to write output to'
|
|
31 |
) |
|
32 | 32 | |
33 | 33 |
def handle(self, *args, **options): |
34 | 34 |
if options['output']: |
bijoe/management/commands/import_site.py | ||
---|---|---|
26 | 26 |
help = 'Import an exported site' |
27 | 27 | |
28 | 28 |
def add_arguments(self, parser): |
29 |
parser.add_argument('filename', metavar='FILENAME', type=str, help='name of file to import') |
|
30 |
parser.add_argument('--clean', action='store_true', default=False, help='Clean site before importing') |
|
29 | 31 |
parser.add_argument( |
30 |
'filename', metavar='FILENAME', type=str, |
|
31 |
help='name of file to import') |
|
32 |
parser.add_argument( |
|
33 |
'--clean', action='store_true', default=False, |
|
34 |
help='Clean site before importing') |
|
35 |
parser.add_argument( |
|
36 |
'--if-empty', action='store_true', default=False, |
|
37 |
help='Import only if site is empty') |
|
32 |
'--if-empty', action='store_true', default=False, help='Import only if site is empty' |
|
33 |
) |
|
38 | 34 | |
39 | 35 |
def handle(self, filename, **options): |
40 | 36 |
if filename == '-': |
bijoe/relative_time.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# bijoe - BI dashboard |
3 | 2 |
# Copyright (C) 2015 Entr'ouvert |
4 | 3 |
# |
... | ... | |
17 | 16 | |
18 | 17 |
import re |
19 | 18 |
from datetime import date |
20 |
from dateutil.relativedelta import relativedelta, MO |
|
19 | ||
21 | 20 |
import isodate |
21 |
from dateutil.relativedelta import MO, relativedelta |
|
22 | 22 | |
23 | 23 | |
24 | 24 |
class RelativeDate(date): |
25 | 25 |
__TEMPLATES = [ |
26 | 26 |
{ |
27 |
'pattern': u' *cette +année *',
|
|
27 |
'pattern': ' *cette +année *', |
|
28 | 28 |
'truncate': 'year', |
29 | 29 |
}, |
30 | 30 |
{ |
31 |
'pattern': u' *l\'année +dernière *',
|
|
31 |
'pattern': ' *l\'année +dernière *', |
|
32 | 32 |
'truncate': 'year', |
33 | 33 |
'timedelta': {'years': -1}, |
34 | 34 |
}, |
35 | 35 |
{ |
36 |
'pattern': u' *l\'année +prochaine *',
|
|
36 |
'pattern': ' *l\'année +prochaine *', |
|
37 | 37 |
'truncate': 'year', |
38 | 38 |
'timedelta': {'years': 1}, |
39 | 39 |
}, |
... | ... | |
131 | 131 |
elif group.startswith('-'): |
132 | 132 |
sign = -1 |
133 | 133 |
group = group[1:] |
134 |
n = re.match('(\d+)\*(\w+)', group) |
|
134 |
n = re.match(r'(\d+)\*(\w+)', group)
|
|
135 | 135 |
try: |
136 | 136 |
if n: |
137 | 137 |
value = int(n.group(1) * m.group(n.group(2))) |
bijoe/schemas.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# |
3 | 2 |
# bijoe - BI dashboard |
4 | 3 |
# Copyright (C) 2015 Entr'ouvert |
... | ... | |
16 | 15 |
# You should have received a copy of the GNU Affero General Public License |
17 | 16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 17 | |
18 |
import collections |
|
19 | 19 |
import datetime |
20 | 20 |
import decimal |
21 |
import collections |
|
22 | 21 | |
23 | 22 |
from django.utils import six |
24 | 23 |
from django.utils.encoding import force_text |
... | ... | |
49 | 48 |
return t |
50 | 49 | |
51 | 50 | |
52 |
class Base(object):
|
|
51 |
class Base: |
|
53 | 52 |
__types__ = {} |
54 | 53 | |
55 | 54 |
def __init__(self, **kwargs): |
... | ... | |
78 | 77 |
def from_json(cls, d): |
79 | 78 |
assert hasattr(d, 'keys') |
80 | 79 |
slots = cls.slots() |
81 |
assert set(d.keys()) <= set(slots), \ |
|
82 |
'given keys %r does not match %s.__slots__: %r' % (d.keys(), cls.__name__, slots) |
|
80 |
assert set(d.keys()) <= set(slots), 'given keys %r does not match %s.__slots__: %r' % ( |
|
81 |
d.keys(), |
|
82 |
cls.__name__, |
|
83 |
slots, |
|
84 |
) |
|
83 | 85 |
types = cls.types() |
84 | 86 | |
85 | 87 |
kwargs = {} |
86 | 88 |
for key in slots: |
87 |
assert key in d or hasattr(cls, key), \ |
|
88 |
'%s.%s is is a mandatory attribute' % (cls.__name__, key) |
|
89 |
assert key in d or hasattr(cls, key), '%s.%s is is a mandatory attribute' % (cls.__name__, key) |
|
89 | 90 |
if not key in d: |
90 | 91 |
continue |
91 | 92 |
value = d[key] |
... | ... | |
133 | 134 |
__slots__ = ['name', 'label', 'type', 'expression'] |
134 | 135 |
__types__ = { |
135 | 136 |
'name': str, |
136 |
'label': six.text_type,
|
|
137 |
'label': str,
|
|
137 | 138 |
'type': type_cast, |
138 | 139 |
'expression': str, |
139 | 140 |
} |
... | ... | |
146 | 147 | |
147 | 148 | |
148 | 149 |
class Dimension(Base): |
149 |
__slots__ = ['name', 'label', 'type', 'join', 'value', 'value_label', |
|
150 |
'order_by', 'group_by', 'filter_in_join', 'filter', 'filter_value', |
|
151 |
'filter_needs_join', 'filter_expression', 'absent_label'] |
|
150 |
__slots__ = [ |
|
151 |
'name', |
|
152 |
'label', |
|
153 |
'type', |
|
154 |
'join', |
|
155 |
'value', |
|
156 |
'value_label', |
|
157 |
'order_by', |
|
158 |
'group_by', |
|
159 |
'filter_in_join', |
|
160 |
'filter', |
|
161 |
'filter_value', |
|
162 |
'filter_needs_join', |
|
163 |
'filter_expression', |
|
164 |
'absent_label', |
|
165 |
] |
|
152 | 166 |
__types__ = { |
153 | 167 |
'name': str, |
154 |
'label': six.text_type,
|
|
168 |
'label': str,
|
|
155 | 169 |
'type': str, |
156 | 170 |
'join': [str], |
157 | 171 |
'value': str, |
... | ... | |
164 | 178 |
'filter_in_join': bool, |
165 | 179 |
'filter_value': str, |
166 | 180 |
'filter_needs_join': bool, |
167 |
'absent_label': six.text_type,
|
|
181 |
'absent_label': str,
|
|
168 | 182 |
} |
169 | 183 | |
170 | 184 |
def __init__(self, **kwargs): |
... | ... | |
180 | 194 |
self.filter_needs_join = True |
181 | 195 |
self.members_query = None |
182 | 196 |
self.absent_label = None |
183 |
super(Dimension, self).__init__(**kwargs)
|
|
197 |
super().__init__(**kwargs) |
|
184 | 198 |
if not self.absent_label: |
185 | 199 |
if self.type in ('date', 'integer', 'string'): |
186 | 200 |
self.absent_label = _('None') |
... | ... | |
196 | 210 |
return [ |
197 | 211 |
self, |
198 | 212 |
Dimension( |
199 |
label=u'année (%s)' % self.label, |
|
200 | ||
213 |
label='année (%s)' % self.label, |
|
201 | 214 |
name=self.name + '__year', |
202 | 215 |
type='integer', |
203 | 216 |
join=self.join, |
204 | 217 |
filter_value='EXTRACT(year from %s)::integer' % filter_value, |
205 | 218 |
filter_in_join=self.filter_in_join, |
206 | 219 |
value='EXTRACT(year from %s)::integer' % self.value, |
207 |
filter=False), |
|
220 |
filter=False, |
|
221 |
), |
|
208 | 222 |
Dimension( |
209 |
label=u'année et mois (%s)' % self.label, |
|
210 | ||
223 |
label='année et mois (%s)' % self.label, |
|
211 | 224 |
name=self.name + '__yearmonth', |
212 | 225 |
type='integer', |
213 | 226 |
join=self.join, |
214 | 227 |
filter_value='EXTRACT(year from %s) || \'M\' || EXTRACT(month from %s)' |
215 |
% (filter_value, filter_value),
|
|
228 |
% (filter_value, filter_value), |
|
216 | 229 |
filter_in_join=self.filter_in_join, |
217 | 230 |
value='TO_CHAR(EXTRACT(month from %s), \'00\') || \'/\' || EXTRACT(year from %s)' |
218 |
% (self.value, self.value), |
|
219 |
group_by='EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, |
|
220 |
self.value), |
|
221 |
order_by=['EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, |
|
222 |
self.value)], |
|
223 |
filter=False), |
|
231 |
% (self.value, self.value), |
|
232 |
group_by='EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, self.value), |
|
233 |
order_by=['EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, self.value)], |
|
234 |
filter=False, |
|
235 |
), |
|
224 | 236 |
Dimension( |
225 |
label=u'mois (%s)' % self.label,
|
|
237 |
label='mois (%s)' % self.label, |
|
226 | 238 |
name=self.name + '__month', |
227 | 239 |
type='integer', |
228 | 240 |
filter_value='EXTRACT(month from %s)' % filter_value, |
... | ... | |
230 | 242 |
join=self.join, |
231 | 243 |
value='EXTRACT(month from %s)' % self.value, |
232 | 244 |
value_label='to_char(date_trunc(\'month\', %s), \'TMmonth\')' % self.value, |
233 |
group_by='EXTRACT(month from %s), ' |
|
234 |
'to_char(date_trunc(\'month\', %s), \'TMmonth\')'
|
|
235 |
% (self.value, self.value),
|
|
236 |
filter=False),
|
|
245 |
group_by='EXTRACT(month from %s), to_char(date_trunc(\'month\', %s), \'TMmonth\')'
|
|
246 |
% (self.value, self.value),
|
|
247 |
filter=False,
|
|
248 |
), |
|
237 | 249 |
Dimension( |
238 |
label=u'jour de la semaine (%s)' % self.label,
|
|
250 |
label='jour de la semaine (%s)' % self.label, |
|
239 | 251 |
name=self.name + '__dow', |
240 | 252 |
type='integer', |
241 | 253 |
join=self.join, |
... | ... | |
243 | 255 |
filter_in_join=self.filter_in_join, |
244 | 256 |
value='EXTRACT(dow from %s)' % self.value, |
245 | 257 |
order_by=['(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value], |
246 |
value_label='to_char(date_trunc(\'week\', current_date)::date ' |
|
247 |
'+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' % self.value, |
|
248 |
filter=False), |
|
258 |
value_label=( |
|
259 |
'to_char(date_trunc(\'week\', current_date)::date ' |
|
260 |
'+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' |
|
261 |
) |
|
262 |
% self.value, |
|
263 |
filter=False, |
|
264 |
), |
|
249 | 265 |
Dimension( |
250 |
label=u'semaine (%s)' % self.label,
|
|
266 |
label='semaine (%s)' % self.label, |
|
251 | 267 |
name=self.name + '__isoweek', |
252 | 268 |
type='integer', |
253 | 269 |
join=self.join, |
254 | 270 |
filter_value='EXTRACT(isoyear from %s) || \'S\' || EXTRACT(week from %s)' |
255 |
% (filter_value, filter_value),
|
|
271 |
% (filter_value, filter_value), |
|
256 | 272 |
filter_in_join=self.filter_in_join, |
257 | 273 |
value='EXTRACT(isoyear from %s) || \'S\' || EXTRACT(week from %s)' |
258 |
% (self.value, self.value), |
|
259 |
group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, |
|
260 |
self.value), |
|
261 |
order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, |
|
262 |
self.value)], |
|
263 |
filter=False) |
|
274 |
% (self.value, self.value), |
|
275 |
group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, self.value), |
|
276 |
order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, self.value)], |
|
277 |
filter=False, |
|
278 |
), |
|
264 | 279 |
] |
265 | 280 |
return [self] |
266 | 281 | |
... | ... | |
268 | 283 |
value = self.filter_value or self.value |
269 | 284 | |
270 | 285 |
if self.type == 'date': |
271 |
assert isinstance(filter_values, dict) and set(filter_values.keys()) == set(['start', |
|
272 |
'end']) |
|
286 |
assert isinstance(filter_values, dict) and set(filter_values.keys()) == {'start', 'end'} |
|
273 | 287 |
filters = [] |
274 | 288 |
values = [] |
275 | 289 | |
... | ... | |
278 | 292 |
filter_value = RelativeDate(filter_value) |
279 | 293 |
filters.append(tpl % (value, '%s')) |
280 | 294 |
values.append(filter_value) |
295 | ||
281 | 296 |
try: |
282 | 297 |
if filter_values['start']: |
283 | 298 |
date_filter('%s >= %s', filter_values['start']) |
... | ... | |
346 | 361 | |
347 | 362 |
def __init__(self, **kwargs): |
348 | 363 |
self.kind = 'full' |
349 |
super(Join, self).__init__(**kwargs)
|
|
364 |
super().__init__(**kwargs) |
|
350 | 365 | |
351 | 366 |
@property |
352 | 367 |
def master_table(self): |
... | ... | |
356 | 371 | |
357 | 372 | |
358 | 373 |
class Cube(Base): |
359 |
__slots__ = ['name', 'label', 'fact_table', 'json_field', 'key', 'joins', 'dimensions', |
|
360 |
'measures', 'warnings'] |
|
374 |
__slots__ = [ |
|
375 |
'name', |
|
376 |
'label', |
|
377 |
'fact_table', |
|
378 |
'json_field', |
|
379 |
'key', |
|
380 |
'joins', |
|
381 |
'dimensions', |
|
382 |
'measures', |
|
383 |
'warnings', |
|
384 |
] |
|
361 | 385 |
__types__ = { |
362 | 386 |
'name': str, |
363 |
'label': six.text_type,
|
|
387 |
'label': str,
|
|
364 | 388 |
'fact_table': str, |
365 | 389 |
'json_field': str, |
366 | 390 |
'key': str, |
367 | 391 |
'joins': [Join], |
368 | 392 |
'dimensions': [Dimension], |
369 | 393 |
'measures': [Measure], |
370 |
'warnings': [six.text_type],
|
|
394 |
'warnings': [str],
|
|
371 | 395 |
} |
372 | 396 | |
373 | 397 |
def __init__(self, **kwargs): |
... | ... | |
376 | 400 |
self.dimensions = () |
377 | 401 |
self.measures = () |
378 | 402 |
self.warnings = () |
379 |
super(Cube, self).__init__(**kwargs)
|
|
403 |
super().__init__(**kwargs) |
|
380 | 404 | |
381 | 405 |
def check(self): |
382 | 406 |
names = collections.Counter() |
... | ... | |
386 | 410 |
duplicates = [k for k, v in names.items() if v > 1] |
387 | 411 |
if duplicates: |
388 | 412 |
raise SchemaError( |
389 |
'More than one join, dimension or measure with name(s) %s' % ', '.join(duplicates)) |
|
413 |
'More than one join, dimension or measure with name(s) %s' % ', '.join(duplicates) |
|
414 |
) |
|
390 | 415 | |
391 | 416 |
@property |
392 | 417 |
def all_dimensions(self): |
393 | 418 |
for dimension in self.dimensions: |
394 |
for sub_dimension in dimension.dimensions: |
|
395 |
yield sub_dimension |
|
419 |
yield from dimension.dimensions |
|
396 | 420 | |
397 | 421 |
def get_dimension(self, name): |
398 | 422 |
for dimension in self.dimensions: |
... | ... | |
419 | 443 |
__types__ = { |
420 | 444 |
'name': str, |
421 | 445 |
'slug': str, |
422 |
'label': six.text_type,
|
|
446 |
'label': str,
|
|
423 | 447 |
'pg_dsn': str, |
424 | 448 |
'search_path': [str], |
425 | 449 |
'cubes': [Cube], |
... | ... | |
430 | 454 |
self.path = None |
431 | 455 |
self.slug = None |
432 | 456 |
self.timestamp = None |
433 |
super(Warehouse, self).__init__(**kwargs)
|
|
457 |
super().__init__(**kwargs) |
|
434 | 458 | |
435 | 459 |
def check(self): |
436 | 460 |
names = collections.Counter(cube.name for cube in self.cubes) |
bijoe/settings.py | ||
---|---|---|
24 | 24 |
https://docs.djangoproject.com/en/1.7/ref/settings/ |
25 | 25 |
""" |
26 | 26 | |
27 |
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
|
28 |
import os |
|
27 | 29 |
from itertools import chain |
28 |
from django.conf import global_settings |
|
29 |
import django |
|
30 | 30 | |
31 |
import django |
|
32 |
from django.conf import global_settings |
|
31 | 33 |
from gadjo.templatetags.gadjo import xstatic |
32 | 34 | |
33 |
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
|
34 |
import os |
|
35 | 35 |
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) |
36 | 36 | |
37 | 37 | |
... | ... | |
74 | 74 |
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
75 | 75 |
) |
76 | 76 | |
77 |
STATICFILES_FINDERS = list(chain(global_settings.STATICFILES_FINDERS, |
|
78 |
('gadjo.finders.XStaticFinder',))) |
|
77 |
STATICFILES_FINDERS = list(chain(global_settings.STATICFILES_FINDERS, ('gadjo.finders.XStaticFinder',))) |
|
79 | 78 | |
80 | 79 | |
81 | 80 |
ROOT_URLCONF = 'bijoe.urls' |
... | ... | |
122 | 121 |
'formatters': { |
123 | 122 |
'verbose': { |
124 | 123 |
'format': '[%(asctime)s] %(levelname)s %(name)s.%(funcName)s: %(message)s', |
125 |
'datefmt': '%Y-%m-%d %a %H:%M:%S' |
|
124 |
'datefmt': '%Y-%m-%d %a %H:%M:%S',
|
|
126 | 125 |
}, |
127 | 126 |
}, |
128 | 127 |
'handlers': { |
bijoe/templatetags/bijoe.py | ||
---|---|---|
21 | 21 |
try: |
22 | 22 |
from django_select2.templatetags.django_select2_tags import * |
23 | 23 |
except ImportError: |
24 | ||
24 | 25 |
@register.simple_tag(name='import_django_select2_js_css') |
25 | 26 |
def import_all(light=0): |
26 | 27 |
return '' |
27 |
bijoe/urls.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from django.conf import settings |
|
17 | 18 |
from django.conf.urls import include, url |
18 | 19 |
from django.contrib import admin |
19 |
from django.conf import settings |
|
20 | 20 | |
21 | 21 |
from . import views |
22 | 22 |
bijoe/utils.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
import datetime |
18 |
import os |
|
19 | 18 |
import glob |
20 | 19 |
import json |
20 |
import os |
|
21 | 21 | |
22 | 22 |
from django.conf import settings |
23 | 23 |
from django.db import connection, transaction |
24 |
from django.utils.translation import ugettext as _ |
|
25 | 24 |
from django.utils.timezone import utc |
25 |
from django.utils.translation import ugettext as _ |
|
26 | 26 | |
27 | 27 |
try: |
28 | 28 |
from functools import lru_cache |
... | ... | |
34 | 34 | |
35 | 35 |
def get_warehouses_paths(): |
36 | 36 |
for pattern in settings.BIJOE_SCHEMAS: |
37 |
for path in glob.glob(pattern): |
|
38 |
yield path |
|
37 |
yield from glob.glob(pattern) |
|
39 | 38 |
if hasattr(connection, 'tenant'): |
40 | 39 |
pattern = os.path.join(connection.tenant.get_directory(), 'schemas', '*.model') |
41 |
for path in glob.glob(pattern): |
|
42 |
yield path |
|
40 |
yield from glob.glob(pattern) |
|
43 | 41 | |
44 | 42 | |
45 | 43 |
@lru_cache() |
... | ... | |
70 | 68 |
if len(l) == 1: |
71 | 69 |
return l[0] |
72 | 70 |
if len(l) > 2: |
73 |
l = u', '.join(l[:-1]), l[-1]
|
|
74 |
return _(u'{0} and {1}').format(l[0], l[1])
|
|
71 |
l = ', '.join(l[:-1]), l[-1] |
|
72 |
return _('{0} and {1}').format(l[0], l[1]) |
|
75 | 73 | |
76 | 74 | |
77 | 75 |
def export_site(): |
bijoe/uwsgi.py | ||
---|---|---|
21 | 21 |
import subprocess |
22 | 22 | |
23 | 23 |
from django.conf import settings |
24 | ||
25 | 24 |
from uwsgidecorators import cron, spool |
26 | 25 | |
27 | 26 |
# existing loggers are disabled by Django on django.setup() due do |
... | ... | |
58 | 57 |
'--all', |
59 | 58 |
wcs_olap_ini_path, |
60 | 59 |
], |
61 |
check=False) |
|
60 |
check=False, |
|
61 |
) |
|
62 | 62 |
logger.info('finished wcs-olap on %s', wcs_olap_ini_path) |
63 | 63 | |
64 | 64 |
bijoe/views.py | ||
---|---|---|
17 | 17 |
import json |
18 | 18 | |
19 | 19 |
from django.conf import settings |
20 |
from django.contrib.auth import logout as auth_logout |
|
21 |
from django.contrib.auth import views as auth_views |
|
22 |
from django.contrib.auth.views import redirect_to_login |
|
23 |
from django.core.exceptions import PermissionDenied |
|
24 |
from django.http import HttpResponse, HttpResponseRedirect |
|
20 | 25 |
from django.shortcuts import resolve_url |
21 | 26 |
from django.urls import reverse |
22 |
from django.views.generic import ListView, View |
|
23 |
from django.http import HttpResponse, HttpResponseRedirect |
|
24 | 27 |
from django.utils.decorators import method_decorator |
25 | 28 |
from django.utils.http import quote |
26 | 29 |
from django.utils.translation import ugettext as _ |
27 |
from django.contrib.auth import logout as auth_logout |
|
28 |
from django.contrib.auth import views as auth_views |
|
29 |
from django.contrib.auth.views import redirect_to_login |
|
30 |
from django.core.exceptions import PermissionDenied |
|
31 | 30 |
from django.views.decorators.cache import never_cache |
31 |
from django.views.generic import ListView, View |
|
32 | 32 | |
33 | 33 |
try: |
34 | 34 |
from mellon.utils import get_idps |
35 | 35 |
except ImportError: |
36 | 36 |
get_idps = lambda: [] |
37 | 37 | |
38 |
from .utils import get_warehouses |
|
39 | 38 |
from .engine import Engine |
39 |
from .utils import get_warehouses |
|
40 | 40 |
from .visualization.models import Visualization |
41 | 41 |
from .visualization.utils import Visualization as VisuUtil |
42 | 42 | |
43 | 43 | |
44 |
class AuthorizationMixin(object):
|
|
44 |
class AuthorizationMixin: |
|
45 | 45 |
def authorize(self, request): |
46 | 46 |
if request.user.is_authenticated: |
47 | 47 |
if not request.user.is_superuser: |
... | ... | |
52 | 52 | |
53 | 53 |
def dispatch(self, request, *args, **kwargs): |
54 | 54 |
if self.authorize(request): |
55 |
return super(AuthorizationMixin, self).dispatch(request, *args, **kwargs)
|
|
55 |
return super().dispatch(request, *args, **kwargs) |
|
56 | 56 |
else: |
57 | 57 |
return redirect_to_login(request.build_absolute_uri()) |
58 | 58 | |
... | ... | |
64 | 64 |
paginate_by = settings.PAGE_LENGTH |
65 | 65 | |
66 | 66 |
def get_context_data(self, **kwargs): |
67 |
ctx = super(HomepageView, self).get_context_data(**kwargs) |
|
68 |
ctx['warehouses'] = sorted((Engine(w) for w in get_warehouses()), |
|
69 |
key=lambda w: w.label) |
|
67 |
ctx = super().get_context_data(**kwargs) |
|
68 |
ctx['warehouses'] = sorted((Engine(w) for w in get_warehouses()), key=lambda w: w.label) |
|
70 | 69 |
ctx['request'] = self.request |
71 | 70 |
return ctx |
72 | 71 | |
... | ... | |
108 | 107 |
return HttpResponseRedirect( |
109 | 108 |
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next')) |
110 | 109 |
) |
111 |
return super(LoginView, self).get(request, *args, **kwargs)
|
|
110 |
return super().get(request, *args, **kwargs) |
|
112 | 111 | |
113 | 112 | |
114 | 113 |
login = LoginView.as_view() |
... | ... | |
119 | 118 |
def dispatch(self, request, *args, **kwargs): |
120 | 119 |
if any(get_idps()): |
121 | 120 |
return HttpResponseRedirect(resolve_url('mellon_logout')) |
122 |
return super(LogoutView, self).dispatch(request, *args, **kwargs)
|
|
121 |
return super().dispatch(request, *args, **kwargs) |
|
123 | 122 | |
124 | 123 | |
125 | 124 |
logout = LogoutView.as_view() |
bijoe/visualization/admin.py | ||
---|---|---|
22 | 22 |
class VisualizationAdmin(admin.ModelAdmin): |
23 | 23 |
list_display = ['name'] |
24 | 24 | |
25 | ||
25 | 26 |
admin.site.register(models.Visualization, VisualizationAdmin) |
bijoe/visualization/forms.py | ||
---|---|---|
1 |
# -*- encoding: utf-8 -*- |
|
2 | 1 |
# bijoe - BI dashboard |
3 | 2 |
# Copyright (C) 2015 Entr'ouvert |
4 | 3 |
# |
... | ... | |
17 | 16 | |
18 | 17 | |
19 | 18 |
from django import forms |
19 |
from django.conf import settings |
|
20 | 20 |
from django.core.exceptions import ValidationError |
21 |
from django.forms import ModelForm, NullBooleanField, TextInput |
|
21 | 22 |
from django.utils.encoding import force_text |
22 |
from django.utils.translation import ugettext as _ |
|
23 | 23 |
from django.utils.safestring import mark_safe |
24 |
from django.forms import ModelForm, TextInput, NullBooleanField |
|
25 |
from django.conf import settings |
|
26 | ||
24 |
from django.utils.translation import ugettext as _ |
|
27 | 25 |
from django_select2.forms import HeavySelect2MultipleWidget |
28 | 26 | |
29 | 27 |
from . import models |
... | ... | |
32 | 30 |
class VisualizationForm(ModelForm): |
33 | 31 |
class Meta: |
34 | 32 |
model = models.Visualization |
35 |
exclude = ('slug', 'parameters',) |
|
33 |
exclude = ( |
|
34 |
'slug', |
|
35 |
'parameters', |
|
36 |
) |
|
36 | 37 |
widgets = { |
37 | 38 |
'name': TextInput, |
38 | 39 |
} |
39 | 40 | |
41 | ||
40 | 42 |
DATE_RANGES = [ |
41 | 43 |
{ |
42 | 44 |
'value': '3_last_months', |
43 | 45 |
'label': _('3 last months'), |
44 |
'start': u"les 3 derniers mois",
|
|
45 |
'end': u"maintenant",
|
|
46 |
'start': "les 3 derniers mois", |
|
47 |
'end': "maintenant", |
|
46 | 48 |
}, |
47 | 49 |
{ |
48 | 50 |
'value': 'this_year', |
49 | 51 |
'label': _('this year'), |
50 |
'start': u"cette année",
|
|
51 |
'end': u"l\'année prochaine",
|
|
52 |
'start': "cette année", |
|
53 |
'end': "l\'année prochaine", |
|
52 | 54 |
}, |
53 | 55 |
{ |
54 | 56 |
'value': 'last_year', |
55 | 57 |
'label': _('last year'), |
56 |
'start': u'l\'année dernière',
|
|
57 |
'end': u'cette année',
|
|
58 |
'start': 'l\'année dernière', |
|
59 |
'end': 'cette année', |
|
58 | 60 |
}, |
59 | 61 |
{ |
60 | 62 |
'value': 'this_quarter', |
61 | 63 |
'label': _('this quarter'), |
62 |
'start': u'ce trimestre',
|
|
64 |
'start': 'ce trimestre', |
|
63 | 65 |
'end': "le prochain trimestre", |
64 | 66 |
}, |
65 | 67 |
{ |
66 | 68 |
'value': 'last_quarter', |
67 | 69 |
'label': _('last quarter'), |
68 |
'start': u'le dernier trimestre',
|
|
69 |
'end': u'ce trimestre',
|
|
70 |
'start': 'le dernier trimestre', |
|
71 |
'end': 'ce trimestre', |
|
70 | 72 |
}, |
71 | 73 |
{ |
72 | 74 |
'value': 'since_1jan_last_year', |
73 | 75 |
'label': _('since 1st january last year'), |
74 |
'start': u'l\'année dernière',
|
|
75 |
'end': u'maintenant',
|
|
76 |
'start': 'l\'année dernière', |
|
77 |
'end': 'maintenant', |
|
76 | 78 |
}, |
77 | 79 |
] |
78 | 80 | |
79 | 81 | |
80 | 82 |
def get_date_range_choices(): |
81 |
return [('', '---')] + [(r['value'], r['label']) |
|
82 |
for r in getattr(settings, 'BIJOE_DATE_RANGES', DATE_RANGES)] |
|
83 |
return [('', '---')] + [ |
|
84 |
(r['value'], r['label']) for r in getattr(settings, 'BIJOE_DATE_RANGES', DATE_RANGES) |
|
85 |
] |
|
83 | 86 | |
84 | 87 | |
85 | 88 |
class DateRangeWidget(forms.MultiWidget): |
... | ... | |
87 | 90 |
attrs = attrs.copy() if attrs else {} |
88 | 91 |
attrs.update({'type': 'date', 'autocomplete': 'off'}) |
89 | 92 |
attrs1 = attrs.copy() |
90 |
attrs1['placeholder'] = _(u'start')
|
|
93 |
attrs1['placeholder'] = _('start') |
|
91 | 94 |
attrs2 = attrs.copy() |
92 |
attrs2['placeholder'] = _(u'end')
|
|
95 |
attrs2['placeholder'] = _('end') |
|
93 | 96 |
widgets = ( |
94 | 97 |
forms.DateInput(attrs=attrs1, format='%Y-%m-%d'), |
95 | 98 |
forms.DateInput(attrs=attrs2, format='%Y-%m-%d'), |
96 | 99 |
forms.Select(choices=get_date_range_choices()), |
97 | 100 |
) |
98 |
super(DateRangeWidget, self).__init__(widgets, attrs=attrs)
|
|
101 |
super().__init__(widgets, attrs=attrs) |
|
99 | 102 | |
100 | 103 |
def decompress(self, value): |
101 | 104 |
if not value: |
... | ... | |
106 | 109 |
return value['start'], value['end'], None |
107 | 110 | |
108 | 111 |
def render(self, name, value, attrs=None, renderer=None): |
109 |
output = super(DateRangeWidget, self).render(name, value, attrs=attrs)
|
|
112 |
output = super().render(name, value, attrs=attrs) |
|
110 | 113 |
_id = self.build_attrs(attrs).get('id', None) |
111 | 114 |
if _id: |
112 |
output += mark_safe("<script>$(function () { bijoe_date_range('#%s'); });</script>" % |
|
113 |
_id) |
|
115 |
output += mark_safe("<script>$(function () { bijoe_date_range('#%s'); });</script>" % _id) |
|
114 | 116 |
return output |
115 | 117 | |
116 | 118 |
class Media: |
... | ... | |
125 | 127 |
fields = ( |
126 | 128 |
forms.DateField(required=False), |
127 | 129 |
forms.DateField(required=False), |
128 |
forms.ChoiceField(choices=get_date_range_choices(), required=False) |
|
130 |
forms.ChoiceField(choices=get_date_range_choices(), required=False),
|
|
129 | 131 |
) |
130 |
super(DateRangeField, self).__init__(fields=fields, require_all_fields=False, *args, |
|
131 |
**kwargs) |
|
132 |
super().__init__(fields=fields, require_all_fields=False, *args, **kwargs) |
|
132 | 133 | |
133 | 134 |
def compress(self, values): |
134 | 135 |
if not values: |
... | ... | |
159 | 160 | |
160 | 161 |
class CubeForm(forms.Form): |
161 | 162 |
representation = forms.ChoiceField( |
162 |
label=_(u'Presentation'),
|
|
163 |
choices=[('table', _('table')), |
|
164 |
('graphical', _('chart'))],
|
|
165 |
widget=forms.RadioSelect())
|
|
163 |
label=_('Presentation'), |
|
164 |
choices=[('table', _('table')), ('graphical', _('chart'))],
|
|
165 |
widget=forms.RadioSelect(),
|
|
166 |
) |
|
166 | 167 | |
167 | 168 |
def __init__(self, *args, **kwargs): |
168 | 169 |
self.cube = cube = kwargs.pop('cube') |
... | ... | |
170 | 171 | |
171 | 172 |
dimension_choices = [('', '')] + [ |
172 | 173 |
(dimension.name, dimension.label) |
173 |
for dimension in cube.dimensions if dimension.type not in ('datetime', 'date')] |
|
174 |
for dimension in cube.dimensions |
|
175 |
if dimension.type not in ('datetime', 'date') |
|
176 |
] |
|
174 | 177 |
# loop |
175 | 178 |
self.base_fields['loop'] = forms.ChoiceField( |
176 |
label=_('Loop by'), |
|
177 |
choices=dimension_choices, |
|
178 |
required=False) |
|
179 |
label=_('Loop by'), choices=dimension_choices, required=False |
|
180 |
) |
|
179 | 181 | |
180 | 182 |
# filters |
181 | 183 |
for dimension in cube.dimensions: |
... | ... | |
184 | 186 |
field_name = 'filter__%s' % dimension.name |
185 | 187 |
if dimension.type == 'date': |
186 | 188 |
self.base_fields[field_name] = DateRangeField( |
187 |
label=dimension.label.capitalize(), required=False) |
|
189 |
label=dimension.label.capitalize(), required=False |
|
190 |
) |
|
188 | 191 |
elif dimension.type == 'bool': |
189 | 192 |
self.base_fields[field_name] = NullBooleanField( |
190 |
label=dimension.label.capitalize(), required=False) |
|
193 |
label=dimension.label.capitalize(), required=False |
|
194 |
) |
|
191 | 195 |
else: |
192 | 196 |
members = [] |
193 | 197 |
for _id, label in dimension.members(): |
... | ... | |
200 | 204 |
if v == s: |
201 | 205 |
return value |
202 | 206 |
return None |
207 | ||
203 | 208 |
return f |
204 | 209 | |
205 | 210 |
self.base_fields[field_name] = forms.TypedMultipleChoiceField( |
... | ... | |
211 | 216 |
data_view='select2-choices', |
212 | 217 |
warehouse=cube.engine.warehouse.name, |
213 | 218 |
cube=cube.name, |
214 |
dimension=dimension.name |
|
215 |
)) |
|
219 |
dimension=dimension.name, |
|
220 |
), |
|
221 |
) |
|
216 | 222 | |
217 | 223 |
# group by |
218 | 224 |
self.base_fields['drilldown_x'] = forms.ChoiceField( |
219 |
label=_('Group by - horizontally'), |
|
220 |
choices=dimension_choices, |
|
221 |
required=False) |
|
225 |
label=_('Group by - horizontally'), choices=dimension_choices, required=False |
|
226 |
) |
|
222 | 227 | |
223 | 228 |
self.base_fields['drilldown_y'] = forms.ChoiceField( |
224 |
label=_('Group by - vertically'), |
|
225 |
choices=dimension_choices, |
|
226 |
required=False) |
|
229 |
label=_('Group by - vertically'), choices=dimension_choices, required=False |
|
230 |
) |
|
227 | 231 | |
228 | 232 |
# measures |
229 |
choices = [(measure.name, measure.label) |
|
230 |
for measure in cube.measures if measure.type != 'point'] |
|
231 |
self.base_fields['measure'] = forms.ChoiceField( |
|
232 |
label=_('Measure'), choices=choices) |
|
233 |
choices = [(measure.name, measure.label) for measure in cube.measures if measure.type != 'point'] |
|
234 |
self.base_fields['measure'] = forms.ChoiceField(label=_('Measure'), choices=choices) |
|
233 | 235 | |
234 |
super(CubeForm, self).__init__(*args, **kwargs)
|
|
236 |
super().__init__(*args, **kwargs) |
|
235 | 237 | |
236 | 238 |
def clean(self): |
237 |
cleaned_data = super(CubeForm, self).clean()
|
|
239 |
cleaned_data = super().clean() |
|
238 | 240 | |
239 | 241 |
loop = cleaned_data.get('loop') |
240 | 242 |
drilldown_x = cleaned_data.get('drilldown_x') |
241 | 243 |
drilldown_y = cleaned_data.get('drilldown_y') |
242 | 244 | |
243 | 245 |
if loop and (loop == drilldown_x or loop == drilldown_y): |
244 |
raise ValidationError({'loop': _('You cannot use the same dimension for looping and' |
|
245 |
' grouping')}) |
|
246 |
raise ValidationError({'loop': _('You cannot use the same dimension for looping and grouping')}) |
|
246 | 247 |
return cleaned_data |
247 | 248 | |
248 | 249 |
bijoe/visualization/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | 1 |
import jsonfield.fields |
2 |
from django.db import migrations, models |
|
6 | 3 | |
7 | 4 | |
8 | 5 |
class Migration(migrations.Migration): |
9 | 6 | |
10 |
dependencies = [ |
|
11 |
] |
|
7 |
dependencies = [] |
|
12 | 8 | |
13 | 9 |
operations = [ |
14 | 10 |
migrations.CreateModel( |
15 | 11 |
name='Visualization', |
16 | 12 |
fields=[ |
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
13 |
( |
|
14 |
'id', |
|
15 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
16 |
), |
|
18 | 17 |
('name', models.TextField(verbose_name='name')), |
19 | 18 |
('parameters', jsonfield.fields.JSONField(default=dict, verbose_name='parameters')), |
20 | 19 |
], |
bijoe/visualization/migrations/0002_rename_parameters.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 | 1 |
from django.db import migrations |
5 | 2 | |
6 | 3 |
bijoe/visualization/migrations/0003_visualization_slug.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# Generated by Django 1.11.12 on 2019-03-28 07:17 |
3 |
from __future__ import unicode_literals |
|
4 | 2 | |
5 | 3 |
from django.db import migrations, models |
6 | 4 |
from django.utils.text import slugify |
... | ... | |
38 | 36 |
name='slug', |
39 | 37 |
field=models.SlugField(null=True, unique=True, verbose_name='Identifier'), |
40 | 38 |
), |
41 |
migrations.RunPython(forward_func, reverse_func) |
|
39 |
migrations.RunPython(forward_func, reverse_func),
|
|
42 | 40 |
] |
bijoe/visualization/migrations/0004_auto_20190328_0825.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# Generated by Django 1.11.12 on 2019-03-28 07:25 |
3 |
from __future__ import unicode_literals |
|
4 | 2 | |
5 | 3 |
from django.db import migrations, models |
6 | 4 |
bijoe/visualization/models.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import json |
|
18 | 17 |
import datetime |
18 |
import json |
|
19 | 19 | |
20 | 20 |
from django.db import models |
21 | 21 |
from django.http import Http404 |
... | ... | |
57 | 57 |
return (self.slug,) |
58 | 58 | |
59 | 59 |
def export_json(self): |
60 |
visualization = { |
|
61 |
'slug': self.slug, |
|
62 |
'name': self.name, |
|
63 |
'parameters': self.parameters |
|
64 |
} |
|
60 |
visualization = {'slug': self.slug, 'name': self.name, 'parameters': self.parameters} |
|
65 | 61 |
return visualization |
66 | 62 | |
67 | 63 |
@classmethod |
68 | 64 |
def import_json(cls, data): |
69 |
defaults = { |
|
70 |
'name': data['name'], |
|
71 |
'parameters': data['parameters'] |
|
72 |
} |
|
65 |
defaults = {'name': data['name'], 'parameters': data['parameters']} |
|
73 | 66 |
_, created = cls.objects.update_or_create(slug=data['slug'], defaults=defaults) |
74 | 67 |
return created |
75 | 68 | |
... | ... | |
85 | 78 |
i += 1 |
86 | 79 |
slug = '%s-%s' % (base_slug, i) |
87 | 80 |
self.slug = slug |
88 |
return super(Visualization, self).save(*args, **kwargs)
|
|
81 |
return super().save(*args, **kwargs) |
|
89 | 82 | |
90 | 83 |
@property |
91 | 84 |
def exists(self): |
bijoe/visualization/ods.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# |
3 | 2 |
# bijoe - BI dashboard |
4 | 3 |
# Copyright (C) 2015 Entr'ouvert |
... | ... | |
17 | 16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 17 | |
19 | 18 |
import sys |
20 | ||
21 |
import zipfile |
|
22 | 19 |
import xml.etree.ElementTree as ET |
20 |
import zipfile |
|
23 | 21 | |
24 | 22 |
from django.utils.encoding import force_text |
25 | 23 | |
26 | ||
27 | 24 |
OFFICE_NS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0' |
28 | 25 |
TABLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0' |
29 | 26 |
TEXT_NS = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0' |
... | ... | |
31 | 28 | |
32 | 29 | |
33 | 30 |
def is_number(x): |
34 |
if sys.version_info >= (3, 0): |
|
35 |
return isinstance(x, (int, float)) |
|
36 |
else: |
|
37 |
return isinstance(x, (int, long, float)) |
|
31 |
return isinstance(x, (int, float)) |
|
38 | 32 | |
39 | 33 | |
40 |
class Workbook(object):
|
|
34 |
class Workbook: |
|
41 | 35 |
def __init__(self, encoding='utf-8'): |
42 | 36 |
self.sheets = [] |
43 | 37 |
self.encoding = encoding |
... | ... | |
64 | 58 |
z = zipfile.ZipFile(output, 'w') |
65 | 59 |
z.writestr('content.xml', self.get_data()) |
66 | 60 |
z.writestr('mimetype', 'application/vnd.oasis.opendocument.spreadsheet') |
67 |
z.writestr('META-INF/manifest.xml', '''<?xml version="1.0" encoding="UTF-8"?> |
|
61 |
z.writestr( |
|
62 |
'META-INF/manifest.xml', |
|
63 |
'''<?xml version="1.0" encoding="UTF-8"?> |
|
68 | 64 |
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"> |
69 | 65 |
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/> |
70 | 66 |
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/> |
71 | 67 |
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/> |
72 | 68 |
<manifest:file-entry manifest:full-path="META-INF/manifest.xml" manifest:media-type="text/xml"/> |
73 | 69 |
<manifest:file-entry manifest:full-path="mimetype" manifest:media-type="text/plain"/> |
74 |
</manifest:manifest>''') |
|
75 |
z.writestr('styles.xml', '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|
70 |
</manifest:manifest>''', |
|
71 |
) |
|
72 |
z.writestr( |
|
73 |
'styles.xml', |
|
74 |
'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|
76 | 75 |
<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"> |
77 |
</office:document-styles>''') |
|
76 |
</office:document-styles>''', |
|
77 |
) |
|
78 | 78 |
z.close() |
79 | 79 | |
80 | 80 | |
81 |
class WorkSheet(object):
|
|
81 |
class WorkSheet: |
|
82 | 82 |
def __init__(self, workbook, name): |
83 | 83 |
self.cells = {} |
84 | 84 |
self.name = name |
... | ... | |
104 | 104 |
return root |
105 | 105 | |
106 | 106 | |
107 |
class WorkCell(object):
|
|
107 |
class WorkCell: |
|
108 | 108 |
def __init__(self, worksheet, value, hint=None): |
109 | 109 |
self.value_type = 'string' |
110 | 110 |
if is_number(value): |
bijoe/visualization/signature.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import datetime |
|
18 | 17 |
import base64 |
19 |
import hmac
|
|
18 |
import datetime
|
|
20 | 19 |
import hashlib |
21 |
import urllib |
|
22 |
import random |
|
20 |
import hmac |
|
23 | 21 |
import logging |
22 |
import random |
|
23 |
import urllib |
|
24 | 24 | |
25 | 25 |
from django.utils import six |
26 | 26 |
from django.utils.encoding import force_bytes, smart_bytes |
... | ... | |
46 | 46 |
new_query = query |
47 | 47 |
if new_query: |
48 | 48 |
new_query += '&' |
49 |
new_query += urlencode(( |
|
50 |
('algo', algo), |
|
51 |
('timestamp', timestamp), |
|
52 |
('nonce', nonce))) |
|
49 |
new_query += urlencode((('algo', algo), ('timestamp', timestamp), ('nonce', nonce))) |
|
53 | 50 |
signature = base64.b64encode(sign_string(new_query, key, algo=algo)) |
54 | 51 |
new_query += '&signature=' + quote(signature) |
55 | 52 |
return new_query |
... | ... | |
71 | 68 |
if not res: |
72 | 69 |
key_hash = 'md5:%s' % hashlib.md5(force_bytes(key)).hexdigest()[:6] |
73 | 70 |
logging.getLogger(__name__).warning( |
74 |
'could not check signature of query %r with key %s: %s', query, key_hash, error) |
|
71 |
'could not check signature of query %r with key %s: %s', query, key_hash, error |
|
72 |
) |
|
75 | 73 |
return res |
76 | 74 | |
77 | 75 | |
... | ... | |
112 | 110 |
if len(signature2) != len(signature): |
113 | 111 |
return False |
114 | 112 |
res = 0 |
115 |
if six.PY3: |
|
116 |
for a, b in zip(signature, signature2): |
|
117 |
res |= a ^ b |
|
118 |
else: |
|
119 |
for a, b in zip(signature, signature2): |
|
120 |
res |= ord(a) ^ ord(b) |
|
113 |
for a, b in zip(signature, signature2): |
|
114 |
res |= a ^ b |
|
121 | 115 |
return res == 0 |
bijoe/visualization/urls.py | ||
---|---|---|
19 | 19 |
from . import views |
20 | 20 | |
21 | 21 |
urlpatterns = [ |
22 |
url(r'^$', |
|
23 |
views.visualizations, name='visualizations'), |
|
24 |
url(r'^json/$', |
|
25 |
views.visualizations_json, name='visualizations-json'), |
|
26 |
url(r'^import/$', |
|
27 |
views.visualizations_import, name='visualizations-import'), |
|
28 |
url(r'^export$', |
|
29 |
views.visualizations_export, name='visualizations-export'), |
|
22 |
url(r'^$', views.visualizations, name='visualizations'), |
|
23 |
url(r'^json/$', views.visualizations_json, name='visualizations-json'), |
|
24 |
url(r'^import/$', views.visualizations_import, name='visualizations-import'), |
|
25 |
url(r'^export$', views.visualizations_export, name='visualizations-export'), |
|
30 | 26 |
url(r'^warehouse/(?P<warehouse>[^/]*)/$', views.warehouse, name='warehouse'), |
31 | 27 |
url(r'^warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/$', views.cube, name='cube'), |
32 |
url(r'^warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/iframe/$', views.cube_iframe, |
|
33 |
name='cube-iframe'), |
|
34 |
url(r'warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/save/$', |
|
35 |
views.create_visualization, name='create-visualization'), |
|
28 |
url(r'^warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/iframe/$', views.cube_iframe, name='cube-iframe'), |
|
29 |
url( |
|
30 |
r'warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/save/$', |
|
31 |
views.create_visualization, |
|
32 |
name='create-visualization', |
|
33 |
), |
|
36 | 34 |
url(r'(?P<pk>\d+)/$', views.visualization, name='visualization'), |
37 | 35 |
url(r'(?P<pk>\d+)/json/$', views.visualization_json, name='visualization-json'), |
38 | 36 |
url(r'(?P<pk>\d+)/geojson/$', views.visualization_geojson, name='visualization-geojson'), |
bijoe/visualization/utils.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from __future__ import unicode_literals |
|
18 | 17 | |
19 |
import re |
|
20 |
import json |
|
21 |
import hashlib |
|
18 |
import collections |
|
19 |
import copy |
|
22 | 20 |
import datetime |
23 | 21 |
import decimal |
24 |
import copy |
|
25 |
import collections |
|
22 |
import hashlib |
|
23 |
import json |
|
24 |
import re |
|
26 | 25 | |
26 |
from django.conf import settings |
|
27 | 27 |
from django.core.cache import cache |
28 |
from django.http import Http404 |
|
28 | 29 |
from django.utils.encoding import force_bytes, force_text |
29 | 30 |
from django.utils.safestring import mark_safe |
30 | 31 |
from django.utils.translation import ugettext_lazy as _ |
31 |
from django.http import Http404 |
|
32 |
from django.conf import settings |
|
33 | 32 | |
33 |
from ..engine import Engine, MeasureCell, Member |
|
34 | 34 |
from ..utils import get_warehouses |
35 |
from ..engine import Engine, Member, MeasureCell |
|
36 | 35 |
from .ods import Workbook |
37 | 36 | |
38 | 37 | |
39 |
class Visualization(object): |
|
40 |
def __init__(self, cube, representation, measure, drilldown_x=None, drilldown_y=None, |
|
41 |
filters=None, loop=None): |
|
38 |
class Visualization: |
|
39 |
def __init__( |
|
40 |
self, cube, representation, measure, drilldown_x=None, drilldown_y=None, filters=None, loop=None |
|
41 |
): |
|
42 | 42 |
self.cube = cube |
43 | 43 |
self.representation = representation |
44 | 44 | |
... | ... | |
74 | 74 |
} |
75 | 75 | |
76 | 76 |
def copy(self): |
77 |
return Visualization(self.cube, self.representation, measure=self.measure, |
|
78 |
drilldown_x=self.drilldown_x, drilldown_y=self.drilldown_y, |
|
79 |
filters=copy.deepcopy(self.filters), loop=self.loop) |
|
77 |
return Visualization( |
|
78 |
self.cube, |
|
79 |
self.representation, |
|
80 |
measure=self.measure, |
|
81 |
drilldown_x=self.drilldown_x, |
|
82 |
drilldown_y=self.drilldown_y, |
|
83 |
filters=copy.deepcopy(self.filters), |
|
84 |
loop=self.loop, |
|
85 |
) |
|
80 | 86 | |
81 | 87 |
@staticmethod |
82 | 88 |
def get_cube(d, warehouses=None): |
... | ... | |
107 | 113 |
loop = d.get('loop') |
108 | 114 |
if loop: |
109 | 115 |
loop = cube.dimensions[loop] |
110 |
return cls(cube, representation, measure, drilldown_x=drilldown_x, drilldown_y=drilldown_y, |
|
111 |
filters=filters, loop=loop) |
|
116 |
return cls( |
|
117 |
cube, |
|
118 |
representation, |
|
119 |
measure, |
|
120 |
drilldown_x=drilldown_x, |
|
121 |
drilldown_y=drilldown_y, |
|
122 |
filters=filters, |
|
123 |
loop=loop, |
|
124 |
) |
|
112 | 125 | |
113 | 126 |
@classmethod |
114 | 127 |
def from_form(cls, cube, form): |
... | ... | |
126 | 139 |
drilldown_y = drilldown_y and cube.dimensions[drilldown_y] |
127 | 140 |
loop = cleaned_data.get('loop') |
128 | 141 |
loop = loop and cube.dimensions[loop] |
129 |
return cls(cube, cleaned_data['representation'], |
|
130 |
measure, |
|
131 |
drilldown_x=drilldown_x, |
|
132 |
drilldown_y=drilldown_y, |
|
133 |
filters=filters, loop=loop) |
|
142 |
return cls( |
|
143 |
cube, |
|
144 |
cleaned_data['representation'], |
|
145 |
measure, |
|
146 |
drilldown_x=drilldown_x, |
|
147 |
drilldown_y=drilldown_y, |
|
148 |
filters=filters, |
|
149 |
loop=loop, |
|
150 |
) |
|
134 | 151 | |
135 | 152 |
@property |
136 | 153 |
def key(self): |
... | ... | |
147 | 164 |
keys.append('$'.join([kw] + sorted(map(force_text, value)))) |
148 | 165 |
else: |
149 | 166 |
# scalar values |
150 |
keys.append(u'%s$%s' % (kw, force_text(value)))
|
|
167 |
keys.append('%s$%s' % (kw, force_text(value))) |
|
151 | 168 |
keys += [dim.name for dim in self.drilldown] |
152 | 169 |
keys += [self.measure.name] |
153 | 170 |
key = '$'.join(v.encode('utf8') for v in keys) |
154 | 171 |
return hashlib.md5(force_bytes(key)).hexdigest() |
155 | 172 | |
156 | 173 |
def data(self): |
157 |
'''Execute aggregation query, list members and check None values in |
|
158 |
dimensions. |
|
159 |
''' |
|
160 |
rows = list(self.cube.query(self.filters.items(), |
|
161 |
self.drilldown, |
|
162 |
[self.measure])) |
|
163 |
self.members = {dimension: list(dimension.members(filters=self.filters.items())) |
|
164 |
for dimension in self.drilldown} |
|
174 |
"""Execute aggregation query, list members and check None values in |
|
175 |
dimensions. |
|
176 |
""" |
|
177 |
rows = list(self.cube.query(self.filters.items(), self.drilldown, [self.measure])) |
|
178 |
self.members = { |
|
179 |
dimension: list(dimension.members(filters=self.filters.items())) for dimension in self.drilldown |
|
180 |
} |
|
165 | 181 |
seen_none = set() |
166 | 182 |
for cells in rows: |
167 | 183 |
# Keep "empty" dimension value if there is a non-zero measure associated |
... | ... | |
268 | 284 |
y_axis, grid = self.table_1d() |
269 | 285 |
table.append([self.drilldown_y.label, self.measure.label]) |
270 | 286 |
for y in y_axis: |
271 |
table.append([ |
|
272 |
y.label, |
|
273 |
'%s' % (grid[y.id],), |
|
274 |
]) |
|
287 |
table.append( |
|
288 |
[ |
|
289 |
y.label, |
|
290 |
'%s' % (grid[y.id],), |
|
291 |
] |
|
292 |
) |
|
275 | 293 |
else: |
276 | 294 |
table.append([self.measure.label, '%s' % (self.data()[0].measures[0],)]) |
277 | 295 | |
... | ... | |
296 | 314 |
if isinstance(value, decimal.Decimal): |
297 | 315 |
value = float(value) |
298 | 316 |
if isinstance(value, datetime.timedelta): |
299 |
value = value.days + value.seconds / 86400. |
|
317 |
value = value.days + value.seconds / 86400.0
|
|
300 | 318 |
return value |
301 | 319 | |
302 | 320 |
if len(self.drilldown) == 2: |
303 | 321 |
(x_axis, y_axis), grid = self.table_2d() |
304 |
cells = ((['%s' % x.label, '%s' % y.label], cell_value(grid[(x.id, y.id)])) for x in x_axis for y in y_axis) |
|
322 |
cells = ( |
|
323 |
(['%s' % x.label, '%s' % y.label], cell_value(grid[(x.id, y.id)])) |
|
324 |
for x in x_axis |
|
325 |
for y in y_axis |
|
326 |
) |
|
305 | 327 |
elif len(self.drilldown) == 1: |
306 | 328 |
axis, grid = self.table_1d() |
307 | 329 |
cells = ((['%s' % x.label], cell_value(grid[x.id])) for x in axis) |
... | ... | |
315 | 337 |
raise NotImplementedError |
316 | 338 | |
317 | 339 |
for coords, value in cells: |
318 |
json_data.append({ |
|
319 |
'coords': [{'value': coord} for coord in coords], |
|
320 |
'measures': [{'value': value}], |
|
321 |
}) |
|
340 |
json_data.append( |
|
341 |
{ |
|
342 |
'coords': [{'value': coord} for coord in coords], |
|
343 |
'measures': [{'value': value}], |
|
344 |
} |
|
345 |
) |
|
322 | 346 | |
323 | 347 |
return json_data |
324 | 348 | |
... | ... | |
350 | 374 |
l.append(self.drilldown_y.label) |
351 | 375 |
if self.loop: |
352 | 376 |
l.append(self.loop.label) |
353 |
return u', '.join(l)
|
|
377 |
return ', '.join(l) |
|
354 | 378 | |
355 | 379 |
def __iter__(self): |
356 | 380 |
if self.loop: |
bijoe/visualization/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from __future__ import unicode_literals |
|
18 | 17 | |
19 | 18 |
import hashlib |
20 | 19 |
import json |
21 | 20 | |
22 | 21 |
from django.conf import settings |
22 |
from django.contrib import messages |
|
23 | 23 |
from django.core import signing |
24 |
from django.core.exceptions import PermissionDenied |
|
24 | 25 |
from django.core.signing import BadSignature |
25 |
from django.contrib import messages |
|
26 |
from django.http import Http404, HttpResponse, JsonResponse |
|
27 |
from django.shortcuts import redirect |
|
28 |
from django.urls import reverse, reverse_lazy |
|
26 | 29 |
from django.utils.encoding import force_bytes, force_text |
27 | 30 |
from django.utils.text import slugify |
28 | 31 |
from django.utils.timezone import now |
29 |
from django.utils.translation import ungettext, ugettext as _ |
|
30 |
from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView |
|
31 |
from django.views.generic.list import MultipleObjectMixin |
|
32 |
from django.views.generic import DetailView, ListView, View, TemplateView |
|
33 |
from django.shortcuts import redirect |
|
34 |
from django.urls import reverse, reverse_lazy |
|
35 |
from django.http import HttpResponse, Http404, JsonResponse |
|
36 |
from django.core.exceptions import PermissionDenied |
|
32 |
from django.utils.translation import ugettext as _ |
|
33 |
from django.utils.translation import ungettext |
|
37 | 34 |
from django.views.decorators.clickjacking import xframe_options_exempt |
38 | ||
35 |
from django.views.generic import DetailView, ListView, TemplateView, View |
|
36 |
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView |
|
37 |
from django.views.generic.list import MultipleObjectMixin |
|
39 | 38 |
from django_select2.cache import cache |
40 | 39 |
from rest_framework import generics |
41 | 40 |
from rest_framework.response import Response |
42 | 41 | |
43 |
from bijoe.utils import get_warehouses, import_site, export_site |
|
42 |
from bijoe.utils import export_site, get_warehouses, import_site |
|
43 | ||
44 |
from .. import views |
|
44 | 45 |
from ..engine import Engine |
45 |
from . import models, forms, signature
|
|
46 |
from . import forms, models, signature
|
|
46 | 47 |
from .utils import Visualization |
47 |
from .. import views |
|
48 | 48 | |
49 | 49 | |
50 | 50 |
class WarehouseView(views.AuthorizationMixin, TemplateView): |
51 | 51 |
template_name = 'bijoe/warehouse.html' |
52 | 52 | |
53 | 53 |
def get_context_data(self, **kwargs): |
54 |
ctx = super(WarehouseView, self).get_context_data(**kwargs)
|
|
54 |
ctx = super().get_context_data(**kwargs) |
|
55 | 55 |
try: |
56 |
warehouse = [warehouse for warehouse in get_warehouses() |
|
57 |
if warehouse.name == self.kwargs['warehouse']][0] |
|
56 |
warehouse = [ |
|
57 |
warehouse for warehouse in get_warehouses() if warehouse.name == self.kwargs['warehouse'] |
|
58 |
][0] |
|
58 | 59 |
except IndexError: |
59 | 60 |
raise Http404 |
60 | 61 |
ctx['warehouse'] = Engine(warehouse) |
... | ... | |
63 | 64 |
return ctx |
64 | 65 | |
65 | 66 | |
66 |
class CubeDisplayMixin(object):
|
|
67 |
class CubeDisplayMixin: |
|
67 | 68 |
def get_context_data(self, **kwargs): |
68 |
ctx = super(CubeDisplayMixin, self).get_context_data(**kwargs)
|
|
69 |
ctx = super().get_context_data(**kwargs) |
|
69 | 70 |
ctx['warehouse'] = self.warehouse |
70 | 71 |
ctx['cube'] = self.cube |
71 | 72 |
ctx['visualization'] = self.visualization |
72 | 73 |
return ctx |
73 | 74 | |
74 | 75 | |
75 |
class CubeMixin(object):
|
|
76 |
class CubeMixin: |
|
76 | 77 |
def visualization(self, request, cube): |
77 | 78 |
self.form = forms.CubeForm(cube=self.cube, data=request.GET or request.POST) |
78 | 79 |
if self.form.is_valid(): |
... | ... | |
80 | 81 | |
81 | 82 |
def dispatch(self, request, *args, **kwargs): |
82 | 83 |
try: |
83 |
self.warehouse = Engine([warehouse for warehouse in get_warehouses() |
|
84 |
if warehouse.name == self.kwargs['warehouse']][0]) |
|
84 |
self.warehouse = Engine( |
|
85 |
[warehouse for warehouse in get_warehouses() if warehouse.name == self.kwargs['warehouse']][0] |
|
86 |
) |
|
85 | 87 |
except IndexError: |
86 | 88 |
raise Http404 |
87 | 89 |
try: |
... | ... | |
89 | 91 |
except KeyError: |
90 | 92 |
raise Http404 |
91 | 93 |
self.visualization = self.visualization(request, cube) |
92 |
return super(CubeMixin, self).dispatch(request, *args, **kwargs)
|
|
94 |
return super().dispatch(request, *args, **kwargs) |
|
93 | 95 | |
94 | 96 | |
95 | 97 |
class CubeView(views.AuthorizationMixin, CubeDisplayMixin, CubeMixin, TemplateView): |
... | ... | |
101 | 103 |
return self.get(request, *args, **kwargs) |
102 | 104 | |
103 | 105 |
def get_context_data(self, **kwargs): |
104 |
ctx = super(CubeView, self).get_context_data(**kwargs)
|
|
106 |
ctx = super().get_context_data(**kwargs) |
|
105 | 107 |
ctx['form'] = self.form |
106 | 108 |
return ctx |
107 | 109 | |
... | ... | |
115 | 117 |
def get(self, request, *args, **kwargs): |
116 | 118 |
if not self.visualization: |
117 | 119 |
return redirect('homepage') |
118 |
return super(CreateVisualizationView, self).get(request, *args, **kwargs)
|
|
120 |
return super().get(request, *args, **kwargs) |
|
119 | 121 | |
120 | 122 |
def form_valid(self, form): |
121 | 123 |
if not self.visualization: |
122 | 124 |
return redirect('homepage') |
123 | 125 |
form.instance.parameters = self.visualization.to_json() |
124 |
return super(CreateVisualizationView, self).form_valid(form)
|
|
126 |
return super().form_valid(form) |
|
125 | 127 | |
126 | 128 | |
127 | 129 |
class SaveAsVisualizationView(views.AuthorizationMixin, DetailView, CreateView): |
... | ... | |
132 | 134 | |
133 | 135 |
def form_valid(self, form): |
134 | 136 |
form.instance.parameters = self.get_object().parameters |
135 |
return super(SaveAsVisualizationView, self).form_valid(form)
|
|
137 |
return super().form_valid(form) |
|
136 | 138 | |
137 | 139 |
def get_initial(self): |
138 |
return { |
|
139 |
'name': '%s %s' % (self.get_object().name, _('(Copy)')) |
|
140 |
} |
|
140 |
return {'name': '%s %s' % (self.get_object().name, _('(Copy)'))} |
|
141 | 141 | |
142 | 142 | |
143 | 143 |
class VisualizationView(views.AuthorizationMixin, CubeDisplayMixin, DetailView): |
... | ... | |
145 | 145 |
template_name = 'bijoe/visualization.html' |
146 | 146 | |
147 | 147 |
def get_object(self): |
148 |
named_visualization = super(VisualizationView, self).get_object()
|
|
148 |
named_visualization = super().get_object() |
|
149 | 149 |
if not hasattr(self, 'visualization'): |
150 | 150 |
self.visualization = Visualization.from_json(named_visualization.parameters) |
151 | 151 |
self.cube = self.visualization.cube |
... | ... | |
165 | 165 |
return self.get(request, *args, **kwargs) |
166 | 166 | |
167 | 167 |
def get_context_data(self, **kwargs): |
168 |
ctx = super(VisualizationView, self).get_context_data(**kwargs)
|
|
168 |
ctx = super().get_context_data(**kwargs) |
|
169 | 169 |
initial = { |
170 | 170 |
'representation': self.visualization.representation, |
171 | 171 |
'measure': self.visualization.measure.name, |
... | ... | |
218 | 218 |
return self.model.all_visualizations() |
219 | 219 | |
220 | 220 |
def get_context_data(self, **kwargs): |
221 |
ctx = super(VisualizationsView, self).get_context_data(**kwargs)
|
|
221 |
ctx = super().get_context_data(**kwargs) |
|
222 | 222 |
ctx['request'] = self.request |
223 | 223 |
return ctx |
224 | 224 | |
... | ... | |
251 | 251 |
sig = hashlib.sha1(force_bytes(sig)).hexdigest() |
252 | 252 |
path += '?signature=' + sig |
253 | 253 |
data_uri = reverse('visualization-json', kwargs={'pk': visualization.pk}) |
254 |
data.append({ |
|
255 |
'name': visualization.name, |
|
256 |
'slug': visualization.slug, |
|
257 |
'path': request.build_absolute_uri(path), |
|
258 |
'data-url': request.build_absolute_uri(data_uri), |
|
259 |
}) |
|
254 |
data.append( |
|
255 |
{ |
|
256 |
'name': visualization.name, |
|
257 |
'slug': visualization.slug, |
|
258 |
'path': request.build_absolute_uri(path), |
|
259 |
'data-url': request.build_absolute_uri(data_uri), |
|
260 |
} |
|
261 |
) |
|
260 | 262 |
response = HttpResponse(content_type='application/json') |
261 | 263 |
response.write(json.dumps(data)) |
262 | 264 |
return response |
... | ... | |
294 | 296 |
for cell in row.dimensions: |
295 | 297 |
properties[cell.dimension.label] = '%s' % (cell,) |
296 | 298 |
points = row.measures[0].value or [] |
297 |
geojson['features'].append({ |
|
298 |
'type': 'Feature', |
|
299 |
'geometry': { |
|
300 |
'type': 'MultiPoint', |
|
301 |
'coordinates': [[coord for coord in point] for point in points], |
|
302 |
}, |
|
303 |
'properties': properties, |
|
304 |
}) |
|
299 |
geojson['features'].append( |
|
300 |
{ |
|
301 |
'type': 'Feature', |
|
302 |
'geometry': { |
|
303 |
'type': 'MultiPoint', |
|
304 |
'coordinates': [[coord for coord in point] for point in points], |
|
305 |
}, |
|
306 |
'properties': properties, |
|
307 |
} |
|
308 |
) |
|
305 | 309 |
return Response(geojson) |
306 | 310 | |
307 | 311 | |
... | ... | |
343 | 347 |
elif len(drilldowns) == 0: |
344 | 348 |
data = cell_value(visualization.data()[0].measures[0]) |
345 | 349 |
axis = {} |
346 |
loop.append({ |
|
347 |
'data': data, |
|
348 |
'axis': axis |
|
349 |
}) |
|
350 |
loop.append({'data': data, 'axis': axis}) |
|
350 | 351 | |
351 | 352 |
if not all_visualizations.loop: |
352 | 353 |
data = loop[0]['data'] |
353 | 354 |
axis = loop[0]['axis'] |
354 | 355 |
else: |
355 | 356 |
axis = loop[0]['axis'] |
356 |
axis['loop'] = [x.label for x in all_visualizations.loop.members(all_visualizations.filters.items())] |
|
357 |
axis['loop'] = [ |
|
358 |
x.label for x in all_visualizations.loop.members(all_visualizations.filters.items()) |
|
359 |
] |
|
357 | 360 |
data = [x['data'] for x in loop] |
358 | 361 | |
359 | 362 |
unit = 'seconds' if all_visualizations.measure.type == 'duration' else None |
360 | 363 |
measure = all_visualizations.measure.type |
361 | 364 | |
362 |
return Response({ |
|
363 |
'data': data, |
|
364 |
'axis': axis, |
|
365 |
'format': '1', |
|
366 |
'unit': unit, # legacy, prefer measure. |
|
367 |
'measure': measure, |
|
368 |
}) |
|
365 |
return Response( |
|
366 |
{ |
|
367 |
'data': data, |
|
368 |
'axis': axis, |
|
369 |
'format': '1', |
|
370 |
'unit': unit, # legacy, prefer measure. |
|
371 |
'measure': measure, |
|
372 |
} |
|
373 |
) |
|
369 | 374 | |
370 | 375 | |
371 | 376 |
class ExportVisualizationView(views.AuthorizationMixin, DetailView): |
... | ... | |
387 | 392 | |
388 | 393 |
def form_valid(self, form): |
389 | 394 |
try: |
390 |
visualizations_json = json.loads( |
|
391 |
force_text(self.request.FILES['visualizations_json'].read())) |
|
395 |
visualizations_json = json.loads(force_text(self.request.FILES['visualizations_json'].read())) |
|
392 | 396 |
except ValueError: |
393 | 397 |
form.add_error('visualizations_json', _('File is not in the expected JSON format.')) |
394 | 398 |
return self.form_invalid(form) |
... | ... | |
401 | 405 |
if results.get('created') == 0: |
402 | 406 |
message1 = _('No visualization created.') |
403 | 407 |
else: |
404 |
message1 = ungettext( |
|
405 |
'A visualization has been created.', |
|
406 |
'%(count)d visualizations have been created.', |
|
407 |
results['created']) % {'count': results['created']} |
|
408 |
message1 = ( |
|
409 |
ungettext( |
|
410 |
'A visualization has been created.', |
|
411 |
'%(count)d visualizations have been created.', |
|
412 |
results['created'], |
|
413 |
) |
|
414 |
% {'count': results['created']} |
|
415 |
) |
|
408 | 416 |
if results.get('updated') == 0: |
409 | 417 |
message2 = _('No visualization updated.') |
410 | 418 |
else: |
411 |
message2 = ungettext( |
|
412 |
'A visualization has been updated.', |
|
413 |
'%(count)d visualizations have been updated.', |
|
414 |
results['updated']) % {'count': results['updated']} |
|
415 |
messages.info(self.request, u'%s %s' % (message1, message2)) |
|
419 |
message2 = ( |
|
420 |
ungettext( |
|
421 |
'A visualization has been updated.', |
|
422 |
'%(count)d visualizations have been updated.', |
|
423 |
results['updated'], |
|
424 |
) |
|
425 |
% {'count': results['updated']} |
|
426 |
) |
|
427 |
messages.info(self.request, '%s %s' % (message1, message2)) |
|
416 | 428 | |
417 |
return super(VisualizationsImportView, self).form_valid(form)
|
|
429 |
return super().form_valid(form) |
|
418 | 430 | |
419 | 431 | |
420 | 432 |
class VisualizationsExportView(views.AuthorizationMixin, View): |
421 | ||
422 | 433 |
def get(self, request, *args, **kwargs): |
423 | 434 |
response = HttpResponse(content_type='application/json') |
424 | 435 |
response['Content-Disposition'] = ( |
... | ... | |
429 | 440 | |
430 | 441 | |
431 | 442 |
class Select2ChoicesView(View): |
432 | ||
433 | 443 |
def get(self, request, *args, **kwargs): |
434 | 444 |
widget = self.get_widget_or_404() |
435 | 445 |
try: |
436 |
warehouse = Engine([warehouse for warehouse in get_warehouses() |
|
437 |
if warehouse.name == widget.warehouse][0]) |
|
446 |
warehouse = Engine( |
|
447 |
[warehouse for warehouse in get_warehouses() if warehouse.name == widget.warehouse][0] |
|
448 |
) |
|
438 | 449 |
cube = warehouse[widget.cube] |
439 | 450 |
self.dimension = cube.dimensions[widget.dimension] |
440 | 451 |
except IndexError: |
... | ... | |
448 | 459 |
term = request.GET.get('term', '') |
449 | 460 |
choices = self.get_choices(term, page_number, widget.max_results) |
450 | 461 | |
451 |
return JsonResponse({ |
|
452 |
'results': [{'text': label, 'id': s} for s, label in choices], |
|
453 |
'more': not(len(choices) < widget.max_results), |
|
454 |
}) |
|
462 |
return JsonResponse( |
|
463 |
{ |
|
464 |
'results': [{'text': label, 'id': s} for s, label in choices], |
|
465 |
'more': not (len(choices) < widget.max_results), |
|
466 |
} |
|
467 |
) |
|
455 | 468 | |
456 | 469 |
def get_choices(self, term, page_number, max_results): |
457 | 470 |
members = [] |
... | ... | |
460 | 473 |
members.append((None, '__none__', _('None'))) |
461 | 474 | |
462 | 475 |
choices = [(s, label) for v, s, label in members if term in label.lower()] |
463 |
choices = choices[page_number * max_results:(page_number * max_results) + max_results]
|
|
476 |
choices = choices[page_number * max_results : (page_number * max_results) + max_results]
|
|
464 | 477 |
return choices |
465 | 478 | |
466 | 479 |
def get_widget_or_404(self): |
debian/debian_config.py | ||
---|---|---|
13 | 13 | |
14 | 14 |
# SAML2 authentication |
15 | 15 |
AUTHENTICATION_BACKENDS = ('mellon.backends.SAMLBackend',) |
16 |
MELLON_ATTRIBUTE_MAPPING = {
|
|
16 |
MELLON_ATTRIBUTE_MAPPING = { |
|
17 | 17 |
'email': '{attributes[email][0]}', |
18 | 18 |
'first_name': '{attributes[first_name][0]}', |
19 | 19 |
'last_name': '{attributes[last_name][0]}', |
20 | 20 |
} |
21 | 21 | |
22 |
MELLON_SUPERUSER_MAPPING = {
|
|
22 |
MELLON_SUPERUSER_MAPPING = { |
|
23 | 23 |
'is_superuser': 'true', |
24 | 24 |
} |
25 | 25 |
debian/scripts/warehouse_slug.py | ||
---|---|---|
10 | 10 |
from bijoe.utils import get_warehouses |
11 | 11 |
from bijoe.visualization.models import Visualization |
12 | 12 | |
13 | ||
14 | 13 |
warehouses = get_warehouses() |
15 | 14 |
for visu in Visualization.objects.all(): |
16 | 15 |
for warehouse in warehouses: |
debian/settings.py | ||
---|---|---|
14 | 14 |
# SECURITY WARNING: don't run with debug turned on in production! |
15 | 15 |
DEBUG = False |
16 | 16 | |
17 |
#ADMINS = ( |
|
17 |
# ADMINS = (
|
|
18 | 18 |
# # ('User 1', 'watchdog@example.net'), |
19 | 19 |
# # ('User 2', 'janitor@example.net'), |
20 |
#) |
|
20 |
# )
|
|
21 | 21 | |
22 | 22 |
# ALLOWED_HOSTS must be correct in production! |
23 | 23 |
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts |
24 | 24 |
ALLOWED_HOSTS = [ |
25 |
'*',
|
|
25 |
'*', |
|
26 | 26 |
] |
27 | 27 | |
28 | 28 |
# Databases |
format_json_fixtures.py | ||
---|---|---|
1 |
from __future__ import print_function |
|
2 | ||
3 | 1 |
import json |
4 | 2 |
import os |
5 | 3 |
setup.py | ||
---|---|---|
1 | 1 |
#! /usr/bin/env python |
2 | 2 | |
3 |
import sys |
|
4 |
import subprocess |
|
5 | 3 |
import os |
6 | ||
4 |
import subprocess |
|
5 |
import sys |
|
7 | 6 |
from distutils.cmd import Command |
8 | 7 |
from distutils.command.build import build as _build |
9 |
from setuptools import setup, find_packages |
|
10 |
from setuptools.command.sdist import sdist
|
|
8 | ||
9 |
from setuptools import find_packages, setup
|
|
11 | 10 |
from setuptools.command.install_lib import install_lib as _install_lib |
11 |
from setuptools.command.sdist import sdist |
|
12 | ||
12 | 13 | |
13 | 14 |
class eo_sdist(sdist): |
14 | 15 |
def run(self): |
... | ... | |
24 | 25 | |
25 | 26 | |
26 | 27 |
def get_version(): |
27 |
'''Use the VERSION, if absent generates a version with git describe, if not
|
|
28 |
tag exists, take 0.0.0- and add the length of the commit log.
|
|
29 |
'''
|
|
28 |
"""Use the VERSION, if absent generates a version with git describe, if not
|
|
29 |
tag exists, take 0.0.0- and add the length of the commit log. |
|
30 |
"""
|
|
30 | 31 |
if os.path.exists('VERSION'): |
31 |
with open('VERSION', 'r') as v:
|
|
32 |
with open('VERSION') as v: |
|
32 | 33 |
return v.read() |
33 | 34 |
if os.path.exists('.git'): |
34 |
p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, |
|
35 |
stderr=subprocess.PIPE) |
|
35 |
p = subprocess.Popen( |
|
36 |
['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, stderr=subprocess.PIPE |
|
37 |
) |
|
36 | 38 |
result = p.communicate()[0] |
37 | 39 |
if p.returncode == 0: |
38 | 40 |
result = result.decode('ascii').split()[0][1:] |
39 | 41 |
else: |
40 |
result = '0.0.0-%s' % len(subprocess.check_output( |
|
41 |
['git', 'rev-list', 'HEAD']).splitlines()) |
|
42 |
result = '0.0.0-%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines()) |
|
42 | 43 |
return result.replace('-', '.').replace('.g', '+g') |
43 | 44 |
return '0.0.0' |
44 | 45 | |
... | ... | |
56 | 57 |
def run(self): |
57 | 58 |
try: |
58 | 59 |
from django.core.management import call_command |
60 | ||
59 | 61 |
for path, dirs, files in os.walk('bijoe'): |
60 | 62 |
if 'locale' not in dirs: |
61 | 63 |
continue |
... | ... | |
77 | 79 |
_install_lib.run(self) |
78 | 80 | |
79 | 81 | |
80 |
setup(name="bijoe", |
|
81 |
version=get_version(), |
|
82 |
license="AGPLv3+", |
|
83 |
description="BI daashboard from PostgreSQL start schema", |
|
84 |
long_description=open('README.rst').read(), |
|
85 |
url="http://dev.entrouvert.org/projects/publik-bi/", |
|
86 |
author="Entr'ouvert", |
|
87 |
author_email="authentic@listes.entrouvert.com", |
|
88 |
maintainer="Benjamin Dauvergne", |
|
89 |
maintainer_email="bdauvergne@entrouvert.com", |
|
90 |
packages=find_packages(), |
|
91 |
include_package_data=True, |
|
92 |
install_requires=['requests', 'django>=1.11, <2.3', 'psycopg2', 'isodate', 'Django-Select2<6', |
|
93 |
'XStatic-ChartNew.js', 'gadjo', 'django-jsonfield<1.3', |
|
94 |
'python-dateutil', |
|
95 |
'djangorestframework', |
|
96 |
'xstatic-select2'], |
|
97 |
scripts=['manage.py'], |
|
98 |
cmdclass={ |
|
99 |
'sdist': eo_sdist, |
|
100 |
'build': build, |
|
101 |
'install_lib': install_lib, |
|
102 |
'compile_translations': compile_translations, |
|
103 |
}) |
|
82 |
setup( |
|
83 |
name="bijoe", |
|
84 |
version=get_version(), |
|
85 |
license="AGPLv3+", |
|
86 |
description="BI daashboard from PostgreSQL start schema", |
|
87 |
long_description=open('README.rst').read(), |
|
88 |
url="http://dev.entrouvert.org/projects/publik-bi/", |
|
89 |
author="Entr'ouvert", |
|
90 |
author_email="authentic@listes.entrouvert.com", |
|
91 |
maintainer="Benjamin Dauvergne", |
|
92 |
maintainer_email="bdauvergne@entrouvert.com", |
|
93 |
packages=find_packages(), |
|
94 |
include_package_data=True, |
|
95 |
install_requires=[ |
|
96 |
'requests', |
|
97 |
'django>=1.11, <2.3', |
|
98 |
'psycopg2', |
|
99 |
'isodate', |
|
100 |
'Django-Select2<6', |
|
101 |
'XStatic-ChartNew.js', |
|
102 |
'gadjo', |
|
103 |
'django-jsonfield<1.3', |
|
104 |
'python-dateutil', |
|
105 |
'djangorestframework', |
|
106 |
'xstatic-select2', |
|
107 |
], |
|
108 |
scripts=['manage.py'], |
|
109 |
cmdclass={ |
|
110 |
'sdist': eo_sdist, |
|
111 |
'build': build, |
|
112 |
'install_lib': install_lib, |
|
113 |
'compile_translations': compile_translations, |
|
114 |
}, |
|
115 |
) |
tests/conftest.py | ||
---|---|---|
1 |
import os |
|
2 | 1 |
import glob |
3 | 2 |
import json |
4 |
from contextlib import closing, contextmanager |
|
3 |
import os |
|
4 |
import shutil |
|
5 | 5 |
import subprocess |
6 | 6 |
import tempfile |
7 |
import shutil |
|
8 | ||
9 |
import pytest |
|
7 |
from contextlib import closing, contextmanager |
|
10 | 8 | |
11 | 9 |
import django_webtest |
12 | ||
13 | 10 |
import psycopg2 |
14 | ||
15 | ||
16 |
from django.db import transaction |
|
11 |
import pytest |
|
17 | 12 |
from django.contrib.auth.models import User |
18 | 13 |
from django.core.management import call_command |
14 |
from django.db import transaction |
|
19 | 15 | |
20 | 16 | |
21 | 17 |
def pytest_addoption(parser): |
22 | 18 |
parser.addoption( |
23 |
'--bijoe-store-table', action='store_true', default=False, help='Store tables value in new_tables.json', |
|
19 |
'--bijoe-store-table', |
|
20 |
action='store_true', |
|
21 |
default=False, |
|
22 |
help='Store tables value in new_tables.json', |
|
24 | 23 |
) |
25 | 24 | |
26 | 25 | |
... | ... | |
44 | 43 | |
45 | 44 |
@pytest.fixture |
46 | 45 |
def admin(db): |
47 |
u = User(username='super.user', first_name='Super', last_name='User', |
|
48 |
email='super.user@example.net') |
|
46 |
u = User(username='super.user', first_name='Super', last_name='User', email='super.user@example.net') |
|
49 | 47 |
u.set_password('super.user') |
50 | 48 |
u.is_superuser = True |
51 | 49 |
u.is_staff = True |
52 | 50 |
u.save() |
53 | 51 |
return u |
54 | 52 | |
53 | ||
55 | 54 |
SCHEMA_PATHS = os.path.join(os.path.dirname(__file__), 'fixtures/') |
56 | 55 | |
57 | 56 | |
... | ... | |
83 | 82 | |
84 | 83 |
# load data |
85 | 84 |
for sql_path in sorted(glob.glob(os.path.join(schema_dir, '*.sql'))): |
86 |
process = subprocess.Popen(['psql', '-c', '\\set ON_ERROR_STOP on', '--single-transaction', database_name, '-f', sql_path], |
|
87 |
stdout=subprocess.PIPE, |
|
88 |
stderr=subprocess.PIPE) |
|
85 |
process = subprocess.Popen( |
|
86 |
[ |
|
87 |
'psql', |
|
88 |
'-c', |
|
89 |
'\\set ON_ERROR_STOP on', |
|
90 |
'--single-transaction', |
|
91 |
database_name, |
|
92 |
'-f', |
|
93 |
sql_path, |
|
94 |
], |
|
95 |
stdout=subprocess.PIPE, |
|
96 |
stderr=subprocess.PIPE, |
|
97 |
) |
|
89 | 98 |
stdout, stderr = process.communicate() |
90 | 99 |
return_code = process.returncode |
91 | 100 |
assert return_code == 0, [stdout, stderr] |
... | ... | |
97 | 106 |
'schema_dir': schema_dir, |
98 | 107 |
'database_name': database_name, |
99 | 108 |
'bijoe_schemas': [os.path.join(bijoe_schema_dir, '*_schema.json')], |
100 |
'fixtures': fixtures |
|
109 |
'fixtures': fixtures,
|
|
101 | 110 |
} |
102 | 111 |
tables_path = os.path.join(schema_dir, 'tables.json') |
103 | 112 |
if os.path.exists(tables_path): |
tests/test_bijoe_schemas.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
from bijoe.schemas import Warehouse |
3 | 2 | |
4 | 3 | |
5 | 4 |
def test_simple_parsing(): |
6 |
Warehouse.from_json({ |
|
7 |
'name': 'coin', |
|
8 |
'label': 'coin', |
|
9 |
'pg_dsn': 'dbname=zozo', |
|
10 |
'search_path': ['cam', 'public'], |
|
11 |
'cubes': [ |
|
12 |
{ |
|
13 |
'name': 'all_formdata', |
|
14 |
'label': 'Tous les formulaires', |
|
15 |
'fact_table': 'formdata', |
|
16 |
'key': 'id', |
|
17 |
'joins': [ |
|
18 |
{ |
|
19 |
'name': 'formdef', |
|
20 |
'master': '{fact_table}.formdef_id', |
|
21 |
'table': 'formdef', |
|
22 |
'detail': 'formdef.id', |
|
23 |
} |
|
24 |
], |
|
25 |
'dimensions': [ |
|
26 |
{ |
|
27 |
'label': 'formulaire', |
|
28 |
'name': 'formdef', |
|
29 |
'type': 'integer', |
|
30 |
'join': ['formdef'], |
|
31 |
'value': 'formdef.id', |
|
32 |
'value_label': 'formdef.label', |
|
33 |
'order_by': 'formdef.label' |
|
34 |
}, |
|
35 |
{ |
|
36 |
'name': 'receipt_time', |
|
37 |
'label': 'date de soumission', |
|
38 |
'join': ['receipt_time'], |
|
39 |
'type': 'date', |
|
40 |
'value': 'receipt_time.date' |
|
41 |
} |
|
42 |
], |
|
43 |
'measures': [ |
|
44 |
{ |
|
45 |
'type': 'integer', |
|
46 |
'label': 'Nombre de demandes', |
|
47 |
'expression': 'count({fact_table}.id)', |
|
48 |
'name': 'count' |
|
49 |
}, |
|
50 |
{ |
|
51 |
'type': 'integer', |
|
52 |
'label': u'Délai de traitement', |
|
53 |
'expression': 'avg((to_char(endpoint_delay, \'9999.999\') || \' days\')::interval)', |
|
54 |
'name': 'avg_endpoint_delay' |
|
55 |
}, |
|
56 |
{ |
|
57 |
'type': 'percent', |
|
58 |
'label': 'Pourcentage', |
|
59 |
'expression': 'count({fact_table}.id) * 100. / (select count({fact_table}.id) from {table_expression} where {where_conditions})', |
|
60 |
'name': 'percentage' |
|
61 |
} |
|
62 |
] |
|
63 |
} |
|
64 |
], |
|
65 |
}) |
|
5 |
Warehouse.from_json( |
|
6 |
{ |
|
7 |
'name': 'coin', |
|
8 |
'label': 'coin', |
|
9 |
'pg_dsn': 'dbname=zozo', |
|
10 |
'search_path': ['cam', 'public'], |
|
11 |
'cubes': [ |
|
12 |
{ |
|
13 |
'name': 'all_formdata', |
|
14 |
'label': 'Tous les formulaires', |
|
15 |
'fact_table': 'formdata', |
|
16 |
'key': 'id', |
|
17 |
'joins': [ |
|
18 |
{ |
|
19 |
'name': 'formdef', |
|
20 |
'master': '{fact_table}.formdef_id', |
|
21 |
'table': 'formdef', |
|
22 |
'detail': 'formdef.id', |
|
23 |
} |
|
24 |
], |
|
25 |
'dimensions': [ |
|
26 |
{ |
|
27 |
'label': 'formulaire', |
|
28 |
'name': 'formdef', |
|
29 |
'type': 'integer', |
|
30 |
'join': ['formdef'], |
|
31 |
'value': 'formdef.id', |
|
32 |
'value_label': 'formdef.label', |
|
33 |
'order_by': 'formdef.label', |
|
34 |
}, |
|
35 |
{ |
|
36 |
'name': 'receipt_time', |
|
37 |
'label': 'date de soumission', |
|
38 |
'join': ['receipt_time'], |
|
39 |
'type': 'date', |
|
40 |
'value': 'receipt_time.date', |
|
41 |
}, |
|
42 |
], |
|
43 |
'measures': [ |
|
44 |
{ |
|
45 |
'type': 'integer', |
|
46 |
'label': 'Nombre de demandes', |
|
47 |
'expression': 'count({fact_table}.id)', |
|
48 |
'name': 'count', |
|
49 |
}, |
|
50 |
{ |
|
51 |
'type': 'integer', |
|
52 |
'label': 'Délai de traitement', |
|
53 |
'expression': ( |
|
54 |
'avg((to_char(endpoint_delay, \'9999.999\') || \' days\')::interval)' |
|
55 |
), |
|
56 |
'name': 'avg_endpoint_delay', |
|
57 |
}, |
|
58 |
{ |
|
59 |
'type': 'percent', |
|
60 |
'label': 'Pourcentage', |
|
61 |
'expression': ( |
|
62 |
'count({fact_table}.id) * 100. / (select count({fact_table}.id) from' |
|
63 |
' {table_expression} where {where_conditions})' |
|
64 |
), |
|
65 |
'name': 'percentage', |
|
66 |
}, |
|
67 |
], |
|
68 |
} |
|
69 |
], |
|
70 |
} |
|
71 |
) |
tests/test_hobo_agent.py | ||
---|---|---|
20 | 20 |
def test_schema_from_url(): |
21 | 21 |
for hash_length in [4, 5, 6, 7]: |
22 | 22 |
for length in [64, 65, 66]: |
23 |
assert len(hobo_deploy.schema_from_url('https://' + ('x' * length), hash_length=hash_length)) == 63 |
|
23 |
assert ( |
|
24 |
len(hobo_deploy.schema_from_url('https://' + ('x' * length), hash_length=hash_length)) == 63 |
|
25 |
) |
|
24 | 26 | |
25 |
schema = hobo_deploy.schema_from_url('https://demarches-saint-didier-au-mont-dor.guichet-recette.grandlyon.com/') |
|
27 |
schema = hobo_deploy.schema_from_url( |
|
28 |
'https://demarches-saint-didier-au-mont-dor.guichet-recette.grandlyon.com/' |
|
29 |
) |
|
26 | 30 | |
27 | 31 |
assert len(schema) == 63 |
28 | 32 |
assert schema == 'demarches_saint_didier_au_mo0757cfguichet_recette_grandlyon_com' |
tests/test_hobo_deploy.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
# bijoe - BI dashboard |
3 | 2 |
# Copyright (C) 2015 Entr'ouvert |
4 | 3 |
# |
... | ... | |
17 | 16 | |
18 | 17 |
from contextlib import contextmanager |
19 | 18 | |
20 |
from psycopg2.extensions import parse_dsn |
|
21 | ||
22 | 19 |
import pytest |
23 | ||
24 |
from django.utils.six.moves import configparser as ConfigParser |
|
25 | ||
26 | 20 |
import sentry_sdk |
21 |
from django.utils.six.moves import configparser as ConfigParser |
|
22 |
from psycopg2.extensions import parse_dsn |
|
27 | 23 | |
28 | 24 |
from bijoe.hobo_agent.management.commands import hobo_deploy |
29 | 25 | |
... | ... | |
33 | 29 |
yield |
34 | 30 | |
35 | 31 | |
36 |
class FakeTenant(object):
|
|
32 |
class FakeTenant: |
|
37 | 33 |
domain_url = 'fake.tenant.com' |
38 | 34 | |
39 | 35 |
def __init__(self, directory): |
... | ... | |
45 | 41 | |
46 | 42 |
@pytest.fixture |
47 | 43 |
def sentry(): |
48 |
sentry_sdk.init( |
|
49 |
dsn='https://1234@sentry.example.com/1', |
|
50 |
environment='prod') |
|
44 |
sentry_sdk.init(dsn='https://1234@sentry.example.com/1', environment='prod') |
|
51 | 45 |
yield |
52 | 46 |
sentry_sdk.init() |
53 | 47 |
tests/test_import_export.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | ||
3 |
from __future__ import unicode_literals |
|
4 | ||
5 | 1 |
import json |
6 | 2 |
import os |
7 | 3 |
import shutil |
... | ... | |
13 | 9 |
from django.utils.encoding import force_bytes |
14 | 10 |
from django.utils.six import StringIO |
15 | 11 | |
16 |
from bijoe.visualization.models import Visualization |
|
17 | 12 |
from bijoe.utils import import_site |
13 |
from bijoe.visualization.models import Visualization |
|
18 | 14 | |
19 | 15 |
pytestmark = pytest.mark.django_db |
20 | 16 | |
... | ... | |
35 | 31 |
'representation': 'table', |
36 | 32 |
'loop': '', |
37 | 33 |
'filters': {}, |
38 |
'drilldown_x': 'date__yearmonth' |
|
34 |
'drilldown_x': 'date__yearmonth',
|
|
39 | 35 |
} |
40 | 36 | |
41 | 37 |
def create_visu(i=0): |
tests/test_relative_time.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | 1 |
from datetime import date |
2 | ||
3 | 3 |
from bijoe.relative_time import RelativeDate |
4 | 4 | |
5 | 5 | |
6 | 6 |
def test_relative_date(): |
7 | 7 |
today = date(2016, 3, 3) |
8 |
assert RelativeDate(u'cette année', today=today) == date(2016, 1, 1) |
|
9 |
assert RelativeDate(u'ce mois', today=today) == date(2016, 3, 1) |
|
10 |
assert RelativeDate(u'le mois dernier', today=today) == date(2016, 2, 1) |
|
11 |
assert RelativeDate(u'les 4 derniers mois', today=today) == date(2015, 11, 1) |
|
12 |
assert RelativeDate(u'le mois prochain', today=today) == date(2016, 4, 1) |
|
13 |
assert RelativeDate(u'les 3 prochains mois', today=today) == date(2016, 6, 1) |
|
14 |
assert RelativeDate(u' cette semaine', today=today) == date(2016, 2, 29) |
|
15 |
assert RelativeDate(u' maintenant', today=today) == today |
|
16 |
assert RelativeDate(u'2016-01-01', today=today) == date(2016, 1, 1) |
|
8 |
assert RelativeDate('cette année', today=today) == date(2016, 1, 1) |
|
9 |
assert RelativeDate('ce mois', today=today) == date(2016, 3, 1) |
|
10 |
assert RelativeDate('le mois dernier', today=today) == date(2016, 2, 1) |
|
11 |
assert RelativeDate('les 4 derniers mois', today=today) == date(2015, 11, 1) |
|
12 |
assert RelativeDate('le mois prochain', today=today) == date(2016, 4, 1) |
|
13 |
assert RelativeDate('les 3 prochains mois', today=today) == date(2016, 6, 1) |
|
14 |
assert RelativeDate(' cette semaine', today=today) == date(2016, 2, 29) |
|
15 |
assert RelativeDate(' maintenant', today=today) == today |
|
16 |
assert RelativeDate('2016-01-01', today=today) == date(2016, 1, 1) |
tests/test_schema1.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | ||
3 | 1 |
import json |
4 | 2 | |
5 |
from utils import login, get_table, get_ods_table, get_ods_document, request_select2
|
|
3 |
from utils import get_ods_document, get_ods_table, get_table, login, request_select2
|
|
6 | 4 | |
7 |
from bijoe.visualization.ods import OFFICE_NS, TABLE_NS |
|
8 | 5 |
from bijoe.visualization.models import Visualization as VisualizationModel |
6 |
from bijoe.visualization.ods import OFFICE_NS, TABLE_NS |
|
9 | 7 |
from bijoe.visualization.utils import Visualization |
10 | 8 | |
11 | 9 | |
... | ... | |
15 | 13 |
response = response.click('schema1') |
16 | 14 |
response = response.click('Facts 1') |
17 | 15 |
assert 'big-msg-info' in response |
18 |
assert u'le champ « pouët »' in response
|
|
16 |
assert 'le champ « pouët »' in response |
|
19 | 17 |
assert 'warning2' in response |
20 | 18 |
form = response.form |
21 | 19 |
form.set('representation', 'table') |
... | ... | |
24 | 22 |
response = form.submit('visualize') |
25 | 23 |
assert 'big-msg-info' not in response |
26 | 24 |
assert get_table(response) == [ |
27 |
['Inner SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98', |
|
28 |
u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91'], |
|
29 |
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15'] |
|
25 |
[ |
|
26 |
'Inner SubCategory', |
|
27 |
'sub\xe910', |
|
28 |
'sub\xe911', |
|
29 |
'sub\xe94', |
|
30 |
'sub\xe95', |
|
31 |
'sub\xe96', |
|
32 |
'sub\xe98', |
|
33 |
'sub\xe99', |
|
34 |
'sub\xe97', |
|
35 |
'sub\xe92', |
|
36 |
'sub\xe93', |
|
37 |
'sub\xe91', |
|
38 |
], |
|
39 |
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15'], |
|
30 | 40 |
] |
31 | 41 |
form = response.form |
32 | 42 |
form.set('representation', 'table') |
... | ... | |
35 | 45 |
response = form.submit('visualize') |
36 | 46 |
assert 'big-msg-info' not in response |
37 | 47 |
assert get_table(response) == [ |
38 |
['mois (Date)', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt'],
|
|
48 |
['mois (Date)', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt'],
|
|
39 | 49 |
['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'], |
40 | 50 |
] |
41 | 51 | |
... | ... | |
54 | 64 |
freezer.move_to('2019-01-01 01:00:00') |
55 | 65 |
response = form.submit('visualize') |
56 | 66 |
assert get_table(response) == [ |
57 |
['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'],
|
|
67 |
['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'],
|
|
58 | 68 |
['2017', '0', '0', '0', '0', '0', '0', '0', '0', '0'], |
59 | 69 |
] |
60 | 70 | |
61 | 71 |
freezer.move_to('2018-01-01 01:00:00') |
62 | 72 |
response = form.submit('visualize') |
63 | 73 |
assert get_table(response) == [ |
64 |
['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'],
|
|
74 |
['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'],
|
|
65 | 75 |
['2017', '10', '1', '1', '1', '1', '1', '1', '1', '17'], |
66 | 76 |
] |
67 | 77 | |
... | ... | |
77 | 87 |
form.set('drilldown_x', 'boolean') |
78 | 88 |
response = form.submit('visualize') |
79 | 89 |
assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '9']] |
80 |
form['filter__boolean'].force_value([o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0]) |
|
90 |
form['filter__boolean'].force_value( |
|
91 |
[o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0] |
|
92 |
) |
|
81 | 93 |
response = form.submit('visualize') |
82 | 94 |
assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '0']] |
83 | 95 | |
... | ... | |
92 | 104 |
form.set('measure', 'simple_count') |
93 | 105 |
form.set('drilldown_x', 'string') |
94 | 106 |
response = form.submit('visualize') |
95 |
assert get_table(response) == [['String', 'a', 'b', 'c', 'Aucun(e)'], ['number of rows', '11', '2', '3', '1']] |
|
107 |
assert get_table(response) == [ |
|
108 |
['String', 'a', 'b', 'c', 'Aucun(e)'], |
|
109 |
['number of rows', '11', '2', '3', '1'], |
|
110 |
] |
|
96 | 111 |
form['filter__string'].force_value(['a', 'b', '__none__']) |
97 | 112 |
response = form.submit('visualize') |
98 | 113 |
assert get_table(response) == [['String', 'a', 'b', 'Aucun(e)'], ['number of rows', '11', '2', '1']] |
... | ... | |
100 | 115 | |
101 | 116 |
def test_string_dimension_json_data(schema1, app, admin): |
102 | 117 |
# test conversion to Javascript declaration |
103 |
visu = Visualization.from_json({ |
|
104 |
'warehouse': 'schema1', |
|
105 |
'cube': 'facts1', |
|
106 |
'representation': 'table', |
|
107 |
'measure': 'simple_count', |
|
108 |
'drilldown_x': 'string' |
|
109 |
}) |
|
118 |
visu = Visualization.from_json( |
|
119 |
{ |
|
120 |
'warehouse': 'schema1', |
|
121 |
'cube': 'facts1', |
|
122 |
'representation': 'table', |
|
123 |
'measure': 'simple_count', |
|
124 |
'drilldown_x': 'string', |
|
125 |
} |
|
126 |
) |
|
110 | 127 |
assert json.loads(json.dumps(visu.json_data())) == [ |
111 |
{u'coords': [{u'value': u'a'}], u'measures': [{u'value': 11}]},
|
|
112 |
{u'coords': [{u'value': u'b'}], u'measures': [{u'value': 2}]},
|
|
113 |
{u'coords': [{u'value': u'c'}], u'measures': [{u'value': 3}]},
|
|
114 |
{u'coords': [{u'value': u'Aucun(e)'}], u'measures': [{u'value': 1}]}
|
|
128 |
{'coords': [{'value': 'a'}], 'measures': [{'value': 11}]},
|
|
129 |
{'coords': [{'value': 'b'}], 'measures': [{'value': 2}]},
|
|
130 |
{'coords': [{'value': 'c'}], 'measures': [{'value': 3}]},
|
|
131 |
{'coords': [{'value': 'Aucun(e)'}], 'measures': [{'value': 1}]},
|
|
115 | 132 |
] |
116 | 133 | |
117 | 134 | |
... | ... | |
126 | 143 |
form.set('drilldown_x', 'outersubcategory') |
127 | 144 |
response = form.submit('visualize') |
128 | 145 |
assert get_table(response) == [ |
129 |
['Outer SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98', |
|
130 |
u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91', 'Aucun(e)'], |
|
131 |
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1'] |
|
146 |
[ |
|
147 |
'Outer SubCategory', |
|
148 |
'sub\xe910', |
|
149 |
'sub\xe911', |
|
150 |
'sub\xe94', |
|
151 |
'sub\xe95', |
|
152 |
'sub\xe96', |
|
153 |
'sub\xe98', |
|
154 |
'sub\xe99', |
|
155 |
'sub\xe97', |
|
156 |
'sub\xe92', |
|
157 |
'sub\xe93', |
|
158 |
'sub\xe91', |
|
159 |
'Aucun(e)', |
|
160 |
], |
|
161 |
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1'], |
|
132 | 162 |
] |
133 | 163 |
form['filter__outersubcategory'].force_value(['__none__']) |
134 | 164 |
response = form.submit('visualize') |
135 |
assert get_table(response) == [ |
|
136 |
['Outer SubCategory', 'Aucun(e)'], |
|
137 |
['number of rows', '1'] |
|
138 |
] |
|
165 |
assert get_table(response) == [['Outer SubCategory', 'Aucun(e)'], ['number of rows', '1']] |
|
139 | 166 | |
140 | 167 | |
141 | 168 |
def test_yearmonth_drilldown(schema1, app, admin): |
... | ... | |
149 | 176 |
form.set('drilldown_x', 'date__yearmonth') |
150 | 177 |
response = form.submit('visualize') |
151 | 178 |
assert get_table(response) == [ |
152 |
[u'ann\xe9e et mois (Date)', '01/2017', '02/2017', '03/2017', |
|
153 |
'04/2017', '05/2017', '06/2017', '07/2017', '08/2017'], |
|
154 |
['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'] |
|
179 |
[ |
|
180 |
'ann\xe9e et mois (Date)', |
|
181 |
'01/2017', |
|
182 |
'02/2017', |
|
183 |
'03/2017', |
|
184 |
'04/2017', |
|
185 |
'05/2017', |
|
186 |
'06/2017', |
|
187 |
'07/2017', |
|
188 |
'08/2017', |
|
189 |
], |
|
190 |
['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'], |
|
155 | 191 |
] |
156 | 192 | |
157 | 193 | |
... | ... | |
196 | 232 |
freezer.move_to('2019-01-01 01:00:00') |
197 | 233 |
response = form.submit('visualize') |
198 | 234 |
assert get_table(response) == [ |
199 |
['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'],
|
|
235 |
['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'],
|
|
200 | 236 |
['2017', '0', '0', '0', '0', '0', '0', '0', '0', '0'], |
201 | 237 |
] |
202 | 238 |
freezer.move_to('2018-01-01 01:00:00') |
203 | 239 |
response = form.submit('visualize') |
204 | 240 |
assert get_table(response) == [ |
205 |
['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'],
|
|
241 |
['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'],
|
|
206 | 242 |
['2017', '10', '1', '1', '1', '1', '1', '1', '1', '17'], |
207 | 243 |
] |
208 | 244 | |
209 | 245 | |
210 | 246 |
def test_none_percent_json_data_0d(schema1, app, admin): |
211 | 247 |
# test conversion to Javascript declaration |
212 |
visu = Visualization.from_json({ |
|
213 |
'warehouse': 'schema1', |
|
214 |
'cube': 'facts1', |
|
215 |
'representation': 'graphical', |
|
216 |
'measure': 'percent', |
|
217 |
}) |
|
218 |
assert visu.json_data() == [{u'coords': [], u'measures': [{u'value': 100.0}]}] |
|
248 |
visu = Visualization.from_json( |
|
249 |
{ |
|
250 |
'warehouse': 'schema1', |
|
251 |
'cube': 'facts1', |
|
252 |
'representation': 'graphical', |
|
253 |
'measure': 'percent', |
|
254 |
} |
|
255 |
) |
|
256 |
assert visu.json_data() == [{'coords': [], 'measures': [{'value': 100.0}]}] |
|
219 | 257 | |
220 | 258 | |
221 | 259 |
def test_none_percent_json_data_2d(schema1, app, admin): |
222 | 260 |
# test conversion to Javascript declaration |
223 |
visu = Visualization.from_json({ |
|
224 |
'warehouse': 'schema1', |
|
225 |
'cube': 'facts1', |
|
226 |
'representation': 'graphical', |
|
227 |
'measure': 'percent', |
|
228 |
'drilldown_y': 'leftcategory', |
|
229 |
'drilldown_x': 'date__year', |
|
230 |
}) |
|
231 |
assert visu.json_data() == [ |
|
232 |
{ |
|
233 |
'coords': [{'value': u'2017'}, {'value': u'cat\xe92'}], |
|
234 |
'measures': [{'value': 0}] |
|
235 |
}, |
|
236 |
{ |
|
237 |
'coords': [{'value': u'2017'}, {'value': u'cat\xe93'}], |
|
238 |
'measures': [{'value': 0}]}, |
|
239 |
{ |
|
240 |
'coords': [{'value': u'2017'}, {'value': u'cat\xe91'}], |
|
241 |
'measures': [{'value': 94.11764705882354}]}, |
|
261 |
visu = Visualization.from_json( |
|
242 | 262 |
{ |
243 |
'coords': [{'value': u'2017'}, {'value': u'Aucun(e)'}], |
|
244 |
'measures': [{'value': 5.882352941176471}] |
|
263 |
'warehouse': 'schema1', |
|
264 |
'cube': 'facts1', |
|
265 |
'representation': 'graphical', |
|
266 |
'measure': 'percent', |
|
267 |
'drilldown_y': 'leftcategory', |
|
268 |
'drilldown_x': 'date__year', |
|
245 | 269 |
} |
270 |
) |
|
271 |
assert visu.json_data() == [ |
|
272 |
{'coords': [{'value': '2017'}, {'value': 'cat\xe92'}], 'measures': [{'value': 0}]}, |
|
273 |
{'coords': [{'value': '2017'}, {'value': 'cat\xe93'}], 'measures': [{'value': 0}]}, |
|
274 |
{'coords': [{'value': '2017'}, {'value': 'cat\xe91'}], 'measures': [{'value': 94.11764705882354}]}, |
|
275 |
{'coords': [{'value': '2017'}, {'value': 'Aucun(e)'}], 'measures': [{'value': 5.882352941176471}]}, |
|
246 | 276 |
] |
247 | 277 | |
248 | 278 | |
... | ... | |
258 | 288 |
'measure': 'percent', |
259 | 289 |
'drilldown_y': 'outercategory', |
260 | 290 |
'drilldown_x': 'date__year', |
261 |
}) |
|
291 |
}, |
|
292 |
) |
|
262 | 293 |
response = app.get('/visualization/%d/geojson/' % visu.pk) |
263 | 294 |
assert response.json == { |
264 | 295 |
'type': 'FeatureCollection', |
265 |
'features': [{ |
|
266 |
u'geometry': { |
|
267 |
u'coordinates': [ |
|
268 |
[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], |
|
269 |
[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], |
|
270 |
[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0] |
|
271 |
], |
|
272 |
u'type': u'MultiPoint' |
|
296 |
'features': [ |
|
297 |
{ |
|
298 |
'geometry': { |
|
299 |
'coordinates': [ |
|
300 |
[1.0, 1.0], |
|
301 |
[1.0, 1.0], |
|
302 |
[1.0, 1.0], |
|
303 |
[1.0, 1.0], |
|
304 |
[1.0, 1.0], |
|
305 |
[1.0, 1.0], |
|
306 |
[1.0, 1.0], |
|
307 |
[1.0, 1.0], |
|
308 |
[1.0, 1.0], |
|
309 |
[1.0, 1.0], |
|
310 |
[1.0, 1.0], |
|
311 |
[1.0, 1.0], |
|
312 |
[1.0, 1.0], |
|
313 |
[1.0, 1.0], |
|
314 |
[1.0, 1.0], |
|
315 |
[1.0, 1.0], |
|
316 |
], |
|
317 |
'type': 'MultiPoint', |
|
318 |
}, |
|
319 |
'properties': {'Outer Category': 'cat\xe91', 'ann\xe9e (Date)': '2017'}, |
|
320 |
'type': 'Feature', |
|
273 | 321 |
}, |
274 |
u'properties': { |
|
275 |
u'Outer Category': u'cat\xe91', |
|
276 |
u'ann\xe9e (Date)': u'2017' |
|
322 |
{ |
|
323 |
'geometry': {'coordinates': [[1.0, 1.0]], 'type': 'MultiPoint'}, |
|
324 |
'properties': {'Outer Category': 'Aucun(e)', 'ann\xe9e (Date)': '2017'}, |
|
325 |
'type': 'Feature', |
|
277 | 326 |
}, |
278 |
u'type': u'Feature' |
|
279 |
}, |
|
280 |
{ |
|
281 |
u'geometry': { |
|
282 |
u'coordinates': [[1.0, 1.0]], |
|
283 |
u'type': u'MultiPoint' |
|
284 |
}, |
|
285 |
u'properties': { |
|
286 |
u'Outer Category': u'Aucun(e)', |
|
287 |
u'ann\xe9e (Date)': u'2017' |
|
288 |
}, |
|
289 |
u'type': u'Feature' |
|
290 |
}, |
|
291 |
{ |
|
292 |
u'geometry': { |
|
293 |
u'coordinates': [], |
|
294 |
u'type': u'MultiPoint' |
|
327 |
{ |
|
328 |
'geometry': {'coordinates': [], 'type': 'MultiPoint'}, |
|
329 |
'properties': {'Outer Category': 'cat\xe92', 'ann\xe9e (Date)': 'Aucun(e)'}, |
|
330 |
'type': 'Feature', |
|
295 | 331 |
}, |
296 |
u'properties': { |
|
297 |
u'Outer Category': u'cat\xe92', |
|
298 |
u'ann\xe9e (Date)': u'Aucun(e)' |
|
332 |
{ |
|
333 |
'geometry': {'coordinates': [], 'type': 'MultiPoint'}, |
|
334 |
'properties': {'Outer Category': 'cat\xe93', 'ann\xe9e (Date)': 'Aucun(e)'}, |
|
335 |
'type': 'Feature', |
|
299 | 336 |
}, |
300 |
u'type': u'Feature' |
|
301 |
}, |
|
302 |
{ |
|
303 |
u'geometry': { |
|
304 |
u'coordinates': [], |
|
305 |
u'type': u'MultiPoint' |
|
337 |
{ |
|
338 |
'geometry': {'coordinates': [], 'type': 'MultiPoint'}, |
|
339 |
'properties': {'Outer Category': 'cat\xe91', 'ann\xe9e (Date)': 'Aucun(e)'}, |
|
340 |
'type': 'Feature', |
|
306 | 341 |
}, |
307 |
u'properties': { |
|
308 |
u'Outer Category': u'cat\xe93', |
|
309 |
u'ann\xe9e (Date)': u'Aucun(e)' |
|
310 |
}, |
|
311 |
u'type': u'Feature' |
|
312 |
}, |
|
313 |
{ |
|
314 |
u'geometry': { |
|
315 |
u'coordinates': [], |
|
316 |
u'type': u'MultiPoint' |
|
317 |
}, |
|
318 |
u'properties': { |
|
319 |
u'Outer Category': u'cat\xe91', |
|
320 |
u'ann\xe9e (Date)': u'Aucun(e)' |
|
321 |
}, |
|
322 |
u'type': u'Feature' |
|
323 |
} |
|
324 |
]} |
|
342 |
], |
|
343 |
} |
|
325 | 344 | |
326 | 345 | |
327 | 346 |
def test_filter_type_mismatch(schema1, app, admin): |
328 | 347 |
# test conversion to Javascript declaration |
329 |
visu = Visualization.from_json({ |
|
330 |
'warehouse': 'schema1', |
|
331 |
'cube': 'facts1', |
|
332 |
'representation': 'graphical', |
|
333 |
'measure': 'simple_count', |
|
334 |
'filters': { |
|
335 |
'string': [1], |
|
348 |
visu = Visualization.from_json( |
|
349 |
{ |
|
350 |
'warehouse': 'schema1', |
|
351 |
'cube': 'facts1', |
|
352 |
'representation': 'graphical', |
|
353 |
'measure': 'simple_count', |
|
354 |
'filters': { |
|
355 |
'string': [1], |
|
356 |
}, |
|
336 | 357 |
} |
337 |
})
|
|
358 |
) |
|
338 | 359 |
assert visu.json_data() == [{'coords': [], 'measures': [{'value': 0}]}] |
339 | 360 | |
340 | 361 | |
341 | 362 |
def test_empty_filter(schema1, app, admin): |
342 |
visu = Visualization.from_json({ |
|
343 |
'warehouse': 'schema1', |
|
344 |
'cube': 'facts1', |
|
345 |
'representation': 'graphical', |
|
346 |
'measure': 'simple_count', |
|
347 |
'filters': { |
|
348 |
'innercategory': [], |
|
363 |
visu = Visualization.from_json( |
|
364 |
{ |
|
365 |
'warehouse': 'schema1', |
|
366 |
'cube': 'facts1', |
|
367 |
'representation': 'graphical', |
|
368 |
'measure': 'simple_count', |
|
369 |
'filters': { |
|
370 |
'innercategory': [], |
|
371 |
}, |
|
349 | 372 |
} |
350 |
})
|
|
373 |
) |
|
351 | 374 |
assert visu.json_data() == [{'coords': [], 'measures': [{'value': 17}]}] |
352 | 375 | |
353 | 376 | |
... | ... | |
362 | 385 |
form.set('measure', 'simple_count') |
363 | 386 |
form.set('drilldown_x', 'a') |
364 | 387 |
response = form.submit('visualize') |
365 |
assert get_table(response) == [ |
|
366 |
['A', 'x', 'y', 'z'], |
|
367 |
['number of rows', '7', '9', '1'] |
|
368 |
] |
|
388 |
assert get_table(response) == [['A', 'x', 'y', 'z'], ['number of rows', '7', '9', '1']] |
|
369 | 389 | |
370 | 390 |
assert 'filter__a' in form.fields |
371 | 391 |
choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']] |
... | ... | |
373 | 393 | |
374 | 394 |
form['filter__a'].force_value(['x', 'y']) |
375 | 395 |
response = form.submit('visualize') |
376 |
assert get_table(response) == [ |
|
377 |
['A', 'x', 'y', 'z'], |
|
378 |
['number of rows', '7', '9', '0'] |
|
379 |
] |
|
396 |
assert get_table(response) == [['A', 'x', 'y', 'z'], ['number of rows', '7', '9', '0']] |
|
380 | 397 | |
381 | 398 | |
382 | 399 |
def test_json_dimensions_having_percent(schema1, app, admin): |
... | ... | |
392 | 409 |
response = form.submit('visualize') |
393 | 410 |
assert get_table(response) == [ |
394 | 411 |
['A', 'x', 'y', 'z'], |
395 |
['pourcentage des demandes', '41,18 %', '52,94 %', '5,88 %'] |
|
412 |
['pourcentage des demandes', '41,18 %', '52,94 %', '5,88 %'],
|
|
396 | 413 |
] |
397 | 414 | |
398 | 415 |
assert 'filter__a' in form.fields |
... | ... | |
403 | 420 |
response = form.submit('visualize') |
404 | 421 |
assert get_table(response) == [ |
405 | 422 |
['A', 'x', 'y', 'z'], |
406 |
['pourcentage des demandes', '43,75 %', '56,25 %', '0,00 %'] |
|
423 |
['pourcentage des demandes', '43,75 %', '56,25 %', '0,00 %'],
|
|
407 | 424 |
] |
408 | 425 | |
409 | 426 |
tests/test_schema2.py | ||
---|---|---|
4 | 4 | |
5 | 5 |
import pytest |
6 | 6 |
from tabulate import tabulate |
7 | ||
8 |
from utils import login, get_table |
|
7 |
from utils import get_table, login |
|
9 | 8 | |
10 | 9 | |
11 | 10 |
def pytest_generate_tests(metafunc): |
12 | 11 |
if hasattr(metafunc, 'function'): |
13 | 12 |
fcode = metafunc.function.__code__ |
14 |
if 'visualization' in fcode.co_varnames[:fcode.co_argcount]: |
|
15 |
with open( |
|
16 |
os.path.join( |
|
17 |
os.path.dirname(__file__), |
|
18 |
'fixtures', |
|
19 |
'schema2', |
|
20 |
'tables.json')) as fd: |
|
13 |
if 'visualization' in fcode.co_varnames[: fcode.co_argcount]: |
|
14 |
with open(os.path.join(os.path.dirname(__file__), 'fixtures', 'schema2', 'tables.json')) as fd: |
|
21 | 15 |
tables = json.load(fd) |
22 | 16 |
metafunc.parametrize(['visualization'], [[x] for x in tables]) |
23 | 17 | |
... | ... | |
53 | 47 |
d[visualization] = table |
54 | 48 |
with open(new_table_path, 'w') as fd: |
55 | 49 |
json.dump(d, fd, indent=4, sort_keys=True, separators=(',', ': ')) |
56 |
assert_equal_tables( |
|
57 |
schema2['tables'][visualization], |
|
58 |
table) |
|
50 |
assert_equal_tables(schema2['tables'][visualization], table) |
tests/test_schemas.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 | |
18 |
from __future__ import unicode_literals |
|
19 | ||
20 | 18 |
import pytest |
21 | ||
22 | 19 |
from django.utils.translation import ugettext as _ |
23 | 20 | |
24 |
from bijoe.schemas import ( |
|
25 |
Dimension, |
|
26 |
) |
|
21 |
from bijoe.schemas import Dimension |
|
27 | 22 | |
28 | 23 | |
29 | 24 |
def test_absent_label(): |
30 | 25 |
assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'integer'}).absent_label == _('None') |
31 | 26 |
assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'string'}).absent_label == _('None') |
32 | 27 |
assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'bool'}).absent_label == _('N/A') |
33 |
assert Dimension.from_json( |
|
34 |
{'name': 'x', 'value': 'x', 'type': 'boolean', 'absent_label': 'coin'}).absent_label == 'coin' |
|
28 |
assert ( |
|
29 |
Dimension.from_json( |
|
30 |
{'name': 'x', 'value': 'x', 'type': 'boolean', 'absent_label': 'coin'} |
|
31 |
).absent_label |
|
32 |
== 'coin' |
|
33 |
) |
|
35 | 34 | |
36 | 35 |
with pytest.raises(NotImplementedError): |
37 | 36 |
Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'coin'}).absent_label |
38 |
tests/test_signature.py | ||
---|---|---|
38 | 38 |
assert not signature.check_string(STRING, signature.sign_string(STRING, KEY), OTHER_KEY) |
39 | 39 |
assert not signature.check_query(signature.sign_query(QUERY, KEY), OTHER_KEY) |
40 | 40 |
assert not signature.check_url(signature.sign_url(URL, KEY), OTHER_KEY) |
41 |
#assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), KEY) |
|
41 |
# assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), KEY)
|
|
42 | 42 | |
43 | 43 |
# Test URL is preserved |
44 | 44 |
assert URL in signature.sign_url(URL, KEY) |
... | ... | |
50 | 50 |
assert '&nonce=' in signature.sign_url(URL, KEY) |
51 | 51 | |
52 | 52 |
# Test unicode key conversion to UTF-8 |
53 |
assert signature.check_url(signature.sign_url(URL, u'\xe9\xe9'), b'\xc3\xa9\xc3\xa9')
|
|
54 |
assert signature.check_url(signature.sign_url(URL, b'\xc3\xa9\xc3\xa9'), u'\xe9\xe9')
|
|
53 |
assert signature.check_url(signature.sign_url(URL, '\xe9\xe9'), b'\xc3\xa9\xc3\xa9') |
|
54 |
assert signature.check_url(signature.sign_url(URL, b'\xc3\xa9\xc3\xa9'), '\xe9\xe9') |
|
55 | 55 | |
56 | 56 |
# Test timedelta parameter |
57 | 57 |
now = datetime.datetime.utcnow() |
58 |
assert '×tamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in \ |
|
59 |
signature.sign_url(URL, KEY, timestamp=now) |
|
58 |
assert '×tamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in signature.sign_url( |
|
59 |
URL, KEY, timestamp=now |
|
60 |
) |
|
60 | 61 | |
61 | 62 |
# Test nonce parameter |
62 | 63 |
assert '&nonce=uuu&' in signature.sign_url(URL, KEY, nonce='uuu') |
63 | 64 | |
64 | 65 |
# Test known_nonce |
65 |
assert signature.check_url(signature.sign_url(URL, KEY), KEY, |
|
66 |
known_nonce=lambda nonce: nonce == 'xxx')
|
|
67 |
assert signature.check_url(signature.sign_url(URL, KEY, nonce='xxx'), KEY,
|
|
68 |
known_nonce=lambda nonce: nonce == 'xxx')
|
|
66 |
assert signature.check_url(signature.sign_url(URL, KEY), KEY, known_nonce=lambda nonce: nonce == 'xxx')
|
|
67 |
assert signature.check_url(
|
|
68 |
signature.sign_url(URL, KEY, nonce='xxx'), KEY, known_nonce=lambda nonce: nonce == 'xxx'
|
|
69 |
) |
|
69 | 70 | |
70 | 71 |
# Test timedelta |
71 |
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=29))
|
|
72 |
now = datetime.datetime.utcnow() - datetime.timedelta(seconds=29)
|
|
72 | 73 |
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) |
73 |
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=30))
|
|
74 |
now = datetime.datetime.utcnow() - datetime.timedelta(seconds=30)
|
|
74 | 75 |
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) |
75 |
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=2))
|
|
76 |
now = datetime.datetime.utcnow() - datetime.timedelta(seconds=2)
|
|
76 | 77 |
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) |
77 | 78 |
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY, timedelta=2) |
tests/test_views.py | ||
---|---|---|
17 | 17 |
import copy |
18 | 18 |
import hashlib |
19 | 19 |
import json |
20 |
from unittest import mock |
|
20 | 21 | |
21 |
import mock |
|
22 | 22 |
import pytest |
23 |
from webtest import Upload |
|
24 | ||
25 | 23 |
from django.urls import reverse |
26 | 24 |
from django.utils.encoding import force_bytes |
25 |
from utils import login |
|
26 |
from webtest import Upload |
|
27 | 27 | |
28 | 28 |
from bijoe.visualization.models import Visualization |
29 | 29 |
from bijoe.visualization.signature import sign_url |
30 | 30 | |
31 |
from utils import login |
|
32 | ||
33 | 31 | |
34 | 32 |
@pytest.fixture |
35 | 33 |
def visualization(): |
... | ... | |
42 | 40 |
'representation': 'table', |
43 | 41 |
'loop': '', |
44 | 42 |
'filters': {}, |
45 |
'drilldown_x': 'date__yearmonth'}) |
|
43 |
'drilldown_x': 'date__yearmonth', |
|
44 |
}, |
|
45 |
) |
|
46 | 46 | |
47 | 47 | |
48 | 48 |
def test_simple_user_403(app, john_doe): |
... | ... | |
78 | 78 |
'default': { |
79 | 79 |
'verif_orig': orig, |
80 | 80 |
'secret': key, |
81 |
}}} |
|
81 |
} |
|
82 |
} |
|
83 |
} |
|
82 | 84 |
url = '%s?orig=%s' % (reverse('visualizations-json'), orig) |
83 | 85 |
url = sign_url(url, key) |
84 | 86 |
resp = app.get(url, status=200) |
85 |
assert set([x['slug'] for x in resp.json]) == set(['test', 'test-2', 'test-3', 'test-4'])
|
|
87 |
assert {x['slug'] for x in resp.json} == {'test', 'test-2', 'test-3', 'test-4'}
|
|
86 | 88 | |
87 | 89 |
url = '%s?orig=%s' % (reverse('visualizations-json'), orig) |
88 | 90 |
url = sign_url(url, 'wrong-key') |
... | ... | |
97 | 99 | |
98 | 100 |
login(app, admin) |
99 | 101 |
resp = app.get(reverse('visualizations-json')) |
100 |
assert set([x['slug'] for x in resp.json]) == set(['test', 'test-2', 'test-3', 'test-4'])
|
|
102 |
assert {x['slug'] for x in resp.json} == {'test', 'test-2', 'test-3', 'test-4'}
|
|
101 | 103 | |
102 | 104 | |
103 | 105 |
def test_visualization_json_api(schema1, app, admin, visualization): |
... | ... | |
105 | 107 |
resp = app.get(reverse('visualization-json', kwargs={'pk': visualization.id})) |
106 | 108 |
# values from test_schem1/test_yearmonth_drilldown |
107 | 109 |
assert resp.json == { |
108 |
'axis': {'x_labels': ['01/2017', '02/2017', '03/2017', '04/2017', '05/2017', '06/2017', '07/2017', '08/2017']}, |
|
110 |
'axis': { |
|
111 |
'x_labels': [ |
|
112 |
'01/2017', |
|
113 |
'02/2017', |
|
114 |
'03/2017', |
|
115 |
'04/2017', |
|
116 |
'05/2017', |
|
117 |
'06/2017', |
|
118 |
'07/2017', |
|
119 |
'08/2017', |
|
120 |
] |
|
121 |
}, |
|
109 | 122 |
'data': [10, 1, 1, 1, 1, 1, 1, 1], |
110 | 123 |
'format': '1', |
111 | 124 |
'unit': None, |
... | ... | |
121 | 134 |
resp = app.get(reverse('visualization-json', kwargs={'pk': visualization.id})) |
122 | 135 |
# values from test_schem1/test_yearmonth_drilldown |
123 | 136 |
assert resp.json == { |
124 |
'axis': {'x_labels': ['01/2017', '02/2017', '03/2017', '04/2017', '05/2017', '06/2017', '07/2017', '08/2017']}, |
|
125 |
'data': [536968800.0, 539258400.0, 541677600.0, 544352400.0, |
|
126 |
546944400.0, 549622800.0, 552214800.0, 554893200.0], |
|
137 |
'axis': { |
|
138 |
'x_labels': [ |
|
139 |
'01/2017', |
|
140 |
'02/2017', |
|
141 |
'03/2017', |
|
142 |
'04/2017', |
|
143 |
'05/2017', |
|
144 |
'06/2017', |
|
145 |
'07/2017', |
|
146 |
'08/2017', |
|
147 |
] |
|
148 |
}, |
|
149 |
'data': [ |
|
150 |
536968800.0, |
|
151 |
539258400.0, |
|
152 |
541677600.0, |
|
153 |
544352400.0, |
|
154 |
546944400.0, |
|
155 |
549622800.0, |
|
156 |
552214800.0, |
|
157 |
554893200.0, |
|
158 |
], |
|
127 | 159 |
'format': '1', |
128 | 160 |
'unit': 'seconds', |
129 | 161 |
'measure': 'duration', |
... | ... | |
194 | 226 |
# existing visualization |
195 | 227 |
resp = app.get('/', status=200) |
196 | 228 |
resp = resp.click('Import') |
197 |
resp.form['visualizations_json'] = Upload('export.json', visualization_export.encode('utf-8'), 'application/json') |
|
229 |
resp.form['visualizations_json'] = Upload( |
|
230 |
'export.json', visualization_export.encode('utf-8'), 'application/json' |
|
231 |
) |
|
198 | 232 |
resp = resp.form.submit().follow() |
199 | 233 |
assert 'No visualization created. A visualization has been updated.' in resp.text |
200 | 234 |
assert Visualization.objects.count() == 1 |
... | ... | |
203 | 237 |
Visualization.objects.all().delete() |
204 | 238 |
resp = app.get('/') |
205 | 239 |
resp = resp.click('Import') |
206 |
resp.form['visualizations_json'] = Upload('export.json', visualization_export.encode('utf-8'), 'application/json') |
|
240 |
resp.form['visualizations_json'] = Upload( |
|
241 |
'export.json', visualization_export.encode('utf-8'), 'application/json' |
|
242 |
) |
|
207 | 243 |
resp = resp.form.submit().follow() |
208 | 244 |
assert 'A visualization has been created. No visualization updated.' in resp.text |
209 | 245 |
assert Visualization.objects.count() == 1 |
... | ... | |
220 | 256 |
resp = app.get('/', status=200) |
221 | 257 |
resp = resp.click('Import') |
222 | 258 |
resp.form['visualizations_json'] = Upload( |
223 |
'export.json', |
|
224 |
json.dumps(visualizations).encode('utf-8'), 'application/json')
|
|
259 |
'export.json', json.dumps(visualizations).encode('utf-8'), 'application/json'
|
|
260 |
) |
|
225 | 261 |
resp = resp.form.submit().follow() |
226 | 262 |
assert '2 visualizations have been created. A visualization has been updated.' in resp.text |
227 | 263 |
assert Visualization.objects.count() == 3 |
... | ... | |
235 | 271 | |
236 | 272 |
resp = app.get('/') |
237 | 273 |
resp = resp.click('Import') |
238 |
resp.form['visualizations_json'] = Upload('export.json', visualizations_export.encode('utf-8'), 'application/json') |
|
274 |
resp.form['visualizations_json'] = Upload( |
|
275 |
'export.json', visualizations_export.encode('utf-8'), 'application/json' |
|
276 |
) |
|
239 | 277 |
resp = resp.form.submit().follow() |
240 | 278 |
assert '3 visualizations have been created. No visualization updated.' in resp.text |
241 | 279 |
assert Visualization.objects.count() == 3 |
tests/utils.py | ||
---|---|---|
1 | 1 |
import io |
2 |
import zipfile |
|
3 | 2 |
import xml.etree.ElementTree as ET |
3 |
import zipfile |
|
4 | 4 | |
5 | 5 |
from django.conf import settings |
6 | 6 | |
... | ... | |
35 | 35 | |
36 | 36 | |
37 | 37 |
def xml_node_text_content(node): |
38 |
'''Extract text content from node and all its children. Equivalent to
|
|
39 |
xmlNodeGetContent from libxml.'''
|
|
38 |
"""Extract text content from node and all its children. Equivalent to
|
|
39 |
xmlNodeGetContent from libxml."""
|
|
40 | 40 | |
41 | 41 |
if node is None: |
42 | 42 |
return '' |
... | ... | |
50 | 50 |
if child.tail: |
51 | 51 |
s.append(child.tail) |
52 | 52 |
return s |
53 |
return u''.join(helper(node)) |
|
53 | ||
54 |
return ''.join(helper(node)) |
|
54 | 55 | |
55 | 56 | |
56 | 57 |
def get_ods_document(response): |
57 |
- |