Merge lp:~mwhudson/launchpad/move-SpecificationDepCandidatesVocabulary into lp:launchpad
- move-SpecificationDepCandidatesVocabulary
- Merge into devel
Proposed by
Michael Hudson-Doyle
Status: | Merged |
---|---|
Approved by: | Michael Hudson-Doyle |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11446 |
Proposed branch: | lp:~mwhudson/launchpad/move-SpecificationDepCandidatesVocabulary |
Merge into: | lp:launchpad |
Diff against target: |
735 lines (+292/-223) 9 files modified
lib/canonical/launchpad/doc/vocabularies.txt (+38/-124) lib/canonical/launchpad/vocabularies/configure.zcml (+0/-14) lib/canonical/launchpad/vocabularies/dbobjects.py (+0/-80) lib/lp/blueprints/configure.zcml (+1/-0) lib/lp/blueprints/vocabularies/configure.zcml (+19/-0) lib/lp/blueprints/vocabularies/specificationdependency.py (+108/-0) lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt (+99/-0) lib/lp/blueprints/vocabularies/tests/test_doc.py (+17/-0) lib/lp/testing/factory.py (+10/-5) |
To merge this branch: | bzr merge lp:~mwhudson/launchpad/move-SpecificationDepCandidatesVocabulary |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+33611@code.launchpad.net |
Commit message
Move the SpecificationDe
Description of the change
Hi there,
As a prep for some linaro work on blueprints (bug 3552), I moved the implementation and tests for SpecificationDe
I tidied up the test a very little bit and de-moined the doctest they came from. I didn't touch the implementation at all.
Cheers,
mwh
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/launchpad/doc/vocabularies.txt' | |||
2 | --- lib/canonical/launchpad/doc/vocabularies.txt 2010-02-17 11:13:06 +0000 | |||
3 | +++ lib/canonical/launchpad/doc/vocabularies.txt 2010-08-25 21:37:45 +0000 | |||
4 | @@ -1,6 +1,8 @@ | |||
6 | 1 | = Vocabularies = | 1 | Vocabularies |
7 | 2 | ============ | ||
8 | 2 | 3 | ||
10 | 3 | == Introduction == | 4 | Introduction |
11 | 5 | ------------ | ||
12 | 4 | 6 | ||
13 | 5 | Vocabularies are lists of terms. In Launchpad's Component Architecture | 7 | Vocabularies are lists of terms. In Launchpad's Component Architecture |
14 | 6 | (CA), a vocabulary is a list of terms that a widget (normally a selection | 8 | (CA), a vocabulary is a list of terms that a widget (normally a selection |
15 | @@ -18,7 +20,8 @@ | |||
16 | 18 | >>> launchbag.clear() | 20 | >>> launchbag.clear() |
17 | 19 | 21 | ||
18 | 20 | 22 | ||
20 | 21 | === Values, Tokens, and Titles === | 23 | Values, Tokens, and Titles |
21 | 24 | .......................... | ||
22 | 22 | 25 | ||
23 | 23 | In Launchpad, we generally use "tokenized vocabularies." Each term in | 26 | In Launchpad, we generally use "tokenized vocabularies." Each term in |
24 | 24 | a vocabulary has a value, token and title. A term is rendered in a | 27 | a vocabulary has a value, token and title. A term is rendered in a |
25 | @@ -31,7 +34,8 @@ | |||
26 | 31 | to the user. | 34 | to the user. |
27 | 32 | 35 | ||
28 | 33 | 36 | ||
30 | 34 | == Launchpad Vocabularies == | 37 | Launchpad Vocabularies |
31 | 38 | ---------------------- | ||
32 | 35 | 39 | ||
33 | 36 | There are two kinds of vocabularies in Launchpad: enumerable and | 40 | There are two kinds of vocabularies in Launchpad: enumerable and |
34 | 37 | non-enumerable. Enumerable vocabularies are short enough to render in a | 41 | non-enumerable. Enumerable vocabularies are short enough to render in a |
35 | @@ -53,10 +57,12 @@ | |||
36 | 53 | 'Select a project' | 57 | 'Select a project' |
37 | 54 | 58 | ||
38 | 55 | 59 | ||
43 | 56 | == Enumerable Vocabularies == | 60 | Enumerable Vocabularies |
44 | 57 | 61 | ----------------------- | |
45 | 58 | 62 | ||
46 | 59 | === DistributionUsingMaloneVocabulary === | 63 | |
47 | 64 | DistributionUsingMaloneVocabulary | ||
48 | 65 | ................................. | ||
49 | 60 | 66 | ||
50 | 61 | All the distributions that use Malone as their main bug tracker. | 67 | All the distributions that use Malone as their main bug tracker. |
51 | 62 | 68 | ||
52 | @@ -99,7 +105,8 @@ | |||
53 | 99 | LookupError:... | 105 | LookupError:... |
54 | 100 | 106 | ||
55 | 101 | 107 | ||
57 | 102 | === BugNominatableSeriesVocabulary === | 108 | BugNominatableSeriesVocabulary |
58 | 109 | .............................. | ||
59 | 103 | 110 | ||
60 | 104 | All the series that can be nominated for fixing. | 111 | All the series that can be nominated for fixing. |
61 | 105 | 112 | ||
62 | @@ -244,7 +251,8 @@ | |||
63 | 244 | NoSuchDistroSeries... | 251 | NoSuchDistroSeries... |
64 | 245 | 252 | ||
65 | 246 | 253 | ||
67 | 247 | === ProjectProductsVocabularyUsingMalone === | 254 | ProjectProductsVocabularyUsingMalone |
68 | 255 | .................................... | ||
69 | 248 | 256 | ||
70 | 249 | All the products in a project using Malone. | 257 | All the products in a project using Malone. |
71 | 250 | 258 | ||
72 | @@ -262,14 +270,16 @@ | |||
73 | 262 | firefox: Mozilla Firefox | 270 | firefox: Mozilla Firefox |
74 | 263 | 271 | ||
75 | 264 | 272 | ||
77 | 265 | == Non-Enumerable Vocabularies == | 273 | Non-Enumerable Vocabularies |
78 | 274 | --------------------------- | ||
79 | 266 | 275 | ||
80 | 267 | Iterating over non-enumerable vocabularies, while possible, will | 276 | Iterating over non-enumerable vocabularies, while possible, will |
81 | 268 | probably kill the database. Instead, these vocabularies are | 277 | probably kill the database. Instead, these vocabularies are |
82 | 269 | search-driven. | 278 | search-driven. |
83 | 270 | 279 | ||
84 | 271 | 280 | ||
86 | 272 | === BinaryAndSourcePackageNameVocabulary === | 281 | BinaryAndSourcePackageNameVocabulary |
87 | 282 | .................................... | ||
88 | 273 | 283 | ||
89 | 274 | The list of binary and source package names, ordered by name. | 284 | The list of binary and source package names, ordered by name. |
90 | 275 | 285 | ||
91 | @@ -315,7 +325,8 @@ | |||
92 | 315 | ('linux-source-2.6.15', u'Source of: linux-2.6.12')] | 325 | ('linux-source-2.6.15', u'Source of: linux-2.6.12')] |
93 | 316 | 326 | ||
94 | 317 | 327 | ||
96 | 318 | === BinaryPackageNameVocabulary === | 328 | BinaryPackageNameVocabulary |
97 | 329 | ........................... | ||
98 | 319 | 330 | ||
99 | 320 | All the binary packages in Launchpad. | 331 | All the binary packages in Launchpad. |
100 | 321 | 332 | ||
101 | @@ -331,7 +342,8 @@ | |||
102 | 331 | ('mozilla-firefox-data', u'Mozilla Firefox Data is .....')] | 342 | ('mozilla-firefox-data', u'Mozilla Firefox Data is .....')] |
103 | 332 | 343 | ||
104 | 333 | 344 | ||
106 | 334 | === SourcePackageNameVocabulary === | 345 | SourcePackageNameVocabulary |
107 | 346 | ........................... | ||
108 | 335 | 347 | ||
109 | 336 | All the source packages in Launchpad. | 348 | All the source packages in Launchpad. |
110 | 337 | 349 | ||
111 | @@ -352,7 +364,8 @@ | |||
112 | 352 | [('pmount', u'pmount')] | 364 | [('pmount', u'pmount')] |
113 | 353 | 365 | ||
114 | 354 | 366 | ||
116 | 355 | === BranchVocabulary === | 367 | BranchVocabulary |
117 | 368 | ................ | ||
118 | 356 | 369 | ||
119 | 357 | The list of bzr branches registered in Launchpad. | 370 | The list of bzr branches registered in Launchpad. |
120 | 358 | 371 | ||
121 | @@ -432,7 +445,8 @@ | |||
122 | 432 | >>> login('foo.bar@canonical.com') | 445 | >>> login('foo.bar@canonical.com') |
123 | 433 | 446 | ||
124 | 434 | 447 | ||
126 | 435 | === BranchRestrictedOnProduct === | 448 | BranchRestrictedOnProduct |
127 | 449 | ......................... | ||
128 | 436 | 450 | ||
129 | 437 | The BranchRestrictedOnProduct vocabulary restricts the result set to | 451 | The BranchRestrictedOnProduct vocabulary restricts the result set to |
130 | 438 | those of the product of the context. Currently only two types of | 452 | those of the product of the context. Currently only two types of |
131 | @@ -461,7 +475,8 @@ | |||
132 | 461 | BranchVocabulary with respect to the tokens and privacy awareness. | 475 | BranchVocabulary with respect to the tokens and privacy awareness. |
133 | 462 | 476 | ||
134 | 463 | 477 | ||
136 | 464 | === HostedBranchRestrictedOnOwner === | 478 | HostedBranchRestrictedOnOwner |
137 | 479 | ............................. | ||
138 | 465 | 480 | ||
139 | 466 | Here's a vocabulary for all hosted branches owned by the current user. | 481 | Here's a vocabulary for all hosted branches owned by the current user. |
140 | 467 | 482 | ||
141 | @@ -486,7 +501,8 @@ | |||
142 | 486 | ~a-branching-user/product-two/hosted | 501 | ~a-branching-user/product-two/hosted |
143 | 487 | 502 | ||
144 | 488 | 503 | ||
146 | 489 | === Processor === | 504 | Processor |
147 | 505 | ......... | ||
148 | 490 | 506 | ||
149 | 491 | All processors type available in Launchpad. | 507 | All processors type available in Launchpad. |
150 | 492 | 508 | ||
151 | @@ -498,7 +514,8 @@ | |||
152 | 498 | ['386'] | 514 | ['386'] |
153 | 499 | 515 | ||
154 | 500 | 516 | ||
156 | 501 | === BugWatchVocabulary === | 517 | BugWatchVocabulary |
157 | 518 | .................. | ||
158 | 502 | 519 | ||
159 | 503 | All bug watches associated with a bugtask's bug. | 520 | All bug watches associated with a bugtask's bug. |
160 | 504 | 521 | ||
161 | @@ -555,111 +572,8 @@ | |||
162 | 555 | Lionel Richtea (mailto:<email address hidden>) | 572 | Lionel Richtea (mailto:<email address hidden>) |
163 | 556 | 573 | ||
164 | 557 | 574 | ||
270 | 558 | == SpecificationDepCandidatesVocabulary == | 575 | PPA |
271 | 559 | 576 | ... | |
167 | 560 | All blueprints that can be added as a dependency of the | ||
168 | 561 | context blueprint. | ||
169 | 562 | |||
170 | 563 | First, we set up a product with three blueprints. | ||
171 | 564 | |||
172 | 565 | >>> from canonical.launchpad.interfaces import ( | ||
173 | 566 | ... ISpecificationSet, SpecificationDefinitionStatus) | ||
174 | 567 | >>> evolution = product_set.getByName('evolution') | ||
175 | 568 | >>> foobar_person = person_set.getByName('name16') | ||
176 | 569 | >>> foobar_person.displayname | ||
177 | 570 | u'Foo Bar' | ||
178 | 571 | >>> specset = getUtility(ISpecificationSet) | ||
179 | 572 | >>> spec_a = specset.new('spec-a', 'Spec A', | ||
180 | 573 | ... 'http://www.example.org/SpecA', 'The first spec', | ||
181 | 574 | ... SpecificationDefinitionStatus.APPROVED, foobar_person, | ||
182 | 575 | ... product=evolution) | ||
183 | 576 | >>> spec_b = specset.new('spec-b', 'Spec B', | ||
184 | 577 | ... 'http://www.example.org/SpecB', 'The second spec', | ||
185 | 578 | ... SpecificationDefinitionStatus.APPROVED, foobar_person, | ||
186 | 579 | ... product=evolution) | ||
187 | 580 | >>> spec_c = specset.new('spec-c', 'Spec C', | ||
188 | 581 | ... 'http://www.example.org/SpecC', 'The third spec', | ||
189 | 582 | ... SpecificationDefinitionStatus.APPROVED, foobar_person, | ||
190 | 583 | ... product=evolution) | ||
191 | 584 | >>> sorted([spec.name for spec in evolution.specifications()]) | ||
192 | 585 | [u'spec-a', u'spec-b', u'spec-c'] | ||
193 | 586 | |||
194 | 587 | The dependency candidates for spec_a are all blueprints for evolution | ||
195 | 588 | except for spec_a itself. | ||
196 | 589 | |||
197 | 590 | >>> vocab = vocabulary_registry.get( | ||
198 | 591 | ... spec_a, "SpecificationDepCandidates") | ||
199 | 592 | >>> sorted([term.value.name for term in vocab]) | ||
200 | 593 | [u'spec-b', u'spec-c'] | ||
201 | 594 | |||
202 | 595 | Dependency candidate come only from the same product of the blueprint | ||
203 | 596 | they depend on. | ||
204 | 597 | |||
205 | 598 | >>> unrelated_spec = specset.new('unrelated-spec', 'Unrelated Spec', | ||
206 | 599 | ... 'http://example.com/SpecU', 'A spec unrelated to Evolution', | ||
207 | 600 | ... SpecificationDefinitionStatus.APPROVED, foobar_person, | ||
208 | 601 | ... product=firefox) | ||
209 | 602 | >>> vocab = vocabulary_registry.get( | ||
210 | 603 | ... spec_a, "SpecificationDepCandidates") | ||
211 | 604 | >>> unrelated_spec in vocab | ||
212 | 605 | False | ||
213 | 606 | >>> [term.value.product for term in vocab | ||
214 | 607 | ... if term.value.product != evolution] | ||
215 | 608 | [] | ||
216 | 609 | |||
217 | 610 | We mark spec_b as a dependency of spec_a and spec_c as a dependency | ||
218 | 611 | of spec_b. | ||
219 | 612 | |||
220 | 613 | >>> spec_a.createDependency(spec_b) | ||
221 | 614 | <SpecificationDependency at ...> | ||
222 | 615 | >>> [spec.name for spec in spec_a.dependencies] | ||
223 | 616 | [u'spec-b'] | ||
224 | 617 | |||
225 | 618 | >>> spec_b.createDependency(spec_c) | ||
226 | 619 | <SpecificationDependency at ...> | ||
227 | 620 | >>> [spec.name for spec in spec_b.dependencies] | ||
228 | 621 | [u'spec-c'] | ||
229 | 622 | |||
230 | 623 | No circular dependencies - the vocabulary excludes specifications that | ||
231 | 624 | are a dependency of the context spec. | ||
232 | 625 | |||
233 | 626 | >>> spec_a in spec_b.all_blocked | ||
234 | 627 | True | ||
235 | 628 | >>> spec_b in spec_c.all_blocked | ||
236 | 629 | True | ||
237 | 630 | >>> vocab = vocabulary_registry.get( | ||
238 | 631 | ... spec_c, "SpecificationDepCandidates") | ||
239 | 632 | >>> spec_a in [term.value for term in vocab] | ||
240 | 633 | False | ||
241 | 634 | |||
242 | 635 | This vocabulary provides the IHugeVocabulary interface. | ||
243 | 636 | |||
244 | 637 | >>> from canonical.launchpad.webapp.testing import verifyObject | ||
245 | 638 | >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary | ||
246 | 639 | >>> verifyObject(IHugeVocabulary, vocab) | ||
247 | 640 | True | ||
248 | 641 | |||
249 | 642 | The search() method returns specifications within the vocabulary | ||
250 | 643 | that matches the search string. The string is matched against the name, | ||
251 | 644 | or fallbacks to a full text search. | ||
252 | 645 | |||
253 | 646 | >>> vocab = get_naked_vocab(spec_a, "SpecificationDepCandidates") | ||
254 | 647 | >>> list(vocab.search('spec-b')) == [spec_b] | ||
255 | 648 | True | ||
256 | 649 | >>> list(vocab.search('third')) == [spec_c] | ||
257 | 650 | True | ||
258 | 651 | |||
259 | 652 | The search method uses the SQL `LIKE` operator, with the values quoted | ||
260 | 653 | appropriately. Queries conataining regual expression operators, for | ||
261 | 654 | example, will simply look for the respective characters within the | ||
262 | 655 | vocabulary's item (this used to be the cause of an OOPS, see | ||
263 | 656 | https://bugs.edge.launchpad.net/blueprint/+bug/139385 for more details). | ||
264 | 657 | |||
265 | 658 | >>> list(vocab.search('*')) | ||
266 | 659 | [] | ||
267 | 660 | |||
268 | 661 | |||
269 | 662 | === PPA === | ||
272 | 663 | 577 | ||
273 | 664 | The PPA vocabulary contains all the PPAs available in a particular | 578 | The PPA vocabulary contains all the PPAs available in a particular |
274 | 665 | collection. It provides the IHugeVocabulary interface. | 579 | collection. It provides the IHugeVocabulary interface. |
275 | 666 | 580 | ||
276 | === modified file 'lib/canonical/launchpad/vocabularies/configure.zcml' | |||
277 | --- lib/canonical/launchpad/vocabularies/configure.zcml 2010-06-21 04:08:54 +0000 | |||
278 | +++ lib/canonical/launchpad/vocabularies/configure.zcml 2010-08-25 21:37:45 +0000 | |||
279 | @@ -309,20 +309,6 @@ | |||
280 | 309 | <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/> | 309 | <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/> |
281 | 310 | </class> | 310 | </class> |
282 | 311 | 311 | ||
283 | 312 | |||
284 | 313 | <securedutility | ||
285 | 314 | name="SpecificationDepCandidates" | ||
286 | 315 | component="canonical.launchpad.vocabularies.SpecificationDepCandidatesVocabulary" | ||
287 | 316 | provides="zope.schema.interfaces.IVocabularyFactory" | ||
288 | 317 | > | ||
289 | 318 | <allow interface="zope.schema.interfaces.IVocabularyFactory"/> | ||
290 | 319 | </securedutility> | ||
291 | 320 | |||
292 | 321 | <class class="canonical.launchpad.vocabularies.SpecificationDepCandidatesVocabulary"> | ||
293 | 322 | <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/> | ||
294 | 323 | </class> | ||
295 | 324 | |||
296 | 325 | |||
297 | 326 | <securedutility | 312 | <securedutility |
298 | 327 | name="Sprint" | 313 | name="Sprint" |
299 | 328 | component="canonical.launchpad.vocabularies.SprintVocabulary" | 314 | component="canonical.launchpad.vocabularies.SprintVocabulary" |
300 | 329 | 315 | ||
301 | === modified file 'lib/canonical/launchpad/vocabularies/dbobjects.py' | |||
302 | --- lib/canonical/launchpad/vocabularies/dbobjects.py 2010-08-20 20:31:18 +0000 | |||
303 | +++ lib/canonical/launchpad/vocabularies/dbobjects.py 2010-08-25 21:37:45 +0000 | |||
304 | @@ -33,7 +33,6 @@ | |||
305 | 33 | 'ProcessorFamilyVocabulary', | 33 | 'ProcessorFamilyVocabulary', |
306 | 34 | 'ProcessorVocabulary', | 34 | 'ProcessorVocabulary', |
307 | 35 | 'project_products_using_malone_vocabulary_factory', | 35 | 'project_products_using_malone_vocabulary_factory', |
308 | 36 | 'SpecificationDepCandidatesVocabulary', | ||
309 | 37 | 'SpecificationDependenciesVocabulary', | 36 | 'SpecificationDependenciesVocabulary', |
310 | 38 | 'SpecificationVocabulary', | 37 | 'SpecificationVocabulary', |
311 | 39 | 'SprintVocabulary', | 38 | 'SprintVocabulary', |
312 | @@ -70,7 +69,6 @@ | |||
313 | 70 | 69 | ||
314 | 71 | from canonical.database.sqlbase import ( | 70 | from canonical.database.sqlbase import ( |
315 | 72 | quote, | 71 | quote, |
316 | 73 | quote_like, | ||
317 | 74 | sqlvalues, | 72 | sqlvalues, |
318 | 75 | ) | 73 | ) |
319 | 76 | from canonical.launchpad.database import ( | 74 | from canonical.launchpad.database import ( |
320 | @@ -87,7 +85,6 @@ | |||
321 | 87 | SQLObjectVocabularyBase, | 85 | SQLObjectVocabularyBase, |
322 | 88 | ) | 86 | ) |
323 | 89 | from lp.app.browser.stringformatter import FormattersAPI | 87 | from lp.app.browser.stringformatter import FormattersAPI |
324 | 90 | from lp.blueprints.interfaces.specification import SpecificationFilter | ||
325 | 91 | from lp.blueprints.model.specification import Specification | 88 | from lp.blueprints.model.specification import Specification |
326 | 92 | from lp.blueprints.model.sprint import Sprint | 89 | from lp.blueprints.model.sprint import Sprint |
327 | 93 | from lp.bugs.interfaces.bugtask import IBugTask | 90 | from lp.bugs.interfaces.bugtask import IBugTask |
328 | @@ -518,83 +515,6 @@ | |||
329 | 518 | yield SimpleTerm(spec, spec.name, spec.title) | 515 | yield SimpleTerm(spec, spec.name, spec.title) |
330 | 519 | 516 | ||
331 | 520 | 517 | ||
332 | 521 | class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase): | ||
333 | 522 | """Specifications that could be dependencies of this spec. | ||
334 | 523 | |||
335 | 524 | This includes only those specs that are not blocked by this spec | ||
336 | 525 | (directly or indirectly), unless they are already dependencies. | ||
337 | 526 | |||
338 | 527 | The current spec is not included. | ||
339 | 528 | """ | ||
340 | 529 | |||
341 | 530 | implements(IHugeVocabulary) | ||
342 | 531 | |||
343 | 532 | _table = Specification | ||
344 | 533 | _orderBy = 'name' | ||
345 | 534 | displayname = 'Select a blueprint' | ||
346 | 535 | |||
347 | 536 | def _filter_specs(self, specs): | ||
348 | 537 | # XXX intellectronica 2007-07-05: is 100 a reasonable count before | ||
349 | 538 | # starting to warn? | ||
350 | 539 | speclist = shortlist(specs, 100) | ||
351 | 540 | return [spec for spec in speclist | ||
352 | 541 | if (spec != self.context and | ||
353 | 542 | spec.target == self.context.target | ||
354 | 543 | and spec not in self.context.all_blocked)] | ||
355 | 544 | |||
356 | 545 | def _doSearch(self, query): | ||
357 | 546 | """Return terms where query is in the text of name | ||
358 | 547 | or title, or matches the full text index. | ||
359 | 548 | """ | ||
360 | 549 | |||
361 | 550 | if not query: | ||
362 | 551 | return [] | ||
363 | 552 | |||
364 | 553 | quoted_query = quote_like(query) | ||
365 | 554 | sql_query = (""" | ||
366 | 555 | (Specification.name LIKE %s OR | ||
367 | 556 | Specification.title LIKE %s OR | ||
368 | 557 | fti @@ ftq(%s)) | ||
369 | 558 | """ | ||
370 | 559 | % (quoted_query, quoted_query, quoted_query)) | ||
371 | 560 | all_specs = Specification.select(sql_query, orderBy=self._orderBy) | ||
372 | 561 | |||
373 | 562 | return self._filter_specs(all_specs) | ||
374 | 563 | |||
375 | 564 | def toTerm(self, obj): | ||
376 | 565 | return SimpleTerm(obj, obj.name, obj.title) | ||
377 | 566 | |||
378 | 567 | def getTermByToken(self, token): | ||
379 | 568 | search_results = self._doSearch(token) | ||
380 | 569 | for search_result in search_results: | ||
381 | 570 | if search_result.name == token: | ||
382 | 571 | return self.toTerm(search_result) | ||
383 | 572 | raise LookupError(token) | ||
384 | 573 | |||
385 | 574 | def search(self, query): | ||
386 | 575 | candidate_specs = self._doSearch(query) | ||
387 | 576 | return CountableIterator(len(candidate_specs), | ||
388 | 577 | candidate_specs) | ||
389 | 578 | |||
390 | 579 | def _all_specs(self): | ||
391 | 580 | all_specs = self.context.target.specifications( | ||
392 | 581 | filter=[SpecificationFilter.ALL], | ||
393 | 582 | prejoin_people=False) | ||
394 | 583 | return self._filter_specs(all_specs) | ||
395 | 584 | |||
396 | 585 | def __iter__(self): | ||
397 | 586 | return (self.toTerm(spec) for spec in self._all_specs()) | ||
398 | 587 | |||
399 | 588 | def __contains__(self, obj): | ||
400 | 589 | # We don't use self._all_specs here, since it will call | ||
401 | 590 | # self._filter_specs(all_specs) which will cause all the specs | ||
402 | 591 | # to be loaded, whereas obj in all_specs will query a single object. | ||
403 | 592 | all_specs = self.context.target.specifications( | ||
404 | 593 | filter=[SpecificationFilter.ALL], | ||
405 | 594 | prejoin_people=False) | ||
406 | 595 | return obj in all_specs and len(self._filter_specs([obj])) > 0 | ||
407 | 596 | |||
408 | 597 | |||
409 | 598 | class SprintVocabulary(NamedSQLObjectVocabulary): | 518 | class SprintVocabulary(NamedSQLObjectVocabulary): |
410 | 599 | _table = Sprint | 519 | _table = Sprint |
411 | 600 | 520 | ||
412 | 601 | 521 | ||
413 | === modified file 'lib/lp/blueprints/configure.zcml' | |||
414 | --- lib/lp/blueprints/configure.zcml 2010-08-19 03:06:27 +0000 | |||
415 | +++ lib/lp/blueprints/configure.zcml 2010-08-25 21:37:45 +0000 | |||
416 | @@ -11,6 +11,7 @@ | |||
417 | 11 | i18n_domain="launchpad"> | 11 | i18n_domain="launchpad"> |
418 | 12 | 12 | ||
419 | 13 | <include package=".browser"/> | 13 | <include package=".browser"/> |
420 | 14 | <include package=".vocabularies"/> | ||
421 | 14 | 15 | ||
422 | 15 | <publisher | 16 | <publisher |
423 | 16 | name="blueprints" | 17 | name="blueprints" |
424 | 17 | 18 | ||
425 | === added directory 'lib/lp/blueprints/vocabularies' | |||
426 | === added file 'lib/lp/blueprints/vocabularies/__init__.py' | |||
427 | === added file 'lib/lp/blueprints/vocabularies/configure.zcml' | |||
428 | --- lib/lp/blueprints/vocabularies/configure.zcml 1970-01-01 00:00:00 +0000 | |||
429 | +++ lib/lp/blueprints/vocabularies/configure.zcml 2010-08-25 21:37:45 +0000 | |||
430 | @@ -0,0 +1,19 @@ | |||
431 | 1 | <!-- Copyright 2010 Canonical Ltd. This software is licensed under the | ||
432 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | ||
433 | 3 | --> | ||
434 | 4 | |||
435 | 5 | <configure xmlns="http://namespaces.zope.org/zope"> | ||
436 | 6 | |||
437 | 7 | <securedutility | ||
438 | 8 | name="SpecificationDepCandidates" | ||
439 | 9 | component=".specificationdependency.SpecificationDepCandidatesVocabulary" | ||
440 | 10 | provides="zope.schema.interfaces.IVocabularyFactory" | ||
441 | 11 | > | ||
442 | 12 | <allow interface="zope.schema.interfaces.IVocabularyFactory"/> | ||
443 | 13 | </securedutility> | ||
444 | 14 | |||
445 | 15 | <class class=".specificationdependency.SpecificationDepCandidatesVocabulary"> | ||
446 | 16 | <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/> | ||
447 | 17 | </class> | ||
448 | 18 | |||
449 | 19 | </configure> | ||
450 | 0 | 20 | ||
451 | === added file 'lib/lp/blueprints/vocabularies/specificationdependency.py' | |||
452 | --- lib/lp/blueprints/vocabularies/specificationdependency.py 1970-01-01 00:00:00 +0000 | |||
453 | +++ lib/lp/blueprints/vocabularies/specificationdependency.py 2010-08-25 21:37:45 +0000 | |||
454 | @@ -0,0 +1,108 @@ | |||
455 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
456 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
457 | 3 | |||
458 | 4 | """The vocabularies relating to dependencies of specifications.""" | ||
459 | 5 | |||
460 | 6 | __metaclass__ = type | ||
461 | 7 | __all__ = ['SpecificationDepCandidatesVocabulary'] | ||
462 | 8 | |||
463 | 9 | from zope.interface import implements | ||
464 | 10 | from zope.schema.vocabulary import SimpleTerm | ||
465 | 11 | |||
466 | 12 | from canonical.database.sqlbase import quote_like | ||
467 | 13 | from canonical.launchpad.helpers import shortlist | ||
468 | 14 | from canonical.launchpad.webapp.vocabulary import ( | ||
469 | 15 | CountableIterator, | ||
470 | 16 | IHugeVocabulary, | ||
471 | 17 | SQLObjectVocabularyBase, | ||
472 | 18 | ) | ||
473 | 19 | |||
474 | 20 | from lp.blueprints.interfaces.specification import SpecificationFilter | ||
475 | 21 | from lp.blueprints.model.specification import Specification | ||
476 | 22 | |||
477 | 23 | |||
478 | 24 | class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase): | ||
479 | 25 | """Specifications that could be dependencies of this spec. | ||
480 | 26 | |||
481 | 27 | This includes only those specs that are not blocked by this spec | ||
482 | 28 | (directly or indirectly), unless they are already dependencies. | ||
483 | 29 | |||
484 | 30 | The current spec is not included. | ||
485 | 31 | """ | ||
486 | 32 | |||
487 | 33 | implements(IHugeVocabulary) | ||
488 | 34 | |||
489 | 35 | _table = Specification | ||
490 | 36 | _orderBy = 'name' | ||
491 | 37 | displayname = 'Select a blueprint' | ||
492 | 38 | |||
493 | 39 | def _filter_specs(self, specs): | ||
494 | 40 | """Filter `specs` to remove invalid candidates. | ||
495 | 41 | |||
496 | 42 | Invalid candidates are: | ||
497 | 43 | |||
498 | 44 | * The spec that we're adding a depdency to, | ||
499 | 45 | * Specs for a different target, and | ||
500 | 46 | * Specs that depend on this one. | ||
501 | 47 | |||
502 | 48 | Preventing the last category prevents loops in the dependency graph. | ||
503 | 49 | """ | ||
504 | 50 | # XXX intellectronica 2007-07-05: is 100 a reasonable count before | ||
505 | 51 | # starting to warn? | ||
506 | 52 | speclist = shortlist(specs, 100) | ||
507 | 53 | return [spec for spec in speclist | ||
508 | 54 | if (spec != self.context and | ||
509 | 55 | spec.target == self.context.target | ||
510 | 56 | and spec not in self.context.all_blocked)] | ||
511 | 57 | |||
512 | 58 | def _doSearch(self, query): | ||
513 | 59 | """Return terms where query is in the text of name | ||
514 | 60 | or title, or matches the full text index. | ||
515 | 61 | """ | ||
516 | 62 | |||
517 | 63 | if not query: | ||
518 | 64 | return [] | ||
519 | 65 | |||
520 | 66 | quoted_query = quote_like(query) | ||
521 | 67 | sql_query = (""" | ||
522 | 68 | (Specification.name LIKE %s OR | ||
523 | 69 | Specification.title LIKE %s OR | ||
524 | 70 | fti @@ ftq(%s)) | ||
525 | 71 | """ | ||
526 | 72 | % (quoted_query, quoted_query, quoted_query)) | ||
527 | 73 | all_specs = Specification.select(sql_query, orderBy=self._orderBy) | ||
528 | 74 | |||
529 | 75 | return self._filter_specs(all_specs) | ||
530 | 76 | |||
531 | 77 | def toTerm(self, obj): | ||
532 | 78 | return SimpleTerm(obj, obj.name, obj.title) | ||
533 | 79 | |||
534 | 80 | def getTermByToken(self, token): | ||
535 | 81 | search_results = self._doSearch(token) | ||
536 | 82 | for search_result in search_results: | ||
537 | 83 | if search_result.name == token: | ||
538 | 84 | return self.toTerm(search_result) | ||
539 | 85 | raise LookupError(token) | ||
540 | 86 | |||
541 | 87 | def search(self, query): | ||
542 | 88 | candidate_specs = self._doSearch(query) | ||
543 | 89 | return CountableIterator(len(candidate_specs), | ||
544 | 90 | candidate_specs) | ||
545 | 91 | |||
546 | 92 | def _all_specs(self): | ||
547 | 93 | all_specs = self.context.target.specifications( | ||
548 | 94 | filter=[SpecificationFilter.ALL], | ||
549 | 95 | prejoin_people=False) | ||
550 | 96 | return self._filter_specs(all_specs) | ||
551 | 97 | |||
552 | 98 | def __iter__(self): | ||
553 | 99 | return (self.toTerm(spec) for spec in self._all_specs()) | ||
554 | 100 | |||
555 | 101 | def __contains__(self, obj): | ||
556 | 102 | # We don't use self._all_specs here, since it will call | ||
557 | 103 | # self._filter_specs(all_specs) which will cause all the specs | ||
558 | 104 | # to be loaded, whereas obj in all_specs will query a single object. | ||
559 | 105 | all_specs = self.context.target.specifications( | ||
560 | 106 | filter=[SpecificationFilter.ALL], | ||
561 | 107 | prejoin_people=False) | ||
562 | 108 | return obj in all_specs and len(self._filter_specs([obj])) > 0 | ||
563 | 0 | 109 | ||
564 | === added directory 'lib/lp/blueprints/vocabularies/tests' | |||
565 | === added file 'lib/lp/blueprints/vocabularies/tests/__init__.py' | |||
566 | === added file 'lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt' | |||
567 | --- lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 1970-01-01 00:00:00 +0000 | |||
568 | +++ lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 2010-08-25 21:37:45 +0000 | |||
569 | @@ -0,0 +1,99 @@ | |||
570 | 1 | SpecificationDepCandidatesVocabulary | ||
571 | 2 | ==================================== | ||
572 | 3 | |||
573 | 4 | All blueprints that can be added as a dependency of the context | ||
574 | 5 | blueprint. | ||
575 | 6 | |||
576 | 7 | >>> from zope.schema.vocabulary import getVocabularyRegistry | ||
577 | 8 | >>> vocabulary_registry = getVocabularyRegistry() | ||
578 | 9 | |||
579 | 10 | First, we set up a product with three blueprints. | ||
580 | 11 | |||
581 | 12 | >>> specced_product = factory.makeProduct() | ||
582 | 13 | >>> spec_a = factory.makeSpecification( | ||
583 | 14 | ... name='spec-a', summary='The first spec', | ||
584 | 15 | ... product=specced_product) | ||
585 | 16 | >>> spec_b = factory.makeSpecification( | ||
586 | 17 | ... name='spec-b', summary='The second spec', | ||
587 | 18 | ... product=specced_product) | ||
588 | 19 | >>> spec_c = factory.makeSpecification( | ||
589 | 20 | ... name='spec-c', summary='The third spec', | ||
590 | 21 | ... product=specced_product) | ||
591 | 22 | >>> sorted([spec.name for spec in specced_product.specifications()]) | ||
592 | 23 | [u'spec-a', u'spec-b', u'spec-c'] | ||
593 | 24 | |||
594 | 25 | The dependency candidates for spec_a are all blueprints for | ||
595 | 26 | specced_product except for spec_a itself. | ||
596 | 27 | |||
597 | 28 | >>> vocab = vocabulary_registry.get( | ||
598 | 29 | ... spec_a, "SpecificationDepCandidates") | ||
599 | 30 | >>> sorted([term.value.name for term in vocab]) | ||
600 | 31 | [u'spec-b', u'spec-c'] | ||
601 | 32 | |||
602 | 33 | Dependency candidate come only from the same product of the blueprint | ||
603 | 34 | they depend on. | ||
604 | 35 | |||
605 | 36 | >>> unrelated_spec = factory.makeSpecification( | ||
606 | 37 | ... product=factory.makeProduct()) | ||
607 | 38 | >>> vocab = vocabulary_registry.get( | ||
608 | 39 | ... spec_a, "SpecificationDepCandidates") | ||
609 | 40 | >>> unrelated_spec in vocab | ||
610 | 41 | False | ||
611 | 42 | >>> [term.value.product for term in vocab | ||
612 | 43 | ... if term.value.product != specced_product] | ||
613 | 44 | [] | ||
614 | 45 | |||
615 | 46 | We mark spec_b as a dependency of spec_a and spec_c as a dependency of | ||
616 | 47 | spec_b. | ||
617 | 48 | |||
618 | 49 | >>> spec_a.createDependency(spec_b) | ||
619 | 50 | <SpecificationDependency at ...> | ||
620 | 51 | >>> [spec.name for spec in spec_a.dependencies] | ||
621 | 52 | [u'spec-b'] | ||
622 | 53 | |||
623 | 54 | >>> spec_b.createDependency(spec_c) | ||
624 | 55 | <SpecificationDependency at ...> | ||
625 | 56 | >>> [spec.name for spec in spec_b.dependencies] | ||
626 | 57 | [u'spec-c'] | ||
627 | 58 | |||
628 | 59 | No circular dependencies - the vocabulary excludes specifications that | ||
629 | 60 | are a dependency of the context spec. | ||
630 | 61 | |||
631 | 62 | >>> spec_a in spec_b.all_blocked | ||
632 | 63 | True | ||
633 | 64 | >>> spec_b in spec_c.all_blocked | ||
634 | 65 | True | ||
635 | 66 | >>> vocab = vocabulary_registry.get( | ||
636 | 67 | ... spec_c, "SpecificationDepCandidates") | ||
637 | 68 | >>> spec_a in [term.value for term in vocab] | ||
638 | 69 | False | ||
639 | 70 | |||
640 | 71 | This vocabulary provides the IHugeVocabulary interface. | ||
641 | 72 | |||
642 | 73 | >>> from canonical.launchpad.webapp.testing import verifyObject | ||
643 | 74 | >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary | ||
644 | 75 | >>> verifyObject(IHugeVocabulary, vocab) | ||
645 | 76 | True | ||
646 | 77 | |||
647 | 78 | The search() method returns specifications within the vocabulary that | ||
648 | 79 | matches the search string. The string is matched against the name, or | ||
649 | 80 | fallbacks to a full text search. | ||
650 | 81 | |||
651 | 82 | >>> from zope.security.proxy import removeSecurityProxy | ||
652 | 83 | >>> naked_vocab = removeSecurityProxy( | ||
653 | 84 | ... vocabulary_registry.get( | ||
654 | 85 | ... spec_a, "SpecificationDepCandidates")) | ||
655 | 86 | >>> list(naked_vocab.search('spec-b')) == [spec_b] | ||
656 | 87 | True | ||
657 | 88 | >>> list(naked_vocab.search('third')) == [spec_c] | ||
658 | 89 | True | ||
659 | 90 | |||
660 | 91 | The search method uses the SQL `LIKE` operator, with the values quoted | ||
661 | 92 | appropriately. Queries conataining regual expression operators, for | ||
662 | 93 | example, will simply look for the respective characters within the | ||
663 | 94 | vocabulary's item (this used to be the cause of an OOPS, see | ||
664 | 95 | https://bugs.edge.launchpad.net/blueprint/+bug/139385 for more | ||
665 | 96 | details). | ||
666 | 97 | |||
667 | 98 | >>> list(naked_vocab.search('*')) | ||
668 | 99 | [] | ||
669 | 0 | 100 | ||
670 | === added file 'lib/lp/blueprints/vocabularies/tests/test_doc.py' | |||
671 | --- lib/lp/blueprints/vocabularies/tests/test_doc.py 1970-01-01 00:00:00 +0000 | |||
672 | +++ lib/lp/blueprints/vocabularies/tests/test_doc.py 2010-08-25 21:37:45 +0000 | |||
673 | @@ -0,0 +1,17 @@ | |||
674 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
675 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
676 | 3 | |||
677 | 4 | """ | ||
678 | 5 | Run the doctests. | ||
679 | 6 | """ | ||
680 | 7 | |||
681 | 8 | import os | ||
682 | 9 | |||
683 | 10 | from lp.services.testing import build_doctest_suite | ||
684 | 11 | |||
685 | 12 | |||
686 | 13 | here = os.path.dirname(os.path.realpath(__file__)) | ||
687 | 14 | |||
688 | 15 | |||
689 | 16 | def test_suite(): | ||
690 | 17 | return build_doctest_suite(here, '') | ||
691 | 0 | 18 | ||
692 | === modified file 'lib/lp/testing/factory.py' | |||
693 | --- lib/lp/testing/factory.py 2010-08-22 18:31:30 +0000 | |||
694 | +++ lib/lp/testing/factory.py 2010-08-25 21:37:45 +0000 | |||
695 | @@ -439,7 +439,6 @@ | |||
696 | 439 | registry_team.addMember(user, registry_team.teamowner) | 439 | registry_team.addMember(user, registry_team.teamowner) |
697 | 440 | return user | 440 | return user |
698 | 441 | 441 | ||
699 | 442 | |||
700 | 443 | def makeCopyArchiveLocation(self, distribution=None, owner=None, | 442 | def makeCopyArchiveLocation(self, distribution=None, owner=None, |
701 | 444 | name=None, enabled=True): | 443 | name=None, enabled=True): |
702 | 445 | """Create and return a new arbitrary location for copy packages.""" | 444 | """Create and return a new arbitrary location for copy packages.""" |
703 | @@ -1495,7 +1494,9 @@ | |||
704 | 1495 | mail.parsed_string = mail.as_string() | 1494 | mail.parsed_string = mail.as_string() |
705 | 1496 | return mail | 1495 | return mail |
706 | 1497 | 1496 | ||
708 | 1498 | def makeSpecification(self, product=None, title=None, distribution=None): | 1497 | def makeSpecification(self, product=None, title=None, distribution=None, |
709 | 1498 | name=None, summary=None, | ||
710 | 1499 | status=SpecificationDefinitionStatus.NEW): | ||
711 | 1499 | """Create and return a new, arbitrary Blueprint. | 1500 | """Create and return a new, arbitrary Blueprint. |
712 | 1500 | 1501 | ||
713 | 1501 | :param product: The product to make the blueprint on. If one is | 1502 | :param product: The product to make the blueprint on. If one is |
714 | @@ -1503,14 +1504,18 @@ | |||
715 | 1503 | """ | 1504 | """ |
716 | 1504 | if distribution is None and product is None: | 1505 | if distribution is None and product is None: |
717 | 1505 | product = self.makeProduct() | 1506 | product = self.makeProduct() |
718 | 1507 | if name is None: | ||
719 | 1508 | name = self.getUniqueString('name') | ||
720 | 1509 | if summary is None: | ||
721 | 1510 | summary = self.getUniqueString('summary') | ||
722 | 1506 | if title is None: | 1511 | if title is None: |
723 | 1507 | title = self.getUniqueString('title') | 1512 | title = self.getUniqueString('title') |
724 | 1508 | return getUtility(ISpecificationSet).new( | 1513 | return getUtility(ISpecificationSet).new( |
726 | 1509 | name=self.getUniqueString('name'), | 1514 | name=name, |
727 | 1510 | title=title, | 1515 | title=title, |
728 | 1511 | specurl=None, | 1516 | specurl=None, |
731 | 1512 | summary=self.getUniqueString('summary'), | 1517 | summary=summary, |
732 | 1513 | definition_status=SpecificationDefinitionStatus.NEW, | 1518 | definition_status=status, |
733 | 1514 | owner=self.makePerson(), | 1519 | owner=self.makePerson(), |
734 | 1515 | product=product, | 1520 | product=product, |
735 | 1516 | distribution=distribution) | 1521 | distribution=distribution) |
Thanks for making the changes. (FTR: Discussed on IRC, see revision log). There were a few formalities missing with the code you moved, but no need to fix them all here.
One more nit: s/canidates/ candidates/ g
Jeroen