Merge lp:~adiroiban/launchpad/bug-340662-take-2 into lp:launchpad/db-devel

Proposed by Adi Roiban
Status: Merged
Approved by: Brad Crittenden
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~adiroiban/launchpad/bug-340662-take-2
Merge into: lp:launchpad/db-devel
Diff against target: 275 lines (+72/-52)
5 files modified
lib/canonical/launchpad/security.py (+3/-28)
lib/lp/translations/browser/configure.zcml (+2/-2)
lib/lp/translations/browser/potemplate.py (+9/-8)
lib/lp/translations/configure.zcml (+2/-2)
lib/lp/translations/stories/standalone/xx-potemplate-edit.txt (+56/-12)
To merge this branch: bzr merge lp:~adiroiban/launchpad/bug-340662-take-2
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+17598@code.launchpad.net

Commit message

Remove "name" field from POTemplate edit view. On POTemplateSubset url traversal, check permission on POFile. Remove EditPOTemplateSubset from security.py

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

= Bug #340662 and #507498=
This branch should fix the issues raised in this MP
https://code.edge.launchpad.net/~adiroiban/launchpad/bug-340662/+merge/16629

== Proposed fix ==
Remove "name" field from PoTemplate edit page.

In the PoTemplateSubset URL traversal code, check permissions for the POTemplate and not for the context.

== Pre-implementation notes ==
Danilo asked to remove the "name" field from the edit page since it is to fragile to be edited by project owners.

Henning suggest that permission checking for POTemplateSubset URL traversal can be done for POTemplate and in this way we can remove the EditPOTemplateSubSet from security.py

== Implementation details ==
Nothing special here.

== Tests ==
lp-test -t templates

== Demo and Q/A ==
As a product owner (ex. <email address hidden>) for a project (ex. evolution) go to a template page for that project
https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/

You should see the „Change details” that links to the +edit page

From this page you should be able to change template name, domain name, path, details, priority, owner and „accept translation”.

Change some values and then save them.

From the series +template page you will see „Edit” links for each template, including disabled templates:
https://translations.launchpad.dev/evolution/trunk/+templates

Disabling a template, it should still be in the list (with a red background) and you can still edit it.

= 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/translations/configure.zcml
  lib/lp/translations/browser/potemplate.py
  lib/lp/translations/stories/standalone/xx-potemplate-edit.txt

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Adi,

Thanks for this branch.

As noted in IRC, there is an import violation that is clearly not your fault but needs to be fixed before landing this branch. Thanks for agreeing to look into it.

Also, when attempting the demo you describe in the MP I get an unauthorized error changing any field b/c date_last_updated requires launchpad.TranslationsAdmin. We need to beef up the tests to ensure the permissions are set as we need.

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

Hi,

Thanks for the review.

The permission problem should be fixed now and test extended to check all fields.

Not sure if there will be branch for fixing the import problem. I will leave the import problem as a final commit.

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Adi,

Thanks again for your changes. Your use of removeSecurityProxy was a bit flawed, though, so date_last_updated was not being changed. This flaw also exposes a hole in the testing as there is not test to ensure the value actually got changed.

If you apply the following change it will work:

=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py 2010-01-19 16:48:51 +0000
+++ lib/lp/translations/browser/potemplate.py 2010-01-20 14:45:58 +0000
@@ -533,9 +533,8 @@
             # We only update date_last_updated when translation_domain field
             # is changed because is the only significative change that,
             # somehow, affects the content of the potemplate.
- UTC = pytz.timezone('UTC')
- date_last_updated = removeSecurityProxy(context.date_last_updated)
- date_last_updated = datetime.datetime.now(UTC)
+ naked_context = removeSecurityProxy(context)
+ naked_context.date_last_updated = datetime.datetime.now(pytz.UTC)

Also, while you're there could you fix the preceding comment so that it reads:

We only change date_last_updated when the translation_domain field is changed because it is the only relevant field we care about regarding the date of last update.

In your test it would be great to grab the value of date_last_updated before and after the change and show that new > old. It's kind of messy in a story test but should be ok.

Please post a diff when you're done and I'll have a final look at it.

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

Hi,
Here is the diff. Hope everything is OK now.

