← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:delete-oci-project into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:delete-oci-project into launchpad:master.

Commit message:
Adding API to delete OCI projects

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1925079 in Launchpad itself: "Add possibility to delete a OCI project"
  https://bugs.launchpad.net/launchpad/+bug/1925079

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401424
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:delete-oci-project into launchpad:master.
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index bb0a785..b1ea1a2 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 __all__ = [
+    'CannotDeleteOCIProject',
     'IOCIProject',
     'IOCIProjectSet',
     'OCI_PROJECT_ALLOW_CREATE',
@@ -16,6 +17,7 @@ __all__ = [
 from lazr.restful.declarations import (
     call_with,
     error_status,
+    export_destructor_operation,
     export_factory_operation,
     exported,
     exported_as_webservice_entry,
@@ -60,6 +62,11 @@ from lp.services.fields import (
 OCI_PROJECT_ALLOW_CREATE = 'oci.project.create.enabled'
 
 
+@error_status(http_client.BAD_REQUEST)
+class CannotDeleteOCIProject(Exception):
+    """The OCIProject cannnot be deleted."""
+
+
 class IOCIProjectView(IHasGitRepositories, Interface):
     """IOCIProject attributes that require launchpad.View permission."""
 
@@ -160,6 +167,16 @@ class IOCIProjectEdit(Interface):
     def setOfficialRecipeStatus(recipe, status):
         """Change whether an OCI Recipe is official or not for this project."""
 
+    @export_destructor_operation()
+    @operation_for_version('devel')
+    def destroySelf():
+        """Delete this OCI project.
+
+        Any OCI recipe and git repository related to this OCI project should
+        be deleted beforehand. OCIProjectSeries objects are automatically
+        deleted.
+        """
+
 
 class IOCIProjectLegitimate(Interface):
     """IOCIProject methods that require launchpad.AnyLegitimatePerson
diff --git a/lib/lp/registry/interfaces/ociprojectseries.py b/lib/lp/registry/interfaces/ociprojectseries.py
index d51eebd..ee20e4b 100644
--- a/lib/lp/registry/interfaces/ociprojectseries.py
+++ b/lib/lp/registry/interfaces/ociprojectseries.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces to allow bug filing on multiple versions of an OCI Project."""
@@ -77,6 +77,9 @@ class IOCIProjectSeriesEditableAttributes(Interface):
 class IOCIProjectSeriesEdit(Interface):
     """IOCIProjectSeries attributes that require launchpad.Edit permission."""
 
+    def destroySelf():
+        """Delete this OCI project series."""
+
 
 @exported_as_webservice_entry(
     publish_web_link=True, as_of="devel", singular_name="oci_project_series")
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index e98b3ea..da0bb98 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -46,6 +46,7 @@ from lp.code.model.branchnamespace import (
 from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.ociproject import (
+    CannotDeleteOCIProject,
     IOCIProject,
     IOCIProjectSet,
     )
@@ -297,6 +298,28 @@ class OCIProject(BugTargetBase, StormBase):
         namespace = getUtility(IGitNamespaceSet).get(person, oci_project=self)
         return namespace.name
 
+    def destroySelf(self):
+        """See `IOCIProject`."""
+        from lp.oci.model.ocirecipe import OCIRecipe
+        from lp.code.model.gitrepository import GitRepository
+
+        exists_recipes = not IStore(OCIRecipe).find(
+            OCIRecipe,
+            OCIRecipe.oci_project == self).is_empty()
+        if exists_recipes:
+            raise CannotDeleteOCIProject("This OCI recipe contains recipes.")
+
+        git_repos = IStore(GitRepository).find(
+            GitRepository, GitRepository.oci_project == self)
+        if not git_repos.is_empty():
+            repos = ", ".join(repo.display_name for repo in git_repos)
+            raise CannotDeleteOCIProject(
+                "There are git repositories associated with this OCI project: "
+                + repos)
+        for series in self.series:
+            series.destroySelf()
+        IStore(self).remove(self)
+
 
 @implementer(IOCIProjectSet)
 class OCIProjectSet:
diff --git a/lib/lp/registry/model/ociprojectseries.py b/lib/lp/registry/model/ociprojectseries.py
index 31ccac3..5d935d6 100644
--- a/lib/lp/registry/model/ociprojectseries.py
+++ b/lib/lp/registry/model/ociprojectseries.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Model implementing `IOCIProjectSeries`."""
@@ -25,6 +25,7 @@ from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.database.constants import DEFAULT
 from lp.services.database.enumcol import DBEnum
+from lp.services.database.interfaces import IStore
 from lp.services.database.stormbase import StormBase
 
 
@@ -63,3 +64,6 @@ class OCIProjectSeries(StormBase):
         self.registrant = registrant
         self.status = status
         self.date_created = date_created
+
+    def destroySelf(self):
+        IStore(self).remove(self)
diff --git a/lib/lp/registry/tests/test_ociproject.py b/lib/lp/registry/tests/test_ociproject.py
index 0232b2e..e731961 100644
--- a/lib/lp/registry/tests/test_ociproject.py
+++ b/lib/lp/registry/tests/test_ociproject.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for `OCIProject` and `OCIProjectSet`."""
@@ -20,12 +20,16 @@ from zope.schema.vocabulary import getVocabularyRegistry
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
 from lp.registry.interfaces.ociproject import (
+    CannotDeleteOCIProject,
     IOCIProject,
     IOCIProjectSet,
     OCI_PROJECT_ALLOW_CREATE,
     )
 from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
+from lp.registry.model.ociproject import OCIProject
+from lp.services.database.interfaces import IStore
 from lp.services.features.testing import FeatureFixture
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.testing import (
@@ -133,6 +137,48 @@ class TestOCIProject(TestCaseWithFactory):
             'OCI project test-name for %s' % oci_project.pillar.display_name,
             oci_project.display_name)
 
+    def test_destroy_fails_if_there_are_recipes(self):
+        self.useFixture(FeatureFixture({
+            OCI_PROJECT_ALLOW_CREATE: 'on',
+            OCI_RECIPE_ALLOW_CREATE: 'on'}))
+        driver = self.factory.makePerson()
+        distribution = self.factory.makeDistribution(driver=driver)
+        oci_project = self.factory.makeOCIProject(pillar=distribution)
+
+        self.factory.makeOCIRecipe(oci_project=oci_project)
+        with person_logged_in(driver):
+            self.assertRaises(CannotDeleteOCIProject, oci_project.destroySelf)
+
+    def test_destroy_fails_if_there_are_git_repos(self):
+        driver = self.factory.makePerson()
+        distribution = self.factory.makeDistribution(driver=driver)
+        oci_project = self.factory.makeOCIProject(pillar=distribution)
+
+        self.factory.makeGitRepository(target=oci_project)
+
+        with person_logged_in(driver):
+            self.assertRaises(CannotDeleteOCIProject, oci_project.destroySelf)
+
+    def test_destroy_fails_for_non_driver_user(self):
+        driver = self.factory.makePerson()
+        distribution = self.factory.makeDistribution(driver=driver)
+        oci_project = self.factory.makeOCIProject(pillar=distribution)
+        with person_logged_in(self.factory.makePerson()):
+            self.assertRaises(
+                Unauthorized, getattr, oci_project, 'destroySelf')
+
+    def test_destroy(self):
+        driver = self.factory.makePerson()
+        distribution = self.factory.makeDistribution(driver=driver)
+        oci_project = self.factory.makeOCIProject(pillar=distribution)
+
+        with person_logged_in(driver):
+            oci_project.newSeries("name", "summary", registrant=driver)
+            oci_project.destroySelf()
+        self.assertEqual(
+            None, IStore(OCIProject).find(
+                OCIProject, OCIProject.id == oci_project.id).one())
+
 
 class TestOCIProjectSet(TestCaseWithFactory):
 
@@ -315,6 +361,18 @@ class TestOCIProjectWebservice(TestCaseWithFactory):
 
         self.assertCanCreateOCIProject(distro, self.person)
 
+    def test_delete(self):
+        with admin_logged_in():
+            distribution = self.factory.makeDistribution(driver=self.person)
+            oci_project = self.factory.makeOCIProject(pillar=distribution)
+        with person_logged_in(self.person):
+            url = api_url(oci_project)
+        webservice = self.webservice
+        response = webservice.delete(url, api_version='devel')
+        self.assertEqual(200, response.status)
+        response = webservice.get(url, api_version='devel')
+        self.assertEqual(404, response.status)
+
 
 class TestOCIProjectVocabulary(TestCaseWithFactory):
     layer = DatabaseFunctionalLayer

Follow ups