Merge lp:~stevenk/launchpad/less-powerful-bug-supervisors into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 14341
Proposed branch: lp:~stevenk/launchpad/less-powerful-bug-supervisors
Merge into: lp:launchpad
Diff against target: 497 lines (+140/-100)
10 files modified
lib/lp/bugs/browser/bugtarget.py (+4/-1)
lib/lp/bugs/browser/bugtask.py (+28/-42)
lib/lp/bugs/browser/tests/test_bugtask.py (+1/-1)
lib/lp/bugs/configure.zcml (+1/-2)
lib/lp/bugs/interfaces/bugtask.py (+5/-4)
lib/lp/bugs/model/bugtask.py (+30/-42)
lib/lp/bugs/model/tests/test_bugtask.py (+65/-0)
lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt (+2/-4)
lib/lp/bugs/templates/bugtask-edit-form.pt (+2/-2)
lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt (+2/-2)
To merge this branch: bzr merge lp:~stevenk/launchpad/less-powerful-bug-supervisors
Reviewer Review Type Date Requested Status
Ian Booth (community) code Approve
Review via email: mp+82632@code.launchpad.net

Commit message

[r=wallyworld][bug=885692] Consolidate who has access to change details on bugtasks.

Description of the change

This branch is somewhat misnamed. It is not about making bug supervisors less powerful or taking away abilities they have today, it is about making sure that pillar owners, drivers and admins have the same rights, so make sure everyone has the same level of power.

The only place I've not fixed is the browser code inside bugtarget, which is a little difficult and requires thought since we don't actually have a bugtask yet -- but it has been marked with an XXX.

I have discussed this branch with Curtis and William, they like the approach.

