Merge lp:~cjwatson/launchpad/git-repository-delete-ui into lp:launchpad

Proposed by Colin Watson on 2015-05-19
Status: Merged
Merged at revision: 17522
Proposed branch: lp:~cjwatson/launchpad/git-repository-delete-ui
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-repository-delete
Diff against target: 305 lines (+216/-2)
4 files modified
lib/lp/code/browser/configure.zcml (+8/-1)
lib/lp/code/browser/gitrepository.py (+93/-0)
lib/lp/code/browser/tests/test_gitrepository.py (+65/-1)
lib/lp/code/templates/gitrepository-delete.pt (+50/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-repository-delete-ui
Reviewer Review Type Date Requested Status
William Grant code 2015-05-19 Approve on 2015-05-22
Review via email: mp+259488@code.launchpad.net

Commit message

Add a new GitRepository:+delete view, linked from a new edit menu on GitRepository.

Description of the change

Add a new GitRepository:+delete view, linked from a new edit menu on GitRepository.

To post a comment you must log in.
William Grant (wgrant) :
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/code/browser/configure.zcml'
2--- lib/lp/code/browser/configure.zcml 2015-05-12 17:20:22 +0000
3+++ lib/lp/code/browser/configure.zcml 2015-05-19 11:36:43 +0000
4@@ -753,7 +753,8 @@
5 <browser:menus
6 module="lp.code.browser.gitrepository"
7 classes="
8- GitRepositoryContextMenu"/>
9+ GitRepositoryContextMenu
10+ GitRepositoryEditMenu"/>
11 <browser:pages
12 for="lp.code.interfaces.gitrepository.IGitRepository"
13 class="lp.code.browser.gitrepository.GitRepositoryView"
14@@ -781,6 +782,12 @@
15 template="../templates/gitrepository-portlet-subscribers-content.pt"/>
16 <browser:page
17 for="lp.code.interfaces.gitrepository.IGitRepository"
18+ class="lp.code.browser.gitrepository.GitRepositoryDeletionView"
19+ permission="launchpad.Edit"
20+ name="+delete"
21+ template="../templates/gitrepository-delete.pt"/>
22+ <browser:page
23+ for="lp.code.interfaces.gitrepository.IGitRepository"
24 class="lp.code.browser.gitsubscription.GitSubscriptionAddView"
25 permission="launchpad.AnyPerson"
26 name="+subscribe"
27
28=== modified file 'lib/lp/code/browser/gitrepository.py'
29--- lib/lp/code/browser/gitrepository.py 2015-05-01 13:18:54 +0000
30+++ lib/lp/code/browser/gitrepository.py 2015-05-19 11:36:43 +0000
31@@ -9,6 +9,8 @@
32 'GitRefBatchNavigator',
33 'GitRepositoryBreadcrumb',
34 'GitRepositoryContextMenu',
35+ 'GitRepositoryDeletionView',
36+ 'GitRepositoryEditMenu',
37 'GitRepositoryNavigation',
38 'GitRepositoryURL',
39 'GitRepositoryView',
40@@ -18,16 +20,23 @@
41 from zope.interface import implements
42
43 from lp.app.browser.informationtype import InformationTypePortletMixin
44+from lp.app.browser.launchpadform import (
45+ action,
46+ LaunchpadFormView,
47+ )
48 from lp.app.errors import NotFoundError
49 from lp.code.interfaces.gitref import IGitRefBatchNavigator
50 from lp.code.interfaces.gitrepository import IGitRepository
51 from lp.services.config import config
52+from lp.services.propertycache import cachedproperty
53 from lp.services.webapp import (
54+ canonical_url,
55 ContextMenu,
56 enabled_with_permission,
57 LaunchpadView,
58 Link,
59 Navigation,
60+ NavigationMenu,
61 stepto,
62 )
63 from lp.services.webapp.authorization import (
64@@ -80,6 +89,20 @@
65 raise NotFoundError
66
67
68+class GitRepositoryEditMenu(NavigationMenu):
69+ """Edit menu for `IGitRepository`."""
70+
71+ usedfor = IGitRepository
72+ facet = "branches"
73+ title = "Edit Git repository"
74+ links = ["delete"]
75+
76+ @enabled_with_permission("launchpad.Edit")
77+ def delete(self):
78+ text = "Delete repository"
79+ return Link("+delete", text, icon="trash-icon")
80+
81+
82 class GitRepositoryContextMenu(ContextMenu):
83 """Context menu for `IGitRepository`."""
84
85@@ -165,3 +188,73 @@
86 def branches(self):
87 """All branches in this repository, sorted for display."""
88 return GitRefBatchNavigator(self, self.context)
89+
90+
91+class GitRepositoryDeletionView(LaunchpadFormView):
92+
93+ schema = IGitRepository
94+ field_names = []
95+
96+ @property
97+ def page_title(self):
98+ return "Delete repository %s" % self.context.display_name
99+
100+ label = page_title
101+
102+ @cachedproperty
103+ def display_deletion_requirements(self):
104+ """Normal deletion requirements, indication of permissions.
105+
106+ :return: A list of tuples of (item, action, reason, allowed)
107+ """
108+ reqs = []
109+ for item, (action, reason) in (
110+ self.context.getDeletionRequirements().iteritems()):
111+ allowed = check_permission("launchpad.Edit", item)
112+ reqs.append((item, action, reason, allowed))
113+ return reqs
114+
115+ def all_permitted(self):
116+ """Return True if all deletion requirements are permitted, else False.
117+
118+ Uses display_deletion_requirements as its source data.
119+ """
120+ return len([item for item, action, reason, allowed in
121+ self.display_deletion_requirements if not allowed]) == 0
122+
123+ @action("Delete", name="delete_repository",
124+ condition=lambda x, _: x.all_permitted())
125+ def delete_repository_action(self, action, data):
126+ repository = self.context
127+ if self.all_permitted():
128+ # Since the user is going to delete the repository, we need to
129+ # have somewhere valid to send them next.
130+ self.next_url = canonical_url(repository.target)
131+ message = "Repository %s deleted." % repository.unique_name
132+ self.context.destroySelf(break_references=True)
133+ self.request.response.addNotification(message)
134+ else:
135+ self.request.response.addNotification(
136+ "This repository cannot be deleted.")
137+ self.next_url = canonical_url(repository)
138+
139+ @property
140+ def repository_deletion_actions(self):
141+ """Return the repository deletion actions as a ZPT-friendly dict.
142+
143+ The keys are "delete" and "alter"; the values are dicts of
144+ "item", "reason" and "allowed".
145+ """
146+ row_dict = {"delete": [], "alter": []}
147+ for item, action, reason, allowed in (
148+ self.display_deletion_requirements):
149+ row = {"item": item,
150+ "reason": reason,
151+ "allowed": allowed,
152+ }
153+ row_dict[action].append(row)
154+ return row_dict
155+
156+ @property
157+ def cancel_url(self):
158+ return canonical_url(self.context)
159
160=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
161--- lib/lp/code/browser/tests/test_gitrepository.py 2015-05-14 13:57:51 +0000
162+++ lib/lp/code/browser/tests/test_gitrepository.py 2015-05-19 11:36:43 +0000
163@@ -6,11 +6,15 @@
164 __metaclass__ = type
165
166 from datetime import datetime
167+import doctest
168
169 from BeautifulSoup import BeautifulSoup
170 from fixtures import FakeLogger
171 import pytz
172-from testtools.matchers import Equals
173+from testtools.matchers import (
174+ DocTestMatches,
175+ Equals,
176+ )
177 from zope.component import getUtility
178 from zope.publisher.interfaces import NotFound
179 from zope.security.proxy import removeSecurityProxy
180@@ -32,6 +36,7 @@
181 from lp.testing.layers import DatabaseFunctionalLayer
182 from lp.testing.matchers import HasQueryCount
183 from lp.testing.pages import (
184+ get_feedback_messages,
185 setupBrowser,
186 setupBrowserForUser,
187 )
188@@ -179,3 +184,62 @@
189 recorder1, recorder2 = record_two_runs(
190 lambda: self.getMainText(repository, "+index"), create_ref, 10)
191 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
192+
193+
194+class TestGitRepositoryDeletionView(BrowserTestCase):
195+
196+ layer = DatabaseFunctionalLayer
197+
198+ def test_repository_has_delete_link(self):
199+ # A newly-created repository has a "Delete repository" link.
200+ repository = self.factory.makeGitRepository()
201+ delete_url = canonical_url(
202+ repository, view_name="+delete", rootsite="code")
203+ browser = self.getViewBrowser(
204+ repository, "+index", rootsite="code", user=repository.owner)
205+ delete_link = browser.getLink("Delete repository")
206+ self.assertEqual(delete_url, delete_link.url)
207+
208+ def test_warning_message(self):
209+ # The deletion view informs the user what will happen if they delete
210+ # the repository.
211+ repository = self.factory.makeGitRepository()
212+ name = repository.display_name
213+ text = self.getMainText(
214+ repository, "+delete", rootsite="code", user=repository.owner)
215+ self.assertThat(
216+ text, DocTestMatches(
217+ "Delete repository %s ...\n"
218+ "Repository deletion is permanent.\n"
219+ "or Cancel" % name,
220+ flags=(doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)))
221+
222+ def test_next_url(self):
223+ # Deleting a repository takes the user back to the code listing for
224+ # the target, and shows a notification message.
225+ project = self.factory.makeProduct()
226+ project_url = canonical_url(project, rootsite="code")
227+ repository = self.factory.makeGitRepository(target=project)
228+ name = repository.unique_name
229+ browser = self.getViewBrowser(
230+ repository, "+delete", rootsite="code", user=repository.owner)
231+ browser.getControl("Delete").click()
232+ self.assertEqual(project_url, browser.url)
233+ self.assertEqual(
234+ ["Repository %s deleted." % name],
235+ get_feedback_messages(browser.contents))
236+
237+ def test_next_url_personal(self):
238+ # Deleting a personal repository takes the user back to the code
239+ # listing for the owner, and shows a notification message.
240+ owner = self.factory.makePerson()
241+ owner_url = canonical_url(owner, rootsite="code")
242+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
243+ name = repository.unique_name
244+ browser = self.getViewBrowser(
245+ repository, "+delete", rootsite="code", user=repository.owner)
246+ browser.getControl("Delete").click()
247+ self.assertEqual(owner_url, browser.url)
248+ self.assertEqual(
249+ ["Repository %s deleted." % name],
250+ get_feedback_messages(browser.contents))
251
252=== added file 'lib/lp/code/templates/gitrepository-delete.pt'
253--- lib/lp/code/templates/gitrepository-delete.pt 1970-01-01 00:00:00 +0000
254+++ lib/lp/code/templates/gitrepository-delete.pt 2015-05-19 11:36:43 +0000
255@@ -0,0 +1,50 @@
256+<html
257+ xmlns="http://www.w3.org/1999/xhtml"
258+ xmlns:tal="http://xml.zope.org/namespaces/tal"
259+ xmlns:metal="http://xml.zope.org/namespaces/metal"
260+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
261+ metal:use-macro="view/macro:page/main_only"
262+ i18n:domain="launchpad">
263+<body>
264+
265+ <div metal:fill-slot="main">
266+
267+ <tal:deletelist condition="view/repository_deletion_actions/delete">
268+ The following items must be <em>deleted</em>:
269+ <ul id="deletion-items">
270+ <tal:actions repeat="row view/repository_deletion_actions/delete">
271+ <li>
272+ <img src="/@@/no" title="Insufficient privileges"
273+ tal:condition="not:row/allowed"/>
274+ <tal:item tal:content="structure row/item/fmt:link" />
275+ (<tal:reason tal:content="row/reason" />)
276+ </li>
277+ </tal:actions>
278+ </ul>
279+ </tal:deletelist>
280+ <tal:alterlist condition="view/repository_deletion_actions/alter">
281+ <div>The following items will be <em>updated</em>:</div>
282+ <ul>
283+ <tal:actions repeat="row view/repository_deletion_actions/alter">
284+ <li>
285+ <img src="/@@/no" title="Insufficient privileges"
286+ tal:condition="not:row/allowed"/>
287+ <tal:item tal:content="structure row/item/fmt:link" />
288+ (<tal:reason tal:content="row/reason" />)
289+ </li>
290+ </tal:actions>
291+ </ul>
292+ </tal:alterlist>
293+ <p tal:condition="view/all_permitted">
294+ Repository deletion is permanent.
295+ </p>
296+ <p tal:condition="not:view/all_permitted">
297+ You do not have permission to make all the changes required to delete
298+ this repository.
299+ </p>
300+
301+ <div metal:use-macro="context/@@launchpad_form/form" />
302+
303+</div>
304+</body>
305+</html>