Projet

Général

Profil

0001-engine-make-Dimension.order_by-a-list-fixes-28175.patch

Benjamin Dauvergne, 14 janvier 2019 11:01

Télécharger (18,8 ko)

Voir les différences:

Subject: [PATCH] engine: make Dimension.order_by a list (fixes #28175)

 bijoe/engine.py                       | 144 +++++++++++++++-----------
 bijoe/locale/fr/LC_MESSAGES/django.po |  40 ++++---
 bijoe/schemas.py                      |   8 +-
 bijoe/visualization/forms.py          |   6 ++
 tests/fixtures/schema1/01_schema.json |   8 ++
 tests/test_schema1.py                 |   4 +-
 tox.ini                               |   5 +-
 7 files changed, 127 insertions(+), 88 deletions(-)
bijoe/engine.py
60 60
    @property
61 61
    def members(self):
62 62
        assert self.type != 'date'
63
        value = self.value
64
        value_label = self.value_label or value
65
        order_by = self.order_by
66

  
63 67
        with self.engine.get_cursor() as cursor:
64 68
            sql = self.members_query
65 69
            if not sql:
66
                if self.dimension.join:
67
                    join = self.engine_cube.get_join(self.dimension.join[-1])
68
                    sql = ('SELECT %s AS value, %s::text AS label FROM %s AS "%s" '
69
                           'GROUP BY %s, %s ORDER BY %s' % (
70
                               self.value, self.value_label or self.value, join.table, join.name,
71
                               self.value, self.value_label or self.value, self.order_by or self.value))
70
                table_expression = self.engine_cube.fact_table
71
                if self.join:
72
                    table_expression = self.engine_cube.build_table_expression(
73
                        self.join, self.engine_cube.fact_table)
74
                sql = 'SELECT %s AS value, %s::text AS label ' % (value, value_label)
75
                sql += 'FROM %s ' % table_expression
76
                if order_by:
77
                    if not isinstance(order_by, list):
78
                        order_by = [order_by]
72 79
                else:
73
                    sql = ('SELECT %s AS value, %s::text AS label FROM {fact_table} '
74
                           'GROUP BY %s, %s ORDER BY %s' % (
75
                               self.value, self.value_label or self.value, self.value,
76
                               self.value_label or self.value, self.order_by or self.value))
80
                    order_by = [value]
81
                group_by = [value]
82
                if value_label not in group_by:
83
                    group_by.append(value_label)
84
                for order_value in order_by:
85
                    if order_value not in group_by:
86
                        group_by.append(order_value)
87
                sql += 'GROUP BY %s ' % ', '.join(group_by)
88
                sql += 'ORDER BY (%s) ' % ', '.join(order_by)
77 89
            sql = sql.format(fact_table=self.engine_cube.fact_table)
78 90
            self.engine.log.debug('SQL: %s', sql)
79 91
            cursor.execute(sql)
80 92
            for row in cursor.fetchall():
93
                if row[0] is None:
94
                    continue
81 95
                yield Member(*row)
82 96

  
83 97

  
......
196 210
        return ProxyList(obj.engine, obj, self.attribute, self.cls, chain=self.chain)
197 211

  
198 212

  
199
def build_table_expression(join_tree, table_name, alias=None, top=True, other_conditions=None):
200
    '''Recursively build the table expression from the join tree,
201
       starting from the fact table'''
202

  
203
    sql = table_name
204
    if alias:
205
        sql += ' AS "%s"' % alias
206
    add_paren = False
207
    for kind in ['left', 'inner', 'right', 'full']:
208
        joins = join_tree.get(table_name, {}).get(kind)
209
        if not joins:
210
            continue
211
        add_paren = True
212
        join_kinds = {
213
            'inner': 'INNER JOIN',
214
            'left': 'LEFT OUTER JOIN',
215
            'right': 'RIGHT OUTER JOIN',
216
            'full': 'FULL OUTER JOIN',
217
        }
218
        sql += ' %s ' % join_kinds[kind]
219
        sub_joins = []
220
        conditions = []
221
        if other_conditions:
222
            conditions = other_conditions
223
            other_conditions = None
224
        for join_name, join in joins.iteritems():
225
            sub_joins.append(
226
                build_table_expression(join_tree, join.table, join.name, top=False))
227
            conditions.append('"%s".%s = "%s"."%s"' % (
228
                alias or table_name,
229
                join.master.split('.')[-1],
230
                join.name, join.detail))
231
        sub_join = ' CROSS JOIN '.join(sub_joins)
232
        if len(sub_joins) > 1:
233
            sub_join = '(%s)' % sub_join
234
        sql += sub_join
235
        sql += ' ON %s' % ' AND '.join(conditions)
236
    if not top and add_paren:
237
        sql = '(%s)' % sql
238
    return sql
239

  
240

  
241 213
class EngineCube(object):
242 214
    dimensions = ProxyListDescriptor('all_dimensions', EngineDimension, chain=JSONDimensions)
243 215
    measures = ProxyListDescriptor('measures', EngineMeasure)
......
293 265
                projections.append('%s AS %s' % (dimension.value_label or dimension.value,
294 266
                                                 dimension.name))
295 267
                group_by.append(dimension.group_by or dimension.value)
296
                order_by.append(dimension.order_by or dimension.value)
268
                order_by.extend(dimension.order_by or [dimension.value])
269

  
270
            for order_value in order_by:
271
                if order_value not in group_by:
272
                    group_by.append(order_value)
297 273

  
298 274
            for measure_name in measures:
299 275
                measure = self.get_measure(measure_name)
......
302 278
            sql = 'SELECT ' + ', '.join(projections)
303 279
            table_expression = ' %s' % self.cube.fact_table
304 280
            if joins:
305
                join_tree = {}
306
                # Build join tree
307
                for join_name in joins:
308
                    join = self.get_join(join_name)
309
                    master_table = join.master_table or self.fact_table
310
                    join_tree.setdefault(master_table, {}).setdefault(join.kind, {})[join.name] = join
311
                table_expression = build_table_expression(join_tree,
312
                                                          self.fact_table,
313
                                                          other_conditions=join_conditions)
281
                table_expression = self.build_table_expression(
282
                    joins, self.fact_table, other_conditions=join_conditions)
314 283
            sql += ' FROM %s' % table_expression
315 284
            where_conditions = 'true'
316 285
            if where:
......
349 318
                    'value': value,
350 319
                } for cell, value in zip(cells, row)]
