← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:ui-oci-recipe-list into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ui-oci-recipe-list into launchpad:master.

Commit message:
Adding OCI recipe list page on OCI project.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/383094
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ui-oci-recipe-list into launchpad:master.
diff --git a/lib/lp/oci/browser/configure.zcml b/lib/lp/oci/browser/configure.zcml
index 3ee0af0..fe40f18 100644
--- a/lib/lp/oci/browser/configure.zcml
+++ b/lib/lp/oci/browser/configure.zcml
@@ -30,6 +30,12 @@
             name="+index"
             template="../templates/ocirecipe-index.pt" />
         <browser:page
+            for="lp.registry.interfaces.personociproject.IOCIProject"
+            class="lp.oci.browser.ocirecipe.OCIRecipeSetView"
+            permission="launchpad.View"
+            name="+recipes"
+            template="../templates/ocirecipe-set.pt" />
+        <browser:page
             for="lp.registry.interfaces.ociproject.IOCIProject"
             class="lp.oci.browser.ocirecipe.OCIRecipeAddView"
             permission="launchpad.AnyLegitimatePerson"
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index fcced66..93b686e 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -64,6 +64,7 @@ from lp.services.webapp import (
     stepthrough,
     structured,
     )
+from lp.services.webapp.batching import BatchNavigator
 from lp.services.webapp.breadcrumb import NameBreadcrumb
 from lp.services.webhooks.browser import WebhookTargetNavigationMixin
 from lp.soyuz.browser.archive import EnableProcessorsMixin
@@ -144,6 +145,35 @@ class OCIRecipeContextMenu(ContextMenu):
         return Link('+request-builds', 'Request builds', icon='add')
 
 
+class OCIRecipeSetView(LaunchpadView):
+    """Default view for the list of OCI recipes of an OCI project."""
+    page_title = 'Recipes'
+    description = 'These are the recipes created for this OCI project.'
+
+    @property
+    def title(self):
+        return self.context.name
+
+    @cachedproperty
+    def recipes(self):
+        recipes = getUtility(IOCIRecipeSet).findByOCIProject(self.context)
+        return recipes.order_by('name')
+
+    @property
+    def recipes_navigator(self):
+        return BatchNavigator(self.recipes, self.request)
+
+    @cachedproperty
+    def count(self):
+        return self.recipes_navigator.batch.total()
+
+    @property
+    def preloaded_recipes_batch(self):
+        recipes = self.recipes_navigator.batch
+        getUtility(IOCIRecipeSet).preloadDataForOCIRecipes(recipes)
+        return recipes
+
+
 class OCIRecipeView(LaunchpadView):
     """Default view of an OCI recipe."""
 
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 1dfb3df..91ee725 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # Copyright 2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
@@ -11,6 +12,7 @@ from datetime import (
     datetime,
     timedelta,
     )
+from operator import attrgetter
 import re
 
 from fixtures import FakeLogger
@@ -48,6 +50,7 @@ from lp.testing import (
     login,
     login_person,
     person_logged_in,
+    record_two_runs,
     TestCaseWithFactory,
     time_counter,
     )
@@ -812,3 +815,80 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
         self.assertIn(
             "You need to select at least one architecture.",
             extract_text(find_main_content(browser.contents)))
