← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:ui-create-ociproject-for-projects into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ui-create-ociproject-for-projects into launchpad:master with ~pappacena/launchpad:gitrepo-ociproject-refactoring as a prerequisite.

Commit message:
UI to allow creating and editing OCI projects based on projects.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/384506
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ui-create-ociproject-for-projects into launchpad:master.
diff --git a/lib/lp/code/browser/tests/test_product.py b/lib/lp/code/browser/tests/test_product.py
index 360745e..d33a85e 100644
--- a/lib/lp/code/browser/tests/test_product.py
+++ b/lib/lp/code/browser/tests/test_product.py
@@ -426,3 +426,17 @@ class TestCanConfigureBranches(TestCaseWithFactory):
         login_person(product.owner)
         view = create_view(product, '+branches')
         self.assertTrue(view.can_configure_branches())
+
+
+class TestProductOverviewOCIProject(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_displays_create_oci_project_link(self):
+        product = self.factory.makeProduct()
+
+        browser = self.getUserBrowser(
+            canonical_url(product), user=product.owner)
+        text = extract_text(find_tag_by_id(browser.contents, 'global-actions'))
+
+        self.assertIn("Create an OCI Project", text)
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index b8dc1bb..949e074 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -2156,6 +2156,13 @@
         template="../templates/ociproject-new.pt"
         />
     <browser:page
+        name="+new-oci-project"
+        for="lp.registry.interfaces.product.IProduct"
+        class="lp.registry.browser.ociproject.OCIProjectAddView"
+        permission="launchpad.AnyPerson"
+        template="../templates/ociproject-new.pt"
+        />
+    <browser:page
         name="+search-oci-project"
         for="lp.registry.interfaces.distribution.IDistribution"
         class="lp.registry.browser.distribution.DistributionOCIProjectSearchView"
diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
index df7ac85..6ca8786 100644
--- a/lib/lp/registry/browser/ociproject.py
+++ b/lib/lp/registry/browser/ociproject.py
@@ -14,6 +14,8 @@ __all__ = [
     'OCIProjectNavigationMenu',
     ]
 
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.product import IProduct
 from zope.component import getUtility
 from zope.formlib import form
 from zope.interface import implementer
@@ -170,11 +172,18 @@ class OCIProjectEditView(LaunchpadEditFormView):
 
     schema = IOCIProject
     field_names = [
-        'distribution',
         'name',
         'official_recipe',
         ]
 
+    def setUpFields(self):
+        pillar = self.context.pillar
+        if IDistribution.providedBy(pillar):
+            self.field_names = ['distribution'] + self.field_names
+        elif IProduct.providedBy(pillar):
+            self.field_names = ['project'] + self.field_names
+        super(OCIProjectEditView, self).setUpFields()
+
     def extendFields(self):
         official_recipe = self.context.getOfficialRecipe()
         self.form_fields += form.Fields(
diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py
index c059dbb..e08a986 100644
--- a/lib/lp/registry/browser/product.py
+++ b/lib/lp/registry/browser/product.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for products."""
@@ -275,6 +275,10 @@ class ProductNavigation(
     def traverse_commercialsubscription(self, name):
         return self.context.commercial_subscription
 
+    @stepthrough('+oci')
+    def traverse_oci(self, name):
+        return self.context.getOCIProject(name)
+
     def traverse(self, name):
         return self.context.getSeries(name)
 
@@ -426,6 +430,7 @@ class ProductNavigationMenu(NavigationMenu):
     facet = 'overview'
     links = [
         'details',
+        'new_oci_project',
         'announcements',
         'downloads',
         ]
@@ -499,6 +504,11 @@ class ProductEditLinksMixin(StructuralSubscriptionMenuMixin):
     def sharing(self):
         return Link('+sharing', 'Sharing', icon='edit')
 
+    @enabled_with_permission('launchpad.Driver')
+    def new_oci_project(self):
+        text = 'Create an OCI Project'
+        return Link('+new-oci-project', text, icon='add')
+
 
 class IProductEditMenu(Interface):
     """A marker interface for the 'Change details' navigation menu."""
@@ -517,7 +527,8 @@ class ProductActionNavigationMenu(NavigationMenu, ProductEditLinksMixin):
 
     @cachedproperty
     def links(self):
-        links = ['edit', 'review_license', 'administer', 'sharing']
+        links = ['edit', 'review_license', 'administer', 'sharing',
+                 'new_oci_project']
         add_subscribe_link(links)
         return links
 
diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
index 5187cd6..c9aaa77 100644
--- a/lib/lp/registry/browser/tests/test_ociproject.py
+++ b/lib/lp/registry/browser/tests/test_ociproject.py
@@ -79,7 +79,7 @@ class TestOCIProjectView(BrowserTestCase):
 
     layer = DatabaseFunctionalLayer
 
-    def test_index(self):
+    def test_index_distribution_pillar(self):
         distribution = self.factory.makeDistribution(displayname="My Distro")
         oci_project = self.factory.makeOCIProject(
             pillar=distribution, ociprojectname="oci-name")
@@ -91,6 +91,18 @@ class TestOCIProjectView(BrowserTestCase):
             Name: oci-name
             """, self.getMainText(oci_project))
 
+    def test_index_project_pillar(self):
+        product = self.factory.makeProduct(displayname="My Project")
+        oci_project = self.factory.makeOCIProject(
+            pillar=product, ociprojectname="oci-name")
+        self.assertTextMatchesExpressionIgnoreWhitespace("""\
+            OCI project oci-name for My Project
+            .*
+            OCI project information
+            Project: My Project
+            Name: oci-name
+            """, self.getMainText(oci_project))
+
 
 class TestOCIProjectEditView(BrowserTestCase):
 
@@ -123,11 +135,39 @@ class TestOCIProjectEditView(BrowserTestCase):
         self.assertThat(
             "Distribution:\n%s\nEdit OCI project" % (
                 new_distribution.display_name),
-            MatchesTagText(content, "distribution"))
+            MatchesTagText(content, "pillar"))
         self.assertThat(
             "Name:\nnew-name\nEdit OCI project",
             MatchesTagText(content, "name"))
 
