Projet

Général

Profil

0001-misc-apply-pyupgrade-isort-black-56062.patch

Benjamin Dauvergne, 09 août 2021 15:39

Télécharger (134 ko)

Voir les différences:

Subject: [PATCH 1/2] misc: apply pyupgrade/isort/black (#56062)

 bijoe/engine.py                               | 157 +++++----
 .../management/commands/hobo_deploy.py        |  41 ++-
 bijoe/management/commands/export_site.py      |   4 +-
 bijoe/management/commands/import_site.py      |  12 +-
 bijoe/relative_time.py                        |  12 +-
 bijoe/schemas.py                              | 136 ++++----
 bijoe/settings.py                             |  13 +-
 bijoe/templatetags/bijoe.py                   |   2 +-
 bijoe/urls.py                                 |   2 +-
 bijoe/utils.py                                |  14 +-
 bijoe/uwsgi.py                                |   4 +-
 bijoe/views.py                                |  27 +-
 bijoe/visualization/admin.py                  |   1 +
 bijoe/visualization/forms.py                  | 109 +++---
 .../visualization/migrations/0001_initial.py  |  13 +-
 .../migrations/0002_rename_parameters.py      |   3 -
 .../migrations/0003_visualization_slug.py     |   4 +-
 .../migrations/0004_auto_20190328_0825.py     |   2 -
 bijoe/visualization/models.py                 |  15 +-
 bijoe/visualization/ods.py                    |  30 +-
 bijoe/visualization/signature.py              |  24 +-
 bijoe/visualization/urls.py                   |  22 +-
 bijoe/visualization/utils.py                  | 108 +++---
 bijoe/visualization/views.py                  | 173 +++++-----
 debian/debian_config.py                       |   4 +-
 debian/scripts/warehouse_slug.py              |   1 -
 debian/settings.py                            |   6 +-
 format_json_fixtures.py                       |   2 -
 setup.py                                      |  86 ++---
 tests/conftest.py                             |  41 ++-
 tests/test_bijoe_schemas.py                   | 128 ++++----
 tests/test_hobo_agent.py                      |   8 +-
 tests/test_hobo_deploy.py                     |  14 +-
 tests/test_import_export.py                   |   8 +-
 tests/test_relative_time.py                   |  20 +-
 tests/test_schema1.py                         | 309 +++++++++---------
 tests/test_schema2.py                         |  16 +-
 tests/test_schemas.py                         |  16 +-
 tests/test_signature.py                       |  25 +-
 tests/test_views.py                           |  74 ++++-
 tests/utils.py                                |   9 +-
 41 files changed, 915 insertions(+), 780 deletions(-)
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 '&timestamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in \
59
        signature.sign_url(URL, KEY, timestamp=now)
58
    assert '&timestamp=%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
-