351 320

  
321
    def build_table_expression(self, joins, table_name, other_conditions=None):
322
        '''Recursively build the table expression from the join tree,
323
           starting from the fact table'''
324

  
325
        join_tree = {}
326
        # Build join tree
327
        for join_name in joins:
328
            join = self.get_join(join_name)
329
            master_table = join.master_table or self.fact_table
330
            join_tree.setdefault(master_table, {}).setdefault(join.kind, {})[join.name] = join
331

  
332
        def build_table_expression_helper(join_tree, table_name, alias=None, top=True, other_conditions=None):
333
            sql = table_name
334
            if alias:
335
                sql += ' AS "%s"' % alias
336
            add_paren = False
337
            for kind in ['left', 'inner', 'right', 'full']:
338
                joins = join_tree.get(alias or table_name, {}).get(kind)
339
                if not joins:
340
                    continue
341
                add_paren = True
342
                join_kinds = {
343
                    'inner': 'INNER JOIN',
344
                    'left': 'LEFT OUTER JOIN',
345
                    'right': 'RIGHT OUTER JOIN',
346
                    'full': 'FULL OUTER JOIN',
347
                }
348
                sql += ' %s ' % join_kinds[kind]
349
                sub_joins = []
350
                conditions = []
351
                if other_conditions:
352
                    conditions = other_conditions
353
                    other_conditions = None
354
                for join_name, join in joins.iteritems():
355
                    sub_joins.append(
356
                        build_table_expression_helper(join_tree, join.table, alias=join.name, top=False))
357
                    conditions.append('"%s".%s = "%s"."%s"' % (
358
                        alias or table_name,
359
                        join.master.split('.')[-1],
360
                        join.name, join.detail))
361
                sub_join = ' CROSS JOIN '.join(sub_joins)
362
                if len(sub_joins) > 1:
363
                    sub_join = '(%s)' % sub_join
364
                sql += sub_join
365
                sql += ' ON %s' % ' AND '.join(conditions)
366
            if not top and add_paren:
367
                sql = '(%s)' % sql
368
            return sql
369
        return build_table_expression_helper(
370
            join_tree, table_name, other_conditions=other_conditions)
371

  
352 372

  
353 373
class Engine(object):
354 374
    def __init__(self, warehouse):