=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py 2010-01-19 16:48:51 +0000
+++ lib/lp/translations/browser/potemplate.py 2010-01-20 18:12:49 +0000
@@ -530,12 +530,12 @@
                 product=context.product, distribution=context.distribution,
                 sourcepackagename=context.sourcepackagename)
         if old_translation_domain != context.translation_domain:
- # We only update date_last_updated when translation_domain field
- # is changed because is the only significative change that,
- # somehow, affects the content of the potemplate.
+ # We only change date_last_updated when the translation_domain
+ # field is changed because it is the only relevant field we
+ # care about regarding the date of last update.
             UTC = pytz.timezone('UTC')
- date_last_updated = removeSecurityProxy(context.date_last_updated)
- date_last_updated = datetime.datetime.now(UTC)
+ naked_context = removeSecurityProxy(context)
+ naked_context.date_last_updated = datetime.datetime.now(UTC)

     @property
     def cancel_url(self):

=== modified file 'lib/lp/translations/stories/standalone/xx-potemplate-edit.txt'
--- lib/lp/translations/stories/standalone/xx-potemplate-edit.txt 2010-01-19 16:48:51 +0000
+++ lib/lp/translations/stories/standalone/xx-potemplate-edit.txt 2010-01-20 19:10:54 +0000
@@ -90,6 +90,22 @@
   >>> browser.getControl(name='field.translation_domain').value
   'evolution-2.2'

+We remember the 'last_update_date' in order to check if it was changed
+after updating the template.
+
+ >>> from zope.component import getUtility
+ >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
+ >>> from lp.translations.model.potemplate import POTemplateSubset
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> login('<email address hidden>')
+ >>> evolution = getUtility(IProductSet).getByName('evolution')
+ >>> evolution_trunk = evolution.getSeries('trunk')
+ >>> hoary_subset = POTemplateSubset(productseries=evolution_trunk)
+ >>> evolution_template = hoary_subset.getPOTemplateByName(
+ ... 'evolution-2.2')
+ >>> previous_date_last_updated = evolution_template.date_last_updated
+ >>> logout()
+
 The visible fields can be changed and saved.

   >>> browser.getControl(name='field.translation_domain').value = u'evo'
@@ -120,3 +136,6 @@
   'name12'
   >>> browser.getControl(name='field.description').value
   'foo'
+ >>> previous_date_last_updated != evolution_template.date_last_updated
+ True
+

Revision history for this message
Brad Crittenden (bac) wrote :

Adi it looks very good now. Thanks for the fixes.

One tiny thing: remove the UTC = line and just use 'pytz.UTC' instead, as shown in my previous comment. pytz is nice enough to export a pre-defined UTC constant so we should use it rather than looking it up again.

review: Approve (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (4.3 KiB)

Here is the diff after running the syntactic check for modified files.

=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-01-18 16:54:07 +0000
+++ lib/canonical/launchpad/security.py 2010-01-20 20:29:00 +0000
@@ -469,7 +469,7 @@
         return (user.inTeam(self.obj.person) or
                 user.isOneOf(
                     self.obj.specification,
- ['owner','drafter', 'assignee', 'approver']) or
+ ['owner', 'drafter', 'assignee', 'approver']) or
                 user.in_admin)

@@ -530,6 +530,7 @@
             return True
         return user.isOwner(self.obj.target)

+
 class AdminMilestoneByLaunchpadAdmins(AuthorizationBase):
     permission = 'launchpad.Admin'
     usedfor = IMilestone
@@ -574,7 +575,7 @@

     def checkAuthenticated(self, user):
         """Is the user a privileged team member or Launchpad staff?
-
+
         Return true when the user is a member of Launchpad admins,
         registry experts, team admins, or the team owners.
         """
@@ -1431,7 +1432,6 @@

     # This code MUST match the logic in IBuildSet.getBuildsForBuilder()
     # otherwise users are likely to get 403 errors, or worse.
-
     def checkAuthenticated(self, user):
         """Private restricts to admins and archive members."""
         if not self.obj.archive.private:

=== modified file 'lib/lp/translations/browser/configure.zcml'
--- lib/lp/translations/browser/configure.zcml 2009-12-12 05:47:41 +0000
+++ lib/lp/translations/browser/configure.zcml 2010-01-20 20:31:52 +0000
@@ -391,9 +391,9 @@
             <browser:page
                 name="+chart"
                 template="../templates/potemplate-chart.pt"/>
