Merge lp:~wallyworld/launchpad/delete-bugtasks-1324 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14176
Proposed branch: lp:~wallyworld/launchpad/delete-bugtasks-1324
Merge into: lp:launchpad
Diff against target: 486 lines (+275/-4) (has conflicts)
8 files modified
lib/canonical/launchpad/permissions.zcml (+3/-0)
lib/lp/bugs/configure.zcml (+3/-0)
lib/lp/bugs/interfaces/bugtask.py (+29/-1)
lib/lp/bugs/model/bugtask.py (+23/-0)
lib/lp/bugs/model/tests/test_bugtask.py (+157/-0)
lib/lp/bugs/security.py (+51/-0)
lib/lp/services/features/flags.py (+4/-0)
lib/lp/services/features/rulesource.py (+5/-3)
Text conflict in lib/lp/bugs/model/tests/test_bugtask.py
To merge this branch: bzr merge lp:~wallyworld/launchpad/delete-bugtasks-1324
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Данило Шеган (community) Needs Fixing
Review via email: mp+79541@code.launchpad.net

Commit message

Add a bugtask delete method to the IBugTask API and also export to the web service.

Description of the change

Add a bugtask delete method to the IBugTask API and also export to the web service.

A followup branch will add an entry to the bug activity log when a task is deleted and an email will be sent to the bug supervisor of the pillar for which the task is deleted.

== Preimplementation ==

Talked with lifeless and wgrant. lifeless was concerned about deleting a bugtask if it were a conjoined master/slave. wgrant said this doesn't matter due to recent changes to allow such 'orphaned' bugtasks.

== Implementation ==

The delete implementation was done in a destroySelf method on BugTask. This fits with how other entities are deleted in Launchpad (even though my preference is for the lifecycle of entities to be managed by a service based interface rather than the entiy itself). In terms of security, a new Launchpad permission is introduced - "launchpad.Delete". I feel it is better more semantically correct to use this new permission rather than just piggyback onto the launchpad.Edit permission.
A security adaptor is provided to check the necessary role based permissions. A new interface is introduced to allow the security and permissions to be wired up that that everything works in both the server side and web service api - IBugTaskDelete.

There are also other business rules which stop a bugtask from being deleted eg the last bugtask cannot be deleted. These rules are checked in the canBeDeleted() method and a CannotDeleteBugtask exception is raised in such cases.

A feature flag is used to hide this operation - 'disclosure.delete_bugtask.enabled'. During testing, a problem was found with the feature flag infrastructure. The StormFeatureRulesSource.setAllRules() method needs to call flush(), or else the rules passed in when the fixture is set up are not guaranteed to be in the database when required. A case in point: the check_permission method uses a @block_implicit_flushes decorator.

== Demo and QA ==

An example script to invoke the api using launchpadlib:

from launchpadlib.launchpad import Launchpad

def test_delete_bugtask(bug_num, pillar_name):
    lp = Launchpad.login_with(
        'testing', service_root=uris.LPNET_SERVICE_ROOT, version='devel')
    bug = lp.bugs[bug_num]
    bugtasks = bug.bug_tasks
    if len(bugtasks) == 1:
        print "This bug has no extra bugtasks to be deleted."
    else:
        for bugtask in bugtasks:
            if bugtask.target.name == pillar_name:
                bugtask.lp_delete()
                print "deleted task for %s on bug %d." % (pillar_name, bug_num)

== Tests ==

Add a new testcase TestBugTaskDeletion with some tests:

test_cannot_delete_if_not_logged_in
test_unauthorised_cannot_delete
test_pillar_owner_can_delete
test_bug_supervisor_can_delete
test_task_reporter_can_delete
test_cannot_delete_only_bugtask
test_delete_bugtask
test_delete_default_bugtask
test_bug_heat_updated

Add a testcase for the web service interface: TestWebService

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/permissions.zcml
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/security.py
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/model/bugtask.py
  lib/lp/bugs/model/tests/test_bugtask.py
  lib/lp/services/features/flags.py
  lib/lp/services/features/rulesource.py

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :

Looks pretty good: very nice to see this happening.

I do have a few observations/questions that I'd like addressed first.

