Merge lp:~ursinha/launchpad/bug422056-add-translation-focus into lp:launchpad/db-devel

Proposed by Ursula Junque
Status: Merged
Approved by: Edwin Grubbs
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~ursinha/launchpad/bug422056-add-translation-focus
Merge into: lp:launchpad/db-devel
Diff against target: 808 lines (+363/-86)
15 files modified
cronscripts/expire-ppa-files.py (+1/-1)
database/schema/security.cfg (+3/-0)
lib/lp/registry/browser/menu.py (+1/-1)
lib/lp/registry/browser/peoplemerge.py (+4/-4)
lib/lp/registry/browser/tests/peoplemerge-views.txt (+34/-4)
lib/lp/registry/configure.zcml (+2/-1)
lib/lp/registry/interfaces/product.py (+8/-0)
lib/lp/registry/model/product.py (+10/-4)
lib/lp/registry/stories/team/xx-adminteammerge.txt (+43/-34)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+2/-0)
lib/lp/soyuz/scripts/expire_ppa_binaries.py (+57/-2)
lib/lp/soyuz/scripts/tests/test_expire_ppa_bins.py (+70/-33)
lib/lp/translations/browser/product.py (+7/-2)
lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt (+87/-0)
lib/lp/translations/stories/webservice/xx-translationfocus.txt (+34/-0)
To merge this branch: bzr merge lp:~ursinha/launchpad/bug422056-add-translation-focus
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Review via email: mp+17744@code.launchpad.net

Commit message

This branch fixes bug 422056, adding the ability of choosing the translation focus to a product, as it works with distributions now.

To post a comment you must log in.
Revision history for this message
Ursula Junque (ursinha) wrote :
Download full text (4.0 KiB)

= Summary =

This branch fixes bug 422056, adding a way to set translation focus of a product, as we can do with distributions.

I've started another branch, so there's another one that bac reviewed, and his suggestions were applied here.
Here's the old MP: https://code.edge.launchpad.net/~ursinha/launchpad/add-translation-focus/+merge/15520.

== Proposed fix ==

Adding a field with all series of a project on its +changetranslators page.

== Implementation details ==

The implementation is simple: I've added the translation_focus field in product model and interface, and changed the primary_translatable to choose translation_focus if available (and translatable).

== Tests ==

./bin/test -vvt lp.translations.*
./bin/test -vvt lp.registry.*

== Demo and Q/A ==

Preparing:
 1) Go to a project page, such as https://launchpad.dev/alsa-utils
 2) Add another series to the project

A) Choosing non-translatable series as translation focus:
 1) Go to the translations page of the project, https://translations.launchpad.dev/alsa-utils
 2) As an admin user, you'll see the Change permissions option, choose it: https://translations.launchpad.dev/alsa-utils/+changetranslators
 3) Choose the series you just added, and save it
    Result: You should notice the message in the project translations page saying "Launchpad currently recommends translating alsa-utils trunk series", which means that it chose the development focus series because the one chosen as translation focus isn't translatable.

B) Choosing translatable series as translation focus:
 1) Add information to the series you added so it becomes translatable
    Result: You should notice the message in the project translations page saying "Launchpad currently recommends translating <the series you added>", which means that it chose the translation focus now that it's a translatable series.

C) Choosing no translation focus at all:
 1) Choose no series as translation focus
    Result: You should notice that it will recommend to translate the development focus.

= Launchpad lint =

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

Linting changed files:
  lib/lp/registry/configure.zcml
  lib/lp/registry/interfaces/product.py
  lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt
  lib/lp/registry/stories/webservice/xx-project-registry.txt
  lib/lp/translations/browser/product.py
  lib/lp/registry/model/product.py
  lib/lp/translations/stories/webservice/xx-translationfocus.txt

== Pylint notices ==

lib/lp/registry/interfaces/product.py
    34: [F0401] Unable to import 'lazr.enum' (No module named enum)
    72: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    73: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    74: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    778: [C0322, IProductSet.createProduct] Operator not preceded by a space
    freshmeatproject='freshmeat_project', wikiurl='wiki_url',
    ^
    downloadurl='download_url',
    sourceforgeproject='sourceforge_project',
    programminglang='programm...

Read more...

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (7.1 KiB)

Hi Ursula,

This is a nice branch. All my comments are for minor changes.

merge-conditional

-Edwin

>=== modified file 'lib/lp/translations/browser/product.py'
>--- lib/lp/translations/browser/product.py 2010-01-15 14:44:39 +0000
>+++ lib/lp/translations/browser/product.py 2010-01-20 18:38:55 +0000
>@@ -16,7 +16,8 @@
> LaunchpadView, Link, canonical_url, enabled_with_permission)
> from canonical.launchpad.webapp.authorization import check_permission
> from canonical.launchpad.webapp.menu import NavigationMenu
>-from lp.registry.interfaces.product import IProduct, IProductSeries
>+from lp.registry.interfaces.product import IProduct
>+from lp.registry.interfaces.productseries import IProductSeries
> from lp.registry.browser.product import ProductEditView
> from lp.translations.browser.translations import TranslationsMixin
>
>@@ -63,7 +64,8 @@
> class ProductChangeTranslatorsView(TranslationsMixin, ProductEditView):
> label = "Set permissions and policies"
> page_title = "Permissions and policies"
>- field_names = ["translationgroup", "translationpermission"]
>+ field_names = ["translationgroup", "translationpermission",
>+ "translation_focus"]

This does not follow the style guide for multiline lists.
https://dev.launchpad.net/PythonStyleGuide#Multiline braces

