Merge lp:~adiroiban/launchpad/bug-509252-take-2 into lp:launchpad

Proposed by Adi Roiban
Status: Merged
Approved by: Henning Eggers
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~adiroiban/launchpad/bug-509252-take-2
Merge into: lp:launchpad
Diff against target: 760 lines (+184/-213)
11 files modified
lib/canonical/launchpad/security.py (+0/-15)
lib/lp/registry/browser/distroseries.py (+9/-17)
lib/lp/registry/browser/sourcepackage.py (+8/-4)
lib/lp/registry/doc/distroseries.txt (+18/-76)
lib/lp/registry/interfaces/distroseries.py (+4/-13)
lib/lp/registry/model/distroseries.py (+12/-37)
lib/lp/translations/browser/browser_helpers.py (+4/-6)
lib/lp/translations/browser/distroseries.py (+52/-29)
lib/lp/translations/browser/tests/distroseries-views.txt (+69/-11)
lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt (+3/-5)
lib/lp/translations/templates/distroseries-langchart.pt (+5/-0)
To merge this branch: bzr merge lp:~adiroiban/launchpad/bug-509252-take-2
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Michael Nelson (community) code Needs Information
Review via email: mp+19484@code.launchpad.net

Commit message

Refactor checkTranslationsViewable(). Merge the model and view logic and move it to a view for distroseries. Landed by henninge.

To post a comment you must log in.
Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (4.0 KiB)

= Bug 509252 =
After landing the fix for bug 340662 (https://code.edge.launchpad.net/~adiroiban/launchpad/bug-340662-take-2/+merge/17598) the POTemplateSubsetNavigation will render the AdminPOTemplateSubset useless.

We should clean the security.py.

AdminPOTemplateSubset was added to fix bug 497438

== Proposed fix ==

Remove unused classed from security.py and refactor

== Pre-implementation notes ==

Talking with Henning we decide not to include security checking code in the model. This is why the logic of showing translations of a series was spitted between the model and the view and the security checks were done in multiple places.

This restriction lead to a strange implementation where the check was done both in the view and the model and it was a source of confusion.

I merged this logic in a browser helper function and now the security check is only done once.

== Implementation details ==

These changes renders the AdminDistroSeriesLanguage class from security.py useless, so I have also removed it.

== Tests ==
lp-test -t distroseries-translations -t distroseries-views

== Demo and Q/A ==
Make sure you are a member of Ubuntu Translation Coordinators team (ubuntu-l10n-coordinator).
https://launchpad.dev/~ubuntu-l10n-coordinator

Go to the translation page for a distribution and hide translations for this series
ie: https://translations.launchpad.dev/ubuntu/hoary/+admin

Next go to the distribution series +template page
https://translations.launchpad.dev/ubuntu/hoary/+templates

You should see the Administer page for Evolution templates, including disabled-template .

As a member of Ubuntu Translation Coordinators team you should be able to administer them, just like for a normal series.

Login as a normal user you should see a page informing that the translations are currently closed

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/registry/browser/distroseries.py
  lib/lp/registry/browser/sourcepackage.py
  lib/lp/registry/interfaces/distroseries.py
  lib/lp/registry/model/distroseries.py
  lib/lp/translations/browser/browser_helpers.py
  lib/lp/translations/browser/distroseries.py
  lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt

== Pylint notices ==

lib/lp/registry/browser/sourcepackage.py
    29: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)

lib/lp/registry/interfaces/distroseries.py
    23: [F0401] Unable to import 'lazr.enum' (No module named enum)
    50: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    51: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    117: [E1002, DistroSeriesVersionField._validate] Use super on an old style class
    447: [C0322, IDistroSeriesPublic.getPackageUploads] Operator not preceded by a space
    description=_("Return items that are more recent than this "
    ^
    "timestamp."),
    required=False),
    status=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Package Upload Status"),
    des...

Read more...

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hi Adi,

Thanks for all the cleanups that you've done in this branch.

After reading the recent email discussion at:

https://lists.launchpad.net/launchpad-dev/msg02442.html

and particularly BjornT's message:

https://lists.launchpad.net/launchpad-dev/msg02504.html

I don't think putting the security check into a browser helper is the way to go.

As outlined in the above email:
"The current solution is to have this code in model code, and have the
security adapter ask the model. No code duplication really."

this ensures that any security can be applied not only in the view, but also via other modes of access (API), even if they are not yet enabled.

As it turns out in your code, it seems most of IDistroSeries.checkTranslationsViewable() is not actually security code, but simply appropriate error generation. The first few lines however are security related.

So I wonder if we can do something like this:

http://pastebin.ubuntu.com/379120/

That is, simply add a launchpad.View check for IDistroSeriesLanguage, which itself ensures that launchpad.View is granted if the user is an admin. That way, as in the example in the diff, the current double security checks:

         if not check_permission(
            'launchpad.TranslationsAdmin', distroserieslang):
            self.context.checkTranslationsViewable()

could be replaced simply by:

         if not check_permission(
            'launchpad.View', distroserieslang):
            translation_unavailable_error()

that way you'd have all the security in the right place, separate from the error generation (ie. translation_unavailable_error() would be your proposed view helper, check_distroseries_translations_viewable, just without the security checks?

I'm not 100%, but as far as I can see, the above should do what you're after while at the same time keeping all the security code in the one place. But I'll check with Henninge to be sure (hence marking this as needs info).

IRC log of conversation starts at:

http://irclogs.ubuntu.com/2010/02/18/%23launchpad-reviews.html#t11:18

review: Needs Information (code)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

OK, I spoke to Adi, and he said that the security for IDistroSeriesLanguage needs to go anyway, which means that the above paste is useless, as we can't update the permission checks for DistroSeries or SourcePackage (or is there a way to have checks for these objects only when they are traversed in the translations context? - an extra 'facet' option to check_permission?)