If concerns were raised about conjoined bug tasks, I suppose it would be nice to have a test for that, though I believe nothing would really be affected (a slave would lose a master and vice versa, but that's about it).

Next, I wonder if there was a good reason not to use launchpad.Admin privilege instead? I.e. the way you implement Delete for BugTask seems limited to checking permissions on the context. Real admins (like LOSAs) will not be able to delete bug tasks, if I am reading it correctly.

I also don't feel like checking for feature flags belongs in the permissions code: you don't have a permission because feature is not enabled? Just sounds wrong to me, no big deal otherwise. Tied to that, perhaps tests should ensure UnauthorizedError is raised when delete is attempted instead of using check_permission as well?

Also, I'd prefer if tests which contain logic for when both feature flag is on and off are split into two separate tests each.

Also, is adding a store.flush() really necessary to the rulesource.py? If you need it for tests, maybe it's not necessary for production (and would introduce a query or two that we could avoid, if updates to a single object happen before and after flags are modified). Not a big deal though.

Thanks for the lint fixes :)

review: Needs Fixing
Revision history for this message
Ian Booth (wallyworld) wrote :

Thanks for the thorough review :-)

>
> If concerns were raised about conjoined bug tasks, I suppose it would be nice
> to have a test for that, though I believe nothing would really be affected (a
> slave would lose a master and vice versa, but that's about it).
>

As discussed on irc, I think we are ok here. wgrant showed me a demo on qas where the code and ui didn't care if a conjoined pair is "broken"

> Next, I wonder if there was a good reason not to use launchpad.Admin privilege
> instead? I.e. the way you implement Delete for BugTask seems limited to
> checking permissions on the context. Real admins (like LOSAs) will not be able
> to delete bug tasks, if I am reading it correctly.
>

I think this is a good point to raise and we need to add support for launchpad.Admin to be able to delete bug tasks.

> I also don't feel like checking for feature flags belongs in the permissions
> code: you don't have a permission because feature is not enabled? Just sounds
> wrong to me, no big deal otherwise. Tied to that, perhaps tests should ensure
> UnauthorizedError is raised when delete is attempted instead of using
> check_permission as well?
>

My understanding of the requirements is that if the fflag is off, then it is to be treated as if the user doesn't have permission and the implementation does achieve that goal. If I am wrong, I'll change it.

> Also, I'd prefer if tests which contain logic for when both feature flag is on
> and off are split into two separate tests each.
>

As discussed on irc, the fflag will (hopefully) be temporary and when it's removed, the tests will be updated. So it seems ok to me to leave as is. It seems easier to reduce the setup duplication and just remove the fflag bit later on.

> Also, is adding a store.flush() really necessary to the rulesource.py? If you
> need it for tests, maybe it's not necessary for production (and would
> introduce a query or two that we could avoid, if updates to a single object
> happen before and after flags are modified). Not a big deal though.
>

It is needed for tests for sure. I don't think we use that same code path in prod so I don't believe it's an issue there. And even if we do, all we are doing is writing to the db sooner than is otherwise the case.

Revision history for this message
Ian Booth (wallyworld) wrote :

>
> > Next, I wonder if there was a good reason not to use launchpad.Admin
> privilege
> > instead? I.e. the way you implement Delete for BugTask seems limited to
> > checking permissions on the context. Real admins (like LOSAs) will not be
> able
> > to delete bug tasks, if I am reading it correctly.
> >
>
> I think this is a good point to raise and we need to add support for
> launchpad.Admin to be able to delete bug tasks.
>

I've added a security adapter to allow lp admins to also delete bug tasks.

Revision history for this message
Данило Шеган (danilo) wrote :

Since I don't see you on IRC and I might be leaving soon, here's what I typed there:
 * I don't think the way you introduced security adapter is sufficient: in my reading, it modifies the launchpad.Admin privilege for BugTask to match the launchpad.Delete privilege, but does not really allow admins to do the delete
 * your test is not really testing what I thought it should (i.e. log in as <email address hidden> and not someone holding a contextual launchpad.Admin permission [like target owner])
 * (or better yet, login_celebrity('admin') from the same module login is in)