+
+
+class TestOCIRecipeSetView(BaseTestOCIRecipeView):
+    def setUp(self):
+        super(TestOCIRecipeSetView, self).setUp()
+        self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+        self.distroseries = self.factory.makeDistroSeries(
+            distribution=self.ubuntu, name="shiny", displayname="Shiny")
+        self.architectures = []
+        for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
+            das = self.factory.makeDistroArchSeries(
+                distroseries=self.distroseries, architecturetag=architecture,
+                processor=getUtility(IProcessorSet).getByName(processor))
+            das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
+            self.architectures.append(das)
+        self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+        self.oci_project = self.factory.makeOCIProject(
+            pillar=self.distroseries.distribution,
+            ociprojectname="oci-project-name")
+
+    def makeRecipes(self, count=1):
+        with person_logged_in(self.person):
+            owner = self.factory.makePerson()
+            return [self.factory.makeOCIRecipe(
+                registrant=owner, owner=owner, oci_project=self.oci_project)
+                for _ in range(count)]
+
+    def test_shows_no_recipe(self):
+        browser = self.getViewBrowser(
+            self.oci_project, "+recipes", user=self.person)
+        main_text = extract_text(find_main_content(browser.contents))
+        with person_logged_in(self.person):
+            self.assertIn(
+                "There are no recipes registered for %s"
+                % self.oci_project.name,
+                main_text)
+
+    def test_paginates_recipes(self):
+        batch_size = 5
+        self.pushConfig("launchpad", default_batch_size=batch_size)
+        recipes = self.makeRecipes(10)
+        browser = self.getViewBrowser(
+            self.oci_project, "+recipes", user=self.person)
+
+        main_text = extract_text(find_main_content(browser.contents))
+        no_wrap_main_text = main_text.replace('\n', ' ')
+        with person_logged_in(self.person):
+            self.assertIn(
+                "There are 10 recipes registered for %s"
+                % self.oci_project.name,
+                no_wrap_main_text)
+            self.assertIn("1 → 5 of 10 results", no_wrap_main_text)
+            self.assertIn("First • Previous • Next • Last", no_wrap_main_text)
+
+            # Make sure it's listing the first set of recipes
+            items = sorted(recipes, key=attrgetter('name'))
+            for recipe in items[:batch_size]:
+                self.assertIn(recipe.name, main_text)
+
+    def test_constant_query_count(self):
+        self.pushConfig("launchpad", default_batch_size=2)
+
+        def getView():
+            view = self.getViewBrowser(
+                self.oci_project, "+recipes", user=self.person)
+            return view
+
+        def do_login():
+            self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+            login_person(self.person)
+
+        recorder1, recorder2 = record_two_runs(
+            getView, self.makeRecipes, 1, 15, login_method=do_login)
+
+        # The first run (with no extra pages) makes BatchNavigator issue one
+        # extra count(*) on OCIRecipe. Shouldn't be a big deal.
+        self.assertEqual(recorder1.count, recorder2.count - 1)
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 1f3cf55..3989124 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -42,6 +42,8 @@ from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.buildmaster.model.processor import Processor
+from lp.code.model.gitcollection import GenericGitCollection
+from lp.code.model.gitrepository import GitRepository
 from lp.oci.enums import OCIRecipeBuildRequestStatus
 from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
 from lp.oci.interfaces.ocirecipe import (
@@ -66,8 +68,11 @@ from lp.oci.model.ocipushrule import OCIPushRule
 from lp.oci.model.ocirecipebuild import OCIRecipeBuild
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.role import IPersonRoles
+from lp.registry.model.distribution import Distribution
 from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.person import Person
 from lp.registry.model.series import ACTIVE_STATUSES
+from lp.services.database.bulk import load_related
 from lp.services.database.constants import (
     DEFAULT,
     UTC_NOW,
@@ -544,6 +549,15 @@ class OCIRecipeSet:
         list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
             person_ids, need_validity=True))
 
+        # Preload projects
+        projects = [recipe.oci_project for recipe in recipes]
+        load_related(Distribution, projects, ["distribution_id"])
+
+        # Preload repos
+        repos = load_related(GitRepository, recipes, ["git_repository_id"])
+        load_related(Person, repos, ['owner_id', 'registrant_id'])
+        GenericGitCollection.preloadDataForRepositories(repos)
+
 
 @implementer(IOCIRecipeBuildRequest)
 class OCIRecipeBuildRequest:
diff --git a/lib/lp/oci/templates/ocirecipe-set.pt b/lib/lp/oci/templates/ocirecipe-set.pt
new file mode 100644
index 0000000..9618002
--- /dev/null
+++ b/lib/lp/oci/templates/ocirecipe-set.pt
@@ -0,0 +1,75 @@
+<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_side"
+  i18n:domain="launchpad"
+>
+
+<body>
+  <metal:side fill-slot="side">
+    <div tal:replace="structure context/@@+global-actions"/>
+  </metal:side>
+
+  <metal:heading fill-slot="heading">
+    <h1>Recipes for <span tal:replace="context/name"/></h1>
+  </metal:heading>
+
+  <div metal:fill-slot="main">
+      <div class="main-portlet">
+        <p class="application-summary"
+           tal:condition="view/description"
+           tal:content="view/description"/>
+        <p tal:define="count view/count"
+           tal:condition="count">
+          <span tal:condition="python: count == 1">
+            There is <strong>1</strong> recipe</span>
+          <span tal:condition="python: count != 1">
+            There are <strong tal:content="count" /> recipes
+          </span>
+          registered for <tal:context replace="view/title" />.
+        </p>
+        <p tal:define="count view/count"
+           tal:condition="not: count">
+           There are no recipes registered for <tal:context replace="view/title" />.
+        </p>
+      </div>
+
+      <table class="listing" id="mirrors_list">
+        <tbody>
+          <tr class="head">
+            <th>Name</th>
+            <th>Repository</th>
+            <th>Branch</th>
+            <th>Build file</th>
+            <th>Requires virtualization</th>
+            <th>Builds daily</th>
+            <th>Owner</th>
+            <th>Date created</th>
+            <th>Last modified</th>
+          </tr>
+
+          <tr tal:repeat="recipe view/preloaded_recipes_batch">
+            <td>
+              <a tal:content="recipe/name"
+                 tal:attributes="href recipe/fmt:url" />
+            </td>
+            <td tal:content="structure recipe/git_repository/fmt:link"/>
+            <td tal:content="recipe/git_path" />
+            <td tal:content="recipe/build_file" />
+            <td tal:content="recipe/require_virtualized" />
+            <td tal:content="recipe/build_daily" />
+            <td tal:content="structure recipe/owner/fmt:link" />
+            <td tal:content="recipe/date_created/fmt:displaydate" />
+            <td tal:content="recipe/date_last_modified/fmt:displaydate" />
+          </tr>
+        </tbody>
+      </table>
+
+       <tal:navigation
+          replace="structure view/recipes_navigator/@@+navigation-links-lower" />
+  </div>
+
+</body>
+</html>