launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24700
[Merge] ~pappacena/launchpad:ui-oci-project-search into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ui-oci-project-search into launchpad:master.
Commit message:
Adding page to see the (paginated) list of OCI projects of a distribution, including the possibility to search.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/383657
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ui-oci-project-search into launchpad:master.
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index 2fdc177..96eb8a8 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="+oci-project-search"
+ for="lp.registry.interfaces.distribution.IDistribution"
+ class="lp.registry.browser.ociproject.OCIProjectSearchView"
+ permission="zope.Public"
+ template="../templates/ociproject-search.pt"
+ />
+ <browser:page
name="+search"
for="lp.registry.interfaces.distribution.IDistribution"
class="lp.registry.browser.distribution.DistributionPackageSearchView"
diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
index 4b3b18c..e5f3e46 100644
--- a/lib/lp/registry/browser/distribution.py
+++ b/lib/lp/registry/browser/distribution.py
@@ -311,11 +311,16 @@ class DistributionNavigationMenu(NavigationMenu, DistributionLinksMixin):
text = 'Create an OCI Project'
return Link('+new-oci-project', text, icon='add')
+ def search_oci_project(self):
+ text = 'Search for OCI Project'
+ return Link('+oci-project-search', text, icon='info')
+
@cachedproperty
def links(self):
return [
'edit', 'admin', 'pubconf', 'subscribe_to_bug_mail',
- 'edit_bug_mail', 'sharing', 'new_oci_project']
+ 'edit_bug_mail', 'sharing', 'new_oci_project',
+ 'search_oci_project']
class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin):
diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
index 700af68..cba86f4 100644
--- a/lib/lp/registry/browser/ociproject.py
+++ b/lib/lp/registry/browser/ociproject.py
@@ -37,16 +37,19 @@ from lp.registry.interfaces.ociprojectname import (
IOCIProjectNameSet,
)
from lp.services.features import getFeatureFlag
+from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
canonical_url,
ContextMenu,
enabled_with_permission,
+ LaunchpadView,
Link,
Navigation,
NavigationMenu,
StandardLaunchpadFacets,
stepthrough,
)
+from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb
@@ -200,3 +203,44 @@ class OCIProjectEditView(LaunchpadEditFormView):
return canonical_url(self.context)
cancel_url = next_url
+
+
+class OCIProjectSearchView(LaunchpadView):
+ """Page to search for OCI projects of a given distribution."""
+ page_title = ''
+
+ @property
+ def text(self):
+ text = self.request.get("text", None)
+ if isinstance(text, list):
+ text = text[-1]
+ return text
+
+ @property
+ def search_requested(self):
+ return self.text is not None
+
+ @property
+ def title(self):
+ return self.context.name
+
+ @cachedproperty
+ def count(self):
+ """Return the number of matched search results."""
+ return self.batchnav.batch.total()
+
+ @cachedproperty
+ def batchnav(self):
+ """Return the batch navigator for the search results."""
+ return BatchNavigator(self.search_results, self.request)
+
+ @cachedproperty
+ def preloaded_batch(self):
+ projects = self.batchnav.batch
+ getUtility(IOCIProjectSet).preloadDataForOCIProjects(projects)
+ return projects
+
+ @property
+ def search_results(self):
+ return getUtility(IOCIProjectSet).findByDistributionAndName(
+ self.context, self.text or '')
diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
index 13adfbf..c73603f 100644
--- a/lib/lp/registry/browser/tests/test_ociproject.py
+++ b/lib/lp/registry/browser/tests/test_ociproject.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
@@ -21,8 +22,11 @@ from lp.services.features.testing import FeatureFixture
from lp.services.webapp import canonical_url
from lp.services.webapp.escaping import structured
from lp.testing import (
+ admin_logged_in,
BrowserTestCase,
+ login_person,
person_logged_in,
+ record_two_runs,
test_tales,
TestCaseWithFactory,
)
@@ -31,6 +35,7 @@ from lp.testing.matchers import MatchesTagText
from lp.testing.pages import (
extract_text,
find_main_content,
+ find_tag_by_id,
find_tags_by_class,
)
from lp.testing.publication import test_traverse
@@ -161,9 +166,6 @@ class TestOCIProjectAddView(BrowserTestCase):
layer = DatabaseFunctionalLayer
- def setUp(self):
- super(TestOCIProjectAddView, self).setUp()
-
def test_create_oci_project(self):
oci_project = self.factory.makeOCIProject()
user = oci_project.pillar.owner
@@ -213,3 +215,132 @@ class TestOCIProjectAddView(BrowserTestCase):
new_distribution,
user=another_person,
view_name='+new-oci-project')
+
+
+class TestOCIProjectSearchView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def assertPaginationIsPresent(
+ self, browser, results_in_page, total_result):
+ """Checks that pagination is shown at the browser."""
+ nav_index = find_tags_by_class(
+ browser.contents, "batch-navigation-index")[0]
+ nav_index_text = extract_text(nav_index).replace('\n', ' ')
+ self.assertIn(
+ "1 → %s of %s results" % (results_in_page, total_result),
+ nav_index_text)
+
+ nav_links = find_tags_by_class(
+ browser.contents, "batch-navigation-links")[0]
+ nav_links_text = extract_text(nav_links).replace('\n', ' ')
+ self.assertIn("First • Previous • Next • Last", nav_links_text)
+
+ def assertOCIProjectsArePresent(self, browser, oci_projects):
+ table = find_tag_by_id(browser.contents, "projects_list")
+ with admin_logged_in():
+ for oci_project in oci_projects:
+ url = canonical_url(oci_project, force_local_path=True)
+ self.assertIn(url, str(table))
+ self.assertIn(oci_project.name, str(table))
+
+ def assertOCIProjectsAreNotPresent(self, browser, oci_projects):
+ table = find_tag_by_id(browser.contents, "projects_list")
+ with admin_logged_in():
+ for oci_project in oci_projects:
+ url = canonical_url(oci_project, force_local_path=True)
+ self.assertNotIn(url, str(table))
+ self.assertNotIn(oci_project.name, str(table))
+
+ def test_search_no_oci_projects(self):
+ person = self.factory.makePerson()
+ distribution = self.factory.makeDistribution()
+ browser = self.getViewBrowser(
+ distribution, user=person, view_name='+oci-project-search')
+
+ main_portlet = find_tags_by_class(browser.contents, "main-portlet")[0]
+ self.assertIn(
+ "There are no OCI projects registered for %s" % distribution.name,
+ extract_text(main_portlet).replace("\n", " "))
+
+ def test_oci_projects_no_search_keyword(self):
+ person = self.factory.makePerson()
+ distro = self.factory.makeDistribution(owner=person)
+
+ # Creates 3 OCI Projects
+ oci_projects = [
+ self.factory.makeOCIProject(
+ ociprojectname="test-project-%s" % i,
+ registrant=person, pillar=distro) for i in range(3)]
+
+ browser = self.getViewBrowser(
+ distro, user=person, view_name='+oci-project-search')
+
+ # Check top message.
+ main_portlet = find_tags_by_class(browser.contents, "main-portlet")[0]
+ self.assertIn(
+ "There are 3 OCI projects registered for %s" % distro.name,
+ extract_text(main_portlet).replace("\n", " "))
+
+ self.assertOCIProjectsArePresent(browser, oci_projects)
+ self.assertPaginationIsPresent(browser, 3, 3)
+
+ def test_oci_projects_with_search_keyword(self):
+ person = self.factory.makePerson()
+ distro = self.factory.makeDistribution(owner=person)
+
+ # And 2 OCI projects that will match the name
+ oci_projects = [
+ self.factory.makeOCIProject(
+ ociprojectname="find-me-%s" % i,
+ registrant=person, pillar=distro) for i in range(2)]
+
+ # Creates 2 OCI Projects that will not match search
+ other_oci_projects = [
+ self.factory.makeOCIProject(
+ ociprojectname="something-%s" % i,
+ registrant=person, pillar=distro) for i in range(2)]
+
+ browser = self.getViewBrowser(
+ distro, user=person, view_name='+oci-project-search')
+ browser.getControl(name="text").value = "find-me"
+ browser.getControl("Search").click()
+
+ # Check top message.
+ main_portlet = find_tags_by_class(browser.contents, "main-portlet")[0]
+ self.assertIn(
+ 'There are 2 OCI projects registered for %s matching "%s"' %
+ (distro.name, "find-me"),
+ extract_text(main_portlet).replace("\n", " "))
+
+ self.assertOCIProjectsArePresent(browser, oci_projects)
+ self.assertOCIProjectsAreNotPresent(browser, other_oci_projects)
+ self.assertPaginationIsPresent(browser, 2, 2)
+
+ def test_query_count_is_constant(self):
+ batch_size = 3
+ self.pushConfig("launchpad", default_batch_size=batch_size)
+
+ person = self.factory.makePerson()
+ distro = self.factory.makeDistribution(owner=person)
+ name_pattern = "find-me-"
+
+ def createOCIProject():
+ self.factory.makeOCIProject(
+ ociprojectname=self.factory.getUniqueString(name_pattern),
+ pillar=distro)
+
+ viewer = self.factory.makePerson()
+ def getView():
+ browser = self.getViewBrowser(
+ distro, user=viewer, view_name='+oci-project-search')
+ browser.getControl(name="text").value = name_pattern
+ browser.getControl("Search").click()
+ return browser
+
+ def do_login():
+ login_person(person)
+
+ recorder1, recorder2 = record_two_runs(
+ getView, createOCIProject, 1, 10, login_method=do_login)
+ self.assertEqual(recorder1.count, recorder2.count)
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index 7394079..99a8496 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -54,7 +54,6 @@ from lp.services.fields import (
PublicPersonChoice,
)
-
# XXX: pappacena 2020-04-20: It is ok to remove the feature flag since we
# already have in place the correct permission check for this feature.
OCI_PROJECT_ALLOW_CREATE = 'oci.project.create.enabled'
@@ -177,6 +176,13 @@ class IOCIProjectSet(Interface):
def getByDistributionAndName(distribution, name):
"""Get the OCIProjects for a given distribution."""
+ def findByDistributionAndName(distribution, name):
+ """Find OCIProjects for a given distribution that contains the
+ provided name."""
+
+ def preloadDataForOCIProjects(oci_projects):
+ """Preload data for the given list of OCIProject objects."""
+
@error_status(http_client.UNAUTHORIZED)
class OCIProjectCreateFeatureDisabled(Unauthorized):
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index 2967007..de4ea46 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -12,6 +12,7 @@ __all__ = [
]
import pytz
+from lp.registry.interfaces.person import IPersonSet
from six import text_type
from storm.locals import (
Bool,
@@ -35,6 +36,7 @@ from lp.registry.interfaces.ociprojectname import IOCIProjectNameSet
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.ociprojectname import OCIProjectName
from lp.registry.model.ociprojectseries import OCIProjectSeries
+from lp.services.database.bulk import load_related
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
@@ -191,3 +193,21 @@ class OCIProjectSet:
OCIProject.ociprojectname == OCIProjectName.id,
OCIProjectName.name == name).one()
return target
+
+ def findByDistributionAndName(self, distribution, name):
+ """See `IOCIProjectSet`."""
+ return IStore(OCIProject).find(
+ OCIProject,
+ OCIProject.distribution == distribution,
+ OCIProject.ociprojectname == OCIProjectName.id,
+ OCIProjectName.name.contains_string(name))
+
+ def preloadDataForOCIProjects(self, oci_projects):
+ """See `IOCIProjectSet`."""
+ oci_projects = [removeSecurityProxy(i) for i in oci_projects]
+
+ person_ids = [i.registrant_id for i in oci_projects]
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True))
+
+ load_related(OCIProjectName, oci_projects, ["ociprojectname_id"])
diff --git a/lib/lp/registry/templates/ociproject-search.pt b/lib/lp/registry/templates/ociproject-search.pt
new file mode 100644
index 0000000..b30ade7
--- /dev/null
+++ b/lib/lp/registry/templates/ociproject-search.pt
@@ -0,0 +1,92 @@
+<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/searchless"
+ i18n:domain="launchpad"
+>
+<body>
+
+<div metal:fill-slot="heading">
+ <h2>Search OCI projects in <tal:title replace="context/title" /></h2>
+</div>
+
+<metal:side fill-slot="side">
+ <div tal:replace="structure context/@@+global-actions"/>
+</metal:side>
+
+<div metal:fill-slot="main">
+ <div class="top-portlet">
+ <form name="search" method="GET">
+ <div>
+ <label for="text">
+ Show <span tal:replace="context/title" /> OCI projects containing:
+ </label>
+ </div>
+
+ <input type="text" name="text" size="35"
+ tal:attributes="value request/text|nothing" />
+
+ <input type="submit" value="Search"
+ tal:condition="not: view/search_requested" />
+
+ <input type="submit" value="Search Again"
+ tal:condition="view/search_requested" />
+ </form>
+
+
+ <tal:search_result>
+ <div class="main-portlet">
+ <p tal:define="count view/count"
+ tal:condition="count">
+ <span tal:condition="python: count == 1">
+ There is <strong>1</strong> OCI project</span>
+ <span tal:condition="python: count != 1">
+ There are <strong tal:content="count" /> OCI projects
+ </span>
+ registered for <tal:context replace="view/title" />
+ <span tal:condition= "python: view.search_requested and view.text">
+ matching "<strong tal:content="view/text" />".
+ </span>
+ </p>
+ <p tal:define="count view/count"
+ tal:condition="not: count">
+ There are no OCI projects registered for
+ <tal:context replace="view/title" />
+ <span tal:condition= "python: view.search_requested and view.text">
+ matching "<strong tal:content="view/text" />".
+ </span>
+ </p>
+
+ <table class="listing" id="projects_list">
+ <tbody>
+ <tr class="head">
+ <th>Name</th>
+ <th>Description</th>
+ <th>Registrant</th>
+ <th>Date created</th>
+ </tr>
+
+ <tr tal:repeat="item view/preloaded_batch">
+ <td>
+ <a tal:content="item/name"
+ tal:attributes="href item/fmt:url" />
+ </td>
+ <td tal:content="item/description" />
+ <td tal:content="structure item/registrant/fmt:link" />
+ <td tal:content="item/date_created/fmt:displaydate" />
+ </tr>
+ </tbody>
+ </table>
+
+ <tal:navigation
+ replace="structure view/batchnav/@@+navigation-links-lower" />
+ </div>
+ </tal:search_result>
+ </div>
+</div>
+
+</body>
+
+</html>