Merge lp:~stevenk/launchpad/remove-suggested-translations into lp:launchpad

Proposed by Steve Kowalik on 2012-10-29
Status: Merged
Approved by: Curtis Hovey on 2012-10-29
Approved revision: no longer in the source branch.
Merged at revision: 16212
Proposed branch: lp:~stevenk/launchpad/remove-suggested-translations
Merge into: lp:launchpad
Diff against target: 273 lines (+8/-156)
5 files modified
lib/lp/translations/browser/person.py (+1/-25)
lib/lp/translations/browser/tests/test_persontranslationview.py (+6/-26)
lib/lp/translations/interfaces/translationsperson.py (+0/-12)
lib/lp/translations/model/translationsperson.py (+0/-16)
lib/lp/translations/tests/test_translations_to_review.py (+1/-77)
To merge this branch: bzr merge lp:~stevenk/launchpad/remove-suggested-translations
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code 2012-10-29 Approve on 2012-10-29
Review via email: mp+131820@code.launchpad.net

Commit Message

Remove ITranslationsPerson.suggestReviewableTranslationFiles() and the parts of Person:+translations that call it.

Description of the Change

Remove ITranslationsPerson.suggestReviewableTranslationFiles() and the parts of Person:+translations that call it.

The slightly longer story is that in r16208, the query that is at the heart of ITranslationsPerson.getReviewableTranslationFiles() was rewritten to make use of 4 CTEs rather than 10 joins. This dropped it from taking over 1.5 seconds to about 11ms. The new query was written with one assumption at its core -- we have *lots* of pofiles, but translators themselves don't tend to touch many of them, so if we limit there first, we get a small result, and therefore a fast query.

The query in suggestReviewableTranslationFiles() on the other hand, can not abide that assumption at all. The very first thing is does is a anti-join because it wants a list of pofiles that the translator has never touched. This query is incredibly expensive, and I personally don't think it's worth the value of showing maybe 1 suggestion on Person:+translations if the query is going to take over 2 seconds to run -- or worse, cause a timeout.

To post a comment you must log in.
Jeroen T. Vermeulen (jtv) wrote :

Discussed this on IRC and IRL with Steve and Huw.

The functionality is there to get some random scatter on where translators go. We still want that. Some translations are naturally easier to get to than others, which leads to duplication of effort. For example, if many translators end up on the same translation page on the same day, they could produce a hundred suggestions for the same string — and review may become the bottleneck. There's much more use to spreading the effort out randomly, even if it guides people to lower-priority work. This is also why we can't do a lot of caching here.

From a UI perspective, filling out the list of translations-you-should-work-on with random suggestions is confusing. It's very hard to avoid making nonsensical suggestions, but also necessary because the suggestions are unsolicited. And coincidentally, that difficulty also drives up the cost of the query.

So what we suggest is to stop filling out the list with random suggestions, but add a “take me to a random project where I can help” link. It doesn't need to contain a precomputed link (it could link to a view that produces a temporary redirect) and the “slot-machine” presentation makes it a lot more acceptable to suggest projects that the user has already worked on, for example.

This may simplify the search. For example, the random suggestion could now:

 - be a bit more limited in which permissions models a project must have, to simplify the privileges check;

 - ignore the risk of suggesting a project the user is currently active in;

 - skip the complex multi-level aggregation logic for grouping POFiles.

Each as appropriate, of course. One caveat is that the list of suggested work items may now be empty, but the link would still have to be there.