I have cleaned up some lint that I noticed.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Looks good. Thanks for adding the tests. I think the one which asserts that any old user doesn't have privileges is particularly important.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtarget.py'
2--- lib/lp/bugs/browser/bugtarget.py 2011-07-28 17:31:16 +0000
3+++ lib/lp/bugs/browser/bugtarget.py 2011-11-21 00:29:30 +0000
4@@ -131,7 +131,6 @@
5 from lp.bugs.interfaces.bugtracker import IBugTracker
6 from lp.bugs.interfaces.malone import IMaloneApplication
7 from lp.bugs.interfaces.securitycontact import IHasSecurityContact
8-from lp.bugs.model.bugtask import BugTask
9 from lp.bugs.model.structuralsubscription import (
10 get_structural_subscriptions_for_target,
11 )
12@@ -402,6 +401,8 @@
13 # If the context is a project group we want to render the optional
14 # fields since they will initially be hidden and later exposed if the
15 # selected project supports them.
16+ # XXX: StevenK 2011-11-18 bug=885692 This should make use of
17+ # IBugTask.userHasPrivileges().
18 include_extra_fields = IProjectGroup.providedBy(context)
19 if not include_extra_fields and IHasBugSupervisor.providedBy(context):
20 include_extra_fields = self.user.inTeam(context.bug_supervisor)
21@@ -627,6 +628,8 @@
22 if extra_data.private:
23 params.private = extra_data.private
24
25+ # XXX: StevenK 2011-11-18 bug=885692 This should make use of
26+ # IBugTask.userHasPrivileges().
27 # Apply any extra options given by a bug supervisor.
28 if IHasBugSupervisor.providedBy(context):
29 if self.user.inTeam(context.bug_supervisor):
30
31=== modified file 'lib/lp/bugs/browser/bugtask.py'
32--- lib/lp/bugs/browser/bugtask.py 2011-11-20 23:50:47 +0000
33+++ lib/lp/bugs/browser/bugtask.py 2011-11-21 00:29:30 +0000
34@@ -1281,7 +1281,20 @@
35 }
36
37
38-class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin):
39+class BugTaskPrivilegeMixin:
40+
41+ @cachedproperty
42+ def user_has_privileges(self):
43+ """Is the user privileged? That is, an admin, pillar owner, driver
44+ or bug supervisor.
45+
46+ If yes, return True, otherwise return False.
47+ """
48+ return self.context.userHasPrivileges(self.user)
49+
50+
51+class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
52+ BugTaskPrivilegeMixin):
53 """The view class used for the task +editstatus page."""
54
55 schema = IBugTask
56@@ -1346,26 +1359,24 @@
57
58 # XXX: Brad Bollenbach 2006-09-29 bug=63000: Permission checking
59 # doesn't belong here!
60- if ('milestone' in editable_field_names and
61- not self.userCanEditMilestone()):
62- editable_field_names.remove("milestone")
63-
64- if ('importance' in editable_field_names and
65- not self.userCanEditImportance()):
66- editable_field_names.remove("importance")
67+ if not self.user_has_privileges:
68+ if 'milestone' in editable_field_names:
69+ editable_field_names.remove("milestone")
70+ if 'importance' in editable_field_names:
71+ editable_field_names.remove("importance")
72 else:
73 editable_field_names = set(('bugwatch', ))
74 if self.context.bugwatch is None:
75 editable_field_names.update(('status', 'assignee'))
76 if ('importance' in self.default_field_names
77- and self.userCanEditImportance()):
78+ and self.user_has_privileges):
79 editable_field_names.add('importance')
80 else:
81 bugtracker = self.context.bugwatch.bugtracker
82 if bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS:
83 editable_field_names.add('status')
84 if ('importance' in self.default_field_names
85- and self.userCanEditImportance()):
86+ and self.user_has_privileges):
87 editable_field_names.add('importance')
88
89 if self.show_target_widget:
90@@ -1519,10 +1530,8 @@
91 if self.context.target_uses_malone:
92 read_only_field_names = []
93
94- if not self.userCanEditMilestone():
95+ if not self.user_has_privileges:
96 read_only_field_names.append("milestone")
97-
98- if not self.userCanEditImportance():
99 read_only_field_names.append("importance")
100 else:
101 editable_field_names = self.editable_field_names
102@@ -1532,20 +1541,6 @@
103
104 return read_only_field_names
105
106- def userCanEditMilestone(self):
107- """Can the user edit the Milestone field?
108-
109- If yes, return True, otherwise return False.
110- """
111- return self.context.userCanEditMilestone(self.user)
112-
113- def userCanEditImportance(self):
114- """Can the user edit the Importance field?
115-
116- If yes, return True, otherwise return False.
117- """
118- return self.context.userCanEditImportance(self.user)
119-
120 def validate(self, data):
121 if self.show_sourcepackagename_widget and 'sourcepackagename' in data:
122 data['target'] = self.context.distroseries
123@@ -3776,7 +3771,8 @@
124 return True
125
126
127-class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin):
128+class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
129+ BugTaskPrivilegeMixin):
130 """Browser class for rendering a bugtask row on the bug page."""
131
132 is_conjoined_slave = None
133@@ -3824,7 +3820,7 @@
134 target_link_title=self.target_link_title,
135 user_can_delete=self.user_can_delete_bugtask,
136 delete_link=delete_link,
137- user_can_edit_importance=self.user_can_edit_importance,
138+ user_can_edit_importance=self.user_has_privileges,
139 importance_css_class='importance' + self.context.importance.name,
140 importance_title=self.context.importance.title,
141 # We always look up all milestones, so there's no harm
142@@ -3963,9 +3959,7 @@
143
144 If yes, return True, otherwise return False.
145 """
146- bugtask = self.context
147- return (self.user_can_edit_status
148- and bugtask.userCanEditImportance(self.user))
149+ return self.user_can_edit_status and self.user_has_privileges
150
151 @cachedproperty
152 def user_can_edit_status(self):
153@@ -3990,14 +3984,6 @@
154 return self.user is not None
155
156 @cachedproperty
157- def user_can_edit_milestone(self):
158- """Can the user edit the Milestone field?
159-
160- If yes, return True, otherwise return False.
161- """
162- return self.context.userCanEditMilestone(self.user)
163-
164- @cachedproperty
165 def user_can_delete_bugtask(self):
166 """Can the user delete the bug task?
167
168@@ -4071,9 +4057,9 @@
169 request=self.api_request)
170 if cx.milestone else None),
171 user_can_edit_assignee=self.user_can_edit_assignee,
172- user_can_edit_milestone=self.user_can_edit_milestone,
173+ user_can_edit_milestone=self.user_has_privileges,
174 user_can_edit_status=self.user_can_edit_status,
175- user_can_edit_importance=self.user_can_edit_importance,
176+ user_can_edit_importance=self.user_has_privileges,
177 )
178
179
180
181=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
182--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-19 05:44:39 +0000
183+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-21 00:29:30 +0000
184@@ -185,7 +185,7 @@
185 self.invalidate_caches(bug.default_bugtask)
186 self.getUserBrowser(url, owner)
187 # At least 20 of these should be removed.
188- self.assertThat(recorder, HasQueryCount(LessThan(100)))
189+ self.assertThat(recorder, HasQueryCount(LessThan(101)))
190 count_with_no_branches = recorder.count
191 for sp in sourcepackages:
192 self.makeLinkedBranchMergeProposal(sp, bug, owner)
193
194=== modified file 'lib/lp/bugs/configure.zcml'
195--- lib/lp/bugs/configure.zcml 2011-11-16 07:25:58 +0000
196+++ lib/lp/bugs/configure.zcml 2011-11-21 00:29:30 +0000
197@@ -199,8 +199,7 @@
198 isSubscribed
199 getPackageComponent
200 maybeConfirm
201- userCanEditImportance
202- userCanEditMilestone
203+ userHasPrivileges
204 userCanSetAnyAssignee
205 userCanUnassign"/>
206 <require
207
208=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
209--- lib/lp/bugs/interfaces/bugtask.py 2011-11-04 18:55:42 +0000
210+++ lib/lp/bugs/interfaces/bugtask.py 2011-11-21 00:29:30 +0000
211@@ -913,11 +913,12 @@
212 not a package task, returns None.
213 """
214
215- def userCanEditMilestone(user):
216- """Can the user edit the Milestone field?"""
217+ def userHasPrivileges(user):
218+ """Is the user a privledged one, allowed to changed details on a
219+ bug?.
220
221- def userCanEditImportance(user):
222- """Can the user edit the Importance field?"""
223+ :return: A boolean.
224+ """
225
226
227 # Set schemas that were impossible to specify during the definition of
228
229=== modified file 'lib/lp/bugs/model/bugtask.py'
230--- lib/lp/bugs/model/bugtask.py 2011-11-19 05:44:39 +0000
231+++ lib/lp/bugs/model/bugtask.py 2011-11-21 00:29:30 +0000
232@@ -155,6 +155,7 @@
233 from lp.registry.interfaces.product import IProduct
234 from lp.registry.interfaces.productseries import IProductSeries
235 from lp.registry.interfaces.projectgroup import IProjectGroup
236+from lp.registry.interfaces.role import IPersonRoles
237 from lp.registry.interfaces.sourcepackage import ISourcePackage
238 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
239 from lp.registry.model.pillar import pillar_sort_key
240@@ -835,7 +836,7 @@
241
242 def transitionToMilestone(self, new_milestone, user):
243 """See `IBugTask`."""
244- if not self.userCanEditMilestone(user):
245+ if not self.userHasPrivileges(user):
246 raise UserCannotEditBugTaskMilestone(
247 "User does not have sufficient permissions "
248 "to edit the bug task milestone.")
249@@ -844,7 +845,7 @@
250
251 def transitionToImportance(self, new_importance, user):
252 """See `IBugTask`."""
253- if not self.userCanEditImportance(user):
254+ if not self.userHasPrivileges(user):
255 raise UserCannotEditBugTaskImportance(
256 "User does not have sufficient permissions "
257 "to edit the bug task importance.")
258@@ -912,15 +913,10 @@
259 def canTransitionToStatus(self, new_status, user):
260 """See `IBugTask`."""
261 new_status = normalize_bugtask_status(new_status)
262- celebrities = getUtility(ILaunchpadCelebrities)
263 if (self.status == BugTaskStatus.FIXRELEASED and
264 (user.id == self.bug.ownerID or user.inTeam(self.bug.owner))):
265 return True
266- elif (user.inTeam(self.pillar.bug_supervisor) or
267- user.inTeam(self.pillar.owner) or
268- user.id == celebrities.bug_watch_updater.id or
269- user.id == celebrities.bug_importer.id or
270- user.id == celebrities.janitor.id):
271+ elif self.userHasPrivileges(user):
272 return True
273 else:
274 return (self.status not in (
275@@ -1065,20 +1061,6 @@
276 if new_status < BugTaskStatus.FIXRELEASED:
277 self.date_fix_released = None
278
279- def _userCanSetAssignee(self, user):
280- """Used by methods to check if user can assign or unassign bugtask."""
281- celebrities = getUtility(ILaunchpadCelebrities)
282- return (
283- user.inTeam(self.pillar.bug_supervisor) or
284- user.inTeam(self.pillar.owner) or
285- user.inTeam(self.pillar.driver) or
286- (self.distroseries is not None and
287- user.inTeam(self.distroseries.driver)) or
288- (self.productseries is not None and
289- user.inTeam(self.productseries.driver)) or
290- user.inTeam(celebrities.admin)
291- or user == celebrities.bug_importer)
292-
293 def userCanSetAnyAssignee(self, user):
294 """See `IBugTask`."""
295 if user is None:
296@@ -1086,7 +1068,7 @@
297 elif self.pillar.bug_supervisor is None:
298 return True
299 else:
300- return self._userCanSetAssignee(user)
301+ return self.userHasPrivileges(user)
302
303 def userCanUnassign(self, user):
304 """True if user can set the assignee to None.
305@@ -1096,7 +1078,7 @@
306 Launchpad admins can always unassign.
307 """
308 return user is not None and (
309- user.inTeam(self.assignee) or self._userCanSetAssignee(user))
310+ user.inTeam(self.assignee) or self.userHasPrivileges(user))
311
312 def canTransitionToAssignee(self, assignee):
313 """See `IBugTask`."""
314@@ -1364,25 +1346,31 @@
315 else:
316 return None
317
318- def _userIsPillarEditor(self, user):
319- """Can the user edit this tasks's pillar?"""
320- if user is None:
321+ def userHasPrivileges(self, user):
322+ """See `IBugTask`."""
323+ if not user:
324 return False
325- pillar = self.pillar
326- return ((pillar.bug_supervisor is not None and
327- user.inTeam(pillar.bug_supervisor)) or
328- pillar.userCanEdit(user))
329-
330- def userCanEditMilestone(self, user):
331- """See `IBugTask`."""
332- return self._userIsPillarEditor(user)
333-
334- def userCanEditImportance(self, user):
335- """See `IBugTask`."""
336- celebs = getUtility(ILaunchpadCelebrities)
337- return (self._userIsPillarEditor(user) or
338- user == celebs.bug_watch_updater or
339- user == celebs.bug_importer)
340+ role = IPersonRoles(user)
341+ # Admins can always change bug details.
342+ if role.in_admin:
343+ return True
344+
345+ # Similar to admins, the Bug Watch Updater, Bug Importer and
346+ # Janitor can always change bug details.
347+ if (
348+ role.in_bug_watch_updater or role.in_bug_importer or
349+ role.in_janitor):
350+ return True
351+
352+ # Otherwise, if you're a member of the pillar owner, drivers, or the
353+ # bug supervisor, you can change bug details.
354+ return (
355+ role.isOwner(self.pillar) or role.isOneOfDrivers(self.pillar) or
356+ role.isBugSupervisor(self.pillar) or
357+ (self.distroseries is not None and
358+ role.isDriver(self.distroseries)) or
359+ (self.productseries is not None and
360+ role.isDriver(self.productseries)))
361
362 def __repr__(self):
363 return "<BugTask for bug %s on %r>" % (self.bugID, self.target)
364
365=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
366--- lib/lp/bugs/model/tests/test_bugtask.py 2011-11-14 12:23:59 +0000
367+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-11-21 00:29:30 +0000
368@@ -2590,3 +2590,68 @@
369 # Check the delete really worked.
370 with person_logged_in(removeSecurityProxy(db_bug).owner):
371 self.assertEqual([db_bug.default_bugtask], db_bug.bugtasks)
372+
373+
374+class TestBugTaskUserHasPriviliges(TestCaseWithFactory):
375+
376+ layer = DatabaseFunctionalLayer
377+
378+ def setUp(self):
379+ super(TestBugTaskUserHasPriviliges, self).setUp()
380+ self.celebrities = getUtility(ILaunchpadCelebrities)
381+
382+ def test_admin_is_allowed(self):
383+ # An admin always has privileges.
384+ bugtask = self.factory.makeBugTask()
385+ self.assertTrue(bugtask.userHasPrivileges(self.celebrities.admin))
386+
387+ def test_bug_celebrities_are_allowed(self):
388+ # The three bug celebrities (bug watcher, bug importer and
389+ # janitor always have privileges.
390+ bugtask = self.factory.makeBugTask()
391+ for celeb in (
392+ self.celebrities.bug_watch_updater,
393+ self.celebrities.bug_importer, self.celebrities.janitor):
394+ self.assertTrue(bugtask.userHasPrivileges(celeb))
395+
396+ def test_pillar_owner_is_allowed(self):
397+ # The pillar owner has privileges.
398+ pillar = self.factory.makeProduct()
399+ bugtask = self.factory.makeBugTask(target=pillar)
400+ self.assertTrue(bugtask.userHasPrivileges(pillar.owner))
401+
402+ def test_pillar_driver_is_allowed(self):
403+ # The pillar driver has privileges.
404+ pillar = self.factory.makeProduct()
405+ removeSecurityProxy(pillar).driver = self.factory.makePerson()
406+ bugtask = self.factory.makeBugTask(target=pillar)
407+ self.assertTrue(bugtask.userHasPrivileges(pillar.driver))
408+
409+ def test_pillar_bug_supervisor(self):
410+ # The pillar bug supervisor has privileges.
411+ pillar = self.factory.makeProduct()
412+ bugsupervisor = self.factory.makePerson()
413+ removeSecurityProxy(pillar).setBugSupervisor(
414+ bugsupervisor, self.celebrities.admin)
415+ bugtask = self.factory.makeBugTask(target=pillar)
416+ self.assertTrue(bugtask.userHasPrivileges(bugsupervisor))
417+
418+ def test_productseries_driver_is_allowed(self):
419+ # The series driver has privileges.
420+ series = self.factory.makeProductSeries()
421+ removeSecurityProxy(series).driver = self.factory.makePerson()
422+ bugtask = self.factory.makeBugTask(target=series)
423+ self.assertTrue(bugtask.userHasPrivileges(series.driver))
424+
425+ def test_distroseries_driver_is_allowed(self):
426+ # The series driver has privileges.
427+ distroseries = self.factory.makeDistroSeries()
428+ removeSecurityProxy(distroseries).driver = self.factory.makePerson()
429+ bugtask = self.factory.makeBugTask(target=distroseries)
430+ self.assertTrue(bugtask.userHasPrivileges(distroseries.driver))
431+
432+ def test_random_has_no_privileges(self):
433+ # Joe Random has no privileges.
434+ bugtask = self.factory.makeBugTask()
435+ self.assertFalse(
436+ bugtask.userHasPrivileges(self.factory.makePerson()))
437
438=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt'
439--- lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2011-11-14 08:30:52 +0000
440+++ lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2011-11-21 00:29:30 +0000
441@@ -29,11 +29,9 @@
442 Traceback (most recent call last):
443 ...
444 LinkNotFoundError
445+
446 >>> print admin_browser.getLink('Low', index=1).url
447- Traceback (most recent call last):
448- ...
449- LinkNotFoundError
450-
451+ http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/1/+editstatus
452 >>> print admin_browser.getLink('New', index=1).url
453 http://bugs...dev/ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
454 >>> print admin_browser.getLink('Medium').url
455
456=== modified file 'lib/lp/bugs/templates/bugtask-edit-form.pt'
457--- lib/lp/bugs/templates/bugtask-edit-form.pt 2011-08-11 06:03:15 +0000
458+++ lib/lp/bugs/templates/bugtask-edit-form.pt 2011-11-21 00:29:30 +0000
459@@ -179,14 +179,14 @@
460 <td tal:content="structure view/widgets/status" />
461 <td title="Changeable only by a project maintainer or bug supervisor">
462 <span
463- tal:condition="not:view/userCanEditImportance"
464+ tal:condition="not:view/user_has_privileges"
465 class="sprite read-only"></span>
466 <tal:widget content="structure view/widgets/importance" />
467 </td>
468 <td tal:condition="view/widgets/milestone"
469 title="Changeable only by a project maintainer or bug supervisor">
470 <span
471- tal:condition="not:view/userCanEditMilestone"
472+ tal:condition="not:view/user_has_privileges"
473 class="sprite read-only"></span>
474 <tal:widget content="structure view/widgets/milestone" />
475 </td>
476
477=== modified file 'lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt'
478--- lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-04 18:55:42 +0000
479+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-21 00:29:30 +0000
480@@ -164,7 +164,7 @@
481 <div class="milestone-content"
482 tal:condition="data/target_has_milestones"
483 style="width: 100%; float: left">
484- <a tal:condition="view/user_can_edit_milestone"
485+ <a tal:condition="view/user_has_privileges"
486 class="nulltext addicon js-action sprite add"
487 tal:attributes="href data/edit_link;
488 style view/style_for_add_milestone">
489@@ -173,7 +173,7 @@
490 <a class="value"
491 tal:attributes="href context/milestone/fmt:url | nothing"
492 tal:content="context/milestone/title | nothing" />
493- <a tal:condition="view/user_can_edit_milestone"
494+ <a tal:condition="view/user_has_privileges"
495 tal:attributes="href data/edit_link"
496 ><img class="editicon" src="/@@/edit"
497 tal:attributes="style view/style_for_edit_milestone"