Merge ~ilasc/launchpad:registry-can-delete-branch into launchpad:master

Proposed by Ioana Lasc
Status: Merged
Merged at revision: 9b7925e10f31572b81a5e636e1ca3d66d307ec23
Proposed branch: ~ilasc/launchpad:registry-can-delete-branch
Merge into: launchpad:master
Diff against target: 213 lines (+111/-19)
5 files modified
lib/lp/code/browser/branch.py (+2/-2)
lib/lp/code/browser/configure.zcml (+1/-1)
lib/lp/code/browser/tests/test_branch.py (+93/-1)
lib/lp/code/interfaces/branch.py (+14/-14)
lib/lp/code/security.py (+1/-1)
Reviewer Review Type Date Requested Status
Andrey Fedoseev (community) Approve
Review via email: mp+433366@code.launchpad.net

Commit message

Registry experts can delete bzr branch

To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

I added some non-blocking comments

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/browser/branch.py b/lib/lp/code/browser/branch.py
2index 5ecce7e..0645172 100644
3--- a/lib/lp/code/browser/branch.py
4+++ b/lib/lp/code/browser/branch.py
5@@ -219,7 +219,7 @@ class BranchEditMenu(NavigationMenu):
6 text = "Change branch details"
7 return Link("+edit", text, icon="edit")
8
9- @enabled_with_permission("launchpad.Edit")
10+ @enabled_with_permission("launchpad.Moderate")
11 def delete(self):
12 text = "Delete branch"
13 return Link("+delete", text, icon="trash-icon")
14@@ -983,7 +983,7 @@ class BranchDeletionView(LaunchpadFormView):
15 for item, (operation, reason) in self.context.deletionRequirements(
16 eager_load=True
17 ).items():
18- allowed = check_permission("launchpad.Edit", item)
19+ allowed = check_permission("launchpad.Moderate", item)
20 reqs.append((item, operation, reason, allowed))
21 return reqs
22
23diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml
24index d0e3d9a..4d009ef 100644
25--- a/lib/lp/code/browser/configure.zcml
26+++ b/lib/lp/code/browser/configure.zcml
27@@ -468,7 +468,7 @@
28 name="+delete"
29 for="lp.code.interfaces.branch.IBranch"
30 class="lp.code.browser.branch.BranchDeletionView"
31- permission="launchpad.Edit"
32+ permission="launchpad.Moderate"
33 template="../templates/branch-delete.pt"/>
34 <browser:pages
35 for="lp.code.interfaces.branch.IBranch"
36diff --git a/lib/lp/code/browser/tests/test_branch.py b/lib/lp/code/browser/tests/test_branch.py
37index e8d914e..3276f09 100644
38--- a/lib/lp/code/browser/tests/test_branch.py
39+++ b/lib/lp/code/browser/tests/test_branch.py
40@@ -29,7 +29,7 @@ from lp.code.model.branchjob import BranchScanJob
41 from lp.code.tests.helpers import BranchHostingFixture
42 from lp.registry.enums import BranchSharingPolicy
43 from lp.registry.interfaces.accesspolicy import IAccessPolicySource
44-from lp.registry.interfaces.person import PersonVisibility
45+from lp.registry.interfaces.person import IPersonSet, PersonVisibility
46 from lp.services.beautifulsoup import BeautifulSoup
47 from lp.services.config import config
48 from lp.services.database.constants import UTC_NOW
49@@ -208,6 +208,36 @@ class TestBranchView(BrowserTestCase):
50 view = create_initialized_view(branch, "+index")
51 self.assertFalse(view.show_merge_links)
52
53+ def testDeleteBranch(self):
54+ expert = self.factory.makePerson(
55+ member_of=[getUtility(IPersonSet).getByName("registry")]
56+ )
57+ commercial_admin = self.factory.makeCommercialAdmin()
58+ random_user = self.factory.makePerson()
59+
60+ branch = self.factory.makeAnyBranch()
61+ branch_url = canonical_url(branch, view_name="+index", rootsite="code")
62+
63+ # the branch owner sees the Delete Branch link
64+ browser = self.getUserBrowser(branch_url, user=branch.owner)
65+ browser.open(branch_url)
66+ self.assertIn("Delete branch", browser.contents)
67+
68+ # a commercial admin sees the Delete Branch link
69+ browser = self.getUserBrowser(branch_url, user=commercial_admin)
70+ browser.open(branch_url)
71+ self.assertIn("Delete branch", browser.contents)
72+
73+ # a registry expert sees the Delete Branch link
74+ browser = self.getUserBrowser(branch_url, user=expert)
75+ browser.open(branch_url)
76+ self.assertIn("Delete branch", browser.contents)
77+
78+ # random user does not see the Delete Branch link
79+ browser = self.getUserBrowser(branch_url, user=random_user)
80+ browser.open(branch_url)
81+ self.assertNotIn("Delete branch", browser.contents)
82+
83 def testNoProductSeriesPushingTranslations(self):
84 # By default, a branch view shows no product series pushing
85 # translations to the branch.
86@@ -673,6 +703,68 @@ class TestBranchView(BrowserTestCase):
87 self.assertThat(recorder, HasQueryCount(Equals(30)))
88
89
90+class TestBranchDeletionView(BrowserTestCase):
91+
92+ layer = DatabaseFunctionalLayer
93+
94+ def test_owner_can_delete(self):
95+ branch = self.factory.makeAnyBranch()
96+ branch_name = branch.displayname
97+ branch_unique_name = branch.unique_name
98+ branch_url = canonical_url(
99+ branch, view_name="+delete", rootsite="code"
100+ )
101+ browser = self.getUserBrowser(branch_url, user=branch.owner)
102+ browser.open(branch_url)
103+ self.assertIn("Delete branch %s" % branch_name, browser.contents)
104+
105+ browser.getControl("Delete").click()
106+ self.assertIn(
107+ "Branch %s deleted." % branch_unique_name, browser.contents
108+ )
109+
110+ def test_registry_expert_can_delete(self):
111+ expert = self.factory.makePerson(
112+ member_of=[getUtility(IPersonSet).getByName("registry")]
113+ )
114+ branch = self.factory.makeAnyBranch()
115+ branch_name = branch.displayname
116+ branch_unique_name = branch.unique_name
117+ branch_url = canonical_url(
118+ branch, view_name="+delete", rootsite="code"
119+ )
120+ browser = self.getUserBrowser(branch_url, user=expert)
121+ browser.open(branch_url)
122+ self.assertIn("Delete branch %s" % branch_name, browser.contents)
123+ browser.getControl("Delete").click()
124+ self.assertIn(
125+ "Branch %s deleted." % branch_unique_name, browser.contents
126+ )
127+
128+ def test_commercial_admin_can_delete(self):
129+ commercial_admin = self.factory.makeCommercialAdmin()
130+ branch = self.factory.makeAnyBranch()
131+ branch_name = branch.displayname
132+ branch_unique_name = branch.unique_name
133+ branch_url = canonical_url(
134+ branch, view_name="+delete", rootsite="code"
135+ )
136+ browser = self.getUserBrowser(branch_url, user=commercial_admin)
137+ browser.open(branch_url)
138+ self.assertIn("Delete branch %s" % branch_name, browser.contents)
139+ browser.getControl("Delete").click()
140+ self.assertIn(
141+ "Branch %s deleted." % branch_unique_name, browser.contents
142+ )
143+
144+ def test_other_user_can_not_delete(self):
145+ branch = self.factory.makeAnyBranch()
146+ branch_url = canonical_url(
147+ branch, view_name="+delete", rootsite="code"
148+ )
149+ self.assertRaises(Unauthorized, self.getUserBrowser, branch_url)
150+
151+
152 class TestBranchRescanView(BrowserTestCase):
153
154 layer = DatabaseFunctionalLayer
155diff --git a/lib/lp/code/interfaces/branch.py b/lib/lp/code/interfaces/branch.py
156index 8caedc9..d579132 100644
157--- a/lib/lp/code/interfaces/branch.py
158+++ b/lib/lp/code/interfaces/branch.py
159@@ -1266,6 +1266,20 @@ class IBranchModerate(Interface):
160 A convenience function wrapper around unscan().
161 """
162
163+ @call_with(break_references=True)
164+ @export_destructor_operation()
165+ @operation_for_version("beta")
166+ def destroySelf(break_references=False):
167+ """Delete the specified branch.
168+
169+ BranchRevisions associated with this branch will also be deleted.
170+
171+ :param break_references: If supplied, break any references to this
172+ branch by deleting items with mandatory references and
173+ NULLing other references.
174+ :raise: CannotDeleteBranch if the branch cannot be deleted.
175+ """
176+
177
178 class IBranchEditableAttributes(Interface):
179 """IBranch attributes that can be edited.
180@@ -1401,20 +1415,6 @@ class IBranchEdit(IWebhookTarget):
181 branch.
182 """
183
184- @call_with(break_references=True)
185- @export_destructor_operation()
186- @operation_for_version("beta")
187- def destroySelf(break_references=False):
188- """Delete the specified branch.
189-
190- BranchRevisions associated with this branch will also be deleted.
191-
192- :param break_references: If supplied, break any references to this
193- branch by deleting items with mandatory references and
194- NULLing other references.
195- :raise: CannotDeleteBranch if the branch cannot be deleted.
196- """
197-
198
199 @exported_as_webservice_entry(plural_name="branches", as_of="beta")
200 class IBranch(
201diff --git a/lib/lp/code/security.py b/lib/lp/code/security.py
202index d6852ed..ddb230f 100644
203--- a/lib/lp/code/security.py
204+++ b/lib/lp/code/security.py
205@@ -206,7 +206,7 @@ class ModerateBranch(EditBranch):
206 pillar = branch.product or branch.distribution
207 if pillar is not None and user.inTeam(pillar.owner):
208 return True
209- return user.in_commercial_admin
210+ return user.in_commercial_admin or user.in_registry_experts
211
212
213 def can_upload_linked_package(person_role, branch):

Subscribers

People subscribed via source and target branches

to status/vote changes: