diff --git a/bijoe/engine.py b/bijoe/engine.py index 62508a3..0cbec26 100644 --- a/bijoe/engine.py +++ b/bijoe/engine.py @@ -17,14 +17,13 @@ import collections import contextlib import datetime -import logging -import itertools import hashlib +import itertools +import logging import psycopg2 - -from django.core.cache import cache from django.conf import settings +from django.core.cache import cache from django.utils.encoding import force_bytes, force_text from django.utils.six import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -63,20 +62,20 @@ class MeasureCell(collections.namedtuple('_Cell', ['measure', 'value'])): return _('N/A') else: try: - return (u'%4.2f' % float(value)).replace('.', ',') + u' %' + return ('%4.2f' % float(value)).replace('.', ',') + ' %' except TypeError: return _('N/A') elif self.measure.type == 'duration': if value is None: - return u'0' + return '0' else: - s = u'' + s = '' if value.days: - s += u'%d jour(s)' % value.days + s += '%d jour(s)' % value.days if value.seconds // 3600: - s += u' %d heure(s)' % (value.seconds // 3600) + s += ' %d heure(s)' % (value.seconds // 3600) if not s: - s = u'moins d\'1 heure' + s = 'moins d\'1 heure' return s elif self.measure.type == 'bool': if value is None: @@ -99,7 +98,7 @@ class Cells(collections.namedtuple('Cells', ['dimensions', 'measures'])): def __new__(cls, dimensions=[], measures=[]): dimensions = list(dimensions) measures = list(measures) - return super(Cells, cls).__new__(cls, dimensions, measures) + return super().__new__(cls, dimensions, measures) def quote(s): @@ -126,7 +125,7 @@ def to_tuple(cur, values): Member = collections.namedtuple('Member', ['id', 'label']) -class EngineDimension(object): +class EngineDimension: def __init__(self, engine, engine_cube, dimension): self.engine = engine self.engine_cube = engine_cube @@ -148,9 +147,13 @@ class EngineDimension(object): cache_key = self.cache_key(filters) members = cache.get(cache_key) if members is not None: - self.engine.log.debug('MEMBERS: (from cache) dimension %s.%s filters=%s: %s', - self.engine_cube.name, self.name, filters, - members) + self.engine.log.debug( + 'MEMBERS: (from cache) dimension %s.%s filters=%s: %s', + self.engine_cube.name, + self.name, + filters, + members, + ) return members members = [] @@ -184,7 +187,8 @@ class EngineDimension(object): table_expression = '%s' % self.engine_cube.fact_table if joins: table_expression = self.engine_cube.build_table_expression( - joins, self.engine_cube.fact_table, force_join='right') + joins, self.engine_cube.fact_table, force_join='right' + ) sql = 'SELECT %s AS value, %s::text AS label ' % (value, value_label) sql += 'FROM %s ' % table_expression if order_by: @@ -199,7 +203,7 @@ class EngineDimension(object): if order_value not in group_by: group_by.append(order_value) if conditions: - sql += 'WHERE %s ' % (' AND '.join(conditions)) + sql += 'WHERE %s ' % ' AND '.join(conditions) sql += 'GROUP BY %s ' % ', '.join(group_by) sql += 'ORDER BY (%s) ' % ', '.join(order_by) sql = sql.format(fact_table=self.engine_cube.fact_table) @@ -210,14 +214,15 @@ class EngineDimension(object): continue members.append(Member(id=row[0], label=force_text(row[1]))) cache.set(cache_key, members, 600) - self.engine.log.debug('MEMBERS: dimension %s.%s filters=%s: %s', - self.engine_cube.name, self.name, filters, - members) + self.engine.log.debug( + 'MEMBERS: dimension %s.%s filters=%s: %s', self.engine_cube.name, self.name, filters, members + ) return members class SchemaJSONDimension(schemas.Dimension): '''Generated dimensions for JSON fields keys''' + filter = False order_by = None group_by = None @@ -225,7 +230,7 @@ class SchemaJSONDimension(schemas.Dimension): type = 'string' def __init__(self, json_field, name): - super(SchemaJSONDimension, self).__init__() + super().__init__() name = str(name) self.name = name self.label = name.title() @@ -233,27 +238,30 @@ class SchemaJSONDimension(schemas.Dimension): self.value_label = expr self.value = expr self.join = ['json_' + name] - sql = ('SELECT DISTINCT {json_field}->>\'%s\' AS v, {json_field}->>\'%s\' AS v' - ' FROM {{fact_table}} WHERE ({json_field}->>\'%s\') IS NOT NULL ORDER BY v' % - (self.name, self.name, self.name)) + sql = ( + 'SELECT DISTINCT {json_field}->>\'%s\' AS v, {json_field}->>\'%s\' AS v' + ' FROM {{fact_table}} WHERE ({json_field}->>\'%s\') IS NOT NULL ORDER BY v' + % (self.name, self.name, self.name) + ) self.members_query = sql.format(json_field=json_field) - self.filter_expression = ('({fact_table}.id IS NULL ' - 'OR ({fact_table}.%s->>\'%s\') IN (%%s))' - % (json_field, name)) + self.filter_expression = '({fact_table}.id IS NULL OR ({fact_table}.%s->>\'%s\') IN (%%s))' % ( + json_field, + name, + ) self.filter_needs_join = False self.absent_label = _('None') class EngineJSONDimension(EngineDimension): - def __init__(self, engine, engine_cube, name): self.engine = engine self.engine_cube = engine_cube self.dimension = SchemaJSONDimension(self.engine_cube.json_field, name) def cache_key(self, filters): - key = (self.engine.path + self.engine_cube.json_field - + self.engine_cube.name + self.name + repr(filters)) + key = ( + self.engine.path + self.engine_cube.json_field + self.engine_cube.name + self.name + repr(filters) + ) return hashlib.md5(force_bytes(key)).hexdigest() def to_json(self): @@ -263,7 +271,7 @@ class EngineJSONDimension(EngineDimension): } -class EngineMeasure(object): +class EngineMeasure: def __init__(self, engine, engine_cube, measure): self.engine = engine self.engine_cube = engine_cube @@ -273,7 +281,7 @@ class EngineMeasure(object): return getattr(self.measure, name) -class ProxyList(object): +class ProxyList: chain = None def __init__(self, engine, engine_cube, attribute, cls, chain=None): @@ -285,8 +293,9 @@ class ProxyList(object): self.chain = chain(engine, engine_cube) def __iter__(self): - i = (self.cls(self.engine, self.engine_cube, o) - for o in getattr(self.engine_cube.cube, self.attribute)) + i = ( + self.cls(self.engine, self.engine_cube, o) for o in getattr(self.engine_cube.cube, self.attribute) + ) if self.chain: i = itertools.chain(i, self.chain) return i @@ -300,7 +309,7 @@ class ProxyList(object): raise KeyError -class JSONDimensions(object): +class JSONDimensions: __cache = None def __init__(self, engine, engine_cube): @@ -313,8 +322,10 @@ class JSONDimensions(object): return [] if not self.__cache: with self.engine.get_cursor() as cursor: - sql = ('select distinct jsonb_object_keys(%s) as a from %s order by a' - % (self.engine_cube.json_field, self.engine_cube.fact_table)) + sql = 'select distinct jsonb_object_keys(%s) as a from %s order by a' % ( + self.engine_cube.json_field, + self.engine_cube.fact_table, + ) cursor.execute(sql) self.__cache = [row[0] for row in cursor.fetchall()] return self.__cache @@ -329,7 +340,7 @@ class JSONDimensions(object): return EngineJSONDimension(self.engine, self.engine_cube, name) -class ProxyListDescriptor(object): +class ProxyListDescriptor: def __init__(self, attribute, cls, chain=None): self.attribute = attribute self.cls = cls @@ -342,7 +353,7 @@ class ProxyListDescriptor(object): return obj.__dict__[key] -class EngineCube(object): +class EngineCube: dimensions = ProxyListDescriptor('all_dimensions', EngineDimension, chain=JSONDimensions) measures = ProxyListDescriptor('measures', EngineMeasure) @@ -365,10 +376,16 @@ class EngineCube(object): name=name, table=( '(SELECT DISTINCT %s.%s->>\'%s\' AS value FROM %s ' - 'WHERE (%s.%s->>\'%s\') IS NOT NULL ORDER BY value)' % ( - self.fact_table, self.json_field, json_key, self.fact_table, - self.fact_table, self.json_field, json_key - ) + 'WHERE (%s.%s->>\'%s\') IS NOT NULL ORDER BY value)' + ) + % ( + self.fact_table, + self.json_field, + json_key, + self.fact_table, + self.fact_table, + self.json_field, + json_key, ), master='%s->>\'%s\'' % (self.json_field, json_key), detail='value', @@ -427,15 +444,22 @@ class EngineCube(object): sql += ' GROUP BY %s' % ', '.join(group_by) if order_by: sql += ' ORDER BY %s' % ', '.join(order_by) - sql = sql.format(fact_table=self.cube.fact_table, - table_expression=table_expression, - where_conditions=where_conditions) + sql = sql.format( + fact_table=self.cube.fact_table, + table_expression=table_expression, + where_conditions=where_conditions, + ) return sql def query(self, filters, drilldown, measures, **kwargs): - self.engine.log.debug('%s.%s query filters=%s drilldown=%s measures=%s', - self.engine.warehouse.name, self.cube.name, filters, drilldown, - measures) + self.engine.log.debug( + '%s.%s query filters=%s drilldown=%s measures=%s', + self.engine.warehouse.name, + self.cube.name, + filters, + drilldown, + measures, + ) with self.engine.get_cursor() as cursor: sql = self.sql_query(filters=filters, drilldown=drilldown, measures=measures, **kwargs) self.engine.log.debug('SQL: %s', sql) @@ -451,16 +475,20 @@ class EngineCube(object): else: value_label = row[j + 1] j += 2 - cells.dimensions.append(DimensionCell( - dimension=dimension, - value=value, - value_label=value_label, - )) + cells.dimensions.append( + DimensionCell( + dimension=dimension, + value=value, + value_label=value_label, + ) + ) for i, measure in enumerate(measures): - cells.measures.append(MeasureCell( - measure=measure, - value=row[j + i], - )) + cells.measures.append( + MeasureCell( + measure=measure, + value=row[j + i], + ) + ) yield cells JOIN_KINDS = { @@ -471,8 +499,8 @@ class EngineCube(object): } def build_table_expression(self, joins, table_name, force_join=None): - '''Recursively build the table expression from the join tree, - starting from the fact table''' + """Recursively build the table expression from the join tree, + starting from the fact table""" join_tree = {} # Build join tree @@ -495,12 +523,15 @@ class EngineCube(object): for join_name, join in joins.items(): contain_joins = True sql += ' %s ' % self.JOIN_KINDS[kind] - sql += ' ' + build_table_expression_helper(join_tree, join.table, alias=join.name, top=False) + sql += ' ' + build_table_expression_helper( + join_tree, join.table, alias=join.name, top=False + ) condition = '%s.%s = %s.%s' % ( alias or table_name, join.master.split('.')[-1], quote(join.name), - join.detail) + join.detail, + ) sql += ' ON ' + condition # if the table expression contains joins and is not the full table @@ -514,7 +545,7 @@ class EngineCube(object): return build_table_expression_helper(join_tree, table_name) -class Engine(object): +class Engine: def __init__(self, warehouse): self.warehouse = warehouse self.log = logging.getLogger(__name__) diff --git a/bijoe/hobo_agent/management/commands/hobo_deploy.py b/bijoe/hobo_agent/management/commands/hobo_deploy.py index 97e41dc..b0c0873 100644 --- a/bijoe/hobo_agent/management/commands/hobo_deploy.py +++ b/bijoe/hobo_agent/management/commands/hobo_deploy.py @@ -25,12 +25,11 @@ try: except ImportError: sentry_sdk = None -from tenant_schemas.utils import tenant_context +from django.conf import settings +from django.utils.encoding import force_str from hobo.agent.common.management.commands import hobo_deploy from hobo.multitenant.settings_loaders import KnownServices - -from django.utils.encoding import force_str -from django.conf import settings +from tenant_schemas.utils import tenant_context def pg_dsn_quote(value): @@ -47,9 +46,10 @@ def truncate_pg_identifier(identifier, hash_length=6, force_hash=False): else: # insert hash in the middle, to keep some readability return ( - identifier[:(63 - hash_length) // 2] + identifier[: (63 - hash_length) // 2] + hashlib.md5(identifier.encode('utf-8')).hexdigest()[:hash_length] - + identifier[-(63 - hash_length) // 2:]) + + identifier[-(63 - hash_length) // 2 :] + ) def schema_from_url(url, hash_length=6): @@ -62,7 +62,7 @@ def schema_from_url(url, hash_length=6): class Command(hobo_deploy.Command): def deploy_specifics(self, hobo_environment, tenant): - super(Command, self).deploy_specifics(hobo_environment, tenant) + super().deploy_specifics(hobo_environment, tenant) with tenant_context(tenant): services = hobo_environment.get('services') ini_file = os.path.join(tenant.get_directory(), 'wcs-olap.ini') @@ -77,15 +77,18 @@ class Command(hobo_deploy.Command): config.add_section('wcs-olap') config.set('wcs-olap', 'cubes_model_dirs', schemas_path) pg_dsn_parts = [] - for pg_dsn_part in [('NAME', 'dbname'), - ('HOST', 'host'), - ('USER', 'user'), - ('PASSWORD', 'password'), - ('PORT', 'port')]: + for pg_dsn_part in [ + ('NAME', 'dbname'), + ('HOST', 'host'), + ('USER', 'user'), + ('PASSWORD', 'password'), + ('PORT', 'port'), + ]: if settings.DATABASES['default'].get(pg_dsn_part[0]): - pg_dsn_parts.append('%s=%s' % ( - pg_dsn_part[1], - pg_dsn_quote(settings.DATABASES['default'].get(pg_dsn_part[0])))) + pg_dsn_parts.append( + '%s=%s' + % (pg_dsn_part[1], pg_dsn_quote(settings.DATABASES['default'].get(pg_dsn_part[0]))) + ) config.set('wcs-olap', 'pg_dsn', config_parser_quote(' '.join(pg_dsn_parts))) for service in services: @@ -97,8 +100,12 @@ class Command(hobo_deploy.Command): our_key = this['secret_key'] for service in services: base_url = service.get('base_url') - if (service.get('this') or not service.get('secret_key') - or service['service-id'] != 'wcs' or not service.get('base_url')): + if ( + service.get('this') + or not service.get('secret_key') + or service['service-id'] != 'wcs' + or not service.get('base_url') + ): continue elif service.get('secondary') and not config.has_section(base_url): # skip secondary instances unless they were already added, diff --git a/bijoe/management/commands/export_site.py b/bijoe/management/commands/export_site.py index cc7e254..5ee3199 100644 --- a/bijoe/management/commands/export_site.py +++ b/bijoe/management/commands/export_site.py @@ -27,8 +27,8 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '--output', metavar='FILE', default=None, - help='name of a file to write output to') + '--output', metavar='FILE', default=None, help='name of a file to write output to' + ) def handle(self, *args, **options): if options['output']: diff --git a/bijoe/management/commands/import_site.py b/bijoe/management/commands/import_site.py index a4df0cb..8641ecd 100644 --- a/bijoe/management/commands/import_site.py +++ b/bijoe/management/commands/import_site.py @@ -26,15 +26,11 @@ class Command(BaseCommand): help = 'Import an exported site' def add_arguments(self, parser): + parser.add_argument('filename', metavar='FILENAME', type=str, help='name of file to import') + parser.add_argument('--clean', action='store_true', default=False, help='Clean site before importing') parser.add_argument( - 'filename', metavar='FILENAME', type=str, - help='name of file to import') - parser.add_argument( - '--clean', action='store_true', default=False, - help='Clean site before importing') - parser.add_argument( - '--if-empty', action='store_true', default=False, - help='Import only if site is empty') + '--if-empty', action='store_true', default=False, help='Import only if site is empty' + ) def handle(self, filename, **options): if filename == '-': diff --git a/bijoe/relative_time.py b/bijoe/relative_time.py index 1ab9230..60431b0 100644 --- a/bijoe/relative_time.py +++ b/bijoe/relative_time.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert # @@ -17,23 +16,24 @@ import re from datetime import date -from dateutil.relativedelta import relativedelta, MO + import isodate +from dateutil.relativedelta import MO, relativedelta class RelativeDate(date): __TEMPLATES = [ { - 'pattern': u' *cette +année *', + 'pattern': ' *cette +année *', 'truncate': 'year', }, { - 'pattern': u' *l\'année +dernière *', + 'pattern': ' *l\'année +dernière *', 'truncate': 'year', 'timedelta': {'years': -1}, }, { - 'pattern': u' *l\'année +prochaine *', + 'pattern': ' *l\'année +prochaine *', 'truncate': 'year', 'timedelta': {'years': 1}, }, @@ -131,7 +131,7 @@ class RelativeDate(date): elif group.startswith('-'): sign = -1 group = group[1:] - n = re.match('(\d+)\*(\w+)', group) + n = re.match(r'(\d+)\*(\w+)', group) try: if n: value = int(n.group(1) * m.group(n.group(2))) diff --git a/bijoe/schemas.py b/bijoe/schemas.py index c63e3b2..12957ae 100644 --- a/bijoe/schemas.py +++ b/bijoe/schemas.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert @@ -16,9 +15,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import collections import datetime import decimal -import collections from django.utils import six from django.utils.encoding import force_text @@ -49,7 +48,7 @@ def type_cast(t): return t -class Base(object): +class Base: __types__ = {} def __init__(self, **kwargs): @@ -78,14 +77,16 @@ class Base(object): def from_json(cls, d): assert hasattr(d, 'keys') slots = cls.slots() - assert set(d.keys()) <= set(slots), \ - 'given keys %r does not match %s.__slots__: %r' % (d.keys(), cls.__name__, slots) + assert set(d.keys()) <= set(slots), 'given keys %r does not match %s.__slots__: %r' % ( + d.keys(), + cls.__name__, + slots, + ) types = cls.types() kwargs = {} for key in slots: - assert key in d or hasattr(cls, key), \ - '%s.%s is is a mandatory attribute' % (cls.__name__, key) + assert key in d or hasattr(cls, key), '%s.%s is is a mandatory attribute' % (cls.__name__, key) if not key in d: continue value = d[key] @@ -133,7 +134,7 @@ class Measure(Base): __slots__ = ['name', 'label', 'type', 'expression'] __types__ = { 'name': str, - 'label': six.text_type, + 'label': str, 'type': type_cast, 'expression': str, } @@ -146,12 +147,25 @@ class Measure(Base): class Dimension(Base): - __slots__ = ['name', 'label', 'type', 'join', 'value', 'value_label', - 'order_by', 'group_by', 'filter_in_join', 'filter', 'filter_value', - 'filter_needs_join', 'filter_expression', 'absent_label'] + __slots__ = [ + 'name', + 'label', + 'type', + 'join', + 'value', + 'value_label', + 'order_by', + 'group_by', + 'filter_in_join', + 'filter', + 'filter_value', + 'filter_needs_join', + 'filter_expression', + 'absent_label', + ] __types__ = { 'name': str, - 'label': six.text_type, + 'label': str, 'type': str, 'join': [str], 'value': str, @@ -164,7 +178,7 @@ class Dimension(Base): 'filter_in_join': bool, 'filter_value': str, 'filter_needs_join': bool, - 'absent_label': six.text_type, + 'absent_label': str, } def __init__(self, **kwargs): @@ -180,7 +194,7 @@ class Dimension(Base): self.filter_needs_join = True self.members_query = None self.absent_label = None - super(Dimension, self).__init__(**kwargs) + super().__init__(**kwargs) if not self.absent_label: if self.type in ('date', 'integer', 'string'): self.absent_label = _('None') @@ -196,33 +210,31 @@ class Dimension(Base): return [ self, Dimension( - label=u'année (%s)' % self.label, - + label='année (%s)' % self.label, name=self.name + '__year', type='integer', join=self.join, filter_value='EXTRACT(year from %s)::integer' % filter_value, filter_in_join=self.filter_in_join, value='EXTRACT(year from %s)::integer' % self.value, - filter=False), + filter=False, + ), Dimension( - label=u'année et mois (%s)' % self.label, - + label='année et mois (%s)' % self.label, name=self.name + '__yearmonth', type='integer', join=self.join, filter_value='EXTRACT(year from %s) || \'M\' || EXTRACT(month from %s)' - % (filter_value, filter_value), + % (filter_value, filter_value), filter_in_join=self.filter_in_join, value='TO_CHAR(EXTRACT(month from %s), \'00\') || \'/\' || EXTRACT(year from %s)' - % (self.value, self.value), - group_by='EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, - self.value), - order_by=['EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, - self.value)], - filter=False), + % (self.value, self.value), + group_by='EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, self.value), + order_by=['EXTRACT(year from %s), EXTRACT(month from %s)' % (self.value, self.value)], + filter=False, + ), Dimension( - label=u'mois (%s)' % self.label, + label='mois (%s)' % self.label, name=self.name + '__month', type='integer', filter_value='EXTRACT(month from %s)' % filter_value, @@ -230,12 +242,12 @@ class Dimension(Base): join=self.join, value='EXTRACT(month from %s)' % self.value, value_label='to_char(date_trunc(\'month\', %s), \'TMmonth\')' % self.value, - group_by='EXTRACT(month from %s), ' - 'to_char(date_trunc(\'month\', %s), \'TMmonth\')' - % (self.value, self.value), - filter=False), + group_by='EXTRACT(month from %s), to_char(date_trunc(\'month\', %s), \'TMmonth\')' + % (self.value, self.value), + filter=False, + ), Dimension( - label=u'jour de la semaine (%s)' % self.label, + label='jour de la semaine (%s)' % self.label, name=self.name + '__dow', type='integer', join=self.join, @@ -243,24 +255,27 @@ class Dimension(Base): filter_in_join=self.filter_in_join, value='EXTRACT(dow from %s)' % self.value, order_by=['(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value], - value_label='to_char(date_trunc(\'week\', current_date)::date ' - '+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' % self.value, - filter=False), + value_label=( + 'to_char(date_trunc(\'week\', current_date)::date ' + '+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' + ) + % self.value, + filter=False, + ), Dimension( - label=u'semaine (%s)' % self.label, + label='semaine (%s)' % self.label, name=self.name + '__isoweek', type='integer', join=self.join, filter_value='EXTRACT(isoyear from %s) || \'S\' || EXTRACT(week from %s)' - % (filter_value, filter_value), + % (filter_value, filter_value), filter_in_join=self.filter_in_join, value='EXTRACT(isoyear from %s) || \'S\' || EXTRACT(week from %s)' - % (self.value, self.value), - group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, - self.value), - order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, - self.value)], - filter=False) + % (self.value, self.value), + group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, self.value), + order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value, self.value)], + filter=False, + ), ] return [self] @@ -268,8 +283,7 @@ class Dimension(Base): value = self.filter_value or self.value if self.type == 'date': - assert isinstance(filter_values, dict) and set(filter_values.keys()) == set(['start', - 'end']) + assert isinstance(filter_values, dict) and set(filter_values.keys()) == {'start', 'end'} filters = [] values = [] @@ -278,6 +292,7 @@ class Dimension(Base): filter_value = RelativeDate(filter_value) filters.append(tpl % (value, '%s')) values.append(filter_value) + try: if filter_values['start']: date_filter('%s >= %s', filter_values['start']) @@ -346,7 +361,7 @@ class Join(Base): def __init__(self, **kwargs): self.kind = 'full' - super(Join, self).__init__(**kwargs) + super().__init__(**kwargs) @property def master_table(self): @@ -356,18 +371,27 @@ class Join(Base): class Cube(Base): - __slots__ = ['name', 'label', 'fact_table', 'json_field', 'key', 'joins', 'dimensions', - 'measures', 'warnings'] + __slots__ = [ + 'name', + 'label', + 'fact_table', + 'json_field', + 'key', + 'joins', + 'dimensions', + 'measures', + 'warnings', + ] __types__ = { 'name': str, - 'label': six.text_type, + 'label': str, 'fact_table': str, 'json_field': str, 'key': str, 'joins': [Join], 'dimensions': [Dimension], 'measures': [Measure], - 'warnings': [six.text_type], + 'warnings': [str], } def __init__(self, **kwargs): @@ -376,7 +400,7 @@ class Cube(Base): self.dimensions = () self.measures = () self.warnings = () - super(Cube, self).__init__(**kwargs) + super().__init__(**kwargs) def check(self): names = collections.Counter() @@ -386,13 +410,13 @@ class Cube(Base): duplicates = [k for k, v in names.items() if v > 1] if duplicates: raise SchemaError( - 'More than one join, dimension or measure with name(s) %s' % ', '.join(duplicates)) + 'More than one join, dimension or measure with name(s) %s' % ', '.join(duplicates) + ) @property def all_dimensions(self): for dimension in self.dimensions: - for sub_dimension in dimension.dimensions: - yield sub_dimension + yield from dimension.dimensions def get_dimension(self, name): for dimension in self.dimensions: @@ -419,7 +443,7 @@ class Warehouse(Base): __types__ = { 'name': str, 'slug': str, - 'label': six.text_type, + 'label': str, 'pg_dsn': str, 'search_path': [str], 'cubes': [Cube], @@ -430,7 +454,7 @@ class Warehouse(Base): self.path = None self.slug = None self.timestamp = None - super(Warehouse, self).__init__(**kwargs) + super().__init__(**kwargs) def check(self): names = collections.Counter(cube.name for cube in self.cubes) diff --git a/bijoe/settings.py b/bijoe/settings.py index 67080fd..b70efe8 100644 --- a/bijoe/settings.py +++ b/bijoe/settings.py @@ -24,14 +24,14 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ -from itertools import chain -from django.conf import global_settings -import django - -from gadjo.templatetags.gadjo import xstatic - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +from itertools import chain + +import django +from django.conf import global_settings +from gadjo.templatetags.gadjo import xstatic + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -74,8 +74,7 @@ MIDDLEWARE = ( 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) -STATICFILES_FINDERS = list(chain(global_settings.STATICFILES_FINDERS, - ('gadjo.finders.XStaticFinder',))) +STATICFILES_FINDERS = list(chain(global_settings.STATICFILES_FINDERS, ('gadjo.finders.XStaticFinder',))) ROOT_URLCONF = 'bijoe.urls' @@ -122,7 +121,7 @@ LOGGING = { 'formatters': { 'verbose': { 'format': '[%(asctime)s] %(levelname)s %(name)s.%(funcName)s: %(message)s', - 'datefmt': '%Y-%m-%d %a %H:%M:%S' + 'datefmt': '%Y-%m-%d %a %H:%M:%S', }, }, 'handlers': { diff --git a/bijoe/templatetags/bijoe.py b/bijoe/templatetags/bijoe.py index d6c0b3e..a3b2c84 100644 --- a/bijoe/templatetags/bijoe.py +++ b/bijoe/templatetags/bijoe.py @@ -21,7 +21,7 @@ register = template.Library() try: from django_select2.templatetags.django_select2_tags import * except ImportError: + @register.simple_tag(name='import_django_select2_js_css') def import_all(light=0): return '' - diff --git a/bijoe/urls.py b/bijoe/urls.py index c37dac3..45fd264 100644 --- a/bijoe/urls.py +++ b/bijoe/urls.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from django.conf import settings from . import views diff --git a/bijoe/utils.py b/bijoe/utils.py index 2680f5c..0c7ee38 100644 --- a/bijoe/utils.py +++ b/bijoe/utils.py @@ -15,14 +15,14 @@ # along with this program. If not, see . import datetime -import os import glob import json +import os from django.conf import settings from django.db import connection, transaction -from django.utils.translation import ugettext as _ from django.utils.timezone import utc +from django.utils.translation import ugettext as _ try: from functools import lru_cache @@ -34,12 +34,10 @@ from .schemas import Warehouse def get_warehouses_paths(): for pattern in settings.BIJOE_SCHEMAS: - for path in glob.glob(pattern): - yield path + yield from glob.glob(pattern) if hasattr(connection, 'tenant'): pattern = os.path.join(connection.tenant.get_directory(), 'schemas', '*.model') - for path in glob.glob(pattern): - yield path + yield from glob.glob(pattern) @lru_cache() @@ -70,8 +68,8 @@ def human_join(l): if len(l) == 1: return l[0] if len(l) > 2: - l = u', '.join(l[:-1]), l[-1] - return _(u'{0} and {1}').format(l[0], l[1]) + l = ', '.join(l[:-1]), l[-1] + return _('{0} and {1}').format(l[0], l[1]) def export_site(): diff --git a/bijoe/uwsgi.py b/bijoe/uwsgi.py index f8f0153..1632c47 100644 --- a/bijoe/uwsgi.py +++ b/bijoe/uwsgi.py @@ -21,7 +21,6 @@ import os import subprocess from django.conf import settings - from uwsgidecorators import cron, spool # existing loggers are disabled by Django on django.setup() due do @@ -58,7 +57,8 @@ def launch_wcs_olap(wcs_olap_ini_path): '--all', wcs_olap_ini_path, ], - check=False) + check=False, + ) logger.info('finished wcs-olap on %s', wcs_olap_ini_path) diff --git a/bijoe/views.py b/bijoe/views.py index a7b6c95..60f1911 100644 --- a/bijoe/views.py +++ b/bijoe/views.py @@ -17,31 +17,31 @@ import json from django.conf import settings -from django.shortcuts import resolve_url -from django.urls import reverse -from django.views.generic import ListView, View -from django.http import HttpResponse, HttpResponseRedirect -from django.utils.decorators import method_decorator -from django.utils.http import quote -from django.utils.translation import ugettext as _ from django.contrib.auth import logout as auth_logout from django.contrib.auth import views as auth_views from django.contrib.auth.views import redirect_to_login from django.core.exceptions import PermissionDenied +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import resolve_url +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import quote +from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache +from django.views.generic import ListView, View try: from mellon.utils import get_idps except ImportError: get_idps = lambda: [] -from .utils import get_warehouses from .engine import Engine +from .utils import get_warehouses from .visualization.models import Visualization from .visualization.utils import Visualization as VisuUtil -class AuthorizationMixin(object): +class AuthorizationMixin: def authorize(self, request): if request.user.is_authenticated: if not request.user.is_superuser: @@ -52,7 +52,7 @@ class AuthorizationMixin(object): def dispatch(self, request, *args, **kwargs): if self.authorize(request): - return super(AuthorizationMixin, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) else: return redirect_to_login(request.build_absolute_uri()) @@ -64,9 +64,8 @@ class HomepageView(AuthorizationMixin, ListView): paginate_by = settings.PAGE_LENGTH def get_context_data(self, **kwargs): - ctx = super(HomepageView, self).get_context_data(**kwargs) - ctx['warehouses'] = sorted((Engine(w) for w in get_warehouses()), - key=lambda w: w.label) + ctx = super().get_context_data(**kwargs) + ctx['warehouses'] = sorted((Engine(w) for w in get_warehouses()), key=lambda w: w.label) ctx['request'] = self.request return ctx @@ -108,7 +107,7 @@ class LoginView(auth_views.LoginView): return HttpResponseRedirect( resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next')) ) - return super(LoginView, self).get(request, *args, **kwargs) + return super().get(request, *args, **kwargs) login = LoginView.as_view() @@ -119,7 +118,7 @@ class LogoutView(auth_views.LogoutView): def dispatch(self, request, *args, **kwargs): if any(get_idps()): return HttpResponseRedirect(resolve_url('mellon_logout')) - return super(LogoutView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) logout = LogoutView.as_view() diff --git a/bijoe/visualization/admin.py b/bijoe/visualization/admin.py index a57ff62..2abf221 100644 --- a/bijoe/visualization/admin.py +++ b/bijoe/visualization/admin.py @@ -22,4 +22,5 @@ from . import models class VisualizationAdmin(admin.ModelAdmin): list_display = ['name'] + admin.site.register(models.Visualization, VisualizationAdmin) diff --git a/bijoe/visualization/forms.py b/bijoe/visualization/forms.py index 57e651c..50aa845 100644 --- a/bijoe/visualization/forms.py +++ b/bijoe/visualization/forms.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert # @@ -17,13 +16,12 @@ from django import forms -from django.core.exceptions import ValidationError -from django.utils.encoding import force_text -from django.utils.translation import ugettext as _ -from django.utils.safestring import mark_safe -from django.forms import ModelForm, TextInput, NullBooleanField from django.conf import settings - +from django.core.exceptions import ValidationError +from django.forms import ModelForm, NullBooleanField, TextInput +from django.utils.encoding import force_text +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ from django_select2.forms import HeavySelect2MultipleWidget from . import models @@ -32,54 +30,59 @@ from . import models class VisualizationForm(ModelForm): class Meta: model = models.Visualization - exclude = ('slug', 'parameters',) + exclude = ( + 'slug', + 'parameters', + ) widgets = { 'name': TextInput, } + DATE_RANGES = [ { 'value': '3_last_months', 'label': _('3 last months'), - 'start': u"les 3 derniers mois", - 'end': u"maintenant", + 'start': "les 3 derniers mois", + 'end': "maintenant", }, { 'value': 'this_year', 'label': _('this year'), - 'start': u"cette année", - 'end': u"l\'année prochaine", + 'start': "cette année", + 'end': "l\'année prochaine", }, { 'value': 'last_year', 'label': _('last year'), - 'start': u'l\'année dernière', - 'end': u'cette année', + 'start': 'l\'année dernière', + 'end': 'cette année', }, { 'value': 'this_quarter', 'label': _('this quarter'), - 'start': u'ce trimestre', + 'start': 'ce trimestre', 'end': "le prochain trimestre", }, { 'value': 'last_quarter', 'label': _('last quarter'), - 'start': u'le dernier trimestre', - 'end': u'ce trimestre', + 'start': 'le dernier trimestre', + 'end': 'ce trimestre', }, { 'value': 'since_1jan_last_year', 'label': _('since 1st january last year'), - 'start': u'l\'année dernière', - 'end': u'maintenant', + 'start': 'l\'année dernière', + 'end': 'maintenant', }, ] def get_date_range_choices(): - return [('', '---')] + [(r['value'], r['label']) - for r in getattr(settings, 'BIJOE_DATE_RANGES', DATE_RANGES)] + return [('', '---')] + [ + (r['value'], r['label']) for r in getattr(settings, 'BIJOE_DATE_RANGES', DATE_RANGES) + ] class DateRangeWidget(forms.MultiWidget): @@ -87,15 +90,15 @@ class DateRangeWidget(forms.MultiWidget): attrs = attrs.copy() if attrs else {} attrs.update({'type': 'date', 'autocomplete': 'off'}) attrs1 = attrs.copy() - attrs1['placeholder'] = _(u'start') + attrs1['placeholder'] = _('start') attrs2 = attrs.copy() - attrs2['placeholder'] = _(u'end') + attrs2['placeholder'] = _('end') widgets = ( forms.DateInput(attrs=attrs1, format='%Y-%m-%d'), forms.DateInput(attrs=attrs2, format='%Y-%m-%d'), forms.Select(choices=get_date_range_choices()), ) - super(DateRangeWidget, self).__init__(widgets, attrs=attrs) + super().__init__(widgets, attrs=attrs) def decompress(self, value): if not value: @@ -106,11 +109,10 @@ class DateRangeWidget(forms.MultiWidget): return value['start'], value['end'], None def render(self, name, value, attrs=None, renderer=None): - output = super(DateRangeWidget, self).render(name, value, attrs=attrs) + output = super().render(name, value, attrs=attrs) _id = self.build_attrs(attrs).get('id', None) if _id: - output += mark_safe("" % - _id) + output += mark_safe("" % _id) return output class Media: @@ -125,10 +127,9 @@ class DateRangeField(forms.MultiValueField): fields = ( forms.DateField(required=False), forms.DateField(required=False), - forms.ChoiceField(choices=get_date_range_choices(), required=False) + forms.ChoiceField(choices=get_date_range_choices(), required=False), ) - super(DateRangeField, self).__init__(fields=fields, require_all_fields=False, *args, - **kwargs) + super().__init__(fields=fields, require_all_fields=False, *args, **kwargs) def compress(self, values): if not values: @@ -159,10 +160,10 @@ class Select2ChoicesWidget(HeavySelect2MultipleWidget): class CubeForm(forms.Form): representation = forms.ChoiceField( - label=_(u'Presentation'), - choices=[('table', _('table')), - ('graphical', _('chart'))], - widget=forms.RadioSelect()) + label=_('Presentation'), + choices=[('table', _('table')), ('graphical', _('chart'))], + widget=forms.RadioSelect(), + ) def __init__(self, *args, **kwargs): self.cube = cube = kwargs.pop('cube') @@ -170,12 +171,13 @@ class CubeForm(forms.Form): dimension_choices = [('', '')] + [ (dimension.name, dimension.label) - for dimension in cube.dimensions if dimension.type not in ('datetime', 'date')] + for dimension in cube.dimensions + if dimension.type not in ('datetime', 'date') + ] # loop self.base_fields['loop'] = forms.ChoiceField( - label=_('Loop by'), - choices=dimension_choices, - required=False) + label=_('Loop by'), choices=dimension_choices, required=False + ) # filters for dimension in cube.dimensions: @@ -184,10 +186,12 @@ class CubeForm(forms.Form): field_name = 'filter__%s' % dimension.name if dimension.type == 'date': self.base_fields[field_name] = DateRangeField( - label=dimension.label.capitalize(), required=False) + label=dimension.label.capitalize(), required=False + ) elif dimension.type == 'bool': self.base_fields[field_name] = NullBooleanField( - label=dimension.label.capitalize(), required=False) + label=dimension.label.capitalize(), required=False + ) else: members = [] for _id, label in dimension.members(): @@ -200,6 +204,7 @@ class CubeForm(forms.Form): if v == s: return value return None + return f self.base_fields[field_name] = forms.TypedMultipleChoiceField( @@ -211,38 +216,34 @@ class CubeForm(forms.Form): data_view='select2-choices', warehouse=cube.engine.warehouse.name, cube=cube.name, - dimension=dimension.name - )) + dimension=dimension.name, + ), + ) # group by self.base_fields['drilldown_x'] = forms.ChoiceField( - label=_('Group by - horizontally'), - choices=dimension_choices, - required=False) + label=_('Group by - horizontally'), choices=dimension_choices, required=False + ) self.base_fields['drilldown_y'] = forms.ChoiceField( - label=_('Group by - vertically'), - choices=dimension_choices, - required=False) + label=_('Group by - vertically'), choices=dimension_choices, required=False + ) # measures - choices = [(measure.name, measure.label) - for measure in cube.measures if measure.type != 'point'] - self.base_fields['measure'] = forms.ChoiceField( - label=_('Measure'), choices=choices) + choices = [(measure.name, measure.label) for measure in cube.measures if measure.type != 'point'] + self.base_fields['measure'] = forms.ChoiceField(label=_('Measure'), choices=choices) - super(CubeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def clean(self): - cleaned_data = super(CubeForm, self).clean() + cleaned_data = super().clean() loop = cleaned_data.get('loop') drilldown_x = cleaned_data.get('drilldown_x') drilldown_y = cleaned_data.get('drilldown_y') if loop and (loop == drilldown_x or loop == drilldown_y): - raise ValidationError({'loop': _('You cannot use the same dimension for looping and' - ' grouping')}) + raise ValidationError({'loop': _('You cannot use the same dimension for looping and grouping')}) return cleaned_data diff --git a/bijoe/visualization/migrations/0001_initial.py b/bijoe/visualization/migrations/0001_initial.py index ebef41b..337dcdf 100644 --- a/bijoe/visualization/migrations/0001_initial.py +++ b/bijoe/visualization/migrations/0001_initial.py @@ -1,20 +1,19 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models import jsonfield.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( name='Visualization', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('name', models.TextField(verbose_name='name')), ('parameters', jsonfield.fields.JSONField(default=dict, verbose_name='parameters')), ], diff --git a/bijoe/visualization/migrations/0002_rename_parameters.py b/bijoe/visualization/migrations/0002_rename_parameters.py index 7a0a88a..0045893 100644 --- a/bijoe/visualization/migrations/0002_rename_parameters.py +++ b/bijoe/visualization/migrations/0002_rename_parameters.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations diff --git a/bijoe/visualization/migrations/0003_visualization_slug.py b/bijoe/visualization/migrations/0003_visualization_slug.py index 5d284f4..b0fbdb7 100644 --- a/bijoe/visualization/migrations/0003_visualization_slug.py +++ b/bijoe/visualization/migrations/0003_visualization_slug.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.12 on 2019-03-28 07:17 -from __future__ import unicode_literals from django.db import migrations, models from django.utils.text import slugify @@ -38,5 +36,5 @@ class Migration(migrations.Migration): name='slug', field=models.SlugField(null=True, unique=True, verbose_name='Identifier'), ), - migrations.RunPython(forward_func, reverse_func) + migrations.RunPython(forward_func, reverse_func), ] diff --git a/bijoe/visualization/migrations/0004_auto_20190328_0825.py b/bijoe/visualization/migrations/0004_auto_20190328_0825.py index fe33d77..8a22d39 100644 --- a/bijoe/visualization/migrations/0004_auto_20190328_0825.py +++ b/bijoe/visualization/migrations/0004_auto_20190328_0825.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.12 on 2019-03-28 07:25 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/bijoe/visualization/models.py b/bijoe/visualization/models.py index 76600b9..f8805df 100644 --- a/bijoe/visualization/models.py +++ b/bijoe/visualization/models.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import json import datetime +import json from django.db import models from django.http import Http404 @@ -57,19 +57,12 @@ class Visualization(models.Model): return (self.slug,) def export_json(self): - visualization = { - 'slug': self.slug, - 'name': self.name, - 'parameters': self.parameters - } + visualization = {'slug': self.slug, 'name': self.name, 'parameters': self.parameters} return visualization @classmethod def import_json(cls, data): - defaults = { - 'name': data['name'], - 'parameters': data['parameters'] - } + defaults = {'name': data['name'], 'parameters': data['parameters']} _, created = cls.objects.update_or_create(slug=data['slug'], defaults=defaults) return created @@ -85,7 +78,7 @@ class Visualization(models.Model): i += 1 slug = '%s-%s' % (base_slug, i) self.slug = slug - return super(Visualization, self).save(*args, **kwargs) + return super().save(*args, **kwargs) @property def exists(self): diff --git a/bijoe/visualization/ods.py b/bijoe/visualization/ods.py index 1b0e964..ec2d836 100644 --- a/bijoe/visualization/ods.py +++ b/bijoe/visualization/ods.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert @@ -17,13 +16,11 @@ # along with this program. If not, see . import sys - -import zipfile import xml.etree.ElementTree as ET +import zipfile from django.utils.encoding import force_text - OFFICE_NS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0' TABLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0' TEXT_NS = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0' @@ -31,13 +28,10 @@ XLINK_NS = 'http://www.w3.org/1999/xlink' def is_number(x): - if sys.version_info >= (3, 0): - return isinstance(x, (int, float)) - else: - return isinstance(x, (int, long, float)) + return isinstance(x, (int, float)) -class Workbook(object): +class Workbook: def __init__(self, encoding='utf-8'): self.sheets = [] self.encoding = encoding @@ -64,21 +58,27 @@ class Workbook(object): z = zipfile.ZipFile(output, 'w') z.writestr('content.xml', self.get_data()) z.writestr('mimetype', 'application/vnd.oasis.opendocument.spreadsheet') - z.writestr('META-INF/manifest.xml', ''' + z.writestr( + 'META-INF/manifest.xml', + ''' -''') - z.writestr('styles.xml', ''' +''', + ) + z.writestr( + 'styles.xml', + ''' -''') +''', + ) z.close() -class WorkSheet(object): +class WorkSheet: def __init__(self, workbook, name): self.cells = {} self.name = name @@ -104,7 +104,7 @@ class WorkSheet(object): return root -class WorkCell(object): +class WorkCell: def __init__(self, worksheet, value, hint=None): self.value_type = 'string' if is_number(value): diff --git a/bijoe/visualization/signature.py b/bijoe/visualization/signature.py index b542b76..62cd3f7 100644 --- a/bijoe/visualization/signature.py +++ b/bijoe/visualization/signature.py @@ -14,13 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import datetime import base64 -import hmac +import datetime import hashlib -import urllib -import random +import hmac import logging +import random +import urllib from django.utils import six from django.utils.encoding import force_bytes, smart_bytes @@ -46,10 +46,7 @@ def sign_query(query, key, algo='sha256', timestamp=None, nonce=None): new_query = query if new_query: new_query += '&' - new_query += urlencode(( - ('algo', algo), - ('timestamp', timestamp), - ('nonce', nonce))) + new_query += urlencode((('algo', algo), ('timestamp', timestamp), ('nonce', nonce))) signature = base64.b64encode(sign_string(new_query, key, algo=algo)) new_query += '&signature=' + quote(signature) return new_query @@ -71,7 +68,8 @@ def check_query(query, key, known_nonce=None, timedelta=30): if not res: key_hash = 'md5:%s' % hashlib.md5(force_bytes(key)).hexdigest()[:6] logging.getLogger(__name__).warning( - 'could not check signature of query %r with key %s: %s', query, key_hash, error) + 'could not check signature of query %r with key %s: %s', query, key_hash, error + ) return res @@ -112,10 +110,6 @@ def check_string(s, signature, key, algo='sha256'): if len(signature2) != len(signature): return False res = 0 - if six.PY3: - for a, b in zip(signature, signature2): - res |= a ^ b - else: - for a, b in zip(signature, signature2): - res |= ord(a) ^ ord(b) + for a, b in zip(signature, signature2): + res |= a ^ b return res == 0 diff --git a/bijoe/visualization/urls.py b/bijoe/visualization/urls.py index 7187e99..2a3e79b 100644 --- a/bijoe/visualization/urls.py +++ b/bijoe/visualization/urls.py @@ -19,20 +19,18 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^$', - views.visualizations, name='visualizations'), - url(r'^json/$', - views.visualizations_json, name='visualizations-json'), - url(r'^import/$', - views.visualizations_import, name='visualizations-import'), - url(r'^export$', - views.visualizations_export, name='visualizations-export'), + url(r'^$', views.visualizations, name='visualizations'), + url(r'^json/$', views.visualizations_json, name='visualizations-json'), + url(r'^import/$', views.visualizations_import, name='visualizations-import'), + url(r'^export$', views.visualizations_export, name='visualizations-export'), url(r'^warehouse/(?P[^/]*)/$', views.warehouse, name='warehouse'), url(r'^warehouse/(?P[^/]*)/(?P[^/]*)/$', views.cube, name='cube'), - url(r'^warehouse/(?P[^/]*)/(?P[^/]*)/iframe/$', views.cube_iframe, - name='cube-iframe'), - url(r'warehouse/(?P[^/]*)/(?P[^/]*)/save/$', - views.create_visualization, name='create-visualization'), + url(r'^warehouse/(?P[^/]*)/(?P[^/]*)/iframe/$', views.cube_iframe, name='cube-iframe'), + url( + r'warehouse/(?P[^/]*)/(?P[^/]*)/save/$', + views.create_visualization, + name='create-visualization', + ), url(r'(?P\d+)/$', views.visualization, name='visualization'), url(r'(?P\d+)/json/$', views.visualization_json, name='visualization-json'), url(r'(?P\d+)/geojson/$', views.visualization_geojson, name='visualization-geojson'), diff --git a/bijoe/visualization/utils.py b/bijoe/visualization/utils.py index f206cdc..7b36546 100644 --- a/bijoe/visualization/utils.py +++ b/bijoe/visualization/utils.py @@ -14,31 +14,31 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from __future__ import unicode_literals -import re -import json -import hashlib +import collections +import copy import datetime import decimal -import copy -import collections +import hashlib +import json +import re +from django.conf import settings from django.core.cache import cache +from django.http import Http404 from django.utils.encoding import force_bytes, force_text from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -from django.http import Http404 -from django.conf import settings +from ..engine import Engine, MeasureCell, Member from ..utils import get_warehouses -from ..engine import Engine, Member, MeasureCell from .ods import Workbook -class Visualization(object): - def __init__(self, cube, representation, measure, drilldown_x=None, drilldown_y=None, - filters=None, loop=None): +class Visualization: + def __init__( + self, cube, representation, measure, drilldown_x=None, drilldown_y=None, filters=None, loop=None + ): self.cube = cube self.representation = representation @@ -74,9 +74,15 @@ class Visualization(object): } def copy(self): - return Visualization(self.cube, self.representation, measure=self.measure, - drilldown_x=self.drilldown_x, drilldown_y=self.drilldown_y, - filters=copy.deepcopy(self.filters), loop=self.loop) + return Visualization( + self.cube, + self.representation, + measure=self.measure, + drilldown_x=self.drilldown_x, + drilldown_y=self.drilldown_y, + filters=copy.deepcopy(self.filters), + loop=self.loop, + ) @staticmethod def get_cube(d, warehouses=None): @@ -107,8 +113,15 @@ class Visualization(object): loop = d.get('loop') if loop: loop = cube.dimensions[loop] - return cls(cube, representation, measure, drilldown_x=drilldown_x, drilldown_y=drilldown_y, - filters=filters, loop=loop) + return cls( + cube, + representation, + measure, + drilldown_x=drilldown_x, + drilldown_y=drilldown_y, + filters=filters, + loop=loop, + ) @classmethod def from_form(cls, cube, form): @@ -126,11 +139,15 @@ class Visualization(object): drilldown_y = drilldown_y and cube.dimensions[drilldown_y] loop = cleaned_data.get('loop') loop = loop and cube.dimensions[loop] - return cls(cube, cleaned_data['representation'], - measure, - drilldown_x=drilldown_x, - drilldown_y=drilldown_y, - filters=filters, loop=loop) + return cls( + cube, + cleaned_data['representation'], + measure, + drilldown_x=drilldown_x, + drilldown_y=drilldown_y, + filters=filters, + loop=loop, + ) @property def key(self): @@ -147,21 +164,20 @@ class Visualization(object): keys.append('$'.join([kw] + sorted(map(force_text, value)))) else: # scalar values - keys.append(u'%s$%s' % (kw, force_text(value))) + keys.append('%s$%s' % (kw, force_text(value))) keys += [dim.name for dim in self.drilldown] keys += [self.measure.name] key = '$'.join(v.encode('utf8') for v in keys) return hashlib.md5(force_bytes(key)).hexdigest() def data(self): - '''Execute aggregation query, list members and check None values in - dimensions. - ''' - rows = list(self.cube.query(self.filters.items(), - self.drilldown, - [self.measure])) - self.members = {dimension: list(dimension.members(filters=self.filters.items())) - for dimension in self.drilldown} + """Execute aggregation query, list members and check None values in + dimensions. + """ + rows = list(self.cube.query(self.filters.items(), self.drilldown, [self.measure])) + self.members = { + dimension: list(dimension.members(filters=self.filters.items())) for dimension in self.drilldown + } seen_none = set() for cells in rows: # Keep "empty" dimension value if there is a non-zero measure associated @@ -268,10 +284,12 @@ class Visualization(object): y_axis, grid = self.table_1d() table.append([self.drilldown_y.label, self.measure.label]) for y in y_axis: - table.append([ - y.label, - '%s' % (grid[y.id],), - ]) + table.append( + [ + y.label, + '%s' % (grid[y.id],), + ] + ) else: table.append([self.measure.label, '%s' % (self.data()[0].measures[0],)]) @@ -296,12 +314,16 @@ class Visualization(object): if isinstance(value, decimal.Decimal): value = float(value) if isinstance(value, datetime.timedelta): - value = value.days + value.seconds / 86400. + value = value.days + value.seconds / 86400.0 return value if len(self.drilldown) == 2: (x_axis, y_axis), grid = self.table_2d() - cells = ((['%s' % x.label, '%s' % y.label], cell_value(grid[(x.id, y.id)])) for x in x_axis for y in y_axis) + cells = ( + (['%s' % x.label, '%s' % y.label], cell_value(grid[(x.id, y.id)])) + for x in x_axis + for y in y_axis + ) elif len(self.drilldown) == 1: axis, grid = self.table_1d() cells = ((['%s' % x.label], cell_value(grid[x.id])) for x in axis) @@ -315,10 +337,12 @@ class Visualization(object): raise NotImplementedError for coords, value in cells: - json_data.append({ - 'coords': [{'value': coord} for coord in coords], - 'measures': [{'value': value}], - }) + json_data.append( + { + 'coords': [{'value': coord} for coord in coords], + 'measures': [{'value': value}], + } + ) return json_data @@ -350,7 +374,7 @@ class Visualization(object): l.append(self.drilldown_y.label) if self.loop: l.append(self.loop.label) - return u', '.join(l) + return ', '.join(l) def __iter__(self): if self.loop: diff --git a/bijoe/visualization/views.py b/bijoe/visualization/views.py index 7d24ca6..af467eb 100644 --- a/bijoe/visualization/views.py +++ b/bijoe/visualization/views.py @@ -14,47 +14,48 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from __future__ import unicode_literals import hashlib import json from django.conf import settings -from django.core import signing -from django.core.signing import BadSignature from django.contrib import messages +from django.core import signing +from django.core.exceptions import PermissionDenied +from django.core.signing import BadSignature +from django.http import Http404, HttpResponse, JsonResponse +from django.shortcuts import redirect +from django.urls import reverse, reverse_lazy from django.utils.encoding import force_bytes, force_text from django.utils.text import slugify from django.utils.timezone import now -from django.utils.translation import ungettext, ugettext as _ -from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView -from django.views.generic.list import MultipleObjectMixin -from django.views.generic import DetailView, ListView, View, TemplateView -from django.shortcuts import redirect -from django.urls import reverse, reverse_lazy -from django.http import HttpResponse, Http404, JsonResponse -from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext from django.views.decorators.clickjacking import xframe_options_exempt - +from django.views.generic import DetailView, ListView, TemplateView, View +from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView +from django.views.generic.list import MultipleObjectMixin from django_select2.cache import cache from rest_framework import generics from rest_framework.response import Response -from bijoe.utils import get_warehouses, import_site, export_site -from ..engine import Engine -from . import models, forms, signature -from .utils import Visualization +from bijoe.utils import export_site, get_warehouses, import_site + from .. import views +from ..engine import Engine +from . import forms, models, signature +from .utils import Visualization class WarehouseView(views.AuthorizationMixin, TemplateView): template_name = 'bijoe/warehouse.html' def get_context_data(self, **kwargs): - ctx = super(WarehouseView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) try: - warehouse = [warehouse for warehouse in get_warehouses() - if warehouse.name == self.kwargs['warehouse']][0] + warehouse = [ + warehouse for warehouse in get_warehouses() if warehouse.name == self.kwargs['warehouse'] + ][0] except IndexError: raise Http404 ctx['warehouse'] = Engine(warehouse) @@ -63,16 +64,16 @@ class WarehouseView(views.AuthorizationMixin, TemplateView): return ctx -class CubeDisplayMixin(object): +class CubeDisplayMixin: def get_context_data(self, **kwargs): - ctx = super(CubeDisplayMixin, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) ctx['warehouse'] = self.warehouse ctx['cube'] = self.cube ctx['visualization'] = self.visualization return ctx -class CubeMixin(object): +class CubeMixin: def visualization(self, request, cube): self.form = forms.CubeForm(cube=self.cube, data=request.GET or request.POST) if self.form.is_valid(): @@ -80,8 +81,9 @@ class CubeMixin(object): def dispatch(self, request, *args, **kwargs): try: - self.warehouse = Engine([warehouse for warehouse in get_warehouses() - if warehouse.name == self.kwargs['warehouse']][0]) + self.warehouse = Engine( + [warehouse for warehouse in get_warehouses() if warehouse.name == self.kwargs['warehouse']][0] + ) except IndexError: raise Http404 try: @@ -89,7 +91,7 @@ class CubeMixin(object): except KeyError: raise Http404 self.visualization = self.visualization(request, cube) - return super(CubeMixin, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) class CubeView(views.AuthorizationMixin, CubeDisplayMixin, CubeMixin, TemplateView): @@ -101,7 +103,7 @@ class CubeView(views.AuthorizationMixin, CubeDisplayMixin, CubeMixin, TemplateVi return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): - ctx = super(CubeView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) ctx['form'] = self.form return ctx @@ -115,13 +117,13 @@ class CreateVisualizationView(views.AuthorizationMixin, CubeMixin, CreateView): def get(self, request, *args, **kwargs): if not self.visualization: return redirect('homepage') - return super(CreateVisualizationView, self).get(request, *args, **kwargs) + return super().get(request, *args, **kwargs) def form_valid(self, form): if not self.visualization: return redirect('homepage') form.instance.parameters = self.visualization.to_json() - return super(CreateVisualizationView, self).form_valid(form) + return super().form_valid(form) class SaveAsVisualizationView(views.AuthorizationMixin, DetailView, CreateView): @@ -132,12 +134,10 @@ class SaveAsVisualizationView(views.AuthorizationMixin, DetailView, CreateView): def form_valid(self, form): form.instance.parameters = self.get_object().parameters - return super(SaveAsVisualizationView, self).form_valid(form) + return super().form_valid(form) def get_initial(self): - return { - 'name': '%s %s' % (self.get_object().name, _('(Copy)')) - } + return {'name': '%s %s' % (self.get_object().name, _('(Copy)'))} class VisualizationView(views.AuthorizationMixin, CubeDisplayMixin, DetailView): @@ -145,7 +145,7 @@ class VisualizationView(views.AuthorizationMixin, CubeDisplayMixin, DetailView): template_name = 'bijoe/visualization.html' def get_object(self): - named_visualization = super(VisualizationView, self).get_object() + named_visualization = super().get_object() if not hasattr(self, 'visualization'): self.visualization = Visualization.from_json(named_visualization.parameters) self.cube = self.visualization.cube @@ -165,7 +165,7 @@ class VisualizationView(views.AuthorizationMixin, CubeDisplayMixin, DetailView): return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): - ctx = super(VisualizationView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) initial = { 'representation': self.visualization.representation, 'measure': self.visualization.measure.name, @@ -218,7 +218,7 @@ class VisualizationsView(views.AuthorizationMixin, ListView): return self.model.all_visualizations() def get_context_data(self, **kwargs): - ctx = super(VisualizationsView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) ctx['request'] = self.request return ctx @@ -251,12 +251,14 @@ class VisualizationsJSONView(MultipleObjectMixin, View): sig = hashlib.sha1(force_bytes(sig)).hexdigest() path += '?signature=' + sig data_uri = reverse('visualization-json', kwargs={'pk': visualization.pk}) - data.append({ - 'name': visualization.name, - 'slug': visualization.slug, - 'path': request.build_absolute_uri(path), - 'data-url': request.build_absolute_uri(data_uri), - }) + data.append( + { + 'name': visualization.name, + 'slug': visualization.slug, + 'path': request.build_absolute_uri(path), + 'data-url': request.build_absolute_uri(data_uri), + } + ) response = HttpResponse(content_type='application/json') response.write(json.dumps(data)) return response @@ -294,14 +296,16 @@ class VisualizationGeoJSONView(generics.GenericAPIView): for cell in row.dimensions: properties[cell.dimension.label] = '%s' % (cell,) points = row.measures[0].value or [] - geojson['features'].append({ - 'type': 'Feature', - 'geometry': { - 'type': 'MultiPoint', - 'coordinates': [[coord for coord in point] for point in points], - }, - 'properties': properties, - }) + geojson['features'].append( + { + 'type': 'Feature', + 'geometry': { + 'type': 'MultiPoint', + 'coordinates': [[coord for coord in point] for point in points], + }, + 'properties': properties, + } + ) return Response(geojson) @@ -343,29 +347,30 @@ class VisualizationJSONView(generics.GenericAPIView): elif len(drilldowns) == 0: data = cell_value(visualization.data()[0].measures[0]) axis = {} - loop.append({ - 'data': data, - 'axis': axis - }) + loop.append({'data': data, 'axis': axis}) if not all_visualizations.loop: data = loop[0]['data'] axis = loop[0]['axis'] else: axis = loop[0]['axis'] - axis['loop'] = [x.label for x in all_visualizations.loop.members(all_visualizations.filters.items())] + axis['loop'] = [ + x.label for x in all_visualizations.loop.members(all_visualizations.filters.items()) + ] data = [x['data'] for x in loop] unit = 'seconds' if all_visualizations.measure.type == 'duration' else None measure = all_visualizations.measure.type - return Response({ - 'data': data, - 'axis': axis, - 'format': '1', - 'unit': unit, # legacy, prefer measure. - 'measure': measure, - }) + return Response( + { + 'data': data, + 'axis': axis, + 'format': '1', + 'unit': unit, # legacy, prefer measure. + 'measure': measure, + } + ) class ExportVisualizationView(views.AuthorizationMixin, DetailView): @@ -387,8 +392,7 @@ class VisualizationsImportView(views.AuthorizationMixin, FormView): def form_valid(self, form): try: - visualizations_json = json.loads( - force_text(self.request.FILES['visualizations_json'].read())) + visualizations_json = json.loads(force_text(self.request.FILES['visualizations_json'].read())) except ValueError: form.add_error('visualizations_json', _('File is not in the expected JSON format.')) return self.form_invalid(form) @@ -401,24 +405,31 @@ class VisualizationsImportView(views.AuthorizationMixin, FormView): if results.get('created') == 0: message1 = _('No visualization created.') else: - message1 = ungettext( - 'A visualization has been created.', - '%(count)d visualizations have been created.', - results['created']) % {'count': results['created']} + message1 = ( + ungettext( + 'A visualization has been created.', + '%(count)d visualizations have been created.', + results['created'], + ) + % {'count': results['created']} + ) if results.get('updated') == 0: message2 = _('No visualization updated.') else: - message2 = ungettext( - 'A visualization has been updated.', - '%(count)d visualizations have been updated.', - results['updated']) % {'count': results['updated']} - messages.info(self.request, u'%s %s' % (message1, message2)) + message2 = ( + ungettext( + 'A visualization has been updated.', + '%(count)d visualizations have been updated.', + results['updated'], + ) + % {'count': results['updated']} + ) + messages.info(self.request, '%s %s' % (message1, message2)) - return super(VisualizationsImportView, self).form_valid(form) + return super().form_valid(form) class VisualizationsExportView(views.AuthorizationMixin, View): - def get(self, request, *args, **kwargs): response = HttpResponse(content_type='application/json') response['Content-Disposition'] = ( @@ -429,12 +440,12 @@ class VisualizationsExportView(views.AuthorizationMixin, View): class Select2ChoicesView(View): - def get(self, request, *args, **kwargs): widget = self.get_widget_or_404() try: - warehouse = Engine([warehouse for warehouse in get_warehouses() - if warehouse.name == widget.warehouse][0]) + warehouse = Engine( + [warehouse for warehouse in get_warehouses() if warehouse.name == widget.warehouse][0] + ) cube = warehouse[widget.cube] self.dimension = cube.dimensions[widget.dimension] except IndexError: @@ -448,10 +459,12 @@ class Select2ChoicesView(View): term = request.GET.get('term', '') choices = self.get_choices(term, page_number, widget.max_results) - return JsonResponse({ - 'results': [{'text': label, 'id': s} for s, label in choices], - 'more': not(len(choices) < widget.max_results), - }) + return JsonResponse( + { + 'results': [{'text': label, 'id': s} for s, label in choices], + 'more': not (len(choices) < widget.max_results), + } + ) def get_choices(self, term, page_number, max_results): members = [] @@ -460,7 +473,7 @@ class Select2ChoicesView(View): members.append((None, '__none__', _('None'))) choices = [(s, label) for v, s, label in members if term in label.lower()] - choices = choices[page_number * max_results:(page_number * max_results) + max_results] + choices = choices[page_number * max_results : (page_number * max_results) + max_results] return choices def get_widget_or_404(self): diff --git a/debian/debian_config.py b/debian/debian_config.py index ee7bed8..ee9cae8 100644 --- a/debian/debian_config.py +++ b/debian/debian_config.py @@ -13,13 +13,13 @@ exec(open('/usr/lib/hobo/debian_config_common.py').read()) # SAML2 authentication AUTHENTICATION_BACKENDS = ('mellon.backends.SAMLBackend',) -MELLON_ATTRIBUTE_MAPPING = { +MELLON_ATTRIBUTE_MAPPING = { 'email': '{attributes[email][0]}', 'first_name': '{attributes[first_name][0]}', 'last_name': '{attributes[last_name][0]}', } -MELLON_SUPERUSER_MAPPING = { +MELLON_SUPERUSER_MAPPING = { 'is_superuser': 'true', } diff --git a/debian/scripts/warehouse_slug.py b/debian/scripts/warehouse_slug.py index 477c942..5397375 100644 --- a/debian/scripts/warehouse_slug.py +++ b/debian/scripts/warehouse_slug.py @@ -10,7 +10,6 @@ sudo -u bijoe bijoe-manage tenant_command runscript --all-tenants /usr/share/doc from bijoe.utils import get_warehouses from bijoe.visualization.models import Visualization - warehouses = get_warehouses() for visu in Visualization.objects.all(): for warehouse in warehouses: diff --git a/debian/settings.py b/debian/settings.py index 6cb5f8d..a9d8ae2 100644 --- a/debian/settings.py +++ b/debian/settings.py @@ -14,15 +14,15 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False -#ADMINS = ( +# ADMINS = ( # # ('User 1', 'watchdog@example.net'), # # ('User 2', 'janitor@example.net'), -#) +# ) # ALLOWED_HOSTS must be correct in production! # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = [ - '*', + '*', ] # Databases diff --git a/format_json_fixtures.py b/format_json_fixtures.py index 4d0419c..48f687a 100644 --- a/format_json_fixtures.py +++ b/format_json_fixtures.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import json import os diff --git a/setup.py b/setup.py index 50e631c..8d763f1 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,15 @@ #! /usr/bin/env python -import sys -import subprocess import os - +import subprocess +import sys from distutils.cmd import Command from distutils.command.build import build as _build -from setuptools import setup, find_packages -from setuptools.command.sdist import sdist + +from setuptools import find_packages, setup from setuptools.command.install_lib import install_lib as _install_lib +from setuptools.command.sdist import sdist + class eo_sdist(sdist): def run(self): @@ -24,21 +25,21 @@ class eo_sdist(sdist): def get_version(): - '''Use the VERSION, if absent generates a version with git describe, if not - tag exists, take 0.0.0- and add the length of the commit log. - ''' + """Use the VERSION, if absent generates a version with git describe, if not + tag exists, take 0.0.0- and add the length of the commit log. + """ if os.path.exists('VERSION'): - with open('VERSION', 'r') as v: + with open('VERSION') as v: return v.read() if os.path.exists('.git'): - p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + ['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) result = p.communicate()[0] if p.returncode == 0: result = result.decode('ascii').split()[0][1:] else: - result = '0.0.0-%s' % len(subprocess.check_output( - ['git', 'rev-list', 'HEAD']).splitlines()) + result = '0.0.0-%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines()) return result.replace('-', '.').replace('.g', '+g') return '0.0.0' @@ -56,6 +57,7 @@ class compile_translations(Command): def run(self): try: from django.core.management import call_command + for path, dirs, files in os.walk('bijoe'): if 'locale' not in dirs: continue @@ -77,27 +79,37 @@ class install_lib(_install_lib): _install_lib.run(self) -setup(name="bijoe", - version=get_version(), - license="AGPLv3+", - description="BI daashboard from PostgreSQL start schema", - long_description=open('README.rst').read(), - url="http://dev.entrouvert.org/projects/publik-bi/", - author="Entr'ouvert", - author_email="authentic@listes.entrouvert.com", - maintainer="Benjamin Dauvergne", - maintainer_email="bdauvergne@entrouvert.com", - packages=find_packages(), - include_package_data=True, - install_requires=['requests', 'django>=1.11, <2.3', 'psycopg2', 'isodate', 'Django-Select2<6', - 'XStatic-ChartNew.js', 'gadjo', 'django-jsonfield<1.3', - 'python-dateutil', - 'djangorestframework', - 'xstatic-select2'], - scripts=['manage.py'], - cmdclass={ - 'sdist': eo_sdist, - 'build': build, - 'install_lib': install_lib, - 'compile_translations': compile_translations, - }) +setup( + name="bijoe", + version=get_version(), + license="AGPLv3+", + description="BI daashboard from PostgreSQL start schema", + long_description=open('README.rst').read(), + url="http://dev.entrouvert.org/projects/publik-bi/", + author="Entr'ouvert", + author_email="authentic@listes.entrouvert.com", + maintainer="Benjamin Dauvergne", + maintainer_email="bdauvergne@entrouvert.com", + packages=find_packages(), + include_package_data=True, + install_requires=[ + 'requests', + 'django>=1.11, <2.3', + 'psycopg2', + 'isodate', + 'Django-Select2<6', + 'XStatic-ChartNew.js', + 'gadjo', + 'django-jsonfield<1.3', + 'python-dateutil', + 'djangorestframework', + 'xstatic-select2', + ], + scripts=['manage.py'], + cmdclass={ + 'sdist': eo_sdist, + 'build': build, + 'install_lib': install_lib, + 'compile_translations': compile_translations, + }, +) diff --git a/tests/conftest.py b/tests/conftest.py index 0701086..d37d6ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,26 +1,25 @@ -import os import glob import json -from contextlib import closing, contextmanager +import os +import shutil import subprocess import tempfile -import shutil - -import pytest +from contextlib import closing, contextmanager import django_webtest - import psycopg2 - - -from django.db import transaction +import pytest from django.contrib.auth.models import User from django.core.management import call_command +from django.db import transaction def pytest_addoption(parser): parser.addoption( - '--bijoe-store-table', action='store_true', default=False, help='Store tables value in new_tables.json', + '--bijoe-store-table', + action='store_true', + default=False, + help='Store tables value in new_tables.json', ) @@ -44,14 +43,14 @@ def john_doe(db): @pytest.fixture def admin(db): - u = User(username='super.user', first_name='Super', last_name='User', - email='super.user@example.net') + u = User(username='super.user', first_name='Super', last_name='User', email='super.user@example.net') u.set_password('super.user') u.is_superuser = True u.is_staff = True u.save() return u + SCHEMA_PATHS = os.path.join(os.path.dirname(__file__), 'fixtures/') @@ -83,9 +82,19 @@ def load_schema_db(schema): # load data for sql_path in sorted(glob.glob(os.path.join(schema_dir, '*.sql'))): - process = subprocess.Popen(['psql', '-c', '\\set ON_ERROR_STOP on', '--single-transaction', database_name, '-f', sql_path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + process = subprocess.Popen( + [ + 'psql', + '-c', + '\\set ON_ERROR_STOP on', + '--single-transaction', + database_name, + '-f', + sql_path, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) stdout, stderr = process.communicate() return_code = process.returncode assert return_code == 0, [stdout, stderr] @@ -97,7 +106,7 @@ def load_schema_db(schema): 'schema_dir': schema_dir, 'database_name': database_name, 'bijoe_schemas': [os.path.join(bijoe_schema_dir, '*_schema.json')], - 'fixtures': fixtures + 'fixtures': fixtures, } tables_path = os.path.join(schema_dir, 'tables.json') if os.path.exists(tables_path): diff --git a/tests/test_bijoe_schemas.py b/tests/test_bijoe_schemas.py index 9b426d5..6ed39f2 100644 --- a/tests/test_bijoe_schemas.py +++ b/tests/test_bijoe_schemas.py @@ -1,65 +1,71 @@ -# -*- coding: utf-8 -*- from bijoe.schemas import Warehouse def test_simple_parsing(): - Warehouse.from_json({ - 'name': 'coin', - 'label': 'coin', - 'pg_dsn': 'dbname=zozo', - 'search_path': ['cam', 'public'], - 'cubes': [ - { - 'name': 'all_formdata', - 'label': 'Tous les formulaires', - 'fact_table': 'formdata', - 'key': 'id', - 'joins': [ - { - 'name': 'formdef', - 'master': '{fact_table}.formdef_id', - 'table': 'formdef', - 'detail': 'formdef.id', - } - ], - 'dimensions': [ - { - 'label': 'formulaire', - 'name': 'formdef', - 'type': 'integer', - 'join': ['formdef'], - 'value': 'formdef.id', - 'value_label': 'formdef.label', - 'order_by': 'formdef.label' - }, - { - 'name': 'receipt_time', - 'label': 'date de soumission', - 'join': ['receipt_time'], - 'type': 'date', - 'value': 'receipt_time.date' - } - ], - 'measures': [ - { - 'type': 'integer', - 'label': 'Nombre de demandes', - 'expression': 'count({fact_table}.id)', - 'name': 'count' - }, - { - 'type': 'integer', - 'label': u'Délai de traitement', - 'expression': 'avg((to_char(endpoint_delay, \'9999.999\') || \' days\')::interval)', - 'name': 'avg_endpoint_delay' - }, - { - 'type': 'percent', - 'label': 'Pourcentage', - 'expression': 'count({fact_table}.id) * 100. / (select count({fact_table}.id) from {table_expression} where {where_conditions})', - 'name': 'percentage' - } - ] - } - ], - }) + Warehouse.from_json( + { + 'name': 'coin', + 'label': 'coin', + 'pg_dsn': 'dbname=zozo', + 'search_path': ['cam', 'public'], + 'cubes': [ + { + 'name': 'all_formdata', + 'label': 'Tous les formulaires', + 'fact_table': 'formdata', + 'key': 'id', + 'joins': [ + { + 'name': 'formdef', + 'master': '{fact_table}.formdef_id', + 'table': 'formdef', + 'detail': 'formdef.id', + } + ], + 'dimensions': [ + { + 'label': 'formulaire', + 'name': 'formdef', + 'type': 'integer', + 'join': ['formdef'], + 'value': 'formdef.id', + 'value_label': 'formdef.label', + 'order_by': 'formdef.label', + }, + { + 'name': 'receipt_time', + 'label': 'date de soumission', + 'join': ['receipt_time'], + 'type': 'date', + 'value': 'receipt_time.date', + }, + ], + 'measures': [ + { + 'type': 'integer', + 'label': 'Nombre de demandes', + 'expression': 'count({fact_table}.id)', + 'name': 'count', + }, + { + 'type': 'integer', + 'label': 'Délai de traitement', + 'expression': ( + 'avg((to_char(endpoint_delay, \'9999.999\') || \' days\')::interval)' + ), + 'name': 'avg_endpoint_delay', + }, + { + 'type': 'percent', + 'label': 'Pourcentage', + 'expression': ( + 'count({fact_table}.id) * 100. / (select count({fact_table}.id) from' + ' {table_expression} where {where_conditions})' + ), + 'name': 'percentage', + }, + ], + } + ], + } + ) diff --git a/tests/test_hobo_agent.py b/tests/test_hobo_agent.py index 9f53d39..669b8b7 100644 --- a/tests/test_hobo_agent.py +++ b/tests/test_hobo_agent.py @@ -20,9 +20,13 @@ from bijoe.hobo_agent.management.commands import hobo_deploy def test_schema_from_url(): for hash_length in [4, 5, 6, 7]: for length in [64, 65, 66]: - assert len(hobo_deploy.schema_from_url('https://' + ('x' * length), hash_length=hash_length)) == 63 + assert ( + len(hobo_deploy.schema_from_url('https://' + ('x' * length), hash_length=hash_length)) == 63 + ) - schema = hobo_deploy.schema_from_url('https://demarches-saint-didier-au-mont-dor.guichet-recette.grandlyon.com/') + schema = hobo_deploy.schema_from_url( + 'https://demarches-saint-didier-au-mont-dor.guichet-recette.grandlyon.com/' + ) assert len(schema) == 63 assert schema == 'demarches_saint_didier_au_mo0757cfguichet_recette_grandlyon_com' diff --git a/tests/test_hobo_deploy.py b/tests/test_hobo_deploy.py index 3d74f05..ef31a0a 100644 --- a/tests/test_hobo_deploy.py +++ b/tests/test_hobo_deploy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert # @@ -17,13 +16,10 @@ from contextlib import contextmanager -from psycopg2.extensions import parse_dsn - import pytest - -from django.utils.six.moves import configparser as ConfigParser - import sentry_sdk +from django.utils.six.moves import configparser as ConfigParser +from psycopg2.extensions import parse_dsn from bijoe.hobo_agent.management.commands import hobo_deploy @@ -33,7 +29,7 @@ def donothing(tenant): yield -class FakeTenant(object): +class FakeTenant: domain_url = 'fake.tenant.com' def __init__(self, directory): @@ -45,9 +41,7 @@ class FakeTenant(object): @pytest.fixture def sentry(): - sentry_sdk.init( - dsn='https://1234@sentry.example.com/1', - environment='prod') + sentry_sdk.init(dsn='https://1234@sentry.example.com/1', environment='prod') yield sentry_sdk.init() diff --git a/tests/test_import_export.py b/tests/test_import_export.py index e251f23..14688d9 100644 --- a/tests/test_import_export.py +++ b/tests/test_import_export.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - import json import os import shutil @@ -13,8 +9,8 @@ from django.core.management import call_command from django.utils.encoding import force_bytes from django.utils.six import StringIO -from bijoe.visualization.models import Visualization from bijoe.utils import import_site +from bijoe.visualization.models import Visualization pytestmark = pytest.mark.django_db @@ -35,7 +31,7 @@ def test_import_export(schema1, app): 'representation': 'table', 'loop': '', 'filters': {}, - 'drilldown_x': 'date__yearmonth' + 'drilldown_x': 'date__yearmonth', } def create_visu(i=0): diff --git a/tests/test_relative_time.py b/tests/test_relative_time.py index ac5e17b..22d78d0 100644 --- a/tests/test_relative_time.py +++ b/tests/test_relative_time.py @@ -1,16 +1,16 @@ -# -*- coding: utf-8 -*- from datetime import date + from bijoe.relative_time import RelativeDate def test_relative_date(): today = date(2016, 3, 3) - assert RelativeDate(u'cette année', today=today) == date(2016, 1, 1) - assert RelativeDate(u'ce mois', today=today) == date(2016, 3, 1) - assert RelativeDate(u'le mois dernier', today=today) == date(2016, 2, 1) - assert RelativeDate(u'les 4 derniers mois', today=today) == date(2015, 11, 1) - assert RelativeDate(u'le mois prochain', today=today) == date(2016, 4, 1) - assert RelativeDate(u'les 3 prochains mois', today=today) == date(2016, 6, 1) - assert RelativeDate(u' cette semaine', today=today) == date(2016, 2, 29) - assert RelativeDate(u' maintenant', today=today) == today - assert RelativeDate(u'2016-01-01', today=today) == date(2016, 1, 1) + assert RelativeDate('cette année', today=today) == date(2016, 1, 1) + assert RelativeDate('ce mois', today=today) == date(2016, 3, 1) + assert RelativeDate('le mois dernier', today=today) == date(2016, 2, 1) + assert RelativeDate('les 4 derniers mois', today=today) == date(2015, 11, 1) + assert RelativeDate('le mois prochain', today=today) == date(2016, 4, 1) + assert RelativeDate('les 3 prochains mois', today=today) == date(2016, 6, 1) + assert RelativeDate(' cette semaine', today=today) == date(2016, 2, 29) + assert RelativeDate(' maintenant', today=today) == today + assert RelativeDate('2016-01-01', today=today) == date(2016, 1, 1) diff --git a/tests/test_schema1.py b/tests/test_schema1.py index de521a0..32c047c 100644 --- a/tests/test_schema1.py +++ b/tests/test_schema1.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - import json -from utils import login, get_table, get_ods_table, get_ods_document, request_select2 +from utils import get_ods_document, get_ods_table, get_table, login, request_select2 -from bijoe.visualization.ods import OFFICE_NS, TABLE_NS from bijoe.visualization.models import Visualization as VisualizationModel +from bijoe.visualization.ods import OFFICE_NS, TABLE_NS from bijoe.visualization.utils import Visualization @@ -15,7 +13,7 @@ def test_simple(schema1, app, admin): response = response.click('schema1') response = response.click('Facts 1') assert 'big-msg-info' in response - assert u'le champ « pouët »' in response + assert 'le champ « pouët »' in response assert 'warning2' in response form = response.form form.set('representation', 'table') @@ -24,9 +22,21 @@ def test_simple(schema1, app, admin): response = form.submit('visualize') assert 'big-msg-info' not in response assert get_table(response) == [ - ['Inner SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98', - u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91'], - ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15'] + [ + 'Inner SubCategory', + 'sub\xe910', + 'sub\xe911', + 'sub\xe94', + 'sub\xe95', + 'sub\xe96', + 'sub\xe98', + 'sub\xe99', + 'sub\xe97', + 'sub\xe92', + 'sub\xe93', + 'sub\xe91', + ], + ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15'], ] form = response.form form.set('representation', 'table') @@ -35,7 +45,7 @@ def test_simple(schema1, app, admin): response = form.submit('visualize') assert 'big-msg-info' not in response assert get_table(response) == [ - ['mois (Date)', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt'], + ['mois (Date)', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt'], ['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'], ] @@ -54,14 +64,14 @@ def test_truncated_previous_year_range(schema1, app, admin, freezer): freezer.move_to('2019-01-01 01:00:00') response = form.submit('visualize') assert get_table(response) == [ - ['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'], + ['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'], ['2017', '0', '0', '0', '0', '0', '0', '0', '0', '0'], ] freezer.move_to('2018-01-01 01:00:00') response = form.submit('visualize') assert get_table(response) == [ - ['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'], + ['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'], ['2017', '10', '1', '1', '1', '1', '1', '1', '1', '17'], ] @@ -77,7 +87,9 @@ def test_boolean_dimension(schema1, app, admin): form.set('drilldown_x', 'boolean') response = form.submit('visualize') assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '9']] - form['filter__boolean'].force_value([o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0]) + form['filter__boolean'].force_value( + [o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0] + ) response = form.submit('visualize') assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '0']] @@ -92,7 +104,10 @@ def test_string_dimension(schema1, app, admin): form.set('measure', 'simple_count') form.set('drilldown_x', 'string') response = form.submit('visualize') - assert get_table(response) == [['String', 'a', 'b', 'c', 'Aucun(e)'], ['number of rows', '11', '2', '3', '1']] + assert get_table(response) == [ + ['String', 'a', 'b', 'c', 'Aucun(e)'], + ['number of rows', '11', '2', '3', '1'], + ] form['filter__string'].force_value(['a', 'b', '__none__']) response = form.submit('visualize') assert get_table(response) == [['String', 'a', 'b', 'Aucun(e)'], ['number of rows', '11', '2', '1']] @@ -100,18 +115,20 @@ def test_string_dimension(schema1, app, admin): def test_string_dimension_json_data(schema1, app, admin): # test conversion to Javascript declaration - visu = Visualization.from_json({ - 'warehouse': 'schema1', - 'cube': 'facts1', - 'representation': 'table', - 'measure': 'simple_count', - 'drilldown_x': 'string' - }) + visu = Visualization.from_json( + { + 'warehouse': 'schema1', + 'cube': 'facts1', + 'representation': 'table', + 'measure': 'simple_count', + 'drilldown_x': 'string', + } + ) assert json.loads(json.dumps(visu.json_data())) == [ - {u'coords': [{u'value': u'a'}], u'measures': [{u'value': 11}]}, - {u'coords': [{u'value': u'b'}], u'measures': [{u'value': 2}]}, - {u'coords': [{u'value': u'c'}], u'measures': [{u'value': 3}]}, - {u'coords': [{u'value': u'Aucun(e)'}], u'measures': [{u'value': 1}]} + {'coords': [{'value': 'a'}], 'measures': [{'value': 11}]}, + {'coords': [{'value': 'b'}], 'measures': [{'value': 2}]}, + {'coords': [{'value': 'c'}], 'measures': [{'value': 3}]}, + {'coords': [{'value': 'Aucun(e)'}], 'measures': [{'value': 1}]}, ] @@ -126,16 +143,26 @@ def test_item_dimension(schema1, app, admin): form.set('drilldown_x', 'outersubcategory') response = form.submit('visualize') assert get_table(response) == [ - ['Outer SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98', - u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91', 'Aucun(e)'], - ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1'] + [ + 'Outer SubCategory', + 'sub\xe910', + 'sub\xe911', + 'sub\xe94', + 'sub\xe95', + 'sub\xe96', + 'sub\xe98', + 'sub\xe99', + 'sub\xe97', + 'sub\xe92', + 'sub\xe93', + 'sub\xe91', + 'Aucun(e)', + ], + ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1'], ] form['filter__outersubcategory'].force_value(['__none__']) response = form.submit('visualize') - assert get_table(response) == [ - ['Outer SubCategory', 'Aucun(e)'], - ['number of rows', '1'] - ] + assert get_table(response) == [['Outer SubCategory', 'Aucun(e)'], ['number of rows', '1']] def test_yearmonth_drilldown(schema1, app, admin): @@ -149,9 +176,18 @@ def test_yearmonth_drilldown(schema1, app, admin): form.set('drilldown_x', 'date__yearmonth') response = form.submit('visualize') assert get_table(response) == [ - [u'ann\xe9e et mois (Date)', '01/2017', '02/2017', '03/2017', - '04/2017', '05/2017', '06/2017', '07/2017', '08/2017'], - ['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'] + [ + 'ann\xe9e et mois (Date)', + '01/2017', + '02/2017', + '03/2017', + '04/2017', + '05/2017', + '06/2017', + '07/2017', + '08/2017', + ], + ['number of rows', '10', '1', '1', '1', '1', '1', '1', '1'], ] @@ -196,53 +232,47 @@ def test_truncated_previous_year_range_on_datetime(schema1, app, admin, freezer) freezer.move_to('2019-01-01 01:00:00') response = form.submit('visualize') assert get_table(response) == [ - ['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'], + ['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'], ['2017', '0', '0', '0', '0', '0', '0', '0', '0', '0'], ] freezer.move_to('2018-01-01 01:00:00') response = form.submit('visualize') assert get_table(response) == [ - ['', 'janvier', u'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', u'ao\xfbt', 'Total'], + ['', 'janvier', 'f\xe9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\xfbt', 'Total'], ['2017', '10', '1', '1', '1', '1', '1', '1', '1', '17'], ] def test_none_percent_json_data_0d(schema1, app, admin): # test conversion to Javascript declaration - visu = Visualization.from_json({ - 'warehouse': 'schema1', - 'cube': 'facts1', - 'representation': 'graphical', - 'measure': 'percent', - }) - assert visu.json_data() == [{u'coords': [], u'measures': [{u'value': 100.0}]}] + visu = Visualization.from_json( + { + 'warehouse': 'schema1', + 'cube': 'facts1', + 'representation': 'graphical', + 'measure': 'percent', + } + ) + assert visu.json_data() == [{'coords': [], 'measures': [{'value': 100.0}]}] def test_none_percent_json_data_2d(schema1, app, admin): # test conversion to Javascript declaration - visu = Visualization.from_json({ - 'warehouse': 'schema1', - 'cube': 'facts1', - 'representation': 'graphical', - 'measure': 'percent', - 'drilldown_y': 'leftcategory', - 'drilldown_x': 'date__year', - }) - assert visu.json_data() == [ + visu = Visualization.from_json( { - 'coords': [{'value': u'2017'}, {'value': u'cat\xe92'}], - 'measures': [{'value': 0}] - }, - { - 'coords': [{'value': u'2017'}, {'value': u'cat\xe93'}], - 'measures': [{'value': 0}]}, - { - 'coords': [{'value': u'2017'}, {'value': u'cat\xe91'}], - 'measures': [{'value': 94.11764705882354}]}, - { - 'coords': [{'value': u'2017'}, {'value': u'Aucun(e)'}], - 'measures': [{'value': 5.882352941176471}] + 'warehouse': 'schema1', + 'cube': 'facts1', + 'representation': 'graphical', + 'measure': 'percent', + 'drilldown_y': 'leftcategory', + 'drilldown_x': 'date__year', } + ) + assert visu.json_data() == [ + {'coords': [{'value': '2017'}, {'value': 'cat\xe92'}], 'measures': [{'value': 0}]}, + {'coords': [{'value': '2017'}, {'value': 'cat\xe93'}], 'measures': [{'value': 0}]}, + {'coords': [{'value': '2017'}, {'value': 'cat\xe91'}], 'measures': [{'value': 94.11764705882354}]}, + {'coords': [{'value': '2017'}, {'value': 'Aucun(e)'}], 'measures': [{'value': 5.882352941176471}]}, ] @@ -258,96 +288,89 @@ def test_geoloc(schema1, app, admin): 'measure': 'percent', 'drilldown_y': 'outercategory', 'drilldown_x': 'date__year', - }) + }, + ) response = app.get('/visualization/%d/geojson/' % visu.pk) assert response.json == { 'type': 'FeatureCollection', - 'features': [{ - u'geometry': { - u'coordinates': [ - [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], - [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], - [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0] - ], - u'type': u'MultiPoint' + 'features': [ + { + 'geometry': { + 'coordinates': [ + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + ], + 'type': 'MultiPoint', + }, + 'properties': {'Outer Category': 'cat\xe91', 'ann\xe9e (Date)': '2017'}, + 'type': 'Feature', }, - u'properties': { - u'Outer Category': u'cat\xe91', - u'ann\xe9e (Date)': u'2017' + { + 'geometry': {'coordinates': [[1.0, 1.0]], 'type': 'MultiPoint'}, + 'properties': {'Outer Category': 'Aucun(e)', 'ann\xe9e (Date)': '2017'}, + 'type': 'Feature', }, - u'type': u'Feature' - }, - { - u'geometry': { - u'coordinates': [[1.0, 1.0]], - u'type': u'MultiPoint' + { + 'geometry': {'coordinates': [], 'type': 'MultiPoint'}, + 'properties': {'Outer Category': 'cat\xe92', 'ann\xe9e (Date)': 'Aucun(e)'}, + 'type': 'Feature', }, - u'properties': { - u'Outer Category': u'Aucun(e)', - u'ann\xe9e (Date)': u'2017' + { + 'geometry': {'coordinates': [], 'type': 'MultiPoint'}, + 'properties': {'Outer Category': 'cat\xe93', 'ann\xe9e (Date)': 'Aucun(e)'}, + 'type': 'Feature', }, - u'type': u'Feature' - }, - { - u'geometry': { - u'coordinates': [], - u'type': u'MultiPoint' + { + 'geometry': {'coordinates': [], 'type': 'MultiPoint'}, + 'properties': {'Outer Category': 'cat\xe91', 'ann\xe9e (Date)': 'Aucun(e)'}, + 'type': 'Feature', }, - u'properties': { - u'Outer Category': u'cat\xe92', - u'ann\xe9e (Date)': u'Aucun(e)' - }, - u'type': u'Feature' - }, - { - u'geometry': { - u'coordinates': [], - u'type': u'MultiPoint' - }, - u'properties': { - u'Outer Category': u'cat\xe93', - u'ann\xe9e (Date)': u'Aucun(e)' - }, - u'type': u'Feature' - }, - { - u'geometry': { - u'coordinates': [], - u'type': u'MultiPoint' - }, - u'properties': { - u'Outer Category': u'cat\xe91', - u'ann\xe9e (Date)': u'Aucun(e)' - }, - u'type': u'Feature' - } - ]} + ], + } def test_filter_type_mismatch(schema1, app, admin): # test conversion to Javascript declaration - visu = Visualization.from_json({ - 'warehouse': 'schema1', - 'cube': 'facts1', - 'representation': 'graphical', - 'measure': 'simple_count', - 'filters': { - 'string': [1], + visu = Visualization.from_json( + { + 'warehouse': 'schema1', + 'cube': 'facts1', + 'representation': 'graphical', + 'measure': 'simple_count', + 'filters': { + 'string': [1], + }, } - }) + ) assert visu.json_data() == [{'coords': [], 'measures': [{'value': 0}]}] def test_empty_filter(schema1, app, admin): - visu = Visualization.from_json({ - 'warehouse': 'schema1', - 'cube': 'facts1', - 'representation': 'graphical', - 'measure': 'simple_count', - 'filters': { - 'innercategory': [], + visu = Visualization.from_json( + { + 'warehouse': 'schema1', + 'cube': 'facts1', + 'representation': 'graphical', + 'measure': 'simple_count', + 'filters': { + 'innercategory': [], + }, } - }) + ) assert visu.json_data() == [{'coords': [], 'measures': [{'value': 17}]}] @@ -362,10 +385,7 @@ def test_json_dimensions(schema1, app, admin): form.set('measure', 'simple_count') form.set('drilldown_x', 'a') response = form.submit('visualize') - assert get_table(response) == [ - ['A', 'x', 'y', 'z'], - ['number of rows', '7', '9', '1'] - ] + assert get_table(response) == [['A', 'x', 'y', 'z'], ['number of rows', '7', '9', '1']] assert 'filter__a' in form.fields choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']] @@ -373,10 +393,7 @@ def test_json_dimensions(schema1, app, admin): form['filter__a'].force_value(['x', 'y']) response = form.submit('visualize') - assert get_table(response) == [ - ['A', 'x', 'y', 'z'], - ['number of rows', '7', '9', '0'] - ] + assert get_table(response) == [['A', 'x', 'y', 'z'], ['number of rows', '7', '9', '0']] def test_json_dimensions_having_percent(schema1, app, admin): @@ -392,7 +409,7 @@ def test_json_dimensions_having_percent(schema1, app, admin): response = form.submit('visualize') assert get_table(response) == [ ['A', 'x', 'y', 'z'], - ['pourcentage des demandes', '41,18 %', '52,94 %', '5,88 %'] + ['pourcentage des demandes', '41,18 %', '52,94 %', '5,88 %'], ] assert 'filter__a' in form.fields @@ -403,7 +420,7 @@ def test_json_dimensions_having_percent(schema1, app, admin): response = form.submit('visualize') assert get_table(response) == [ ['A', 'x', 'y', 'z'], - ['pourcentage des demandes', '43,75 %', '56,25 %', '0,00 %'] + ['pourcentage des demandes', '43,75 %', '56,25 %', '0,00 %'], ] diff --git a/tests/test_schema2.py b/tests/test_schema2.py index 57b32e9..1c8da14 100644 --- a/tests/test_schema2.py +++ b/tests/test_schema2.py @@ -4,20 +4,14 @@ import re import pytest from tabulate import tabulate - -from utils import login, get_table +from utils import get_table, login def pytest_generate_tests(metafunc): if hasattr(metafunc, 'function'): fcode = metafunc.function.__code__ - if 'visualization' in fcode.co_varnames[:fcode.co_argcount]: - with open( - os.path.join( - os.path.dirname(__file__), - 'fixtures', - 'schema2', - 'tables.json')) as fd: + if 'visualization' in fcode.co_varnames[: fcode.co_argcount]: + with open(os.path.join(os.path.dirname(__file__), 'fixtures', 'schema2', 'tables.json')) as fd: tables = json.load(fd) metafunc.parametrize(['visualization'], [[x] for x in tables]) @@ -53,6 +47,4 @@ def test_simple(request, schema2, app, admin, visualization): d[visualization] = table with open(new_table_path, 'w') as fd: json.dump(d, fd, indent=4, sort_keys=True, separators=(',', ': ')) - assert_equal_tables( - schema2['tables'][visualization], - table) + assert_equal_tables(schema2['tables'][visualization], table) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 31ddfde..d506c78 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -15,24 +15,22 @@ # along with this program. If not, see . -from __future__ import unicode_literals - import pytest - from django.utils.translation import ugettext as _ -from bijoe.schemas import ( - Dimension, -) +from bijoe.schemas import Dimension def test_absent_label(): assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'integer'}).absent_label == _('None') assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'string'}).absent_label == _('None') assert Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'bool'}).absent_label == _('N/A') - assert Dimension.from_json( - {'name': 'x', 'value': 'x', 'type': 'boolean', 'absent_label': 'coin'}).absent_label == 'coin' + assert ( + Dimension.from_json( + {'name': 'x', 'value': 'x', 'type': 'boolean', 'absent_label': 'coin'} + ).absent_label + == 'coin' + ) with pytest.raises(NotImplementedError): Dimension.from_json({'name': 'x', 'value': 'x', 'type': 'coin'}).absent_label - diff --git a/tests/test_signature.py b/tests/test_signature.py index 1d8a576..a0808e0 100644 --- a/tests/test_signature.py +++ b/tests/test_signature.py @@ -38,7 +38,7 @@ def test_signature(): assert not signature.check_string(STRING, signature.sign_string(STRING, KEY), OTHER_KEY) assert not signature.check_query(signature.sign_query(QUERY, KEY), OTHER_KEY) assert not signature.check_url(signature.sign_url(URL, KEY), OTHER_KEY) - #assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), KEY) + # assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), KEY) # Test URL is preserved assert URL in signature.sign_url(URL, KEY) @@ -50,28 +50,29 @@ def test_signature(): assert '&nonce=' in signature.sign_url(URL, KEY) # Test unicode key conversion to UTF-8 - assert signature.check_url(signature.sign_url(URL, u'\xe9\xe9'), b'\xc3\xa9\xc3\xa9') - assert signature.check_url(signature.sign_url(URL, b'\xc3\xa9\xc3\xa9'), u'\xe9\xe9') + assert signature.check_url(signature.sign_url(URL, '\xe9\xe9'), b'\xc3\xa9\xc3\xa9') + assert signature.check_url(signature.sign_url(URL, b'\xc3\xa9\xc3\xa9'), '\xe9\xe9') # Test timedelta parameter now = datetime.datetime.utcnow() - assert '×tamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in \ - signature.sign_url(URL, KEY, timestamp=now) + assert '×tamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in signature.sign_url( + URL, KEY, timestamp=now + ) # Test nonce parameter assert '&nonce=uuu&' in signature.sign_url(URL, KEY, nonce='uuu') # Test known_nonce - assert signature.check_url(signature.sign_url(URL, KEY), KEY, - known_nonce=lambda nonce: nonce == 'xxx') - assert signature.check_url(signature.sign_url(URL, KEY, nonce='xxx'), KEY, - known_nonce=lambda nonce: nonce == 'xxx') + assert signature.check_url(signature.sign_url(URL, KEY), KEY, known_nonce=lambda nonce: nonce == 'xxx') + assert signature.check_url( + signature.sign_url(URL, KEY, nonce='xxx'), KEY, known_nonce=lambda nonce: nonce == 'xxx' + ) # Test timedelta - now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=29)) + now = datetime.datetime.utcnow() - datetime.timedelta(seconds=29) assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) - now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=30)) + now = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) - now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=2)) + now = datetime.datetime.utcnow() - datetime.timedelta(seconds=2) assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY, timedelta=2) diff --git a/tests/test_views.py b/tests/test_views.py index 908ed85..8a62d4e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -17,19 +17,17 @@ import copy import hashlib import json +from unittest import mock -import mock import pytest -from webtest import Upload - from django.urls import reverse from django.utils.encoding import force_bytes +from utils import login +from webtest import Upload from bijoe.visualization.models import Visualization from bijoe.visualization.signature import sign_url -from utils import login - @pytest.fixture def visualization(): @@ -42,7 +40,9 @@ def visualization(): 'representation': 'table', 'loop': '', 'filters': {}, - 'drilldown_x': 'date__yearmonth'}) + 'drilldown_x': 'date__yearmonth', + }, + ) def test_simple_user_403(app, john_doe): @@ -78,11 +78,13 @@ def test_visualizations_json_api(schema1, app, admin, settings): 'default': { 'verif_orig': orig, 'secret': key, - }}} + } + } + } url = '%s?orig=%s' % (reverse('visualizations-json'), orig) url = sign_url(url, key) resp = app.get(url, status=200) - assert set([x['slug'] for x in resp.json]) == set(['test', 'test-2', 'test-3', 'test-4']) + assert {x['slug'] for x in resp.json} == {'test', 'test-2', 'test-3', 'test-4'} url = '%s?orig=%s' % (reverse('visualizations-json'), orig) url = sign_url(url, 'wrong-key') @@ -97,7 +99,7 @@ def test_visualizations_json_api(schema1, app, admin, settings): login(app, admin) resp = app.get(reverse('visualizations-json')) - assert set([x['slug'] for x in resp.json]) == set(['test', 'test-2', 'test-3', 'test-4']) + assert {x['slug'] for x in resp.json} == {'test', 'test-2', 'test-3', 'test-4'} def test_visualization_json_api(schema1, app, admin, visualization): @@ -105,7 +107,18 @@ def test_visualization_json_api(schema1, app, admin, visualization): resp = app.get(reverse('visualization-json', kwargs={'pk': visualization.id})) # values from test_schem1/test_yearmonth_drilldown assert resp.json == { - 'axis': {'x_labels': ['01/2017', '02/2017', '03/2017', '04/2017', '05/2017', '06/2017', '07/2017', '08/2017']}, + 'axis': { + 'x_labels': [ + '01/2017', + '02/2017', + '03/2017', + '04/2017', + '05/2017', + '06/2017', + '07/2017', + '08/2017', + ] + }, 'data': [10, 1, 1, 1, 1, 1, 1, 1], 'format': '1', 'unit': None, @@ -121,9 +134,28 @@ def test_visualization_json_api_duration(schema1, app, admin, visualization): resp = app.get(reverse('visualization-json', kwargs={'pk': visualization.id})) # values from test_schem1/test_yearmonth_drilldown assert resp.json == { - 'axis': {'x_labels': ['01/2017', '02/2017', '03/2017', '04/2017', '05/2017', '06/2017', '07/2017', '08/2017']}, - 'data': [536968800.0, 539258400.0, 541677600.0, 544352400.0, - 546944400.0, 549622800.0, 552214800.0, 554893200.0], + 'axis': { + 'x_labels': [ + '01/2017', + '02/2017', + '03/2017', + '04/2017', + '05/2017', + '06/2017', + '07/2017', + '08/2017', + ] + }, + 'data': [ + 536968800.0, + 539258400.0, + 541677600.0, + 544352400.0, + 546944400.0, + 549622800.0, + 552214800.0, + 554893200.0, + ], 'format': '1', 'unit': 'seconds', 'measure': 'duration', @@ -194,7 +226,9 @@ def test_import_visualization(schema1, app, admin, visualization, settings, free # existing visualization resp = app.get('/', status=200) resp = resp.click('Import') - resp.form['visualizations_json'] = Upload('export.json', visualization_export.encode('utf-8'), 'application/json') + resp.form['visualizations_json'] = Upload( + 'export.json', visualization_export.encode('utf-8'), 'application/json' + ) resp = resp.form.submit().follow() assert 'No visualization created. A visualization has been updated.' in resp.text assert Visualization.objects.count() == 1 @@ -203,7 +237,9 @@ def test_import_visualization(schema1, app, admin, visualization, settings, free Visualization.objects.all().delete() resp = app.get('/') resp = resp.click('Import') - resp.form['visualizations_json'] = Upload('export.json', visualization_export.encode('utf-8'), 'application/json') + resp.form['visualizations_json'] = Upload( + 'export.json', visualization_export.encode('utf-8'), 'application/json' + ) resp = resp.form.submit().follow() assert 'A visualization has been created. No visualization updated.' in resp.text assert Visualization.objects.count() == 1 @@ -220,8 +256,8 @@ def test_import_visualization(schema1, app, admin, visualization, settings, free resp = app.get('/', status=200) resp = resp.click('Import') resp.form['visualizations_json'] = Upload( - 'export.json', - json.dumps(visualizations).encode('utf-8'), 'application/json') + 'export.json', json.dumps(visualizations).encode('utf-8'), 'application/json' + ) resp = resp.form.submit().follow() assert '2 visualizations have been created. A visualization has been updated.' in resp.text assert Visualization.objects.count() == 3 @@ -235,7 +271,9 @@ def test_import_visualization(schema1, app, admin, visualization, settings, free resp = app.get('/') resp = resp.click('Import') - resp.form['visualizations_json'] = Upload('export.json', visualizations_export.encode('utf-8'), 'application/json') + resp.form['visualizations_json'] = Upload( + 'export.json', visualizations_export.encode('utf-8'), 'application/json' + ) resp = resp.form.submit().follow() assert '3 visualizations have been created. No visualization updated.' in resp.text assert Visualization.objects.count() == 3 diff --git a/tests/utils.py b/tests/utils.py index 77b57fe..c2217a0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,6 @@ import io -import zipfile import xml.etree.ElementTree as ET +import zipfile from django.conf import settings @@ -35,8 +35,8 @@ def get_table(response): def xml_node_text_content(node): - '''Extract text content from node and all its children. Equivalent to - xmlNodeGetContent from libxml.''' + """Extract text content from node and all its children. Equivalent to + xmlNodeGetContent from libxml.""" if node is None: return '' @@ -50,7 +50,8 @@ def xml_node_text_content(node): if child.tail: s.append(child.tail) return s - return u''.join(helper(node)) + + return ''.join(helper(node)) def get_ods_document(response):