Without this, I can't see a way to keep these security checks encapsulated in security.py or on the model. I'm going to have to defer to Danilo or Henninge.

Revision history for this message
Henning Eggers (henninge) wrote :

Hi Adi,
thanks for cleaning this up. Your code is almost good, I'd just ask two changes of you.

I agree with Michael's point that check_distroseries_translations_viewable should not go into browser_helper. DistroSeries.checkTranslationsViewable also is a bad place to raise exceptions because these should go into the browser code. So you were right in removing this.

The code in check_distroseries_translations_viewable is not a security check but an error handling routine that uses existing security checks. So I think it is perfectly valid in browser code.

Something that was already wrong before this branch but that should be cleaned up is this: DistroSeriesView.checkTranslationsViewable is only used in a tal:omit-tag and is therefore expected to return a bool. Even with your branch it raises exceptions. I don't know why that worked before but I guess this is some historic garbage floating around.

So my first request is that you find a new implementation for DistroSeriesView.checkTranslationsViewable that is a bool view property and does not raise exceptions. But maybe you can remove it all together if that one call site proves to be garbage.

My other request is that you move check_distroseries_translations_viewable into lib/lp/translations/browser/distroseries.py. I would have liked to see it in a Mixin that you can add to DistroSeriesNavigation and SourcePackageNavigation but that may not make too much sense. Think about it, though.

Finally I noticed DistroSeries.getDistroSeriesLanguageOrDummy which could be used to make the code in DistroSeriesNavigation.traverse_lang much shorter ;-)

Thanks. ;-)

review: Needs Fixing (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

DistroSeriesView.checkTranslationsViewable() is used in a „metal” tag... so it really doesn't matter its return value as its sole purpose is to raise those exceptions.

<metal:check-available tal:omit-tag="view/checkTranslationsViewable" />

If we want to remove this behavior, we would need to raise those exception in Distribution URL traversal for series... and then things will get a bit complicated as those exception should be raised only for „translations” facet.

-------

There are a couple of view tests in translations/browser/tests/distroseries-views.txt and registry/doc/distroseries.txt using these exceptions. I guess we need to put them in a single file (ie: translations/browser/tests/distroseries-views.txt)

-----

Thanks for the getDistroSeriesLanguageOrDummy tip :)

Revision history for this message
Henning Eggers (henninge) wrote :

Hi Adi,
sorry for not replying but I had expected that you push an updated branch with whatever changes you see possible.

I did not know that metal tags are being used to raise exceptions and I am pretty sure that should not be that way. If you think it's too much work to improve that in this branch, I can understand that. Please file a bug so that it gets done at some later time.

So, a new push to the branch would be the next thing to get the review forward.

Cheers,
Henning

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (18.5 KiB)

Hi,
Don't worry about the communication problem ... be happy :)

I have opened a new bug for TAL expression used for raising an exception.
I have merged the tests for translation permissions.