+    def test_edit_oci_project_change_project_pillar(self):
+        with admin_logged_in():
+            owner = self.factory.makePerson()
+            project = self.factory.makeProduct(owner=owner)
+            new_project = self.factory.makeProduct(owner=owner)
+            oci_project = self.factory.makeOCIProject(pillar=project)
+            new_project_name = new_project.name
+
+        browser = self.getViewBrowser(
+            oci_project, user=oci_project.pillar.owner)
+        browser.getLink("Edit OCI project").click()
+        browser.getControl(name="field.project").value = [new_project_name]
+        browser.getControl(name="field.name").value = "new-name"
+        browser.getControl("Update OCI project").click()
+
+        content = find_main_content(browser.contents)
+        with person_logged_in(owner):
+            self.assertEqual(
+                "OCI project new-name for %s" % new_project.display_name,
+                extract_text(content.h1))
+            self.assertThat(
+                "Project:\n%s\nEdit OCI project" % (
+                    new_project.display_name),
+                MatchesTagText(content, "pillar"))
+            self.assertThat(
+                "Name:\nnew-name\nEdit OCI project",
+                MatchesTagText(content, "name"))
+
     def test_edit_oci_project_ad_oci_project_admin(self):
         admin_person = self.factory.makePerson()
         admin_team = self.factory.makeTeam(members=[admin_person])
@@ -153,7 +193,7 @@ class TestOCIProjectEditView(BrowserTestCase):
         self.assertThat(
             "Distribution:\n%s\nEdit OCI project" % (
                 new_distribution.display_name),
-            MatchesTagText(content, "distribution"))
+            MatchesTagText(content, "pillar"))
         self.assertThat(
             "Name:\nnew-name\nEdit OCI project",
             MatchesTagText(content, "name"))
@@ -280,11 +320,33 @@ class TestOCIProjectAddView(BrowserTestCase):
         self.assertThat(
             "Distribution:\n%s\nEdit OCI project" % (
                 new_distribution.display_name),
-            MatchesTagText(content, "distribution"))
+            MatchesTagText(content, "pillar"))
         self.assertThat(
              "Name:\nnew-name\nEdit OCI project",
              MatchesTagText(content, "name"))
 
+    def test_create_oci_project_for_project(self):
+        oci_project = self.factory.makeOCIProject()
+        user = oci_project.pillar.owner
+        project = self.factory.makeProduct(owner=user)
+        browser = self.getViewBrowser(
+            project, user=user, view_name='+new-oci-project')
+        browser.getControl(name="field.name").value = "new-name"
+        browser.getControl("Create OCI Project").click()
+
+        content = find_main_content(browser.contents)
+        with person_logged_in(user):
+            self.assertEqual(
+                "OCI project new-name for %s" % project.display_name,
+                extract_text(content.h1))
+            self.assertThat(
+                "Project:\n%s\nEdit OCI project" % (
+                    project.display_name),
+                MatchesTagText(content, "pillar"))
+            self.assertThat(
+                 "Name:\nnew-name\nEdit OCI project",
+                 MatchesTagText(content, "name"))
+
     def test_create_oci_project_already_exists(self):
         person = self.factory.makePerson()
         distribution = self.factory.makeDistribution(oci_project_admin=person)
diff --git a/lib/lp/registry/interfaces/product.py b/lib/lp/registry/interfaces/product.py
index 6b3df06..55091a1 100644
--- a/lib/lp/registry/interfaces/product.py
+++ b/lib/lp/registry/interfaces/product.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces including and related to IProduct."""
@@ -798,6 +798,14 @@ class IProductView(
                                 filter_statuses.
         """
 
+    def canAdministerOCIProjects(person):
+        """Checks if the given person can manage OCI projects for this
+        Product."""
+
+    def getOCIProject(name):
+        """Return a `OCIProject` with the given name for this product, or None.
+        """
+
     def getPackage(distroseries):
         """Return a package in that distroseries for this product."""
 
diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
index a99d846..570f097 100644
--- a/lib/lp/registry/model/product.py
+++ b/lib/lp/registry/model/product.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database classes including and related to Product."""
@@ -142,6 +142,7 @@ from lp.registry.interfaces.accesspolicy import (
     IAccessPolicyGrantSource,
     IAccessPolicySource,
     )
+from lp.registry.interfaces.ociproject import IOCIProjectSet
 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
 from lp.registry.interfaces.person import (
     IPersonSet,
@@ -1140,6 +1141,23 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
         """See `IBugTarget`."""
         return self.name
 
+    def getOCIProject(self, name):
+        return getUtility(IOCIProjectSet).getByPillarAndName(
+            self, name)
+
+    def canAdministerOCIProjects(self, person):
+        if person is None:
+            return False
+        # XXX: pappacena 2020-05-25: Maybe we should have a
+        # oci_project_admin on product too, the same way we have on
+        # Distribution.
+        if person.inTeam(self.driver):
+            return True
+        person_roles = IPersonRoles(person)
+        if person_roles.in_admin or person_roles.isOwner(self):
+            return True
+        return False
+
     def getPackage(self, distroseries):
         """See `IProduct`."""
         if isinstance(distroseries, Distribution):
diff --git a/lib/lp/registry/templates/ociproject-index.pt b/lib/lp/registry/templates/ociproject-index.pt
index 427b383..9a14cb7 100644
--- a/lib/lp/registry/templates/ociproject-index.pt
+++ b/lib/lp/registry/templates/ociproject-index.pt
@@ -28,11 +28,14 @@
   <div metal:fill-slot="main">
     <h2>OCI project information</h2>
     <div class="two-column-list">
-      <dl id="distribution" tal:define="distribution context/distribution">
-        <dt>Distribution:</dt>
+      <dl id="pillar" tal:define="pillar context/pillar">
+        <dt>
+            <span tal:condition="context/distribution">Distribution:</span>
+            <span tal:condition="context/project">Project:</span>
+        </dt>
         <dd>
-          <a tal:attributes="href distribution/fmt:url"
-             tal:content="distribution/display_name"/>
+          <a tal:attributes="href pillar/fmt:url"
+             tal:content="pillar/display_name"/>
           <a tal:replace="structure context/menu:overview/edit/fmt:icon"/>
         </dd>
       </dl>
diff --git a/lib/lp/security.py b/lib/lp/security.py
index f7d7bf8..dfc06c2 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -3464,7 +3464,7 @@ class EditOCIProject(AuthorizationBase):
         """Maintainers, drivers, and admins can drive projects."""
         return (user.in_admin or
                 user.isDriver(self.obj.pillar) or
-                user.inTeam(self.obj.pillar.oci_project_admin))
+                self.obj.pillar.canAdministerOCIProjects(user))
 
 
 class EditOCIProjectSeries(DelegatedAuthorization):

References