0001-engine-make-Dimension.order_by-a-list-fixes-28175.patch
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 |
- |