Note that I might be wrong and your test would pass even with the permissions as-is because admins are always allowed. Which would indicate that we have plenty of permission definitions which check for is_admin needlessly.

If I am not very responsive, feel free to find a different reviewer: I wouldn't want this branch to be held back because of me! :)

Revision history for this message
Ian Booth (wallyworld) wrote :

> Since I don't see you on IRC and I might be leaving soon, here's what I typed
> there:
> * I don't think the way you introduced security adapter is sufficient: in my
> reading, it modifies the launchpad.Admin privilege for BugTask to match the
> launchpad.Delete privilege, but does not really allow admins to do the delete
> * your test is not really testing what I thought it should (i.e. log in as
> <email address hidden> and not someone holding a contextual launchpad.Admin
> permission [like target owner])
> * (or better yet, login_celebrity('admin') from the same module login is in)
>

You are right - my test was flawed and the security adaptor wasn't quite right. They're both fixed now.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for including Danilo's suggestion. This looks good to land.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/permissions.zcml'
2--- lib/canonical/launchpad/permissions.zcml 2010-12-10 23:10:09 +0000
3+++ lib/canonical/launchpad/permissions.zcml 2011-10-19 22:37:25 +0000
4@@ -27,6 +27,9 @@
5 id="launchpad.Append" title="Adding something" access_level="write" />
6
7 <permission
8+ id="launchpad.Delete" title="Deleting something" access_level="write" />
9+
10+ <permission
11 id="launchpad.Moderate" title="Moderate something" access_level="write" />
12
13 <permission
14
15=== modified file 'lib/lp/bugs/configure.zcml'
16--- lib/lp/bugs/configure.zcml 2011-10-05 18:02:45 +0000
17+++ lib/lp/bugs/configure.zcml 2011-10-19 22:37:25 +0000
18@@ -236,6 +236,9 @@
19 findSimilarBugs
20 getContributorInfo"/>
21 <require
22+ permission="launchpad.Delete"
23+ interface="lp.bugs.interfaces.bugtask.IBugTaskDelete"/>
24+ <require
25 permission="launchpad.Edit"
26 attributes="
27 setStatusFromDebbugs
28
29=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
30--- lib/lp/bugs/interfaces/bugtask.py 2011-10-11 11:11:10 +0000
31+++ lib/lp/bugs/interfaces/bugtask.py 2011-10-19 22:37:25 +0000
32@@ -17,6 +17,7 @@
33 'BugTaskStatus',
34 'BugTaskStatusSearch',
35 'BugTaskStatusSearchDisplay',
36+ 'CannotDeleteBugtask',
37 'DB_INCOMPLETE_BUGTASK_STATUSES',
38 'DB_UNRESOLVED_BUGTASK_STATUSES',
39 'DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY',
40@@ -24,6 +25,7 @@
41 'IAddBugTaskForm',
42 'IAddBugTaskWithProductCreationForm',
43 'IBugTask',
44+ 'IBugTaskDelete',
45 'IBugTaskDelta',
46 'IBugTaskSearch',
47 'IBugTaskSet',
48@@ -58,6 +60,7 @@
49 call_with,
50 error_status,
51 export_as_webservice_entry,
52+ export_destructor_operation,
53 export_read_operation,
54 export_write_operation,
55 exported,
56@@ -435,6 +438,15 @@
57 for item in DEFAULT_SEARCH_BUGTASK_STATUSES]
58
59
60+@error_status(httplib.BAD_REQUEST)
61+class CannotDeleteBugtask(Exception):
62+ """The bugtask cannot be deleted.
63+
64+ Raised when a user tries to delete a bugtask but the deletion cannot
65+ proceed because of a model constraint or other business rule violation.
66+ """
67+
68+
69 @error_status(httplib.UNAUTHORIZED)
70 class UserCannotEditBugTaskStatus(Unauthorized):
71 """User not permitted to change status.
72@@ -482,7 +494,23 @@
73 in a search for related bug tasks"""
74
75
76-class IBugTask(IHasDateCreated, IHasBug):
77+class IBugTaskDelete(Interface):
78+ """An interface for operations allowed with the Delete permission."""
79+ @export_destructor_operation()
80+ @call_with(who=REQUEST_USER)
81+ @operation_for_version('devel')
82+ def delete(who):
83+ """Delete this bugtask.
84+
85+ :param who: the user who is removing the bugtask.
86+ :raises: CannotDeleteBugtask if the bugtask cannot be deleted due to a
87+ business rule or other model constraint.
88+ :raises: Unauthorized if the user does not have permission
89+ to delete the bugtask.
90+ """
91+
92+
93+class IBugTask(IHasDateCreated, IHasBug, IBugTaskDelete):
94 """A bug needing fixing in a particular product or package."""
95 export_as_webservice_entry()
96
97
98=== modified file 'lib/lp/bugs/model/bugtask.py'
99--- lib/lp/bugs/model/bugtask.py 2011-10-19 20:19:27 +0000
100+++ lib/lp/bugs/model/bugtask.py 2011-10-19 22:37:25 +0000
101@@ -114,6 +114,7 @@
102 BugTaskSearchParams,
103 BugTaskStatus,
104 BugTaskStatusSearch,
105+ CannotDeleteBugtask,
106 DB_INCOMPLETE_BUGTASK_STATUSES,
107 DB_UNRESOLVED_BUGTASK_STATUSES,
108 get_bugtask_status,
109@@ -620,6 +621,28 @@
110 """
111 return self._status in RESOLVED_BUGTASK_STATUSES
112
113+ def canBeDeleted(self):
114+ num_bugtasks = Store.of(self).find(
115+ BugTask, bug=self.bug).count()
116+
117+ return num_bugtasks > 1
118+
119+ def delete(self, who=None):
120+ """See `IBugTask`."""
121+ if who is None:
122+ who = getUtility(ILaunchBag).user
123+
124+ if not self.canBeDeleted():
125+ raise CannotDeleteBugtask(
126+ "Cannot delete bugtask: %s" % self.title)
127+ bug = self.bug
128+ target = self.target
129+ self.destroySelf()
130+ del get_property_cache(bug).bugtasks
131+
132+ # When a task is deleted the bug's heat needs to be recalculated.
133+ target.recalculateBugHeatCache()
134+
135 def findSimilarBugs(self, user, limit=10):
136 """See `IBugTask`."""
137 if self.product is not None:
138
139=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
140--- lib/lp/bugs/model/tests/test_bugtask.py 2011-10-18 21:11:23 +0000
141+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-10-19 22:37:25 +0000
142@@ -4,11 +4,16 @@
143 __metaclass__ = type
144
145 from datetime import timedelta
146+import transaction
147 import unittest
148
149 from lazr.lifecycle.event import ObjectModifiedEvent
150 from lazr.lifecycle.snapshot import Snapshot
151+<<<<<<< TREE
152 from testtools.testcase import ExpectedException
153+=======
154+from lazr.restfulclient.errors import Unauthorized
155+>>>>>>> MERGE-SOURCE
156 from testtools.matchers import Equals
157 from zope.component import getUtility
158 from zope.event import notify
159@@ -21,8 +26,13 @@
160 any,
161 not_equals,
162 )
163+from canonical.launchpad.webapp.authorization import (
164+ check_permission,
165+ clear_cache,
166+ )
167 from canonical.launchpad.webapp.interfaces import ILaunchBag
168 from canonical.testing.layers import (
169+ AppServerLayer,
170 DatabaseFunctionalLayer,
171 LaunchpadZopelessLayer,
172 )
173@@ -34,6 +44,7 @@
174 BugTaskImportance,
175 BugTaskSearchParams,
176 BugTaskStatus,
177+ CannotDeleteBugtask,
178 DB_UNRESOLVED_BUGTASK_STATUSES,
179 IBugTaskSet,
180 RESOLVED_BUGTASK_STATUSES,
181@@ -61,12 +72,14 @@
182 )
183 from lp.registry.interfaces.product import IProductSet
184 from lp.registry.interfaces.projectgroup import IProjectGroupSet
185+from lp.services.features.testing import FeatureFixture
186 from lp.soyuz.interfaces.archive import ArchivePurpose
187 from lp.testing import (
188 ANONYMOUS,
189 EventRecorder,
190 feature_flags,
191 login,
192+ login_celebrity,
193 login_person,
194 logout,
195 normalize_whitespace,
196@@ -75,6 +88,7 @@
197 StormStatementRecorder,
198 TestCase,
199 TestCaseWithFactory,
200+ ws_object,
201 )
202 from lp.testing.factory import LaunchpadObjectFactory
203 from lp.testing.fakemethod import FakeMethod
204@@ -1456,6 +1470,119 @@
205 bug.default_bugtask.pillar.displayname, result['pillar_name'])
206
207
208+class TestBugTaskDeletion(TestCaseWithFactory):
209+ """Test the different cases that makes a bugtask deletable or not."""
210+
211+ layer = DatabaseFunctionalLayer
212+
213+ flags = {u"disclosure.delete_bugtask.enabled": u"on"}
214+
215+ def test_cannot_delete_if_not_logged_in(self):
216+ # You cannot delete a bug task if not logged in.
217+ bug = self.factory.makeBug()
218+ with FeatureFixture(self.flags):
219+ self.assertFalse(
220+ check_permission('launchpad.Delete', bug.default_bugtask))
221+
222+ def test_unauthorised_cannot_delete(self):
223+ # Unauthorised users cannot delete a bug task.
224+ bug = self.factory.makeBug()
225+ unauthorised = self.factory.makePerson()
226+ login_person(unauthorised)
227+ with FeatureFixture(self.flags):
228+ self.assertFalse(
229+ check_permission('launchpad.Delete', bug.default_bugtask))
230+
231+ def test_admin_can_delete(self):
232+ # With the feature flag on, an admin can delete a bug task.
233+ bug = self.factory.makeBug()
234+ login_celebrity('admin')
235+ with FeatureFixture(self.flags):
236+ self.assertTrue(
237+ check_permission('launchpad.Admin', bug.default_bugtask))
238+ # Admins can also the task even without the feature flag.
239+ clear_cache()
240+ self.assertTrue(
241+ check_permission('launchpad.Admin', bug.default_bugtask))
242+
243+ def test_pillar_owner_can_delete(self):
244+ # With the feature flag on, the pillar owner can delete a bug task.
245+ bug = self.factory.makeBug()
246+ login_person(bug.default_bugtask.pillar.owner)
247+ with FeatureFixture(self.flags):
248+ self.assertTrue(
249+ check_permission('launchpad.Delete', bug.default_bugtask))
250+ # They can't delete the task without the feature flag.
251+ clear_cache()
252+ self.assertFalse(
253+ check_permission('launchpad.Delete', bug.default_bugtask))
254+
255+ def test_bug_supervisor_can_delete(self):
256+ # With the feature flag on, the bug supervisor can delete a bug task.
257+ bug_supervisor = self.factory.makePerson()
258+ product = self.factory.makeProduct(bug_supervisor=bug_supervisor)
259+ bug = self.factory.makeBug(product=product)
260+ login_person(bug_supervisor)
261+ with FeatureFixture(self.flags):
262+ self.assertTrue(
263+ check_permission('launchpad.Delete', bug.default_bugtask))
264+ # They can't delete the task without the feature flag.
265+ clear_cache()
266+ self.assertFalse(
267+ check_permission('launchpad.Delete', bug.default_bugtask))
268+
269+ def test_task_reporter_can_delete(self):
270+ # With the feature flag on, the bug task reporter can delete bug task.
271+ bug = self.factory.makeBug()
272+ login_person(bug.default_bugtask.owner)
273+ with FeatureFixture(self.flags):
274+ self.assertTrue(
275+ check_permission('launchpad.Delete', bug.default_bugtask))
276+ # They can't delete the task without the feature flag.
277+ clear_cache()
278+ self.assertFalse(
279+ check_permission('launchpad.Delete', bug.default_bugtask))
280+
281+ def test_cannot_delete_only_bugtask(self):
282+ # The only bugtask cannot be deleted.
283+ bug = self.factory.makeBug()
284+ bugtask = bug.default_bugtask
285+ login_person(bugtask.owner)
286+ with FeatureFixture(self.flags):
287+ self.assertRaises(CannotDeleteBugtask, bugtask.delete)
288+
289+ def test_delete_bugtask(self):
290+ # A bugtask can be deleted.
291+ bugtask = self.factory.makeBugTask()
292+ bug = bugtask.bug
293+ login_person(bugtask.owner)
294+ with FeatureFixture(self.flags):
295+ bugtask.delete()
296+ self.assertEqual([bug.default_bugtask], bug.bugtasks)
297+
298+ def test_delete_default_bugtask(self):
299+ # The default bugtask can be deleted.
300+ bugtask = self.factory.makeBugTask()
301+ bug = bugtask.bug
302+ login_person(bug.default_bugtask.owner)
303+ with FeatureFixture(self.flags):
304+ bug.default_bugtask.delete()
305+ self.assertEqual([bugtask], bug.bugtasks)
306+ self.assertEqual(bugtask, bug.default_bugtask)
307+
308+ def test_bug_heat_updated(self):
309+ # Test that the bug heat is updated when a bugtask is deleted.
310+ bug = self.factory.makeBug()
311+ distro = self.factory.makeDistribution()
312+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
313+ login_person(distro.owner)
314+ dsp_task = bug.addTask(bug.owner, dsp)
315+ self.assertTrue(dsp.total_bug_heat > 0)
316+ with FeatureFixture(self.flags):
317+ dsp_task.delete()
318+ self.assertTrue(dsp.total_bug_heat == 0)
319+
320+
321 class TestConjoinedBugTasks(TestCaseWithFactory):
322 """Tests for conjoined bug task functionality."""
323
324@@ -2323,3 +2450,33 @@
325 "package in which the bug has not yet been reported."
326 % d.displayname,
327 validate_new_target, task.bug, d)
328+
329+
330+class TestWebservice(TestCaseWithFactory):
331+ """Tests for the webservice."""
332+
333+ layer = AppServerLayer
334+
335+ def test_delete_bugtask(self):
336+ """Test that a bugtask can be deleted with the feature flag on."""
337+ product = self.factory.makeProduct()
338+ owner = self.factory.makePerson()
339+ db_bugtask = self.factory.makeBugTask(target=product, owner=owner)
340+ db_bug = db_bugtask.bug
341+ transaction.commit()
342+ logout()
343+
344+ # It will fail without feature flag enabled.
345+ launchpad = self.factory.makeLaunchpadService(owner)
346+ bugtask = ws_object(launchpad, db_bugtask)
347+ self.assertRaises(Unauthorized, bugtask.lp_delete)
348+
349+ flags = {u"disclosure.delete_bugtask.enabled": u"on"}
350+ with FeatureFixture(flags):
351+ launchpad = self.factory.makeLaunchpadService(owner)
352+ bugtask = ws_object(launchpad, db_bugtask)
353+ bugtask.lp_delete()
354+ transaction.commit()
355+ # Check the delete really worked.
356+ with person_logged_in(removeSecurityProxy(db_bug).owner):
357+ self.assertEqual([db_bug.default_bugtask], db_bug.bugtasks)
358
359=== modified file 'lib/lp/bugs/security.py'
360--- lib/lp/bugs/security.py 2011-09-07 21:53:15 +0000
361+++ lib/lp/bugs/security.py 2011-10-19 22:37:25 +0000
362@@ -6,8 +6,11 @@
363 __metaclass__ = type
364 __all__ = []
365
366+from zope.component import getUtility
367+
368 from canonical.launchpad.interfaces.launchpad import IHasBug
369 from lp.services.messages.interfaces.message import IMessage
370+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
371 from lp.app.security import (
372 AnonymousAuthorization,
373 AuthorizationBase,
374@@ -19,9 +22,13 @@
375 from lp.bugs.interfaces.bugnomination import IBugNomination
376 from lp.bugs.interfaces.bugsubscription import IBugSubscription
377 from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
378+from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
379+from lp.bugs.interfaces.bugtask import IBugTaskDelete
380 from lp.bugs.interfaces.bugtracker import IBugTracker
381 from lp.bugs.interfaces.bugwatch import IBugWatch
382 from lp.bugs.interfaces.structuralsubscription import IStructuralSubscription
383+from lp.registry.interfaces.role import IHasOwner
384+from lp.services.features import getFeatureFlag
385
386
387 class EditBugNominationStatus(AuthorizationBase):
388@@ -47,6 +54,50 @@
389 return self.obj.bug.userCanView(user)
390
391
392+class DeleteBugTask(AuthorizationBase):
393+ permission = 'launchpad.Delete'
394+ usedfor = IBugTaskDelete
395+
396+ def checkAuthenticated(self, user):
397+ """Check that a user may delete a bugtask.
398+
399+ A user may delete a bugtask if:
400+ - The disclosure.delete_bugtask.enabled feature flag is enabled,
401+ and they are:
402+ - project maintainer
403+ - task creator
404+ - bug supervisor
405+ """
406+ if user is None:
407+ return False
408+
409+ # Admins can always delete bugtasks.
410+ if user.inTeam(getUtility(ILaunchpadCelebrities).admin):
411+ return True
412+
413+ delete_allowed = bool(getFeatureFlag(
414+ 'disclosure.delete_bugtask.enabled'))
415+ if not delete_allowed:
416+ return False
417+
418+ bugtask = self.obj
419+ owner = None
420+ if IHasOwner.providedBy(bugtask.pillar):
421+ owner = bugtask.pillar.owner
422+ bugsupervisor = None
423+ if IHasBugSupervisor.providedBy(bugtask.pillar):
424+ bugsupervisor = bugtask.pillar.bug_supervisor
425+ return (
426+ user.inTeam(owner) or
427+ user.inTeam(bugsupervisor) or
428+ user.inTeam(bugtask.owner))
429+
430+
431+class AdminDeleteBugTask(DeleteBugTask):
432+ """Launchpad admins can also delete bug tasks."""
433+ permission = 'launchpad.Admin'
434+
435+
436 class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):
437 permission = 'launchpad.View'
438 usedfor = IHasBug
439
440=== modified file 'lib/lp/services/features/flags.py'
441--- lib/lp/services/features/flags.py 2011-10-14 05:00:27 +0000
442+++ lib/lp/services/features/flags.py 2011-10-19 22:37:25 +0000
443@@ -143,6 +143,10 @@
444 ('Enables the auto subscribing and unsubscribing of users as a bug '
445 'transitions between public, private and security related states.'),
446 ''),
447+ ('disclosure.delete_bugtask.enabled',
448+ 'boolean',
449+ ('Enables bugtasks to be deleted by authorised users.'),
450+ ''),
451 ('bugs.autoconfirm.enabled_distribution_names',
452 'space delimited',
453 ('Enables auto-confirming bugtasks for distributions (and their '
454
455=== modified file 'lib/lp/services/features/rulesource.py'
456--- lib/lp/services/features/rulesource.py 2011-04-27 02:27:20 +0000
457+++ lib/lp/services/features/rulesource.py 2011-10-19 22:37:25 +0000
458@@ -87,7 +87,8 @@
459 def parseRules(self, text_form):
460 """Return a list of tuples for the parsed form of the text input.
461
462- For each non-blank line gives back a tuple of (flag, scope, priority, value).
463+ For each non-blank line gives back a tuple of
464+ (flag, scope, priority, value).
465
466 Returns a list rather than a generator so that you see any syntax
467 errors immediately.
468@@ -139,8 +140,8 @@
469
470 :param new_rules: List of (name, scope, priority, value) tuples.
471 """
472- # XXX: would be slightly better to only update rules as necessary so we keep
473- # timestamps, and to avoid the direct sql etc -- mbp 20100924
474+ # XXX: would be slightly better to only update rules as necessary so
475+ # we keep timestamps, and to avoid the direct sql etc -- mbp 20100924
476 store = getFeatureStore()
477 store.execute('DELETE FROM FeatureFlag')
478 for (flag, scope, priority, value) in new_rules:
479@@ -149,6 +150,7 @@
480 flag=unicode(flag),
481 value=value,
482 priority=priority))
483+ store.flush()
484
485
486 class NullFeatureRuleSource(FeatureRuleSource):