-
+
             <!-- Potemplate Portlets -->
-
+
             <browser:page
                 name="+portlet-details"
                 template="../templates/potemplate-portlet-details.pt"/>

=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py 2010-01-20 20:16:29 +0000
+++ lib/lp/translations/browser/potemplate.py 2010-01-20 20:33:54 +0000
@@ -533,9 +533,8 @@
             # We only change date_last_updated when the translation_domain
             # field is changed because it is the only relevant field we
             # care about regarding the date of last update.
- UTC = pytz.timezone('UTC')
             naked_context = removeSecurityProxy(context)
- naked_context.date_last_updated = datetime.datetime.now(UTC)
+ naked_context.date_last_updated = datetime.datetime.now(pytz.UTC)

     @property
     def cancel_url(self):

=== modified file 'lib/lp/translations/configure.zcml'
--- lib/lp/translations/configure.zcml 2010-01-17 02:07:16 +0000
+++ lib/lp/translations/configure.zcml 2010-01-20 20:32:41 +0000
@@ -415,7 +415,7 @@
             <require
                 permission="launchpad.TranslationsAdmin"
                 set_attributes="
- name productseries distroseries sourcepackagename
+ name productseries distroseries sourcepackagename
                     sourcepackageversion binaryp...

Read more...

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-01-14 11:36:51 +0000
3+++ lib/canonical/launchpad/security.py 2010-01-20 20:38:18 +0000
4@@ -469,7 +469,7 @@
5 return (user.inTeam(self.obj.person) or
6 user.isOneOf(
7 self.obj.specification,
8- ['owner','drafter', 'assignee', 'approver']) or
9+ ['owner', 'drafter', 'assignee', 'approver']) or
10 user.in_admin)
11
12
13@@ -530,6 +530,7 @@
14 return True
15 return user.isOwner(self.obj.target)
16
17+
18 class AdminMilestoneByLaunchpadAdmins(AuthorizationBase):
19 permission = 'launchpad.Admin'
20 usedfor = IMilestone
21@@ -574,7 +575,7 @@
22
23 def checkAuthenticated(self, user):
24 """Is the user a privileged team member or Launchpad staff?
25-
26+
27 Return true when the user is a member of Launchpad admins,
28 registry experts, team admins, or the team owners.
29 """
30@@ -1144,8 +1145,6 @@
31 self, user)
32
33
34-# Please keep EditPOTemplateSubset in sync with this, unless you
35-# know exactly what you are doing.
36 class EditPOTemplateDetails(AdminPOTemplateDetails, EditByOwnersOrAdmins):
37 permission = 'launchpad.Edit'
38 usedfor = IPOTemplate
39@@ -1433,7 +1432,6 @@
40
41 # This code MUST match the logic in IBuildSet.getBuildsForBuilder()
42 # otherwise users are likely to get 403 errors, or worse.
43-
44 def checkAuthenticated(self, user):
45 """Private restricts to admins and archive members."""
46 if not self.obj.archive.private:
47@@ -1648,29 +1646,6 @@
48 return OnlyRosettaExpertsAndAdmins.checkAuthenticated(self, user)
49
50
51-# Please keep EditPOTemplate in sync with this, unless you
52-# know exactly what you are doing. Note that this permission controls
53-# access to browsing into individual potemplates, but it's on a different
54-# object (POTemplateSubset) from EditPOTemplateDetails,
55-# even though it looks almost identical
56-class EditPOTemplateSubset(AuthorizationBase):
57- permission = 'launchpad.Edit'
58- usedfor = IPOTemplateSubset
59-
60- def checkAuthenticated(self, user):
61- """Allow anyone with admin rights; owners, product owners and
62- distribution owners; and for distros, translation group owners.
63- """
64- if (self.obj.productseries is not None and
65- user.inTeam(self.obj.productseries.product.owner)):
66- # The user is the owner of the product.
67- return True
68-
69- return (
70- AdminPOTemplateSubset(self.obj).checkAuthenticated(user) or
71- EditByOwnersOrAdmins(self.obj).checkAuthenticated(user))
72-
73-
74 class AdminDistroSeriesTranslations(AuthorizationBase):
75 permission = 'launchpad.TranslationsAdmin'
76 usedfor = IDistroSeries
77
78=== modified file 'lib/lp/translations/browser/configure.zcml'
79--- lib/lp/translations/browser/configure.zcml 2009-12-12 05:47:41 +0000
80+++ lib/lp/translations/browser/configure.zcml 2010-01-20 20:38:18 +0000
81@@ -391,9 +391,9 @@
82 <browser:page
83 name="+chart"
84 template="../templates/potemplate-chart.pt"/>
85-
86+
87 <!-- Potemplate Portlets -->
88-
89+
90 <browser:page
91 name="+portlet-details"
92 template="../templates/potemplate-portlet-details.pt"/>
93
94=== modified file 'lib/lp/translations/browser/potemplate.py'
95--- lib/lp/translations/browser/potemplate.py 2009-12-28 22:58:18 +0000
96+++ lib/lp/translations/browser/potemplate.py 2010-01-20 20:38:18 +0000
97@@ -31,6 +31,7 @@
98 from zope.component import getUtility
99 from zope.interface import implements
100 from zope.publisher.browser import FileUpload
101+from zope.security.proxy import removeSecurityProxy
102
103 from canonical.lazr.utils import smartquote
104
105@@ -291,7 +292,7 @@
106 # SourcePackageName is needed to avoid hardcoding this URL.
107 url = (canonical_url(
108 self.context.distroseries, rootsite="translations") +
109- "/+source/" + self.context.sourcepackagename.name +
110+ "/+source/" + self.context.sourcepackagename.name +
111 "/+translations")
112 else:
113 url = canonical_url(
114@@ -512,7 +513,7 @@
115 """View class that lets you edit a POTemplate object."""
116
117 schema = IPOTemplate
118- field_names = ['name', 'translation_domain', 'description', 'priority',
119+ field_names = ['translation_domain', 'description', 'priority',
120 'path', 'owner', 'iscurrent']
121 label = 'Edit translation template details'
122 page_title = 'Edit details'
123@@ -529,11 +530,11 @@
124 product=context.product, distribution=context.distribution,
125 sourcepackagename=context.sourcepackagename)
126 if old_translation_domain != context.translation_domain:
127- # We only update date_last_updated when translation_domain field
128- # is changed because is the only significative change that,
129- # somehow, affects the content of the potemplate.
130- UTC = pytz.timezone('UTC')
131- context.date_last_updated = datetime.datetime.now(UTC)
132+ # We only change date_last_updated when the translation_domain
133+ # field is changed because it is the only relevant field we
134+ # care about regarding the date of last update.
135+ naked_context = removeSecurityProxy(context)
136+ naked_context.date_last_updated = datetime.datetime.now(pytz.UTC)
137
138 @property
139 def cancel_url(self):
140@@ -713,7 +714,7 @@
141 raise AssertionError('Unknown context for %s' % potemplate.title)
142
143 if ((official_rosetta and potemplate.iscurrent) or
144- check_permission('launchpad.Edit', self.context)):
145+ check_permission('launchpad.Edit', potemplate)):
146 # The target is using officially Launchpad Translations and the
147 # template is available to be translated, or the user is a is a
148 # Launchpad administrator in which case we show everything.
149
150=== modified file 'lib/lp/translations/configure.zcml'
151--- lib/lp/translations/configure.zcml 2010-01-12 23:25:16 +0000
152+++ lib/lp/translations/configure.zcml 2010-01-20 20:38:18 +0000
153@@ -411,11 +411,11 @@
154 permission="launchpad.Edit"
155 set_attributes="
156 owner priority description translation_domain path
157- iscurrent name"/>
158+ iscurrent"/>
159 <require
160 permission="launchpad.TranslationsAdmin"
161 set_attributes="
162- productseries distroseries sourcepackagename
163+ name productseries distroseries sourcepackagename
164 sourcepackageversion binarypackagename languagepack
165 source_file_format source_file date_last_updated
166 from_sourcepackagename header"/>
167
168=== modified file 'lib/lp/translations/stories/standalone/xx-potemplate-edit.txt'
169--- lib/lp/translations/stories/standalone/xx-potemplate-edit.txt 2009-12-29 18:21:37 +0000
170+++ lib/lp/translations/stories/standalone/xx-potemplate-edit.txt 2010-01-20 20:38:18 +0000
171@@ -33,6 +33,8 @@
172 >>> print browser.url
173 http://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/+edit
174
175+The owner of a product has access to edit page for PO templates.
176+
177 >>> browser = setupBrowser(auth='Basic test@canonical.com:test')
178 >>> browser.open(
179 ... 'http://translations.launchpad.dev/evolution/trunk/+pots/'
180@@ -40,20 +42,17 @@
181 >>> print browser.url
182 http://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/+edit
183
184-We should be sure that the edit form get only the non admin fields.
185+Owner will not see admin fields, only those fields designated for the
186+edit page.
187
188 >>> browser.getControl(name='field.name').value
189- 'evolution-2.2'
190- >>> browser.getControl(name='field.description').value
191- 'Template for evolution in hoary'
192+ Traceback (most recent call last):
193+ ...
194+ LookupError:...
195 >>> browser.getControl(name='field.header').value
196 Traceback (most recent call last):
197 ...
198 LookupError:...
199- >>> browser.getControl(name='field.iscurrent').value
200- True
201- >>> browser.getControl(name='field.owner').value
202- 'rosetta-admins'
203 >>> browser.getControl(name='field.productseries').value
204 Traceback (most recent call last):
205 ...
206@@ -78,20 +77,65 @@
207 Traceback (most recent call last):
208 ...
209 LookupError:...
210+ >>> browser.getControl(name='field.description').value
211+ 'Template for evolution in hoary'
212 >>> browser.getControl(name='field.path').value
213 'po/evolution-2.2.pot'
214-
215-And that we are able to POST it.
216-
217+ >>> browser.getControl(name='field.iscurrent').value
218+ True
219+ >>> browser.getControl(name='field.owner').value
220+ 'rosetta-admins'
221+ >>> browser.getControl(name='field.priority').value
222+ '0'
223+ >>> browser.getControl(name='field.translation_domain').value
224+ 'evolution-2.2'
225+
226+We remember the 'last_update_date' in order to check if it was changed
227+after updating the template.
228+
229+ >>> from zope.component import getUtility
230+ >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
231+ >>> from lp.translations.model.potemplate import POTemplateSubset
232+ >>> from lp.registry.interfaces.product import IProductSet
233+ >>> login('foo.bar@canonical.com')
234+ >>> evolution = getUtility(IProductSet).getByName('evolution')
235+ >>> evolution_trunk = evolution.getSeries('trunk')
236+ >>> hoary_subset = POTemplateSubset(productseries=evolution_trunk)
237+ >>> evolution_template = hoary_subset.getPOTemplateByName(
238+ ... 'evolution-2.2')
239+ >>> previous_date_last_updated = evolution_template.date_last_updated
240+ >>> logout()
241+
242+The visible fields can be changed and saved.
243+
244+ >>> browser.getControl(name='field.translation_domain').value = u'evo'
245+ >>> browser.getControl(name='field.priority').value = '100'
246+ >>> browser.getControl(name='field.iscurrent').value = False
247+ >>> browser.getControl(name='field.path').value = 'po/evolution.pot'
248+ >>> browser.getControl(name='field.owner').value = u'name12'
249 >>> browser.getControl(name='field.description').value = u'foo'
250 >>> browser.getControl('Change').click()
251 >>> print browser.url
252 http://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2
253
254-And the changed value is stored.
255+The changed values will be stored and visible by accesing again the
256+edit page.
257
258 >>> browser.open(
259 ... 'http://translations.launchpad.dev/evolution/trunk/+pots/'
260 ... 'evolution-2.2/+edit')
261+ >>> browser.getControl(name='field.translation_domain').value
262+ 'evo'
263+ >>> browser.getControl(name='field.priority').value
264+ '100'
265+ >>> browser.getControl(name='field.iscurrent').value
266+ False
267+ >>> browser.getControl(name='field.path').value
268+ 'po/evolution.pot'
269+ >>> browser.getControl(name='field.owner').value
270+ 'name12'
271 >>> browser.getControl(name='field.description').value
272 'foo'
273+ >>> previous_date_last_updated != evolution_template.date_last_updated
274+ True
275+

Subscribers

People subscribed via source and target branches

to status/vote changes: