Merge ~pappacena/launchpad:ociproject-picker into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 91ac078ece7a1d98c644885d70499519d30c073b
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:ociproject-picker
Merge into: launchpad:master
Diff against target: 250 lines (+141/-8)
5 files modified
lib/lp/registry/interfaces/ociproject.py (+5/-1)
lib/lp/registry/model/ociproject.py (+41/-6)
lib/lp/registry/tests/test_ociproject.py (+52/-0)
lib/lp/registry/vocabularies.py (+31/-0)
lib/lp/registry/vocabularies.zcml (+12/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+393627@code.launchpad.net

Commit message

Adding OCIProject vocabulary, to support OCIProject picker widgets in forms

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Addressed the comments, and created a MP to add a tsvector column on OCIProjectName here: https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/393666.

a1c626e... by Thiago F. Pappacena

Reverting the search on OCIProjectName.name to use trigram index

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
2index e72a4cf..82ba8ad 100644
3--- a/lib/lp/registry/interfaces/ociproject.py
4+++ b/lib/lp/registry/interfaces/ociproject.py
5@@ -204,7 +204,8 @@ class IOCIProjectSet(Interface):
6 def getByPillarAndName(pillar, name):
7 """Get the OCIProjects for a given distribution or project.
8
9- :param pillar: An instance of Distribution or Product.
10+ :param pillar: An instance of Distribution or Product, or the
11+ respective pillar name.
12 :param name: The OCIProject name to find.
13 :return: The OCIProject found.
14 """
15@@ -213,6 +214,9 @@ class IOCIProjectSet(Interface):
16 """Find OCIProjects for a given pillar that contain the provided
17 name."""
18
19+ def searchByName(name_substring):
20+ """Search OCIProjects that contain the provided name."""
21+
22 def preloadDataForOCIProjects(oci_projects):
23 """Preload data for the given list of OCIProject objects."""
24
25diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
26index c27f2c6..3b376be 100644
27--- a/lib/lp/registry/model/ociproject.py
28+++ b/lib/lp/registry/model/ociproject.py
29@@ -12,7 +12,14 @@ __all__ = [
30 ]
31
32 import pytz
33+import six
34+from lp.services.database.stormexpr import fti_search
35 from six import text_type
36+from storm.expr import (
37+ Join,
38+ LeftJoin,
39+ Or,
40+ )
41 from storm.locals import (
42 Bool,
43 DateTime,
44@@ -263,12 +270,34 @@ class OCIProjectSet:
45
46 def getByPillarAndName(self, pillar, name):
47 """See `IOCIProjectSet`."""
48- target = IStore(OCIProject).find(
49- OCIProject,
50- self._get_pillar_attribute(pillar) == pillar,
51- OCIProject.ociprojectname == OCIProjectName.id,
52- OCIProjectName.name == name).one()
53- return target
54+ from lp.registry.model.product import Product
55+ from lp.registry.model.distribution import Distribution
56+
57+ # If pillar is not an string, we expect it to be either an
58+ # IDistribution or IProduct.
59+ if not isinstance(pillar, six.string_types):
60+ return IStore(OCIProject).find(
61+ OCIProject,
62+ self._get_pillar_attribute(pillar) == pillar,
63+ OCIProject.ociprojectname == OCIProjectName.id,
64+ OCIProjectName.name == name).one()
65+ else:
66+ # If we got a pillar name instead, we need to join with both
67+ # Distribution and Product tables, to find out which one has the
68+ # provided name.
69+ tables = [
70+ OCIProject,
71+ Join(OCIProjectName,
72+ OCIProject.ociprojectname == OCIProjectName.id),
73+ LeftJoin(Distribution,
74+ OCIProject.distribution == Distribution.id),
75+ LeftJoin(Product,
76+ OCIProject.project == Product.id)
77+ ]
78+ return IStore(OCIProject).using(*tables).find(
79+ OCIProject,
80+ Or(Distribution.name == pillar, Product.name == pillar),
81+ OCIProjectName.name == name).one()
82
83 def findByPillarAndName(self, pillar, name_substring):
84 """See `IOCIProjectSet`."""
85@@ -278,6 +307,12 @@ class OCIProjectSet:
86 OCIProject.ociprojectname == OCIProjectName.id,
87 OCIProjectName.name.contains_string(name_substring))
88
89+ def searchByName(self, name_substring):
90+ return IStore(OCIProject).find(
91+ OCIProject,
92+ OCIProject.ociprojectname == OCIProjectName.id,
93+ OCIProjectName.name.contains_string(name_substring))
94+
95 def preloadDataForOCIProjects(self, oci_projects):
96 """See `IOCIProjectSet`."""
97 oci_projects = [removeSecurityProxy(i) for i in oci_projects]
98diff --git a/lib/lp/registry/tests/test_ociproject.py b/lib/lp/registry/tests/test_ociproject.py
99index c01f939..0232b2e 100644
100--- a/lib/lp/registry/tests/test_ociproject.py
101+++ b/lib/lp/registry/tests/test_ociproject.py
102@@ -16,6 +16,7 @@ from testtools.matchers import (
103 )
104 from testtools.testcase import ExpectedException
105 from zope.component import getUtility
106+from zope.schema.vocabulary import getVocabularyRegistry
107 from zope.security.interfaces import Unauthorized
108 from zope.security.proxy import removeSecurityProxy
109
110@@ -313,3 +314,54 @@ class TestOCIProjectWebservice(TestCaseWithFactory):
111 owner=other_user))
112
113 self.assertCanCreateOCIProject(distro, self.person)
114+
115+
116+class TestOCIProjectVocabulary(TestCaseWithFactory):
117+ layer = DatabaseFunctionalLayer
118+
119+ def createOCIProjects(self, name_tpl="my-ociproject-%s", count=5):
120+ return [self.factory.makeOCIProject(ociprojectname=name_tpl % i)
121+ for i in range(count)]
122+
123+ def getVocabulary(self, context=None):
124+ vocabulary_registry = getVocabularyRegistry()
125+ return vocabulary_registry.get(context, "OCIProject")
126+
127+ def assertContainsSameOCIProjects(self, ociprojects, search_result):
128+ """Asserts that the search result contains only the given list of OCI
129+ projects.
130+ """
131+ naked = removeSecurityProxy
132+ self.assertEqual(
133+ set(naked(ociproject).id for ociproject in ociprojects),
134+ set(naked(term.value).id for term in search_result))
135+
136+ def test_search_with_name_substring(self):
137+ vocabulary = self.getVocabulary()
138+ projects = self.createOCIProjects("test-project-%s", 10)
139+ self.createOCIProjects("another-pattern-%s", 10)
140+
141+ search_result = vocabulary.searchForTerms("test-project")
142+ self.assertContainsSameOCIProjects(projects, search_result)
143+
144+ def test_search_without_name_substring(self):
145+ vocabulary = self.getVocabulary()
146+ projects = self.createOCIProjects()
147+ search_result = vocabulary.searchForTerms("")
148+ self.assertContainsSameOCIProjects(projects, search_result)
149+
150+ def test_to_term(self):
151+ vocabulary = self.getVocabulary()
152+ ociproject = self.factory.makeOCIProject()
153+ term = removeSecurityProxy(vocabulary).toTerm(ociproject)
154+
155+ expected_token = "%s/%s" % (ociproject.pillar.name, ociproject.name)
156+ self.assertEqual(expected_token, term.title)
157+ self.assertEqual(expected_token, term.token)
158+
159+ def test_getTermByToken(self):
160+ vocabulary = self.getVocabulary()
161+ ociproject = self.factory.makeOCIProject()
162+ token = "%s/%s" % (ociproject.pillar.name, ociproject.name)
163+ term = removeSecurityProxy(vocabulary).getTermByToken(token)
164+ self.assertEqual(ociproject, term.value)
165diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
166index 05e3900..31d9f3d 100644
167--- a/lib/lp/registry/vocabularies.py
168+++ b/lib/lp/registry/vocabularies.py
169@@ -128,6 +128,7 @@ from lp.registry.interfaces.milestone import (
170 IMilestoneSet,
171 IProjectGroupMilestone,
172 )
173+from lp.registry.interfaces.ociproject import IOCIProjectSet
174 from lp.registry.interfaces.person import (
175 IPerson,
176 IPersonSet,
177@@ -155,6 +156,7 @@ from lp.registry.model.featuredproject import FeaturedProject
178 from lp.registry.model.karma import KarmaCategory
179 from lp.registry.model.mailinglist import MailingList
180 from lp.registry.model.milestone import Milestone
181+from lp.registry.model.ociproject import OCIProject
182 from lp.registry.model.person import (
183 get_person_visibility_terms,
184 IrcID,
185@@ -213,6 +215,7 @@ from lp.services.webapp.vocabulary import (
186 NamedSQLObjectVocabulary,
187 NamedStormHugeVocabulary,
188 SQLObjectVocabularyBase,
189+ StormVocabularyBase,
190 VocabularyFilter,
191 )
192 from lp.soyuz.model.archive import Archive
193@@ -2205,3 +2208,31 @@ class DistributionSourcePackageVocabulary(FilteredVocabularyBase):
194 return self.toTerm((dsp, binary_names))
195
196 return CountableIterator(results.count(), results, make_term)
197+
198+
199+@implementer(IHugeVocabulary)
200+class OCIProjectVocabulary(StormVocabularyBase):
201+ """All OCI Projects."""
202+
203+ _table = OCIProject
204+ displayname = 'Select an OCI project'
205+ step_title = 'Search'
206+
207+ def toTerm(self, ociproject):
208+ token = "%s/%s" % (ociproject.pillar.name, ociproject.name)
209+ title = "%s" % token
210+ return SimpleTerm(ociproject, token, title)
211+
212+ def getTermByToken(self, token):
213+ pillar_name, name = token.split('/')
214+ ociproject = getUtility(IOCIProjectSet).getByPillarAndName(
215+ pillar_name, name)
216+ if ociproject is None:
217+ raise LookupError(token)
218+ return self.toTerm(ociproject)
219+
220+ def search(self, query, vocab_filter=None):
221+ return getUtility(IOCIProjectSet).searchByName(query)
222+
223+ def _entries(self):
224+ return getUtility(IOCIProjectSet).searchByName('')
225diff --git a/lib/lp/registry/vocabularies.zcml b/lib/lp/registry/vocabularies.zcml
226index 4e2e488..37b7b14 100644
227--- a/lib/lp/registry/vocabularies.zcml
228+++ b/lib/lp/registry/vocabularies.zcml
229@@ -1,4 +1,4 @@
230-<!-- Copyright 2009-2016 Canonical Ltd. This software is licensed under the
231+<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the
232 GNU Affero General Public License version 3 (see the file LICENSE).
233 -->
234
235@@ -562,4 +562,15 @@
236 permission="zope.Public"
237 attributes="name title description"/>
238 </class>
239+
240+ <securedutility
241+ name="OCIProject"
242+ component="lp.registry.vocabularies.OCIProjectVocabulary"
243+ provides="zope.schema.interfaces.IVocabularyFactory">
244+ <allow interface="zope.schema.interfaces.IVocabularyFactory" />
245+ </securedutility>
246+
247+ <class class="lp.registry.vocabularies.OCIProjectVocabulary">
248+ <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
249+ </class>
250 </configure>