launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24651
[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>