Merge lp:~wgrant/launchpad/xref-buglinks into lp:launchpad
- xref-buglinks
- Merge into devel
Proposed by
William Grant
Status: | Merged |
---|---|
Merged at revision: | 17782 |
Proposed branch: | lp:~wgrant/launchpad/xref-buglinks |
Merge into: | lp:launchpad |
Prerequisite: | lp:~wgrant/launchpad/xref-model |
Diff against target: |
1243 lines (+555/-92) 20 files modified
database/schema/security.cfg (+8/-0) lib/lp/answers/browser/tests/views.txt (+2/-2) lib/lp/answers/model/question.py (+51/-14) lib/lp/blueprints/model/specification.py (+32/-6) lib/lp/blueprints/tests/test_specification.py (+17/-0) lib/lp/bugs/browser/tests/test_bugtask.py (+5/-5) lib/lp/bugs/doc/cve.txt (+3/-3) lib/lp/bugs/model/bug.py (+57/-16) lib/lp/bugs/model/bugcve.py (+3/-0) lib/lp/bugs/model/bugtask.py (+11/-3) lib/lp/bugs/model/bugtasksearch.py (+34/-9) lib/lp/bugs/model/cve.py (+67/-34) lib/lp/bugs/model/tests/test_bugtask.py (+16/-0) lib/lp/bugs/model/tests/test_bugtasksearch.py (+21/-0) lib/lp/bugs/tests/test_cve.py (+53/-0) lib/lp/coop/answersbugs/model.py (+3/-0) lib/lp/coop/answersbugs/tests/test_questionbug.py (+17/-0) lib/lp/scripts/garbo.py (+63/-0) lib/lp/scripts/tests/test_garbo.py (+88/-0) lib/lp/services/xref/model.py (+4/-0) |
To merge this branch: | bzr merge lp:~wgrant/launchpad/xref-buglinks |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+272591@code.launchpad.net |
Commit message
BugCve/
Description of the change
BugCve/
There's also a garbo job to backfill XRef, and a test-only feature flag to disable writing to the old schema.
There are a couple of XXXs about filling in XRef.creator, but the old schema doesn't store that data, and this branch is big enough as is. creator comes later.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
William Grant (wgrant) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2015-09-18 01:31:50 +0000 |
3 | +++ database/schema/security.cfg 2015-10-01 01:43:11 +0000 |
4 | @@ -654,6 +654,7 @@ |
5 | public.validpersoncache = SELECT |
6 | public.validpersonorteamcache = SELECT |
7 | public.wikiname = SELECT, INSERT |
8 | +public.xref = SELECT, INSERT |
9 | type=user |
10 | |
11 | [branchscanner] |
12 | @@ -945,6 +946,7 @@ |
13 | public.translationgroup = SELECT |
14 | public.validpersoncache = SELECT |
15 | public.validpersonorteamcache = SELECT |
16 | +public.xref = SELECT, INSERT |
17 | type=user |
18 | |
19 | [fiera] |
20 | @@ -1295,6 +1297,7 @@ |
21 | public.teamparticipation = SELECT |
22 | public.validpersoncache = SELECT |
23 | public.validpersonorteamcache = SELECT |
24 | +public.xref = SELECT |
25 | type=user |
26 | |
27 | [expire_questions] |
28 | @@ -1436,6 +1439,7 @@ |
29 | public.validpersoncache = SELECT |
30 | public.validpersonorteamcache = SELECT |
31 | public.wikiname = SELECT, INSERT |
32 | +public.xref = SELECT, INSERT |
33 | type=group |
34 | |
35 | [queued] |
36 | @@ -1545,6 +1549,7 @@ |
37 | public.teamparticipation = SELECT, INSERT |
38 | public.validpersoncache = SELECT |
39 | public.validpersonorteamcache = SELECT |
40 | +public.xref = SELECT, INSERT |
41 | type=user |
42 | |
43 | [process_accepted] |
44 | @@ -1625,6 +1630,7 @@ |
45 | public.teamparticipation = SELECT |
46 | public.validpersoncache = SELECT |
47 | public.validpersonorteamcache = SELECT |
48 | +public.xref = SELECT |
49 | type=user |
50 | |
51 | [personnotification] |
52 | @@ -1835,6 +1841,7 @@ |
53 | public.teamparticipation = SELECT |
54 | public.validpersoncache = SELECT |
55 | public.validpersonorteamcache = SELECT |
56 | +public.xref = SELECT, INSERT |
57 | type=user |
58 | |
59 | [mlist-sync] |
60 | @@ -2337,6 +2344,7 @@ |
61 | public.translationmessage = SELECT, DELETE |
62 | public.translationtemplateitem = SELECT, DELETE |
63 | public.webhookjob = SELECT, DELETE |
64 | +public.xref = SELECT, INSERT |
65 | type=user |
66 | |
67 | [garbo_daily] |
68 | |
69 | === modified file 'lib/lp/answers/browser/tests/views.txt' |
70 | --- lib/lp/answers/browser/tests/views.txt 2014-04-24 02:53:05 +0000 |
71 | +++ lib/lp/answers/browser/tests/views.txt 2015-10-01 01:43:11 +0000 |
72 | @@ -334,8 +334,8 @@ |
73 | ... 'field.description': 'Bug description.'}) |
74 | >>> request.method = 'POST' |
75 | >>> makebug = getMultiAdapter((question_three, request), name='+makebug') |
76 | - >>> question_three.bugs.count() == 0 |
77 | - True |
78 | + >>> question_three.bugs |
79 | + [] |
80 | |
81 | >>> makebug.initialize() |
82 | >>> print question_three.bugs[0].title |
83 | |
84 | === modified file 'lib/lp/answers/model/question.py' |
85 | --- lib/lp/answers/model/question.py 2015-09-29 05:02:28 +0000 |
86 | +++ lib/lp/answers/model/question.py 2015-10-01 01:43:11 +0000 |
87 | @@ -94,12 +94,14 @@ |
88 | IProductSet, |
89 | ) |
90 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet |
91 | +from lp.services.database import bulk |
92 | from lp.services.database.constants import ( |
93 | DEFAULT, |
94 | UTC_NOW, |
95 | ) |
96 | from lp.services.database.datetimecol import UtcDateTimeCol |
97 | from lp.services.database.enumcol import EnumCol |
98 | +from lp.services.database.interfaces import IStore |
99 | from lp.services.database.nl_search import nl_phrase_search |
100 | from lp.services.database.sqlbase import ( |
101 | cursor, |
102 | @@ -108,6 +110,7 @@ |
103 | sqlvalues, |
104 | ) |
105 | from lp.services.database.stormexpr import rank_by_fti |
106 | +from lp.services.features import getFeatureFlag |
107 | from lp.services.mail.notificationrecipientset import NotificationRecipientSet |
108 | from lp.services.messages.interfaces.message import IMessage |
109 | from lp.services.messages.model.message import ( |
110 | @@ -119,6 +122,7 @@ |
111 | from lp.services.worlddata.helpers import is_english_variant |
112 | from lp.services.worlddata.interfaces.language import ILanguage |
113 | from lp.services.worlddata.model.language import Language |
114 | +from lp.services.xref.interfaces import IXRefSet |
115 | |
116 | |
117 | class notify_question_modified: |
118 | @@ -210,8 +214,6 @@ |
119 | subscribers = SQLRelatedJoin('Person', |
120 | joinColumn='question', otherColumn='person', |
121 | intermediateTable='QuestionSubscription', orderBy='name') |
122 | - bugs = SQLRelatedJoin('Bug', joinColumn='question', otherColumn='bug', |
123 | - intermediateTable='QuestionBug', orderBy='id') |
124 | messages = SQLMultipleJoin('QuestionMessage', joinColumn='question', |
125 | prejoins=['message'], orderBy=['QuestionMessage.id']) |
126 | reopenings = SQLMultipleJoin('QuestionReopening', orderBy='datecreated', |
127 | @@ -660,14 +662,35 @@ |
128 | self.status = new_status |
129 | return tktmsg |
130 | |
131 | + @property |
132 | + def bugs(self): |
133 | + from lp.bugs.model.bug import Bug |
134 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
135 | + bug_ids = [ |
136 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
137 | + (u'question', unicode(self.id)), types=[u'bug'])] |
138 | + else: |
139 | + bug_ids = list(IStore(QuestionBug).find( |
140 | + QuestionBug, |
141 | + QuestionBug.question == self).values(QuestionBug.bugID)) |
142 | + return list(sorted( |
143 | + bulk.load(Bug, bug_ids), key=operator.attrgetter('id'))) |
144 | + |
145 | # IBugLinkTarget implementation |
146 | def createBugLink(self, bug): |
147 | """See BugLinkTargetMixin.""" |
148 | - QuestionBug(question=self, bug=bug) |
149 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
150 | + QuestionBug(question=self, bug=bug) |
151 | + # XXX: Should set creator. |
152 | + getUtility(IXRefSet).create( |
153 | + {(u'question', unicode(self.id)): {(u'bug', unicode(bug.id)): {}}}) |
154 | |
155 | def deleteBugLink(self, bug): |
156 | """See BugLinkTargetMixin.""" |
157 | - Store.of(self).find(QuestionBug, question=self, bug=bug).remove() |
158 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
159 | + Store.of(self).find(QuestionBug, question=self, bug=bug).remove() |
160 | + getUtility(IXRefSet).delete( |
161 | + {(u'question', unicode(self.id)): [(u'bug', unicode(bug.id))]}) |
162 | |
163 | def setCommentVisibility(self, user, comment_number, visible): |
164 | """See `IQuestion`.""" |
165 | @@ -693,24 +716,38 @@ |
166 | # This query joins to bugtasks that are not BugTaskStatus.INVALID |
167 | # because there are many bugtasks to one question. A question is |
168 | # included when BugTask.status IS NULL. |
169 | - return Question.select(""" |
170 | - id in (SELECT Question.id |
171 | - FROM Question |
172 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
173 | + bugtask_join = """ |
174 | + LEFT OUTER JOIN XRef ON ( |
175 | + XRef.from_type = 'question' |
176 | + AND XRef.from_id_int = Question.id |
177 | + AND XRef.to_type = 'bug') |
178 | + LEFT OUTER JOIN BugTask ON ( |
179 | + BugTask.bug = XRef.to_id_int |
180 | + AND BugTask.status != %s) |
181 | + """ |
182 | + else: |
183 | + bugtask_join = """ |
184 | LEFT OUTER JOIN QuestionBug |
185 | ON Question.id = QuestionBug.question |
186 | - LEFT OUTER JOIN BugTask |
187 | - ON QuestionBug.bug = BugTask.bug |
188 | - AND BugTask.status != %s |
189 | + LEFT OUTER JOIN BugTask ON ( |
190 | + BugTask.bug = QuestionBug.bug |
191 | + AND BugTask.status != %s) |
192 | + """ |
193 | + return Question.select((""" |
194 | + id in (SELECT Question.id |
195 | + FROM Question |
196 | + %s |
197 | WHERE |
198 | - Question.status IN (%s, %s) |
199 | + Question.status IN (%%s, %%s) |
200 | AND (Question.datelastresponse IS NULL |
201 | OR Question.datelastresponse < (CURRENT_TIMESTAMP |
202 | - AT TIME ZONE 'UTC' - interval '%s days')) |
203 | + AT TIME ZONE 'UTC' - interval '%%s days')) |
204 | AND Question.datelastquery < (CURRENT_TIMESTAMP |
205 | - AT TIME ZONE 'UTC' - interval '%s days') |
206 | + AT TIME ZONE 'UTC' - interval '%%s days') |
207 | AND Question.assignee IS NULL |
208 | AND BugTask.status IS NULL) |
209 | - """ % sqlvalues( |
210 | + """ % bugtask_join) % sqlvalues( |
211 | BugTaskStatus.INVALID, |
212 | QuestionStatus.OPEN, QuestionStatus.NEEDSINFO, |
213 | days_before_expiration, days_before_expiration)) |
214 | |
215 | === modified file 'lib/lp/blueprints/model/specification.py' |
216 | --- lib/lp/blueprints/model/specification.py 2015-09-28 07:57:17 +0000 |
217 | +++ lib/lp/blueprints/model/specification.py 2015-10-01 01:43:11 +0000 |
218 | @@ -11,6 +11,8 @@ |
219 | 'SpecificationSet', |
220 | ] |
221 | |
222 | +import operator |
223 | + |
224 | from lazr.lifecycle.event import ( |
225 | ObjectCreatedEvent, |
226 | ObjectModifiedEvent, |
227 | @@ -86,6 +88,7 @@ |
228 | from lp.registry.interfaces.person import validate_public_person |
229 | from lp.registry.interfaces.product import IProduct |
230 | from lp.registry.interfaces.productseries import IProductSeries |
231 | +from lp.services.database import bulk |
232 | from lp.services.database.constants import ( |
233 | DEFAULT, |
234 | UTC_NOW, |
235 | @@ -99,12 +102,14 @@ |
236 | SQLBase, |
237 | sqlvalues, |
238 | ) |
239 | +from lp.services.features import getFeatureFlag |
240 | from lp.services.mail.helpers import get_contact_email_addresses |
241 | from lp.services.propertycache import ( |
242 | cachedproperty, |
243 | get_property_cache, |
244 | ) |
245 | from lp.services.webapp.interfaces import ILaunchBag |
246 | +from lp.services.xref.interfaces import IXRefSet |
247 | |
248 | |
249 | def recursive_blocked_query(user): |
250 | @@ -239,9 +244,6 @@ |
251 | sprints = SQLRelatedJoin('Sprint', orderBy='name', |
252 | joinColumn='specification', otherColumn='sprint', |
253 | intermediateTable='SprintSpecification') |
254 | - bugs = SQLRelatedJoin('Bug', |
255 | - joinColumn='specification', otherColumn='bug', |
256 | - intermediateTable='SpecificationBug', orderBy='id') |
257 | spec_dependency_links = SQLMultipleJoin('SpecificationDependency', |
258 | joinColumn='specification', orderBy='id') |
259 | |
260 | @@ -791,14 +793,38 @@ |
261 | |
262 | return bool(self.subscription(person)) |
263 | |
264 | + @property |
265 | + def bugs(self): |
266 | + from lp.bugs.model.bug import Bug |
267 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
268 | + bug_ids = [ |
269 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
270 | + (u'specification', unicode(self.id)), types=[u'bug'])] |
271 | + else: |
272 | + bug_ids = list(IStore(SpecificationBug).find( |
273 | + SpecificationBug, |
274 | + SpecificationBug.specification == self).values( |
275 | + SpecificationBug.bugID)) |
276 | + return list(sorted( |
277 | + bulk.load(Bug, bug_ids), key=operator.attrgetter('id'))) |
278 | + |
279 | def createBugLink(self, bug): |
280 | """See BugLinkTargetMixin.""" |
281 | - SpecificationBug(specification=self, bug=bug) |
282 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
283 | + SpecificationBug(specification=self, bug=bug) |
284 | + # XXX: Should set creator. |
285 | + getUtility(IXRefSet).create( |
286 | + {(u'specification', unicode(self.id)): |
287 | + {(u'bug', unicode(bug.id)): {}}}) |
288 | |
289 | def deleteBugLink(self, bug): |
290 | """See BugLinkTargetMixin.""" |
291 | - Store.of(self).find( |
292 | - SpecificationBug, specification=self, bug=bug).remove() |
293 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
294 | + Store.of(self).find( |
295 | + SpecificationBug, specification=self, bug=bug).remove() |
296 | + getUtility(IXRefSet).delete( |
297 | + {(u'specification', unicode(self.id)): |
298 | + [(u'bug', unicode(bug.id))]}) |
299 | |
300 | # sprint linking |
301 | def linkSprint(self, sprint, user): |
302 | |
303 | === modified file 'lib/lp/blueprints/tests/test_specification.py' |
304 | --- lib/lp/blueprints/tests/test_specification.py 2015-09-28 07:39:28 +0000 |
305 | +++ lib/lp/blueprints/tests/test_specification.py 2015-10-01 01:43:11 +0000 |
306 | @@ -59,6 +59,7 @@ |
307 | EditSpecificationByRelatedPeople, |
308 | ViewSpecification, |
309 | ) |
310 | +from lp.services.features.testing import FeatureFixture |
311 | from lp.services.propertycache import get_property_cache |
312 | from lp.services.webapp.authorization import check_permission |
313 | from lp.services.webapp.interaction import ANONYMOUS |
314 | @@ -876,3 +877,19 @@ |
315 | self.assertContentEqual([bug1], spec2.bugs) |
316 | self.assertContentEqual([spec2], bug1.specifications) |
317 | self.assertContentEqual([], bug2.specifications) |
318 | + |
319 | + |
320 | +class TestBugLinksWithXRef(TestBugLinks): |
321 | + |
322 | + def setUp(self): |
323 | + super(TestBugLinksWithXRef, self).setUp() |
324 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
325 | + |
326 | + |
327 | +class TestBugLinksWithXRefAndNoOld(TestBugLinks): |
328 | + |
329 | + def setUp(self): |
330 | + super(TestBugLinksWithXRefAndNoOld, self).setUp() |
331 | + self.useFixture(FeatureFixture({ |
332 | + 'bugs.xref_buglinks.query': 'true', |
333 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
334 | |
335 | === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py' |
336 | --- lib/lp/bugs/browser/tests/test_bugtask.py 2015-06-30 01:10:06 +0000 |
337 | +++ lib/lp/bugs/browser/tests/test_bugtask.py 2015-10-01 01:43:11 +0000 |
338 | @@ -125,7 +125,7 @@ |
339 | 0, 10, login_method=lambda: login(ADMIN_EMAIL)) |
340 | # This may seem large: it is; there is easily another 25% fat in |
341 | # there. |
342 | - self.assertThat(recorder1, HasQueryCount(LessThan(81))) |
343 | + self.assertThat(recorder1, HasQueryCount(LessThan(83))) |
344 | self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count))) |
345 | |
346 | def test_rendered_query_counts_constant_with_attachments(self): |
347 | @@ -136,7 +136,7 @@ |
348 | lambda: self.getUserBrowser(url, person), |
349 | lambda: self.factory.makeBugAttachment(bug=task.bug), |
350 | 1, 9, login_method=lambda: login(ADMIN_EMAIL)) |
351 | - self.assertThat(recorder1, HasQueryCount(LessThan(82))) |
352 | + self.assertThat(recorder1, HasQueryCount(LessThan(84))) |
353 | self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count))) |
354 | |
355 | def makeLinkedBranchMergeProposal(self, sourcepackage, bug, owner): |
356 | @@ -171,7 +171,7 @@ |
357 | recorder1, recorder2 = record_two_runs( |
358 | lambda: self.getUserBrowser(url, owner), |
359 | make_merge_proposals, 0, 1) |
360 | - self.assertThat(recorder1, HasQueryCount(LessThan(87))) |
361 | + self.assertThat(recorder1, HasQueryCount(LessThan(89))) |
362 | # Ideally this should be much fewer, but this tries to keep a win of |
363 | # removing more than half of these. |
364 | self.assertThat( |
365 | @@ -217,7 +217,7 @@ |
366 | lambda: self.getUserBrowser(url, person), |
367 | lambda: add_activity("description", self.factory.makePerson()), |
368 | 1, 20, login_method=lambda: login(ADMIN_EMAIL)) |
369 | - self.assertThat(recorder1, HasQueryCount(LessThan(82))) |
370 | + self.assertThat(recorder1, HasQueryCount(LessThan(84))) |
371 | self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count))) |
372 | |
373 | def test_rendered_query_counts_constant_with_milestones(self): |
374 | @@ -227,7 +227,7 @@ |
375 | |
376 | with celebrity_logged_in('admin'): |
377 | browses_under_limit = BrowsesWithQueryLimit( |
378 | - 82, self.factory.makePerson()) |
379 | + 84, self.factory.makePerson()) |
380 | |
381 | self.assertThat(bug, browses_under_limit) |
382 | |
383 | |
384 | === modified file 'lib/lp/bugs/doc/cve.txt' |
385 | --- lib/lp/bugs/doc/cve.txt 2015-09-25 09:48:57 +0000 |
386 | +++ lib/lp/bugs/doc/cve.txt 2015-10-01 01:43:11 +0000 |
387 | @@ -75,16 +75,16 @@ |
388 | |
389 | Let's add the new CVE: |
390 | |
391 | - >>> b.cves.count() |
392 | + >>> len(b.cves) |
393 | 1 |
394 | >>> b.linkCVE(cve, no_priv) |
395 | - >>> b.cves.count() |
396 | + >>> len(b.cves) |
397 | 2 |
398 | |
399 | Ah, but that was a bad idea. Let's unlink it. |
400 | |
401 | >>> b.unlinkCVE(cve, user=no_priv) |
402 | - >>> b.cves.count() |
403 | + >>> len(b.cves) |
404 | 1 |
405 | |
406 | Alternatively, we can link CVEs to bugs by looking for CVEs in a |
407 | |
408 | === modified file 'lib/lp/bugs/model/bug.py' |
409 | --- lib/lp/bugs/model/bug.py 2015-09-30 01:51:52 +0000 |
410 | +++ lib/lp/bugs/model/bug.py 2015-10-01 01:43:11 +0000 |
411 | @@ -99,11 +99,6 @@ |
412 | from lp.app.interfaces.services import IService |
413 | from lp.app.model.launchpad import InformationTypeMixin |
414 | from lp.app.validators import LaunchpadValidationError |
415 | -from lp.blueprints.model.specification import Specification |
416 | -from lp.blueprints.model.specificationbug import SpecificationBug |
417 | -from lp.blueprints.model.specificationsearch import ( |
418 | - get_specification_privacy_filter, |
419 | - ) |
420 | from lp.bugs.adapters.bug import convert_to_information_type |
421 | from lp.bugs.adapters.bugchange import ( |
422 | BranchLinkedToBug, |
423 | @@ -198,6 +193,7 @@ |
424 | from lp.registry.model.pillar import pillar_sort_key |
425 | from lp.registry.model.teammembership import TeamParticipation |
426 | from lp.services.config import config |
427 | +from lp.services.database import bulk |
428 | from lp.services.database.constants import UTC_NOW |
429 | from lp.services.database.datetimecol import UtcDateTimeCol |
430 | from lp.services.database.decoratedresultset import DecoratedResultSet |
431 | @@ -208,6 +204,7 @@ |
432 | sqlvalues, |
433 | ) |
434 | from lp.services.database.stormbase import StormBase |
435 | +from lp.services.features import getFeatureFlag |
436 | from lp.services.fields import DuplicateBug |
437 | from lp.services.helpers import shortlist |
438 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
439 | @@ -234,6 +231,7 @@ |
440 | from lp.services.webapp.publisher import ( |
441 | get_raw_form_value_from_current_request, |
442 | ) |
443 | +from lp.services.xref.interfaces import IXRefSet |
444 | |
445 | |
446 | def snapshot_bug_params(bug_params): |
447 | @@ -363,15 +361,7 @@ |
448 | 'BugMessage', joinColumn='bug', orderBy='index') |
449 | watches = SQLMultipleJoin( |
450 | 'BugWatch', joinColumn='bug', orderBy=['bugtracker', 'remotebug']) |
451 | - cves = SQLRelatedJoin('Cve', intermediateTable='BugCve', |
452 | - orderBy='sequence', joinColumn='bug', otherColumn='cve') |
453 | duplicates = SQLMultipleJoin('Bug', joinColumn='duplicateof', orderBy='id') |
454 | - specifications = SQLRelatedJoin( |
455 | - 'Specification', joinColumn='bug', otherColumn='specification', |
456 | - intermediateTable='SpecificationBug', orderBy='-datecreated') |
457 | - questions = SQLRelatedJoin('Question', joinColumn='bug', |
458 | - otherColumn='question', intermediateTable='QuestionBug', |
459 | - orderBy='-datecreated') |
460 | linked_branches = SQLMultipleJoin( |
461 | 'BugBranch', joinColumn='bug', orderBy='id') |
462 | date_last_message = UtcDateTimeCol(default=None) |
463 | @@ -383,12 +373,63 @@ |
464 | heat_last_updated = UtcDateTimeCol(default=None) |
465 | latest_patch_uploaded = UtcDateTimeCol(default=None) |
466 | |
467 | + @property |
468 | + def cves(self): |
469 | + from lp.bugs.model.bugcve import BugCve |
470 | + from lp.bugs.model.cve import Cve |
471 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
472 | + xref_cve_sequences = [ |
473 | + sequence for _, sequence in getUtility(IXRefSet).findFrom( |
474 | + (u'bug', unicode(self.id)), types=[u'cve'])] |
475 | + expr = Cve.sequence.is_in(xref_cve_sequences) |
476 | + else: |
477 | + old_cve_ids = list(IStore(BugCve).find( |
478 | + BugCve, |
479 | + BugCve.bug == self).values(BugCve.cveID)) |
480 | + expr = Cve.id.is_in(old_cve_ids) |
481 | + return list(sorted( |
482 | + IStore(Cve).find(Cve, expr), key=operator.attrgetter('sequence'))) |
483 | + |
484 | + @property |
485 | + def questions(self): |
486 | + from lp.answers.model.question import Question |
487 | + from lp.coop.answersbugs.model import QuestionBug |
488 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
489 | + question_ids = [ |
490 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
491 | + (u'bug', unicode(self.id)), types=[u'question'])] |
492 | + else: |
493 | + question_ids = list(IStore(QuestionBug).find( |
494 | + QuestionBug, |
495 | + QuestionBug.bug == self).values(QuestionBug.questionID)) |
496 | + return list(sorted( |
497 | + bulk.load(Question, question_ids), key=operator.attrgetter('id'))) |
498 | + |
499 | + @property |
500 | + def specifications(self): |
501 | + from lp.blueprints.model.specification import Specification |
502 | + from lp.blueprints.model.specificationbug import SpecificationBug |
503 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
504 | + spec_ids = [ |
505 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
506 | + (u'bug', unicode(self.id)), types=[u'specification'])] |
507 | + else: |
508 | + spec_ids = list(IStore(SpecificationBug).find( |
509 | + SpecificationBug, |
510 | + SpecificationBug.bug == self).values( |
511 | + SpecificationBug.specificationID)) |
512 | + return list(sorted( |
513 | + bulk.load(Specification, spec_ids), key=operator.attrgetter('id'))) |
514 | + |
515 | def getSpecifications(self, user): |
516 | """See `IBug`.""" |
517 | - return IStore(SpecificationBug).find( |
518 | + from lp.blueprints.model.specification import Specification |
519 | + from lp.blueprints.model.specificationsearch import ( |
520 | + get_specification_privacy_filter, |
521 | + ) |
522 | + return IStore(Specification).find( |
523 | Specification, |
524 | - SpecificationBug.bugID == self.id, |
525 | - SpecificationBug.specificationID == Specification.id, |
526 | + Specification.id.is_in(spec.id for spec in self.specifications), |
527 | *get_specification_privacy_filter(user)) |
528 | |
529 | @property |
530 | |
531 | === modified file 'lib/lp/bugs/model/bugcve.py' |
532 | --- lib/lp/bugs/model/bugcve.py 2015-09-25 10:15:37 +0000 |
533 | +++ lib/lp/bugs/model/bugcve.py 2015-10-01 01:43:11 +0000 |
534 | @@ -6,6 +6,8 @@ |
535 | |
536 | from sqlobject import ForeignKey |
537 | |
538 | +from lp.services.database.constants import UTC_NOW |
539 | +from lp.services.database.datetimecol import UtcDateTimeCol |
540 | from lp.services.database.sqlbase import SQLBase |
541 | |
542 | |
543 | @@ -17,3 +19,4 @@ |
544 | # db field names |
545 | bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True) |
546 | cve = ForeignKey(dbName='cve', foreignKey='Cve', notNull=True) |
547 | + date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) |
548 | |
549 | === modified file 'lib/lp/bugs/model/bugtask.py' |
550 | --- lib/lp/bugs/model/bugtask.py 2015-07-08 16:05:11 +0000 |
551 | +++ lib/lp/bugs/model/bugtask.py 2015-10-01 01:43:11 +0000 |
552 | @@ -139,10 +139,12 @@ |
553 | SQLBase, |
554 | sqlvalues, |
555 | ) |
556 | +from lp.services.features import getFeatureFlag |
557 | from lp.services.helpers import shortlist |
558 | from lp.services.propertycache import get_property_cache |
559 | from lp.services.searchbuilder import any |
560 | from lp.services.webapp.interfaces import ILaunchBag |
561 | +from lp.services.xref.interfaces import IXRefSet |
562 | |
563 | |
564 | def bugtask_sort_key(bugtask): |
565 | @@ -1388,9 +1390,15 @@ |
566 | from lp.bugs.model.bugbranch import BugBranch |
567 | |
568 | bug_ids = set(bugtask.bugID for bugtask in bugtasks) |
569 | - bug_ids_with_specifications = set(IStore(SpecificationBug).find( |
570 | - SpecificationBug.bugID, |
571 | - SpecificationBug.bugID.is_in(bug_ids))) |
572 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
573 | + bug_ids_with_specifications = set( |
574 | + int(id) for _, id in getUtility(IXRefSet).findFromMany( |
575 | + [(u'bug', unicode(bug_id)) for bug_id in bug_ids], |
576 | + types=[u'specification']).keys()) |
577 | + else: |
578 | + bug_ids_with_specifications = set(IStore(SpecificationBug).find( |
579 | + SpecificationBug.bugID, |
580 | + SpecificationBug.bugID.is_in(bug_ids))) |
581 | bug_ids_with_branches = set(IStore(BugBranch).find( |
582 | BugBranch.bugID, BugBranch.bugID.is_in(bug_ids))) |
583 | # Badging looks up milestones too : eager load into the storm cache. |
584 | |
585 | === modified file 'lib/lp/bugs/model/bugtasksearch.py' |
586 | --- lib/lp/bugs/model/bugtasksearch.py 2015-09-28 12:33:22 +0000 |
587 | +++ lib/lp/bugs/model/bugtasksearch.py 2015-10-01 01:43:11 +0000 |
588 | @@ -95,6 +95,7 @@ |
589 | rank_by_fti, |
590 | Unnest, |
591 | ) |
592 | +from lp.services.features import getFeatureFlag |
593 | from lp.services.propertycache import get_property_cache |
594 | from lp.services.searchbuilder import ( |
595 | all, |
596 | @@ -103,6 +104,7 @@ |
597 | not_equals, |
598 | NULL, |
599 | ) |
600 | +from lp.services.xref.model import XRef |
601 | from lp.soyuz.enums import PackagePublishingStatus |
602 | from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
603 | |
604 | @@ -421,9 +423,18 @@ |
605 | BugTaskFlat.productseries == None)) |
606 | |
607 | if params.has_cve: |
608 | - extra_clauses.append( |
609 | - BugTaskFlat.bug_id.is_in( |
610 | - Select(BugCve.bugID, tables=[BugCve]))) |
611 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
612 | + where = [ |
613 | + XRef.from_type == u'bug', |
614 | + XRef.from_id_int == BugTaskFlat.bug_id, |
615 | + XRef.to_type == u'cve', |
616 | + ] |
617 | + extra_clauses.append(Exists(Select( |
618 | + 1, tables=[XRef], where=And(*where)))) |
619 | + else: |
620 | + extra_clauses.append( |
621 | + BugTaskFlat.bug_id.is_in( |
622 | + Select(BugCve.bugID, tables=[BugCve]))) |
623 | |
624 | if params.attachmenttype is not None: |
625 | if params.attachmenttype == BugAttachmentType.PATCH: |
626 | @@ -1013,12 +1024,26 @@ |
627 | linked_blueprints = params.linked_blueprints |
628 | |
629 | def make_clause(blueprints=None): |
630 | - where = [SpecificationBug.bugID == BugTaskFlat.bug_id] |
631 | - if blueprints is not None: |
632 | - where.append( |
633 | - search_value_to_storm_where_condition( |
634 | - SpecificationBug.specificationID, blueprints)) |
635 | - return Exists(Select(1, tables=[SpecificationBug], where=And(*where))) |
636 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
637 | + where = [ |
638 | + XRef.from_type == u'bug', |
639 | + XRef.from_id_int == BugTaskFlat.bug_id, |
640 | + XRef.to_type == u'specification', |
641 | + ] |
642 | + if blueprints is not None: |
643 | + where.append( |
644 | + search_value_to_storm_where_condition( |
645 | + XRef.to_id_int, blueprints)) |
646 | + return Exists(Select( |
647 | + 1, tables=[XRef], where=And(*where))) |
648 | + else: |
649 | + where = [SpecificationBug.bugID == BugTaskFlat.bug_id] |
650 | + if blueprints is not None: |
651 | + where.append( |
652 | + search_value_to_storm_where_condition( |
653 | + SpecificationBug.specificationID, blueprints)) |
654 | + return Exists(Select( |
655 | + 1, tables=[SpecificationBug], where=And(*where))) |
656 | |
657 | if linked_blueprints is None: |
658 | return None |
659 | |
660 | === modified file 'lib/lp/bugs/model/cve.py' |
661 | --- lib/lp/bugs/model/cve.py 2015-09-28 07:57:17 +0000 |
662 | +++ lib/lp/bugs/model/cve.py 2015-10-01 01:43:11 +0000 |
663 | @@ -8,16 +8,16 @@ |
664 | 'CveSet', |
665 | ] |
666 | |
667 | -# SQL imports |
668 | +import operator |
669 | + |
670 | from sqlobject import ( |
671 | SQLMultipleJoin, |
672 | SQLObjectNotFound, |
673 | - SQLRelatedJoin, |
674 | StringCol, |
675 | ) |
676 | from storm.expr import In |
677 | from storm.store import Store |
678 | -# Zope |
679 | +from zope.component import getUtility |
680 | from zope.interface import implementer |
681 | |
682 | from lp.app.validators.cve import ( |
683 | @@ -34,12 +34,16 @@ |
684 | from lp.bugs.model.bugcve import BugCve |
685 | from lp.bugs.model.buglinktarget import BugLinkTargetMixin |
686 | from lp.bugs.model.cvereference import CveReference |
687 | -from lp.services.database.bulk import load_related |
688 | +from lp.services.database import bulk |
689 | from lp.services.database.constants import UTC_NOW |
690 | from lp.services.database.datetimecol import UtcDateTimeCol |
691 | from lp.services.database.enumcol import EnumCol |
692 | +from lp.services.database.interfaces import IStore |
693 | from lp.services.database.sqlbase import SQLBase |
694 | from lp.services.database.stormexpr import fti_search |
695 | +from lp.services.features import getFeatureFlag |
696 | +from lp.services.xref.interfaces import IXRefSet |
697 | +from lp.services.xref.model import XRef |
698 | |
699 | |
700 | @implementer(ICve, IBugLinkTarget) |
701 | @@ -54,10 +58,6 @@ |
702 | datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW) |
703 | datemodified = UtcDateTimeCol(notNull=True, default=UTC_NOW) |
704 | |
705 | - # joins |
706 | - bugs = SQLRelatedJoin('Bug', intermediateTable='BugCve', |
707 | - joinColumn='cve', otherColumn='bug', orderBy='id') |
708 | - bug_links = SQLMultipleJoin('BugCve', joinColumn='cve', orderBy='id') |
709 | references = SQLMultipleJoin( |
710 | 'CveReference', joinColumn='cve', orderBy='id') |
711 | |
712 | @@ -75,6 +75,19 @@ |
713 | def title(self): |
714 | return 'CVE-%s (%s)' % (self.sequence, self.status.title) |
715 | |
716 | + @property |
717 | + def bugs(self): |
718 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
719 | + bug_ids = [ |
720 | + int(id) for _, id in getUtility(IXRefSet).findFrom( |
721 | + (u'cve', self.sequence), types=[u'bug'])] |
722 | + else: |
723 | + bug_ids = list(IStore(BugCve).find( |
724 | + BugCve, |
725 | + BugCve.cve == self).values(BugCve.bugID)) |
726 | + return list(sorted( |
727 | + bulk.load(Bug, bug_ids), key=operator.attrgetter('id'))) |
728 | + |
729 | # CveReference's |
730 | def createReference(self, source, content, url=None): |
731 | """See ICveReference.""" |
732 | @@ -87,11 +100,18 @@ |
733 | |
734 | def createBugLink(self, bug): |
735 | """See BugLinkTargetMixin.""" |
736 | - BugCve(cve=self, bug=bug) |
737 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
738 | + BugCve(cve=self, bug=bug) |
739 | + # XXX: Should set creator. |
740 | + getUtility(IXRefSet).create( |
741 | + {(u'cve', self.sequence): {(u'bug', unicode(bug.id)): {}}}) |
742 | |
743 | def deleteBugLink(self, bug): |
744 | """See BugLinkTargetMixin.""" |
745 | - Store.of(self).find(BugCve, cve=self, bug=bug).remove() |
746 | + if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'): |
747 | + Store.of(self).find(BugCve, cve=self, bug=bug).remove() |
748 | + getUtility(IXRefSet).delete( |
749 | + {(u'cve', self.sequence): [(u'bug', unicode(bug.id))]}) |
750 | |
751 | |
752 | @implementer(ICveSet) |
753 | @@ -177,39 +197,52 @@ |
754 | |
755 | def getBugCvesForBugTasks(self, bugtasks, cve_mapper=None): |
756 | """See ICveSet.""" |
757 | - bugs = load_related(Bug, bugtasks, ('bugID', )) |
758 | + bugs = bulk.load_related(Bug, bugtasks, ('bugID', )) |
759 | if len(bugs) == 0: |
760 | return [] |
761 | - bug_ids = [bug.id for bug in bugs] |
762 | - |
763 | - # Do not use BugCve instances: Storm may need a very long time |
764 | - # to look up the bugs and CVEs referenced by a BugCve instance |
765 | - # when the +cve view of a distroseries is rendered: There may |
766 | - # be a few thousand (bug, CVE) tuples, while the number of bugs |
767 | - # and CVEs is in the order of hundred. It is much more efficient |
768 | - # to retrieve just (bug_id, cve_id) from the BugCve table and |
769 | - # to map this to (Bug, CVE) here, instead of letting Storm |
770 | - # look up the CVE and bug for a BugCve instance, even if bugs |
771 | - # and CVEs are bulk loaded. |
772 | store = Store.of(bugtasks[0]) |
773 | - bugcve_ids = store.find( |
774 | - (BugCve.bugID, BugCve.cveID), In(BugCve.bugID, bug_ids)) |
775 | - bugcve_ids.order_by(BugCve.bugID, BugCve.cveID) |
776 | - bugcve_ids = list(bugcve_ids) |
777 | - |
778 | - cve_ids = set(cve_id for bug_id, cve_id in bugcve_ids) |
779 | - cves = store.find(Cve, In(Cve.id, list(cve_ids))) |
780 | + |
781 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
782 | + xrefs = getUtility(IXRefSet).findFromMany( |
783 | + [(u'bug', unicode(bug.id)) for bug in bugs], types=[u'cve']) |
784 | + bugcve_ids = set() |
785 | + for bug_key in xrefs: |
786 | + for cve_key in xrefs[bug_key]: |
787 | + bugcve_ids.add((int(bug_key[1]), cve_key[1])) |
788 | + else: |
789 | + # Do not use BugCve instances: Storm may need a very long time |
790 | + # to look up the bugs and CVEs referenced by a BugCve instance |
791 | + # when the +cve view of a distroseries is rendered: There may |
792 | + # be a few thousand (bug, CVE) tuples, while the number of bugs |
793 | + # and CVEs is in the order of hundred. It is much more efficient |
794 | + # to retrieve just (Bug.id, Cve.sequence) from the BugCve |
795 | + # table and to map this to (Bug, CVE) here, instead of |
796 | + # letting Storm look up the CVE and bug for a BugCve |
797 | + # instance, even if bugs and CVEs are bulk loaded. |
798 | + bug_ids = [bug.id for bug in bugs] |
799 | + bugcve_ids = store.find( |
800 | + (BugCve.bugID, Cve.sequence), |
801 | + Cve.id == BugCve.cveID, In(BugCve.bugID, bug_ids)) |
802 | + |
803 | + bugcve_ids = list(sorted(bugcve_ids)) |
804 | + |
805 | + cves = store.find( |
806 | + Cve, In(Cve.sequence, [seq for _, seq in bugcve_ids])) |
807 | |
808 | if cve_mapper is None: |
809 | - cvemap = dict((cve.id, cve) for cve in cves) |
810 | + cvemap = dict((cve.sequence, cve) for cve in cves) |
811 | else: |
812 | - cvemap = dict((cve.id, cve_mapper(cve)) for cve in cves) |
813 | + cvemap = dict((cve.sequence, cve_mapper(cve)) for cve in cves) |
814 | bugmap = dict((bug.id, bug) for bug in bugs) |
815 | return [ |
816 | - (bugmap[bug_id], cvemap[cve_id]) |
817 | - for bug_id, cve_id in bugcve_ids |
818 | + (bugmap[bug_id], cvemap[cve_sequence]) |
819 | + for bug_id, cve_sequence in bugcve_ids |
820 | ] |
821 | |
822 | def getBugCveCount(self): |
823 | """See ICveSet.""" |
824 | - return BugCve.select().count() |
825 | + if getFeatureFlag('bugs.xref_buglinks.query'): |
826 | + return IStore(XRef).find( |
827 | + XRef, XRef.from_type == u'bug', XRef.to_type == u'cve').count() |
828 | + else: |
829 | + return BugCve.select().count() |
830 | |
831 | === modified file 'lib/lp/bugs/model/tests/test_bugtask.py' |
832 | --- lib/lp/bugs/model/tests/test_bugtask.py 2015-09-28 17:38:45 +0000 |
833 | +++ lib/lp/bugs/model/tests/test_bugtask.py 2015-10-01 01:43:11 +0000 |
834 | @@ -523,6 +523,22 @@ |
835 | ]) |
836 | |
837 | |
838 | +class TestBugTaskBadgesWithXRef(TestBugTaskBadges): |
839 | + |
840 | + def setUp(self): |
841 | + super(TestBugTaskBadges, self).setUp() |
842 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
843 | + |
844 | + |
845 | +class TestBugTaskBadgesWithXRefAndNoOld(TestBugTaskBadges): |
846 | + |
847 | + def setUp(self): |
848 | + super(TestBugTaskBadges, self).setUp() |
849 | + self.useFixture(FeatureFixture({ |
850 | + 'bugs.xref_buglinks.query': 'true', |
851 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
852 | + |
853 | + |
854 | class TestBugTaskPrivacy(TestCaseWithFactory): |
855 | """Verify that the bug is either private or public. |
856 | |
857 | |
858 | === modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py' |
859 | --- lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-09-28 12:33:22 +0000 |
860 | +++ lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-10-01 01:43:11 +0000 |
861 | @@ -67,6 +67,7 @@ |
862 | from lp.registry.model.person import Person |
863 | from lp.services.database.interfaces import IStore |
864 | from lp.services.database.sqlbase import convert_storm_clause_to_string |
865 | +from lp.services.features.testing import FeatureFixture |
866 | from lp.services.searchbuilder import ( |
867 | all, |
868 | any, |
869 | @@ -419,6 +420,16 @@ |
870 | BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS)) |
871 | self.assertSearchFinds(params, self.bugtasks[1:]) |
872 | |
873 | + def test_blueprints_linked_with_xref(self): |
874 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
875 | + self.test_blueprints_linked() |
876 | + |
877 | + def test_blueprints_linked_with_xref_and_no_old(self): |
878 | + self.useFixture(FeatureFixture({ |
879 | + 'bugs.xref_buglinks.query': 'true', |
880 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
881 | + self.test_blueprints_linked() |
882 | + |
883 | def test_limit_search_to_one_bug(self): |
884 | # Search results can be limited to a given bug. |
885 | params = self.getBugTaskSearchParams( |
886 | @@ -492,6 +503,16 @@ |
887 | params = self.getBugTaskSearchParams(user=None, has_cve=True) |
888 | self.assertSearchFinds(params, self.bugtasks[:1]) |
889 | |
890 | + def test_has_cve_with_xref(self): |
891 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
892 | + self.test_has_cve() |
893 | + |
894 | + def test_has_cve_with_xref_and_no_old(self): |
895 | + self.useFixture(FeatureFixture({ |
896 | + 'bugs.xref_buglinks.query': 'true', |
897 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
898 | + self.test_has_cve() |
899 | + |
900 | def test_sort_by_milestone_name(self): |
901 | expected = self.setUpMilestoneSorting() |
902 | params = self.getBugTaskSearchParams( |
903 | |
904 | === modified file 'lib/lp/bugs/tests/test_cve.py' |
905 | --- lib/lp/bugs/tests/test_cve.py 2015-09-28 07:39:28 +0000 |
906 | +++ lib/lp/bugs/tests/test_cve.py 2015-10-01 01:43:11 +0000 |
907 | @@ -7,6 +7,7 @@ |
908 | |
909 | from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
910 | from lp.bugs.interfaces.cve import ICveSet |
911 | +from lp.services.features.testing import FeatureFixture |
912 | from lp.testing import ( |
913 | login_person, |
914 | person_logged_in, |
915 | @@ -77,6 +78,42 @@ |
916 | u'CVE-2000-0004'] |
917 | self.assertEqual(expected, cve_data) |
918 | |
919 | + def test_getBugCveCount(self): |
920 | + login_person(self.factory.makePerson()) |
921 | + |
922 | + base = getUtility(ICveSet).getBugCveCount() |
923 | + bug1 = self.factory.makeBug() |
924 | + bug2 = self.factory.makeBug() |
925 | + cve1 = self.factory.makeCVE(sequence='2099-1234') |
926 | + cve2 = self.factory.makeCVE(sequence='2099-2468') |
927 | + self.assertEqual(base, getUtility(ICveSet).getBugCveCount()) |
928 | + cve1.linkBug(bug1) |
929 | + self.assertEqual(base + 1, getUtility(ICveSet).getBugCveCount()) |
930 | + cve1.linkBug(bug2) |
931 | + self.assertEqual(base + 2, getUtility(ICveSet).getBugCveCount()) |
932 | + cve2.linkBug(bug1) |
933 | + self.assertEqual(base + 3, getUtility(ICveSet).getBugCveCount()) |
934 | + cve1.unlinkBug(bug1) |
935 | + cve1.unlinkBug(bug2) |
936 | + cve2.unlinkBug(bug1) |
937 | + self.assertEqual(base, getUtility(ICveSet).getBugCveCount()) |
938 | + |
939 | + |
940 | +class TestCveSetWithXRef(TestCveSet): |
941 | + |
942 | + def setUp(self): |
943 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
944 | + super(TestCveSetWithXRef, self).setUp() |
945 | + |
946 | + |
947 | +class TestCveSetWithXRefAndNoOld(TestCveSet): |
948 | + |
949 | + def setUp(self): |
950 | + self.useFixture(FeatureFixture({ |
951 | + 'bugs.xref_buglinks.query': 'true', |
952 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
953 | + super(TestCveSetWithXRefAndNoOld, self).setUp() |
954 | + |
955 | |
956 | class TestBugLinks(TestCaseWithFactory): |
957 | |
958 | @@ -113,3 +150,19 @@ |
959 | self.assertContentEqual([bug1], cve2.bugs) |
960 | self.assertContentEqual([cve2], bug1.cves) |
961 | self.assertContentEqual([], bug2.cves) |
962 | + |
963 | + |
964 | +class TestBugLinksWithXRef(TestBugLinks): |
965 | + |
966 | + def setUp(self): |
967 | + super(TestBugLinksWithXRef, self).setUp() |
968 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
969 | + |
970 | + |
971 | +class TestBugLinksWithXRefAndNoOld(TestBugLinks): |
972 | + |
973 | + def setUp(self): |
974 | + super(TestBugLinksWithXRefAndNoOld, self).setUp() |
975 | + self.useFixture(FeatureFixture({ |
976 | + 'bugs.xref_buglinks.query': 'true', |
977 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
978 | |
979 | === modified file 'lib/lp/coop/answersbugs/model.py' |
980 | --- lib/lp/coop/answersbugs/model.py 2015-09-25 10:15:37 +0000 |
981 | +++ lib/lp/coop/answersbugs/model.py 2015-10-01 01:43:11 +0000 |
982 | @@ -9,6 +9,8 @@ |
983 | |
984 | from sqlobject import ForeignKey |
985 | |
986 | +from lp.services.database.constants import UTC_NOW |
987 | +from lp.services.database.datetimecol import UtcDateTimeCol |
988 | from lp.services.database.sqlbase import SQLBase |
989 | |
990 | |
991 | @@ -20,3 +22,4 @@ |
992 | question = ForeignKey( |
993 | dbName='question', foreignKey='Question', notNull=True) |
994 | bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True) |
995 | + date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) |
996 | |
997 | === modified file 'lib/lp/coop/answersbugs/tests/test_questionbug.py' |
998 | --- lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-09-29 05:04:44 +0000 |
999 | +++ lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-10-01 01:43:11 +0000 |
1000 | @@ -1,6 +1,7 @@ |
1001 | # Copyright 2015 Canonical Ltd. This software is licensed under the |
1002 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1003 | |
1004 | +from lp.services.features.testing import FeatureFixture |
1005 | from lp.testing import ( |
1006 | login_person, |
1007 | TestCaseWithFactory, |
1008 | @@ -100,3 +101,19 @@ |
1009 | self.assertFalse(bug.isSubscribed(question.owner)) |
1010 | question.unlinkBug(bug) |
1011 | self.assertFalse(bug.isSubscribed(question.owner)) |
1012 | + |
1013 | + |
1014 | +class TestQuestionBugLinksWithXRef(TestQuestionBugLinks): |
1015 | + |
1016 | + def setUp(self): |
1017 | + super(TestQuestionBugLinksWithXRef, self).setUp() |
1018 | + self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'})) |
1019 | + |
1020 | + |
1021 | +class TestQuestionBugLinksWithXRefAndNoOld(TestQuestionBugLinks): |
1022 | + |
1023 | + def setUp(self): |
1024 | + super(TestQuestionBugLinksWithXRefAndNoOld, self).setUp() |
1025 | + self.useFixture(FeatureFixture({ |
1026 | + 'bugs.xref_buglinks.query': 'true', |
1027 | + 'bugs.xref_buglinks.write_old.disabled': 'true'})) |
1028 | |
1029 | === modified file 'lib/lp/scripts/garbo.py' |
1030 | --- lib/lp/scripts/garbo.py 2015-07-22 07:09:12 +0000 |
1031 | +++ lib/lp/scripts/garbo.py 2015-10-01 01:43:11 +0000 |
1032 | @@ -12,6 +12,7 @@ |
1033 | 'save_garbo_job_state', |
1034 | ] |
1035 | |
1036 | +from collections import defaultdict |
1037 | from datetime import ( |
1038 | datetime, |
1039 | timedelta, |
1040 | @@ -81,6 +82,7 @@ |
1041 | from lp.services.database.bulk import ( |
1042 | create, |
1043 | dbify_value, |
1044 | + load_related, |
1045 | ) |
1046 | from lp.services.database.constants import UTC_NOW |
1047 | from lp.services.database.interfaces import IMasterStore |
1048 | @@ -1678,6 +1680,66 @@ |
1049 | transaction.abort() |
1050 | |
1051 | |
1052 | +class BugXRefMigrator(TunableLoop): |
1053 | + """Creates an XRef record for each former IBugLink.""" |
1054 | + |
1055 | + maximum_chunk_size = 5000 |
1056 | + |
1057 | + def __init__(self, log, abort_time=None): |
1058 | + super(BugXRefMigrator, self).__init__(log, abort_time) |
1059 | + self.start_at = 1 |
1060 | + self.store = IMasterStore(Bug) |
1061 | + |
1062 | + def findBugs(self): |
1063 | + if not getFeatureFlag('bugs.xref_buglinks.garbo.enabled'): |
1064 | + return EmptyResultSet() |
1065 | + return self.store.find( |
1066 | + Bug, Bug.id >= self.start_at).order_by(Bug.id) |
1067 | + |
1068 | + def isDone(self): |
1069 | + return self.findBugs().is_empty() |
1070 | + |
1071 | + def __call__(self, chunk_size): |
1072 | + # Grab a chunk of Bug IDs. |
1073 | + # Find all QuestionBugs, SpecificationBugs and BugCves for each |
1074 | + # of those bugs. |
1075 | + # Compose a list of link IDs that should exist. |
1076 | + # Perform a bulk XRef find for all of those. |
1077 | + # Create any missing. |
1078 | + from lp.blueprints.model.specificationbug import SpecificationBug |
1079 | + from lp.bugs.model.bugcve import BugCve |
1080 | + from lp.bugs.model.cve import Cve |
1081 | + from lp.coop.answersbugs.model import QuestionBug |
1082 | + from lp.services.xref.interfaces import IXRefSet |
1083 | + bug_ids = list(self.findBugs()[:chunk_size].values(Bug.id)) |
1084 | + qbs = list(self.store.find( |
1085 | + QuestionBug, QuestionBug.bugID.is_in(bug_ids))) |
1086 | + sbs = list(self.store.find( |
1087 | + SpecificationBug, SpecificationBug.bugID.is_in(bug_ids))) |
1088 | + bcs = list(self.store.find(BugCve, BugCve.bugID.is_in(bug_ids))) |
1089 | + wanted = defaultdict(dict) |
1090 | + for qb in qbs: |
1091 | + wanted[(u'bug', unicode(qb.bugID))][ |
1092 | + (u'question', unicode(qb.questionID))] = { |
1093 | + 'date_created': qb.date_created} |
1094 | + for sb in sbs: |
1095 | + wanted[(u'bug', unicode(sb.bugID))][ |
1096 | + (u'specification', unicode(sb.specificationID))] = {} |
1097 | + load_related(Cve, bcs, ['cveID']) |
1098 | + for bc in bcs: |
1099 | + wanted[(u'bug', unicode(bc.bugID))][ |
1100 | + (u'cve', unicode(bc.cve.sequence))] = {} |
1101 | + existing = getUtility(IXRefSet).findFromMany(wanted.keys()) |
1102 | + needed = { |
1103 | + bug: { |
1104 | + other: meta for other, meta in others.iteritems() |
1105 | + if other not in existing.get(bug, {})} |
1106 | + for bug, others in wanted.iteritems() if others} |
1107 | + getUtility(IXRefSet).create(needed) |
1108 | + self.start_at = bug_ids[-1] + 1 |
1109 | + transaction.commit() |
1110 | + |
1111 | + |
1112 | class FrequentDatabaseGarbageCollector(BaseDatabaseGarbageCollector): |
1113 | """Run every 5 minutes. |
1114 | |
1115 | @@ -1711,6 +1773,7 @@ |
1116 | tunable_loops = [ |
1117 | BugHeatUpdater, |
1118 | BugWatchScheduler, |
1119 | + BugXRefMigrator, |
1120 | DuplicateSessionPruner, |
1121 | RevisionCachePruner, |
1122 | UnusedSessionPruner, |
1123 | |
1124 | === modified file 'lib/lp/scripts/tests/test_garbo.py' |
1125 | --- lib/lp/scripts/tests/test_garbo.py 2015-07-22 07:09:12 +0000 |
1126 | +++ lib/lp/scripts/tests/test_garbo.py 2015-10-01 01:43:11 +0000 |
1127 | @@ -1433,6 +1433,94 @@ |
1128 | for person in people_enf_true: |
1129 | _assert_enf_by_person(person, True) |
1130 | |
1131 | + def test_BugXRefMigrator(self): |
1132 | + from testtools.matchers import ( |
1133 | + Equals, |
1134 | + Is, |
1135 | + MatchesDict, |
1136 | + Not, |
1137 | + ) |
1138 | + |
1139 | + from lp.bugs.model.bug import Bug |
1140 | + from lp.bugs.model.bugcve import BugCve |
1141 | + from lp.blueprints.model.specificationbug import SpecificationBug |
1142 | + from lp.coop.answersbugs.model import QuestionBug |
1143 | + from lp.services.database.interfaces import IStore |
1144 | + from lp.services.xref.interfaces import IXRefSet |
1145 | + |
1146 | + switch_dbuser('testadmin') |
1147 | + self.useFixture(FeatureFixture( |
1148 | + {'bugs.xref_buglinks.garbo.enabled': 'on'})) |
1149 | + store = IStore(Bug) |
1150 | + |
1151 | + # The first bug has a spec and a question. |
1152 | + bug1 = self.factory.makeBug() |
1153 | + spec1 = self.factory.makeSpecification() |
1154 | + sb1 = SpecificationBug(specification=spec1, bug=bug1) |
1155 | + store.add(sb1) |
1156 | + question1 = self.factory.makeQuestion() |
1157 | + qb1 = QuestionBug(question=question1, bug=bug1) |
1158 | + store.add(qb1) |
1159 | + |
1160 | + # A second bug has a question and a CVE. |
1161 | + bug2 = self.factory.makeBug() |
1162 | + question2 = self.factory.makeQuestion() |
1163 | + qb2 = QuestionBug(question=question2, bug=bug2) |
1164 | + store.add(qb2) |
1165 | + cve2 = self.factory.makeCVE(sequence='2099-1234') |
1166 | + bc2 = BugCve(bug=bug2, cve=cve2) |
1167 | + store.add(bc2) |
1168 | + |
1169 | + # Bug the third is all alone. |
1170 | + bug3 = self.factory.makeBug() |
1171 | + |
1172 | + # Bug four has just a spec. |
1173 | + bug4 = self.factory.makeBug() |
1174 | + spec4 = self.factory.makeSpecification() |
1175 | + sb4 = SpecificationBug(specification=spec4, bug=bug4) |
1176 | + store.add(sb4) |
1177 | + |
1178 | + # Initially the new XRef table has no links for the bugs. |
1179 | + self.assertEqual( |
1180 | + {}, |
1181 | + getUtility(IXRefSet).findFromMany( |
1182 | + (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4))) |
1183 | + |
1184 | + # Garbo fills in links for each QuestionBug, SpecificationBug |
1185 | + # and BugCve. |
1186 | + self.runHourly() |
1187 | + matches_expected = MatchesDict({ |
1188 | + (u'bug', unicode(bug1.id)): MatchesDict({ |
1189 | + (u'specification', unicode(spec1.id)): MatchesDict({ |
1190 | + 'metadata': Is(None), 'creator': Is(None), |
1191 | + 'date_created': Not(Is(None))}), |
1192 | + (u'question', unicode(question1.id)): MatchesDict({ |
1193 | + 'metadata': Is(None), 'creator': Is(None), |
1194 | + 'date_created': Equals(qb1.date_created)}), |
1195 | + }), |
1196 | + (u'bug', unicode(bug2.id)): MatchesDict({ |
1197 | + (u'question', unicode(question2.id)): MatchesDict({ |
1198 | + 'metadata': Is(None), 'creator': Is(None), |
1199 | + 'date_created': Equals(qb2.date_created)}), |
1200 | + (u'cve', cve2.sequence): MatchesDict({ |
1201 | + 'metadata': Is(None), 'creator': Is(None), |
1202 | + 'date_created': Not(Is(None))}), |
1203 | + }), |
1204 | + (u'bug', unicode(bug4.id)): MatchesDict({ |
1205 | + (u'specification', unicode(spec4.id)): MatchesDict({ |
1206 | + 'metadata': Is(None), 'creator': Is(None), |
1207 | + 'date_created': Not(Is(None))}), |
1208 | + }), |
1209 | + }) |
1210 | + self.assertThat( |
1211 | + getUtility(IXRefSet).findFromMany( |
1212 | + (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4)), |
1213 | + matches_expected) |
1214 | + |
1215 | + # A second run is harmless. |
1216 | + self.runHourly() |
1217 | + |
1218 | + |
1219 | |
1220 | class TestGarboTasks(TestCaseWithFactory): |
1221 | layer = LaunchpadZopelessLayer |
1222 | |
1223 | === modified file 'lib/lp/services/xref/model.py' |
1224 | --- lib/lp/services/xref/model.py 2015-09-28 23:37:26 +0000 |
1225 | +++ lib/lp/services/xref/model.py 2015-10-01 01:43:11 +0000 |
1226 | @@ -5,6 +5,7 @@ |
1227 | |
1228 | __metaclass__ = type |
1229 | __all__ = [ |
1230 | + "XRef", |
1231 | "XRefSet", |
1232 | ] |
1233 | |
1234 | @@ -37,6 +38,9 @@ |
1235 | |
1236 | The to_id_int and from_id_int columns exist for efficient SQL joins. |
1237 | They are set automatically when the ID looks like an integer. |
1238 | + |
1239 | + NOTE: This should rarely be used directly. Prefer IXRefSet unless |
1240 | + porting an old query. |
1241 | """ |
1242 | |
1243 | __storm_table__ = 'XRef' |