forked from mozilla/kitsune
/
readouts.py
448 lines (381 loc) · 19.4 KB
/
readouts.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
"""Data aggregators for dashboards"""
from django.conf import settings
from django.db import connections, router
from django.utils.datastructures import SortedDict
import jingo
from tower import ugettext as _, ugettext_lazy as _lazy
from dashboards.models import THIS_WEEK, ALL_TIME, PERIODS
from sumo.urlresolvers import reverse
from wiki.models import Document, MEDIUM_SIGNIFICANCE, MAJOR_SIGNIFICANCE
MOST_VIEWED = 1
MOST_RECENT = 2
def _cursor():
"""Return a DB cursor for reading."""
return connections[router.db_for_read(Document)].cursor()
def overview_rows(locale):
"""Return the iterable of dicts needed to draw the Overview table."""
def percent_or_100(num, denom):
return int(round(num / float(denom) * 100)) if denom else 100
# The Overview table is a special case: it has only a static number of
# rows, so it has no expanded, all-rows view, and thus needs no slug, no
# "max" kwarg on rows(), etc. It doesn't fit the Readout signature, so we
# don't shoehorn it in.
total = Document.uncached.exclude(current_revision=None).filter(
locale=settings.WIKI_DEFAULT_LANGUAGE,
is_localizable=True).count()
# How many approved documents are there in German that have parents?
translated = Document.uncached.filter(locale=locale).exclude(
current_revision=None).exclude(parent=None).count()
# Of the top 20 most visited English articles, how many are not translated
# into German?
TOP_N = 20
cursor = _cursor()
cursor.execute(
'SELECT count(*) FROM '
'(SELECT trans.id FROM dashboards_wikidocumentvisits '
'LEFT JOIN wiki_document trans '
'ON dashboards_wikidocumentvisits.document_id=trans.parent_id '
'AND trans.locale=%s '
'WHERE dashboards_wikidocumentvisits.period=%s '
'ORDER BY dashboards_wikidocumentvisits.visits DESC '
'LIMIT %s) t1 '
'WHERE t1.id IS NOT NULL',
(locale, THIS_WEEK, TOP_N))
popular_translated = cursor.fetchone()[0]
return [dict(title=_('Most-Visited Articles'),
url='#' + MostVisitedTranslationsReadout.slug,
numerator=popular_translated, denominator=TOP_N,
percent=percent_or_100(popular_translated, TOP_N),
description=_('These are the top %s most visited English '
'articles, which in sum account for over 50%% '
'of the total traffic to the English '
'Knowledge Base.') % TOP_N),
dict(title=_('All Knowledge Base Articles'),
numerator=translated, denominator=total,
percent=percent_or_100(translated, total),
description=_('How many of the approved English articles '
'which allow translations have an approved '
'translation into this language'))]
class Readout(object):
"""Abstract class representing one table on the Localization Dashboard
Describing these as atoms gives us the whole-page details views for free.
"""
# title = _lazy(u'Title of Readout')
# short_title= = _lazy(u'Short Title of Readout for In-Page Links')
# slug = 'URL slug for detail page'
# details_link_text = _lazy(u'All articles from this readout...')
column3_label = _lazy(u'Visits this week')
column4_label = _lazy(u'Status')
modes = [(MOST_VIEWED, _lazy('Most Viewed')),
(MOST_RECENT, _lazy('Most Recent'))]
def __init__(self, request, locale=None, mode=None):
"""Take request so the template can use contextual macros that need it.
Renders the data for the locale specified by the request, but you can
override it by passing another in `locale`.
"""
self.request = request
self.locale = locale or request.locale
self.mode = mode or self.modes[0][1]
# self.mode is allowed to be invalid.
def rows(self, max=None):
"""Return an iterable of dicts containing the data for the table.
This default implementation calls _query_and_params and _format_row.
You can either implement those or, if you need more flexibility,
override this.
Limit to `max` rows.
"""
cursor = _cursor()
cursor.execute(*self._query_and_params(max))
return [self._format_row(r) for r in cursor.fetchall()]
def render(self, max_rows=None):
"""Return HTML table rows, optionally limiting to a number of rows."""
# Compute percents for bar widths:
rows = self.rows(max_rows)
max_visits = max(r['visits'] for r in rows) if rows else 0
for r in rows:
visits = r['visits']
r['percent'] = (0 if visits is None or not max_visits
else int(round(visits / float(max_visits) * 100)))
# Render:
return jingo.render_to_string(
self.request,
'dashboards/includes/kb_readout.html',
{'rows': rows, 'column3_label': self.column3_label,
'column4_label': self.column4_label})
# To override:
def _query_and_params(self, max):
"""Return a tuple: (query, params to bind it to)."""
raise NotImplementedError
def _format_row(self, row):
"""Turn a DB row tuple into a dict for the template."""
raise NotImplementedError
# Convenience methods:
@staticmethod
def _limit_clause(max):
"""Return a SQL LIMIT clause limiting returned rows to `max`.
Return '' if max is None.
"""
return ' LIMIT %i' % max if max else ''
class MostVisitedDefaultLanguageReadout(Readout):
title = _lazy(u'Most-Visited Articles')
# No short_title; the link to this one is hard-coded in Overview readout
details_link_text = _lazy(u'All knowledge base articles...')
slug = 'most-visited'
column3_label = _lazy(u'Visits')
modes = PERIODS
review_statuses = {
1: (_lazy(u'Review Needed'), 'wiki.document_revisions'),
0: (u'', '')}
def _query_and_params(self, max):
# Review Needed: link to /history.
return ('SELECT engdoc.slug, engdoc.title, '
'dashboards_wikidocumentvisits.visits, '
'count(engrev.document_id) '
'FROM wiki_document engdoc '
'LEFT JOIN dashboards_wikidocumentvisits ON '
'engdoc.id=dashboards_wikidocumentvisits.document_id '
'AND dashboards_wikidocumentvisits.period=%s '
'LEFT JOIN wiki_revision engrev ON '
'engrev.document_id=engdoc.id '
'AND engrev.reviewed IS NULL '
'AND engrev.id>engdoc.current_revision_id '
'WHERE engdoc.locale=%s '
'GROUP BY engdoc.id '
'ORDER BY dashboards_wikidocumentvisits.visits DESC, '
'engdoc.title ASC' + self._limit_clause(max),
(ALL_TIME if self.mode == ALL_TIME else THIS_WEEK, self.locale))
def _format_row(self, (slug, title, visits, num_unreviewed)):
needs_review = int(num_unreviewed > 0)
status, view_name = self.review_statuses[needs_review]
return (dict(title=title,
url=reverse('wiki.document', args=[slug],
locale=self.locale),
visits=visits,
status=status,
status_url=reverse(view_name, args=[slug],
locale=self.locale)
if view_name else ''))
class MostVisitedTranslationsReadout(MostVisitedDefaultLanguageReadout):
slug = 'most-visited-translations'
# L10n: Replace "English" with your language.
details_link_text = _lazy(u'All translations in English...')
significance_statuses = {
MEDIUM_SIGNIFICANCE: (_lazy(u'Update Needed'), 'wiki.edit_document'),
MAJOR_SIGNIFICANCE: (_lazy(u'Out of Date'), 'wiki.edit_document')}
def _query_and_params(self, max):
# Out of Date or Update Needed: link to /edit.
# Review Needed: link to /history.
# These match the behavior of the corresponding readouts.
return ('SELECT transdoc.slug, transdoc.title, '
'dashboards_wikidocumentvisits.visits, '
# The most significant approved change to the English article
# since the English revision the current translated revision is
# based on:
'(SELECT MAX(engrev.significance) '
'FROM wiki_revision engrev, wiki_revision transrev '
'WHERE engrev.is_approved '
'AND transrev.id=transdoc.current_revision_id '
'AND engrev.document_id=transdoc.parent_id '
'AND engrev.id>transrev.based_on_id '
'), '
# Whether there are any unreviewed revs of the translation made
# since the current one:
'(SELECT EXISTS '
'(SELECT * '
'FROM wiki_revision transrev '
'WHERE transrev.document_id=transdoc.id '
'AND transrev.reviewed IS NULL '
'AND transrev.id>transdoc.current_revision_id '
')'
') '
'FROM wiki_document transdoc '
'LEFT JOIN dashboards_wikidocumentvisits ON transdoc.parent_id='
'dashboards_wikidocumentvisits.document_id '
'AND dashboards_wikidocumentvisits.period=%s '
'WHERE transdoc.locale=%s '
'ORDER BY dashboards_wikidocumentvisits.visits DESC, '
'transdoc.title ASC' + self._limit_clause(max),
(ALL_TIME if self.mode == ALL_TIME else THIS_WEEK, self.locale))
def _format_row(self, (slug, title, visits, significance, needs_review)):
status, view_name = self.significance_statuses.get(
significance,
self.review_statuses[needs_review])
return (dict(title=title,
url=reverse('wiki.document', args=[slug],
locale=self.locale),
visits=visits,
status=status,
status_url=reverse(view_name, args=[slug],
locale=self.locale)
if view_name else ''))
class UntranslatedReadout(Readout):
title = _lazy(u'Untranslated Articles')
short_title = _lazy(u'Untranslated')
details_link_text = _lazy(u'All untranslated articles...')
slug = 'untranslated'
column4_label = _lazy(u'Updated')
def _query_and_params(self, max):
# Incidentally, we tried this both as a left join and as a search
# against an inner query returning translated docs, and the left join
# yielded a faster-looking plan (on a production corpus).
return ('SELECT parent.slug, parent.title, '
'wiki_revision.reviewed, dashboards_wikidocumentvisits.visits '
'FROM wiki_document parent '
'INNER JOIN wiki_revision ON '
'parent.current_revision_id=wiki_revision.id '
'LEFT JOIN wiki_document translated ON '
'parent.id=translated.parent_id AND translated.locale=%s '
'LEFT JOIN dashboards_wikidocumentvisits ON '
'parent.id=dashboards_wikidocumentvisits.document_id AND '
'dashboards_wikidocumentvisits.period=%s '
'WHERE '
'translated.id IS NULL AND parent.is_localizable AND '
'parent.locale=%s '
+ self._order_clause() + self._limit_clause(max),
(self.locale, THIS_WEEK, settings.WIKI_DEFAULT_LANGUAGE))
def _order_clause(self):
return ('ORDER BY wiki_revision.reviewed DESC, parent.title ASC'
if self.mode == MOST_RECENT
else 'ORDER BY dashboards_wikidocumentvisits.visits DESC, '
'parent.title ASC')
def _format_row(self, (slug, title, reviewed, visits)):
# Run the data through the model to (potentially) format it and
# take advantage of SPOTs (like for get_absolute_url()):
d = Document(slug=slug, title=title,
locale=settings.WIKI_DEFAULT_LANGUAGE)
return (dict(title=d.title,
url=d.get_absolute_url(),
visits=visits,
updated=reviewed))
class OutOfDateReadout(Readout):
title = _lazy(u'Out-of-Date Translations')
short_title = _lazy(u'Out-of-Date')
details_link_text = _lazy(u'All out-of-date translations...')
slug = 'out-of-date'
column4_label = _lazy(u'Out of date since')
# To show up in this readout, an article's revision since the last
# approved translation must have a maximum significance equal to this
# value:
_max_significance = MAJOR_SIGNIFICANCE
def _query_and_params(self, max):
# At the moment, the "Out of Date Since" column shows the time since
# the translation was out of date at a MEDIUM level of severity or
# higher. We could arguably knock this up to MAJOR, but technically it
# is out of date when the original gets anything more than typo
# corrections.
return ('SELECT transdoc.slug, transdoc.title, engrev.reviewed, '
'dashboards_wikidocumentvisits.visits '
'FROM wiki_document transdoc '
'INNER JOIN wiki_revision engrev ON engrev.id='
# The oldest english rev to have an approved level-30 change since
# the translated doc had an approved rev based on it. NULL if there
# is none:
'(SELECT min(id) FROM wiki_revision '
# Narrow engrev rows to those representing revision of parent
# doc:
'WHERE wiki_revision.document_id=transdoc.parent_id '
# For the purposes of computing the "Out of Date Since" column,
# the revision that threw the translation out of date had
# better be more recent than the one the current translation is
# based on:
'AND wiki_revision.id>'
'(SELECT based_on_id FROM wiki_revision basedonrev '
'WHERE basedonrev.id=transdoc.current_revision_id) '
'AND wiki_revision.significance>=%s '
'AND %s='
# Completely filter out outer selections where 30 is not the
# max signif of english revisions since trans was last
# approved. Other maxes will be shown by other readouts.
# Optimize: try "30 IN" if MySQL's statistics gatherer is
# stupid/nonexistent; the inner query should be able to bail
# out early. [Ed: No effect on EXPLAIN on admittedly light test
# corpus.]
'(SELECT MAX(engsince.significance) '
'FROM wiki_revision engsince '
'WHERE engsince.document_id=transdoc.parent_id '
# Assumes that any approved revision became the current
# revision at some point: we don't let the user go back and
# approve revisions older than the latest approved one.
'AND engsince.is_approved '
'AND engsince.id>'
# The English revision the current translation's based on:
'(SELECT based_on_id FROM wiki_revision basedonrev '
'WHERE basedonrev.id=transdoc.current_revision_id) '
')'
') '
# Join up the visits table for stats:
'LEFT JOIN dashboards_wikidocumentvisits ON '
'engrev.document_id=dashboards_wikidocumentvisits.document_id '
'AND dashboards_wikidocumentvisits.period=%s '
'WHERE transdoc.locale=%s '
+ self._order_clause() + self._limit_clause(max),
(MEDIUM_SIGNIFICANCE, self._max_significance, THIS_WEEK,
self.locale))
def _order_clause(self):
return ('ORDER BY engrev.reviewed DESC'
if self.mode == MOST_RECENT
else 'ORDER BY dashboards_wikidocumentvisits.visits DESC, '
'transdoc.title ASC')
def _format_row(self, (slug, title, reviewed, visits)):
return (dict(title=title,
url=reverse('wiki.edit_document', args=[slug]),
visits=visits, updated=reviewed))
class NeedingUpdatesReadout(OutOfDateReadout):
title = _lazy(u'Translations Needing Updates')
short_title = _lazy(u'Needing Updates')
details_link_text = _lazy(u'All translations needing updates...')
slug = 'needing-updates'
_max_significance = MEDIUM_SIGNIFICANCE
class UnreviewedReadout(Readout):
title = _lazy(u'Unreviewed Changes')
# ^ Not just changes to translations but also unreviewed chanages to docs
# in this locale that are not translations
short_title = _lazy(u'Unreviewed', 'document')
details_link_text = _lazy(u'All articles requiring review...')
slug = 'unreviewed'
column4_label = _lazy(u'Changed')
def _query_and_params(self, max):
english_id = ('id' if self.locale == settings.WIKI_DEFAULT_LANGUAGE
else 'parent_id')
return ('SELECT wiki_document.slug, wiki_document.title, '
'MAX(wiki_revision.created) maxcreated, '
'GROUP_CONCAT(DISTINCT auth_user.username '
"ORDER BY wiki_revision.id SEPARATOR ', '), "
'dashboards_wikidocumentvisits.visits '
'FROM wiki_document '
'INNER JOIN wiki_revision ON '
'wiki_document.id=wiki_revision.document_id '
'INNER JOIN auth_user ON wiki_revision.creator_id=auth_user.id '
'LEFT JOIN dashboards_wikidocumentvisits ON '
'wiki_document.' + english_id
+ '=dashboards_wikidocumentvisits.document_id AND '
'dashboards_wikidocumentvisits.period=%s '
'WHERE wiki_revision.reviewed IS NULL '
'AND (wiki_document.current_revision_id IS NULL OR '
'wiki_revision.id>wiki_document.current_revision_id) '
'AND wiki_document.locale=%s '
'GROUP BY wiki_document.id '
+ self._order_clause() + self._limit_clause(max),
(THIS_WEEK, self.locale))
def _order_clause(self):
return ('ORDER BY maxcreated DESC'
if self.mode == MOST_RECENT
else 'ORDER BY dashboards_wikidocumentvisits.visits DESC, '
'wiki_document.title ASC')
def _format_row(self, (slug, title, changed, users, visits)):
return (dict(title=title,
url=reverse('wiki.document_revisions', args=[slug],
locale=self.locale),
visits=visits,
updated=changed,
users=users))
# L10n Dashboard tables that have their own whole-page views:
L10N_READOUTS = SortedDict((t.slug, t) for t in
[MostVisitedTranslationsReadout, UntranslatedReadout, OutOfDateReadout,
NeedingUpdatesReadout, UnreviewedReadout])
# Contributors ones:
CONTRIBUTOR_READOUTS = SortedDict((t.slug, t) for t in
[MostVisitedDefaultLanguageReadout, UnreviewedReadout])
# All:
READOUTS = L10N_READOUTS.copy()
READOUTS.update(CONTRIBUTOR_READOUTS)