bijoe/locale/fr/LC_MESSAGES/django.po
7 7
msgstr ""
8 8
"Project-Id-Version: bijoe 0.x\n"
9 9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2018-01-03 11:04+0000\n"
10
"POT-Creation-Date: 2019-01-14 10:01+0000\n"
11 11
"PO-Revision-Date: 2018-07-17 21:18+0200\n"
12 12
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
13 13
"Language-Team: fr <fr@li.org>\n"
......
17 17
"Content-Transfer-Encoding: 8bit\n"
18 18
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
19 19

  
20
#: templates/bijoe/base.html:11 templates/bijoe/base.html.py:15
20
#: templates/bijoe/base.html:11 templates/bijoe/base.html:15
21 21
#: templates/bijoe/warehouse.html:5 views.py:79
22 22
msgid "Statistics"
23 23
msgstr "Statistiques"
......
26 26
msgid "Save visualization"
27 27
msgstr "Enregistrer une visualisation"
28 28

  
29
#: templates/bijoe/create_visualization.html:19 templates/bijoe/cube.html:65
29
#: templates/bijoe/create_visualization.html:19 templates/bijoe/cube.html:67
30 30
#: templates/bijoe/visualization.html:23
31 31
msgid "Save"
32 32
msgstr "Enregistrer"
......
41 41
msgid "Homepage"
42 42
msgstr "Accueil"
43 43

  
44
#: templates/bijoe/cube.html:48
44
#: templates/bijoe/cube.html:50
45 45
msgid "Visualiser"
46 46
msgstr "Visualiser"
47 47

  
48
#: templates/bijoe/cube.html:63 templates/bijoe/visualization.html:18
48
#: templates/bijoe/cube.html:65 templates/bijoe/visualization.html:18
49 49
msgid "URL for IFRAME"
50 50
msgstr "URL pour IFRAME"
51 51

  
52
#: templates/bijoe/cube.html:68
52
#: templates/bijoe/cube.html:70
53 53
msgid "Please choose some measures and groupings."
54 54
msgstr "Veuillez choisir des mesures et des regroupements."
55 55

  
......
120 120
msgid "last quarter"
121 121
msgstr "le trimestre précédent"
122 122

  
123
#: visualization/forms.py:93
123
#: visualization/forms.py:82
124
msgid "since 1st january last year"
125
msgstr "depuis le 1er janvier de l'année précédente"
126

  
127
#: visualization/forms.py:99
124 128
msgid "start"
125 129
msgstr "début"
126 130

  
127
#: visualization/forms.py:95
131
#: visualization/forms.py:101
128 132
msgid "end"
129 133
msgstr "fin"
130 134

  
131
#: visualization/forms.py:150
135
#: visualization/forms.py:156
132 136
msgid "Presentation"
133 137
msgstr "Représentation"
134 138

  
135
#: visualization/forms.py:151
139
#: visualization/forms.py:157
136 140
msgid "table"
137 141
msgstr "tableau"
138 142

  
139
#: visualization/forms.py:152
143
#: visualization/forms.py:158
140 144
msgid "chart"
141 145
msgstr "graphique"
142 146

  
143
#: visualization/forms.py:164
147
#: visualization/forms.py:170
144 148
msgid "Loop by"
145 149
msgstr "Regroupement(s)"
146 150

  
147
#: visualization/forms.py:185
151
#: visualization/forms.py:191
148 152
msgid "Group by horizontaly"
149 153
msgstr "Regroupement horizontal"
150 154

  
151
#: visualization/forms.py:190
155
#: visualization/forms.py:196
152 156
msgid "Group by vertically"
153 157
msgstr "Regroupement vertical"
154 158

  
155
#: visualization/forms.py:198
159
#: visualization/forms.py:204
156 160
msgid "Measure"
157 161
msgstr "Mesure"
158 162

  
159
#: visualization/forms.py:210
163
#: visualization/forms.py:216
160 164
msgid "You cannot use the same dimension for looping and grouping"
161
msgstr "Vous ne pouvez pas utiliser la même dimension pour la répétition et le regroupement."
165
msgstr ""
166
"Vous ne pouvez pas utiliser la même dimension pour la répétition et le "
167
"regroupement."
162 168

  
163 169
#: visualization/models.py:36
164 170
msgid "name"
bijoe/schemas.py
143 143
        'join': [str],
144 144
        'value': str,
145 145
        'value_label': str,
146
        'order_by': str,
146
        'order_by': [str],
147 147
        'group_by': str,
148 148
        'filter': bool,
149 149
        'members_query': str,
......
202 202
                    filter_value='EXTRACT(dow from %s)' % filter_value,
203 203
                    filter_in_join=self.filter_in_join,
204 204
                    value='EXTRACT(dow from %s)' % self.value,
205
                    order_by='(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value,
205
                    order_by=['(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value],
206 206
                    value_label='to_char(date_trunc(\'week\', current_date)::date '
207 207
                                '+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' % self.value,
208 208
                    filter=False),