Here is the diff. Thanks!
=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py 2010-02-17 13:06:35 +0000
+++ lib/lp/registry/browser/distroseries.py 2010-02-24 14:57:59 +0000
@@ -37,10 +37,8 @@
 from lp.services.worlddata.interfaces.country import ICountry
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.distroseries import IDistroSeries
-from lp.translations.browser.browser_helpers import (
+from lp.translations.browser.distroseries import (
     check_distroseries_translations_viewable)
-from lp.translations.interfaces.distroserieslanguage import (
- IDistroSeriesLanguageSet)
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.registry.browser.structuralsubscription import (
     StructuralSubscriptionMenuMixin,
@@ -81,19 +79,12 @@
         except IndexError:
             # Unknown language code.
             raise NotFoundError
- distroserieslang = self.context.getDistroSeriesLanguage(lang)

- if distroserieslang is None:
- # There is no IDistroSeriesLanguage yet for this IDistroSeries,
- # but we still need to list it as an available language, so we
- # generate a dummy one so users have a chance to get to it in the
- # navigation and start adding translations for it.
- distroserieslangset = getUtility(IDistroSeriesLanguageSet)
- distroserieslang = distroserieslangset.getDummy(
- self.context, lang)
+ distroserieslang = self.context.getDistroSeriesLanguageOrDummy(lang)

         # Check if user is able to view the translations for
- # this distribution series
+ # this distribution series language.
+ # If not, raise TranslationUnavailable.
         check_distroseries_translations_viewable(self.context)

         return distroserieslang

=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py 2010-02-17 13:06:35 +0000
+++ lib/lp/registry/browser/sourcepackage.py 2010-02-24 14:55:26 +0000
@@ -40,7 +40,7 @@
 from lp.registry.interfaces.packaging import IPackaging
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.sourcepackage import ISourcePackage
-from lp.translations.browser.browser_helpers import (
+from lp.translations.browser.distroseries import (
     check_distroseries_translations_viewable)
 from lp.translations.interfaces.potemplate import IPOTemplateSet
 from canonical.launchpad import _
@@ -68,7 +68,8 @@

         # If we are able to view the translations for distribution series
         # we should also be allowed to see them for a distribution
- # source package
+ # source package.
+ # If not, raise TranslationUnavailable.
         check_distroseries_translations_viewable(self.context.distroseries)

         return sourcepackage_pots

=== modified file 'lib/lp/registry/doc/distroseries.t...

Revision history for this message
Henning Eggers (henninge) wrote :

Thanks, this looks much better. Unfortunately the diff shows conflicts, so I guess you need to merge devel and push again for the diff to be correct.

Also, is there no unit test that could go through all the different SeriesStatus values? Doing this in a doc test looks like it's in the wrong place. But please only do this if it is fairly easy. I think this branch is already old enough ... ;-)

Revision history for this message
Adi Roiban (adiroiban) wrote :

În data de Mi, 24-02-2010 la 17:42 +0000, Henning Eggers a scris:
> Thanks, this looks much better. Unfortunately the diff shows
> conflicts, so I guess you need to merge devel and push again for the
> diff to be correct.
Conflict solved.

> Also, is there no unit test that could go through all the different
> SeriesStatus values? Doing this in a doc test looks like it's in the
> wrong place. But please only do this if it is fairly easy. I think
> this branch is already old enough ... ;-)
I was thinking that view unit tests should be put in:
lib/lp/<application>/browser/tests

translations/browser/tests/distroseries-views.txt was already there and
I have just moved those tests from registry/doc/distroseries.txt to this
file.

Are you saying that we should not write unit tests using doctest format?

Cheers

--
Adi Roiban

Revision history for this message
Henning Eggers (henninge) wrote :

> Conflict solved.

Cool, looks good. ;)

> Are you saying that we should not write unit tests using doctest format?

Yes, at least that's what I do. They execute slower, don't they? But as you pointed out, the test was already there, so you just extended it. That's what I do, too ... ;) So you can keep it as it is if you want to.

Thanks for the extra comments in the code, btw. And all the clean-up work!
You have a double "allowed allowed" in there somewhere, btw.

Cheers,
Henning

review: Approve (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

În data de Mi, 24-02-2010 la 18:27 +0000, Henning Eggers a scris:
> Review: Approve code
> > Conflict solved.
>
> Cool, looks good. ;)
>
>
> > Are you saying that we should not write unit tests using doctest format?
>
> Yes, at least that's what I do. They execute slower, don't they? But
> as you pointed out, the test was already there, so you just extended
> it. That's what I do, too ... ;) So you can keep it as it is if you
> want to.
OK. This is not the only unit test written in doctest format.

$ ls lib/lp/translations/browser/tests/*.txt
lib/lp/translations/browser/tests/distroseries-views.txt
lib/lp/translations/browser/tests/language-views.txt
lib/lp/translations/browser/tests/poexport-request-views.txt
lib/lp/translations/browser/tests/pofile-base-views.txt
lib/lp/translations/browser/tests/pofile-views.txt
lib/lp/translations/browser/tests/potemplate-views.txt
lib/lp/translations/browser/tests/productseries-views.txt
lib/lp/translations/browser/tests/translationimportqueue-views.txt
lib/lp/translations/browser/tests/translationmessage-views.txt
lib/lp/translations/browser/tests/translator-views.txt

Maybe we should open a bug to convert all those test from doctest to
"pure" python code.

> Thanks for the extra comments in the code, btw. And all the clean-up
> work!
> You have a double "allowed allowed" in there somewhere, btw.

Typo fixed and pushed.

When you have time, can you please send this branch to ec2 test?

Kindest regards,

--
Adi Roiban

Revision history for this message
Henning Eggers (henninge) wrote :

I am having trouble landing this. First, there is the current problem with ec2 test not sending emails, so the failure was kind of obscure. Secondly, after merging the current devel, it failed because "python-zope.interface" needed to be installed. You will have to do that, too!

Now, this error remains when running "make build":

    ZopeXMLConfigurationError: File "/home/henning/canonical/lp-branches/adiroiban-bug-509252-take-2/lib/lp/registry/configure.zcml", line 172.4-176.34
    ImportError: cannot import name check_distroseries_translations_viewable

Revision history for this message
Adi Roiban (adiroiban) wrote :

Hm...

I did not get the „python-zope.interface” error but got the circular import error.

I have fixed the problem and pushed the branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-02-24 23:02:56 +0000
3+++ lib/canonical/launchpad/security.py 2010-03-01 23:03:20 +0000
4@@ -49,8 +49,6 @@
5 from lp.registry.interfaces.distributionsourcepackage import (
6 IDistributionSourcePackage)
7 from lp.registry.interfaces.distroseries import IDistroSeries
8-from lp.translations.interfaces.distroserieslanguage import (
9- IDistroSeriesLanguage)
10 from lp.registry.interfaces.entitlement import IEntitlement
11 from lp.hardwaredb.interfaces.hwdb import (
12 IHWDBApplication, IHWDevice, IHWDeviceClass, IHWDriver, IHWDriverName,
13@@ -1664,19 +1662,6 @@
14 self.obj.distribution).checkAuthenticated(user))
15
16
17-class AdminDistroSeriesLanguage(AuthorizationBase):
18- permission = 'launchpad.TranslationsAdmin'
19- usedfor = IDistroSeriesLanguage
20-
21- def checkAuthenticated(self, user):
22- """Is the user able to manage `IDistroSeriesLanguage` translations.
23-
24- Distribution managers can also manage IDistroSeriesLanguage
25- """
26- return (AdminDistroSeriesTranslations(
27- self.obj.distroseries).checkAuthenticated(user))
28-
29-
30 class BranchSubscriptionEdit(AuthorizationBase):
31 permission = 'launchpad.Edit'
32 usedfor = IBranchSubscription
33
34=== modified file 'lib/lp/registry/browser/distroseries.py'
35--- lib/lp/registry/browser/distroseries.py 2010-02-26 19:15:17 +0000
36+++ lib/lp/registry/browser/distroseries.py 2010-03-01 23:03:20 +0000
37@@ -37,8 +37,8 @@
38 from lp.services.worlddata.interfaces.country import ICountry
39 from lp.registry.interfaces.series import SeriesStatus
40 from lp.registry.interfaces.distroseries import IDistroSeries
41-from lp.translations.interfaces.distroserieslanguage import (
42- IDistroSeriesLanguageSet)
43+from lp.translations.browser.distroseries import (
44+ check_distroseries_translations_viewable)
45 from lp.services.worlddata.interfaces.language import ILanguageSet
46 from lp.registry.browser.structuralsubscription import (
47 StructuralSubscriptionMenuMixin,
48@@ -48,7 +48,6 @@
49 from canonical.launchpad.webapp import (
50 StandardLaunchpadFacets, GetitemNavigation, action, custom_widget)
51 from canonical.launchpad.webapp.batching import BatchNavigator
52-from canonical.launchpad.webapp.authorization import check_permission
53 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
54 from canonical.launchpad.webapp.launchpadform import (
55 LaunchpadEditFormView, LaunchpadFormView)
56@@ -80,20 +79,13 @@
57 except IndexError:
58 # Unknown language code.
59 raise NotFoundError
60- distroserieslang = self.context.getDistroSeriesLanguage(lang)
61-
62- if distroserieslang is None:
63- # There is no IDistroSeriesLanguage yet for this IDistroSeries,
64- # but we still need to list it as an available language, so we
65- # generate a dummy one so users have a chance to get to it in the
66- # navigation and start adding translations for it.
67- distroserieslangset = getUtility(IDistroSeriesLanguageSet)
68- distroserieslang = distroserieslangset.getDummy(
69- self.context, lang)
70-
71- if not check_permission(
72- 'launchpad.TranslationsAdmin', distroserieslang):
73- self.context.checkTranslationsViewable()
74+
75+ distroserieslang = self.context.getDistroSeriesLanguageOrDummy(lang)
76+
77+ # Check if user is able to view the translations for
78+ # this distribution series language.
79+ # If not, raise TranslationUnavailable.
80+ check_distroseries_translations_viewable(self.context)
81
82 return distroserieslang
83
84
85=== modified file 'lib/lp/registry/browser/sourcepackage.py'
86--- lib/lp/registry/browser/sourcepackage.py 2010-02-23 19:43:58 +0000
87+++ lib/lp/registry/browser/sourcepackage.py 2010-03-01 23:03:20 +0000
88@@ -52,7 +52,6 @@
89 action, ApplicationMenu, custom_widget, GetitemNavigation,
90 LaunchpadFormView, Link, redirection, StandardLaunchpadFacets, stepto)
91 from canonical.launchpad.webapp import canonical_url
92-from canonical.launchpad.webapp.authorization import check_permission
93 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
94 from canonical.launchpad.webapp.menu import structured
95
96@@ -70,9 +69,13 @@
97 distroseries=self.context.distroseries,
98 sourcepackagename=self.context.sourcepackagename)
99
100- if not check_permission(
101- 'launchpad.TranslationsAdmin', sourcepackage_pots):
102- self.context.distroseries.checkTranslationsViewable()
103+ # If we are able to view the translations for distribution series
104+ # we should also be allowed to see them for a distribution
105+ # source package.
106+ # If not, raise TranslationUnavailable.
107+ from lp.translations.browser.distroseries import (
108+ check_distroseries_translations_viewable)
109+ check_distroseries_translations_viewable(self.context.distroseries)
110
111 return sourcepackage_pots
112
113@@ -92,6 +95,7 @@
114
115 class SourcePackageBreadcrumb(Breadcrumb):
116 """Builds a breadcrumb for an `ISourcePackage`."""
117+
118 @property
119 def text(self):
120 return smartquote('"%s" source package') % (self.context.name)
121
122=== modified file 'lib/lp/registry/doc/distroseries.txt'
123--- lib/lp/registry/doc/distroseries.txt 2010-02-25 16:27:34 +0000
124+++ lib/lp/registry/doc/distroseries.txt 2010-03-01 23:03:20 +0000
125@@ -1,4 +1,5 @@
126-= DistroSeries =
127+DistroSeries
128+============
129
130 From the DerivationOverview spec
131 <https://launchpad.canonical.com/DerivationOverview>:
132@@ -200,7 +201,8 @@
133 True
134
135
136-== Package searching ==
137+Package searching
138+-----------------
139
140 You can search through binary packages publishing in a distribution
141 release by using the searchPackages method, which uses magical fti:
142@@ -223,7 +225,8 @@
143 DistroSeriesBinaryPackage: at
144
145
146-== DistroSeriess have components and sections ==
147+DistroSeriess have components and sections
148+------------------------------------------
149
150 A distroseries has some number of components and/or sections which
151 are valid for that distroseries. These selections are used by (among
152@@ -286,7 +289,8 @@
153 partner
154
155
156-== DistroSeries can be initialised from their parents ==
157+DistroSeries can be initialised from their parents
158+--------------------------------------------------
159
160 When a distroseries is derived from another distroseries (be it a
161 derivative distribution, or simply the next release in a sequence from
162@@ -396,74 +400,8 @@
163 netapplet
164
165
166-Hiding translations
167--------------------
168-
169-The hide_all_translations flag, if set, hides a distroseries'
170-translations in the user interface. The check for visibility happens in
171-checkTranslationsViewable.
172-
173- >>> untranslatable_series = factory.makeDistroRelease()
174- >>> untranslatable_series.hide_all_translations = False
175-
176-When translations are visible, checkTranslationsViewable() completes
177-normally.
178-
179- >>> from canonical.launchpad.webapp.interfaces import (
180- ... TranslationUnavailable)
181- >>> def get_visibility_notice(series):
182- ... """Print the notice about series' translations being hidden."""
183- ... try:
184- ... series.checkTranslationsViewable()
185- ... except TranslationUnavailable, e:
186- ... return str(e)
187- ... return None
188-
189- >>> print get_visibility_notice(untranslatable_series)
190- None
191-
192-But when translations are hidden, it raises TranslationUnavailable.
193-
194- >>> untranslatable_series.hide_all_translations = True
195- >>> print get_visibility_notice(untranslatable_series)
196- Translations for this release series...
197-
198-Exactly what message is displayed depends on the series' status.
199-
200- >>> untranslatable_series.status = SeriesStatus.EXPERIMENTAL
201- >>> print get_visibility_notice(untranslatable_series)
202- Translations for this release series are not available yet.
203-
204- >>> untranslatable_series.status = SeriesStatus.DEVELOPMENT
205- >>> print get_visibility_notice(untranslatable_series)
206- Translations for this release series are not available yet.
207-
208- >>> untranslatable_series.status = SeriesStatus.FROZEN
209- >>> print get_visibility_notice(untranslatable_series)
210- Translations for this release series are not currently available.
211- Please come back soon.
212-
213- >>> untranslatable_series.status = SeriesStatus.CURRENT
214- >>> print get_visibility_notice(untranslatable_series)
215- Translations for this release series are not currently available.
216- Please come back soon.
217-
218- >>> untranslatable_series.status = SeriesStatus.SUPPORTED
219- >>> print get_visibility_notice(untranslatable_series)
220- Translations for this release series are not currently available.
221- Please come back soon.
222-
223- >>> untranslatable_series.status = SeriesStatus.OBSOLETE
224- >>> print get_visibility_notice(untranslatable_series)
225- This release series is obsolete. Its translations are no longer
226- available.
227-
228- >>> untranslatable_series.status = SeriesStatus.FUTURE
229- >>> print get_visibility_notice(untranslatable_series)
230- Translations for this release series are not available yet.
231-
232-
233-== Translatable Packages and Packaging ==
234+Translatable Packages and Packaging
235+-----------------------------------
236
237 You can easily find out what packages are translatable in a
238 distribution release:
239@@ -668,7 +606,8 @@
240 True
241
242
243-== SourcePackagePublishingHistory ==
244+SourcePackagePublishingHistory
245+------------------------------
246
247 IDistroSeries.getSourcePackagePublishing returns all the ISPPH
248 records for a given status in a given pocket. It makes easy to
249@@ -1042,7 +981,8 @@
250 thinclient-local-devices
251
252
253-= Drivers =
254+Drivers
255+=======
256
257 Distributions have drivers, who are people that have permission to approve
258 bugs and features for specific releases. The rules are that:
259@@ -1124,7 +1064,8 @@
260 mark
261
262
263-== Lastest Uploads ==
264+Lastest Uploads
265+---------------
266
267 IDistroSeries provides the 'getLatestUpload' method which returns a
268 list of the last 5 (five) IDistroSeriesSourcePackageRelease (IDRSPR)
269@@ -1150,7 +1091,8 @@
270 0
271
272
273-== Getting build records for a distro series ==
274+Getting build records for a distro series
275+-----------------------------------------
276
277 IDistroSeries inherits the IHasBuildRecords interfaces and therefore provides
278 a getBuildRecords() method.
279
280=== modified file 'lib/lp/registry/interfaces/distroseries.py'
281--- lib/lp/registry/interfaces/distroseries.py 2010-02-27 10:19:18 +0000
282+++ lib/lp/registry/interfaces/distroseries.py 2010-03-01 23:03:20 +0000
283@@ -54,6 +54,7 @@
284 from lp.translations.interfaces.languagepack import ILanguagePack
285
286
287+
288 class DistroSeriesNameField(ContentNameField):
289 """A class to ensure `IDistroSeries` has unique names."""
290 errormessage = _("%s is already in use by another series.")
291@@ -132,6 +133,7 @@
292
293 class IDistroSeriesEditRestricted(Interface):
294 """IDistroSeries properties which require launchpad.Edit."""
295+
296 @rename_parameters_as(dateexpected='date_targeted')
297 @export_factory_operation(
298 IMilestone, ['name', 'dateexpected', 'summary', 'code_name'])
299@@ -198,7 +200,7 @@
300 Interface, # Really IDistribution, see circular import fix below.
301 title=_("Distribution"), required=True,
302 description=_("The distribution for which this is a series.")))
303- named_version = Attribute('The combined display name and version.')
304+ named_version = Attribute('The combined display name and version.')
305 parent = Attribute('The structural parent of this series - the distro')
306 components = Attribute("The series components.")
307 upload_components = Attribute("The series components that can be "
308@@ -240,7 +242,7 @@
309 title=_("Defer translation imports"),
310 description=_("Suspends any translation imports for this series"),
311 default=True,
312- required=True
313+ required=True,
314 )
315 binarycount = Attribute("Binary Packages Counter")
316
317@@ -497,17 +499,6 @@
318 :return: A result set containing `IPackageUpload`
319 """
320
321- def checkTranslationsViewable():
322- """Raise `TranslationUnavailable` if translations are hidden.
323-
324- Checks the `hide_all_translations` flag. If it is set, these
325- translations are not to be shown to the public. In that case an
326- appropriate message is composed based on the series' `status`,
327- and a `TranslationUnavailable` exception is raised.
328-
329- Simply returns if translations are not hidden.
330- """
331-
332 def getUnlinkedTranslatableSourcePackages():
333 """Return a list of source packages that can be translated in
334 this distribution series but which lack Packaging links.
335
336=== modified file 'lib/lp/registry/model/distroseries.py'
337--- lib/lp/registry/model/distroseries.py 2010-02-25 16:27:34 +0000
338+++ lib/lp/registry/model/distroseries.py 2010-03-01 23:03:20 +0000
339@@ -118,8 +118,7 @@
340 from canonical.launchpad.mail import signed_message_from_string
341 from lp.registry.interfaces.person import validate_public_person
342 from canonical.launchpad.webapp.interfaces import (
343- IStoreSelector, MAIN_STORE, NotFoundError, SLAVE_FLAVOR,
344- TranslationUnavailable)
345+ IStoreSelector, MAIN_STORE, NotFoundError, SLAVE_FLAVOR)
346 from lp.soyuz.interfaces.sourcepackageformat import (
347 ISourcePackageFormatSelectionSet)
348
349@@ -134,7 +133,7 @@
350 SeriesStatus.DEVELOPMENT,
351 SeriesStatus.FROZEN,
352 SeriesStatus.CURRENT,
353- SeriesStatus.SUPPORTED
354+ SeriesStatus.SUPPORTED,
355 ]
356
357
358@@ -162,7 +161,7 @@
359 dbName='releasestatus', notNull=True, schema=SeriesStatus)
360 date_created = UtcDateTimeCol(notNull=False, default=UTC_NOW)
361 datereleased = UtcDateTimeCol(notNull=False, default=None)
362- parent_series = ForeignKey(
363+ parent_series = ForeignKey(
364 dbName='parent_series', foreignKey='DistroSeries', notNull=False)
365 owner = ForeignKey(
366 dbName='owner', foreignKey='Person',
367@@ -173,7 +172,7 @@
368 lucilleconfig = StringCol(notNull=False, default=None)
369 changeslist = StringCol(notNull=False, default=None)
370 nominatedarchindep = ForeignKey(
371- dbName='nominatedarchindep',foreignKey='DistroArchSeries',
372+ dbName='nominatedarchindep', foreignKey='DistroArchSeries',
373 notNull=False, default=None)
374 messagecount = IntCol(notNull=True, default=0)
375 binarycount = IntCol(notNull=True, default=DEFAULT)
376@@ -751,7 +750,7 @@
377
378 # filter based on completion. see the implementation of
379 # Specification.is_complete() for more details
380- completeness = Specification.completeness_clause
381+ completeness = Specification.completeness_clause
382
383 if SpecificationFilter.COMPLETE in filter:
384 query += ' AND ( %s ) ' % completeness
385@@ -886,29 +885,6 @@
386 DistroSeriesSourcePackageRelease(self, release))
387 for release in releases)
388
389- def checkTranslationsViewable(self):
390- """See `IDistroSeries`."""
391- if not self.hide_all_translations:
392- # Yup, viewable.
393- return
394-
395- future = [
396- SeriesStatus.EXPERIMENTAL,
397- SeriesStatus.DEVELOPMENT,
398- SeriesStatus.FUTURE,
399- ]
400- if self.status in future:
401- raise TranslationUnavailable(
402- "Translations for this release series are not available yet.")
403- elif self.status == SeriesStatus.OBSOLETE:
404- raise TranslationUnavailable(
405- "This release series is obsolete. Its translations are no "
406- "longer available.")
407- else:
408- raise TranslationUnavailable(
409- "Translations for this release series are not currently "
410- "available. Please come back soon.")
411-
412 def getTranslatableSourcePackages(self):
413 """See `IDistroSeries`."""
414 query = """
415@@ -1069,13 +1045,12 @@
416 SourcePackagePublishingHistory.archive IN %s AND
417 SourcePackagePublishingHistory.status=%s AND
418 SourcePackagePublishingHistory.pocket=%s
419- """ % sqlvalues(self, archives, status, pocket)
420+ """ % sqlvalues(self, archives, status, pocket)
421
422 if component:
423 clause += (
424 " AND SourcePackagePublishingHistory.component=%s"
425- % sqlvalues(component)
426- )
427+ % sqlvalues(component))
428
429 orderBy = ['SourcePackageName.name']
430 clauseTables = ['SourcePackageRelease', 'SourcePackageName']
431@@ -1136,7 +1111,7 @@
432
433 clauseTables = ['BinaryPackagePublishingHistory', 'DistroArchSeries',
434 'BinaryPackageRelease', 'BinaryPackageName', 'Build',
435- 'SourcePackageRelease', 'SourcePackageName' ]
436+ 'SourcePackageRelease', 'SourcePackageName']
437
438 result = BinaryPackagePublishingHistory.select(
439 query, distinct=False, clauseTables=clauseTables, orderBy=orderBy)
440@@ -1971,8 +1946,9 @@
441 return '%s%s' % (self.name, pocketsuffix[pocket])
442
443 def isSourcePackageFormatPermitted(self, format):
444- return getUtility(ISourcePackageFormatSelectionSet
445- ).getBySeriesAndFormat(self, format) is not None
446+ return getUtility(
447+ ISourcePackageFormatSelectionSet).getBySeriesAndFormat(
448+ self, format) is not None
449
450
451 class DistroSeriesSet:
452@@ -1990,8 +1966,7 @@
453 result_set = store.using((DistroSeries, POTemplate)).find(
454 DistroSeries,
455 DistroSeries.hide_all_translations == False,
456- DistroSeries.id == POTemplate.distroseriesID
457- ).config(distinct=True)
458+ DistroSeries.id == POTemplate.distroseriesID).config(distinct=True)
459 # XXX: henninge 2009-02-11 bug=217644: Convert to sequence right here
460 # because ResultSet reports a wrong count() when using DISTINCT. Also
461 # ResultSet does not implement __len__(), which would make it more
462
463=== modified file 'lib/lp/translations/browser/browser_helpers.py'
464--- lib/lp/translations/browser/browser_helpers.py 2009-09-04 11:54:50 +0000
465+++ lib/lp/translations/browser/browser_helpers.py 2010-03-01 23:03:20 +0000
466@@ -30,16 +30,16 @@
467 """Replace Rosetta escape sequences with the real characters."""
468 return helpers.text_replaced(text, {'[tab]': '\t',
469 r'\[tab]': '[tab]',
470- '[nbsp]' : u'\u00a0',
471- r'\[nbsp]' : '[nbsp]' })
472+ '[nbsp]': u'\u00a0',
473+ r'\[nbsp]': '[nbsp]'})
474
475
476 def expand_rosetta_escapes(unicode_text):
477 """Replace characters needing a Rosetta escape sequences."""
478 escapes = {u'\t': TranslationConstants.TAB_CHAR,
479 u'[tab]': TranslationConstants.TAB_CHAR_ESCAPED,
480- u'\u00a0' : TranslationConstants.NO_BREAK_SPACE_CHAR,
481- u'[nbsp]' : TranslationConstants.NO_BREAK_SPACE_CHAR_ESCAPED }
482+ u'\u00a0': TranslationConstants.NO_BREAK_SPACE_CHAR,
483+ u'[nbsp]': TranslationConstants.NO_BREAK_SPACE_CHAR_ESCAPED}
484 return helpers.text_replaced(unicode_text, escapes)
485
486
487@@ -187,5 +187,3 @@
488 raise UnrecognisedCFormatString(string)
489
490 return segments
491-
492-
493
494=== modified file 'lib/lp/translations/browser/distroseries.py'
495--- lib/lp/translations/browser/distroseries.py 2010-02-02 15:32:00 +0000
496+++ lib/lp/translations/browser/distroseries.py 2010-03-01 23:03:20 +0000
497@@ -11,15 +11,16 @@
498 'DistroSeriesTranslationsAdminView',
499 'DistroSeriesTranslationsMenu',
500 'DistroSeriesView',
501+ 'check_distroseries_translations_viewable',
502 ]
503
504 from zope.component import getUtility
505
506 from canonical.cachedproperty import cachedproperty
507 from canonical.launchpad import helpers
508-from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
509 from canonical.launchpad.webapp import action
510 from canonical.launchpad.webapp.authorization import check_permission
511+from canonical.launchpad.webapp.interfaces import TranslationUnavailable
512 from canonical.launchpad.webapp.launchpadform import LaunchpadEditFormView
513 from canonical.launchpad.webapp.menu import (
514 Link, NavigationMenu, enabled_with_permission)
515@@ -27,6 +28,7 @@
516 canonical_url, LaunchpadView)
517
518 from lp.registry.interfaces.distroseries import IDistroSeries
519+from lp.registry.interfaces.series import SeriesStatus
520 from lp.translations.browser.translations import TranslationsMixin
521 from lp.translations.browser.potemplate import BaseSeriesTemplatesView
522 from lp.translations.interfaces.distroserieslanguage import (
523@@ -175,34 +177,11 @@
524 self.context.version)
525
526 def checkTranslationsViewable(self):
527- """Check that these translations are visible to the current user.
528-
529- Launchpad admins, Translations admins, and users with admin
530- rights on the `DistroSeries` are always allowed. For others
531- this delegates to `IDistroSeries.checkTranslationsViewable`,
532- which raises `TranslationUnavailable` if the translations are
533- set to be hidden.
534-
535- :return: Returns normally if this series' translations are
536- viewable to the current user.
537- :raise TranslationUnavailable: if this series' translations are
538- hidden and the user is not one of the limited caste that is
539- allowed to access them.
540- """
541- if check_permission('launchpad.TranslationsAdmin', self.context):
542- # Anyone with admin rights on this series passes. This
543- # includes Launchpad admins.
544- return
545-
546- user = self.user
547- experts = getUtility(ILaunchpadCelebrities).rosetta_experts
548- if user is not None and user.inTeam(experts):
549- # Translations admins also pass.
550- return
551-
552- # Everyone else passes only if translations are viewable to the
553- # public.
554- self.context.checkTranslationsViewable()
555+ """ Check if user can view translations for this `IDistroSeries`"""
556+
557+ # Is user allowed to see translations for this distroseries?
558+ # If not, raise TranslationUnavailable.
559+ check_distroseries_translations_viewable(self.context)
560
561 def distroserieslanguages(self):
562 """Produces a list containing a DistroSeriesLanguage object for
563@@ -271,3 +250,47 @@
564
565 def language_packs(self):
566 return Link('+language-packs', 'Language packs')
567+
568+
569+def check_distroseries_translations_viewable(distroseries):
570+ """Check that these distribution series translations are visible.
571+
572+ Launchpad admins, Translations admins, and users with admin
573+ rights on the `IDistroSeries` are always allowed.
574+
575+ Checks the `hide_all_translations` flag. If it is set, these
576+ translations are not to be shown to the public. In that case an
577+ appropriate message is composed based on the series' `status`,
578+ and a `TranslationUnavailable` exception is raised.
579+
580+ :return: Returns normally if this series' translations are
581+ viewable to the current user.
582+ :raise TranslationUnavailable: if this series' translations are
583+ hidden and the user is not one of the limited caste that is
584+ allowed to access them.
585+ """
586+
587+ if not distroseries.hide_all_translations:
588+ # Yup, viewable.
589+ return
590+
591+ if check_permission(
592+ 'launchpad.TranslationsAdmin', distroseries):
593+ return
594+
595+ future = [
596+ SeriesStatus.EXPERIMENTAL,
597+ SeriesStatus.DEVELOPMENT,
598+ SeriesStatus.FUTURE,
599+ ]
600+ if distroseries.status in future:
601+ raise TranslationUnavailable(
602+ "Translations for this release series are not available yet.")
603+ elif distroseries.status == SeriesStatus.OBSOLETE:
604+ raise TranslationUnavailable(
605+ "This release series is obsolete. Its translations are no "
606+ "longer available.")
607+ else:
608+ raise TranslationUnavailable(
609+ "Translations for this release series are not currently "
610+ "available. Please come back soon.")
611
612=== modified file 'lib/lp/translations/browser/tests/distroseries-views.txt'
613--- lib/lp/translations/browser/tests/distroseries-views.txt 2009-12-13 11:55:40 +0000
614+++ lib/lp/translations/browser/tests/distroseries-views.txt 2010-03-01 23:03:20 +0000
615@@ -1,9 +1,9 @@
616-= DistroSeries translations view classes =
617+DistroSeries translations view classes
618+======================================
619+
620+Let's use ubuntu/hoary for these tests.
621
622 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
623-
624-Let's use ubuntu/hoary for these tests.
625-
626 >>> from canonical.launchpad.interfaces import IDistributionSet
627 >>> from lp.registry.interfaces.series import SeriesStatus
628 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
629@@ -15,7 +15,9 @@
630 >>> login('foo.bar@canonical.com')
631 >>> hoary.status = SeriesStatus.CURRENT
632
633-== Hiding translations ==
634+
635+Hiding translations
636+-------------------
637
638 Each distroseries has a switch that allows administrators to either
639 reveal its translations to the public or hide them from the public.
640@@ -95,9 +97,65 @@
641 release series are not currently available. Please come back soon.
642
643 The same goes for anonymous users.
644-
645- >>> login(ANONYMOUS)
646- >>> check_effect_of_hiding(hoary)
647- User can access revealed translations.
648- User can not access hidden translations: Translations for this
649- release series are not currently available. Please come back soon.
650+Exactly what message is displayed depends on the series' status.
651+
652+ >>> login('foo.bar@canonical.com')
653+ >>> hoary.status = SeriesStatus.EXPERIMENTAL
654+ >>> login(ANONYMOUS)
655+ >>> check_effect_of_hiding(hoary)
656+ User can access revealed translations.
657+ User can not access hidden translations:
658+ Translations for this release series are not available yet.
659+
660+ >>> login('foo.bar@canonical.com')
661+ >>> hoary.status = SeriesStatus.DEVELOPMENT
662+ >>> login(ANONYMOUS)
663+ >>> check_effect_of_hiding(hoary)
664+ User can access revealed translations.
665+ User can not access hidden translations:
666+ Translations for this release series are not available yet.
667+
668+ >>> login('foo.bar@canonical.com')
669+ >>> hoary.status = SeriesStatus.FROZEN
670+ >>> login(ANONYMOUS)
671+ >>> check_effect_of_hiding(hoary)
672+ User can access revealed translations.
673+ User can not access hidden translations:
674+ Translations for this release series are not currently available.
675+ Please come back soon.
676+
677+ >>> login('foo.bar@canonical.com')
678+ >>> hoary.status = SeriesStatus.CURRENT
679+ >>> login(ANONYMOUS)
680+ >>> check_effect_of_hiding(hoary)
681+ User can access revealed translations.
682+ User can not access hidden translations:
683+ Translations for this release series are not currently available.
684+ Please come back soon.
685+
686+ >>> login('foo.bar@canonical.com')
687+ >>> hoary.status = SeriesStatus.SUPPORTED
688+ >>> login(ANONYMOUS)
689+ >>> check_effect_of_hiding(hoary)
690+ User can access revealed translations.
691+ User can not access hidden translations:
692+ Translations for this release series are not currently available.
693+ Please come back soon.
694+
695+ >>> login('foo.bar@canonical.com')
696+ >>> hoary.status = SeriesStatus.OBSOLETE
697+ >>> login(ANONYMOUS)
698+ >>> check_effect_of_hiding(hoary)
699+ User can access revealed translations.
700+ User can not access hidden translations:
701+ This release series is obsolete. Its translations are no longer
702+ available.
703+
704+ >>> login('foo.bar@canonical.com')
705+ >>> hoary.status = SeriesStatus.FUTURE
706+ >>> login(ANONYMOUS)
707+ >>> check_effect_of_hiding(hoary)
708+ User can access revealed translations.
709+ User can not access hidden translations:
710+ Translations for this release series are not available yet.
711+
712
713=== modified file 'lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt'
714--- lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt 2009-12-21 17:18:12 +0000
715+++ lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt 2010-03-01 23:03:20 +0000
716@@ -46,9 +46,7 @@
717 ... but the link is available to administrators:
718
719 >>> dtc_browser = setupDTCBrowser()
720-
721 >>> dtc_browser.open('http://translations.launchpad.dev/ubuntu/hoary')
722-
723 >>> dtc_browser.getLink('Change settings').click()
724
725 Once the administrator hides all translations...
726@@ -115,8 +113,8 @@
727 non-administrative users.
728
729 >>> user_browser.open(
730- ... 'http://translations.launchpad.dev/ubuntu/hoary/+sources/evolution/'
731- ... '+pots/evolution-2.2')
732+ ... 'http://translations.launchpad.dev/ubuntu/hoary/'
733+ ... '+sources/evolution/+pots/evolution-2.2')
734 Traceback (most recent call last):
735 ...
736 TranslationUnavailable: ...
737@@ -126,7 +124,7 @@
738
739 >>> dtc_browser.open(
740 ... 'http://translations.launchpad.dev/ubuntu/hoary/'
741- ... '+sources/evolution')
742+ ... '+sources/evolution/+pots/evolution-2.2')
743
744 There is also an option to set/unset whether translation imports for a
745 distribution should be deferred. That option is set also from the same
746
747=== modified file 'lib/lp/translations/templates/distroseries-langchart.pt'
748--- lib/lp/translations/templates/distroseries-langchart.pt 2009-12-27 20:58:33 +0000
749+++ lib/lp/translations/templates/distroseries-langchart.pt 2010-03-01 23:03:20 +0000
750@@ -8,6 +8,11 @@
751 <strong>Translations for this series are currently hidden.</strong>
752
753 <!-- Bounce regular users to "translations unavailable" page. -->
754+ <tal:XXX condition="nothing">
755+ 20100224 adiroiban bug=527069: The following tal:omit-tag is only
756+ used with the sole purpose of raising an exception.
757+ There should be a better solution for this problem.
758+ </tal:XXX>
759 <metal:check-available tal:omit-tag="view/checkTranslationsViewable" />
760 </p>
761