Merge lp:~cjwatson/launchpad/storm-vocabulary-base into lp:launchpad

Proposed by Colin Watson on 2015-04-28
Status: Merged
Merged at revision: 17462
Proposed branch: lp:~cjwatson/launchpad/storm-vocabulary-base
Merge into: lp:launchpad
Diff against target: 251 lines (+147/-11)
5 files modified
lib/lp/code/vocabularies/gitrepository.py (+3/-3)
lib/lp/services/webapp/marshallers.py (+4/-3)
lib/lp/services/webapp/vocabulary.py (+125/-4)
lib/lp/services/webservice/configure.zcml (+14/-0)
lib/lp/services/webservice/doc/webservice-marshallers.txt (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/storm-vocabulary-base
Reviewer Review Type Date Requested Status
William Grant code 2015-04-28 Approve on 2015-04-29
Review via email: mp+257655@code.launchpad.net

Commit Message

Add StormVocabularyBase, mostly copied from SQLObjectVocabularyBase, and use it for GitRepositoryVocabulary.

Description of the Change

Add StormVocabularyBase, mostly copied from SQLObjectVocabularyBase, and use it for GitRepositoryVocabulary.

(I ran into this when working on the browser code for Git merge proposals.)

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/vocabularies/gitrepository.py'
2--- lib/lp/code/vocabularies/gitrepository.py 2015-04-13 19:02:15 +0000
3+++ lib/lp/code/vocabularies/gitrepository.py 2015-04-28 15:36:55 +0000
4@@ -19,17 +19,17 @@
5 from lp.services.webapp.vocabulary import (
6 CountableIterator,
7 IHugeVocabulary,
8- SQLObjectVocabularyBase,
9+ StormVocabularyBase,
10 )
11
12
13-class GitRepositoryVocabulary(SQLObjectVocabularyBase):
14+class GitRepositoryVocabulary(StormVocabularyBase):
15 """A vocabulary for searching Git repositories."""
16
17 implements(IHugeVocabulary)
18
19 _table = GitRepository
20- _orderBy = ['name', 'id']
21+ _order_by = ['name', 'id']
22 displayname = 'Select a Git repository'
23 step_title = 'Search'
24
25
26=== modified file 'lib/lp/services/webapp/marshallers.py'
27--- lib/lp/services/webapp/marshallers.py 2010-03-26 19:18:31 +0000
28+++ lib/lp/services/webapp/marshallers.py 2015-04-28 15:36:55 +0000
29@@ -10,12 +10,13 @@
30
31 def choiceMarshallerError(field, request, vocabulary=None):
32 # We don't support marshalling a normal Choice field with a
33- # SQLObjectVocabularyBase-based vocabulary.
34+ # SQLObjectVocabularyBase/StormVocabularyBase-based vocabulary.
35 # Normally for this kind of use case, one returns None and
36 # lets the Zope machinery alert the user that the lookup has gone wrong.
37 # However, we want to be more helpful, so we make an assertion,
38 # with a comment on how to make things better.
39 raise AssertionError("You exported %s as an IChoice based on an "
40- "SQLObjectVocabularyBase, you should use "
41- "lazr.restful.fields.ReferenceChoice instead."
42+ "SQLObjectVocabularyBase/StormVocabularyBase; you "
43+ "should use lazr.restful.fields.ReferenceChoice "
44+ "instead."
45 % field.__name__)
46
47=== modified file 'lib/lp/services/webapp/vocabulary.py'
48--- lib/lp/services/webapp/vocabulary.py 2013-01-07 02:40:55 +0000
49+++ lib/lp/services/webapp/vocabulary.py 2015-04-28 15:36:55 +0000
50@@ -10,14 +10,15 @@
51 __metaclass__ = type
52
53 __all__ = [
54+ 'BatchedCountableIterator',
55+ 'CountableIterator',
56 'FilteredVocabularyBase',
57 'ForgivingSimpleVocabulary',
58 'IHugeVocabulary',
59+ 'NamedSQLObjectHugeVocabulary',
60+ 'NamedSQLObjectVocabulary',
61 'SQLObjectVocabularyBase',
62- 'NamedSQLObjectVocabulary',
63- 'NamedSQLObjectHugeVocabulary',
64- 'CountableIterator',
65- 'BatchedCountableIterator',
66+ 'StormVocabularyBase',
67 'VocabularyFilter',
68 ]
69
70@@ -28,6 +29,8 @@
71 AND,
72 CONTAINSSTRING,
73 )
74+from storm.base import Storm
75+from storm.store import EmptyResultSet
76 from zope.interface import (
77 Attribute,
78 implements,
79@@ -43,6 +46,7 @@
80 )
81 from zope.security.proxy import isinstance as zisinstance
82
83+from lp.services.database.interfaces import IStore
84 from lp.services.database.sqlbase import SQLBase
85 from lp.services.helpers import ensure_unicode
86
87@@ -464,3 +468,120 @@
88 clause = AND(clause, self._filter)
89 results = self._table.select(clause, orderBy=self._orderBy)
90 return self.iterator(results.count(), results, self.toTerm)
91+
92+
93+class StormVocabularyBase(FilteredVocabularyBase):
94+ """A base class for widgets that are rendered to collect values
95+ for attributes that are Storm references.
96+
97+ So if a content class behind some form looks like:
98+
99+ class Foo(StormBase):
100+ id = Int(...)
101+ bar_id = Int(...)
102+ bar = Reference(bar_id, ...)
103+ ...
104+
105+ Then the vocabulary for the widget that captures a value for bar
106+ should derive from StormVocabularyBase.
107+ """
108+ implements(IVocabulary, IVocabularyTokenized)
109+ _order_by = None
110+ _clauses = []
111+
112+ def __init__(self, context=None):
113+ self.context = context
114+
115+ # XXX kiko 2007-01-16: note that the method searchForTerms is part of
116+ # IHugeVocabulary, and so should not necessarily need to be
117+ # implemented here; however, many of our vocabularies depend on
118+ # searchForTerms for popup functionality so I have chosen to just do
119+ # that. It is possible that a better solution would be to have the
120+ # search functionality produce a new vocabulary restricted to the
121+ # desired subset.
122+ def searchForTerms(self, query=None, vocab_filter=None):
123+ results = self.search(query, vocab_filter)
124+ return CountableIterator(results.count(), results, self.toTerm)
125+
126+ def search(self, query, vocab_filter=None):
127+ # This default implementation of searchForTerms glues together
128+ # the legacy API of search() with the toTerm method. If you
129+ # don't reimplement searchForTerms you will need to at least
130+ # provide your own search() method.
131+ raise NotImplementedError
132+
133+ def toTerm(self, obj):
134+ # This default implementation assumes that your object has a
135+ # title attribute. If it does not you will need to reimplement
136+ # toTerm, or reimplement the whole searchForTerms.
137+ return SimpleTerm(obj, obj.id, obj.title)
138+
139+ @property
140+ def _entries(self):
141+ entries = IStore(self._table).find(self._table, *self._clauses)
142+ if self._order_by is not None:
143+ entries = entries.order_by(self._order_by)
144+ return entries
145+
146+ def __iter__(self):
147+ """Return an iterator which provides the terms from the vocabulary."""
148+ for obj in self._entries:
149+ yield self.toTerm(obj)
150+
151+ def __len__(self):
152+ return self._entries.count()
153+
154+ def __contains__(self, obj):
155+ # Sometimes this method is called with a Storm instance, but z3 form
156+ # machinery sends through integer ids. This might be due to a bug
157+ # somewhere.
158+ if zisinstance(obj, Storm):
159+ clauses = [self._table.id == obj.id]
160+ if self._clauses:
161+ # XXX kiko 2007-01-16: this code is untested.
162+ clauses.extend(self._clauses)
163+ found_obj = IStore(self._table).find(self._table, *clauses).one()
164+ return found_obj is not None and found_obj == obj
165+ else:
166+ clauses = [self._table.id == int(obj)]
167+ if self._clauses:
168+ # XXX kiko 2007-01-16: this code is untested.
169+ clauses.extend(self._clauses)
170+ found_obj = IStore(self._table).find(self._table, *clauses).one()
171+ return found_obj is not None
172+
173+ def getTerm(self, value):
174+ # Short circuit. There is probably a design problem here since we
175+ # sometimes get the id and sometimes a Storm instance.
176+ if zisinstance(value, Storm):
177+ return self.toTerm(value)
178+
179+ try:
180+ value = int(value)
181+ except ValueError:
182+ raise LookupError(value)
183+
184+ clauses = [self._table.id == value]
185+ if self._clauses:
186+ clauses.extend(self._clauses)
187+ try:
188+ obj = IStore(self._table).find(self._table, *clauses).one()
189+ except ValueError:
190+ raise LookupError(value)
191+
192+ if obj is None:
193+ raise LookupError(value)
194+
195+ return self.toTerm(obj)
196+
197+ def getTermByToken(self, token):
198+ return self.getTerm(token)
199+
200+ def emptySelectResults(self):
201+ """Return a SelectResults object without any elements.
202+
203+ This is to be used when no search string is given to the search()
204+ method of subclasses, in order to be consistent and always return
205+ a SelectResults object.
206+ """
207+ return EmptyResultSet()
208
209=== modified file 'lib/lp/services/webservice/configure.zcml'
210--- lib/lp/services/webservice/configure.zcml 2012-01-31 01:19:45 +0000
211+++ lib/lp/services/webservice/configure.zcml 2015-04-28 15:36:55 +0000
212@@ -64,12 +64,26 @@
213 factory="lp.services.webapp.marshallers.choiceMarshallerError"
214 />
215 <adapter
216+ for="zope.schema.interfaces.IChoice
217+ zope.publisher.interfaces.http.IHTTPRequest
218+ lp.services.webapp.vocabulary.StormVocabularyBase"
219+ provides="lazr.restful.interfaces.IFieldMarshaller"
220+ factory="lp.services.webapp.marshallers.choiceMarshallerError"
221+ />
222+ <adapter
223 for="lazr.restful.interfaces.IReferenceChoice
224 zope.publisher.interfaces.http.IHTTPRequest
225 lp.services.webapp.vocabulary.SQLObjectVocabularyBase"
226 provides="lazr.restful.interfaces.IFieldMarshaller"
227 factory="lazr.restful.marshallers.ObjectLookupFieldMarshaller"
228 />
229+ <adapter
230+ for="lazr.restful.interfaces.IReferenceChoice
231+ zope.publisher.interfaces.http.IHTTPRequest
232+ lp.services.webapp.vocabulary.StormVocabularyBase"
233+ provides="lazr.restful.interfaces.IFieldMarshaller"
234+ factory="lazr.restful.marshallers.ObjectLookupFieldMarshaller"
235+ />
236
237 <!-- The API documentation -->
238 <browser:page
239
240=== modified file 'lib/lp/services/webservice/doc/webservice-marshallers.txt'
241--- lib/lp/services/webservice/doc/webservice-marshallers.txt 2011-12-24 17:49:30 +0000
242+++ lib/lp/services/webservice/doc/webservice-marshallers.txt 2015-04-28 15:36:55 +0000
243@@ -99,7 +99,7 @@
244 Traceback (most recent call last):
245 ...
246 AssertionError: You exported some_person as an IChoice based on an
247- SQLObjectVocabularyBase, you should use
248+ SQLObjectVocabularyBase/StormVocabularyBase; you should use
249 lazr.restful.fields.ReferenceChoice instead.
250
251 Cleanup.