> @property
> def cancel_url(self):
>
>=== added directory 'lib/lp/translations/stories/translationfocus'
>=== added file 'lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt'
>--- lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 1970-01-01 00:00:00 +0000
>+++ lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 2010-01-20 18:38:55 +0000
>@@ -0,0 +1,89 @@
>+The translation focus of a product can be explicitly set to a specific series.
>+When not set, launchpad recommends the development focus to translate.
>+
>+ >>> login('<email address hidden>')
>+ >>> fooproject = factory.makeProduct(name="fooproject")
>+ >>> fooproject.official_rosetta = True
>+ >>> fooproject_trunk = fooproject.getSeries("trunk")
>+ >>> fooproject_url = canonical_url(
>+ ... fooproject, rootsite="translations")
>+ >>> logout()
>+
>+Only admin users are able to change the translation focus of a product.
>+Unprivileged users visualize the recommended series for translation,

Whitespace at end of line.
s/visualize/can see/

>+but have no access to the 'Change permissions' menu.
>+
>+ >>> admin_browser.open(fooproject_url)
>+ >>> print extract_text(
>+ ... find_tags_by_class(admin_browser.contents, 'edit sprite')[0])
>+ Change permissions
>+
>+ >>> browser.open(fooproject_url)
>+ >>> print extract_text(
>+ ... find_tags_by_class(browser.contents, 'edit sprite')[0])
>+ Traceback (most recent call last):
>+ ...
>+ IndexError: list index out of range
>+

Whitespace at end of line.

>+
>+== Setting the translation focus ==
>+
>+ >>> login('<email address hidden>')
>+ >>> from zope.security.proxy import removeSecurityProxy
>+ >>> pot_main = factory.makePOTemplate(
>+ ... productseries=fooproject_trunk, name="pot1")
>+ >>> rem...

Read more...

review: Approve (code)
Revision history for this message
Ursula Junque (ursinha) wrote :
Download full text (7.6 KiB)

Hi Edwin!

> Hi Ursula,
>
> This is a nice branch. All my comments are for minor changes.

All fixed and pushed. Thanks!

