← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/delete-sprint into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/delete-sprint into lp:launchpad.

Commit message:
Add sprint deletion support.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #2888 in Launchpad itself: "cannot hide or remove sprints"
  https://bugs.launchpad.net/launchpad/+bug/2888

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/delete-sprint/+merge/322255
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/delete-sprint into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2017-03-20 00:03:52 +0000
+++ database/schema/security.cfg	2017-04-09 08:43:16 +0000
@@ -303,7 +303,7 @@
 public.specificationsubscription        = SELECT, INSERT, UPDATE, DELETE
 public.specificationworkitem            = SELECT, INSERT, UPDATE
 public.spokenin                         = SELECT, INSERT, DELETE
-public.sprint                           = SELECT, INSERT, UPDATE
+public.sprint                           = SELECT, INSERT, UPDATE, DELETE
 public.sprintattendance                 = SELECT, INSERT, UPDATE, DELETE
 public.sprintspecification              = SELECT, INSERT, UPDATE, DELETE
 public.structuralsubscription           = SELECT, INSERT, UPDATE, DELETE

=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml	2014-11-24 06:20:03 +0000
+++ lib/lp/blueprints/browser/configure.zcml	2017-04-09 08:43:16 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -78,6 +78,13 @@
         facet="overview"
         template="../../../lp/registry/templates/object-branding.pt"/>
     <browser:page
+        name="+delete"
+        for="lp.blueprints.interfaces.sprint.ISprint"
+        class="lp.blueprints.browser.sprint.SprintDeleteView"
+        permission="launchpad.Edit"
+        facet="overview"
+        template="../templates/sprint-delete.pt"/>
+    <browser:page
         name="+attend"
         for="lp.blueprints.interfaces.sprint.ISprint"
         class="lp.blueprints.browser.sprintattendance.SprintAttendanceAttendView"

=== modified file 'lib/lp/blueprints/browser/sprint.py'
--- lib/lp/blueprints/browser/sprint.py	2015-07-08 16:05:11 +0000
+++ lib/lp/blueprints/browser/sprint.py	2017-04-09 08:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Sprint views."""
@@ -9,6 +9,7 @@
     'SprintAddView',
     'SprintAttendeesCsvExportView',
     'SprintBrandingView',
+    'SprintDeleteView',
     'SprintEditView',
     'SprintFacets',
     'SprintMeetingExportView',
@@ -113,7 +114,7 @@
     usedfor = ISprint
     facet = 'overview'
     links = ['attendance', 'registration', 'attendee_export', 'edit',
-             'branding']
+             'branding', 'delete']
 
     def attendance(self):
         text = 'Register yourself'
@@ -143,6 +144,10 @@
         summary = 'Modify the imagery used to represent this meeting'
         return Link('+branding', text, summary, icon='edit')
 
+    @enabled_with_permission('launchpad.Edit')
+    def delete(self):
+        return Link('+delete', 'Delete sprint', icon='trash-icon')
+
 
 class SprintSpecificationsMenu(NavigationMenu,
                                HasSpecificationsMenuMixin):
@@ -365,6 +370,25 @@
         return canonical_url(self.context)
 
 
+class SprintDeleteView(LaunchpadFormView):
+    """Form for deleting sprints."""
+
+    schema = ISprint
+    field_names = []
+
+    @property
+    def label(self):
+        return smartquote('Delete "%s" sprint' % self.context.displayname)
+
+    page_title = label
+
+    @action("Delete sprint", name="delete")
+    def delete_action(self, action, data):
+        owner = self.context.owner
+        self.context.destroySelf()
+        self.next_url = canonical_url(owner)
+
+
 class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
     """Custom view class to process the results of this unusual page.
 

=== modified file 'lib/lp/blueprints/browser/tests/test_sprint.py'
--- lib/lp/blueprints/browser/tests/test_sprint.py	2015-09-29 01:38:34 +0000
+++ lib/lp/blueprints/browser/tests/test_sprint.py	2017-04-09 08:43:16 +0000
@@ -1,14 +1,19 @@
-# Copyright 2011-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for Sprint pages and views."""
 
 __metaclass__ = type
 
+from fixtures import FakeLogger
+from mechanize import LinkNotFoundError
 from testtools.matchers import Equals
+from zope.publisher.interfaces import NotFound
+from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
+from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
     BrowserTestCase,
     RequestTimelineCollector,