......
218 218
                          % (self.value, self.value),
219 219
                    group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
220 220
                                                                                  self.value),
221
                    order_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
222
                                                                                  self.value),
221
                    order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
222
                                                                                   self.value)],
223 223
                    filter=False)
224 224
            ]
225 225
        return [self]
bijoe/visualization/forms.py
77 77
        'start': u'le dernier trimestre',
78 78
        'end': u'ce trimestre',
79 79
    },
80
    {
81
        'value': 'since_1jan_last_year',
82
        'label': _('since 1st january last year'),
83
        'start': u'l\'année dernière',
84
        'end': u'maintenant',
85
    },
80 86
]
81 87

  
82 88

  
tests/fixtures/schema1/01_schema.json
86 86
                    "type": "integer",
87 87
                    "join": ["innercategory", "innersubcategory"],
88 88
                    "value": "innersubcategory.id",
89
                    "order_by": ["innercategory.ord", "innersubcategory.ord", "innersubcategory.label"],
89 90
                    "value_label": "innersubcategory.label"
90 91
                },
91 92
                {
......
94 95
                    "type": "integer",
95 96
                    "join": ["leftcategory", "leftsubcategory"],
96 97
                    "value": "leftsubcategory.id",
98
                    "order_by": ["leftcategory.ord", "leftsubcategory.ord", "leftsubcategory.label"],
97 99
                    "value_label": "leftsubcategory.label"
98 100
                },
99 101
                {
......
102 104
                    "type": "integer",
103 105
                    "join": ["rightcategory", "rightsubcategory"],
104 106
                    "value": "rightsubcategory.id",
107
                    "order_by": ["rightcategory.ord", "rightsubcategory.ord", "rightsubcategory.label"],
105 108
                    "value_label": "rightsubcategory.label"
106 109
                },
107 110
                {
......
110 113
                    "type": "integer",
111 114
                    "join": ["outercategory", "outersubcategory"],
112 115
                    "value": "outersubcategory.id",
116
                    "order_by": ["outercategory.ord", "outersubcategory.ord", "outersubcategory.label"],
113 117
                    "value_label": "outersubcategory.label"
114 118
                },
115 119
                {
......
118 122
                    "type": "integer",
119 123
                    "join": ["innersubcategory", "innercategory"],
120 124
                    "value": "innercategory.id",
125
                    "order_by": "innercategory.ord",
121 126
                    "value_label": "innercategory.label"
122 127
                },
123 128
                {
......
126 131
                    "type": "integer",
127 132
                    "join": ["leftsubcategory", "leftcategory"],
128 133
                    "value": "leftcategory.id",
134
                    "order_by": "leftcategory.ord",
129 135
                    "value_label": "leftcategory.label"
130 136
                },
131 137
                {
......
134 140
                    "type": "integer",
135 141
                    "join": ["rightsubcategory", "rightcategory"],
136 142
                    "value": "rightcategory.id",
143
                    "order_by": "rightcategory.ord",
137 144
                    "value_label": "rightcategory.label"
138 145
                },
139 146
                {
......
142 149
                    "type": "integer",
143 150
                    "join": ["outersubcategory", "outercategory"],
144 151
                    "value": "outercategory.id",
152
                    "order_by": "outercategory.ord",
145 153
                    "value_label": "outercategory.label"
146 154
                }
147 155
            ],
tests/test_schema1.py
15 15
    response = form.submit('visualize')
16 16
    assert 'big-msg-info' not in response
17 17
    assert get_table(response) == [
18
        [u'Inner SubCategory', u'subé1', u'subé3'],
19
        ['number of rows', '15', '1'],
18
        [u'Inner SubCategory', u'subé3', u'subé1'],
19
        ['number of rows', '1', '15'],
20 20
    ]
21 21
    form = response.form
22 22
    form.set('representation', 'table')
tox.ini
22 22
	coverage
23 23
	pytest
24 24
	pytest-cov
25
	pytest-random
26 25
	pytest-django
27 26
	pytest-freezegun
28 27
	WebTest
29 28
	django-webtest<1.9.3
30 29
	pyquery
31 30
commands =
32
        dj111: py.test {posargs: --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=bijoe --random tests/}
33
        dj18: py.test {posargs: --junitxml=test_{envname}_results.xml --random tests/}
31
        dj111: py.test {posargs: --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=bijoe tests/}
32
        dj18: py.test {posargs: --junitxml=test_{envname}_results.xml tests/}
34 33
[pytest]
35 34
filterwarnings =
36 35
 once:.*
37
-