Jeroen

Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/browser/person.py'
2--- lib/lp/translations/browser/person.py 2012-10-10 18:10:52 +0000
3+++ lib/lp/translations/browser/person.py 2012-10-29 06:41:22 +0000
4@@ -313,21 +313,6 @@
5
6 return ReviewLinksAggregator().aggregate(pofiles)
7
8- def _suggestTargetsForReview(self, max_fetch):
9- """Find random translation targets for review.
10-
11- :param max_fetch: Maximum number of `POFile`s to fetch while
12- looking for these.
13- :return: a list of at most `max_fetch` translation targets.
14- Multiple `POFile`s may be aggregated together into a single
15- target.
16- """
17- person = ITranslationsPerson(self.context)
18- pofiles = person.suggestReviewableTranslationFiles(
19- no_older_than=self.history_horizon)[:max_fetch]
20-
21- return ReviewLinksAggregator().aggregate(pofiles)
22-
23 def _getTargetsForTranslation(self, max_fetch=None):
24 """Get translation targets for this person to translate.
25
26@@ -405,18 +390,9 @@
27 # Start out with the translations that the person has recently
28 # worked on.
29 recent = self._review_targets
30- overall = self._addToTargetsList(
31+ return self._addToTargetsList(
32 [], recent, max_known_targets, list_length)
33
34- # Fill out the list with other, randomly suggested translations
35- # that the person could also be reviewing.
36- fetch = 5 * (list_length - len(overall))
37- suggestions = self._suggestTargetsForReview(fetch)
38- overall = self._addToTargetsList(
39- overall, suggestions, list_length, list_length)
40-
41- return overall
42-
43 @cachedproperty
44 def num_projects_and_packages_to_review(self):
45 """How many translations do we suggest for reviewing?"""
46
47=== modified file 'lib/lp/translations/browser/tests/test_persontranslationview.py'
48--- lib/lp/translations/browser/tests/test_persontranslationview.py 2012-10-10 19:10:07 +0000
49+++ lib/lp/translations/browser/tests/test_persontranslationview.py 2012-10-29 06:41:22 +0000
50@@ -167,20 +167,13 @@
51
52 def test_top_projects_and_packages_to_review(self):
53 # top_projects_and_packages_to_review tries to name at least one
54- # translation target that the person has worked on, and at least
55- # one random suggestion that the person hasn't worked on.
56+ # translation target that the person has worked on.
57 self._makeReviewer()
58 pofile_worked_on = self._makePOFiles(1, previously_worked_on=True)[0]
59- pofile_not_worked_on = self._makePOFiles(
60- 1, previously_worked_on=False)[0]
61-
62 targets = self.view.top_projects_and_packages_to_review
63
64 pofile_suffix = '/+translate?show=new_suggestions'
65- expected_links = [
66- canonical_url(pofile_worked_on) + pofile_suffix,
67- canonical_url(pofile_not_worked_on) + pofile_suffix,
68- ]
69+ expected_links = [canonical_url(pofile_worked_on) + pofile_suffix]
70 self.assertEqual(
71 set(expected_links), set(item['link'] for item in targets))
72
73@@ -221,29 +214,16 @@
74 self.assertEqual(9, len(targets))
75 self.assertEqual(9, len(set(item['link'] for item in targets)))
76
77- def test_top_p_n_p_to_review_caps_suggestions(self):
78- # top_projects_and_packages will suggest at most 10 POFiles that
79- # the person has not worked on.
80- self._makeReviewer()
81- self._makePOFiles(11, previously_worked_on=False)
82-
83- targets = self.view.top_projects_and_packages_to_review
84-
85- self.assertEqual(10, len(targets))
86- self.assertEqual(10, len(set(item['link'] for item in targets)))
87-
88 def test_top_p_n_p_to_review_caps_total(self):
89- # top_projects_and_packages will show at most 10 POFiles
90- # overall. The last one will be a suggestion.
91+ # top_projects_and_packages will show at most 9 POFiles
92+ # overall.
93 self._makeReviewer()
94 pofiles_worked_on = self._makePOFiles(11, previously_worked_on=True)
95- pofiles_not_worked_on = self._makePOFiles(
96- 11, previously_worked_on=False)
97
98 targets = self.view.top_projects_and_packages_to_review
99
100- self.assertEqual(10, len(targets))
101- self.assertEqual(10, len(set(item['link'] for item in targets)))
102+ self.assertEqual(9, len(targets))
103+ self.assertEqual(9, len(set(item['link'] for item in targets)))
104
105 def test_person_is_translator_false(self):
106 # By default, a user is not a translator.
107
108=== modified file 'lib/lp/translations/interfaces/translationsperson.py'
109--- lib/lp/translations/interfaces/translationsperson.py 2011-12-24 16:54:44 +0000
110+++ lib/lp/translations/interfaces/translationsperson.py 2012-10-29 06:41:22 +0000
111@@ -1,8 +1,6 @@
112 # Copyright 2009 Canonical Ltd. This software is licensed under the
113 # GNU Affero General Public License version 3 (see the file LICENSE).
114
115-# pylint: disable-msg=E0213
116-
117 __metaclass__ = type
118 __all__ = [
119 'ITranslationsPerson',
120@@ -61,16 +59,6 @@
121 unreviewed `TranslationMessage` (oldest first).
122 """
123
124- def suggestReviewableTranslationFiles(maximum):
125- """Suggest `POFile`s this person could review.
126-
127- Unlike `getReviewableTranslationFiles`, this method looks for
128- arbitrary translations that the person has not worked on in the
129- recent past.
130-
131- :param maximum: Maximum number of `POFile`s to return.
132- """
133-
134 def getTranslatableFiles(no_older_than=None, urgent_first=True):
135 """List `POFile`s this person should be able to help translate.
136
137
138=== modified file 'lib/lp/translations/model/translationsperson.py'
139--- lib/lp/translations/model/translationsperson.py 2012-10-25 06:08:35 +0000
140+++ lib/lp/translations/model/translationsperson.py 2012-10-29 06:41:22 +0000
141@@ -160,22 +160,6 @@
142 '(SELECT * FROM translatable_distroseries)'))).config(
143 distinct=True).order_by(POFile.date_changed)
144
145- def suggestReviewableTranslationFiles(self, no_older_than=None):
146- """See `ITranslationsPerson`."""
147- tables = self._composePOFileReviewerJoins()
148-
149- # Pick files that this person has no recent POFileTranslator entry
150- # for.
151- translator_join, translator_condition = (
152- self._composePOFileTranslatorJoin(False, no_older_than))
153- tables.append(translator_join)
154-
155- conditions = And(POFile.unreviewed_count > 0, translator_condition)
156-
157- source = Store.of(self.person).using(*tables)
158- query = source.find(POFile, conditions)
159- return query.config(distinct=True).order_by(POFile.id)
160-
161 def _queryTranslatableFiles(self, worked_on, no_older_than=None,
162 languages=None):
163 """Get `POFile`s this person could help translate.
164
165=== modified file 'lib/lp/translations/tests/test_translations_to_review.py'
166--- lib/lp/translations/tests/test_translations_to_review.py 2012-01-01 02:58:52 +0000
167+++ lib/lp/translations/tests/test_translations_to_review.py 2012-10-29 06:41:22 +0000
168@@ -10,7 +10,7 @@
169 timedelta,
170 )
171
172-from pytz import timezone
173+from pytz import UTC
174 import transaction
175 from zope.security.proxy import removeSecurityProxy
176
177@@ -19,13 +19,9 @@
178 from lp.testing import TestCaseWithFactory
179 from lp.testing.layers import DatabaseFunctionalLayer
180 from lp.translations.interfaces.translationsperson import ITranslationsPerson
181-from lp.translations.model.pofiletranslator import POFileTranslatorSet
182 from lp.translations.model.translator import TranslatorSet
183
184
185-UTC = timezone('UTC')
186-
187-
188 class ReviewTestMixin:
189 """Base for testing which translations a reviewer can review."""
190
191@@ -99,12 +95,6 @@
192 return list(person.getReviewableTranslationFiles(
193 *args, **kwargs))
194
195- def _suggestReviewables(self, *args, **kwargs):
196- """Shorthand for `self.person.suggestReviewableTranslationFiles`."""
197- person = ITranslationsPerson(self.person)
198- return list(person.suggestReviewableTranslationFiles(
199- *args, **kwargs))
200-
201
202 class ReviewableTranslationFilesTest:
203 """Test getReviewableTranslationFiles for a given setup.
204@@ -190,69 +180,3 @@
205 def setUp(self):
206 super(TestReviewableDistroTranslationFiles, self).setUp()
207 ReviewTestMixin.setUpMixin(self, for_product=False)
208-
209-
210-class TestSuggestReviewableTranslationFiles(TestCaseWithFactory,
211- ReviewTestMixin):
212- """Test `Person.suggestReviewableTranslationFiles`."""
213- layer = DatabaseFunctionalLayer
214-
215- def setUp(self):
216- super(TestSuggestReviewableTranslationFiles, self).setUp()
217- ReviewTestMixin.setUpMixin(self)
218-
219- def _makeOtherPOFile(self, language_code='nl', same_group=True,
220- with_unreviewed=True):
221- """Set up a `POFile` for an unrelated `POTemplate`."""
222- other_pofile = self.factory.makePOFile(language_code=language_code)
223- other_pofile = removeSecurityProxy(other_pofile)
224-
225- product = other_pofile.potemplate.productseries.product
226- product.translations_usage = ServiceUsage.LAUNCHPAD
227-
228- if with_unreviewed:
229- other_pofile.unreviewed_count = 1
230-
231- if same_group:
232- product.translationgroup = self.translationgroup
233-
234- return other_pofile
235-
236- def test_suggestReviewableTranslationFiles_suggests_files(self):
237- # suggestReviewableTranslationFiles suggests translations to
238- # review.
239- other_pofile = self._makeOtherPOFile()
240- self.assertEqual([other_pofile], self._suggestReviewables())
241-
242- def test_suggestReviewableTranslationFiles_is_complementary(self):
243- # suggestReviewableTranslationFiles does not suggest files that
244- # the person is already working on.
245- self.assertFalse(self.pofile in self._suggestReviewables())
246-
247- def test_suggestReviewableTranslationFiles_ignores_old_involvement(self):
248- # After a person's involvement with a translation grows old
249- # enough, it becomes eligible for suggestion again.
250- poftset = POFileTranslatorSet()
251- involvement = poftset.getForPersonPOFile(self.person, self.pofile)
252- removeSecurityProxy(involvement).date_last_touched -= timedelta(366)
253- suggestions = self._suggestReviewables(
254- no_older_than=involvement.date_last_touched + timedelta(1))
255-
256- self.assertEqual([self.pofile], suggestions)
257-
258- def test_suggestReviewableTranslationFiles_no_translation_group(self):
259- # Only translations that fall under the same translation group
260- # are suggested.
261- other_pofile = self._makeOtherPOFile(same_group=False)
262- self.assertFalse(other_pofile in self._suggestReviewables())
263-
264- def test_suggestReviewableTranslationFiles_ignores_other_languages(self):
265- # suggestReviewableTranslationFiles does not suggest files in
266- # languages that the person is not active in.
267- other_pofile = self._makeOtherPOFile(language_code='ban')
268- self.assertFalse(other_pofile in self._suggestReviewables())
269-
270- def test_suggestReviewableTranslationFiles_checks_unreviewed(self):
271- # Translations without unreviewed suggestions are ignored.
272- other_pofile = self._makeOtherPOFile(with_unreviewed=False)
273- self.assertFalse(other_pofile in self._suggestReviewables())