>
> merge-conditional
>
> -Edwin
>
>
> >=== modified file 'lib/lp/translations/browser/product.py'
> >--- lib/lp/translations/browser/product.py 2010-01-15 14:44:39 +0000
> >+++ lib/lp/translations/browser/product.py 2010-01-20 18:38:55 +0000
> >@@ -16,7 +16,8 @@
> > LaunchpadView, Link, canonical_url, enabled_with_permission)
> > from canonical.launchpad.webapp.authorization import check_permission
> > from canonical.launchpad.webapp.menu import NavigationMenu
> >-from lp.registry.interfaces.product import IProduct, IProductSeries
> >+from lp.registry.interfaces.product import IProduct
> >+from lp.registry.interfaces.productseries import IProductSeries
> > from lp.registry.browser.product import ProductEditView
> > from lp.translations.browser.translations import TranslationsMixin
> >
> >@@ -63,7 +64,8 @@
> > class ProductChangeTranslatorsView(TranslationsMixin, ProductEditView):
> > label = "Set permissions and policies"
> > page_title = "Permissions and policies"
> >- field_names = ["translationgroup", "translationpermission"]
> >+ field_names = ["translationgroup", "translationpermission",
> >+ "translation_focus"]
>
>
> This does not follow the style guide for multiline lists.
> https://dev.launchpad.net/PythonStyleGuide#Multiline braces
>
>
> > @property
> > def cancel_url(self):
> >
> >=== added directory 'lib/lp/translations/stories/translationfocus'
> >=== added file 'lib/lp/translations/stories/translationfocus/xx-product-
> translationfocus.txt'
> >--- lib/lp/translations/stories/translationfocus/xx-product-
> translationfocus.txt 1970-01-01 00:00:00 +0000
> >+++ lib/lp/translations/stories/translationfocus/xx-product-
> translationfocus.txt 2010-01-20 18:38:55 +0000
> >@@ -0,0 +1,89 @@
> >+The translation focus of a product can be explicitly set to a specific
> series.
> >+When not set, launchpad recommends the development focus to translate.
> >+
> >+ >>> login('<email address hidden>')
> >+ >>> fooproject = factory.makeProduct(name="fooproject")
> >+ >>> fooproject.official_rosetta = True
> >+ >>> fooproject_trunk = fooproject.getSeries("trunk")
> >+ >>> fooproject_url = canonical_url(
> >+ ... fooproject, rootsite="translations")
> >+ >>> logout()
> >+
> >+Only admin users are able to change the translation focus of a product.
> >+Unprivileged users visualize the recommended series for translation,
>
>
> Whitespace at end of line.
> s/visualize/can see/
>
>
> >+but have no access to the 'Change permissions' menu.
> >+
> >+ >>> admin_browser.open(fooproject_url)
> >+ >>> print extract_text(
> >+ ... find_tags_by_class(admin_browser.contents, 'edit sprite')[0])
> >+ Change permissions
> >+
> >+ >>> browser.open(fooproject_url)
> >+ >>> print extract_text(
> >+ ... find_tags_by_class(browser.contents, 'edit sprite')[0])
> >+ Traceback (most recent call last):
> >+ ...
> >+ IndexError: list index out of range
> >+
>
>
> Whitespace at end of line.
>
>
>
> >+
> >+== Setting the transla...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== renamed file 'cronscripts/expire-ppa-binaries.py' => 'cronscripts/expire-ppa-files.py'
2--- cronscripts/expire-ppa-binaries.py 2009-10-13 14:38:07 +0000
3+++ cronscripts/expire-ppa-files.py 2010-01-20 20:27:21 +0000
4@@ -17,6 +17,6 @@
5
6 if __name__ == '__main__':
7 script = PPABinaryExpirer(
8- 'expire-ppa-binaries', dbuser=config.binaryfile_expire.dbuser)
9+ 'expire-ppa-files', dbuser=config.binaryfile_expire.dbuser)
10 script.lock_and_run()
11
12
13=== modified file 'database/schema/security.cfg'
14--- database/schema/security.cfg 2010-01-18 09:36:37 +0000
15+++ database/schema/security.cfg 2010-01-20 20:27:21 +0000
16@@ -1621,6 +1621,9 @@
17 public.person = SELECT
18 public.libraryfilealias = SELECT, UPDATE
19 public.securebinarypackagepublishinghistory = SELECT
20+public.sourcepackagereleasefile = SELECT
21+public.sourcepackagepublishinghistory = SELECT
22+public.sourcepackagerelease = SELECT
23
24 [create-merge-proposals]
25 type=user
26
27=== modified file 'lib/lp/registry/browser/menu.py'
28--- lib/lp/registry/browser/menu.py 2009-08-21 20:04:25 +0000
29+++ lib/lp/registry/browser/menu.py 2010-01-20 20:27:21 +0000
30@@ -60,7 +60,7 @@
31 text = 'Merge people'
32 return Link('/people/+adminpeoplemerge', text, icon='edit')
33
34- @enabled_with_permission('launchpad.Admin')
35+ @enabled_with_permission('launchpad.Moderate')
36 def admin_merge_teams(self):
37 text = 'Merge teams'
38 return Link('/people/+adminteammerge', text, icon='edit')
39
40=== modified file 'lib/lp/registry/browser/peoplemerge.py'
41--- lib/lp/registry/browser/peoplemerge.py 2010-01-12 00:19:20 +0000
42+++ lib/lp/registry/browser/peoplemerge.py 2010-01-20 20:27:21 +0000
43@@ -140,15 +140,15 @@
44 from zope.security.proxy import removeSecurityProxy
45 for email in self.dupe_person_emails:
46 email = IMasterObject(email)
47- # XXX: Guilherme Salgado 2007-10-15: Maybe this status change
48- # should be done only when merging people but not when merging
49- # teams.
50- email.status = EmailAddressStatus.NEW
51 # EmailAddress.person and EmailAddress.account are readonly
52 # fields, so we need to remove the security proxy here.
53 naked_email = removeSecurityProxy(email)
54 naked_email.personID = self.target_person.id
55 naked_email.accountID = self.target_person.accountID
56+ # XXX: Guilherme Salgado 2007-10-15: Maybe this status change
57+ # should be done only when merging people but not when merging
58+ # teams.
59+ naked_email.status = EmailAddressStatus.NEW
60 flush_database_updates()
61 getUtility(IPersonSet).merge(self.dupe_person, self.target_person)
62 self.request.response.addInfoNotification(self.merge_message)
63
64=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
65--- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-06 17:52:45 +0000
66+++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-20 20:27:21 +0000
67@@ -6,9 +6,17 @@
68 Team Merges
69 -----------
70
71+Create a member of the registry team that is not a member of the admins
72+team.
73+
74 >>> from lp.registry.interfaces.person import IPersonSet
75+ >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
76+ >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
77 >>> person_set = getUtility(IPersonSet)
78- >>> registry_expert = person_set.getByName('mark')
79+ >>> registry_expert = factory.makePerson()
80+ >>> login_person(registry_experts.teamowner)
81+ >>> ignored = registry_experts.addMember(
82+ ... registry_expert, registry_experts.teamowner)
83 >>> login_person(registry_expert)
84
85 A team (name21) can be merged into another (ubuntu-team).
86@@ -39,6 +47,7 @@
87 >>> parent_team = factory.makeTeam()
88 >>> child_team = factory.makeTeam(name='child-team')
89 >>> random_team = factory.makeTeam()
90+ >>> login('foo.bar@canonical.com')
91 >>> ignored = parent_team.addMember(
92 ... child_team, reviewer=parent_team.teamowner, force_team_add=True)
93 >>> form = {'field.dupe_person': child_team.name,
94@@ -112,10 +121,8 @@
95 Users with launchpad.Moderate such as team admins and registry experts
96 can delete teams.
97
98- >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
99 >>> from canonical.launchpad.webapp.authorization import check_permission
100
101- >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
102 >>> team_owner = factory.makePerson()
103 >>> team_member = factory.makePerson()
104 >>> deletable_team = factory.makeTeam(owner=team_owner, name='deletable')
105@@ -127,7 +134,7 @@
106 >>> check_permission('launchpad.Moderate', view)
107 False
108
109- >>> login_person(registry_experts.teamowner)
110+ >>> login_person(registry_expert)
111 >>> check_permission('launchpad.Moderate', view)
112 True
113
114@@ -241,3 +248,26 @@
115
116 >>> print find_tag_by_id(content, 'field.actions.delete')
117 None
118+
119+The registry experts should be able to delete a team with an
120+validated email address, which will be invisible, since only
121+preferred email addresses are shown for teams.
122+
123+ >>> from canonical.launchpad.interfaces.emailaddress import (
124+ ... IEmailAddressSet)
125+ >>> login_person(registry_expert)
126+ >>> admin = getUtility(ILaunchpadCelebrities).admin
127+ >>> registry_expert.inTeam(admin)
128+ False
129+ >>> deletable_team = factory.makeTeam(email="del@example.com")
130+ >>> deletable_team.setContactAddress(None)
131+ >>> for email in getUtility(IEmailAddressSet).getByPerson(deletable_team):
132+ ... print email.email, email.status.title
133+ del@example.com Validated Email Address
134+ >>> form = {'field.actions.delete': 'Delete'}
135+ >>> view = create_initialized_view(deletable_team, '+delete', form=form)
136+ >>> view.errors
137+ []
138+ >>> for notification in view.request.response.notifications:
139+ ... print notification.message
140+ Team deleted.
141
142=== modified file 'lib/lp/registry/configure.zcml'
143--- lib/lp/registry/configure.zcml 2010-01-04 19:07:58 +0000
144+++ lib/lp/registry/configure.zcml 2010-01-20 20:27:21 +0000
145@@ -1081,7 +1081,8 @@
146 remote_product screenshotsurl
147 security_contact sourceforgeproject
148 summary title translationgroup
149- translationpermission wikiurl"/>
150+ translationpermission translation_focus
151+ wikiurl"/>
152
153 <!-- mark 2006-04-10 I put "name" in the admin group because
154 with Bazaar now in place, lots of people can have personal
155
156=== modified file 'lib/lp/registry/interfaces/product.py'
157--- lib/lp/registry/interfaces/product.py 2009-12-05 18:37:28 +0000
158+++ lib/lp/registry/interfaces/product.py 2010-01-20 20:27:21 +0000
159@@ -586,6 +586,14 @@
160 readonly=True,
161 value_type=Reference(schema=IProductRelease)))
162
163+ translation_focus = exported(
164+ ReferenceChoice(
165+ title=_("Translation Focus"), required=False,
166+ vocabulary='FilteredProductSeries',
167+ schema=IProductSeries,
168+ description=_(
169+ 'The ProductSeries where translations are focused.')))
170+
171 translatable_packages = Attribute(
172 "A list of the source packages for this product that can be "
173 "translated sorted by distroseries.name and sourcepackage.name.")
174
175=== modified file 'lib/lp/registry/model/product.py'
176--- lib/lp/registry/model/product.py 2010-01-10 04:29:39 +0000
177+++ lib/lp/registry/model/product.py 2010-01-20 20:27:21 +0000
178@@ -232,6 +232,9 @@
179 translationpermission = EnumCol(
180 dbName='translationpermission', notNull=True,
181 schema=TranslationPermission, default=TranslationPermission.OPEN)
182+ translation_focus = ForeignKey(
183+ dbName='translation_focus', foreignKey='ProductSeries',
184+ notNull=False, default=None)
185 bugtracker = ForeignKey(
186 foreignKey="BugTracker", dbName="bugtracker", notNull=False,
187 default=None)
188@@ -746,11 +749,14 @@
189 targetseries = ubuntu.currentseries
190 product_series = self.translatable_series
191
192- # First, go with development focus branch
193- if product_series and self.development_focus in product_series:
194- return self.development_focus
195- # Next, go with the latest product series that has templates:
196 if product_series:
197+ # First, go with translation focus
198+ if self.translation_focus in product_series:
199+ return self.translation_focus
200+ # Next, go with development focus
201+ if self.development_focus in product_series:
202+ return self.development_focus
203+ # Next, go with the latest product series that has templates:
204 return product_series[-1]
205 # Otherwise, look for an Ubuntu package in the current distroseries:
206 for package in packages:
207
208=== modified file 'lib/lp/registry/stories/team/xx-adminteammerge.txt'
209--- lib/lp/registry/stories/team/xx-adminteammerge.txt 2009-11-22 15:43:16 +0000
210+++ lib/lp/registry/stories/team/xx-adminteammerge.txt 2010-01-20 20:27:21 +0000
211@@ -4,30 +4,39 @@
212 active members, so the user will first have to confirm that the
213 members should be deactivated before the teams are merged.
214
215- >>> admin_browser.open('http://launchpad.dev/people/+adminteammerge')
216- >>> admin_browser.getControl('Duplicated Team').value = (
217- ... 'landscape-developers')
218- >>> admin_browser.getControl('Target Team').value = 'guadamen'
219- >>> admin_browser.getControl('Merge').click()
220+ >>> from zope.component import getUtility
221+ >>> from lp.registry.interfaces.person import IPersonSet
222+ >>> login('foo.bar@canonical.com')
223+ >>> registry_expert = factory.makePerson(
224+ ... email='reg@example.com', password='test')
225+ >>> new_team = factory.makeTeam(
226+ ... name="new-team", email="new_team@example.com")
227+ >>> registry_experts = getUtility(IPersonSet).getByName('registry')
228+ >>> ignored = registry_experts.addMember(
229+ ... registry_expert, registry_experts.teamowner)
230+ >>> logout()
231+ >>> registry_browser = setupBrowser(auth='Basic reg@example.com:test')
232+ >>> registry_browser.open('http://launchpad.dev/people/+adminteammerge')
233+ >>> registry_browser.getControl('Duplicated Team').value = (
234+ ... 'new-team')
235+ >>> registry_browser.getControl('Target Team').value = 'guadamen'
236+ >>> registry_browser.getControl('Merge').click()
237
238- >>> admin_browser.url
239+ >>> registry_browser.url
240 'http://launchpad.dev/people/+adminteammerge'
241- >>> print get_feedback_messages(admin_browser.contents)[0]
242- Landscape Developers has 2 active members which will have to be
243- deactivated before the teams can be merged.
244+ >>> print get_feedback_messages(registry_browser.contents)[0]
245+ New Team has 1 active members which will have to be deactivated
246+ before the teams can be merged.
247
248- >>> admin_browser.getControl('Deactivate Members and Merge').click()
249- >>> admin_browser.url
250+ >>> registry_browser.getControl('Deactivate Members and Merge').click()
251+ >>> registry_browser.url
252 'http://launchpad.dev/~guadamen'
253
254 >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout
255- >>> from canonical.launchpad.interfaces import IMailingListSet, IPersonSet
256+ >>> from canonical.launchpad.interfaces import IMailingListSet
257 >>> from zope.component import getUtility
258 >>> login(ANONYMOUS)
259- >>> person_set = getUtility(IPersonSet)
260- >>> landscape = person_set.getByName(
261- ... 'landscape-developers-merged', ignore_merged=False)
262- >>> landscape.merged.name
263+ >>> new_team.merged.name
264 u'guadamen'
265 >>> logout()
266
267@@ -36,7 +45,7 @@
268
269 Merged teams are not visible anymore.
270
271- >>> browser.open("http://launchpad.dev/~landscape-developers-merged")
272+ >>> browser.open("http://launchpad.dev/~new-team-merged")
273 Traceback (most recent call last):
274 ...
275 NotFound:...
276@@ -48,45 +57,45 @@
277 merged, though.
278
279 >>> login(ANONYMOUS)
280- >>> guadamen = person_set.getByName('guadamen')
281+ >>> guadamen = getUtility(IPersonSet).getByName('guadamen')
282 >>> mailing_list = getUtility(IMailingListSet).new(guadamen)
283 >>> logout()
284
285- >>> admin_browser.open('http://launchpad.dev/people/+adminteammerge')
286- >>> admin_browser.getControl('Duplicated Team').value = 'guadamen'
287- >>> admin_browser.getControl('Target Team').value = 'ubuntu-team'
288- >>> admin_browser.getControl('Merge').click()
289+ >>> registry_browser.open('http://launchpad.dev/people/+adminteammerge')
290+ >>> registry_browser.getControl('Duplicated Team').value = 'guadamen'
291+ >>> registry_browser.getControl('Target Team').value = 'ubuntu-team'
292+ >>> registry_browser.getControl('Merge').click()
293
294- >>> admin_browser.url
295+ >>> registry_browser.url
296 'http://launchpad.dev/people/+adminteammerge'
297
298- >>> print get_feedback_messages(admin_browser.contents)
299+ >>> print get_feedback_messages(registry_browser.contents)
300 [u'There is 1 error.',
301 u"guadamen is associated with a Launchpad mailing list;
302 we can't merge it."]
303
304 We also can't (for obvious reasons) merge any person/team into itself.
305
306- >>> admin_browser.getControl('Duplicated Team').value = 'shipit-admins'
307- >>> admin_browser.getControl('Target Team').value = 'shipit-admins'
308- >>> admin_browser.getControl('Merge').click()
309+ >>> registry_browser.getControl('Duplicated Team').value = 'shipit-admins'
310+ >>> registry_browser.getControl('Target Team').value = 'shipit-admins'
311+ >>> registry_browser.getControl('Merge').click()
312
313- >>> admin_browser.url
314+ >>> registry_browser.url
315 'http://launchpad.dev/people/+adminteammerge'
316
317- >>> print get_feedback_messages(admin_browser.contents)
318+ >>> print get_feedback_messages(registry_browser.contents)
319 [u'There is 1 error.', u"You can't merge shipit-admins into itself."]
320
321 Nor can we merge a person into a team or a team into a person.
322
323- >>> admin_browser.getControl('Duplicated Team').value = 'ubuntu-team'
324- >>> admin_browser.getControl('Target Team').value = 'salgado'
325- >>> admin_browser.getControl('Merge').click()
326+ >>> registry_browser.getControl('Duplicated Team').value = 'ubuntu-team'
327+ >>> registry_browser.getControl('Target Team').value = 'salgado'
328+ >>> registry_browser.getControl('Merge').click()
329
330- >>> admin_browser.url
331+ >>> registry_browser.url
332 'http://launchpad.dev/people/+adminteammerge'
333
334 # Yes, the error message is not of much help, but this UI is only for
335 # admins and they're supposed to know what they're doing.
336- >>> print get_feedback_messages(admin_browser.contents)
337+ >>> print get_feedback_messages(registry_browser.contents)
338 [u'There is 1 error.', u'Constraint not satisfied']
339
340=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
341--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2009-11-10 01:00:23 +0000
342+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-01-20 20:27:21 +0000
343@@ -167,6 +167,7 @@
344 sourceforge_project: None
345 summary: u'The Mozilla Firefox web browser'
346 title: u'Mozilla Firefox'
347+ translation_focus_link: None
348 wiki_url: None
349
350 In Launchpad project names may not have uppercase letters in their
351@@ -225,6 +226,7 @@
352 sourceforge_project: None
353 summary: u'The Mozilla Firefox web browser'
354 title: u'Mozilla Firefox'
355+ translation_focus_link: None
356 wiki_url: None
357
358 The milestones can be accessed through the
359
360=== modified file 'lib/lp/soyuz/scripts/expire_ppa_binaries.py'
361--- lib/lp/soyuz/scripts/expire_ppa_binaries.py 2009-12-11 14:35:28 +0000
362+++ lib/lp/soyuz/scripts/expire_ppa_binaries.py 2010-01-20 20:27:21 +0000
363@@ -54,7 +54,61 @@
364 help=("The number of days after which to expire binaries. "
365 "Must be specified."))
366
367- def determineExpirables(self, num_days):
368+ def determineSourceExpirables(self, num_days):
369+ """Return expirable libraryfilealias IDs."""
370+ # Avoid circular imports.
371+ from lp.soyuz.interfaces.archive import ArchivePurpose
372+
373+ stay_of_execution = '%d days' % num_days
374+
375+ # The subquery here has to repeat the checks for privacy and
376+ # blacklisting on *other* publications that are also done in
377+ # the main loop for the archive being considered.
378+ results = self.store.execute("""
379+ SELECT lfa.id
380+ FROM
381+ LibraryFileAlias AS lfa,
382+ Archive,
383+ SourcePackageReleaseFile AS sprf,
384+ SourcePackageRelease AS spr,
385+ SourcePackagePublishingHistory AS spph
386+ WHERE
387+ lfa.id = sprf.libraryfile
388+ AND spr.id = sprf.sourcepackagerelease
389+ AND spph.sourcepackagerelease = spr.id
390+ AND spph.dateremoved < (
391+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - interval %s)
392+ AND spph.archive = archive.id
393+ AND archive.purpose = %s
394+ AND lfa.expires IS NULL
395+ EXCEPT
396+ SELECT sprf.libraryfile
397+ FROM
398+ SourcePackageRelease AS spr,
399+ SourcePackageReleaseFile AS sprf,
400+ SourcePackagePublishingHistory AS spph,
401+ Archive AS a,
402+ Person AS p
403+ WHERE
404+ spr.id = sprf.sourcepackagerelease
405+ AND spph.sourcepackagerelease = spr.id
406+ AND spph.archive = a.id
407+ AND p.id = a.owner
408+ AND (
409+ p.name IN %s
410+ OR a.private IS TRUE
411+ OR a.purpose != %s
412+ OR dateremoved >
413+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - interval %s
414+ OR dateremoved IS NULL);
415+ """ % sqlvalues(
416+ stay_of_execution, ArchivePurpose.PPA, self.blacklist,
417+ ArchivePurpose.PPA, stay_of_execution))
418+
419+ lfa_ids = results.get_all()
420+ return lfa_ids
421+
422+ def determineBinaryExpirables(self, num_days):
423 """Return expirable libraryfilealias IDs."""
424 # Avoid circular imports.
425 from lp.soyuz.interfaces.archive import ArchivePurpose
426@@ -116,7 +170,8 @@
427 self.store = getUtility(IStoreSelector).get(
428 MAIN_STORE, DEFAULT_FLAVOR)
429
430- lfa_ids = self.determineExpirables(num_days)
431+ lfa_ids = self.determineSourceExpirables(num_days)
432+ lfa_ids.extend(self.determineBinaryExpirables(num_days))
433 batch_count = 0
434 batch_limit = 500
435 for id in lfa_ids:
436
437=== modified file 'lib/lp/soyuz/scripts/tests/test_expire_ppa_bins.py'
438--- lib/lp/soyuz/scripts/tests/test_expire_ppa_bins.py 2009-12-11 14:35:28 +0000
439+++ lib/lp/soyuz/scripts/tests/test_expire_ppa_bins.py 2010-01-20 20:27:21 +0000
440@@ -74,54 +74,76 @@
441 self.layer.switchDbUser(self.dbuser)
442 script.main()
443
444- def assertExpired(self, publication):
445- self.assertNotEqual(
446- publication.binarypackagerelease.files[0].libraryfile.expires,
447- None,
448- "lfa.expires should be set, but it's not.")
449-
450- def assertNotExpired(self, publication):
451- self.assertEqual(
452- publication.binarypackagerelease.files[0].libraryfile.expires,
453+ def assertBinaryExpired(self, publication):
454+ self.assertNotEqual(
455+ publication.binarypackagerelease.files[0].libraryfile.expires,
456+ None,
457+ "lfa.expires should be set, but it's not.")
458+
459+ def assertBinaryNotExpired(self, publication):
460+ self.assertEqual(
461+ publication.binarypackagerelease.files[0].libraryfile.expires,
462+ None,
463+ "lfa.expires should be None, but it's not.")
464+
465+ def assertSourceExpired(self, publication):
466+ self.assertNotEqual(
467+ publication.sourcepackagerelease.files[0].libraryfile.expires,
468+ None,
469+ "lfa.expires should be set, but it's not.")
470+
471+ def assertSourceNotExpired(self, publication):
472+ self.assertEqual(
473+ publication.sourcepackagerelease.files[0].libraryfile.expires,
474 None,
475 "lfa.expires should be None, but it's not.")
476
477 def testNoExpirationWithNoDateremoved(self):
478 """Test that no expiring happens if no dateremoved set."""
479 pkg1 = self.stp.getPubSource(
480- sourcename="pkg1", architecturehintlist="i386", archive=self.ppa)
481+ sourcename="pkg1", architecturehintlist="i386", archive=self.ppa,
482+ dateremoved=None)
483 [pub] = self.stp.getPubBinaries(
484 pub_source=pkg1, dateremoved=None, archive=self.ppa)
485
486 self.runScript()
487- self.assertNotExpired(pub)
488+ self.assertSourceNotExpired(pkg1)
489+ self.assertBinaryNotExpired(pub)
490
491 def testNoExpirationWithDateUnderThreshold(self):
492 """Test no expiring if dateremoved too recent."""
493 pkg2 = self.stp.getPubSource(
494- sourcename="pkg2", architecturehintlist="i386", archive=self.ppa)
495+ sourcename="pkg2", architecturehintlist="i386", archive=self.ppa,
496+ dateremoved=self.under_threshold_date)
497 [pub] = self.stp.getPubBinaries(
498 pub_source=pkg2, dateremoved=self.under_threshold_date,
499 archive=self.ppa)
500
501 self.runScript()
502- self.assertNotExpired(pub)
503+ self.assertSourceNotExpired(pkg2)
504+ self.assertBinaryNotExpired(pub)
505
506 def testExpirationWithDateOverThreshold(self):
507 """Test expiring works if dateremoved old enough."""
508 pkg3 = self.stp.getPubSource(
509- sourcename="pkg3", architecturehintlist="i386", archive=self.ppa)
510+ sourcename="pkg3", architecturehintlist="i386", archive=self.ppa,
511+ dateremoved=self.over_threshold_date)
512 [pub] = self.stp.getPubBinaries(
513 pub_source=pkg3, dateremoved=self.over_threshold_date,
514 archive=self.ppa)
515
516 self.runScript()
517- self.assertExpired(pub)
518+ self.assertSourceExpired(pkg3)
519+ self.assertBinaryExpired(pub)
520
521 def testNoExpirationWithDateOverThresholdAndOtherValidPublication(self):
522 """Test no expiry if dateremoved old enough but other publication."""
523 pkg4 = self.stp.getPubSource(
524- sourcename="pkg4", architecturehintlist="i386", archive=self.ppa)
525+ sourcename="pkg4", architecturehintlist="i386", archive=self.ppa,
526+ dateremoved=self.over_threshold_date)
527+ other_source = pkg4.copyTo(
528+ pkg4.distroseries, pkg4.pocket, self.ppa2)
529+ other_source.secure_record.dateremoved = None
530 [pub] = self.stp.getPubBinaries(
531 pub_source=pkg4, dateremoved=self.over_threshold_date,
532 archive=self.ppa)
533@@ -130,16 +152,21 @@
534 other_binary.secure_record.dateremoved = None
535
536 self.runScript()
537- self.assertNotExpired(pub)
538+ self.assertSourceNotExpired(pkg4)
539+ self.assertBinaryNotExpired(pub)
540
541 def testNoExpirationWithDateOverThresholdAndOtherPubUnderThreshold(self):
542 """Test no expiring.
543-
544+
545 Test no expiring if dateremoved old enough but other publication
546 not over date threshold.
547 """
548 pkg5 = self.stp.getPubSource(
549- sourcename="pkg5", architecturehintlist="i386", archive=self.ppa)
550+ sourcename="pkg5", architecturehintlist="i386", archive=self.ppa,
551+ dateremoved=self.over_threshold_date)
552+ other_source = pkg5.copyTo(
553+ pkg5.distroseries, pkg5.pocket, self.ppa2)
554+ other_source.secure_record.dateremoved = self.under_threshold_date
555 [pub] = self.stp.getPubBinaries(
556 pub_source=pkg5, dateremoved=self.over_threshold_date,
557 archive=self.ppa)
558@@ -148,67 +175,77 @@
559 other_binary.secure_record.dateremoved = self.under_threshold_date
560
561 self.runScript()
562- self.assertNotExpired(pub)
563+ self.assertSourceNotExpired(pkg5)
564+ self.assertBinaryNotExpired(pub)
565
566 def _setUpExpirablePublications(self, archive=None):
567 """Helper to set up two publications that are both expirable."""
568 if archive is None:
569 archive = self.ppa
570 pkg5 = self.stp.getPubSource(
571- sourcename="pkg5", architecturehintlist="i386", archive=archive)
572+ sourcename="pkg5", architecturehintlist="i386", archive=archive,
573+ dateremoved=self.over_threshold_date)
574+ other_source = pkg5.copyTo(
575+ pkg5.distroseries, pkg5.pocket, self.ppa2)
576+ other_source.secure_record.dateremoved = self.over_threshold_date
577 [pub] = self.stp.getPubBinaries(
578 pub_source=pkg5, dateremoved=self.over_threshold_date,
579 archive=archive)
580 [other_binary] = pub.copyTo(
581 pub.distroarchseries.distroseries, pub.pocket, self.ppa2)
582 other_binary.secure_record.dateremoved = self.over_threshold_date
583- return pub
584+ return pkg5, pub
585
586 def testNoExpirationWithDateOverThresholdAndOtherPubOverThreshold(self):
587 """Test expiring works.
588-
589+
590 Test expiring works if dateremoved old enough and other publication
591 is over date threshold.
592 """
593- pub = self._setUpExpirablePublications()
594+ source, binary = self._setUpExpirablePublications()
595 self.runScript()
596- self.assertExpired(pub)
597+ self.assertSourceExpired(source)
598+ self.assertBinaryExpired(binary)
599
600 def testBlacklistingWorks(self):
601 """Test that blacklisted PPAs are not expired."""
602- pub = self._setUpExpirablePublications()
603+ source, binary = self._setUpExpirablePublications()
604 script = self.getScript()
605 script.blacklist = ["cprov",]
606 self.layer.txn.commit()
607 self.layer.switchDbUser(self.dbuser)
608 script.main()
609- self.assertNotExpired(pub)
610+ self.assertSourceNotExpired(source)
611+ self.assertBinaryNotExpired(binary)
612
613 def testPrivatePPAsNotExpired(self):
614 """Test that private PPAs are not expired."""
615 self.ppa.private = True
616 self.ppa.buildd_secret = "foo"
617- pub = self._setUpExpirablePublications()
618+ source, binary = self._setUpExpirablePublications()
619 self.runScript()
620- self.assertNotExpired(pub)
621+ self.assertSourceNotExpired(source)
622+ self.assertBinaryNotExpired(binary)
623
624 def testDryRun(self):
625 """Test that when dryrun is specified, nothing is expired."""
626- pub = self._setUpExpirablePublications()
627+ source, binary = self._setUpExpirablePublications()
628 # We have to commit here otherwise when the script aborts it
629 # will remove the test publications we just created.
630 self.layer.txn.commit()
631 script = self.getScript(['--dry-run'])
632 self.layer.switchDbUser(self.dbuser)
633 script.main()
634- self.assertNotExpired(pub)
635+ self.assertSourceNotExpired(source)
636+ self.assertBinaryNotExpired(binary)
637
638 def testDoesNotAffectNonPPA(self):
639 """Test that expiry does not happen for non-PPA publications."""
640 ubuntu_archive = getUtility(IDistributionSet)['ubuntu'].main_archive
641- pub = self._setUpExpirablePublications(ubuntu_archive)
642+ source, binary = self._setUpExpirablePublications(ubuntu_archive)
643 self.runScript()
644- self.assertNotExpired(pub)
645+ self.assertSourceNotExpired(source)
646+ self.assertBinaryNotExpired(binary)
647
648
649 def test_suite():
650
651=== modified file 'lib/lp/translations/browser/product.py'
652--- lib/lp/translations/browser/product.py 2010-01-15 14:44:39 +0000
653+++ lib/lp/translations/browser/product.py 2010-01-20 20:27:21 +0000
654@@ -16,7 +16,8 @@
655 LaunchpadView, Link, canonical_url, enabled_with_permission)
656 from canonical.launchpad.webapp.authorization import check_permission
657 from canonical.launchpad.webapp.menu import NavigationMenu
658-from lp.registry.interfaces.product import IProduct, IProductSeries
659+from lp.registry.interfaces.product import IProduct
660+from lp.registry.interfaces.productseries import IProductSeries
661 from lp.registry.browser.product import ProductEditView
662 from lp.translations.browser.translations import TranslationsMixin
663
664@@ -63,7 +64,11 @@
665 class ProductChangeTranslatorsView(TranslationsMixin, ProductEditView):
666 label = "Set permissions and policies"
667 page_title = "Permissions and policies"
668- field_names = ["translationgroup", "translationpermission"]
669+ field_names = [
670+ "translationgroup",
671+ "translationpermission",
672+ "translation_focus"
673+ ]
674
675 @property
676 def cancel_url(self):
677
678=== added directory 'lib/lp/translations/stories/translationfocus'
679=== added file 'lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt'
680--- lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 1970-01-01 00:00:00 +0000
681+++ lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 2010-01-20 20:27:21 +0000
682@@ -0,0 +1,87 @@
683+The translation focus of a product can be explicitly set to a specific series.
684+When not set, launchpad recommends the development focus to translate.
685+
686+ >>> login('admin@canonical.com')
687+ >>> fooproject = factory.makeProduct(name="fooproject")
688+ >>> fooproject.official_rosetta = True
689+ >>> fooproject_trunk = fooproject.getSeries("trunk")
690+ >>> fooproject_url = canonical_url(
691+ ... fooproject, rootsite="translations")
692+ >>> logout()
693+
694+Only admin users are able to change the translation focus of a product.
695+Unprivileged users can see the recommended series for translation,
696+but have no access to the 'Change permissions' menu.
697+
698+ >>> admin_browser.open(fooproject_url)
699+ >>> print extract_text(
700+ ... find_tags_by_class(admin_browser.contents, 'edit sprite')[0])
701+ Change permissions
702+
703+ >>> browser.open(fooproject_url)
704+ >>> print extract_text(
705+ ... find_tags_by_class(browser.contents, 'edit sprite')[0])
706+ Traceback (most recent call last):
707+ ...
708+ IndexError: list index out of range
709+
710+== Setting the translation focus ==
711+
712+ >>> login('admin@canonical.com')
713+ >>> from zope.security.proxy import removeSecurityProxy
714+ >>> pot_main = factory.makePOTemplate(
715+ ... productseries=fooproject_trunk, name="pot1")
716+ >>> removeSecurityProxy(pot_main).messagecount = 10
717+ >>> pofile = factory.makePOFile("pt_BR", potemplate=pot_main)
718+ >>> logout()
719+
720+When the translation focus is not set, Launchpad suggests the
721+development focus as the current series to be translated.
722+It needs to be translatable.
723+
724+ >>> print fooproject.translation_focus
725+ None
726+
727+ >>> browser.open(fooproject_url)
728+ >>> print extract_text(find_tags_by_class(browser.contents, 'portlet')[0])
729+ Translation details...
730+ Launchpad currently recommends translating... Fooproject trunk series.
731+ ...
732+
733+We can set an untranslatable series as the translation focus, but Launchpad
734+won't consider it because there'll be nothing to translate.
735+
736+ >>> login('admin@canonical.com')
737+ >>> fooproject_untranslatableseries = factory.makeProductSeries(
738+ ... product=fooproject,
739+ ... name="untranslatable-series")
740+ >>> fooproject.translation_focus = fooproject_untranslatableseries
741+ >>> logout()
742+
743+ >>> print removeSecurityProxy(fooproject.translation_focus.name)
744+ untranslatable-series
745+
746+ >>> browser.open(fooproject_url)
747+ >>> print extract_text(find_tags_by_class(browser.contents, 'portlet')[0])
748+ Translation details...
749+ Launchpad currently recommends translating... Fooproject trunk series.
750+ ...
751+
752+We need to create a translatable series so we can set it as translation focus.
753+
754+ >>> login('admin@canonical.com')
755+ >>> fooproject_otherseries = factory.makeProductSeries(product=fooproject,
756+ ... name="other-series")
757+ >>> pot_other = factory.makePOTemplate(
758+ ... productseries=fooproject_otherseries, name="pot2")
759+ >>> removeSecurityProxy(pot_other).messagecount = 10
760+ >>> pofile1 = factory.makePOFile('pt_BR', potemplate=pot_other)
761+
762+ >>> fooproject.translation_focus = fooproject_otherseries
763+ >>> logout()
764+
765+ >>> browser.open(fooproject_url)
766+ >>> print extract_text(find_tags_by_class(browser.contents, 'portlet')[0])
767+ Translation details...
768+ Launchpad currently recommends translating... Fooproject other-series series.
769+ ...
770
771=== added file 'lib/lp/translations/stories/webservice/xx-translationfocus.txt'
772--- lib/lp/translations/stories/webservice/xx-translationfocus.txt 1970-01-01 00:00:00 +0000
773+++ lib/lp/translations/stories/webservice/xx-translationfocus.txt 2010-01-20 20:27:21 +0000
774@@ -0,0 +1,34 @@
775+= Translation focus =
776+
777+The translation focus of a project is the series chosen as the preferred
778+one to be translated. It's optional. When not set, Launchpad suggests
779+the development focus as the preferred series to translate, which is
780+outside the scope of this test.
781+
782+ >>> evolution = webservice.get('/evolution').jsonBody()
783+ >>> print evolution['development_focus_link']
784+ http://.../evolution/trunk
785+ >>> print evolution['translation_focus_link']
786+ None
787+
788+It's possible to set the translation focus through the API
789+if you're an admin. The translation focus should be a project series.
790+
791+ >>> from simplejson import dumps
792+ >>> print webservice.patch(
793+ ... evolution['self_link'], 'application/json',
794+ ... dumps({'translation_focus_link' :
795+ ... evolution['development_focus_link']}))
796+ HTTP/1.1 209 Content Returned
797+ ...
798+
799+ >>> print webservice.get('/evolution').jsonBody()['translation_focus_link']
800+ http://.../evolution/trunk
801+
802+Unprivileged users cannot set the translation focus.
803+
804+ >>> print user_webservice.patch(
805+ ... evolution['self_link'], 'application/json',
806+ ... dumps({'translation_focus_link': None}))
807+ HTTP... 401 Unauthorized
808+ ...

Subscribers

People subscribed via source and target branches

to status/vote changes: