Merge lp:~jtv/launchpad/recife-pre-sharing-permissions into lp:~launchpad/launchpad/recife

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 9173
Proposed branch: lp:~jtv/launchpad/recife-pre-sharing-permissions
Merge into: lp:~launchpad/launchpad/recife
Diff against target: 801 lines (+239/-168)
15 files modified
lib/lp/testing/factory.py (+8/-2)
lib/lp/testing/tests/test_factory.py (+8/-0)
lib/lp/translations/browser/pofile.py (+0/-9)
lib/lp/translations/browser/tests/pofile-base-views.txt (+0/-2)
lib/lp/translations/browser/tests/test_translationmessage_view.py (+63/-4)
lib/lp/translations/browser/tests/translationmessage-views.txt (+2/-28)
lib/lp/translations/browser/translationmessage.py (+74/-56)
lib/lp/translations/interfaces/pofile.py (+3/-0)
lib/lp/translations/model/pofile.py (+35/-11)
lib/lp/translations/stories/standalone/xx-licensing.txt (+0/-11)
lib/lp/translations/stories/standalone/xx-pofile-translate.txt (+0/-12)
lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt (+0/-12)
lib/lp/translations/tests/test_pofile.py (+43/-18)
lib/lp/translations/utilities/tests/test_file_importer.py (+2/-2)
lib/lp/translations/utilities/translation_import.py (+1/-1)
To merge this branch: bzr merge lp:~jtv/launchpad/recife-pre-sharing-permissions
Reviewer Review Type Date Requested Status
Gavin Panella (community) code Approve
Launchpad code reviewers code Pending
Review via email: mp+38407@code.launchpad.net

Commit message

Cleanups in preparation for Recife sharing permissions check.

Description of the change

= Cleanups prior to Recife sharing permissions checks =

This is a cleanup to be merged into the Recife feature branch.

I'm working on the part of the Recife model change where we check if a particular translation submission for Ubuntu should be shared with upstream, or vice versa. In my preparations I came across some things that needed cleaning up:

 * The basic translation view checked far too many separate things in one big method.

 * View failures generated some truly horrible error messages.

 * Existing permissions checks needed breaking down into smaller chunks to be comprehensible.

 * Some inefficient "do we know how plural forms work for this POFile" logic was also redundant.

 * Reading of "lock_timestamp" was tested in a doctest instead of a view unit test.

 * Lots and lots of trailing whitespace.

I sped up the inefficient logic by doing the cheapest (and often sufficient) test first, centralized it into a single method, and replaced the view properties that duplicated it with a single attribute.

To test, best run all Translations tests really:
{{{
./bin/test -vvc lp.translations
}}}

There's a bit of lint left, most related to comment blocks that aren't directly attached to their respective next lines. I think that style is valid where the comment applies to a section of a class, as these do, so I didn't touch these cases. The other source of lint is Moin-style headings in doctests, which I didn't touch in order to stay under the review limit.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Nice branch, r=me, with a few very minor comments.

[1]

+from __future__ import with_statement
+

I don't think we need to do this anymore.

[2]

+ if blank_timestamp:
+ view.lock_timestamp = None
+ else:
+ view.lock_timestamp = datetime.now(pytz.utc)

The blank_timestamp argument is only used once. If you did the
following in that one place, could this argument be removed and the
code above simplified?

      view = self._makeView()
      view.lock_timestamp = None

[3]

+All the tests will be submitted as comming from Kurem, an editor for the

s/comming/coming/

[4]

+ if method == 'POST' and self.request.form.get('submit_translations'):

What should submit_translations look like? The style guide would say
that this needs to be explicit.

[5]

+ if is_read_only():
+ raise UnexpectedFormData(
+ "Launchpad is currently in read-only mode for maintenance. "
+ "Please try again later.")

Is this not taken care of earlier in the request by, say, the
publication machinery? I haven't done anything to make views cope with
read-only mode, so I'd like to know if I can continue to be oblivious
to this or if I need to do something about it.

[6]

+ if self.user is None:
+ raise UnexpectedFormData("You are not logged in.")

Is this needed? Similar question to [5] really.

review: Approve
Revision history for this message
Gavin Panella (allenap) :
review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> Nice branch, r=me, with a few very minor comments.

Thanks! Good stuff. My replies:

> [1]
>
> +from __future__ import with_statement
>
> I don't think we need to do this anymore.

So did I, but there have been nasty surprises lately where we _thought_ we were all the way there (just a few days ago there was another "we're on 2.6!—er, almost" thread) so I decided to be conservative. I suppose at some point we'll clean these up collectively.

> [2]
>
> + if blank_timestamp:
> + view.lock_timestamp = None
> + else:
> + view.lock_timestamp = datetime.now(pytz.utc)
>
> The blank_timestamp argument is only used once. If you did the
> following in that one place, could this argument be removed and the
> code above simplified?
>
> view = self._makeView()
> view.lock_timestamp = None

Yup. Much better!

> [3]
>
> +All the tests will be submitted as comming from Kurem, an editor for the
>
> s/comming/coming/

Thanks for spotting that. Normally I'm the big pedant.

> [4]
>
> + if method == 'POST' and self.request.form.get('submit_translations'):
>
> What should submit_translations look like? The style guide would say
> that this needs to be explicit.

You're right. AFAICS the ideal spelling is "if 'submit_translations' in self.request.form". (Its contents are actually irrelevant; it's the presence that matters).

> [5]
>
> + if is_read_only():
> + raise UnexpectedFormData(
> + "Launchpad is currently in read-only mode for maintenance. "
> + "Please try again later.")
>
> Is this not taken care of earlier in the request by, say, the
> publication machinery? I haven't done anything to make views cope with
> read-only mode, so I'd like to know if I can continue to be oblivious
> to this or if I need to do something about it.

In theory, yes, this is taken care of. But read-only mode ignores transaction boundaries and isolation levels: it is signaled by the presence of a particular file. That file can appear while processing a request. This check is here because we had one or two oopses while switching to read-only mode.

> [6]
>
> + if self.user is None:
> + raise UnexpectedFormData("You are not logged in.")
>
> Is this needed? Similar question to [5] really.

It's needed for, arguably, a stupid reason. (Don't think I knew this; I checked up just now). Yes, the permissions machinery takes care of it but at a later stage. We're checking it here because the rest of the initialize code makes use of self.user, and bailing out here is easier than guarding all those cases just so we can arrive at another similar error at the end.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-10-05 17:06:23 +0000
+++ lib/lp/testing/factory.py 2010-10-15 04:57:46 +0000
@@ -1794,16 +1794,22 @@
1794 MessageChunk(message=message, sequence=1, content=content)1794 MessageChunk(message=message, sequence=1, content=content)
1795 return message1795 return message
17961796
1797 def makeLanguage(self, language_code=None, name=None, pluralforms=None):1797 def makeLanguage(self, language_code=None, name=None, pluralforms=None,
1798 plural_expression=None):
1798 """Makes a language given the language_code and name."""1799 """Makes a language given the language_code and name."""
1799 if language_code is None:1800 if language_code is None:
1800 language_code = self.getUniqueString('lang')1801 language_code = self.getUniqueString('lang')
1801 if name is None:1802 if name is None:
1802 name = "Language %s" % language_code1803 name = "Language %s" % language_code
1804 if plural_expression is None and pluralforms is not None:
1805 # If the number of plural forms is known, the language
1806 # should also have a plural expression and vice versa.
1807 plural_expression = 'n %% %d' % pluralforms
18031808
1804 language_set = getUtility(ILanguageSet)1809 language_set = getUtility(ILanguageSet)
1805 return language_set.createLanguage(1810 return language_set.createLanguage(
1806 language_code, name, pluralforms=pluralforms)1811 language_code, name, pluralforms=pluralforms,
1812 pluralexpression=plural_expression)
18071813
1808 def makeLibraryFileAlias(self, filename=None, content=None,1814 def makeLibraryFileAlias(self, filename=None, content=None,
1809 content_type='text/plain', restricted=False,1815 content_type='text/plain', restricted=False,
18101816
=== modified file 'lib/lp/testing/tests/test_factory.py'
--- lib/lp/testing/tests/test_factory.py 2010-09-30 17:57:01 +0000
+++ lib/lp/testing/tests/test_factory.py 2010-10-15 04:57:46 +0000
@@ -431,6 +431,14 @@
431 for number_of_forms in [None, 1, 3]:431 for number_of_forms in [None, 1, 3]:
432 language = self.factory.makeLanguage(pluralforms=number_of_forms)432 language = self.factory.makeLanguage(pluralforms=number_of_forms)
433 self.assertEqual(number_of_forms, language.pluralforms)433 self.assertEqual(number_of_forms, language.pluralforms)
434 self.assertEqual(
435 number_of_forms is None, language.pluralexpression is None)
436
437 def test_makeLanguage_with_plural_expression(self):
438 expression = '(n+1) % 5'
439 language = self.factory.makeLanguage(
440 pluralforms=5, plural_expression=expression)
441 self.assertEqual(expression, language.pluralexpression)
434442
435 # makeSourcePackagePublishingHistory443 # makeSourcePackagePublishingHistory
436 def test_makeSourcePackagePublishingHistory_returns_ISPPH(self):444 def test_makeSourcePackagePublishingHistory_returns_ISPPH(self):
437445
=== modified file 'lib/lp/translations/browser/pofile.py'
--- lib/lp/translations/browser/pofile.py 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/browser/pofile.py 2010-10-15 04:57:46 +0000
@@ -364,15 +364,6 @@
364 return statement364 return statement
365365
366 @property366 @property
367 def has_plural_form_information(self):
368 """Return whether we know the plural forms for this language."""
369 if self.context.potemplate.hasPluralMessage():
370 return self.context.language.pluralforms is not None
371 # If there are no plural forms, we assume that we have the
372 # plural form information for this language.
373 return True
374
375 @property
376 def number_of_plural_forms(self):367 def number_of_plural_forms(self):
377 """The number of plural forms for the language or 1 if not known."""368 """The number of plural forms for the language or 1 if not known."""
378 if self.context.language.pluralforms is not None:369 if self.context.language.pluralforms is not None:
379370
=== modified file 'lib/lp/translations/browser/tests/pofile-base-views.txt'
--- lib/lp/translations/browser/tests/pofile-base-views.txt 2009-09-09 18:03:44 +0000
+++ lib/lp/translations/browser/tests/pofile-base-views.txt 2010-10-15 04:57:46 +0000
@@ -37,8 +37,6 @@
3737
38The view has information about the languages plural forms:38The view has information about the languages plural forms:
3939
40 >>> print view.has_plural_form_information
41 True
42 >>> print view.number_of_plural_forms40 >>> print view.number_of_plural_forms
43 241 2
44 >>> print view.plural_expression42 >>> print view.plural_expression
4543
=== modified file 'lib/lp/translations/browser/tests/test_translationmessage_view.py'
--- lib/lp/translations/browser/tests/test_translationmessage_view.py 2010-08-23 08:35:29 +0000
+++ lib/lp/translations/browser/tests/test_translationmessage_view.py 2010-10-15 04:57:46 +0000
@@ -1,22 +1,30 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from __future__ import with_statement
5
4__metaclass__ = type6__metaclass__ = type
57
6from datetime import (8from datetime import (
7 datetime,9 datetime,
8 timedelta,10 timedelta,
9 )11 )
10from unittest import TestLoader
1112
12import pytz13import pytz
1314
14from canonical.launchpad.webapp.servers import LaunchpadTestRequest15from canonical.launchpad.webapp.servers import LaunchpadTestRequest
15from canonical.testing import ZopelessDatabaseLayer16from canonical.testing import ZopelessDatabaseLayer
16from lp.testing import TestCaseWithFactory17from lp.app.errors import UnexpectedFormData
18from lp.testing import (
19 anonymous_logged_in,
20 person_logged_in,
21 TestCaseWithFactory,
22 )
17from lp.translations.browser.translationmessage import (23from lp.translations.browser.translationmessage import (
24 CurrentTranslationMessagePageView,
18 CurrentTranslationMessageView,25 CurrentTranslationMessageView,
19 )26 )
27from lp.translations.interfaces.translationsperson import ITranslationsPerson
2028
2129
22class TestCurrentTranslationMessage_can_dismiss(TestCaseWithFactory):30class TestCurrentTranslationMessage_can_dismiss(TestCaseWithFactory):
@@ -168,5 +176,56 @@
168 self._assertConfirmEmptyPluralPackaged(True, False, False, False)176 self._assertConfirmEmptyPluralPackaged(True, False, False, False)
169177
170178
171def test_suite():179class TestCurrentTranslationMessagePageView(TestCaseWithFactory):
172 return TestLoader().loadTestsFromName(__name__)180 """Test `CurrentTranslationMessagePageView` and its base class."""
181
182 layer = ZopelessDatabaseLayer
183
184 def _makeView(self):
185 message = self.factory.makeTranslationMessage()
186 request = LaunchpadTestRequest()
187 view = CurrentTranslationMessagePageView(message, request)
188 view.lock_timestamp = datetime.now(pytz.utc)
189 return view
190
191 def test_extractLockTimestamp(self):
192 view = self._makeView()
193 view.request.form['lock_timestamp'] = u'2010-01-01 00:00:00 UTC'
194 self.assertEqual(
195 datetime(2010, 01, 01, tzinfo=pytz.utc),
196 view._extractLockTimestamp())
197
198 def test_extractLockTimestamp_returns_None_by_default(self):
199 view = self._makeView()
200 self.assertIs(None, view._extractLockTimestamp())
201
202 def test_extractLockTimestamp_returns_None_for_bogus_timestamp(self):
203 view = self._makeView()
204 view.request.form['lock_timestamp'] = u'Hi mom!'
205 self.assertIs(None, view._extractLockTimestamp())
206
207 def test_checkSubmitConditions_passes(self):
208 with person_logged_in(self.factory.makePerson()):
209 view = self._makeView()
210 view._checkSubmitConditions()
211
212 def test_checkSubmitConditions_requires_lock_timestamp(self):
213 with person_logged_in(self.factory.makePerson()):
214 view = self._makeView()
215 view.lock_timestamp = None
216 self.assertRaises(UnexpectedFormData, view._checkSubmitConditions)
217
218 def test_checkSubmitConditions_rejects_anonymous_request(self):
219 with anonymous_logged_in():
220 view = self._makeView()
221 self.assertRaises(UnexpectedFormData, view._checkSubmitConditions)
222
223 def test_checkSubmitConditions_rejects_license_decliners(self):
224 # Users who have declined the relicensing agreement can't post
225 # translations.
226 decliner = self.factory.makePerson()
227 ITranslationsPerson(decliner).translations_relicensing_agreement = (
228 False)
229 with person_logged_in(decliner):
230 view = self._makeView()
231 self.assertRaises(UnexpectedFormData, view._checkSubmitConditions)
173232
=== modified file 'lib/lp/translations/browser/tests/translationmessage-views.txt'
--- lib/lp/translations/browser/tests/translationmessage-views.txt 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/browser/tests/translationmessage-views.txt 2010-10-15 04:57:46 +0000
@@ -11,8 +11,8 @@
11 >>> from lp.services.worlddata.interfaces.language import ILanguageSet11 >>> from lp.services.worlddata.interfaces.language import ILanguageSet
12 >>> from canonical.launchpad.webapp import canonical_url12 >>> from canonical.launchpad.webapp import canonical_url
1313
14All the tests will be submitted as comming from Kurem, an editor for the POFile14All the tests will be submitted as coming from Kurem, an editor for the
15that we are going to edit.15POFile that we are going to edit.
1616
17 >>> login('kurem@debian.cz')17 >>> login('kurem@debian.cz')
1818
@@ -230,30 +230,6 @@
230230
231A new translation is submitted through the view.231A new translation is submitted through the view.
232232
233 >>> server_url = '/'.join(
234 ... [canonical_url(translationmessage), '+translate'])
235 >>> form = {
236 ... 'alt': None,
237 ... 'msgset_1': None,
238 ... 'msgset_1_es_translation_0_new_checkbox': True,
239 ... 'msgset_1_es_translation_0_new': 'Foo',
240 ... 'submit_translations': 'Save & Continue'}
241 >>> translationmessage_page_view = create_view(
242 ... translationmessage, "+translate", form=form,
243 ... layer=TranslationsLayer, server_url=server_url)
244 >>> translationmessage_page_view.request.method = 'POST'
245
246And when we initialise the view class, we detect that we missed the timestamp
247that allow us to know whether we are working with latest submitted value or
248someone updated the database while we were working on those translations.
249
250 >>> translationmessage_page_view.initialize()
251 Traceback (most recent call last):
252 ...
253 UnexpectedFormData: We didn't find the timestamp...
254
255We do a new submit, but this time including a lock_timestamp field.
256
257 >>> form = {233 >>> form = {
258 ... 'lock_timestamp': '2006-11-28T13:00:00+00:00',234 ... 'lock_timestamp': '2006-11-28T13:00:00+00:00',
259 ... 'alt': None,235 ... 'alt': None,
@@ -269,8 +245,6 @@
269 >>> translationmessage_page_view.initialize()245 >>> translationmessage_page_view.initialize()
270 >>> transaction.commit()246 >>> transaction.commit()
271247
272This time we didn't get any problem with the submission.
273
274Now, let's see how the system prevents a submission that has a timestamp older248Now, let's see how the system prevents a submission that has a timestamp older
275than when last current translation was submitted.249than when last current translation was submitted.
276250
277251
=== modified file 'lib/lp/translations/browser/translationmessage.py'
--- lib/lp/translations/browser/translationmessage.py 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/browser/translationmessage.py 2010-10-15 04:57:46 +0000
@@ -36,6 +36,7 @@
36from zope.interface import implements36from zope.interface import implements
37from zope.schema.vocabulary import getVocabularyRegistry37from zope.schema.vocabulary import getVocabularyRegistry
3838
39from canonical.launchpad.readonly import is_read_only
39from canonical.launchpad.webapp import (40from canonical.launchpad.webapp import (
40 ApplicationMenu,41 ApplicationMenu,
41 canonical_url,42 canonical_url,
@@ -64,6 +65,7 @@
64 RosettaTranslationOrigin,65 RosettaTranslationOrigin,
65 TranslationConflict,66 TranslationConflict,
66 )67 )
68from lp.translations.interfaces.translations import TranslationConstants
67from lp.translations.interfaces.translationsperson import ITranslationsPerson69from lp.translations.interfaces.translationsperson import ITranslationsPerson
68from lp.translations.utilities.validate import GettextValidationError70from lp.translations.utilities.validate import GettextValidationError
6971
@@ -230,9 +232,6 @@
230 """232 """
231233
232 pofile = None234 pofile = None
233 # There will never be 100 plural forms. Usually, we'll be iterating
234 # over just two or three.
235 MAX_PLURAL_FORMS = 100
236235
237 @property236 @property
238 def label(self):237 def label(self):
@@ -246,6 +245,9 @@
246 def initialize(self):245 def initialize(self):
247 assert self.pofile, "Child class must define self.pofile"246 assert self.pofile, "Child class must define self.pofile"
248247
248 self.has_plural_form_information = (
249 self.pofile.hasPluralFormInformation())
250
249 # These two dictionaries hold translation data parsed from the251 # These two dictionaries hold translation data parsed from the
250 # form submission. They exist mainly because of the need to252 # form submission. They exist mainly because of the need to
251 # redisplay posted translations when they contain errors; if not253 # redisplay posted translations when they contain errors; if not
@@ -286,6 +288,16 @@
286288
287 self._initializeAltLanguage()289 self._initializeAltLanguage()
288290
291 method = self.request.method
292 if method == 'POST':
293 self.lock_timestamp = self._extractLockTimestamp()
294 self._checkSubmitConditions()
295 else:
296 # It's not a POST, so we should generate lock_timestamp.
297 UTC = pytz.timezone('UTC')
298 self.lock_timestamp = datetime.datetime.now(UTC)
299
300
289 # The batch navigator needs to be initialized early, before301 # The batch navigator needs to be initialized early, before
290 # _submitTranslations is called; the reason for this is that302 # _submitTranslations is called; the reason for this is that
291 # _submitTranslations, in the case of no errors, redirects to303 # _submitTranslations, in the case of no errors, redirects to
@@ -297,50 +309,65 @@
297 self.start = self.batchnav.start309 self.start = self.batchnav.start
298 self.size = self.batchnav.currentBatch().size310 self.size = self.batchnav.currentBatch().size
299311
300 if self.request.method == 'POST':312 if method == 'POST' and 'submit_translations' in self.request.form:
301 if self.user is None:313 if self._submitTranslations():
302 raise UnexpectedFormData(314 # If no errors occurred, adios. Otherwise, we need to set up
303 "Anonymous users or users who are not accepting our "315 # the subviews for error display and correction.
304 "licensing terms cannot do POST submissions.")316 return
305 translations_person = ITranslationsPerson(self.user)
306 if (translations_person.translations_relicensing_agreement
307 is not None and
308 not translations_person.translations_relicensing_agreement):
309 raise UnexpectedFormData(
310 "Users who do not agree to licensing terms "
311 "cannot do POST submissions.")
312 try:
313 # Try to get the timestamp when the submitted form was
314 # created. We use it to detect whether someone else updated
315 # the translation we are working on in the elapsed time
316 # between the form loading and its later submission.
317 self.lock_timestamp = zope_datetime.parseDatetimetz(
318 self.request.form.get('lock_timestamp', u''))
319 except zope_datetime.DateTimeError:
320 # invalid format. Either we don't have the timestamp in the
321 # submitted form or it has the wrong format.
322 raise UnexpectedFormData(
323 "We didn't find the timestamp that tells us when was"
324 " generated the submitted form.")
325
326 # Check if this is really the form we are listening for..
327 if self.request.form.get("submit_translations"):
328 # Check if this is really the form we are listening for..
329 if self._submitTranslations():
330 # .. and if no errors occurred, adios. Otherwise, we
331 # need to set up the subviews for error display and
332 # correction.
333 return
334 else:
335 # It's not a POST, so we should generate lock_timestamp.
336 UTC = pytz.timezone('UTC')
337 self.lock_timestamp = datetime.datetime.now(UTC)
338317
339 # Slave view initialization depends on _submitTranslations being318 # Slave view initialization depends on _submitTranslations being
340 # called, because the form data needs to be passed in to it --319 # called, because the form data needs to be passed in to it --
341 # again, because of error handling.320 # again, because of error handling.
342 self._initializeTranslationMessageViews()321 self._initializeTranslationMessageViews()
343322
323 def _extractLockTimestamp(self):
324 """Extract the lock timestamp from the request.
325
326 The lock_timestamp is used to detect conflicting concurrent
327 translation updates: if the translation that is being changed
328 has been set after the current form was generated, the user
329 chose a translation based on outdated information. In that
330 case there is a conflict.
331 """
332 try:
333 return zope_datetime.parseDatetimetz(
334 self.request.form.get('lock_timestamp', u''))
335 except zope_datetime.DateTimeError:
336 # invalid format. Either we don't have the timestamp in the
337 # submitted form or it has the wrong format.
338 return None
339
340 def _checkSubmitConditions(self):
341 """Verify that this submission is possible and valid.
342
343 :raises: `UnexpectedFormData` if conditions are not met. In
344 principle the user should not have been given the option to
345 submit the current request.
346 """
347 if is_read_only():
348 raise UnexpectedFormData(
349 "Launchpad is currently in read-only mode for maintenance. "
350 "Please try again later.")
351
352 if self.user is None:
353 raise UnexpectedFormData("You are not logged in.")
354
355 # Users who have declined the licensing agreement can't post
356 # translations. We don't stop users who haven't made a decision
357 # yet at this point; they may be project owners making
358 # corrections.
359 translations_person = ITranslationsPerson(self.user)
360 relicensing = translations_person.translations_relicensing_agreement
361 if relicensing is not None and not relicensing:
362 raise UnexpectedFormData(
363 "You can't post translations since you have not agreed to "
364 "our translations licensing terms.")
365
366 if self.lock_timestamp is None:
367 raise UnexpectedFormData(
368 "Your form submission did not contain the lock_timestamp "
369 "that tells Launchpad when the submitted form was generated.")
370
344 #371 #
345 # API Hooks372 # API Hooks
346 #373 #
@@ -587,15 +614,6 @@
587 self.second_lang_code = second_lang_code614 self.second_lang_code = second_lang_code
588615
589 @property616 @property
590 def has_plural_form_information(self):
591 """Return whether we know the plural forms for this language."""
592 if self.pofile.potemplate.hasPluralMessage():
593 return self.pofile.language.pluralforms is not None
594 # If there are no plural forms, we assume that we have the
595 # plural form information for this language.
596 return True
597
598 @property
599 def user_is_official_translator(self):617 def user_is_official_translator(self):
600 """Determine whether the current user is an official translator."""618 """Determine whether the current user is an official translator."""
601 return self.pofile.canEditTranslations(self.user)619 return self.pofile.canEditTranslations(self.user)
@@ -658,7 +676,7 @@
658 # Extract the translations from the form, and store them in676 # Extract the translations from the form, and store them in
659 # self.form_posted_translations. We try plural forms in turn,677 # self.form_posted_translations. We try plural forms in turn,
660 # starting at 0.678 # starting at 0.
661 for pluralform in xrange(self.MAX_PLURAL_FORMS):679 for pluralform in xrange(TranslationConstants.MAX_PLURAL_FORMS):
662 msgset_ID_LANGCODE_translation_PLURALFORM_new = '%s%d_new' % (680 msgset_ID_LANGCODE_translation_PLURALFORM_new = '%s%d_new' % (
663 msgset_ID_LANGCODE_translation_, pluralform)681 msgset_ID_LANGCODE_translation_, pluralform)
664 if msgset_ID_LANGCODE_translation_PLURALFORM_new not in form:682 if msgset_ID_LANGCODE_translation_PLURALFORM_new not in form:
@@ -737,7 +755,7 @@
737 potmsgset].append(pluralform)755 potmsgset].append(pluralform)
738 else:756 else:
739 raise AssertionError('More than %d plural forms were submitted!'757 raise AssertionError('More than %d plural forms were submitted!'
740 % self.MAX_PLURAL_FORMS)758 % TranslationConstants.MAX_PLURAL_FORMS)
741759
742 def _observeTranslationUpdate(self, potmsgset):760 def _observeTranslationUpdate(self, potmsgset):
743 """Observe that a translation was updated for the potmsgset.761 """Observe that a translation was updated for the potmsgset.
@@ -1056,7 +1074,7 @@
10561074
1057 diverged_and_have_shared = (1075 diverged_and_have_shared = (
1058 self.context.potemplate is not None and1076 self.context.potemplate is not None and
1059 self.shared_translationmessage is not None) 1077 self.shared_translationmessage is not None)
1060 if diverged_and_have_shared:1078 if diverged_and_have_shared:
1061 pofile = self.shared_translationmessage.ensureBrowserPOFile()1079 pofile = self.shared_translationmessage.ensureBrowserPOFile()
1062 if pofile is None:1080 if pofile is None:
@@ -1155,10 +1173,10 @@
11551173
1156 def _setOnePOFile(self, messages):1174 def _setOnePOFile(self, messages):
1157 """Return a list of messages that all have a browser_pofile set.1175 """Return a list of messages that all have a browser_pofile set.
1158 1176
1159 If a pofile cannot be found for a message, it is not included in1177 If a pofile cannot be found for a message, it is not included in
1160 the resulting list.1178 the resulting list.
1161 """ 1179 """
1162 result = []1180 result = []
1163 for message in messages:1181 for message in messages:
1164 if message.browser_pofile is None:1182 if message.browser_pofile is None:
@@ -1170,7 +1188,7 @@
1170 message.setPOFile(pofile)1188 message.setPOFile(pofile)
1171 result.append(message)1189 result.append(message)
1172 return result1190 return result
1173 1191
1174 def _buildAllSuggestions(self):1192 def _buildAllSuggestions(self):
1175 """Builds all suggestions and puts them into suggestions_block.1193 """Builds all suggestions and puts them into suggestions_block.
11761194
11771195
=== modified file 'lib/lp/translations/interfaces/pofile.py'
--- lib/lp/translations/interfaces/pofile.py 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/interfaces/pofile.py 2010-10-15 04:57:46 +0000
@@ -168,6 +168,9 @@
168 for.168 for.
169 """169 """
170170
171 def hasPluralFormInformation():
172 """Do we know the plural-forms information for this `POFile`?"""
173
171 def getHeader():174 def getHeader():
172 """Return an `ITranslationHeaderData` representing its header."""175 """Return an `ITranslationHeaderData` representing its header."""
173176
174177
=== modified file 'lib/lp/translations/model/pofile.py'
--- lib/lp/translations/model/pofile.py 2010-09-30 08:51:21 +0000
+++ lib/lp/translations/model/pofile.py 2010-10-15 04:57:46 +0000
@@ -177,6 +177,24 @@
177 return False177 return False
178178
179179
180def is_admin(user):
181 """Is `user` an admin or Translations admin?"""
182 celebs = getUtility(ILaunchpadCelebrities)
183 return user.inTeam(celebs.admin) or user.inTeam(celebs.rosetta_experts)
184
185
186def is_product_owner(user, potemplate):
187 """Is `user` the owner of a `Product` that `potemplate` belongs to?
188
189 A product's owners have edit rights on the product's translations.
190 """
191 productseries = potemplate.productseries
192 if productseries is None:
193 return False
194
195 return user.inTeam(productseries.product.owner)
196
197
180def _can_edit_translations(pofile, person):198def _can_edit_translations(pofile, person):
181 """Say if a person is able to edit existing translations.199 """Say if a person is able to edit existing translations.
182200
@@ -190,29 +208,25 @@
190 translation team for the given `IPOFile`.translationpermission and the208 translation team for the given `IPOFile`.translationpermission and the
191 language associated with this `IPOFile`.209 language associated with this `IPOFile`.
192 """210 """
211 if person is None:
212 # Anonymous users can't edit anything.
213 return False
214
193 if is_read_only():215 if is_read_only():
194 # Nothing can be edited in read-only mode.216 # Nothing can be edited in read-only mode.
195 return False217 return False
196218
197 # If the person is None, then they cannot edit
198 if person is None:
199 return False
200
201 # XXX Carlos Perello Marin 2006-02-07 bug=4814:219 # XXX Carlos Perello Marin 2006-02-07 bug=4814:
202 # We should not check the permissions here but use the standard220 # We should not check the permissions here but use the standard
203 # security system.221 # security system.
204222
205 # Rosetta experts and admins can always edit translations.223 # Rosetta experts and admins can always edit translations.
206 admins = getUtility(ILaunchpadCelebrities).admin224 if is_admin(person):
207 rosetta_experts = getUtility(ILaunchpadCelebrities).rosetta_experts
208 if (person.inTeam(admins) or person.inTeam(rosetta_experts)):
209 return True225 return True
210226
211 # The owner of the product is also able to edit translations.227 # The owner of the product is also able to edit translations.
212 if pofile.potemplate.productseries is not None:228 if is_product_owner(person, pofile.potemplate):
213 product = pofile.potemplate.productseries.product229 return True
214 if person.inTeam(product.owner):
215 return True
216230
217 # If a person has decided not to license their translations under BSD231 # If a person has decided not to license their translations under BSD
218 # license they can't edit translations.232 # license they can't edit translations.
@@ -284,6 +298,16 @@
284 """See `IPOFile`."""298 """See `IPOFile`."""
285 return self.language.guessed_pluralforms299 return self.language.guessed_pluralforms
286300
301 def hasPluralFormInformation(self):
302 """See `IPOFile`."""
303 if self.language.pluralforms is None:
304 # We have no plural information for this language. It
305 # doesn't actually matter unless the template contains
306 # messages with plural forms.
307 return not self.potemplate.hasPluralMessage()
308 else:
309 return True
310
287 def canEditTranslations(self, person):311 def canEditTranslations(self, person):
288 """See `IPOFile`."""312 """See `IPOFile`."""
289 return _can_edit_translations(self, person)313 return _can_edit_translations(self, person)
290314
=== modified file 'lib/lp/translations/stories/standalone/xx-licensing.txt'
--- lib/lp/translations/stories/standalone/xx-licensing.txt 2009-09-18 15:42:19 +0000
+++ lib/lp/translations/stories/standalone/xx-licensing.txt 2010-10-15 04:57:46 +0000
@@ -53,17 +53,6 @@
53 >>> for input in main_content.findAll('input'):53 >>> for input in main_content.findAll('input'):
54 ... print 'Found input:\n%s' % input54 ... print 'Found input:\n%s' % input
5555
56Karl is also denied submitting a POST request.
57
58 >>> print http(r"""
59 ... POST /alsa-utils/trunk/+pots/alsa-utils/es/+translate HTTP/1.1
60 ... Host: translations.launchpad.dev
61 ... """)
62 HTTP/1.1 500 Internal Server Error
63 ...
64 ...UnexpectedFormData: Anonymous users or users who are not accepting
65 our licensing terms cannot do POST submissions...
66
67Karl changes his mind. He returns to the licensing page.56Karl changes his mind. He returns to the licensing page.
6857
69 >>> browser.open('http://translations.launchpad.dev/~karl/')58 >>> browser.open('http://translations.launchpad.dev/~karl/')
7059
=== modified file 'lib/lp/translations/stories/standalone/xx-pofile-translate.txt'
--- lib/lp/translations/stories/standalone/xx-pofile-translate.txt 2010-06-12 05:44:36 +0000
+++ lib/lp/translations/stories/standalone/xx-pofile-translate.txt 2010-10-15 04:57:46 +0000
@@ -62,18 +62,6 @@
62data. That part is more convenience than security. The server does not62data. That part is more convenience than security. The server does not
63remember what form it provided to which requester.63remember what form it provided to which requester.
6464
65Separately from that, however, the form also rejects anonymous POST
66submissions.
67
68 >>> print http(r"""
69 ... POST /ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+translate HTTP/1.1
70 ... Host: translations.launchpad.dev
71 ... """)
72 HTTP/1.1 500 Internal Server Error
73 ...
74 ...UnexpectedFormData: Anonymous users or users who are not accepting
75 our licensing terms cannot do POST submissions...
76
7765
78Translation Admin Access66Translation Admin Access
79------------------------67------------------------
8068
=== modified file 'lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt'
--- lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt 2010-05-25 16:45:00 +0000
+++ lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt 2010-10-15 04:57:46 +0000
@@ -44,18 +44,6 @@
44 >> print nav.getLink("Translation details").url44 >> print nav.getLink("Translation details").url
45 https://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+details45 https://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+details
4646
47Also, any anonymous POST submission should fail:
48
49 >>> print http(r"""
50 ... POST /ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/5/+translate HTTP/1.1
51 ... Host: translations.launchpad.dev
52 ... """)
53 HTTP/1.1 500 Internal Server Error
54 ...
55 ...UnexpectedFormData: Anonymous users or users who are not accepting
56 our licensing terms cannot do POST submissions...
57
58
59Let's log in.47Let's log in.
6048
61 >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')49 >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')
6250
=== modified file 'lib/lp/translations/tests/test_pofile.py'
--- lib/lp/translations/tests/test_pofile.py 2010-09-30 17:57:01 +0000
+++ lib/lp/translations/tests/test_pofile.py 2010-10-15 04:57:46 +0000
@@ -1888,6 +1888,31 @@
1888 self.pofile.markChanged(translator=translator)1888 self.pofile.markChanged(translator=translator)
1889 self.assertEqual(translator, self.pofile.lasttranslator)1889 self.assertEqual(translator, self.pofile.lasttranslator)
18901890
1891 def test_hasPluralFormInformation_bluffs_if_irrelevant(self):
1892 # If the template has no messages that use plural forms, the
1893 # POFile has all the relevant plural-form information regardless
1894 # of whether we know the plural forms for the language.
1895 language = self.factory.makeLanguage()
1896 pofile, potmsgset = self.factory.makePOFileAndPOTMsgSet(
1897 language.code, with_plural=False)
1898 self.assertTrue(pofile.hasPluralFormInformation())
1899
1900 def test_hasPluralFormInformation_admits_defeat(self):
1901 # If there are messages with plurals, hasPluralFormInformation
1902 # needs the plural-form information for the language.
1903 language = self.factory.makeLanguage()
1904 pofile, potmsgset = self.factory.makePOFileAndPOTMsgSet(
1905 language.code, with_plural=True)
1906 self.assertFalse(pofile.hasPluralFormInformation())
1907
1908 def test_hasPluralFormInformation_uses_language_info(self):
1909 # hasPluralFormInformation returns True if plural forms
1910 # information is available for the language.
1911 language = self.factory.makeLanguage(pluralforms=5)
1912 pofile, potmsgset = self.factory.makePOFileAndPOTMsgSet(
1913 language.code, with_plural=True)
1914 self.assertTrue(pofile.hasPluralFormInformation())
1915
18911916
1892class TestPOFileTranslationMessages(TestCaseWithFactory):1917class TestPOFileTranslationMessages(TestCaseWithFactory):
1893 """Test PO file getTranslationMessages method."""1918 """Test PO file getTranslationMessages method."""
@@ -1905,66 +1930,66 @@
1905 # A shared message is included in this POFile's messages.1930 # A shared message is included in this POFile's messages.
1906 message = self.factory.makeTranslationMessage(1931 message = self.factory.makeTranslationMessage(
1907 potmsgset=self.potmsgset, pofile=self.pofile, force_shared=True)1932 potmsgset=self.potmsgset, pofile=self.pofile, force_shared=True)
1908 1933
1909 self.assertEqual(1934 self.assertEqual(
1910 [message], list(self.pofile.getTranslationMessages()))1935 [message], list(self.pofile.getTranslationMessages()))
1911 1936
1912 def test_getTranslationMessages_current_diverged(self):1937 def test_getTranslationMessages_current_diverged(self):
1913 # A diverged message is included in this POFile's messages.1938 # A diverged message is included in this POFile's messages.
1914 message = self.factory.makeTranslationMessage(1939 message = self.factory.makeTranslationMessage(
1915 potmsgset=self.potmsgset, pofile=self.pofile, force_diverged=True)1940 potmsgset=self.potmsgset, pofile=self.pofile, force_diverged=True)
1916 1941
1917 self.assertEqual(1942 self.assertEqual(
1918 [message], list(self.pofile.getTranslationMessages()))1943 [message], list(self.pofile.getTranslationMessages()))
1919 1944
1920 def test_getTranslationMessages_suggestion(self):1945 def test_getTranslationMessages_suggestion(self):
1921 # A suggestion is included in this POFile's messages.1946 # A suggestion is included in this POFile's messages.
1922 message = self.factory.makeTranslationMessage(1947 message = self.factory.makeTranslationMessage(
1923 potmsgset=self.potmsgset, pofile=self.pofile)1948 potmsgset=self.potmsgset, pofile=self.pofile)
1924 1949
1925 self.assertEqual(1950 self.assertEqual(
1926 [message], list(self.pofile.getTranslationMessages()))1951 [message], list(self.pofile.getTranslationMessages()))
1927 1952
1928 def test_getTranslationMessages_obsolete(self):1953 def test_getTranslationMessages_obsolete(self):
1929 # A message on an obsolete POTMsgSEt is included in this1954 # A message on an obsolete POTMsgSEt is included in this
1930 # POFile's messages.1955 # POFile's messages.
1931 potmsgset = self.factory.makePOTMsgSet(self.potemplate, sequence=0)1956 potmsgset = self.factory.makePOTMsgSet(self.potemplate, sequence=0)
1932 message = self.factory.makeTranslationMessage(1957 message = self.factory.makeTranslationMessage(
1933 potmsgset=potmsgset, pofile=self.pofile, force_shared=True)1958 potmsgset=potmsgset, pofile=self.pofile, force_shared=True)
1934 1959
1935 self.assertEqual(1960 self.assertEqual(
1936 [message], list(self.pofile.getTranslationMessages()))1961 [message], list(self.pofile.getTranslationMessages()))
1937 1962
1938 def test_getTranslationMessages_other_pofile(self):1963 def test_getTranslationMessages_other_pofile(self):
1939 # A message from another POFiles is not included.1964 # A message from another POFiles is not included.
1940 other_pofile = self.factory.makePOFile('de')1965 other_pofile = self.factory.makePOFile('de')
1941 self.factory.makeTranslationMessage(1966 self.factory.makeTranslationMessage(
1942 potmsgset=self.potmsgset, pofile=other_pofile)1967 potmsgset=self.potmsgset, pofile=other_pofile)
1943 1968
1944 self.assertEqual([], list(self.pofile.getTranslationMessages()))1969 self.assertEqual([], list(self.pofile.getTranslationMessages()))
1945 1970
1946 def test_getTranslationMessages_condition_matches(self):1971 def test_getTranslationMessages_condition_matches(self):
1947 # A message matching the given condition is included.1972 # A message matching the given condition is included.
1948 # Diverged messages are linked to a specific POTemplate.1973 # Diverged messages are linked to a specific POTemplate.
1949 message = self.factory.makeTranslationMessage(1974 message = self.factory.makeTranslationMessage(
1950 potmsgset=self.potmsgset, pofile=self.pofile, force_diverged=True)1975 potmsgset=self.potmsgset, pofile=self.pofile, force_diverged=True)
1951 1976
1952 self.assertContentEqual(1977 self.assertContentEqual(
1953 [message],1978 [message],
1954 self.pofile.getTranslationMessages(1979 self.pofile.getTranslationMessages(
1955 "TranslationMessage.potemplate IS NOT NULL"))1980 "TranslationMessage.potemplate IS NOT NULL"))
1956 1981
1957 def test_getTranslationMessages_condition_matches_not(self):1982 def test_getTranslationMessages_condition_matches_not(self):
1958 # A message not matching the given condition is excluded.1983 # A message not matching the given condition is excluded.
1959 # Shared messages are not linked to a POTemplate.1984 # Shared messages are not linked to a POTemplate.
1960 self.factory.makeTranslationMessage(1985 self.factory.makeTranslationMessage(
1961 potmsgset=self.potmsgset, pofile=self.pofile, force_shared=True)1986 potmsgset=self.potmsgset, pofile=self.pofile, force_shared=True)
1962 1987
1963 self.assertContentEqual(1988 self.assertContentEqual(
1964 [],1989 [],
1965 self.pofile.getTranslationMessages(1990 self.pofile.getTranslationMessages(
1966 "TranslationMessage.potemplate IS NOT NULL"))1991 "TranslationMessage.potemplate IS NOT NULL"))
1967 1992
1968 def test_getTranslationMessages_condition_matches_in_other_pofile(self):1993 def test_getTranslationMessages_condition_matches_in_other_pofile(self):
1969 # A message matching given condition but located in another POFile1994 # A message matching given condition but located in another POFile
1970 # is not included.1995 # is not included.
@@ -1972,12 +1997,12 @@
1972 self.factory.makeTranslationMessage(1997 self.factory.makeTranslationMessage(
1973 potmsgset=self.potmsgset, pofile=other_pofile,1998 potmsgset=self.potmsgset, pofile=other_pofile,
1974 force_diverged=True)1999 force_diverged=True)
1975 2000
1976 self.assertContentEqual(2001 self.assertContentEqual(
1977 [],2002 [],
1978 self.pofile.getTranslationMessages(2003 self.pofile.getTranslationMessages(
1979 "TranslationMessage.potemplate IS NOT NULL"))2004 "TranslationMessage.potemplate IS NOT NULL"))
1980 2005
1981 def test_getTranslationMessages_diverged_elsewhere(self):2006 def test_getTranslationMessages_diverged_elsewhere(self):
1982 # Diverged messages from sharing POTemplates are not included.2007 # Diverged messages from sharing POTemplates are not included.
1983 # Create a sharing potemplate in another product series and share2008 # Create a sharing potemplate in another product series and share
@@ -1992,9 +2017,9 @@
1992 self.factory.makeTranslationMessage(2017 self.factory.makeTranslationMessage(
1993 potmsgset=self.potmsgset, pofile=other_pofile,2018 potmsgset=self.potmsgset, pofile=other_pofile,
1994 force_diverged=True)2019 force_diverged=True)
1995 2020
1996 self.assertEqual([], list(self.pofile.getTranslationMessages()))2021 self.assertEqual([], list(self.pofile.getTranslationMessages()))
1997 2022
19982023
1999class TestPOFileToTranslationFileDataAdapter(TestCaseWithFactory):2024class TestPOFileToTranslationFileDataAdapter(TestCaseWithFactory):
2000 """Test POFile being adapted to IPOFileToTranslationFileData."""2025 """Test POFile being adapted to IPOFileToTranslationFileData."""
20012026
=== modified file 'lib/lp/translations/utilities/tests/test_file_importer.py'
--- lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-06 11:05:13 +0000
+++ lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-15 04:57:46 +0000
@@ -613,7 +613,7 @@
613 if side == self.UPSTREAM:613 if side == self.UPSTREAM:
614 potemplate = self.upstream_template614 potemplate = self.upstream_template
615 else:615 else:
616 # Create a template in a source package. 616 # Create a template in a source package.
617 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu617 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
618 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)618 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)
619 ubuntu.translation_focus = distroseries619 ubuntu.translation_focus = distroseries
@@ -639,7 +639,7 @@
639 uploader=uploader, content=self.POFILE)639 uploader=uploader, content=self.POFILE)
640 entry.potemplate = potemplate640 entry.potemplate = potemplate
641 entry.pofile = pofile641 entry.pofile = pofile
642 # The uploaded file is only created in the librarian by a commit. 642 # The uploaded file is only created in the librarian by a commit.
643 transaction.commit()643 transaction.commit()
644 return entry644 return entry
645645
646646
=== modified file 'lib/lp/translations/utilities/translation_import.py'
--- lib/lp/translations/utilities/translation_import.py 2010-10-01 14:37:53 +0000
+++ lib/lp/translations/utilities/translation_import.py 2010-10-15 04:57:46 +0000
@@ -493,7 +493,7 @@
493 """Are these English strings instead of translations?493 """Are these English strings instead of translations?
494494
495 If this template uses symbolic message ids, the English POFile495 If this template uses symbolic message ids, the English POFile
496 will contain the English original texts that correspond to the 496 will contain the English original texts that correspond to the
497 symbols."""497 symbols."""
498 return (498 return (
499 self.importer.uses_source_string_msgids and499 self.importer.uses_source_string_msgids and

Subscribers

People subscribed via source and target branches