@@ -55,3 +60,37 @@
         with RequestTimelineCollector() as recorder:
             self.getViewBrowser(sprint)
         self.assertThat(recorder, HasQueryCount(Equals(20)))
+
+
+class TestSprintDeleteView(BrowserTestCase):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_unauthorized(self):
+        # A user without edit access cannot delete a sprint.
+        self.useFixture(FakeLogger())
+        sprint = self.factory.makeSprint()
+        sprint_url = canonical_url(sprint)
+        other_person = self.factory.makePerson()
+        browser = self.getViewBrowser(sprint, user=other_person)
+        self.assertRaises(LinkNotFoundError, browser.getLink, "Delete sprint")
+        self.assertRaises(
+            Unauthorized, self.getUserBrowser, sprint_url + "/+delete",
+            user=other_person)
+
+    def test_delete_sprint(self):
+        # A sprint can be deleted, even if it has attendees and specifications.
+        self.useFixture(FakeLogger())
+        sprint = self.factory.makeSprint()
+        sprint_url = canonical_url(sprint)
+        owner_url = canonical_url(sprint.owner)
+        sprint.attend(
+            self.factory.makePerson(), sprint.time_starts, sprint.time_ends,
+            True)
+        blueprint = self.factory.makeSpecification()
+        blueprint.linkSprint(sprint, blueprint.owner).acceptBy(sprint.owner)
+        browser = self.getViewBrowser(sprint, user=sprint.owner)
+        browser.getLink("Delete sprint").click()
+        browser.getControl("Delete sprint").click()
+        self.assertEqual(owner_url, browser.url)
+        self.assertRaises(NotFound, browser.open, sprint_url)

=== modified file 'lib/lp/blueprints/doc/sprint.txt'
--- lib/lp/blueprints/doc/sprint.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/blueprints/doc/sprint.txt	2017-04-09 08:43:16 +0000
@@ -268,3 +268,10 @@
     mustard
 
 
+Sprint deletion
+---------------
+
+The sprint destroySelf() method deletes a sprint.
+
+    >>> ubz.destroySelf()
+    >>> sprintset["ubz"]

=== modified file 'lib/lp/blueprints/interfaces/sprint.py'
--- lib/lp/blueprints/interfaces/sprint.py	2015-01-06 12:13:28 +0000
+++ lib/lp/blueprints/interfaces/sprint.py	2017-04-09 08:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces for a Sprint (a meeting, conference or hack session).
@@ -30,8 +30,8 @@
     )
 
 from lp import _
+from lp.app.interfaces.launchpad import IHeadingContext
 from lp.app.validators.name import name_validator
-from lp.app.interfaces.launchpad import IHeadingContext
 from lp.blueprints.interfaces.specificationtarget import IHasSpecifications
 from lp.registry.interfaces.role import (
     IHasDrivers,
@@ -182,6 +182,12 @@
         in the `driver` attribute or an administrator.
         """
 
+    def destroySelf():
+        """Remove this sprint.
+
+        :raises CannotDeleteSprint: if the sprint cannot be deleted.
+        """
+
 
 class IHasSprints(Interface):
     """An interface for things that have lists of sprints associated with

=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py	2015-07-08 16:05:11 +0000
+++ lib/lp/blueprints/model/sprint.py	2017-04-09 08:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -298,6 +298,15 @@
                 user.inTeam(self.driver) or
                 user.inTeam(admins))
 
+    def destroySelf(self):
+        Store.of(self).find(
+            SprintSpecification,
+            SprintSpecification.sprint == self).remove()
+        Store.of(self).find(
+            SprintAttendance,
+            SprintAttendance.sprint == self).remove()
+        Store.of(self).remove(self)
+
 
 @implementer(ISprintSet)
 class SprintSet:

=== added file 'lib/lp/blueprints/templates/sprint-delete.pt'
--- lib/lp/blueprints/templates/sprint-delete.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/templates/sprint-delete.pt	2017-04-09 08:43:16 +0000
@@ -0,0 +1,17 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad">
+<body>
+<div metal:fill-slot="main">
+
+  <p>Sprint deletion is permanent.</p>
+
+  <div metal:use-macro="context/@@launchpad_form/form" />
+
+</div>
+</body>
+</html>


Follow ups