Merge lp:~frankban/launchpad/bug-904335-get-tags into lp:launchpad
- bug-904335-get-tags
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Francesco Banconi | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 14677 | ||||
Proposed branch: | lp:~frankban/launchpad/bug-904335-get-tags | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~yellow/launchpad/bug-904335-devel-base | ||||
Diff against target: |
5908 lines (+1000/-2618) 85 files modified
buildout-templates/bin/retest.in (+0/-4) database/schema/patch-2209-00-4.sql (+0/-57) lib/canonical/database/postgresql.py (+0/-10) lib/canonical/database/sqlbase.py (+0/-20) lib/canonical/launchpad/scripts/__init__.py (+0/-16) lib/lp/answers/browser/faq.py (+0/-12) lib/lp/app/browser/tests/test_launchpadform_doc.py (+0/-4) lib/lp/app/doc/launchpadform.txt (+0/-16) lib/lp/app/javascript/listing_navigator.js (+0/-4) lib/lp/app/javascript/tests/test_listing_navigator.js (+0/-7) lib/lp/archivepublisher/scripts/generate_extra_overrides.py (+0/-4) lib/lp/archiveuploader/dscfile.py (+0/-4) lib/lp/archiveuploader/tests/test_uploadprocessor.py (+4/-2) lib/lp/bugs/browser/bugattachment.py (+15/-34) lib/lp/bugs/browser/bugsubscription.py (+0/-13) lib/lp/bugs/browser/bugsupervisor.py (+0/-6) lib/lp/bugs/browser/bugtask.py (+1/-1) lib/lp/bugs/browser/tests/test_bugs.py (+0/-1) lib/lp/bugs/interfaces/bugtask.py (+4/-3) lib/lp/bugs/javascript/buglisting.js (+0/-231) lib/lp/bugs/javascript/tests/test_buglisting.html (+0/-63) lib/lp/bugs/javascript/tests/test_buglisting.js (+0/-245) lib/lp/bugs/model/bugtask.py (+54/-9) lib/lp/bugs/tests/test_structuralsubscription.py (+0/-7) lib/lp/buildmaster/doc/buildqueue.txt (+0/-4) lib/lp/buildmaster/interfaces/builder.py (+3/-1) lib/lp/buildmaster/manager.py (+72/-52) lib/lp/buildmaster/model/builder.py (+27/-24) lib/lp/buildmaster/model/buildfarmjobbehavior.py (+63/-32) lib/lp/buildmaster/model/buildqueue.py (+0/-26) lib/lp/buildmaster/model/packagebuild.py (+87/-66) lib/lp/buildmaster/tests/test_builder.py (+3/-17) lib/lp/buildmaster/tests/test_manager.py (+13/-94) lib/lp/buildmaster/tests/test_packagebuild.py (+8/-14) lib/lp/code/browser/sourcepackagerecipebuild.py (+0/-13) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+0/-8) lib/lp/code/model/sourcepackagerecipebuild.py (+0/-15) lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+6/-8) lib/lp/code/scripts/tests/test_revisionkarma.py (+0/-4) lib/lp/hardwaredb/doc/hwdb.txt (+1/-2) lib/lp/registry/browser/__init__.py (+0/-11) lib/lp/registry/browser/branding.py (+0/-8) lib/lp/registry/browser/configure.zcml (+4/-4) lib/lp/registry/browser/distributionsourcepackage.py (+0/-24) lib/lp/registry/browser/distroseries.py (+22/-54) lib/lp/registry/browser/distroseriesdifference.py (+0/-10) lib/lp/registry/browser/driver.py (+0/-9) lib/lp/registry/browser/karma.py (+0/-14) lib/lp/registry/browser/milestone.py (+9/-10) lib/lp/registry/browser/nameblacklist.py (+0/-16) lib/lp/registry/browser/teammembership.py (+9/-22) lib/lp/registry/configure.zcml (+13/-4) lib/lp/registry/doc/mailinglist-subscriptions.txt (+0/-8) lib/lp/registry/interfaces/milestone.py (+78/-34) lib/lp/registry/interfaces/milestonetag.py (+20/-0) lib/lp/registry/model/mailinglist.py (+0/-4) lib/lp/registry/model/milestone.py (+90/-27) lib/lp/registry/model/milestonetag.py (+90/-0) lib/lp/registry/model/projectgroup.py (+3/-3) lib/lp/registry/tests/test_milestone.py (+49/-2) lib/lp/registry/tests/test_milestonetag.py (+202/-0) lib/lp/registry/tests/test_person.py (+6/-806) lib/lp/registry/tests/test_personset.py (+0/-19) lib/lp/registry/tests/test_xmlrpc.py (+0/-4) lib/lp/scripts/garbo.py (+0/-24) lib/lp/security.py (+3/-2) lib/lp/services/database/transaction_policy.py (+5/-2) lib/lp/services/identity/model/emailaddress.py (+0/-4) lib/lp/services/looptuner.py (+0/-35) lib/lp/services/mail/tests/emails/x-unknown-encoding.txt (+1/-2) lib/lp/services/verification/model/logintoken.py (+8/-20) lib/lp/services/webapp/__init__.py (+1/-12) lib/lp/services/webapp/tests/test_login.py (+0/-7) lib/lp/soyuz/browser/builder.py (+0/-44) lib/lp/soyuz/browser/distroarchseries.py (+3/-18) lib/lp/soyuz/browser/tests/test_builder.py (+0/-71) lib/lp/soyuz/browser/tests/test_builder_views.py (+3/-37) lib/lp/soyuz/model/binarypackagebuild.py (+0/-5) lib/lp/soyuz/tests/test_binarypackagebuild.py (+6/-15) lib/lp/translations/browser/distribution.py (+0/-14) lib/lp/translations/browser/distroseries.py (+0/-12) lib/lp/translations/browser/potemplate.py (+0/-28) lib/lp/translations/browser/project.py (+0/-11) lib/lp/translations/model/translationtemplatesbuild.py (+1/-1) lib/lp/translations/model/translationtemplatesbuildbehavior.py (+13/-8) |
||||
To merge this branch: | bzr merge lp:~frankban/launchpad/bug-904335-get-tags | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster (community) | Approve | ||
Review via email: mp+87489@code.launchpad.net |
Commit message
Description of the change
= Summary =
Project groups needs a way to aggregate milestones, and introducing milestone
tags seems a way to do that in a flexible manner.
== Proposed fix ==
A Project Group Milestone Tag object is needed to aggregate milestones
inside a project group. This patch introduces a convenient interface and
its implementation: the behaviour of the new model must be similar to what
is already present in Project Group Milestones. Above all we need the
ability to retrieve bugtasks and specifications associated with a milestone
tag.
== Pre-implementation notes ==
The concept of milestone tag was controversial but appears to be a good
tradeoff to meet Linaro needs. A db-devel change is needed to get milestone
tags work. That change is present in
lp:~frankban/launchpad/db-milestonetags-480123
== Implementation details ==
A milestone interfaces refactoring is present in this patch.
IMilestoneData is now a base interface for any other milestone-like
interface, and IAbstractMilestone is introduced as an intermediate interface
for milestone.
Both IMilestone and IProjectGroupMi
Both IMilestone and IProjectGroupMi
IMilestoneData subclasses.
The milestone model now has getter and setter methods for tags.
A MilestoneTag model is introduced as a storm interface to the new
milestonetag table.
A ProjectGroupMil
and specifications.
== Tests ==
bin/test -vv lp.registry.
bin/test -vv lp.registry.
bin/test -vv -t lp.registry.
== Demo and Q/A ==
The changes are not visible in the web ui.
However, due to milestone refactoring, checking the various milestone
pages may be a good idea.
Francis J. Lacoste (flacoste) wrote : | # |
I don't think it's worth supporting excluding conjoined bug tasks in this. We are planning to remove the concept altogether soon (by having all bugtasks explicitely target a series). It's likely that Deryck's squad will handle that work as we have two escalated bugs around that area and everyone is keen on making this happen.
So I think simply raising an exception here is fine. Once we have only series bugtasks, that guard can go.
See https:/
Francesco Banconi (frankban) wrote : | # |
Thanks Gary and Francis for your suggestions. I'm working on it.
Gary: to add user metadata tests i've introduced a new Milestone.
Gary Poster (gary) wrote : | # |
The changes look great! We talked on IRC about a few small text changes (s/yet/still/, desiderable -> desirable, MiletsoneTag -> MilestoneTag), and a few optional ideas I gave: you could assert that the metadata date was a datetime (I like this), and you could make getTagsData into a readonly property (I don't really like this, but it would work).
Thanks again
Gary
Francesco Banconi (frankban) wrote : | # |
Thank you Gary, fixed typos and switched to isinstance assertion for date_created.
Preview Diff
1 | === modified file 'buildout-templates/bin/retest.in' |
2 | --- buildout-templates/bin/retest.in 2012-01-09 13:42:03 +0000 |
3 | +++ buildout-templates/bin/retest.in 2012-01-05 16:22:45 +0000 |
4 | @@ -28,11 +28,7 @@ |
5 | import os |
6 | import re |
7 | import sys |
8 | -<<<<<<< TREE |
9 | from itertools import takewhile, imap |
10 | -======= |
11 | -from itertools import takewhile |
12 | ->>>>>>> MERGE-SOURCE |
13 | |
14 | ${python-relative-path-setup} |
15 | |
16 | |
17 | === removed file 'database/schema/patch-2209-00-4.sql' |
18 | --- database/schema/patch-2209-00-4.sql 2012-01-09 13:42:03 +0000 |
19 | +++ database/schema/patch-2209-00-4.sql 1970-01-01 00:00:00 +0000 |
20 | @@ -1,57 +0,0 @@ |
21 | -SET client_min_messages=ERROR; |
22 | - |
23 | -CREATE OR REPLACE FUNCTION check_email_address_person_account( |
24 | - person integer, account integer) |
25 | - RETURNS boolean |
26 | - LANGUAGE plpythonu IMMUTABLE RETURNS NULL ON NULL INPUT AS |
27 | -$$ |
28 | - # It's possible for an EmailAddress to be created without an |
29 | - # account. If that happens, and this function is called, we return |
30 | - # True so as to avoid breakages. |
31 | - if account is None: |
32 | - return True |
33 | - results = plpy.execute(""" |
34 | - SELECT account FROM Person WHERE id = %s""" % person) |
35 | - # If there are no accounts with that Person in the DB, or the Person |
36 | - # is new and hasn't yet been linked to an account, return success |
37 | - # anyway. This helps avoid the PGRestore breaking (and referential |
38 | - # integrity will prevent this from causing bugs later. |
39 | - if results.nrows() == 0 or results[0]['account'] is None: |
40 | - return True |
41 | - return results[0]['account'] == account |
42 | -$$; |
43 | - |
44 | -COMMENT ON FUNCTION check_email_address_person_account(integer, integer) IS |
45 | -'Check that the person to which an email address is linked has the same account as that email address.'; |
46 | - |
47 | -CREATE OR REPLACE FUNCTION check_person_email_address_account( |
48 | - person integer, account integer) |
49 | - RETURNS boolean |
50 | - LANGUAGE plpythonu IMMUTABLE RETURNS NULL ON NULL INPUT AS |
51 | -$$ |
52 | - # It's possible for a Person to be created without an account. If |
53 | - # that happens, return True so that things don't break. |
54 | - if account is None: |
55 | - return True |
56 | - email_address_accounts = plpy.execute(""" |
57 | - SELECT account FROM EmailAddress WHERE |
58 | - person = %s AND account IS NOT NULL""" % person) |
59 | - # If there are no email address accounts to check, we're done. |
60 | - if email_address_accounts.nrows() == 0: |
61 | - return True |
62 | - for email_account_row in email_address_accounts: |
63 | - email_account = email_account_row['account'] |
64 | - if email_account is not None and email_account != account: |
65 | - return False |
66 | - return True |
67 | -$$; |
68 | - |
69 | -COMMENT ON FUNCTION check_person_email_address_account(integer, integer) IS |
70 | -'Check that the email addresses linked to a person have the same account ID as that person.'; |
71 | - |
72 | -ALTER TABLE EmailAddress ADD CONSTRAINT valid_account_for_person |
73 | - CHECK (check_email_address_person_account(person, account)); |
74 | -ALTER TABLE Person ADD CONSTRAINT valid_account_for_emailaddresses |
75 | - CHECK (check_person_email_address_account(id, account)); |
76 | - |
77 | -INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 00, 4); |
78 | |
79 | === removed symlink 'lib/canonical/config/__init__.py' |
80 | === target was u'../../lp/services/config/__init__.py' |
81 | === removed directory 'lib/canonical/database' |
82 | === removed file 'lib/canonical/database/__init__.py' |
83 | === removed file 'lib/canonical/database/postgresql.py' |
84 | --- lib/canonical/database/postgresql.py 2012-01-09 13:42:03 +0000 |
85 | +++ lib/canonical/database/postgresql.py 1970-01-01 00:00:00 +0000 |
86 | @@ -1,10 +0,0 @@ |
87 | -# Copyright 2011 Canonical Ltd. This software is licensed under the |
88 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
89 | - |
90 | -# Temporary shim for fti.py. |
91 | - |
92 | -__all__ = [ |
93 | - 'ConnectionString', |
94 | - ] |
95 | - |
96 | -from lp.services.database.postgresql import ConnectionString |
97 | |
98 | === removed file 'lib/canonical/database/sqlbase.py' |
99 | --- lib/canonical/database/sqlbase.py 2012-01-09 13:42:03 +0000 |
100 | +++ lib/canonical/database/sqlbase.py 1970-01-01 00:00:00 +0000 |
101 | @@ -1,20 +0,0 @@ |
102 | -# Copyright 2011 Canonical Ltd. This software is licensed under the |
103 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
104 | - |
105 | -# Temporary shim for fti.py. |
106 | - |
107 | -__all__ = [ |
108 | - 'connect', |
109 | - 'ISOLATION_LEVEL_AUTOCOMMIT', |
110 | - 'ISOLATION_LEVEL_READ_COMMITTED', |
111 | - 'quote', |
112 | - 'quote_identifier', |
113 | - ] |
114 | - |
115 | -from lp.services.database.sqlbase import ( |
116 | - connect, |
117 | - ISOLATION_LEVEL_AUTOCOMMIT, |
118 | - ISOLATION_LEVEL_READ_COMMITTED, |
119 | - quote, |
120 | - quote_identifier, |
121 | - ) |
122 | |
123 | === removed directory 'lib/canonical/launchpad/scripts' |
124 | === removed file 'lib/canonical/launchpad/scripts/__init__.py' |
125 | --- lib/canonical/launchpad/scripts/__init__.py 2012-01-09 13:42:03 +0000 |
126 | +++ lib/canonical/launchpad/scripts/__init__.py 1970-01-01 00:00:00 +0000 |
127 | @@ -1,16 +0,0 @@ |
128 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
129 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
130 | - |
131 | -# This is a temporary shim to support database/schema/fti.py in devel. |
132 | - |
133 | -__all__ = [ |
134 | - 'db_options', |
135 | - 'logger', |
136 | - 'logger_options', |
137 | - ] |
138 | - |
139 | -from lp.services.scripts import ( |
140 | - db_options, |
141 | - logger, |
142 | - logger_options, |
143 | - ) |
144 | |
145 | === modified file 'lib/lp/answers/browser/faq.py' |
146 | --- lib/lp/answers/browser/faq.py 2012-01-09 13:42:03 +0000 |
147 | +++ lib/lp/answers/browser/faq.py 2012-01-05 20:11:40 +0000 |
148 | @@ -12,18 +12,10 @@ |
149 | 'FAQView', |
150 | ] |
151 | |
152 | -<<<<<<< TREE |
153 | from lp import _ |
154 | from lp.answers.interfaces.faq import IFAQ |
155 | from lp.answers.interfaces.faqcollection import IFAQCollection |
156 | from lp.app.browser.launchpadform import ( |
157 | -======= |
158 | -from lp import _ |
159 | -from lp.answers.browser.faqcollection import FAQCollectionMenu |
160 | -from lp.answers.interfaces.faq import IFAQ |
161 | -from lp.answers.interfaces.faqcollection import IFAQCollection |
162 | -from lp.services.webapp import ( |
163 | ->>>>>>> MERGE-SOURCE |
164 | action, |
165 | LaunchpadEditFormView, |
166 | ) |
167 | @@ -33,12 +25,8 @@ |
168 | Link, |
169 | NavigationMenu, |
170 | ) |
171 | -<<<<<<< TREE |
172 | from lp.services.webapp.breadcrumb import Breadcrumb |
173 | from lp.services.webapp.publisher import LaunchpadView |
174 | -======= |
175 | -from lp.services.webapp.breadcrumb import Breadcrumb |
176 | ->>>>>>> MERGE-SOURCE |
177 | |
178 | |
179 | class FAQNavigationMenu(NavigationMenu): |
180 | |
181 | === modified file 'lib/lp/app/browser/tests/test_launchpadform_doc.py' |
182 | --- lib/lp/app/browser/tests/test_launchpadform_doc.py 2012-01-09 13:42:03 +0000 |
183 | +++ lib/lp/app/browser/tests/test_launchpadform_doc.py 2012-01-06 01:29:22 +0000 |
184 | @@ -95,11 +95,7 @@ |
185 | |
186 | >>> from zope.app.form.interfaces import IDisplayWidget, IInputWidget |
187 | >>> from zope.interface import directlyProvides, implements |
188 | -<<<<<<< TREE |
189 | >>> from lp.app.browser.launchpadform import ( |
190 | -======= |
191 | - >>> from lp.services.webapp import ( |
192 | ->>>>>>> MERGE-SOURCE |
193 | ... LaunchpadFormView, custom_widget) |
194 | >>> from zope.schema import Bool |
195 | >>> from zope.publisher.browser import TestRequest |
196 | |
197 | === modified file 'lib/lp/app/doc/launchpadform.txt' |
198 | --- lib/lp/app/doc/launchpadform.txt 2012-01-09 13:42:03 +0000 |
199 | +++ lib/lp/app/doc/launchpadform.txt 2012-01-06 11:08:30 +0000 |
200 | @@ -131,11 +131,7 @@ |
201 | attribute: |
202 | |
203 | >>> from zope.app.form.browser import TextWidget |
204 | -<<<<<<< TREE |
205 | >>> from lp.app.browser.launchpadform import custom_widget |
206 | -======= |
207 | - >>> from lp.services.webapp import custom_widget |
208 | ->>>>>>> MERGE-SOURCE |
209 | >>> from lp.app.widgets.password import PasswordChangeWidget |
210 | |
211 | >>> class FormTestView3(LaunchpadFormView): |
212 | @@ -179,11 +175,7 @@ |
213 | submit actions. These are added to the view class using the "action" |
214 | decorator: |
215 | |
216 | -<<<<<<< TREE |
217 | >>> from lp.app.browser.launchpadform import action |
218 | -======= |
219 | - >>> from lp.services.webapp import action |
220 | ->>>>>>> MERGE-SOURCE |
221 | >>> class FormTestView4(LaunchpadFormView): |
222 | ... schema = IFormTest |
223 | ... field_names = ['displayname'] |
224 | @@ -536,11 +528,7 @@ |
225 | However, there are cases where a form action is safe (e.g. a "search" |
226 | action). Those actions can be marked as such: |
227 | |
228 | -<<<<<<< TREE |
229 | >>> from lp.app.browser.launchpadform import safe_action |
230 | -======= |
231 | - >>> from lp.services.webapp import safe_action |
232 | ->>>>>>> MERGE-SOURCE |
233 | >>> class UnsafeActionTestView(LaunchpadFormView): |
234 | ... schema = IFormTest |
235 | ... field_names = ['name'] |
236 | @@ -600,11 +588,7 @@ |
237 | |
238 | In other respects, it is used the same way as LaunchpadFormView: |
239 | |
240 | -<<<<<<< TREE |
241 | >>> from lp.app.browser.launchpadform import LaunchpadEditFormView |
242 | -======= |
243 | - >>> from lp.services.webapp import LaunchpadEditFormView |
244 | ->>>>>>> MERGE-SOURCE |
245 | >>> class FormTestView8(LaunchpadEditFormView): |
246 | ... schema = IFormTest |
247 | ... field_names = ['displayname'] |
248 | |
249 | === renamed directory 'lib/lp/app/javascript/inlinehelp.moved' => 'lib/lp/app/javascript/inlinehelp' |
250 | === removed symlink 'lib/lp/app/javascript/inlinehelp' |
251 | === target was u'../../../lp/services/inlinehelp/javascript' |
252 | === modified file 'lib/lp/app/javascript/listing_navigator.js' |
253 | --- lib/lp/app/javascript/listing_navigator.js 2012-01-09 13:42:03 +0000 |
254 | +++ lib/lp/app/javascript/listing_navigator.js 2012-01-06 13:17:34 +0000 |
255 | @@ -17,7 +17,6 @@ |
256 | } |
257 | |
258 | /** |
259 | -<<<<<<< TREE |
260 | * If there is no context (say /bugs/+bugs) we need to generate a weblink for |
261 | * the lp.client to use for things. This is a helper to take the current url |
262 | * and try to generate a likely web_link value to build a url off of. |
263 | @@ -33,9 +32,6 @@ |
264 | |
265 | /** |
266 | * Constructor. |
267 | -======= |
268 | - * Constructor. |
269 | ->>>>>>> MERGE-SOURCE |
270 | * cache is the JSONRequestCache for the batch. |
271 | * template is the template to use for rendering batches. |
272 | * target is a YUI node to update when rendering batches. |
273 | |
274 | === modified file 'lib/lp/app/javascript/tests/test_listing_navigator.js' |
275 | --- lib/lp/app/javascript/tests/test_listing_navigator.js 2012-01-09 13:42:03 +0000 |
276 | +++ lib/lp/app/javascript/tests/test_listing_navigator.js 2012-01-06 13:17:34 +0000 |
277 | @@ -623,7 +623,6 @@ |
278 | navigator.prev_batch(); |
279 | Y.Assert.areSame( |
280 | null, navigator.get('io_provider').last_request); |
281 | -<<<<<<< TREE |
282 | }, |
283 | |
284 | /** |
285 | @@ -652,12 +651,6 @@ |
286 | })); |
287 | |
288 | suite.add(new Y.Test.Case({ |
289 | -======= |
290 | - } |
291 | -})); |
292 | - |
293 | -suite.add(new Y.Test.Case({ |
294 | ->>>>>>> MERGE-SOURCE |
295 | name: "pre-fetching batches", |
296 | setUp: function() { |
297 | this.target = Y.Node.create('<div></div>').set( |
298 | |
299 | === modified file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py' |
300 | --- lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2012-01-09 13:42:03 +0000 |
301 | +++ lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2012-01-03 17:08:40 +0000 |
302 | @@ -17,14 +17,10 @@ |
303 | from germinate.archive import TagFile |
304 | from germinate.germinator import Germinator |
305 | from germinate.log import GerminateFormatter |
306 | -<<<<<<< TREE |
307 | from germinate.seeds import ( |
308 | SeedError, |
309 | SeedStructure, |
310 | ) |
311 | -======= |
312 | -from germinate.seeds import SeedStructure |
313 | ->>>>>>> MERGE-SOURCE |
314 | from zope.component import getUtility |
315 | |
316 | from lp.archivepublisher.config import getPubConfig |
317 | |
318 | === modified file 'lib/lp/archiveuploader/dscfile.py' |
319 | --- lib/lp/archiveuploader/dscfile.py 2012-01-09 13:42:03 +0000 |
320 | +++ lib/lp/archiveuploader/dscfile.py 2012-01-06 11:08:30 +0000 |
321 | @@ -1,8 +1,4 @@ |
322 | -<<<<<<< TREE |
323 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
324 | -======= |
325 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
326 | ->>>>>>> MERGE-SOURCE |
327 | # GNU Affero General Public License version 3 (see the file LICENSE). |
328 | |
329 | """ DSCFile and related. |
330 | |
331 | === modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py' |
332 | --- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2012-01-09 13:42:03 +0000 |
333 | +++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2012-01-03 12:11:57 +0000 |
334 | @@ -629,7 +629,8 @@ |
335 | from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
336 | foo_bar = "Foo Bar <foo.bar@canonical.com>" |
337 | daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>" |
338 | - self.assertEqual([e.strip() for e in to_addrs], [foo_bar, daniel]) |
339 | + self.assertContentEqual( |
340 | + [foo_bar, daniel], [e.strip() for e in to_addrs]) |
341 | self.assertTrue( |
342 | "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s" |
343 | % raw_msg) |
344 | @@ -663,7 +664,8 @@ |
345 | from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
346 | daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>" |
347 | foo_bar = "Foo Bar <foo.bar@canonical.com>" |
348 | - self.assertEqual([e.strip() for e in to_addrs], [foo_bar, daniel]) |
349 | + self.assertContentEqual( |
350 | + [foo_bar, daniel], [e.strip() for e in to_addrs]) |
351 | self.assertTrue("Waiting for approval" in raw_msg, |
352 | "Expected an 'upload awaits approval' email.\n" |
353 | "Got:\n%s" % raw_msg) |
354 | |
355 | === modified file 'lib/lp/bugs/browser/bugattachment.py' |
356 | --- lib/lp/bugs/browser/bugattachment.py 2012-01-09 13:42:03 +0000 |
357 | +++ lib/lp/bugs/browser/bugattachment.py 2012-01-05 20:11:40 +0000 |
358 | @@ -33,40 +33,21 @@ |
359 | IBugAttachmentIsPatchConfirmationForm, |
360 | IBugAttachmentSet, |
361 | ) |
362 | -<<<<<<< TREE |
363 | -from lp.services.librarian.browser import ( |
364 | - FileNavigationMixin, |
365 | - ProxiedLibraryFileAlias, |
366 | - ) |
367 | -from lp.services.librarian.interfaces import ILibraryFileAliasWithParent |
368 | -from lp.services.webapp import ( |
369 | - canonical_url, |
370 | - GetitemNavigation, |
371 | - Navigation, |
372 | - ) |
373 | -from lp.services.webapp.interfaces import ( |
374 | - ICanonicalUrlData, |
375 | - ILaunchBag, |
376 | - ) |
377 | -from lp.services.webapp.menu import structured |
378 | -======= |
379 | -from lp.services.librarian.browser import ( |
380 | - FileNavigationMixin, |
381 | - ProxiedLibraryFileAlias, |
382 | - ) |
383 | -from lp.services.librarian.interfaces import ILibraryFileAliasWithParent |
384 | -from lp.services.webapp import ( |
385 | - canonical_url, |
386 | - custom_widget, |
387 | - GetitemNavigation, |
388 | - Navigation, |
389 | - ) |
390 | -from lp.services.webapp.interfaces import ( |
391 | - ICanonicalUrlData, |
392 | - ILaunchBag, |
393 | - ) |
394 | -from lp.services.webapp.menu import structured |
395 | ->>>>>>> MERGE-SOURCE |
396 | +from lp.services.librarian.browser import ( |
397 | + FileNavigationMixin, |
398 | + ProxiedLibraryFileAlias, |
399 | + ) |
400 | +from lp.services.librarian.interfaces import ILibraryFileAliasWithParent |
401 | +from lp.services.webapp import ( |
402 | + canonical_url, |
403 | + GetitemNavigation, |
404 | + Navigation, |
405 | + ) |
406 | +from lp.services.webapp.interfaces import ( |
407 | + ICanonicalUrlData, |
408 | + ILaunchBag, |
409 | + ) |
410 | +from lp.services.webapp.menu import structured |
411 | |
412 | |
413 | class BugAttachmentContentCheck: |
414 | |
415 | === modified file 'lib/lp/bugs/browser/bugsubscription.py' |
416 | --- lib/lp/bugs/browser/bugsubscription.py 2012-01-09 13:42:03 +0000 |
417 | +++ lib/lp/bugs/browser/bugsubscription.py 2012-01-05 20:11:40 +0000 |
418 | @@ -48,7 +48,6 @@ |
419 | get_structural_subscriptions_for_bug, |
420 | ) |
421 | from lp.services.propertycache import cachedproperty |
422 | -<<<<<<< TREE |
423 | from lp.services.webapp import ( |
424 | canonical_url, |
425 | LaunchpadView, |
426 | @@ -59,18 +58,6 @@ |
427 | ) |
428 | from lp.app.browser.launchpadform import ReturnToReferrerMixin |
429 | from lp.services.webapp.menu import structured |
430 | -======= |
431 | -from lp.services.webapp import ( |
432 | - canonical_url, |
433 | - LaunchpadView, |
434 | - ) |
435 | -from lp.services.webapp.authorization import ( |
436 | - check_permission, |
437 | - precache_permission_for_objects, |
438 | - ) |
439 | -from lp.services.webapp.launchpadform import ReturnToReferrerMixin |
440 | -from lp.services.webapp.menu import structured |
441 | ->>>>>>> MERGE-SOURCE |
442 | |
443 | |
444 | class BugSubscriptionAddView(LaunchpadFormView): |
445 | |
446 | === modified file 'lib/lp/bugs/browser/bugsupervisor.py' |
447 | --- lib/lp/bugs/browser/bugsupervisor.py 2012-01-09 13:42:03 +0000 |
448 | +++ lib/lp/bugs/browser/bugsupervisor.py 2012-01-05 20:11:40 +0000 |
449 | @@ -12,15 +12,9 @@ |
450 | from lazr.restful.interface import copy_field |
451 | from zope.interface import Interface |
452 | |
453 | -<<<<<<< TREE |
454 | from lp.bugs.browser.bugrole import BugRoleMixin |
455 | from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor |
456 | from lp.app.browser.launchpadform import ( |
457 | -======= |
458 | -from lp.bugs.browser.bugrole import BugRoleMixin |
459 | -from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor |
460 | -from lp.services.webapp.launchpadform import ( |
461 | ->>>>>>> MERGE-SOURCE |
462 | action, |
463 | LaunchpadEditFormView, |
464 | ) |
465 | |
466 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
467 | --- lib/lp/bugs/browser/bugtask.py 2012-01-09 13:42:03 +0000 |
468 | +++ lib/lp/bugs/browser/bugtask.py 2012-01-09 13:42:13 +0000 |
469 | @@ -3504,7 +3504,7 @@ |
470 | IDistributionSourcePackage.providedBy(self.context)): |
471 | search_params.setSourcePackage(self.context) |
472 | else: |
473 | - raise AssertionError('Uknown context type: %s' % self.context) |
474 | + raise AssertionError('Unknown context type: %s' % self.context) |
475 | |
476 | return u"".join("%d\n" % bug_id for bug_id in |
477 | getUtility(IBugTaskSet).searchBugIds(search_params)) |
478 | |
479 | === modified file 'lib/lp/bugs/browser/tests/test_bugs.py' |
480 | --- lib/lp/bugs/browser/tests/test_bugs.py 2012-01-09 13:42:03 +0000 |
481 | +++ lib/lp/bugs/browser/tests/test_bugs.py 2012-01-05 18:12:05 +0000 |
482 | @@ -10,7 +10,6 @@ |
483 | |
484 | from lp.bugs.interfaces.malone import IMaloneApplication |
485 | from lp.bugs.publisher import BugsLayer |
486 | -from lp.services.webapp.publisher import canonical_url |
487 | from lp.testing import ( |
488 | set_feature_flag, |
489 | feature_flags, |
490 | |
491 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
492 | --- lib/lp/bugs/interfaces/bugtask.py 2012-01-09 13:42:03 +0000 |
493 | +++ lib/lp/bugs/interfaces/bugtask.py 2012-01-09 13:42:13 +0000 |
494 | @@ -1188,9 +1188,9 @@ |
495 | |
496 | def __init__(self, user, bug=None, searchtext=None, fast_searchtext=None, |
497 | status=None, importance=None, milestone=None, |
498 | - assignee=None, sourcepackagename=None, owner=None, |
499 | - attachmenttype=None, orderby=None, omit_dupes=False, |
500 | - subscriber=None, component=None, |
501 | + milestone_tag=None, assignee=None, sourcepackagename=None, |
502 | + owner=None, attachmenttype=None, orderby=None, |
503 | + omit_dupes=False, subscriber=None, component=None, |
504 | pending_bugwatch_elsewhere=False, resolved_upstream=False, |
505 | open_upstream=False, has_no_upstream_bugtask=False, tag=None, |
506 | has_cve=False, bug_supervisor=None, bug_reporter=None, |
507 | @@ -1213,6 +1213,7 @@ |
508 | self.status = status |
509 | self.importance = importance |
510 | self.milestone = milestone |
511 | + self.milestone_tag = milestone_tag |
512 | self.assignee = assignee |
513 | self.sourcepackagename = sourcepackagename |
514 | self.owner = owner |
515 | |
516 | === modified file 'lib/lp/bugs/javascript/buglisting.js' |
517 | --- lib/lp/bugs/javascript/buglisting.js 2012-01-09 13:42:03 +0000 |
518 | +++ lib/lp/bugs/javascript/buglisting.js 2011-12-23 16:34:48 +0000 |
519 | @@ -1,4 +1,3 @@ |
520 | -<<<<<<< TREE |
521 | /* Copyright 2011 Canonical Ltd. This software is licensed under the |
522 | * GNU Affero General Public License version 3 (see the file LICENSE). |
523 | * |
524 | @@ -227,233 +226,3 @@ |
525 | "requires": [ |
526 | "history", "node", 'lp.app.listing_navigator', 'lp.app.inlinehelp'] |
527 | }); |
528 | -======= |
529 | -/* Copyright 2011 Canonical Ltd. This software is licensed under the |
530 | - * GNU Affero General Public License version 3 (see the file LICENSE). |
531 | - * |
532 | - * Client-side rendering of bug listings. |
533 | - * |
534 | - * @module bugs |
535 | - * @submodule buglisting |
536 | - */ |
537 | - |
538 | -YUI.add('lp.bugs.buglisting', function(Y) { |
539 | - |
540 | -var module = Y.namespace('lp.bugs.buglisting'); |
541 | - |
542 | - |
543 | -/** |
544 | - * Constructor. |
545 | - * |
546 | - * This is the model of the current batch, including the ordering, position, |
547 | - * and what fields are visibile. |
548 | - * |
549 | - * These values are stored in the History object, so that the browser |
550 | - * back/next buttons correctly adjust. The system defaults for field |
551 | - * visibility are fixed, so they are stored directly on the object. |
552 | - * |
553 | - * Accepts a config containing: |
554 | - * - field_visibility the requested field visibility as an associative array |
555 | - * - field_visibility_defaults the system defaults for field visibility as an |
556 | - * associative array. |
557 | - * - batch_key: A string representing the position and ordering of the |
558 | - * current batch, as returned by listing_navigator.get_batch_key |
559 | - */ |
560 | -module.BugListingModel = function() { |
561 | - module.BugListingModel.superclass.constructor.apply(this, arguments); |
562 | -}; |
563 | - |
564 | - |
565 | -module.BugListingModel.NAME = 'buglisting-model'; |
566 | - |
567 | - |
568 | -module.BugListingModel.ATTRS = { |
569 | - field_visibility_defaults: { |
570 | - value: null |
571 | - } |
572 | -}; |
573 | - |
574 | - |
575 | -Y.extend(module.BugListingModel, Y.Base, { |
576 | - /** |
577 | - * Initializer sets up the History object that stores most of the model |
578 | - * data. |
579 | - */ |
580 | - initializer: function(config) { |
581 | - this.set('history', new Y.History({ |
582 | - initialState: Y.merge( |
583 | - config.field_visibility, {batch_key: config.batch_key}) |
584 | - })); |
585 | - }, |
586 | - |
587 | - /** |
588 | - * Return the current field visibility, as an associative array. |
589 | - * Since the history contains field values that are not field-visibility, |
590 | - * use field_visibility_defaults to filter out non-field-visibility |
591 | - * values. |
592 | - */ |
593 | - get_field_visibility: function() { |
594 | - var result = this.get('history').get(); |
595 | - var key_source = this.get('field_visibility_defaults'); |
596 | - Y.each(result, function(value, key) { |
597 | - if (!key_source.hasOwnProperty(key)){ |
598 | - delete result[key]; |
599 | - } |
600 | - }); |
601 | - return result; |
602 | - }, |
603 | - |
604 | - /** |
605 | - * Set the field visibility, updating history. Accepts an associative |
606 | - * array. |
607 | - */ |
608 | - set_field_visibility: function(value) { |
609 | - this.get('history').add(value); |
610 | - }, |
611 | - |
612 | - /** |
613 | - * Return the current batch key. |
614 | - */ |
615 | - get_batch_key: function() { |
616 | - return this.get('history').get('batch_key'); |
617 | - }, |
618 | - |
619 | - /** |
620 | - * Set the current batch. The batch_key and the query mapping identifying |
621 | - * the batch must be supplied. |
622 | - */ |
623 | - set_batch: function(batch_key, query) { |
624 | - var url = '?' + Y.QueryString.stringify(query); |
625 | - this.get('history').addValue('batch_key', batch_key, {url: url}); |
626 | - } |
627 | -}); |
628 | - |
629 | - |
630 | -/** |
631 | - * Constructor. |
632 | - * current_url is used to determine search params. |
633 | - * cache is the JSONRequestCache for the batch. |
634 | - * template is the template to use for rendering batches. |
635 | - * target is a YUI node to update when rendering batches. |
636 | - * navigation_indices is a YUI NodeList of nodes to update with the current |
637 | - * batch info. |
638 | - * io_provider is something providing the Y.io interface, typically used for |
639 | - * testing. Defaults to Y.io. |
640 | - */ |
641 | -module.BugListingNavigator = function(config) { |
642 | - module.BugListingNavigator.superclass.constructor.apply( |
643 | - this, arguments); |
644 | -}; |
645 | - |
646 | -module.BugListingNavigator.ATTRS = { |
647 | -}; |
648 | - |
649 | -Y.extend( |
650 | - module.BugListingNavigator, |
651 | - Y.lp.app.listing_navigator.ListingNavigator, { |
652 | - _bindUI: function () { |
653 | - initInlineHelp(); |
654 | - }, |
655 | - |
656 | - initializer: function(config) { |
657 | - this.get('model').get('history').after( |
658 | - 'change', this.history_changed, this); |
659 | - }, |
660 | - /** |
661 | - * Event handler for history:change events. |
662 | - */ |
663 | - history_changed: function(e) { |
664 | - if (e.newVal.hasOwnProperty('batch_key')) { |
665 | - var batch_key = e.newVal.batch_key; |
666 | - var batch = this.get('batches')[batch_key]; |
667 | - this.pre_fetch_batches(); |
668 | - this.render(); |
669 | - this._bindUI(); |
670 | - } |
671 | - else { |
672 | - // Handle Chrom(e|ium)'s initial popstate. |
673 | - this.get('model').get('history').replace(e.prevVal); |
674 | - } |
675 | - }, |
676 | - |
677 | - /** |
678 | - * Return the model to use for rendering the batch. This will include |
679 | - * updates to field visibility. |
680 | - */ |
681 | - get_render_model: function(current_batch) { |
682 | - return Y.merge( |
683 | - current_batch.mustache_model, |
684 | - this.get('model').get_field_visibility()); |
685 | - }, |
686 | - |
687 | - /** |
688 | - * Handle a previously-unseen batch by storing it in the cache and |
689 | - * stripping out field_visibility values that would otherwise shadow the |
690 | - * real values. |
691 | - */ |
692 | - handle_new_batch: function(batch) { |
693 | - var key, i; |
694 | - Y.each(batch.field_visibility, function(value, key) { |
695 | - for (i = 0; i < batch.mustache_model.items.length; i++) { |
696 | - delete batch.mustache_model.items[i][key]; |
697 | - } |
698 | - }); |
699 | - return this.constructor.superclass.handle_new_batch.call(this, batch); |
700 | - } |
701 | - |
702 | -},{ |
703 | - make_model: function(batch_key, cache) { |
704 | - return new module.BugListingModel({ |
705 | - batch_key: batch_key, |
706 | - field_visibility: cache.field_visibility, |
707 | - field_visibility_defaults: cache.field_visibility_defaults |
708 | - }); |
709 | - }, |
710 | - get_search_params: function(config) { |
711 | - var search_params = Y.lp.app.listing_navigator.get_query( |
712 | - config.current_url); |
713 | - delete search_params.start; |
714 | - delete search_params.memo; |
715 | - delete search_params.direction; |
716 | - delete search_params.orderby; |
717 | - return search_params; |
718 | - } |
719 | -}); |
720 | - |
721 | -/** |
722 | - * Factory to return a BugListingNavigator for the given page. |
723 | - */ |
724 | -module.BugListingNavigator.from_page = function() { |
725 | - var target = Y.one('#client-listing'); |
726 | - if (Y.Lang.isNull(target)){ |
727 | - return null; |
728 | - } |
729 | - var navigation_indices = Y.all('.batch-navigation-index'); |
730 | - var pre_fetch = Y.lp.app.listing_navigator.get_feature_flag( |
731 | - 'bugs.dynamic_bug_listings.pre_fetch'); |
732 | - Y.lp.app.listing_navigator.linkify_navigation(); |
733 | - var navigator = new module.BugListingNavigator({ |
734 | - current_url: window.location, |
735 | - cache: LP.cache, |
736 | - template: LP.mustache_listings, |
737 | - target: target, |
738 | - navigation_indices: navigation_indices, |
739 | - pre_fetch: Boolean(pre_fetch) |
740 | - }); |
741 | - navigator.set('backwards_navigation', Y.all('.first,.previous')); |
742 | - navigator.set('forwards_navigation', Y.all('.last,.next')); |
743 | - navigator.clickAction('.first', navigator.first_batch); |
744 | - navigator.clickAction('.next', navigator.next_batch); |
745 | - navigator.clickAction('.previous', navigator.prev_batch); |
746 | - navigator.clickAction('.last', navigator.last_batch); |
747 | - navigator.render_navigation(); |
748 | - return navigator; |
749 | -}; |
750 | - |
751 | - |
752 | - |
753 | -}, "0.1", { |
754 | - "requires": [ |
755 | - "history", "node", 'lp.app.listing_navigator'] |
756 | -}); |
757 | ->>>>>>> MERGE-SOURCE |
758 | |
759 | === modified file 'lib/lp/bugs/javascript/tests/test_buglisting.html' |
760 | --- lib/lp/bugs/javascript/tests/test_buglisting.html 2012-01-09 13:42:03 +0000 |
761 | +++ lib/lp/bugs/javascript/tests/test_buglisting.html 2012-01-05 21:13:29 +0000 |
762 | @@ -1,4 +1,3 @@ |
763 | -<<<<<<< TREE |
764 | <html> |
765 | <head> |
766 | <title>Bug task listing</title> |
767 | @@ -54,65 +53,3 @@ |
768 | <div id="fixture"></div> |
769 | </body> |
770 | </html> |
771 | -======= |
772 | -<html> |
773 | - <head> |
774 | - <title>Bug task listing</title> |
775 | - <script type="text/javascript"> |
776 | - // this is a fake out of the global help overlay tool we need to call |
777 | - var initInlineHelp = function () { |
778 | - return; |
779 | - }; |
780 | - </script> |
781 | - |
782 | - <!-- YUI and test setup --> |
783 | - <script type="text/javascript" |
784 | - src="../../../../canonical/launchpad/icing/yui/yui/yui.js"> |
785 | - </script> |
786 | - <link rel="stylesheet" href="../../../app/javascript/testing/test.css" /> |
787 | - <script type="text/javascript" |
788 | - src="../../../app/javascript/testing/testrunner.js"></script> |
789 | - <script type="text/javascript" |
790 | - src="../../../app/javascript/testing/assert.js"></script> |
791 | - <script type="text/javascript" |
792 | - src="../../../app/javascript/testing/mockio.js"></script> |
793 | - <script type="text/javascript" |
794 | - src="../../../app/javascript/client.js"></script> |
795 | - <script type="text/javascript" |
796 | - src="../../../app/javascript/effects/effects.js"></script> |
797 | - <script type="text/javascript" |
798 | - src="../../../app/javascript/errors.js"></script> |
799 | - <script type="text/javascript" |
800 | - src="../../../app/javascript/expander.js"></script> |
801 | - <script type="text/javascript" |
802 | - src="../../../app/javascript/formoverlay/formoverlay.js"></script> |
803 | - <script type="text/javascript" |
804 | - src="../../../app/javascript/lp.js"></script> |
805 | - |
806 | - <script type="text/javascript" |
807 | - src="../../../contrib/javascript/mustache.js"></script> |
808 | - <script type="text/javascript" |
809 | - src="../../../app/javascript/overlay/overlay.js"></script> |
810 | - <script type="text/javascript" |
811 | - src="../../../app/javascript/indicator/indicator.js"></script> |
812 | - <script type="text/javascript" |
813 | - src="../../../app/javascript/listing_navigator.js"></script> |
814 | - |
815 | - <!-- The module under test --> |
816 | - <script type="text/javascript" |
817 | - src="../buglisting.js"></script> |
818 | - |
819 | - <!-- The test suite --> |
820 | - <script type="text/javascript" |
821 | - src="test_buglisting.js"></script> |
822 | - |
823 | - <!-- Pretty up the sample html --> |
824 | - <style type="text/css"> |
825 | - div#sample {margin:15px; width:200px; border:1px solid #999; padding:10px;} |
826 | - </style> |
827 | - </head> |
828 | - <body class="yui3-skin-sam"> |
829 | - <div id="fixture"></div> |
830 | - </body> |
831 | -</html> |
832 | ->>>>>>> MERGE-SOURCE |
833 | |
834 | === modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js' |
835 | --- lib/lp/bugs/javascript/tests/test_buglisting.js 2012-01-09 13:42:03 +0000 |
836 | +++ lib/lp/bugs/javascript/tests/test_buglisting.js 2012-01-05 21:13:29 +0000 |
837 | @@ -1,4 +1,3 @@ |
838 | -<<<<<<< TREE |
839 | YUI({ |
840 | base: '../../../../canonical/launchpad/icing/yui/', |
841 | filter: 'raw', combine: false, fetchCSS: false |
842 | @@ -241,247 +240,3 @@ |
843 | Y.Test.Runner.run(); |
844 | }); |
845 | }); |
846 | -======= |
847 | -YUI({ |
848 | - base: '../../../../canonical/launchpad/icing/yui/', |
849 | - filter: 'raw', combine: false, fetchCSS: false |
850 | - }).use('test', 'console', 'lp.bugs.buglisting', 'lp.testing.mockio', |
851 | - 'lp.testing.assert', |
852 | - function(Y) { |
853 | - |
854 | -var suite = new Y.Test.Suite("lp.bugs.buglisting Tests"); |
855 | -var module = Y.lp.bugs.buglisting; |
856 | - |
857 | - |
858 | -suite.add(new Y.Test.Case({ |
859 | - name: 'ListingNavigator', |
860 | - setUp: function() { |
861 | - this.target = Y.Node.create('<div></div>').set( |
862 | - 'id', 'client-listing'); |
863 | - Y.one('body').appendChild(this.target); |
864 | - }, |
865 | - tearDown: function() { |
866 | - this.target.remove(); |
867 | - delete this.target; |
868 | - }, |
869 | - test_sets_search_params: function() { |
870 | - // search_parms includes all query values that don't control batching |
871 | - var navigator = new module.BugListingNavigator({ |
872 | - current_url: 'http://yahoo.com?foo=bar&start=1&memo=2&' + |
873 | - 'direction=3&orderby=4', |
874 | - cache: {next: null, prev: null}, |
875 | - target: this.target |
876 | - }); |
877 | - Y.lp.testing.assert.assert_equal_structure( |
878 | - {foo: 'bar'}, navigator.get('search_params')); |
879 | - }, |
880 | - test_cleans_visibility_from_current_batch: function() { |
881 | - // When initial batch is handled, field visibility is stripped. |
882 | - var navigator = new module.BugListingNavigator({ |
883 | - current_url: '', |
884 | - cache: { |
885 | - field_visibility: {show_item: true}, |
886 | - mustache_model: {items: [{show_item: true} ] }, |
887 | - next: null, |
888 | - prev: null |
889 | - }, |
890 | - target: this.target |
891 | - }); |
892 | - bugtask = navigator.get_current_batch().mustache_model.items[0]; |
893 | - Y.Assert.isFalse(bugtask.hasOwnProperty('show_item')); |
894 | - }, |
895 | - test_cleans_visibility_from_new_batch: function() { |
896 | - // When new batch is handled, field visibility is stripped. |
897 | - var bugtask; |
898 | - var model = { |
899 | - mustache_model: {items: []}, |
900 | - memo: 1, |
901 | - next: null, |
902 | - prev: null, |
903 | - field_visibility: {}, |
904 | - field_visibility_defaults: {show_item: true} |
905 | - }; |
906 | - var navigator = new module.BugListingNavigator({ |
907 | - current_url: '', |
908 | - cache: model, |
909 | - template: '', |
910 | - target: this.target |
911 | - }); |
912 | - var batch = { |
913 | - mustache_model: { |
914 | - items: [{show_item: true}]}, |
915 | - memo: 2, |
916 | - next: null, |
917 | - prev: null, |
918 | - field_visibility: {show_item: true} |
919 | - }; |
920 | - var query = navigator.get_batch_query(batch); |
921 | - navigator.update_from_new_model(query, false, batch); |
922 | - bugtask = navigator.get_current_batch().mustache_model.items[0]; |
923 | - Y.Assert.isFalse(bugtask.hasOwnProperty('show_item')); |
924 | - } |
925 | -})); |
926 | - |
927 | - |
928 | -var get_navigator = function(url, config) { |
929 | - var mock_io = new Y.lp.testing.mockio.MockIo(); |
930 | - if (Y.Lang.isUndefined(url)){ |
931 | - url = ''; |
932 | - } |
933 | - if (Y.Lang.isUndefined(config)){ |
934 | - config = {}; |
935 | - } |
936 | - var target = config.target; |
937 | - if (!Y.Lang.isValue(target)){ |
938 | - var target_parent = Y.Node.create('<div></div>'); |
939 | - target = Y.Node.create('<div "id=#client-listing"></div>'); |
940 | - target_parent.appendChild(target); |
941 | - } |
942 | - lp_cache = { |
943 | - context: { |
944 | - resource_type_link: 'http://foo_type', |
945 | - web_link: 'http://foo/bar' |
946 | - }, |
947 | - view_name: '+bugs', |
948 | - next: { |
949 | - memo: 467, |
950 | - start: 500 |
951 | - }, |
952 | - prev: { |
953 | - memo: 457, |
954 | - start: 400 |
955 | - }, |
956 | - forwards: true, |
957 | - order_by: 'foo', |
958 | - memo: 457, |
959 | - start: 450, |
960 | - last_start: 23, |
961 | - field_visibility: {}, |
962 | - field_visibility_defaults: {} |
963 | - }; |
964 | - if (config.no_next){ |
965 | - lp_cache.next = null; |
966 | - } |
967 | - if (config.no_prev){ |
968 | - lp_cache.prev = null; |
969 | - } |
970 | - var navigator_config = { |
971 | - current_url: url, |
972 | - cache: lp_cache, |
973 | - io_provider: mock_io, |
974 | - pre_fetch: config.pre_fetch, |
975 | - target: target, |
976 | - template: '' |
977 | - }; |
978 | - return new module.BugListingNavigator(navigator_config); |
979 | -}; |
980 | - |
981 | -suite.add(new Y.Test.Case({ |
982 | - name: 'browser history', |
983 | - |
984 | - setUp: function() { |
985 | - this.target = Y.Node.create('<div></div>').set( |
986 | - 'id', 'client-listing'); |
987 | - Y.one('body').appendChild(this.target); |
988 | - }, |
989 | - |
990 | - tearDown: function() { |
991 | - this.target.remove(); |
992 | - delete this.target; |
993 | - }, |
994 | - |
995 | - /** |
996 | - * Update from cache generates a change event for the specified batch. |
997 | - */ |
998 | - test_update_from_cache_generates_event: function() { |
999 | - var navigator = get_navigator('', {target: this.target}); |
1000 | - var e = null; |
1001 | - navigator.get('model').get('history').on('change', function(inner_e) { |
1002 | - e = inner_e; |
1003 | - }); |
1004 | - navigator.get('batches')['some-batch-key'] = { |
1005 | - mustache_model: { |
1006 | - items: [] |
1007 | - }, |
1008 | - next: null, |
1009 | - prev: null |
1010 | - }; |
1011 | - navigator.update_from_cache({foo: 'bar'}, 'some-batch-key'); |
1012 | - Y.Assert.areEqual('some-batch-key', e.newVal.batch_key); |
1013 | - Y.Assert.areEqual('?foo=bar', e._options.url); |
1014 | - }, |
1015 | - |
1016 | - /** |
1017 | - * When a change event is emitted, the relevant batch becomes the current |
1018 | - * batch and is rendered. |
1019 | - */ |
1020 | - test_change_event_renders_cache: function() { |
1021 | - var navigator = get_navigator('', {target: this.target}); |
1022 | - var batch = { |
1023 | - mustache_model: { |
1024 | - items: [], |
1025 | - foo: 'bar' |
1026 | - }, |
1027 | - next: null, |
1028 | - prev: null |
1029 | - }; |
1030 | - navigator.set('template', '{{foo}}'); |
1031 | - navigator.get('batches')['some-batch-key'] = batch; |
1032 | - navigator.get('model').get('history').addValue( |
1033 | - 'batch_key', 'some-batch-key'); |
1034 | - Y.Assert.areEqual(batch, navigator.get_current_batch()); |
1035 | - Y.Assert.areEqual('bar', navigator.get('target').getContent()); |
1036 | - } |
1037 | -})); |
1038 | - |
1039 | -suite.add(new Y.Test.Case({ |
1040 | - name: 'from_page tests', |
1041 | - setUp: function() { |
1042 | - window.LP = { |
1043 | - cache: { |
1044 | - current_batch: {}, |
1045 | - next: null, |
1046 | - prev: null, |
1047 | - related_features: { |
1048 | - 'bugs.dynamic_bug_listings.pre_fetch': {value: 'on'} |
1049 | - } |
1050 | - } |
1051 | - }; |
1052 | - }, |
1053 | - getPreviousLink: function() { |
1054 | - return Y.one('.previous').get('href'); |
1055 | - }, |
1056 | - test_from_page_with_client: function() { |
1057 | - Y.one('#fixture').setContent( |
1058 | - '<a class="previous" href="http://example.org/">PreVious</span>' + |
1059 | - '<div id="client-listing"></div>'); |
1060 | - Y.Assert.areSame('http://example.org/', this.getPreviousLink()); |
1061 | - module.BugListingNavigator.from_page(); |
1062 | - Y.Assert.areNotSame('http://example.org/', this.getPreviousLink()); |
1063 | - }, |
1064 | - test_from_page_with_no_client: function() { |
1065 | - Y.one('#fixture').setContent(''); |
1066 | - var navigator = module.BugListingNavigator.from_page(); |
1067 | - Y.Assert.isNull(navigator); |
1068 | - }, |
1069 | - tearDown: function() { |
1070 | - Y.one('#fixture').setContent(""); |
1071 | - delete window.LP; |
1072 | - } |
1073 | -})); |
1074 | - |
1075 | - |
1076 | -var handle_complete = function(data) { |
1077 | - window.status = '::::' + JSON.stringify(data); |
1078 | - }; |
1079 | -Y.Test.Runner.on('complete', handle_complete); |
1080 | -Y.Test.Runner.add(suite); |
1081 | - |
1082 | -var console = new Y.Console({newestOnTop: false}); |
1083 | -console.render('#log'); |
1084 | - |
1085 | -Y.on('domready', function() { |
1086 | - Y.Test.Runner.run(); |
1087 | -}); |
1088 | -}); |
1089 | ->>>>>>> MERGE-SOURCE |
1090 | |
1091 | === modified file 'lib/lp/bugs/model/bugtask.py' |
1092 | --- lib/lp/bugs/model/bugtask.py 2012-01-09 13:42:03 +0000 |
1093 | +++ lib/lp/bugs/model/bugtask.py 2012-01-09 13:42:13 +0000 |
1094 | @@ -117,6 +117,7 @@ |
1095 | IMilestoneSet, |
1096 | IProjectGroupMilestone, |
1097 | ) |
1098 | +from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag |
1099 | from lp.registry.interfaces.person import ( |
1100 | IPerson, |
1101 | validate_person, |
1102 | @@ -2045,6 +2046,18 @@ |
1103 | if params.status is not None: |
1104 | extra_clauses.append(self._buildStatusClause(params.status)) |
1105 | |
1106 | + if params.exclude_conjoined_tasks: |
1107 | + # XXX: frankban 2012-01-05 bug=912370: excluding conjoined |
1108 | + # bugtasks is not currently supported for milestone tags. |
1109 | + if params.milestone_tag: |
1110 | + raise NotImplementedError( |
1111 | + 'Excluding conjoined tasks is not currently supported ' |
1112 | + 'for milestone tags') |
1113 | + if not params.milestone: |
1114 | + raise ValueError( |
1115 | + "BugTaskSearchParam.exclude_conjoined cannot be True if " |
1116 | + "BugTaskSearchParam.milestone is not set") |
1117 | + |
1118 | if params.milestone: |
1119 | if IProjectGroupMilestone.providedBy(params.milestone): |
1120 | where_cond = """ |
1121 | @@ -2064,10 +2077,29 @@ |
1122 | params.milestone) |
1123 | join_tables += tables |
1124 | extra_clauses += clauses |
1125 | - elif params.exclude_conjoined_tasks: |
1126 | - raise ValueError( |
1127 | - "BugTaskSearchParam.exclude_conjoined cannot be True if " |
1128 | - "BugTaskSearchParam.milestone is not set") |
1129 | + |
1130 | + if params.milestone_tag: |
1131 | + where_cond = """ |
1132 | + IN (SELECT Milestone.id |
1133 | + FROM Milestone, Product, MilestoneTag |
1134 | + WHERE Milestone.product = Product.id |
1135 | + AND Product.project = %s |
1136 | + AND MilestoneTag.milestone = Milestone.id |
1137 | + AND MilestoneTag.tag IN %s |
1138 | + GROUP BY Milestone.id |
1139 | + HAVING COUNT(Milestone.id) = %s) |
1140 | + """ % sqlvalues(params.milestone_tag.target, |
1141 | + params.milestone_tag.tags, |
1142 | + len(params.milestone_tag.tags)) |
1143 | + extra_clauses.append("BugTask.milestone %s" % where_cond) |
1144 | + |
1145 | + # XXX: frankban 2012-01-05 bug=912370: excluding conjoined |
1146 | + # bugtasks is not currently supported for milestone tags. |
1147 | + # if params.exclude_conjoined_tasks: |
1148 | + # tables, clauses = self._buildExcludeConjoinedClause( |
1149 | + # params.milestone_tag) |
1150 | + # join_tables += tables |
1151 | + # extra_clauses += clauses |
1152 | |
1153 | if params.project: |
1154 | # Prevent circular import problems. |
1155 | @@ -2865,12 +2897,25 @@ |
1156 | result[row[:-1]] = row[-1] |
1157 | return result |
1158 | |
1159 | - def getPrecachedNonConjoinedBugTasks(self, user, milestone): |
1160 | + def getPrecachedNonConjoinedBugTasks(self, user, milestone_data): |
1161 | """See `IBugTaskSet`.""" |
1162 | - params = BugTaskSearchParams( |
1163 | - user, milestone=milestone, |
1164 | - orderby=['status', '-importance', 'id'], |
1165 | - omit_dupes=True, exclude_conjoined_tasks=True) |
1166 | + kwargs = { |
1167 | + 'orderby': ['status', '-importance', 'id'], |
1168 | + 'omit_dupes': True, |
1169 | + } |
1170 | + if IProjectGroupMilestoneTag.providedBy(milestone_data): |
1171 | + # XXX: frankban 2012-01-05 bug=912370: excluding conjoined |
1172 | + # bugtasks is not currently supported for milestone tags. |
1173 | + kwargs.update({ |
1174 | + 'exclude_conjoined_tasks': False, |
1175 | + 'milestone_tag': milestone_data, |
1176 | + }) |
1177 | + else: |
1178 | + kwargs.update({ |
1179 | + 'exclude_conjoined_tasks': True, |
1180 | + 'milestone': milestone_data, |
1181 | + }) |
1182 | + params = BugTaskSearchParams(user, **kwargs) |
1183 | return self.search(params) |
1184 | |
1185 | def createTask(self, bug, owner, target, |
1186 | |
1187 | === modified file 'lib/lp/bugs/tests/test_structuralsubscription.py' |
1188 | --- lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-09 13:42:03 +0000 |
1189 | +++ lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-03 14:18:27 +0000 |
1190 | @@ -38,7 +38,6 @@ |
1191 | TestCaseWithFactory, |
1192 | ) |
1193 | from lp.testing.factory import is_security_proxied_or_harmless |
1194 | -<<<<<<< TREE |
1195 | from lp.testing.layers import ( |
1196 | DatabaseFunctionalLayer, |
1197 | LaunchpadFunctionalLayer, |
1198 | @@ -46,12 +45,6 @@ |
1199 | |
1200 | |
1201 | RESULT_SETS = ResultSet, EmptyResultSet, DecoratedResultSet |
1202 | -======= |
1203 | -from lp.testing.layers import ( |
1204 | - DatabaseFunctionalLayer, |
1205 | - LaunchpadFunctionalLayer, |
1206 | - ) |
1207 | ->>>>>>> MERGE-SOURCE |
1208 | |
1209 | |
1210 | class TestStructuralSubscription(TestCaseWithFactory): |
1211 | |
1212 | === modified file 'lib/lp/buildmaster/doc/buildqueue.txt' |
1213 | --- lib/lp/buildmaster/doc/buildqueue.txt 2012-01-09 13:42:03 +0000 |
1214 | +++ lib/lp/buildmaster/doc/buildqueue.txt 2012-01-07 17:40:43 +0000 |
1215 | @@ -12,12 +12,8 @@ |
1216 | As soon as a build job is processed succesfully (dispatched & |
1217 | collected) the BuildQueue record representing it is removed. |
1218 | |
1219 | -<<<<<<< TREE |
1220 | >>> from lp.services.webapp.testing import verifyObject |
1221 | >>> from lp.services.propertycache import get_property_cache |
1222 | -======= |
1223 | - >>> from lp.services.webapp.testing import verifyObject |
1224 | ->>>>>>> MERGE-SOURCE |
1225 | >>> from lp.buildmaster.interfaces.buildqueue import ( |
1226 | ... IBuildQueue, IBuildQueueSet) |
1227 | |
1228 | |
1229 | === modified file 'lib/lp/buildmaster/interfaces/builder.py' |
1230 | --- lib/lp/buildmaster/interfaces/builder.py 2012-01-09 13:42:03 +0000 |
1231 | +++ lib/lp/buildmaster/interfaces/builder.py 2012-01-03 12:11:57 +0000 |
1232 | @@ -1,4 +1,4 @@ |
1233 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
1234 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
1235 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1236 | |
1237 | # pylint: disable-msg=E0211,E0213 |
1238 | @@ -195,6 +195,8 @@ |
1239 | |
1240 | def setSlaveForTesting(proxy): |
1241 | """Sets the RPC proxy through which to operate the build slave.""" |
1242 | + # XXX JeroenVermeulen 2011-11-09, bug=888010: Don't use this. |
1243 | + # It's a trap. See bug for details. |
1244 | |
1245 | def verifySlaveBuildCookie(slave_build_id): |
1246 | """Verify that a slave's build cookie is consistent. |
1247 | |
1248 | === modified file 'lib/lp/buildmaster/manager.py' |
1249 | --- lib/lp/buildmaster/manager.py 2012-01-09 13:42:03 +0000 |
1250 | +++ lib/lp/buildmaster/manager.py 2012-01-06 15:24:37 +0000 |
1251 | @@ -1,8 +1,4 @@ |
1252 | -<<<<<<< TREE |
1253 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
1254 | -======= |
1255 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
1256 | ->>>>>>> MERGE-SOURCE |
1257 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1258 | |
1259 | """Soyuz buildd slave manager logic.""" |
1260 | @@ -38,11 +34,8 @@ |
1261 | BuildBehaviorMismatch, |
1262 | ) |
1263 | from lp.buildmaster.model.builder import Builder |
1264 | -<<<<<<< TREE |
1265 | from lp.services.propertycache import get_property_cache |
1266 | from lp.services.database.transaction_policy import DatabaseTransactionPolicy |
1267 | -======= |
1268 | ->>>>>>> MERGE-SOURCE |
1269 | |
1270 | |
1271 | BUILDD_MANAGER_LOG_NAME = "slave-scanner" |
1272 | @@ -124,13 +117,17 @@ |
1273 | # algorithm for polling. |
1274 | SCAN_INTERVAL = 15 |
1275 | |
1276 | - def __init__(self, builder_name, logger): |
1277 | + def __init__(self, builder_name, logger, clock=None): |
1278 | self.builder_name = builder_name |
1279 | self.logger = logger |
1280 | + if clock is None: |
1281 | + clock = reactor |
1282 | + self._clock = clock |
1283 | |
1284 | def startCycle(self): |
1285 | """Scan the builder and dispatch to it or deal with failures.""" |
1286 | self.loop = LoopingCall(self.singleCycle) |
1287 | + self.loop.clock = self._clock |
1288 | self.stopping_deferred = self.loop.start(self.SCAN_INTERVAL) |
1289 | return self.stopping_deferred |
1290 | |
1291 | @@ -151,51 +148,58 @@ |
1292 | 1. Print the error in the log |
1293 | 2. Increment and assess failure counts on the builder and job. |
1294 | """ |
1295 | - # Make sure that pending database updates are removed as it |
1296 | - # could leave the database in an inconsistent state (e.g. The |
1297 | - # job says it's running but the buildqueue has no builder set). |
1298 | + # Since this is a failure path, we could be in a broken |
1299 | + # transaction. Get us a fresh one. |
1300 | transaction.abort() |
1301 | |
1302 | # If we don't recognise the exception include a stack trace with |
1303 | # the error. |
1304 | error_message = failure.getErrorMessage() |
1305 | - if failure.check( |
1306 | + familiar_error = failure.check( |
1307 | BuildSlaveFailure, CannotBuild, BuildBehaviorMismatch, |
1308 | - CannotResumeHost, BuildDaemonError, CannotFetchFile): |
1309 | - self.logger.info("Scanning %s failed with: %s" % ( |
1310 | - self.builder_name, error_message)) |
1311 | + CannotResumeHost, BuildDaemonError, CannotFetchFile) |
1312 | + if familiar_error: |
1313 | + self.logger.info( |
1314 | + "Scanning %s failed with: %s", |
1315 | + self.builder_name, error_message) |
1316 | else: |
1317 | - self.logger.info("Scanning %s failed with: %s\n%s" % ( |
1318 | + self.logger.info( |
1319 | + "Scanning %s failed with: %s\n%s", |
1320 | self.builder_name, failure.getErrorMessage(), |
1321 | - failure.getTraceback())) |
1322 | + failure.getTraceback()) |
1323 | |
1324 | # Decide if we need to terminate the job or fail the |
1325 | # builder. |
1326 | try: |
1327 | builder = get_builder(self.builder_name) |
1328 | - builder.gotFailure() |
1329 | - if builder.currentjob is not None: |
1330 | - build_farm_job = builder.getCurrentBuildFarmJob() |
1331 | - build_farm_job.gotFailure() |
1332 | - self.logger.info( |
1333 | - "builder %s failure count: %s, " |
1334 | - "job '%s' failure count: %s" % ( |
1335 | + transaction.commit() |
1336 | + |
1337 | + with DatabaseTransactionPolicy(read_only=False): |
1338 | + builder.gotFailure() |
1339 | + |
1340 | + if builder.currentjob is None: |
1341 | + self.logger.info( |
1342 | + "Builder %s failed a probe, count: %s", |
1343 | + self.builder_name, builder.failure_count) |
1344 | + else: |
1345 | + build_farm_job = builder.getCurrentBuildFarmJob() |
1346 | + build_farm_job.gotFailure() |
1347 | + self.logger.info( |
1348 | + "builder %s failure count: %s, " |
1349 | + "job '%s' failure count: %s", |
1350 | self.builder_name, |
1351 | builder.failure_count, |
1352 | build_farm_job.title, |
1353 | - build_farm_job.failure_count)) |
1354 | - else: |
1355 | - self.logger.info( |
1356 | - "Builder %s failed a probe, count: %s" % ( |
1357 | - self.builder_name, builder.failure_count)) |
1358 | - assessFailureCounts(builder, failure.getErrorMessage()) |
1359 | - transaction.commit() |
1360 | + build_farm_job.failure_count) |
1361 | + |
1362 | + assessFailureCounts(builder, failure.getErrorMessage()) |
1363 | + transaction.commit() |
1364 | except: |
1365 | # Catastrophic code failure! Not much we can do. |
1366 | + transaction.abort() |
1367 | self.logger.error( |
1368 | "Miserable failure when trying to examine failure counts:\n", |
1369 | exc_info=True) |
1370 | - transaction.abort() |
1371 | |
1372 | def checkCancellation(self, builder): |
1373 | """See if there is a pending cancellation request. |
1374 | @@ -250,14 +254,9 @@ |
1375 | """ |
1376 | # We need to re-fetch the builder object on each cycle as the |
1377 | # Storm store is invalidated over transaction boundaries. |
1378 | - |
1379 | self.builder = get_builder(self.builder_name) |
1380 | |
1381 | def status_updated(ignored): |
1382 | - # Commit the changes done while possibly rescuing jobs, to |
1383 | - # avoid holding table locks. |
1384 | - transaction.commit() |
1385 | - |
1386 | # See if we think there's an active build on the builder. |
1387 | buildqueue = self.builder.getBuildQueue() |
1388 | |
1389 | @@ -267,14 +266,10 @@ |
1390 | return self.builder.updateBuild(buildqueue) |
1391 | |
1392 | def build_updated(ignored): |
1393 | - # Commit changes done while updating the build, to avoid |
1394 | - # holding table locks. |
1395 | - transaction.commit() |
1396 | - |
1397 | # If the builder is in manual mode, don't dispatch anything. |
1398 | if self.builder.manual: |
1399 | self.logger.debug( |
1400 | - '%s is in manual mode, not dispatching.' % |
1401 | + '%s is in manual mode, not dispatching.', |
1402 | self.builder.name) |
1403 | return |
1404 | |
1405 | @@ -292,22 +287,33 @@ |
1406 | job = self.builder.currentjob |
1407 | if job is not None and not self.builder.builderok: |
1408 | self.logger.info( |
1409 | - "%s was made unavailable, resetting attached " |
1410 | - "job" % self.builder.name) |
1411 | - job.reset() |
1412 | + "%s was made unavailable; resetting attached job.", |
1413 | + self.builder.name) |
1414 | transaction.commit() |
1415 | + with DatabaseTransactionPolicy(read_only=False): |
1416 | + job.reset() |
1417 | + transaction.commit() |
1418 | return |
1419 | |
1420 | # See if there is a job we can dispatch to the builder slave. |
1421 | |
1422 | + # XXX JeroenVermeulen 2011-10-11, bug=872112: The job's |
1423 | + # failure count will be reset once the job has started |
1424 | + # successfully. Because of intervening commits, you may see |
1425 | + # a build with a nonzero failure count that's actually going |
1426 | + # to succeed later (and have a failure count of zero). Or |
1427 | + # it may fail yet end up with a lower failure count than you |
1428 | + # saw earlier. |
1429 | d = self.builder.findAndStartJob() |
1430 | |
1431 | def job_started(candidate): |
1432 | if self.builder.currentjob is not None: |
1433 | # After a successful dispatch we can reset the |
1434 | # failure_count. |
1435 | - self.builder.resetFailureCount() |
1436 | transaction.commit() |
1437 | + with DatabaseTransactionPolicy(read_only=False): |
1438 | + self.builder.resetFailureCount() |
1439 | + transaction.commit() |
1440 | return self.builder.slave |
1441 | else: |
1442 | return None |
1443 | @@ -386,6 +392,7 @@ |
1444 | self.logger = self._setupLogger() |
1445 | self.new_builders_scanner = NewBuildersScanner( |
1446 | manager=self, clock=clock) |
1447 | + self.transaction_policy = DatabaseTransactionPolicy(read_only=True) |
1448 | |
1449 | def _setupLogger(self): |
1450 | """Set up a 'slave-scanner' logger that redirects to twisted. |
1451 | @@ -404,16 +411,28 @@ |
1452 | logger.setLevel(level) |
1453 | return logger |
1454 | |
1455 | + def enterReadOnlyDatabasePolicy(self): |
1456 | + """Set the database transaction policy to read-only. |
1457 | + |
1458 | + Any previously pending changes are committed first. |
1459 | + """ |
1460 | + transaction.commit() |
1461 | + self.transaction_policy.__enter__() |
1462 | + |
1463 | + def exitReadOnlyDatabasePolicy(self, *args): |
1464 | + """Reset database transaction policy to the default read-write.""" |
1465 | + self.transaction_policy.__exit__(None, None, None) |
1466 | + |
1467 | def startService(self): |
1468 | """Service entry point, called when the application starts.""" |
1469 | + # Avoiding circular imports. |
1470 | + from lp.buildmaster.interfaces.builder import IBuilderSet |
1471 | + |
1472 | + self.enterReadOnlyDatabasePolicy() |
1473 | |
1474 | # Get a list of builders and set up scanners on each one. |
1475 | - |
1476 | - # Avoiding circular imports. |
1477 | - from lp.buildmaster.interfaces.builder import IBuilderSet |
1478 | - builder_set = getUtility(IBuilderSet) |
1479 | - builders = [builder.name for builder in builder_set] |
1480 | - self.addScanForBuilders(builders) |
1481 | + self.addScanForBuilders( |
1482 | + [builder.name for builder in getUtility(IBuilderSet)]) |
1483 | self.new_builders_scanner.scheduleScan() |
1484 | |
1485 | # Events will now fire in the SlaveScanner objects to scan each |
1486 | @@ -434,6 +453,7 @@ |
1487 | # stopped, so we can wait on them all at once here before |
1488 | # exiting. |
1489 | d = defer.DeferredList(deferreds, consumeErrors=True) |
1490 | + d.addCallback(self.exitReadOnlyDatabasePolicy) |
1491 | return d |
1492 | |
1493 | def addScanForBuilders(self, builders): |
1494 | |
1495 | === modified file 'lib/lp/buildmaster/model/builder.py' |
1496 | --- lib/lp/buildmaster/model/builder.py 2012-01-09 13:42:03 +0000 |
1497 | +++ lib/lp/buildmaster/model/builder.py 2012-01-07 17:40:43 +0000 |
1498 | @@ -1,4 +1,4 @@ |
1499 | -# Copyright 2009,2011 Canonical Ltd. This software is licensed under the |
1500 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
1501 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1502 | |
1503 | # pylint: disable-msg=E0611,W0212 |
1504 | @@ -61,7 +61,6 @@ |
1505 | specific_job_classes, |
1506 | ) |
1507 | from lp.registry.interfaces.person import validate_public_person |
1508 | -<<<<<<< TREE |
1509 | from lp.services.config import config |
1510 | from lp.services.database.sqlbase import ( |
1511 | SQLBase, |
1512 | @@ -69,14 +68,6 @@ |
1513 | ) |
1514 | from lp.services.database.transaction_policy import DatabaseTransactionPolicy |
1515 | from lp.services.helpers import filenameToContentType |
1516 | -======= |
1517 | -from lp.services.config import config |
1518 | -from lp.services.database.sqlbase import ( |
1519 | - SQLBase, |
1520 | - sqlvalues, |
1521 | - ) |
1522 | -from lp.services.helpers import filenameToContentType |
1523 | ->>>>>>> MERGE-SOURCE |
1524 | from lp.services.job.interfaces.job import JobStatus |
1525 | from lp.services.job.model.job import Job |
1526 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
1527 | @@ -561,6 +552,8 @@ |
1528 | |
1529 | def setSlaveForTesting(self, proxy): |
1530 | """See IBuilder.""" |
1531 | + # XXX JeroenVermeulen 2011-11-09, bug=888010: Don't use this. |
1532 | + # It's a trap. See bug for details. |
1533 | self._testing_slave = proxy |
1534 | del get_property_cache(self).slave |
1535 | |
1536 | @@ -689,10 +682,13 @@ |
1537 | bytes_written = out_file.tell() |
1538 | out_file.seek(0) |
1539 | |
1540 | - library_file = getUtility(ILibraryFileAliasSet).create( |
1541 | - filename, bytes_written, out_file, |
1542 | - contentType=filenameToContentType(filename), |
1543 | - restricted=private) |
1544 | + transaction.commit() |
1545 | + with DatabaseTransactionPolicy(read_only=False): |
1546 | + library_file = getUtility(ILibraryFileAliasSet).create( |
1547 | + filename, bytes_written, out_file, |
1548 | + contentType=filenameToContentType(filename), |
1549 | + restricted=private) |
1550 | + transaction.commit() |
1551 | finally: |
1552 | # Remove the temporary file. getFile() closes the file |
1553 | # object. |
1554 | @@ -730,7 +726,7 @@ |
1555 | def acquireBuildCandidate(self): |
1556 | """Acquire a build candidate in an atomic fashion. |
1557 | |
1558 | - When retrieiving a candidate we need to mark it as building |
1559 | + When retrieving a candidate we need to mark it as building |
1560 | immediately so that it is not dispatched by another builder in the |
1561 | build manager. |
1562 | |
1563 | @@ -740,12 +736,15 @@ |
1564 | can be in this code at the same time. |
1565 | |
1566 | If there's ever more than one build manager running at once, then |
1567 | - this code will need some sort of mutex. |
1568 | + this code will need some sort of mutex, or run in a single |
1569 | + transaction. |
1570 | """ |
1571 | candidate = self._findBuildCandidate() |
1572 | if candidate is not None: |
1573 | - candidate.markAsBuilding(self) |
1574 | transaction.commit() |
1575 | + with DatabaseTransactionPolicy(read_only=False): |
1576 | + candidate.markAsBuilding(self) |
1577 | + transaction.commit() |
1578 | return candidate |
1579 | |
1580 | def _findBuildCandidate(self): |
1581 | @@ -808,13 +807,17 @@ |
1582 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
1583 | candidate_jobs = store.execute(query).get_all() |
1584 | |
1585 | - for (candidate_id,) in candidate_jobs: |
1586 | - candidate = getUtility(IBuildQueueSet).get(candidate_id) |
1587 | - job_class = job_classes[candidate.job_type] |
1588 | - candidate_approved = job_class.postprocessCandidate( |
1589 | - candidate, logger) |
1590 | - if candidate_approved: |
1591 | - return candidate |
1592 | + transaction.commit() |
1593 | + with DatabaseTransactionPolicy(read_only=False): |
1594 | + for (candidate_id,) in candidate_jobs: |
1595 | + candidate = getUtility(IBuildQueueSet).get(candidate_id) |
1596 | + job_class = job_classes[candidate.job_type] |
1597 | + candidate_approved = job_class.postprocessCandidate( |
1598 | + candidate, logger) |
1599 | + if candidate_approved: |
1600 | + transaction.commit() |
1601 | + return candidate |
1602 | + transaction.commit() |
1603 | |
1604 | return None |
1605 | |
1606 | |
1607 | === modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py' |
1608 | --- lib/lp/buildmaster/model/buildfarmjobbehavior.py 2012-01-09 13:42:03 +0000 |
1609 | +++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2012-01-03 12:11:57 +0000 |
1610 | @@ -1,4 +1,4 @@ |
1611 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
1612 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
1613 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1614 | |
1615 | # pylint: disable-msg=E0211,E0213 |
1616 | @@ -16,6 +16,7 @@ |
1617 | import socket |
1618 | import xmlrpclib |
1619 | |
1620 | +import transaction |
1621 | from twisted.internet import defer |
1622 | from zope.component import getUtility |
1623 | from zope.interface import implements |
1624 | @@ -30,6 +31,7 @@ |
1625 | IBuildFarmJobBehavior, |
1626 | ) |
1627 | from lp.services import encoding |
1628 | +from lp.services.database.transaction_policy import DatabaseTransactionPolicy |
1629 | from lp.services.job.interfaces.job import JobStatus |
1630 | from lp.services.librarian.interfaces.client import ILibrarianClient |
1631 | |
1632 | @@ -69,6 +71,25 @@ |
1633 | if slave_build_cookie != expected_cookie: |
1634 | raise CorruptBuildCookie("Invalid slave build cookie.") |
1635 | |
1636 | + def _getBuilderStatusHandler(self, status_text, logger): |
1637 | + """Look up the handler method for a given builder status. |
1638 | + |
1639 | + If status is not a known one, logs an error and returns None. |
1640 | + """ |
1641 | + builder_status_handlers = { |
1642 | + 'BuilderStatus.IDLE': self.updateBuild_IDLE, |
1643 | + 'BuilderStatus.BUILDING': self.updateBuild_BUILDING, |
1644 | + 'BuilderStatus.ABORTING': self.updateBuild_ABORTING, |
1645 | + 'BuilderStatus.ABORTED': self.updateBuild_ABORTED, |
1646 | + 'BuilderStatus.WAITING': self.updateBuild_WAITING, |
1647 | + } |
1648 | + handler = builder_status_handlers.get(status_text) |
1649 | + if handler is None: |
1650 | + logger.critical( |
1651 | + "Builder on %s returned unknown status %s; failing it.", |
1652 | + self._builder.url, status_text) |
1653 | + return handler |
1654 | + |
1655 | def updateBuild(self, queueItem): |
1656 | """See `IBuildFarmJobBehavior`.""" |
1657 | logger = logging.getLogger('slave-scanner') |
1658 | @@ -76,6 +97,7 @@ |
1659 | d = self._builder.slaveStatus() |
1660 | |
1661 | def got_failure(failure): |
1662 | + transaction.abort() |
1663 | failure.trap(xmlrpclib.Fault, socket.error) |
1664 | info = failure.value |
1665 | info = ("Could not contact the builder %s, caught a (%s)" |
1666 | @@ -83,27 +105,22 @@ |
1667 | raise BuildSlaveFailure(info) |
1668 | |
1669 | def got_status(slave_status): |
1670 | - builder_status_handlers = { |
1671 | - 'BuilderStatus.IDLE': self.updateBuild_IDLE, |
1672 | - 'BuilderStatus.BUILDING': self.updateBuild_BUILDING, |
1673 | - 'BuilderStatus.ABORTING': self.updateBuild_ABORTING, |
1674 | - 'BuilderStatus.ABORTED': self.updateBuild_ABORTED, |
1675 | - 'BuilderStatus.WAITING': self.updateBuild_WAITING, |
1676 | - } |
1677 | - |
1678 | builder_status = slave_status['builder_status'] |
1679 | - if builder_status not in builder_status_handlers: |
1680 | - logger.critical( |
1681 | - "Builder on %s returned unknown status %s, failing it" |
1682 | - % (self._builder.url, builder_status)) |
1683 | - self._builder.failBuilder( |
1684 | + status_handler = self._getBuilderStatusHandler( |
1685 | + builder_status, logger) |
1686 | + if status_handler is None: |
1687 | + error = ( |
1688 | "Unknown status code (%s) returned from status() probe." |
1689 | % builder_status) |
1690 | - # XXX: This will leave the build and job in a bad state, but |
1691 | - # should never be possible, since our builder statuses are |
1692 | - # known. |
1693 | - queueItem._builder = None |
1694 | - queueItem.setDateStarted(None) |
1695 | + transaction.commit() |
1696 | + with DatabaseTransactionPolicy(read_only=False): |
1697 | + self._builder.failBuilder(error) |
1698 | + # XXX: This will leave the build and job in a bad |
1699 | + # state, but should never be possible since our |
1700 | + # builder statuses are known. |
1701 | + queueItem._builder = None |
1702 | + queueItem.setDateStarted(None) |
1703 | + transaction.commit() |
1704 | return |
1705 | |
1706 | # Since logtail is a xmlrpclib.Binary container and it is |
1707 | @@ -113,9 +130,8 @@ |
1708 | # will simply remove the proxy. |
1709 | logtail = removeSecurityProxy(slave_status.get('logtail')) |
1710 | |
1711 | - method = builder_status_handlers[builder_status] |
1712 | return defer.maybeDeferred( |
1713 | - method, queueItem, slave_status, logtail, logger) |
1714 | + status_handler, queueItem, slave_status, logtail, logger) |
1715 | |
1716 | d.addErrback(got_failure) |
1717 | d.addCallback(got_status) |
1718 | @@ -127,22 +143,32 @@ |
1719 | Log this and reset the record. |
1720 | """ |
1721 | logger.warn( |
1722 | - "Builder %s forgot about buildqueue %d -- resetting buildqueue " |
1723 | - "record" % (queueItem.builder.url, queueItem.id)) |
1724 | - queueItem.reset() |
1725 | + "Builder %s forgot about buildqueue %d -- " |
1726 | + "resetting buildqueue record.", |
1727 | + queueItem.builder.url, queueItem.id) |
1728 | + transaction.commit() |
1729 | + with DatabaseTransactionPolicy(read_only=False): |
1730 | + queueItem.reset() |
1731 | + transaction.commit() |
1732 | |
1733 | def updateBuild_BUILDING(self, queueItem, slave_status, logtail, logger): |
1734 | """Build still building, collect the logtail""" |
1735 | - if queueItem.job.status != JobStatus.RUNNING: |
1736 | - queueItem.job.start() |
1737 | - queueItem.logtail = encoding.guess(str(logtail)) |
1738 | + transaction.commit() |
1739 | + with DatabaseTransactionPolicy(read_only=False): |
1740 | + if queueItem.job.status != JobStatus.RUNNING: |
1741 | + queueItem.job.start() |
1742 | + queueItem.logtail = encoding.guess(str(logtail)) |
1743 | + transaction.commit() |
1744 | |
1745 | def updateBuild_ABORTING(self, queueItem, slave_status, logtail, logger): |
1746 | """Build was ABORTED. |
1747 | |
1748 | Master-side should wait until the slave finish the process correctly. |
1749 | """ |
1750 | - queueItem.logtail = "Waiting for slave process to be terminated" |
1751 | + transaction.commit() |
1752 | + with DatabaseTransactionPolicy(read_only=False): |
1753 | + queueItem.logtail = "Waiting for slave process to be terminated" |
1754 | + transaction.commit() |
1755 | |
1756 | def updateBuild_ABORTED(self, queueItem, slave_status, logtail, logger): |
1757 | """ABORTING process has successfully terminated. |
1758 | @@ -150,11 +176,16 @@ |
1759 | Clean the builder for another jobs. |
1760 | """ |
1761 | d = queueItem.builder.cleanSlave() |
1762 | + |
1763 | def got_cleaned(ignored): |
1764 | - queueItem.builder = None |
1765 | - if queueItem.job.status != JobStatus.FAILED: |
1766 | - queueItem.job.fail() |
1767 | - queueItem.specific_job.jobAborted() |
1768 | + transaction.commit() |
1769 | + with DatabaseTransactionPolicy(read_only=False): |
1770 | + queueItem.builder = None |
1771 | + if queueItem.job.status != JobStatus.FAILED: |
1772 | + queueItem.job.fail() |
1773 | + queueItem.specific_job.jobAborted() |
1774 | + transaction.commit() |
1775 | + |
1776 | return d.addCallback(got_cleaned) |
1777 | |
1778 | def extractBuildStatus(self, slave_status): |
1779 | |
1780 | === modified file 'lib/lp/buildmaster/model/buildqueue.py' |
1781 | --- lib/lp/buildmaster/model/buildqueue.py 2012-01-09 13:42:03 +0000 |
1782 | +++ lib/lp/buildmaster/model/buildqueue.py 2012-01-04 16:39:15 +0000 |
1783 | @@ -53,7 +53,6 @@ |
1784 | ) |
1785 | from lp.services.job.interfaces.job import JobStatus |
1786 | from lp.services.job.model.job import Job |
1787 | -<<<<<<< TREE |
1788 | from lp.services.propertycache import ( |
1789 | cachedproperty, |
1790 | get_property_cache, |
1791 | @@ -63,13 +62,6 @@ |
1792 | IStoreSelector, |
1793 | MAIN_STORE, |
1794 | ) |
1795 | -======= |
1796 | -from lp.services.webapp.interfaces import ( |
1797 | - DEFAULT_FLAVOR, |
1798 | - IStoreSelector, |
1799 | - MAIN_STORE, |
1800 | - ) |
1801 | ->>>>>>> MERGE-SOURCE |
1802 | |
1803 | |
1804 | def normalize_virtualization(virtualized): |
1805 | @@ -156,7 +148,6 @@ |
1806 | specific_class = specific_job_classes()[self.job_type] |
1807 | return specific_class.getByJob(self.job) |
1808 | |
1809 | -<<<<<<< TREE |
1810 | def _clear_specific_job_cache(self): |
1811 | del get_property_cache(self).specific_job |
1812 | |
1813 | @@ -181,23 +172,6 @@ |
1814 | cache = get_property_cache(queue) |
1815 | cache.specific_job = specific_jobs_dict[queue.job] |
1816 | |
1817 | -======= |
1818 | - @staticmethod |
1819 | - def preloadSpecificJobData(queues): |
1820 | - key = attrgetter('job_type') |
1821 | - for job_type, grouped_queues in groupby(queues, key=key): |
1822 | - specific_class = specific_job_classes()[job_type] |
1823 | - queue_subset = list(grouped_queues) |
1824 | - # We need to preload the build farm jobs early to avoid |
1825 | - # the call to _set_build_farm_job to look up BuildFarmBuildJobs |
1826 | - # one by one. |
1827 | - specific_class.preloadBuildFarmJobs(queue_subset) |
1828 | - specific_jobs = specific_class.getByJobs(queue_subset) |
1829 | - if len(list(specific_jobs)) == 0: |
1830 | - continue |
1831 | - specific_class.preloadJobsData(specific_jobs) |
1832 | - |
1833 | ->>>>>>> MERGE-SOURCE |
1834 | @property |
1835 | def date_started(self): |
1836 | """See `IBuildQueue`.""" |
1837 | |
1838 | === modified file 'lib/lp/buildmaster/model/packagebuild.py' |
1839 | --- lib/lp/buildmaster/model/packagebuild.py 2012-01-09 13:42:03 +0000 |
1840 | +++ lib/lp/buildmaster/model/packagebuild.py 2012-01-06 08:24:33 +0000 |
1841 | @@ -1,4 +1,4 @@ |
1842 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
1843 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
1844 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1845 | |
1846 | __metaclass__ = type |
1847 | @@ -24,6 +24,7 @@ |
1848 | Storm, |
1849 | Unicode, |
1850 | ) |
1851 | +import transaction |
1852 | from zope.component import getUtility |
1853 | from zope.interface import ( |
1854 | classProvides, |
1855 | @@ -47,7 +48,6 @@ |
1856 | ) |
1857 | from lp.buildmaster.model.buildqueue import BuildQueue |
1858 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
1859 | -<<<<<<< TREE |
1860 | from lp.services.config import config |
1861 | from lp.services.database.enumcol import DBEnum |
1862 | from lp.services.database.lpstorm import IMasterStore |
1863 | @@ -60,19 +60,6 @@ |
1864 | IStoreSelector, |
1865 | MAIN_STORE, |
1866 | ) |
1867 | -======= |
1868 | -from lp.services.config import config |
1869 | -from lp.services.database.enumcol import DBEnum |
1870 | -from lp.services.database.lpstorm import IMasterStore |
1871 | -from lp.services.helpers import filenameToContentType |
1872 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
1873 | -from lp.services.librarian.interfaces import ILibraryFileAliasSet |
1874 | -from lp.services.webapp.interfaces import ( |
1875 | - DEFAULT_FLAVOR, |
1876 | - IStoreSelector, |
1877 | - MAIN_STORE, |
1878 | - ) |
1879 | ->>>>>>> MERGE-SOURCE |
1880 | from lp.soyuz.adapters.archivedependencies import ( |
1881 | default_component_dependency_name, |
1882 | ) |
1883 | @@ -192,19 +179,24 @@ |
1884 | def storeBuildInfo(build, librarian, slave_status): |
1885 | """See `IPackageBuild`.""" |
1886 | def got_log(lfa_id): |
1887 | + dependencies = slave_status.get('dependencies') |
1888 | + if dependencies is not None: |
1889 | + dependencies = unicode(dependencies) |
1890 | + |
1891 | # log, builder and date_finished are read-only, so we must |
1892 | # currently remove the security proxy to set them. |
1893 | naked_build = removeSecurityProxy(build) |
1894 | - naked_build.log = lfa_id |
1895 | - naked_build.builder = build.buildqueue_record.builder |
1896 | - # XXX cprov 20060615 bug=120584: Currently buildduration includes |
1897 | - # the scanner latency, it should really be asking the slave for |
1898 | - # the duration spent building locally. |
1899 | - naked_build.date_finished = datetime.datetime.now(pytz.UTC) |
1900 | - if slave_status.get('dependencies') is not None: |
1901 | - build.dependencies = unicode(slave_status.get('dependencies')) |
1902 | - else: |
1903 | - build.dependencies = None |
1904 | + |
1905 | + transaction.commit() |
1906 | + with DatabaseTransactionPolicy(read_only=False): |
1907 | + naked_build.log = lfa_id |
1908 | + naked_build.builder = build.buildqueue_record.builder |
1909 | + # XXX cprov 20060615 bug=120584: Currently buildduration |
1910 | + # includes the scanner latency. It should really be asking |
1911 | + # the slave for the duration spent building locally. |
1912 | + naked_build.date_finished = datetime.datetime.now(pytz.UTC) |
1913 | + build.dependencies = dependencies |
1914 | + transaction.commit() |
1915 | |
1916 | d = build.getLogFromSlave(build) |
1917 | return d.addCallback(got_log) |
1918 | @@ -306,22 +298,41 @@ |
1919 | |
1920 | def handleStatus(self, status, librarian, slave_status): |
1921 | """See `IPackageBuild`.""" |
1922 | + # Avoid circular imports. |
1923 | from lp.buildmaster.manager import BUILDD_MANAGER_LOG_NAME |
1924 | + |
1925 | logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME) |
1926 | send_notification = status in self.ALLOWED_STATUS_NOTIFICATIONS |
1927 | method = getattr(self, '_handleStatus_' + status, None) |
1928 | if method is None: |
1929 | - logger.critical("Unknown BuildStatus '%s' for builder '%s'" |
1930 | - % (status, self.buildqueue_record.builder.url)) |
1931 | - return |
1932 | + logger.critical( |
1933 | + "Unknown BuildStatus '%s' for builder '%s'", |
1934 | + status, self.buildqueue_record.builder.url) |
1935 | + return None |
1936 | + |
1937 | d = method(librarian, slave_status, logger, send_notification) |
1938 | return d |
1939 | |
1940 | + def _destroy_buildqueue_record(self, unused_arg): |
1941 | + """Destroy this build's `BuildQueue` record.""" |
1942 | + transaction.commit() |
1943 | + with DatabaseTransactionPolicy(read_only=False): |
1944 | + self.buildqueue_record.destroySelf() |
1945 | + transaction.commit() |
1946 | + |
1947 | def _release_builder_and_remove_queue_item(self): |
1948 | # Release the builder for another job. |
1949 | d = self.buildqueue_record.builder.cleanSlave() |
1950 | # Remove BuildQueue record. |
1951 | - return d.addCallback(lambda x: self.buildqueue_record.destroySelf()) |
1952 | + return d.addCallback(self._destroy_buildqueue_record) |
1953 | + |
1954 | + def _notify_if_appropriate(self, appropriate=True, extra_info=None): |
1955 | + """If `appropriate`, call `self.notify` in a write transaction.""" |
1956 | + if appropriate: |
1957 | + transaction.commit() |
1958 | + with DatabaseTransactionPolicy(read_only=False): |
1959 | + self.notify(extra_info=extra_info) |
1960 | + transaction.commit() |
1961 | |
1962 | def _handleStatus_OK(self, librarian, slave_status, logger, |
1963 | send_notification): |
1964 | @@ -337,16 +348,19 @@ |
1965 | self.buildqueue_record.specific_job.build.title, |
1966 | self.buildqueue_record.builder.name)) |
1967 | |
1968 | - # If this is a binary package build, discard it if its source is |
1969 | - # no longer published. |
1970 | + # If this is a binary package build for a source that is no |
1971 | + # longer published, discard it. |
1972 | if self.build_farm_job_type == BuildFarmJobType.PACKAGEBUILD: |
1973 | build = self.buildqueue_record.specific_job.build |
1974 | if not build.current_source_publication: |
1975 | - build.status = BuildStatus.SUPERSEDED |
1976 | + transaction.commit() |
1977 | + with DatabaseTransactionPolicy(read_only=False): |
1978 | + build.status = BuildStatus.SUPERSEDED |
1979 | + transaction.commit() |
1980 | return self._release_builder_and_remove_queue_item() |
1981 | |
1982 | - # Explode before collect a binary that is denied in this |
1983 | - # distroseries/pocket |
1984 | + # Explode rather than collect a binary that is denied in this |
1985 | + # distroseries/pocket. |
1986 | if not self.archive.allowUpdatesToReleasePocket(): |
1987 | assert self.distro_series.canUploadToPocket(self.pocket), ( |
1988 | "%s (%s) can not be built for pocket %s: illegal status" |
1989 | @@ -391,18 +405,26 @@ |
1990 | # files from the slave. |
1991 | if successful_copy_from_slave: |
1992 | logger.info( |
1993 | - "Gathered %s %d completely. Moving %s to uploader queue." |
1994 | - % (self.__class__.__name__, self.id, upload_leaf)) |
1995 | + "Gathered %s %d completely. " |
1996 | + "Moving %s to uploader queue.", |
1997 | + self.__class__.__name__, self.id, upload_leaf) |
1998 | target_dir = os.path.join(root, "incoming") |
1999 | - self.status = BuildStatus.UPLOADING |
2000 | + resulting_status = BuildStatus.UPLOADING |
2001 | else: |
2002 | logger.warning( |
2003 | - "Copy from slave for build %s was unsuccessful.", self.id) |
2004 | - self.status = BuildStatus.FAILEDTOUPLOAD |
2005 | - if send_notification: |
2006 | - self.notify( |
2007 | - extra_info='Copy from slave was unsuccessful.') |
2008 | + "Copy from slave for build %s was unsuccessful.", |
2009 | + self.id) |
2010 | target_dir = os.path.join(root, "failed") |
2011 | + resulting_status = BuildStatus.FAILEDTOUPLOAD |
2012 | + |
2013 | + transaction.commit() |
2014 | + with DatabaseTransactionPolicy(read_only=False): |
2015 | + self.status = resulting_status |
2016 | + transaction.commit() |
2017 | + |
2018 | + if not successful_copy_from_slave: |
2019 | + self._notify_if_appropriate( |
2020 | + send_notification, "Copy from slave was unsuccessful.") |
2021 | |
2022 | if not os.path.exists(target_dir): |
2023 | os.mkdir(target_dir) |
2024 | @@ -410,10 +432,6 @@ |
2025 | # Release the builder for another job. |
2026 | d = self._release_builder_and_remove_queue_item() |
2027 | |
2028 | - # Commit so there are no race conditions with archiveuploader |
2029 | - # about self.status. |
2030 | - Store.of(self).commit() |
2031 | - |
2032 | # Move the directory used to grab the binaries into |
2033 | # the incoming directory so the upload processor never |
2034 | # sees half-finished uploads. |
2035 | @@ -437,14 +455,15 @@ |
2036 | set the job status as FAILEDTOBUILD, store available info and |
2037 | remove Buildqueue entry. |
2038 | """ |
2039 | - self.status = BuildStatus.FAILEDTOBUILD |
2040 | + transaction.commit() |
2041 | + with DatabaseTransactionPolicy(read_only=False): |
2042 | + self.status = BuildStatus.FAILEDTOBUILD |
2043 | + transaction.commit() |
2044 | |
2045 | def build_info_stored(ignored): |
2046 | - if send_notification: |
2047 | - self.notify() |
2048 | + self._notify_if_appropriate(send_notification) |
2049 | d = self.buildqueue_record.builder.cleanSlave() |
2050 | - return d.addCallback( |
2051 | - lambda x: self.buildqueue_record.destroySelf()) |
2052 | + return d.addCallback(self._destroy_buildqueue_record) |
2053 | |
2054 | d = self.storeBuildInfo(self, librarian, slave_status) |
2055 | return d.addCallback(build_info_stored) |
2056 | @@ -464,11 +483,9 @@ |
2057 | def build_info_stored(ignored): |
2058 | logger.critical("***** %s is MANUALDEPWAIT *****" |
2059 | % self.buildqueue_record.builder.name) |
2060 | - if send_notification: |
2061 | - self.notify() |
2062 | + self._notify_if_appropriate(send_notification) |
2063 | d = self.buildqueue_record.builder.cleanSlave() |
2064 | - return d.addCallback( |
2065 | - lambda x: self.buildqueue_record.destroySelf()) |
2066 | + return d.addCallback(self._destroy_buildqueue_record) |
2067 | |
2068 | d = self.storeBuildInfo(self, librarian, slave_status) |
2069 | return d.addCallback(build_info_stored) |
2070 | @@ -486,17 +503,24 @@ |
2071 | transaction.commit() |
2072 | |
2073 | def build_info_stored(ignored): |
2074 | - logger.critical("***** %s is CHROOTWAIT *****" % |
2075 | - self.buildqueue_record.builder.name) |
2076 | - if send_notification: |
2077 | - self.notify() |
2078 | + logger.critical( |
2079 | + "***** %s is CHROOTWAIT *****", |
2080 | + self.buildqueue_record.builder.name) |
2081 | + |
2082 | + self._notify_if_appropriate(send_notification) |
2083 | d = self.buildqueue_record.builder.cleanSlave() |
2084 | - return d.addCallback( |
2085 | - lambda x: self.buildqueue_record.destroySelf()) |
2086 | + return d.addCallback(self._destroy_buildqueue_record) |
2087 | |
2088 | d = self.storeBuildInfo(self, librarian, slave_status) |
2089 | return d.addCallback(build_info_stored) |
2090 | |
2091 | + def _reset_buildqueue_record(self, ignored_arg=None): |
2092 | + """Reset the `BuildQueue` record, in a write transaction.""" |
2093 | + transaction.commit() |
2094 | + with DatabaseTransactionPolicy(read_only=False): |
2095 | + self.buildqueue_record.reset() |
2096 | + transaction.commit() |
2097 | + |
2098 | def _handleStatus_BUILDERFAIL(self, librarian, slave_status, logger, |
2099 | send_notification): |
2100 | """Handle builder failures. |
2101 | @@ -510,11 +534,8 @@ |
2102 | self.buildqueue_record.builder.failBuilder( |
2103 | "Builder returned BUILDERFAIL when asked for its status") |
2104 | |
2105 | - def build_info_stored(ignored): |
2106 | - # simply reset job |
2107 | - self.buildqueue_record.reset() |
2108 | d = self.storeBuildInfo(self, librarian, slave_status) |
2109 | - return d.addCallback(build_info_stored) |
2110 | + return d.addCallback(self._reset_buildqueue_record) |
2111 | |
2112 | def _handleStatus_GIVENBACK(self, librarian, slave_status, logger, |
2113 | send_notification): |
2114 | @@ -534,7 +555,7 @@ |
2115 | # the next Paris Summit, infinity has some ideas about how |
2116 | # to use this content. For now we just ensure it's stored. |
2117 | d = self.buildqueue_record.builder.cleanSlave() |
2118 | - self.buildqueue_record.reset() |
2119 | + self._reset_buildqueue_record() |
2120 | return d |
2121 | |
2122 | d = self.storeBuildInfo(self, librarian, slave_status) |
2123 | |
2124 | === modified file 'lib/lp/buildmaster/tests/test_builder.py' |
2125 | --- lib/lp/buildmaster/tests/test_builder.py 2012-01-09 13:42:03 +0000 |
2126 | +++ lib/lp/buildmaster/tests/test_builder.py 2012-01-03 13:44:35 +0000 |
2127 | @@ -1,4 +1,4 @@ |
2128 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
2129 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
2130 | # GNU Affero General Public License version 3 (see the file LICENSE). |
2131 | |
2132 | """Test Builder features.""" |
2133 | @@ -15,6 +15,7 @@ |
2134 | AsynchronousDeferredRunTestForBrokenTwisted, |
2135 | SynchronousDeferredRunTest, |
2136 | ) |
2137 | +import transaction |
2138 | from twisted.internet.defer import ( |
2139 | CancelledError, |
2140 | DeferredList, |
2141 | @@ -61,14 +62,9 @@ |
2142 | TrivialBehavior, |
2143 | WaitingSlave, |
2144 | ) |
2145 | -<<<<<<< TREE |
2146 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
2147 | from lp.services.config import config |
2148 | from lp.services.database.sqlbase import flush_database_updates |
2149 | -======= |
2150 | -from lp.services.config import config |
2151 | -from lp.services.database.sqlbase import flush_database_updates |
2152 | ->>>>>>> MERGE-SOURCE |
2153 | from lp.services.job.interfaces.job import JobStatus |
2154 | from lp.services.log.logger import BufferLogger |
2155 | from lp.services.webapp.interfaces import ( |
2156 | @@ -89,14 +85,7 @@ |
2157 | TestCaseWithFactory, |
2158 | ) |
2159 | from lp.testing.fakemethod import FakeMethod |
2160 | -<<<<<<< TREE |
2161 | from lp.testing.layers import LaunchpadZopelessLayer |
2162 | -======= |
2163 | -from lp.testing.layers import ( |
2164 | - DatabaseFunctionalLayer, |
2165 | - LaunchpadZopelessLayer, |
2166 | - ) |
2167 | ->>>>>>> MERGE-SOURCE |
2168 | |
2169 | |
2170 | class TestBuilderBasics(TestCaseWithFactory): |
2171 | @@ -181,7 +170,7 @@ |
2172 | d = lostbuilding_builder.updateStatus(BufferLogger()) |
2173 | def check_slave_status(failure): |
2174 | self.assertIn('abort', slave.call_log) |
2175 | - # 'Fault' comes from the LostBuildingBrokenSlave, this is |
2176 | + # 'Fault' comes from the LostBuildingBrokenSlave. This is |
2177 | # just testing that the value is passed through. |
2178 | self.assertIsInstance(failure.value, xmlrpclib.Fault) |
2179 | return d.addBoth(check_slave_status) |
2180 | @@ -583,7 +572,6 @@ |
2181 | # And the old_candidate is superseded: |
2182 | self.assertEqual(BuildStatus.SUPERSEDED, build.status) |
2183 | |
2184 | -<<<<<<< TREE |
2185 | def test_findBuildCandidate_postprocesses_in_read_write_policy(self): |
2186 | # _findBuildCandidate invokes BuildFarmJob.postprocessCandidate, |
2187 | # which may modify the database. This happens in a read-write |
2188 | @@ -602,8 +590,6 @@ |
2189 | # Passes without a "transaction is read-only" error... |
2190 | transaction.commit() |
2191 | |
2192 | -======= |
2193 | ->>>>>>> MERGE-SOURCE |
2194 | def test_acquireBuildCandidate_marks_building(self): |
2195 | # acquireBuildCandidate() should call _findBuildCandidate and |
2196 | # mark the build as building. |
2197 | |
2198 | === modified file 'lib/lp/buildmaster/tests/test_manager.py' |
2199 | --- lib/lp/buildmaster/tests/test_manager.py 2012-01-09 13:42:03 +0000 |
2200 | +++ lib/lp/buildmaster/tests/test_manager.py 2012-01-03 13:44:35 +0000 |
2201 | @@ -1,8 +1,9 @@ |
2202 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
2203 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
2204 | # GNU Affero General Public License version 3 (see the file LICENSE). |
2205 | |
2206 | """Tests for the renovated slave scanner aka BuilddManager.""" |
2207 | |
2208 | +from collections import namedtuple |
2209 | import os |
2210 | import signal |
2211 | import time |
2212 | @@ -33,25 +34,19 @@ |
2213 | SlaveScanner, |
2214 | ) |
2215 | from lp.buildmaster.model.builder import Builder |
2216 | -<<<<<<< TREE |
2217 | from lp.buildmaster.model.packagebuild import PackageBuild |
2218 | from lp.buildmaster.testing import BuilddManagerTestFixture |
2219 | -======= |
2220 | ->>>>>>> MERGE-SOURCE |
2221 | from lp.buildmaster.tests.harness import BuilddManagerTestSetup |
2222 | from lp.buildmaster.tests.mock_slaves import ( |
2223 | BrokenSlave, |
2224 | BuildingSlave, |
2225 | make_publisher, |
2226 | OkSlave, |
2227 | + WaitingSlave, |
2228 | ) |
2229 | from lp.registry.interfaces.distribution import IDistributionSet |
2230 | -<<<<<<< TREE |
2231 | from lp.services.config import config |
2232 | from lp.services.database.constants import UTC_NOW |
2233 | -======= |
2234 | -from lp.services.config import config |
2235 | ->>>>>>> MERGE-SOURCE |
2236 | from lp.services.log.logger import BufferLogger |
2237 | from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet |
2238 | from lp.testing import ( |
2239 | @@ -62,7 +57,6 @@ |
2240 | ) |
2241 | from lp.testing.factory import LaunchpadObjectFactory |
2242 | from lp.testing.fakemethod import FakeMethod |
2243 | -<<<<<<< TREE |
2244 | from lp.testing.layers import ( |
2245 | LaunchpadScriptLayer, |
2246 | LaunchpadZopelessLayer, |
2247 | @@ -71,17 +65,10 @@ |
2248 | from lp.testing.sampledata import ( |
2249 | BOB_THE_BUILDER_NAME, |
2250 | FROG_THE_BUILDER_NAME, |
2251 | -======= |
2252 | -from lp.testing.layers import ( |
2253 | - LaunchpadScriptLayer, |
2254 | - LaunchpadZopelessLayer, |
2255 | - ZopelessDatabaseLayer, |
2256 | ->>>>>>> MERGE-SOURCE |
2257 | ) |
2258 | -from lp.testing.sampledata import BOB_THE_BUILDER_NAME |
2259 | - |
2260 | - |
2261 | -class TestSlaveScannerScan(TestCase): |
2262 | + |
2263 | + |
2264 | +class TestSlaveScannerScan(TestCaseWithFactory): |
2265 | """Tests `SlaveScanner.scan` method. |
2266 | |
2267 | This method uses the old framework for scanning and dispatching builds. |
2268 | @@ -103,11 +90,8 @@ |
2269 | test_publisher.setUpDefaultDistroSeries(hoary) |
2270 | test_publisher.addFakeChroots() |
2271 | |
2272 | -<<<<<<< TREE |
2273 | self.useFixture(BuilddManagerTestFixture()) |
2274 | |
2275 | -======= |
2276 | ->>>>>>> MERGE-SOURCE |
2277 | def _resetBuilder(self, builder): |
2278 | """Reset the given builder and its job.""" |
2279 | builder.builderok = True |
2280 | @@ -115,7 +99,6 @@ |
2281 | if job is not None: |
2282 | job.reset() |
2283 | |
2284 | -<<<<<<< TREE |
2285 | def getFreshBuilder(self, slave=None, name=BOB_THE_BUILDER_NAME, |
2286 | failure_count=0): |
2287 | """Return a builder. |
2288 | @@ -133,10 +116,6 @@ |
2289 | builder.failure_count = failure_count |
2290 | return builder |
2291 | |
2292 | -======= |
2293 | - transaction.commit() |
2294 | - |
2295 | ->>>>>>> MERGE-SOURCE |
2296 | def assertBuildingJob(self, job, builder, logtail=None): |
2297 | """Assert the given job is building on the given builder.""" |
2298 | from lp.services.job.interfaces.job import JobStatus |
2299 | @@ -151,14 +130,14 @@ |
2300 | self.assertEqual(build.status, BuildStatus.BUILDING) |
2301 | self.assertEqual(job.logtail, logtail) |
2302 | |
2303 | - def _getScanner(self, builder_name=None): |
2304 | + def _getScanner(self, builder_name=None, clock=None): |
2305 | """Instantiate a SlaveScanner object. |
2306 | |
2307 | Replace its default logging handler by a testing version. |
2308 | """ |
2309 | if builder_name is None: |
2310 | builder_name = BOB_THE_BUILDER_NAME |
2311 | - scanner = SlaveScanner(builder_name, BufferLogger()) |
2312 | + scanner = SlaveScanner(builder_name, BufferLogger(), clock=clock) |
2313 | scanner.logger.name = 'slave-scanner' |
2314 | |
2315 | return scanner |
2316 | @@ -174,21 +153,11 @@ |
2317 | def testScanDispatchForResetBuilder(self): |
2318 | # A job gets dispatched to the sampledata builder after it's reset. |
2319 | |
2320 | -<<<<<<< TREE |
2321 | # Obtain a builder. Initialize failure count to 1 so that |
2322 | # _checkDispatch can make sure that a successful dispatch resets |
2323 | # the count to 0. |
2324 | with BuilddManagerTestFixture.extraSetUp(): |
2325 | builder = self.getFreshBuilder(failure_count=1) |
2326 | -======= |
2327 | - # Reset sampledata builder. |
2328 | - builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME] |
2329 | - self._resetBuilder(builder) |
2330 | - builder.setSlaveForTesting(OkSlave()) |
2331 | - # Set this to 1 here so that _checkDispatch can make sure it's |
2332 | - # reset to 0 after a successful dispatch. |
2333 | - builder.failure_count = 1 |
2334 | ->>>>>>> MERGE-SOURCE |
2335 | |
2336 | # Run 'scan' and check its result. |
2337 | self.layer.switchDbUser(config.builddmaster.dbuser) |
2338 | @@ -204,17 +173,16 @@ |
2339 | to the asynchonous dispatcher and the builder remained active |
2340 | and IDLE. |
2341 | """ |
2342 | - self.assertTrue(slave is None, "Unexpected slave.") |
2343 | + self.assertIs(None, slave, "Unexpected slave.") |
2344 | |
2345 | builder = getUtility(IBuilderSet).get(builder.id) |
2346 | self.assertTrue(builder.builderok) |
2347 | - self.assertTrue(builder.currentjob is None) |
2348 | + self.assertIs(None, builder.currentjob) |
2349 | |
2350 | def testNoDispatchForMissingChroots(self): |
2351 | # When a required chroot is not present the `scan` method |
2352 | # should not return any `RecordingSlaves` to be processed |
2353 | # and the builder used should remain active and IDLE. |
2354 | -<<<<<<< TREE |
2355 | with BuilddManagerTestFixture.extraSetUp(): |
2356 | builder = self.getFreshBuilder() |
2357 | # Remove hoary/i386 chroot. |
2358 | @@ -225,20 +193,6 @@ |
2359 | hoary.getDistroArchSeries('i386').getPocketChroot()) |
2360 | removeSecurityProxy(pocket_chroot).chroot = None |
2361 | |
2362 | -======= |
2363 | - |
2364 | - # Reset sampledata builder. |
2365 | - builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME] |
2366 | - self._resetBuilder(builder) |
2367 | - |
2368 | - # Remove hoary/i386 chroot. |
2369 | - login('foo.bar@canonical.com') |
2370 | - ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
2371 | - hoary = ubuntu.getSeries('hoary') |
2372 | - pocket_chroot = hoary.getDistroArchSeries('i386').getPocketChroot() |
2373 | - removeSecurityProxy(pocket_chroot).chroot = None |
2374 | - transaction.commit() |
2375 | ->>>>>>> MERGE-SOURCE |
2376 | login(ANONYMOUS) |
2377 | |
2378 | # Run 'scan' and check its result. |
2379 | @@ -325,31 +279,18 @@ |
2380 | return d |
2381 | |
2382 | def test_scan_with_nothing_to_dispatch(self): |
2383 | -<<<<<<< TREE |
2384 | with BuilddManagerTestFixture.extraSetUp(): |
2385 | builder = self.factory.makeBuilder() |
2386 | builder.setSlaveForTesting(OkSlave()) |
2387 | -======= |
2388 | - factory = LaunchpadObjectFactory() |
2389 | - builder = factory.makeBuilder() |
2390 | - builder.setSlaveForTesting(OkSlave()) |
2391 | ->>>>>>> MERGE-SOURCE |
2392 | scanner = self._getScanner(builder_name=builder.name) |
2393 | d = scanner.scan() |
2394 | return d.addCallback(self._checkNoDispatch, builder) |
2395 | |
2396 | def test_scan_with_manual_builder(self): |
2397 | # Reset sampledata builder. |
2398 | -<<<<<<< TREE |
2399 | with BuilddManagerTestFixture.extraSetUp(): |
2400 | builder = self.getFreshBuilder() |
2401 | builder.manual = True |
2402 | -======= |
2403 | - builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME] |
2404 | - self._resetBuilder(builder) |
2405 | - builder.setSlaveForTesting(OkSlave()) |
2406 | - builder.manual = True |
2407 | ->>>>>>> MERGE-SOURCE |
2408 | scanner = self._getScanner() |
2409 | d = scanner.scan() |
2410 | d.addCallback(self._checkNoDispatch, builder) |
2411 | @@ -357,16 +298,9 @@ |
2412 | |
2413 | def test_scan_with_not_ok_builder(self): |
2414 | # Reset sampledata builder. |
2415 | -<<<<<<< TREE |
2416 | with BuilddManagerTestFixture.extraSetUp(): |
2417 | builder = self.getFreshBuilder() |
2418 | builder.builderok = False |
2419 | -======= |
2420 | - builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME] |
2421 | - self._resetBuilder(builder) |
2422 | - builder.setSlaveForTesting(OkSlave()) |
2423 | - builder.builderok = False |
2424 | ->>>>>>> MERGE-SOURCE |
2425 | scanner = self._getScanner() |
2426 | d = scanner.scan() |
2427 | # Because the builder is not ok, we can't use _checkNoDispatch. |
2428 | @@ -375,41 +309,29 @@ |
2429 | return d |
2430 | |
2431 | def test_scan_of_broken_slave(self): |
2432 | -<<<<<<< TREE |
2433 | with BuilddManagerTestFixture.extraSetUp(): |
2434 | builder = self.getFreshBuilder(slave=BrokenSlave()) |
2435 | -======= |
2436 | - builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME] |
2437 | - self._resetBuilder(builder) |
2438 | - builder.setSlaveForTesting(BrokenSlave()) |
2439 | - builder.failure_count = 0 |
2440 | ->>>>>>> MERGE-SOURCE |
2441 | scanner = self._getScanner(builder_name=builder.name) |
2442 | d = scanner.scan() |
2443 | return assert_fails_with(d, xmlrpclib.Fault) |
2444 | |
2445 | def _assertFailureCounting(self, builder_count, job_count, |
2446 | expected_builder_count, expected_job_count): |
2447 | + # Avoid circular imports. |
2448 | + from lp.buildmaster import manager as manager_module |
2449 | + |
2450 | # If scan() fails with an exception, failure_counts should be |
2451 | # incremented. What we do with the results of the failure |
2452 | # counts is tested below separately, this test just makes sure that |
2453 | # scan() is setting the counts. |
2454 | def failing_scan(): |
2455 | return defer.fail(Exception("fake exception")) |
2456 | -<<<<<<< TREE |
2457 | |
2458 | with BuilddManagerTestFixture.extraSetUp(): |
2459 | scanner = self._getScanner() |
2460 | scanner.scan = failing_scan |
2461 | self.patch(manager_module, 'assessFailureCounts', FakeMethod()) |
2462 | builder = getUtility(IBuilderSet)[scanner.builder_name] |
2463 | -======= |
2464 | - scanner = self._getScanner() |
2465 | - scanner.scan = failing_scan |
2466 | - from lp.buildmaster import manager as manager_module |
2467 | - self.patch(manager_module, 'assessFailureCounts', FakeMethod()) |
2468 | - builder = getUtility(IBuilderSet)[scanner.builder_name] |
2469 | ->>>>>>> MERGE-SOURCE |
2470 | |
2471 | builder.failure_count = builder_count |
2472 | builder.currentjob.specific_job.build.failure_count = job_count |
2473 | @@ -558,7 +480,6 @@ |
2474 | d.addCallback(check_cancelled, builder, buildqueue) |
2475 | return d |
2476 | |
2477 | -<<<<<<< TREE |
2478 | def makeFakeFailure(self): |
2479 | """Produce a fake failure for use with SlaveScanner._scanFailed.""" |
2480 | FakeFailure = namedtuple('FakeFailure', ['getErrorMessage', 'check']) |
2481 | @@ -616,8 +537,6 @@ |
2482 | # The work done by the broken scanner is rolled back. |
2483 | self.assertEqual(original_broken_builder_title, broken_builder.title) |
2484 | |
2485 | -======= |
2486 | ->>>>>>> MERGE-SOURCE |
2487 | |
2488 | class TestCancellationChecking(TestCaseWithFactory): |
2489 | """Unit tests for the checkCancellation method.""" |
2490 | |
2491 | === modified file 'lib/lp/buildmaster/tests/test_packagebuild.py' |
2492 | --- lib/lp/buildmaster/tests/test_packagebuild.py 2012-01-09 13:42:03 +0000 |
2493 | +++ lib/lp/buildmaster/tests/test_packagebuild.py 2012-01-06 08:27:55 +0000 |
2494 | @@ -1,4 +1,4 @@ |
2495 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
2496 | +# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
2497 | # GNU Affero General Public License version 3 (see the file LICENSE). |
2498 | |
2499 | """Tests for `IPackageBuild`.""" |
2500 | @@ -27,6 +27,7 @@ |
2501 | IPackageBuildSet, |
2502 | IPackageBuildSource, |
2503 | ) |
2504 | +from lp.buildmaster.model.builder import BuilderSlave |
2505 | from lp.buildmaster.model.buildfarmjob import BuildFarmJob |
2506 | from lp.buildmaster.model.packagebuild import PackageBuild |
2507 | from lp.buildmaster.testing import BuilddManagerTestFixture |
2508 | @@ -283,10 +284,7 @@ |
2509 | |
2510 | |
2511 | class TestHandleStatusMixin: |
2512 | - """Tests for `IPackageBuild`s handleStatus method. |
2513 | - |
2514 | - This should be run with a Trial TestCase. |
2515 | - """ |
2516 | + """Tests for `IPackageBuild`s handleStatus method.""" |
2517 | |
2518 | layer = LaunchpadZopelessLayer |
2519 | run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20) |
2520 | @@ -306,7 +304,7 @@ |
2521 | self.build.buildqueue_record.setDateStarted(UTC_NOW) |
2522 | self.slave = WaitingSlave('BuildStatus.OK') |
2523 | self.slave.valid_file_hashes.append('test_file_hash') |
2524 | - builder.setSlaveForTesting(self.slave) |
2525 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(self.slave)) |
2526 | |
2527 | # We overwrite the buildmaster root to use a temp directory. |
2528 | tempdir = tempfile.mkdtemp() |
2529 | @@ -349,7 +347,7 @@ |
2530 | def got_status(ignored): |
2531 | self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status) |
2532 | self.assertResultCount(0, "failed") |
2533 | - self.assertIdentical(None, self.build.buildqueue_record) |
2534 | + self.assertIs(None, self.build.buildqueue_record) |
2535 | |
2536 | d = self.build.handleStatus('OK', None, { |
2537 | 'filemap': {'/tmp/myfile.py': 'test_file_hash'}, |
2538 | @@ -392,14 +390,10 @@ |
2539 | |
2540 | def got_status(ignored): |
2541 | if expected_notification: |
2542 | - self.failIf( |
2543 | - len(pop_notifications()) == 0, |
2544 | - "No notifications received") |
2545 | + self.assertNotEqual( |
2546 | + 0, len(pop_notifications()), "No notifications received.") |
2547 | else: |
2548 | - self.failIf( |
2549 | - len(pop_notifications()) > 0, |
2550 | - "Notifications received") |
2551 | - |
2552 | + self.assertContentEqual([], pop_notifications()) |
2553 | d = self.build.handleStatus(status, None, {}) |
2554 | return d.addCallback(got_status) |
2555 | |
2556 | |
2557 | === modified file 'lib/lp/code/browser/sourcepackagerecipebuild.py' |
2558 | --- lib/lp/code/browser/sourcepackagerecipebuild.py 2012-01-09 13:42:03 +0000 |
2559 | +++ lib/lp/code/browser/sourcepackagerecipebuild.py 2012-01-04 20:16:10 +0000 |
2560 | @@ -25,7 +25,6 @@ |
2561 | ISourcePackageRecipeBuild, |
2562 | ) |
2563 | from lp.services.job.interfaces.job import JobStatus |
2564 | -<<<<<<< TREE |
2565 | from lp.services.librarian.browser import FileNavigationMixin |
2566 | from lp.services.propertycache import ( |
2567 | cachedproperty, |
2568 | @@ -38,18 +37,6 @@ |
2569 | Link, |
2570 | Navigation, |
2571 | ) |
2572 | -======= |
2573 | -from lp.services.librarian.browser import FileNavigationMixin |
2574 | -from lp.services.propertycache import cachedproperty |
2575 | -from lp.services.webapp import ( |
2576 | - canonical_url, |
2577 | - ContextMenu, |
2578 | - enabled_with_permission, |
2579 | - LaunchpadView, |
2580 | - Link, |
2581 | - Navigation, |
2582 | - ) |
2583 | ->>>>>>> MERGE-SOURCE |
2584 | |
2585 | |
2586 | UNEDITABLE_BUILD_STATES = ( |
2587 | |
2588 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' |
2589 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-01-09 13:42:03 +0000 |
2590 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-01-06 08:24:33 +0000 |
2591 | @@ -38,7 +38,6 @@ |
2592 | from lp.registry.interfaces.person import TeamSubscriptionPolicy |
2593 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
2594 | from lp.registry.interfaces.series import SeriesStatus |
2595 | -<<<<<<< TREE |
2596 | from lp.services.database.constants import UTC_NOW |
2597 | from lp.services.propertycache import ( |
2598 | clear_property_cache, |
2599 | @@ -46,13 +45,6 @@ |
2600 | from lp.services.webapp import canonical_url |
2601 | from lp.services.webapp.interfaces import ILaunchpadRoot |
2602 | from lp.services.webapp.servers import LaunchpadTestRequest |
2603 | -======= |
2604 | -from lp.services.database.constants import UTC_NOW |
2605 | -from lp.services.propertycache import clear_property_cache |
2606 | -from lp.services.webapp import canonical_url |
2607 | -from lp.services.webapp.interfaces import ILaunchpadRoot |
2608 | -from lp.services.webapp.servers import LaunchpadTestRequest |
2609 | ->>>>>>> MERGE-SOURCE |
2610 | from lp.soyuz.model.processor import ProcessorFamily |
2611 | from lp.testing import ( |
2612 | ANONYMOUS, |
2613 | |
2614 | === modified file 'lib/lp/code/model/sourcepackagerecipebuild.py' |
2615 | --- lib/lp/code/model/sourcepackagerecipebuild.py 2012-01-09 13:42:03 +0000 |
2616 | +++ lib/lp/code/model/sourcepackagerecipebuild.py 2012-01-04 20:12:28 +0000 |
2617 | @@ -287,7 +287,6 @@ |
2618 | PackageBuild.build_farm_job_id == build_farm_job.id).one() |
2619 | |
2620 | @classmethod |
2621 | -<<<<<<< TREE |
2622 | def preloadBuildsData(cls, builds): |
2623 | # Circular imports. |
2624 | from lp.code.model.sourcepackagerecipe import SourcePackageRecipe |
2625 | @@ -306,20 +305,6 @@ |
2626 | SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs) |
2627 | |
2628 | @classmethod |
2629 | -======= |
2630 | - def preloadBuildsData(cls, builds): |
2631 | - # Circular imports. |
2632 | - from lp.code.model.sourcepackagerecipe import SourcePackageRecipe |
2633 | - package_builds = load_related( |
2634 | - PackageBuild, builds, ['package_build_id']) |
2635 | - archives = load_related(Archive, package_builds, ['archive_id']) |
2636 | - load_related(Person, archives, ['ownerID']) |
2637 | - sprs = load_related( |
2638 | - SourcePackageRecipe, builds, ['recipe_id']) |
2639 | - SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs) |
2640 | - |
2641 | - @classmethod |
2642 | ->>>>>>> MERGE-SOURCE |
2643 | def getByBuildFarmJobs(cls, build_farm_jobs): |
2644 | """See `ISpecificBuildFarmJobSource`.""" |
2645 | if len(build_farm_jobs) == 0: |
2646 | |
2647 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py' |
2648 | --- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2012-01-09 13:42:03 +0000 |
2649 | +++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2012-01-03 12:11:57 +0000 |
2650 | @@ -13,14 +13,15 @@ |
2651 | |
2652 | from pytz import utc |
2653 | from storm.locals import Store |
2654 | +from testtools.deferredruntest import AsynchronousDeferredRunTest |
2655 | import transaction |
2656 | -from twisted.trial.unittest import TestCase as TrialTestCase |
2657 | from zope.component import getUtility |
2658 | from zope.security.proxy import removeSecurityProxy |
2659 | |
2660 | from lp.app.errors import NotFoundError |
2661 | from lp.buildmaster.enums import BuildStatus |
2662 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
2663 | +from lp.buildmaster.model.builder import BuilderSlave |
2664 | from lp.buildmaster.model.buildfarmjob import BuildFarmJob |
2665 | from lp.buildmaster.model.packagebuild import PackageBuild |
2666 | from lp.buildmaster.tests.mock_slaves import WaitingSlave |
2667 | @@ -588,14 +589,11 @@ |
2668 | self.assertEquals(0, len(notifications)) |
2669 | |
2670 | |
2671 | -class TestBuildNotifications(TrialTestCase): |
2672 | +class TestBuildNotifications(TestCaseWithFactory): |
2673 | |
2674 | layer = LaunchpadZopelessLayer |
2675 | |
2676 | - def setUp(self): |
2677 | - super(TestBuildNotifications, self).setUp() |
2678 | - from lp.testing.factory import LaunchpadObjectFactory |
2679 | - self.factory = LaunchpadObjectFactory() |
2680 | + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20) |
2681 | |
2682 | def prepare_build(self, fake_successful_upload=False): |
2683 | queue_record = self.factory.makeSourcePackageRecipeBuildJob() |
2684 | @@ -608,7 +606,7 @@ |
2685 | result=True) |
2686 | queue_record.builder = self.factory.makeBuilder() |
2687 | slave = WaitingSlave('BuildStatus.OK') |
2688 | - queue_record.builder.setSlaveForTesting(slave) |
2689 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave)) |
2690 | return build |
2691 | |
2692 | def assertDeferredNotifyCount(self, status, build, expected_count): |
2693 | @@ -666,5 +664,5 @@ |
2694 | |
2695 | |
2696 | class TestHandleStatusForSPRBuild( |
2697 | - MakeSPRecipeBuildMixin, TestHandleStatusMixin, TrialTestCase): |
2698 | + MakeSPRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory): |
2699 | """IPackageBuild.handleStatus works with SPRecipe builds.""" |
2700 | |
2701 | === modified file 'lib/lp/code/scripts/tests/test_revisionkarma.py' |
2702 | --- lib/lp/code/scripts/tests/test_revisionkarma.py 2012-01-09 13:42:03 +0000 |
2703 | +++ lib/lp/code/scripts/tests/test_revisionkarma.py 2012-01-05 00:15:32 +0000 |
2704 | @@ -1,8 +1,4 @@ |
2705 | -<<<<<<< TREE |
2706 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
2707 | -======= |
2708 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
2709 | ->>>>>>> MERGE-SOURCE |
2710 | # GNU Affero General Public License version 3 (see the file LICENSE). |
2711 | |
2712 | """Tests for the cron script that updates revision karma.""" |
2713 | |
2714 | === modified file 'lib/lp/hardwaredb/doc/hwdb.txt' |
2715 | --- lib/lp/hardwaredb/doc/hwdb.txt 2012-01-09 13:42:03 +0000 |
2716 | +++ lib/lp/hardwaredb/doc/hwdb.txt 2012-01-09 13:42:13 +0000 |
2717 | @@ -109,7 +109,7 @@ |
2718 | Limitations: |
2719 | * "No name" products like mainboards from companies like ASRock |
2720 | or Asus that are directly sold to end users have fingerprints like |
2721 | - "American Megatrends Inc. Uknown 1.0". |
2722 | + "American Megatrends Inc. Unknown 1.0". |
2723 | * A manufacturer may erroneously assign identical DMI values for product |
2724 | and vendor to different systems. |
2725 | * submissions for "counterfeit systems". |
2726 | @@ -743,4 +743,3 @@ |
2727 | >>> set(submission.owner.name for submission |
2728 | ... in owner.hardware_submissions) |
2729 | set([u'name12']) |
2730 | - |
2731 | |
2732 | === modified file 'lib/lp/registry/browser/__init__.py' |
2733 | --- lib/lp/registry/browser/__init__.py 2012-01-09 13:42:03 +0000 |
2734 | +++ lib/lp/registry/browser/__init__.py 2012-01-05 20:11:40 +0000 |
2735 | @@ -31,7 +31,6 @@ |
2736 | ) |
2737 | from lp.registry.interfaces.productseries import IProductSeries |
2738 | from lp.registry.interfaces.series import SeriesStatus |
2739 | -<<<<<<< TREE |
2740 | from lp.app.browser.launchpadform import ( |
2741 | action, |
2742 | LaunchpadEditFormView, |
2743 | @@ -40,16 +39,6 @@ |
2744 | canonical_url, |
2745 | LaunchpadView, |
2746 | ) |
2747 | -======= |
2748 | -from lp.services.webapp.launchpadform import ( |
2749 | - action, |
2750 | - LaunchpadEditFormView, |
2751 | - ) |
2752 | -from lp.services.webapp.publisher import ( |
2753 | - canonical_url, |
2754 | - LaunchpadView, |
2755 | - ) |
2756 | ->>>>>>> MERGE-SOURCE |
2757 | |
2758 | |
2759 | class StatusCount: |
2760 | |
2761 | === modified file 'lib/lp/registry/browser/branding.py' |
2762 | --- lib/lp/registry/browser/branding.py 2012-01-09 13:42:03 +0000 |
2763 | +++ lib/lp/registry/browser/branding.py 2012-01-05 20:11:40 +0000 |
2764 | @@ -9,21 +9,13 @@ |
2765 | 'BrandingChangeView', |
2766 | ] |
2767 | |
2768 | -<<<<<<< TREE |
2769 | from lp.app.widgets.image import ImageChangeWidget |
2770 | from lp.app.browser.launchpadform import ( |
2771 | -======= |
2772 | -from lp.app.widgets.image import ImageChangeWidget |
2773 | -from lp.services.webapp import ( |
2774 | ->>>>>>> MERGE-SOURCE |
2775 | action, |
2776 | custom_widget, |
2777 | LaunchpadEditFormView, |
2778 | ) |
2779 | -<<<<<<< TREE |
2780 | from lp.services.webapp import canonical_url |
2781 | -======= |
2782 | ->>>>>>> MERGE-SOURCE |
2783 | |
2784 | |
2785 | class BrandingChangeView(LaunchpadEditFormView): |
2786 | |
2787 | === modified file 'lib/lp/registry/browser/configure.zcml' |
2788 | --- lib/lp/registry/browser/configure.zcml 2011-12-24 17:49:30 +0000 |
2789 | +++ lib/lp/registry/browser/configure.zcml 2012-01-09 13:42:13 +0000 |
2790 | @@ -1279,14 +1279,14 @@ |
2791 | MilestoneNavigation"/> |
2792 | <adapter |
2793 | provides="lp.services.webapp.interfaces.IBreadcrumb" |
2794 | - for="lp.registry.interfaces.milestone.IMilestone" |
2795 | + for="lp.registry.interfaces.milestone.IMilestoneData" |
2796 | factory="lp.registry.browser.milestone.MilestoneBreadcrumb" |
2797 | permission="zope.Public"/> |
2798 | <browser:defaultView |
2799 | - for="lp.registry.interfaces.milestone.IMilestone" |
2800 | + for="lp.registry.interfaces.milestone.IMilestoneData" |
2801 | name="+index"/> |
2802 | <browser:url |
2803 | - for="lp.registry.interfaces.milestone.IMilestone" |
2804 | + for="lp.registry.interfaces.milestone.IMilestoneData" |
2805 | path_expression="string:+milestone/${name}" |
2806 | rootsite="mainsite" |
2807 | attribute_to_parent="target"/> |
2808 | @@ -1297,7 +1297,7 @@ |
2809 | template="../templates/milestone-macros.pt" |
2810 | class="lp.app.browser.launchpad.Macro"/> |
2811 | <browser:pages |
2812 | - for="lp.registry.interfaces.milestone.IMilestone" |
2813 | + for="lp.registry.interfaces.milestone.IMilestoneData" |
2814 | class="lp.registry.browser.milestone.MilestoneView" |
2815 | permission="zope.Public"> |
2816 | <browser:page |
2817 | |
2818 | === modified file 'lib/lp/registry/browser/distributionsourcepackage.py' |
2819 | --- lib/lp/registry/browser/distributionsourcepackage.py 2012-01-09 13:42:03 +0000 |
2820 | +++ lib/lp/registry/browser/distributionsourcepackage.py 2012-01-05 20:11:40 +0000 |
2821 | @@ -39,13 +39,10 @@ |
2822 | QuestionTargetTraversalMixin, |
2823 | ) |
2824 | from lp.answers.enums import QuestionStatus |
2825 | -<<<<<<< TREE |
2826 | from lp.app.browser.launchpadform import ( |
2827 | action, |
2828 | LaunchpadEditFormView, |
2829 | ) |
2830 | -======= |
2831 | ->>>>>>> MERGE-SOURCE |
2832 | from lp.app.browser.stringformatter import ( |
2833 | extract_bug_numbers, |
2834 | extract_email_addresses, |
2835 | @@ -70,7 +67,6 @@ |
2836 | from lp.registry.interfaces.series import SeriesStatus |
2837 | from lp.services.helpers import shortlist |
2838 | from lp.services.propertycache import cachedproperty |
2839 | -<<<<<<< TREE |
2840 | from lp.services.webapp import ( |
2841 | canonical_url, |
2842 | Navigation, |
2843 | @@ -87,26 +83,6 @@ |
2844 | ) |
2845 | from lp.services.webapp.publisher import LaunchpadView |
2846 | from lp.services.webapp.sorting import sorted_dotted_numbers |
2847 | -======= |
2848 | -from lp.services.webapp import ( |
2849 | - action, |
2850 | - canonical_url, |
2851 | - LaunchpadEditFormView, |
2852 | - LaunchpadView, |
2853 | - Navigation, |
2854 | - redirection, |
2855 | - StandardLaunchpadFacets, |
2856 | - ) |
2857 | -from lp.services.webapp.breadcrumb import Breadcrumb |
2858 | -from lp.services.webapp.interfaces import IBreadcrumb |
2859 | -from lp.services.webapp.menu import ( |
2860 | - ApplicationMenu, |
2861 | - enabled_with_permission, |
2862 | - Link, |
2863 | - NavigationMenu, |
2864 | - ) |
2865 | -from lp.services.webapp.sorting import sorted_dotted_numbers |
2866 | ->>>>>>> MERGE-SOURCE |
2867 | from lp.soyuz.browser.sourcepackagerelease import linkify_changelog |
2868 | from lp.soyuz.interfaces.archive import IArchiveSet |
2869 | from lp.soyuz.interfaces.distributionsourcepackagerelease import ( |
2870 | |
2871 | === modified file 'lib/lp/registry/browser/distroseries.py' |
2872 | --- lib/lp/registry/browser/distroseries.py 2012-01-09 13:42:03 +0000 |
2873 | +++ lib/lp/registry/browser/distroseries.py 2012-01-06 11:08:30 +0000 |
2874 | @@ -39,15 +39,10 @@ |
2875 | SimpleVocabulary, |
2876 | ) |
2877 | |
2878 | -<<<<<<< TREE |
2879 | from lp import _ |
2880 | from lp.app.browser.launchpadform import ( |
2881 | action, |
2882 | custom_widget, |
2883 | -======= |
2884 | -from lp import _ |
2885 | -from lp.app.browser.launchpadform import ( |
2886 | ->>>>>>> MERGE-SOURCE |
2887 | LaunchpadEditFormView, |
2888 | LaunchpadFormView, |
2889 | ) |
2890 | @@ -87,55 +82,28 @@ |
2891 | from lp.services.database.constants import UTC_NOW |
2892 | from lp.services.features import getFeatureFlag |
2893 | from lp.services.propertycache import cachedproperty |
2894 | -<<<<<<< TREE |
2895 | -from lp.services.webapp import ( |
2896 | - GetitemNavigation, |
2897 | - StandardLaunchpadFacets, |
2898 | - ) |
2899 | -from lp.services.webapp.authorization import check_permission |
2900 | -from lp.services.webapp.batching import BatchNavigator |
2901 | -from lp.services.webapp.breadcrumb import Breadcrumb |
2902 | -from lp.services.webapp.menu import ( |
2903 | - ApplicationMenu, |
2904 | - enabled_with_permission, |
2905 | - Link, |
2906 | - NavigationMenu, |
2907 | - structured, |
2908 | - ) |
2909 | -from lp.services.webapp.publisher import ( |
2910 | - canonical_url, |
2911 | - LaunchpadView, |
2912 | - stepthrough, |
2913 | - stepto, |
2914 | - ) |
2915 | -from lp.services.webapp.url import urlappend |
2916 | -from lp.services.worlddata.helpers import browser_languages |
2917 | -======= |
2918 | -from lp.services.webapp import ( |
2919 | - action, |
2920 | - custom_widget, |
2921 | - GetitemNavigation, |
2922 | - StandardLaunchpadFacets, |
2923 | - ) |
2924 | -from lp.services.webapp.authorization import check_permission |
2925 | -from lp.services.webapp.batching import BatchNavigator |
2926 | -from lp.services.webapp.breadcrumb import Breadcrumb |
2927 | -from lp.services.webapp.menu import ( |
2928 | - ApplicationMenu, |
2929 | - enabled_with_permission, |
2930 | - Link, |
2931 | - NavigationMenu, |
2932 | - structured, |
2933 | - ) |
2934 | -from lp.services.webapp.publisher import ( |
2935 | - canonical_url, |
2936 | - LaunchpadView, |
2937 | - stepthrough, |
2938 | - stepto, |
2939 | - ) |
2940 | -from lp.services.webapp.url import urlappend |
2941 | -from lp.services.worlddata.helpers import browser_languages |
2942 | ->>>>>>> MERGE-SOURCE |
2943 | +from lp.services.webapp import ( |
2944 | + GetitemNavigation, |
2945 | + StandardLaunchpadFacets, |
2946 | + ) |
2947 | +from lp.services.webapp.authorization import check_permission |
2948 | +from lp.services.webapp.batching import BatchNavigator |
2949 | +from lp.services.webapp.breadcrumb import Breadcrumb |
2950 | +from lp.services.webapp.menu import ( |
2951 | + ApplicationMenu, |
2952 | + enabled_with_permission, |
2953 | + Link, |
2954 | + NavigationMenu, |
2955 | + structured, |
2956 | + ) |
2957 | +from lp.services.webapp.publisher import ( |
2958 | + canonical_url, |
2959 | + LaunchpadView, |
2960 | + stepthrough, |
2961 | + stepto, |
2962 | + ) |
2963 | +from lp.services.webapp.url import urlappend |
2964 | +from lp.services.worlddata.helpers import browser_languages |
2965 | from lp.services.worlddata.interfaces.country import ICountry |
2966 | from lp.services.worlddata.interfaces.language import ILanguageSet |
2967 | from lp.soyuz.browser.archive import PackageCopyingMixin |
2968 | |
2969 | === modified file 'lib/lp/registry/browser/distroseriesdifference.py' |
2970 | --- lib/lp/registry/browser/distroseriesdifference.py 2012-01-09 13:42:03 +0000 |
2971 | +++ lib/lp/registry/browser/distroseriesdifference.py 2012-01-05 20:11:40 +0000 |
2972 | @@ -46,7 +46,6 @@ |
2973 | IConversation, |
2974 | ) |
2975 | from lp.services.propertycache import cachedproperty |
2976 | -<<<<<<< TREE |
2977 | from lp.services.webapp import ( |
2978 | LaunchpadView, |
2979 | Navigation, |
2980 | @@ -54,15 +53,6 @@ |
2981 | ) |
2982 | from lp.services.webapp.authorization import check_permission |
2983 | from lp.app.browser.launchpadform import custom_widget |
2984 | -======= |
2985 | -from lp.services.webapp import ( |
2986 | - LaunchpadView, |
2987 | - Navigation, |
2988 | - stepthrough, |
2989 | - ) |
2990 | -from lp.services.webapp.authorization import check_permission |
2991 | -from lp.services.webapp.launchpadform import custom_widget |
2992 | ->>>>>>> MERGE-SOURCE |
2993 | |
2994 | |
2995 | class DistroSeriesDifferenceNavigation(Navigation): |
2996 | |
2997 | === modified file 'lib/lp/registry/browser/driver.py' |
2998 | --- lib/lp/registry/browser/driver.py 2012-01-09 13:42:03 +0000 |
2999 | +++ lib/lp/registry/browser/driver.py 2012-01-05 20:11:40 +0000 |
3000 | @@ -9,23 +9,14 @@ |
3001 | from zope.interface import providedBy |
3002 | from zope.security.proxy import removeSecurityProxy |
3003 | |
3004 | -<<<<<<< TREE |
3005 | from lp.app.browser.launchpadform import ( |
3006 | -======= |
3007 | -from lp.registry.interfaces.productseries import IProductSeries |
3008 | -from lp.registry.interfaces.role import IHasAppointedDriver |
3009 | -from lp.services.webapp import ( |
3010 | ->>>>>>> MERGE-SOURCE |
3011 | action, |
3012 | LaunchpadEditFormView, |
3013 | ) |
3014 | -<<<<<<< TREE |
3015 | |
3016 | from lp.registry.interfaces.productseries import IProductSeries |
3017 | from lp.registry.interfaces.role import IHasAppointedDriver |
3018 | from lp.services.webapp.publisher import canonical_url |
3019 | -======= |
3020 | ->>>>>>> MERGE-SOURCE |
3021 | |
3022 | |
3023 | class AppointDriverView(LaunchpadEditFormView): |
3024 | |
3025 | === modified file 'lib/lp/registry/browser/karma.py' |
3026 | --- lib/lp/registry/browser/karma.py 2012-01-09 13:42:03 +0000 |
3027 | +++ lib/lp/registry/browser/karma.py 2012-01-05 20:11:40 +0000 |
3028 | @@ -13,15 +13,11 @@ |
3029 | |
3030 | from zope.component import getUtility |
3031 | |
3032 | -<<<<<<< TREE |
3033 | from lp import _ |
3034 | from lp.app.browser.launchpadform import ( |
3035 | action, |
3036 | LaunchpadEditFormView, |
3037 | ) |
3038 | -======= |
3039 | -from lp import _ |
3040 | ->>>>>>> MERGE-SOURCE |
3041 | from lp.registry.interfaces.distribution import IDistribution |
3042 | from lp.registry.interfaces.karma import ( |
3043 | IKarmaAction, |
3044 | @@ -30,21 +26,11 @@ |
3045 | from lp.registry.interfaces.product import IProduct |
3046 | from lp.registry.interfaces.projectgroup import IProjectGroup |
3047 | from lp.services.propertycache import cachedproperty |
3048 | -<<<<<<< TREE |
3049 | from lp.services.webapp import ( |
3050 | canonical_url, |
3051 | Navigation, |
3052 | ) |
3053 | from lp.services.webapp.publisher import LaunchpadView |
3054 | -======= |
3055 | -from lp.services.webapp import ( |
3056 | - action, |
3057 | - canonical_url, |
3058 | - LaunchpadEditFormView, |
3059 | - LaunchpadView, |
3060 | - Navigation, |
3061 | - ) |
3062 | ->>>>>>> MERGE-SOURCE |
3063 | |
3064 | |
3065 | TOP_CONTRIBUTORS_LIMIT = 20 |
3066 | |
3067 | === modified file 'lib/lp/registry/browser/milestone.py' |
3068 | --- lib/lp/registry/browser/milestone.py 2012-01-01 02:58:52 +0000 |
3069 | +++ lib/lp/registry/browser/milestone.py 2012-01-09 13:42:13 +0000 |
3070 | @@ -52,6 +52,7 @@ |
3071 | from lp.registry.browser.product import ProductDownloadFileMixin |
3072 | from lp.registry.interfaces.distroseries import IDistroSeries |
3073 | from lp.registry.interfaces.milestone import ( |
3074 | + IAbstractMilestone, |
3075 | IMilestone, |
3076 | IMilestoneSet, |
3077 | IProjectGroupMilestone, |
3078 | @@ -84,15 +85,15 @@ |
3079 | class MilestoneNavigation(Navigation, |
3080 | StructuralSubscriptionTargetTraversalMixin): |
3081 | """The navigation to traverse to a milestone.""" |
3082 | - usedfor = IMilestone |
3083 | + usedfor = IAbstractMilestone |
3084 | |
3085 | |
3086 | class MilestoneBreadcrumb(Breadcrumb): |
3087 | - """The Breadcrumb for an `IMilestone`.""" |
3088 | + """The Breadcrumb for an `IAbstractMilestone`.""" |
3089 | |
3090 | @property |
3091 | def text(self): |
3092 | - milestone = IMilestone(self.context) |
3093 | + milestone = IAbstractMilestone(self.context) |
3094 | if milestone.code_name: |
3095 | return '%s "%s"' % (milestone.name, milestone.code_name) |
3096 | else: |
3097 | @@ -139,7 +140,7 @@ |
3098 | |
3099 | class MilestoneContextMenu(ContextMenu, MilestoneLinkMixin): |
3100 | """The menu for this milestone.""" |
3101 | - usedfor = IMilestone |
3102 | + usedfor = IAbstractMilestone |
3103 | |
3104 | @cachedproperty |
3105 | def links(self): |
3106 | @@ -151,7 +152,7 @@ |
3107 | |
3108 | class MilestoneOverviewNavigationMenu(NavigationMenu, MilestoneLinkMixin): |
3109 | """Overview navigation menu for `IMilestone` objects.""" |
3110 | - usedfor = IMilestone |
3111 | + usedfor = IAbstractMilestone |
3112 | facet = 'overview' |
3113 | |
3114 | @cachedproperty |
3115 | @@ -165,7 +166,7 @@ |
3116 | """Overview menus for `IMilestone` objects.""" |
3117 | # This menu must not contain 'subscribe' because the link state is too |
3118 | # costly to calculate when this menu is used with a list of milestones. |
3119 | - usedfor = IMilestone |
3120 | + usedfor = IAbstractMilestone |
3121 | facet = 'overview' |
3122 | links = ('edit', 'create_release') |
3123 | |
3124 | @@ -199,7 +200,7 @@ |
3125 | :param request: `ILaunchpadRequest`. |
3126 | """ |
3127 | super(MilestoneView, self).__init__(context, request) |
3128 | - if IMilestone.providedBy(context): |
3129 | + if IAbstractMilestone.providedBy(context): |
3130 | self.milestone = context |
3131 | self.release = context.product_release |
3132 | else: |
3133 | @@ -261,9 +262,7 @@ |
3134 | """The list of non-conjoined bugtasks targeted to this milestone.""" |
3135 | # Put the results in a list so that iterating over it multiple |
3136 | # times in this method does not make multiple queries. |
3137 | - non_conjoined_slaves = list( |
3138 | - getUtility(IBugTaskSet).getPrecachedNonConjoinedBugTasks( |
3139 | - self.user, self.context)) |
3140 | + non_conjoined_slaves = self.context.bugtasks(self.user) |
3141 | # Checking bug permissions is expensive. We know from the query that |
3142 | # the user has at least launchpad.View on the bugtasks and their bugs. |
3143 | # NB: this is in principle unneeded due to injection of permission in |
3144 | |
3145 | === modified file 'lib/lp/registry/browser/nameblacklist.py' |
3146 | --- lib/lp/registry/browser/nameblacklist.py 2012-01-09 13:42:03 +0000 |
3147 | +++ lib/lp/registry/browser/nameblacklist.py 2012-01-05 20:11:40 +0000 |
3148 | @@ -19,7 +19,6 @@ |
3149 | ) |
3150 | from zope.interface import implements |
3151 | |
3152 | -<<<<<<< TREE |
3153 | from lp.app.browser.launchpadform import ( |
3154 | action, |
3155 | custom_widget, |
3156 | @@ -33,21 +32,6 @@ |
3157 | from lp.services.webapp.breadcrumb import Breadcrumb |
3158 | from lp.services.webapp.interfaces import IBreadcrumb |
3159 | from lp.services.webapp.menu import ( |
3160 | -======= |
3161 | -from lp.app.browser.launchpadform import ( |
3162 | - custom_widget, |
3163 | - LaunchpadFormView, |
3164 | - ) |
3165 | -from lp.registry.browser import RegistryEditFormView |
3166 | -from lp.registry.interfaces.nameblacklist import ( |
3167 | - INameBlacklist, |
3168 | - INameBlacklistSet, |
3169 | - ) |
3170 | -from lp.services.webapp import action |
3171 | -from lp.services.webapp.breadcrumb import Breadcrumb |
3172 | -from lp.services.webapp.interfaces import IBreadcrumb |
3173 | -from lp.services.webapp.menu import ( |
3174 | ->>>>>>> MERGE-SOURCE |
3175 | ApplicationMenu, |
3176 | enabled_with_permission, |
3177 | Link, |
3178 | |
3179 | === modified file 'lib/lp/registry/browser/teammembership.py' |
3180 | --- lib/lp/registry/browser/teammembership.py 2012-01-09 13:42:03 +0000 |
3181 | +++ lib/lp/registry/browser/teammembership.py 2012-01-03 22:28:10 +0000 |
3182 | @@ -18,28 +18,15 @@ |
3183 | from zope.formlib import form |
3184 | from zope.schema import Date |
3185 | |
3186 | -<<<<<<< TREE |
3187 | -from lp import _ |
3188 | -from lp.app.errors import UnexpectedFormData |
3189 | -from lp.app.widgets.date import DateWidget |
3190 | -from lp.registry.interfaces.teammembership import TeamMembershipStatus |
3191 | -from lp.services.webapp import ( |
3192 | - canonical_url, |
3193 | - LaunchpadView, |
3194 | - ) |
3195 | -from lp.services.webapp.breadcrumb import Breadcrumb |
3196 | -======= |
3197 | -from lp import _ |
3198 | -from lp.app.errors import UnexpectedFormData |
3199 | -from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
3200 | -from lp.app.widgets.date import DateWidget |
3201 | -from lp.registry.interfaces.teammembership import TeamMembershipStatus |
3202 | -from lp.services.webapp import ( |
3203 | - canonical_url, |
3204 | - LaunchpadView, |
3205 | - ) |
3206 | -from lp.services.webapp.breadcrumb import Breadcrumb |
3207 | ->>>>>>> MERGE-SOURCE |
3208 | +from lp import _ |
3209 | +from lp.app.errors import UnexpectedFormData |
3210 | +from lp.app.widgets.date import DateWidget |
3211 | +from lp.registry.interfaces.teammembership import TeamMembershipStatus |
3212 | +from lp.services.webapp import ( |
3213 | + canonical_url, |
3214 | + LaunchpadView, |
3215 | + ) |
3216 | +from lp.services.webapp.breadcrumb import Breadcrumb |
3217 | |
3218 | |
3219 | class TeamMembershipBreadcrumb(Breadcrumb): |
3220 | |
3221 | === modified file 'lib/lp/registry/configure.zcml' |
3222 | --- lib/lp/registry/configure.zcml 2011-12-30 08:03:42 +0000 |
3223 | +++ lib/lp/registry/configure.zcml 2012-01-09 13:42:13 +0000 |
3224 | @@ -404,8 +404,8 @@ |
3225 | setAliases"/> |
3226 | |
3227 | <!-- IProjectGroupModerate --> |
3228 | - <allow |
3229 | - interface="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/> |
3230 | + <allow |
3231 | + interface="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/> |
3232 | <require |
3233 | permission="launchpad.Moderate" |
3234 | set_schema="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/> |
3235 | @@ -1006,6 +1006,7 @@ |
3236 | series_target |
3237 | displayname |
3238 | title |
3239 | + bugtasks |
3240 | specifications |
3241 | product_release"/> |
3242 | <require |
3243 | @@ -1013,7 +1014,15 @@ |
3244 | attributes=" |
3245 | createProductRelease |
3246 | closeBugsAndBlueprints |
3247 | - destroySelf"/> |
3248 | + destroySelf |
3249 | + setTags |
3250 | + "/> |
3251 | + <require |
3252 | + permission="zope.Public" |
3253 | + attributes=" |
3254 | + getTags |
3255 | + getTagsData |
3256 | + "/> |
3257 | <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/> |
3258 | <allow |
3259 | interface="lp.bugs.interfaces.bugtarget.IHasBugs"/> |
3260 | @@ -1245,7 +1254,7 @@ |
3261 | <!-- https://lists.ubuntu.com/mailman/private/launchpad/2007-April/015189.html |
3262 | for further discussion - stub 20070411 --> |
3263 | |
3264 | - <!-- Per bug 588773, changing to launchpad.Moderate to allow Registry Experts (~registry) --> |
3265 | + <!-- Per bug 588773, changing to launchpad.Moderate to allow Registry Experts (~registry) --> |
3266 | <require |
3267 | permission="launchpad.Moderate" |
3268 | set_attributes="name autoupdate registrant"/> |
3269 | |
3270 | === modified file 'lib/lp/registry/doc/mailinglist-subscriptions.txt' |
3271 | --- lib/lp/registry/doc/mailinglist-subscriptions.txt 2012-01-09 13:42:03 +0000 |
3272 | +++ lib/lp/registry/doc/mailinglist-subscriptions.txt 2012-01-04 23:49:46 +0000 |
3273 | @@ -1331,17 +1331,9 @@ |
3274 | |
3275 | Now Umma's account is suspended by a Launchpad administrator. |
3276 | |
3277 | -<<<<<<< TREE |
3278 | >>> from lp.services.identity.interfaces.account import ( |
3279 | ... AccountStatus) |
3280 | >>> umma.account.status = AccountStatus.SUSPENDED |
3281 | -======= |
3282 | - >>> from lp.services.identity.interfaces.account import ( |
3283 | - ... AccountStatus, IAccountSet) |
3284 | - >>> umma_account = getUtility(IAccountSet).getByEmail( |
3285 | - ... 'umma.person@example.com') |
3286 | - >>> umma_account.status = AccountStatus.SUSPENDED |
3287 | ->>>>>>> MERGE-SOURCE |
3288 | >>> transaction.commit() |
3289 | |
3290 | Umma is no longer subscribed to the mailing list... |
3291 | |
3292 | === modified file 'lib/lp/registry/interfaces/milestone.py' |
3293 | --- lib/lp/registry/interfaces/milestone.py 2012-01-01 02:58:52 +0000 |
3294 | +++ lib/lp/registry/interfaces/milestone.py 2012-01-09 13:42:13 +0000 |
3295 | @@ -1,4 +1,4 @@ |
3296 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
3297 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
3298 | # GNU Affero General Public License version 3 (see the file LICENSE). |
3299 | |
3300 | # pylint: disable-msg=E0211,E0213 |
3301 | @@ -8,9 +8,11 @@ |
3302 | __metaclass__ = type |
3303 | |
3304 | __all__ = [ |
3305 | + 'IAbstractMilestone', |
3306 | 'ICanGetMilestonesDirectly', |
3307 | 'IHasMilestones', |
3308 | 'IMilestone', |
3309 | + 'IMilestoneData', |
3310 | 'IMilestoneSet', |
3311 | 'IProjectGroupMilestone', |
3312 | ] |
3313 | @@ -97,20 +99,52 @@ |
3314 | return milestone |
3315 | |
3316 | |
3317 | -class IMilestone(IHasBugs, IStructuralSubscriptionTarget, |
3318 | - IHasOfficialBugTags): |
3319 | - """A milestone, or a targeting point for bugs and other |
3320 | - release-management items that need coordination. |
3321 | +class IMilestoneData(IHasBugs, IStructuralSubscriptionTarget, |
3322 | + IHasOfficialBugTags): |
3323 | + """Interface containing the data for milestones. |
3324 | + |
3325 | + To be registered for views but not instantiated. |
3326 | """ |
3327 | - export_as_webservice_entry() |
3328 | - |
3329 | id = Int(title=_("Id")) |
3330 | + |
3331 | name = exported( |
3332 | MilestoneNameField( |
3333 | title=_("Name"), |
3334 | description=_( |
3335 | "Only letters, numbers, and simple punctuation are allowed."), |
3336 | constraint=name_validator)) |
3337 | + target = exported( |
3338 | + Reference( |
3339 | + schema=Interface, # IHasMilestones |
3340 | + title=_( |
3341 | + "The product, distribution, or project group for this " |
3342 | + "milestone."), |
3343 | + required=False)) |
3344 | + specifications = Attribute( |
3345 | + "A list of specifications targeted to this object.") |
3346 | + dateexpected = exported( |
3347 | + FormattableDate(title=_("Date Targeted"), required=False, |
3348 | + description=_("Example: 2005-11-24")), |
3349 | + exported_as='date_targeted') |
3350 | + active = exported( |
3351 | + Bool( |
3352 | + title=_("Active"), |
3353 | + description=_("Whether or not this object should be shown " |
3354 | + "in web forms for targeting.")), |
3355 | + exported_as='is_active') |
3356 | + displayname = Attribute("A displayname constructed from the name.") |
3357 | + title = exported( |
3358 | + TextLine(title=_("A context title for pages."), |
3359 | + readonly=True)) |
3360 | + |
3361 | + def bugtasks(user): |
3362 | + """Get a list of non-conjoined bugtasks visible to this user.""" |
3363 | + |
3364 | + |
3365 | +class IAbstractMilestone(IMilestoneData): |
3366 | + """An intermediate interface for milestone, or a targeting point for bugs |
3367 | + and other release-management items that need coordination. |
3368 | + """ |
3369 | code_name = exported( |
3370 | NoneableTextLine( |
3371 | title=u'Code name', required=False, |
3372 | @@ -126,47 +160,24 @@ |
3373 | title=_("Product Series"), |
3374 | description=_("The product series for which this is a milestone."), |
3375 | vocabulary="FilteredProductSeries", |
3376 | - required=False) # for now |
3377 | + required=False) # for now |
3378 | distroseries = Choice( |
3379 | title=_("Distro Series"), |
3380 | description=_( |
3381 | "The distribution series for which this is a milestone."), |
3382 | vocabulary="FilteredDistroSeries", |
3383 | - required=False) # for now |
3384 | - dateexpected = exported( |
3385 | - FormattableDate(title=_("Date Targeted"), required=False, |
3386 | - description=_("Example: 2005-11-24")), |
3387 | - exported_as='date_targeted') |
3388 | - active = exported( |
3389 | - Bool( |
3390 | - title=_("Active"), |
3391 | - description=_("Whether or not this milestone should be shown " |
3392 | - "in web forms for bug targeting.")), |
3393 | - exported_as='is_active') |
3394 | + required=False) # for now |
3395 | summary = exported( |
3396 | NoneableDescription( |
3397 | title=_("Summary"), |
3398 | required=False, |
3399 | description=_( |
3400 | "A summary of the features and status of this milestone."))) |
3401 | - target = exported( |
3402 | - Reference( |
3403 | - schema=Interface, # IHasMilestones |
3404 | - title=_("The product or distribution of this milestone."), |
3405 | - required=False)) |
3406 | series_target = exported( |
3407 | Reference( |
3408 | - schema=Interface, # IHasMilestones |
3409 | + schema=Interface, # IHasMilestones |
3410 | title=_("The productseries or distroseries of this milestone."), |
3411 | required=False)) |
3412 | - displayname = Attribute("A displayname for this milestone, constructed " |
3413 | - "from the milestone name.") |
3414 | - title = exported( |
3415 | - TextLine(title=_("A milestone context title for pages."), |
3416 | - readonly=True)) |
3417 | - specifications = Attribute("A list of the specifications targeted to " |
3418 | - "this milestone.") |
3419 | - |
3420 | product_release = exported( |
3421 | Reference( |
3422 | schema=IProductRelease, |
3423 | @@ -211,6 +222,38 @@ |
3424 | release. |
3425 | """ |
3426 | |
3427 | + |
3428 | +class IMilestone(IAbstractMilestone): |
3429 | + """Actual interface for milestones.""" |
3430 | + |
3431 | + export_as_webservice_entry() |
3432 | + |
3433 | + def setTags(tags, user): |
3434 | + """Set the milestone tags. |
3435 | + |
3436 | + :param: tags The list of tags to be associated with milestone. |
3437 | + :param: user The user who is updating tags for this milestone. |
3438 | + |
3439 | + Note that this is not a property because, while the current user |
3440 | + is needed to store tags metadata, it is desirable to avoid |
3441 | + using thread locals to get the current request in models. |
3442 | + """ |
3443 | + |
3444 | + def getTagsData(): |
3445 | + """Return MilestoneTag instances associated with milestone. |
3446 | + |
3447 | + See above the IMilestone.setTags docstring for an explanation of |
3448 | + why this is not a property. |
3449 | + """ |
3450 | + |
3451 | + def getTags(): |
3452 | + """Return the milestone tags in alphabetical order. |
3453 | + |
3454 | + See above the IMilestone.setTags docstring for an explanation of |
3455 | + why this is not a property. |
3456 | + """ |
3457 | + |
3458 | + |
3459 | # Avoid circular imports |
3460 | IBugTask['milestone'].schema = IMilestone |
3461 | patch_plain_parameter_type( |
3462 | @@ -249,8 +292,9 @@ |
3463 | """Return all visible milestones.""" |
3464 | |
3465 | |
3466 | -class IProjectGroupMilestone(IMilestone): |
3467 | +class IProjectGroupMilestone(IAbstractMilestone): |
3468 | """A marker interface for milestones related to a project""" |
3469 | + export_as_webservice_entry() |
3470 | |
3471 | |
3472 | class IHasMilestones(Interface): |
3473 | |
3474 | === added file 'lib/lp/registry/interfaces/milestonetag.py' |
3475 | --- lib/lp/registry/interfaces/milestonetag.py 1970-01-01 00:00:00 +0000 |
3476 | +++ lib/lp/registry/interfaces/milestonetag.py 2012-01-09 13:42:13 +0000 |
3477 | @@ -0,0 +1,20 @@ |
3478 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
3479 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
3480 | + |
3481 | +"""MilestoneTag interfaces.""" |
3482 | + |
3483 | +__metaclass__ = type |
3484 | +__all__ = [ |
3485 | + 'IProjectGroupMilestoneTag', |
3486 | + ] |
3487 | + |
3488 | + |
3489 | +from lp.registry.interfaces.milestone import IMilestoneData |
3490 | + |
3491 | + |
3492 | +class IProjectGroupMilestoneTag(IMilestoneData): |
3493 | + """An IProjectGroupMilestoneTag is a tag aggretating milestones for the |
3494 | + ProjectGroup with a given tag or tags. |
3495 | + |
3496 | + This interface is just a marker. |
3497 | + """ |
3498 | |
3499 | === modified file 'lib/lp/registry/model/mailinglist.py' |
3500 | --- lib/lp/registry/model/mailinglist.py 2012-01-09 13:42:03 +0000 |
3501 | +++ lib/lp/registry/model/mailinglist.py 2012-01-04 22:30:45 +0000 |
3502 | @@ -1,8 +1,4 @@ |
3503 | -<<<<<<< TREE |
3504 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
3505 | -======= |
3506 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
3507 | ->>>>>>> MERGE-SOURCE |
3508 | # GNU Affero General Public License version 3 (see the file LICENSE). |
3509 | |
3510 | # pylint: disable-msg=E0611,W0212 |
3511 | |
3512 | === modified file 'lib/lp/registry/model/milestone.py' |
3513 | --- lib/lp/registry/model/milestone.py 2011-12-30 06:14:56 +0000 |
3514 | +++ lib/lp/registry/model/milestone.py 2012-01-09 13:42:13 +0000 |
3515 | @@ -1,4 +1,4 @@ |
3516 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
3517 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
3518 | # GNU Affero General Public License version 3 (see the file LICENSE). |
3519 | |
3520 | # pylint: disable-msg=E0611,W0212 |
3521 | @@ -8,6 +8,7 @@ |
3522 | __all__ = [ |
3523 | 'HasMilestonesMixin', |
3524 | 'Milestone', |
3525 | + 'MilestoneData', |
3526 | 'MilestoneSet', |
3527 | 'ProjectMilestone', |
3528 | 'milestone_sort_key', |
3529 | @@ -49,6 +50,7 @@ |
3530 | from lp.registry.interfaces.milestone import ( |
3531 | IHasMilestones, |
3532 | IMilestone, |
3533 | + IMilestoneData, |
3534 | IMilestoneSet, |
3535 | IProjectGroupMilestone, |
3536 | ) |
3537 | @@ -129,11 +131,47 @@ |
3538 | super(MultipleProductReleases, self).__init__(msg) |
3539 | |
3540 | |
3541 | -class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase): |
3542 | +class MilestoneData: |
3543 | + implements(IMilestoneData) |
3544 | + |
3545 | + @property |
3546 | + def displayname(self): |
3547 | + """See IMilestone.""" |
3548 | + return "%s %s" % (self.target.displayname, self.name) |
3549 | + |
3550 | + @property |
3551 | + def title(self): |
3552 | + raise NotImplementedError |
3553 | + |
3554 | + @property |
3555 | + def specifications(self): |
3556 | + raise NotImplementedError |
3557 | + |
3558 | + def bugtasks(self, user): |
3559 | + """The list of non-conjoined bugtasks targeted to this milestone.""" |
3560 | + # Put the results in a list so that iterating over it multiple |
3561 | + # times in this method does not make multiple queries. |
3562 | + non_conjoined_slaves = list( |
3563 | + getUtility(IBugTaskSet).getPrecachedNonConjoinedBugTasks( |
3564 | + user, self)) |
3565 | + return non_conjoined_slaves |
3566 | + |
3567 | + |
3568 | +class Milestone(SQLBase, MilestoneData, StructuralSubscriptionTargetMixin, |
3569 | + HasBugsBase): |
3570 | implements(IHasBugs, IMilestone, IBugSummaryDimension) |
3571 | |
3572 | + active = BoolCol(notNull=True, default=True) |
3573 | + |
3574 | + # XXX: EdwinGrubbs 2009-02-06 bug=326384: |
3575 | + # The Milestone.dateexpected should be changed into a date column, |
3576 | + # since the class defines the field as a DateCol, so that a list of |
3577 | + # milestones can't have some dateexpected attributes that are |
3578 | + # datetimes and others that are dates, which can't be compared. |
3579 | + dateexpected = DateCol(notNull=False, default=None) |
3580 | + |
3581 | # XXX: Guilherme Salgado 2007-03-27 bug=40978: |
3582 | - # Milestones should be associated with productseries/distroseriess |
3583 | + # Milestones should be associated with productseries/distroseries |
3584 | # so these columns are not needed. |
3585 | product = ForeignKey(dbName='product', |
3586 | foreignKey='Product', default=None) |
3587 | @@ -145,23 +183,23 @@ |
3588 | distroseries = ForeignKey(dbName='distroseries', |
3589 | foreignKey='DistroSeries', default=None) |
3590 | name = StringCol(notNull=True) |
3591 | - # XXX: EdwinGrubbs 2009-02-06 bug=326384: |
3592 | - # The Milestone.dateexpected should be changed into a date column, |
3593 | - # since the class defines the field as a DateCol, so that a list of |
3594 | - # milestones can't have some dateexpected attributes that are |
3595 | - # datetimes and others that are dates, which can't be compared. |
3596 | - dateexpected = DateCol(notNull=False, default=None) |
3597 | - active = BoolCol(notNull=True, default=True) |
3598 | summary = StringCol(notNull=False, default=None) |
3599 | code_name = StringCol(dbName='codename', notNull=False, default=None) |
3600 | |
3601 | - # joins |
3602 | specifications = SQLMultipleJoin('Specification', joinColumn='milestone', |
3603 | orderBy=['-priority', 'definition_status', |
3604 | 'implementation_status', 'title'], |
3605 | prejoins=['assignee']) |
3606 | |
3607 | @property |
3608 | + def target(self): |
3609 | + """See IMilestone.""" |
3610 | + if self.product: |
3611 | + return self.product |
3612 | + elif self.distribution: |
3613 | + return self.distribution |
3614 | + |
3615 | + @property |
3616 | def product_release(self): |
3617 | store = Store.of(self) |
3618 | result = store.find(ProductRelease, |
3619 | @@ -173,14 +211,6 @@ |
3620 | return releases[0] |
3621 | |
3622 | @property |
3623 | - def target(self): |
3624 | - """See IMilestone.""" |
3625 | - if self.product: |
3626 | - return self.product |
3627 | - elif self.distribution: |
3628 | - return self.distribution |
3629 | - |
3630 | - @property |
3631 | def series_target(self): |
3632 | """See IMilestone.""" |
3633 | if self.productseries: |
3634 | @@ -189,11 +219,6 @@ |
3635 | return self.distroseries |
3636 | |
3637 | @property |
3638 | - def displayname(self): |
3639 | - """See IMilestone.""" |
3640 | - return "%s %s" % (self.target.displayname, self.name) |
3641 | - |
3642 | - @property |
3643 | def title(self): |
3644 | """See IMilestone.""" |
3645 | if not self.code_name: |
3646 | @@ -255,6 +280,44 @@ |
3647 | from lp.bugs.model.bugsummary import BugSummary |
3648 | return BugSummary.milestone_id == self.id |
3649 | |
3650 | + def setTags(self, tags, user): |
3651 | + """See IMilestone.""" |
3652 | + # Circular reference prevention. |
3653 | + from lp.registry.model.milestonetag import MilestoneTag |
3654 | + store = Store.of(self) |
3655 | + if tags: |
3656 | + current_tags = set(self.getTags()) |
3657 | + new_tags = set(tags) |
3658 | + if new_tags == current_tags: |
3659 | + return |
3660 | + # Removing deleted tags. |
3661 | + to_remove = current_tags.difference(new_tags) |
3662 | + if to_remove: |
3663 | + store.find( |
3664 | + MilestoneTag, MilestoneTag.tag.is_in(to_remove)).remove() |
3665 | + # Adding new tags. |
3666 | + for tag in new_tags.difference(current_tags): |
3667 | + store.add(MilestoneTag(self, tag, user)) |
3668 | + else: |
3669 | + store.find( |
3670 | + MilestoneTag, MilestoneTag.milestone_id == self.id).remove() |
3671 | + store.commit() |
3672 | + |
3673 | + def getTagsData(self): |
3674 | + """See IMilestone.""" |
3675 | + # Prevent circular references. |
3676 | + from lp.registry.model.milestonetag import MilestoneTag |
3677 | + store = Store.of(self) |
3678 | + return store.find( |
3679 | + MilestoneTag, MilestoneTag.milestone_id == self.id |
3680 | + ).order_by(MilestoneTag.tag) |
3681 | + |
3682 | + def getTags(self): |
3683 | + """See IMilestone.""" |
3684 | + # Prevent circular references. |
3685 | + from lp.registry.model.milestonetag import MilestoneTag |
3686 | + return self.getTagsData().values(MilestoneTag.tag) |
3687 | + |
3688 | |
3689 | class MilestoneSet: |
3690 | implements(IMilestoneSet) |
3691 | @@ -300,7 +363,7 @@ |
3692 | return Milestone.selectBy(active=True, orderBy='id') |
3693 | |
3694 | |
3695 | -class ProjectMilestone(HasBugsBase): |
3696 | +class ProjectMilestone(MilestoneData, HasBugsBase): |
3697 | """A virtual milestone implementation for project. |
3698 | |
3699 | The current database schema has no formal concept of milestones related to |
3700 | @@ -315,12 +378,13 @@ |
3701 | implements(IProjectGroupMilestone) |
3702 | |
3703 | def __init__(self, target, name, dateexpected, active): |
3704 | - self.name = name |
3705 | self.code_name = None |
3706 | # The id is necessary for generating a unique memcache key |
3707 | # in a page template loop. The ProjectMilestone.id is passed |
3708 | # in as the third argument to the "cache" TALes. |
3709 | self.id = 'ProjectGroup:%s/Milestone:%s' % (target.name, name) |
3710 | + self.name = name |
3711 | + self.target = target |
3712 | self.code_name = None |
3713 | self.product = None |
3714 | self.distribution = None |
3715 | @@ -329,7 +393,6 @@ |
3716 | self.product_release = None |
3717 | self.dateexpected = dateexpected |
3718 | self.active = active |
3719 | - self.target = target |
3720 | self.series_target = None |
3721 | self.summary = None |
3722 | |
3723 | |
3724 | === added file 'lib/lp/registry/model/milestonetag.py' |
3725 | --- lib/lp/registry/model/milestonetag.py 1970-01-01 00:00:00 +0000 |
3726 | +++ lib/lp/registry/model/milestonetag.py 2012-01-09 13:42:13 +0000 |
3727 | @@ -0,0 +1,90 @@ |
3728 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
3729 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
3730 | + |
3731 | +"""Milestonetag model class.""" |
3732 | + |
3733 | +__metaclass__ = type |
3734 | +__all__ = [ |
3735 | + 'MilestoneTag', |
3736 | + 'ProjectGroupMilestoneTag', |
3737 | + ] |
3738 | + |
3739 | + |
3740 | +from zope.interface import implements |
3741 | +from zope.component import getUtility |
3742 | + |
3743 | +from lp.services.webapp.interfaces import ( |
3744 | + IStoreSelector, |
3745 | + MAIN_STORE, |
3746 | + DEFAULT_FLAVOR, |
3747 | + ) |
3748 | + |
3749 | +from lp.blueprints.model.specification import Specification |
3750 | +from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag |
3751 | +from lp.registry.model.milestone import MilestoneData, Milestone |
3752 | +from lp.registry.model.product import Product |
3753 | +from storm.locals import ( |
3754 | + DateTime, |
3755 | + Int, |
3756 | + Unicode, |
3757 | + Reference, |
3758 | + ) |
3759 | + |
3760 | + |
3761 | +class MilestoneTag(object): |
3762 | + """A tag belonging to a milestone.""" |
3763 | + |
3764 | + __storm_table__ = 'milestonetag' |
3765 | + |
3766 | + id = Int(primary=True) |
3767 | + milestone_id = Int(name='milestone', allow_none=False) |
3768 | + milestone = Reference(milestone_id, 'milestone.id') |
3769 | + tag = Unicode(allow_none=False) |
3770 | + created_by_id = Int(name='created_by', allow_none=False) |
3771 | + created_by = Reference(created_by_id, 'person.id') |
3772 | + date_created = DateTime(allow_none=False) |
3773 | + |
3774 | + def __init__(self, milestone, tag, created_by, date_created=None): |
3775 | + self.milestone_id = milestone.id |
3776 | + self.tag = tag |
3777 | + self.created_by_id = created_by.id |
3778 | + if date_created is not None: |
3779 | + self.date_created = date_created |
3780 | + |
3781 | + |
3782 | +class ProjectGroupMilestoneTag(MilestoneData): |
3783 | + |
3784 | + implements(IProjectGroupMilestoneTag) |
3785 | + |
3786 | + def __init__(self, target, tags): |
3787 | + self.target = target |
3788 | + # Tags is a sequence of Unicode strings. |
3789 | + self.tags = tags |
3790 | + |
3791 | + @property |
3792 | + def name(self): |
3793 | + return u", ".join(self.tags) |
3794 | + |
3795 | + @property |
3796 | + def title(self): |
3797 | + """See IMilestoneData.""" |
3798 | + return self.displayname |
3799 | + |
3800 | + @property |
3801 | + def specifications(self): |
3802 | + """See IMilestoneData.""" |
3803 | + store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
3804 | + results = [] |
3805 | + for tag in self.tags: |
3806 | + result = store.find( |
3807 | + Specification, |
3808 | + Specification.milestone == Milestone.id, |
3809 | + Milestone.product == Product.id, |
3810 | + Product.project == self.target, |
3811 | + MilestoneTag.milestone_id == Milestone.id, |
3812 | + MilestoneTag.tag == tag) |
3813 | + results.append(result) |
3814 | + result = results.pop() |
3815 | + for i in results: |
3816 | + result = result.intersection(i) |
3817 | + return result |
3818 | |
3819 | === modified file 'lib/lp/registry/model/projectgroup.py' |
3820 | --- lib/lp/registry/model/projectgroup.py 2011-12-30 06:14:56 +0000 |
3821 | +++ lib/lp/registry/model/projectgroup.py 2012-01-09 13:42:13 +0000 |
3822 | @@ -464,19 +464,19 @@ |
3823 | @property |
3824 | def milestones(self): |
3825 | """See `IProjectGroup`.""" |
3826 | - return self._getMilestones(True) |
3827 | + return self._getMilestones(only_active=True) |
3828 | |
3829 | @property |
3830 | def product_milestones(self): |
3831 | """Hack to avoid the ProjectMilestone in MilestoneVocabulary.""" |
3832 | # XXX: bug=644977 Robert Collins - this is a workaround for |
3833 | - # insconsistency in project group milestone use. |
3834 | + # inconsistency in project group milestone use. |
3835 | return self._get_milestones() |
3836 | |
3837 | @property |
3838 | def all_milestones(self): |
3839 | """See `IProjectGroup`.""" |
3840 | - return self._getMilestones(False) |
3841 | + return self._getMilestones(only_active=False) |
3842 | |
3843 | def getMilestone(self, name): |
3844 | """See `IProjectGroup`.""" |
3845 | |
3846 | === modified file 'lib/lp/registry/tests/test_milestone.py' |
3847 | --- lib/lp/registry/tests/test_milestone.py 2012-01-01 02:58:52 +0000 |
3848 | +++ lib/lp/registry/tests/test_milestone.py 2012-01-09 13:42:13 +0000 |
3849 | @@ -1,4 +1,4 @@ |
3850 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
3851 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
3852 | # GNU Affero General Public License version 3 (see the file LICENSE). |
3853 | |
3854 | """Milestone related test helper.""" |
3855 | @@ -18,6 +18,7 @@ |
3856 | ) |
3857 | from lp.registry.interfaces.product import IProductSet |
3858 | from lp.testing import ( |
3859 | + person_logged_in, |
3860 | ANONYMOUS, |
3861 | login, |
3862 | logout, |
3863 | @@ -57,7 +58,7 @@ |
3864 | def testMilestoneSetGetIDs(self): |
3865 | """Test of MilestoneSet.getByIds()""" |
3866 | milestone_set = getUtility(IMilestoneSet) |
3867 | - milestones = milestone_set.getByIds([1,3]) |
3868 | + milestones = milestone_set.getByIds([1, 3]) |
3869 | ids = sorted(map(attrgetter('id'), milestones)) |
3870 | self.assertEqual([1, 3], ids) |
3871 | |
3872 | @@ -128,3 +129,49 @@ |
3873 | def test_projectgroup(self): |
3874 | projectgroup = self.factory.makeProject() |
3875 | self.check_skipped(projectgroup) |
3876 | + |
3877 | + |
3878 | +class MilestoneBugTaskSpecificationTest(TestCaseWithFactory): |
3879 | + """Test cases for retrieving bugtasks and specifications for a milestone. |
3880 | + """ |
3881 | + |
3882 | + layer = DatabaseFunctionalLayer |
3883 | + |
3884 | + def setUp(self): |
3885 | + super(MilestoneBugTaskSpecificationTest, self).setUp() |
3886 | + self.owner = self.factory.makePerson() |
3887 | + self.product = self.factory.makeProduct(name="product1") |
3888 | + self.milestone = self.factory.makeMilestone(product=self.product) |
3889 | + |
3890 | + def _make_bug(self, **kwargs): |
3891 | + milestone = kwargs.pop('milestone', None) |
3892 | + bugtask = self.factory.makeBugTask(**kwargs) |
3893 | + bugtask.milestone = milestone |
3894 | + return bugtask |
3895 | + |
3896 | + def _create_items(self, num, factory, **kwargs): |
3897 | + items = [] |
3898 | + with person_logged_in(self.owner): |
3899 | + for n in xrange(num): |
3900 | + items.append(factory(**kwargs)) |
3901 | + return items |
3902 | + |
3903 | + def test_bugtask_retrieval(self): |
3904 | + # Ensure that all bugtasks on a milestone can be retrieved. |
3905 | + bugtasks = self._create_items( |
3906 | + 5, self._make_bug, |
3907 | + milestone=self.milestone, |
3908 | + owner=self.owner, |
3909 | + target=self.product, |
3910 | + ) |
3911 | + self.assertContentEqual(bugtasks, self.milestone.bugtasks(self.owner)) |
3912 | + |
3913 | + def test_specification_retrieval(self): |
3914 | + # Ensure that all specifications on a milestone can be retrieved. |
3915 | + specifications = self._create_items( |
3916 | + 5, self.factory.makeSpecification, |
3917 | + milestone=self.milestone, |
3918 | + owner=self.owner, |
3919 | + product=self.product, |
3920 | + ) |
3921 | + self.assertContentEqual(specifications, self.milestone.specifications) |
3922 | |
3923 | === added file 'lib/lp/registry/tests/test_milestonetag.py' |
3924 | --- lib/lp/registry/tests/test_milestonetag.py 1970-01-01 00:00:00 +0000 |
3925 | +++ lib/lp/registry/tests/test_milestonetag.py 2012-01-09 13:42:13 +0000 |
3926 | @@ -0,0 +1,202 @@ |
3927 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
3928 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
3929 | + |
3930 | +"""Milestone related test helper.""" |
3931 | + |
3932 | +__metaclass__ = type |
3933 | + |
3934 | +import datetime |
3935 | + |
3936 | +from lp.testing.layers import ( |
3937 | + DatabaseFunctionalLayer, |
3938 | + ) |
3939 | +from lp.registry.model.milestonetag import ( |
3940 | + MilestoneTag, |
3941 | + ProjectGroupMilestoneTag, |
3942 | + ) |
3943 | +from lp.testing import ( |
3944 | + person_logged_in, |
3945 | + TestCaseWithFactory, |
3946 | + ) |
3947 | + |
3948 | + |
3949 | +class MilestoneTagTest(TestCaseWithFactory): |
3950 | + """Test cases for setting and retrieving milestone tags.""" |
3951 | + |
3952 | + layer = DatabaseFunctionalLayer |
3953 | + |
3954 | + def setUp(self): |
3955 | + super(MilestoneTagTest, self).setUp() |
3956 | + self.milestone = self.factory.makeMilestone() |
3957 | + self.person = self.milestone.target.owner |
3958 | + self.tags = [u'tag2', u'tag1', u'tag3'] |
3959 | + |
3960 | + def test_no_tags(self): |
3961 | + # Ensure a newly created milestone does not have associated tags. |
3962 | + self.assertEquals([], list(self.milestone.getTags())) |
3963 | + |
3964 | + def test_tags_setting_and_retrieval(self): |
3965 | + # Ensure tags are correctly saved and retrieved from the db. |
3966 | + with person_logged_in(self.person): |
3967 | + self.milestone.setTags(self.tags, self.person) |
3968 | + self.assertEqual(sorted(self.tags), list(self.milestone.getTags())) |
3969 | + |
3970 | + def test_tags_override(self): |
3971 | + # Ensure you can override tags already associated with the milestone. |
3972 | + with person_logged_in(self.person): |
3973 | + self.milestone.setTags(self.tags, self.person) |
3974 | + new_tags = [u'tag2', u'tag4', u'tag3'] |
3975 | + self.milestone.setTags(new_tags, self.person) |
3976 | + self.assertEqual(sorted(new_tags), list(self.milestone.getTags())) |
3977 | + |
3978 | + def test_tags_deletion(self): |
3979 | + # Ensure passing an empty sequence of tags deletes them all. |
3980 | + with person_logged_in(self.person): |
3981 | + self.milestone.setTags(self.tags, self.person) |
3982 | + self.milestone.setTags([], self.person) |
3983 | + self.assertEquals([], list(self.milestone.getTags())) |
3984 | + |
3985 | + def test_user_metadata(self): |
3986 | + # Ensure the correct user metadata is created when tags are added. |
3987 | + tag = u'tag1' |
3988 | + with person_logged_in(self.person): |
3989 | + self.milestone.setTags([tag], self.person) |
3990 | + values = self.milestone.getTagsData().values( |
3991 | + MilestoneTag.created_by_id, |
3992 | + MilestoneTag.date_created, |
3993 | + ) |
3994 | + created_by_id, date_created = values.next() |
3995 | + self.assertEqual(self.person.id, created_by_id) |
3996 | + self.assertIsInstance(date_created, datetime.datetime) |
3997 | + |
3998 | + def test_user_metadata_override(self): |
3999 | + # Ensure the user metadata is correct when tags are saved |
4000 | + # multiple times by different users. |
4001 | + new_person = self.factory.makePerson() |
4002 | + with person_logged_in(self.person): |
4003 | + self.milestone.setTags(self.tags, self.person) |
4004 | + new_tags = [u'tag2', u'tag4', u'tag3'] |
4005 | + self.milestone.setTags(new_tags, new_person) |
4006 | + values = self.milestone.getTagsData().values( |
4007 | + MilestoneTag.tag, |
4008 | + MilestoneTag.created_by_id, |
4009 | + ) |
4010 | + tag_person_map = dict(values) |
4011 | + # Old tags are still created by self.person. |
4012 | + for tag in set(self.tags).intersection(new_tags): |
4013 | + self.assertEqual(self.person.id, tag_person_map[tag]) |
4014 | + # Only new tags are created by new_person. |
4015 | + for tag in set(new_tags).difference(self.tags): |
4016 | + self.assertEqual(new_person.id, tag_person_map[tag]) |
4017 | + |
4018 | + |
4019 | +class ProjectGroupMilestoneTagTest(TestCaseWithFactory): |
4020 | + """Test cases for retrieving bugtasks for a milestonetag.""" |
4021 | + |
4022 | + layer = DatabaseFunctionalLayer |
4023 | + |
4024 | + def setUp(self): |
4025 | + super(ProjectGroupMilestoneTagTest, self).setUp() |
4026 | + self.owner = self.factory.makePerson() |
4027 | + self.project_group = self.factory.makeProject(owner=self.owner) |
4028 | + self.product = self.factory.makeProduct( |
4029 | + name="product1", |
4030 | + owner=self.owner, |
4031 | + project=self.project_group) |
4032 | + self.milestone = self.factory.makeMilestone(product=self.product) |
4033 | + |
4034 | + def _create_bugtasks(self, num, milestone=None): |
4035 | + bugtasks = [] |
4036 | + with person_logged_in(self.owner): |
4037 | + for n in xrange(num): |
4038 | + bugtask = self.factory.makeBugTask( |
4039 | + target=self.product, |
4040 | + owner=self.owner) |
4041 | + if milestone: |
4042 | + bugtask.milestone = milestone |
4043 | + bugtasks.append(bugtask) |
4044 | + return bugtasks |
4045 | + |
4046 | + def _create_specifications(self, num, milestone=None): |
4047 | + specifications = [] |
4048 | + with person_logged_in(self.owner): |
4049 | + for n in xrange(num): |
4050 | + specification = self.factory.makeSpecification( |
4051 | + product=self.product, |
4052 | + owner=self.owner, |
4053 | + milestone=milestone) |
4054 | + specifications.append(specification) |
4055 | + return specifications |
4056 | + |
4057 | + def _create_items_for_retrieval(self, factory, tag=u'tag1'): |
4058 | + with person_logged_in(self.owner): |
4059 | + self.milestone.setTags([tag], self.owner) |
4060 | + items = factory(5, self.milestone) |
4061 | + milestonetag = ProjectGroupMilestoneTag( |
4062 | + target=self.project_group, tags=[tag]) |
4063 | + return items, milestonetag |
4064 | + |
4065 | + def _create_items_for_untagged_milestone(self, factory, tag=u'tag1'): |
4066 | + new_milestone = self.factory.makeMilestone(product=self.product) |
4067 | + with person_logged_in(self.owner): |
4068 | + self.milestone.setTags([tag], self.owner) |
4069 | + items = factory(5, self.milestone) |
4070 | + factory(3, new_milestone) |
4071 | + milestonetag = ProjectGroupMilestoneTag( |
4072 | + target=self.project_group, tags=[tag]) |
4073 | + return items, milestonetag |
4074 | + |
4075 | + def _create_items_for_multiple_tags( |
4076 | + self, factory, tags=(u'tag1', u'tag2')): |
4077 | + new_milestone = self.factory.makeMilestone(product=self.product) |
4078 | + with person_logged_in(self.owner): |
4079 | + self.milestone.setTags(tags, self.owner) |
4080 | + new_milestone.setTags(tags[:1], self.owner) |
4081 | + items = factory(5, self.milestone) |
4082 | + factory(3, new_milestone) |
4083 | + milestonetag = ProjectGroupMilestoneTag( |
4084 | + target=self.project_group, tags=tags) |
4085 | + return items, milestonetag |
4086 | + |
4087 | + # Add a test similar to TestProjectExcludeConjoinedMasterSearch in |
4088 | + # lp.bugs.tests.test_bugsearch_conjoined. |
4089 | + |
4090 | + def test_bugtask_retrieve_single_milestone(self): |
4091 | + # Ensure that all bugtasks on a single milestone can be retrieved. |
4092 | + bugtasks, milestonetag = self._create_items_for_retrieval( |
4093 | + self._create_bugtasks) |
4094 | + self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner)) |
4095 | + |
4096 | + def test_bugtasks_for_untagged_milestone(self): |
4097 | + # Ensure that bugtasks for a project group are retrieved |
4098 | + # only if associated with milestones having specified tags. |
4099 | + bugtasks, milestonetag = self._create_items_for_untagged_milestone( |
4100 | + self._create_bugtasks) |
4101 | + self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner)) |
4102 | + |
4103 | + def test_bugtasks_multiple_tags(self): |
4104 | + # Ensure that, in presence of multiple tags, only bugtasks |
4105 | + # for milestones associated with all the tags are retrieved. |
4106 | + bugtasks, milestonetag = self._create_items_for_multiple_tags( |
4107 | + self._create_bugtasks) |
4108 | + self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner)) |
4109 | + |
4110 | + def test_specification_retrieval(self): |
4111 | + # Ensure that all specifications on a milestone can be retrieved. |
4112 | + specs, milestonetag = self._create_items_for_retrieval( |
4113 | + self._create_specifications) |
4114 | + self.assertContentEqual(specs, milestonetag.specifications) |
4115 | + |
4116 | + def test_specifications_for_untagged_milestone(self): |
4117 | + # Ensure that specifications for a project group are retrieved |
4118 | + # only if associated with milestones having specified tags. |
4119 | + specs, milestonetag = self._create_items_for_untagged_milestone( |
4120 | + self._create_specifications) |
4121 | + self.assertContentEqual(specs, milestonetag.specifications) |
4122 | + |
4123 | + def test_specifications_multiple_tags(self): |
4124 | + # Ensure that, in presence of multiple tags, only specifications |
4125 | + # for milestones associated with all the tags are retrieved. |
4126 | + specs, milestonetag = self._create_items_for_multiple_tags( |
4127 | + self._create_specifications) |
4128 | + self.assertContentEqual(specs, milestonetag.specifications) |
4129 | |
4130 | === modified file 'lib/lp/registry/tests/test_person.py' |
4131 | --- lib/lp/registry/tests/test_person.py 2012-01-09 13:42:03 +0000 |
4132 | +++ lib/lp/registry/tests/test_person.py 2012-01-09 10:08:02 +0000 |
4133 | @@ -19,31 +19,8 @@ |
4134 | from zope.security.interfaces import Unauthorized |
4135 | from zope.security.proxy import removeSecurityProxy |
4136 | |
4137 | -<<<<<<< TREE |
4138 | from lazr.lifecycle.snapshot import Snapshot |
4139 | |
4140 | -======= |
4141 | -from canonical.config import config |
4142 | -from canonical.database.sqlbase import cursor, sqlvalues |
4143 | -from canonical.launchpad.database.account import Account |
4144 | -from canonical.launchpad.database.emailaddress import EmailAddress |
4145 | -from canonical.launchpad.interfaces.account import ( |
4146 | - AccountCreationRationale, |
4147 | - AccountStatus, |
4148 | - ) |
4149 | -from canonical.launchpad.interfaces.emailaddress import ( |
4150 | - EmailAddressAlreadyTaken, |
4151 | - EmailAddressStatus, |
4152 | - IEmailAddressSet, |
4153 | - InvalidEmailAddress, |
4154 | - ) |
4155 | -from canonical.launchpad.interfaces.lpstorm import ( |
4156 | - IMasterStore, |
4157 | - IStore, |
4158 | - ) |
4159 | -from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller |
4160 | -from canonical.testing.layers import DatabaseFunctionalLayer |
4161 | ->>>>>>> MERGE-SOURCE |
4162 | from lp.answers.model.answercontact import AnswerContact |
4163 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
4164 | from lp.blueprints.model.specification import Specification |
4165 | @@ -69,34 +46,12 @@ |
4166 | get_recipients, |
4167 | Person, |
4168 | ) |
4169 | -<<<<<<< TREE |
4170 | -from lp.services.identity.interfaces.account import ( |
4171 | - AccountStatus, |
4172 | - ) |
4173 | -from lp.services.identity.interfaces.emailaddress import ( |
4174 | - EmailAddressStatus, |
4175 | - ) |
4176 | -======= |
4177 | -from lp.services.config import config |
4178 | -from lp.services.database.lpstorm import ( |
4179 | - IMasterStore, |
4180 | - IStore, |
4181 | - ) |
4182 | -from lp.services.database.sqlbase import cursor |
4183 | -from lp.services.identity.interfaces.account import ( |
4184 | - AccountCreationRationale, |
4185 | - AccountStatus, |
4186 | - ) |
4187 | -from lp.services.identity.interfaces.emailaddress import ( |
4188 | - EmailAddressAlreadyTaken, |
4189 | - EmailAddressStatus, |
4190 | - IEmailAddressSet, |
4191 | - InvalidEmailAddress, |
4192 | - ) |
4193 | -from lp.services.identity.model.account import Account |
4194 | -from lp.services.identity.model.emailaddress import EmailAddress |
4195 | -from lp.services.openid.model.openididentifier import OpenIdIdentifier |
4196 | ->>>>>>> MERGE-SOURCE |
4197 | +from lp.services.identity.interfaces.account import ( |
4198 | + AccountStatus, |
4199 | + ) |
4200 | +from lp.services.identity.interfaces.emailaddress import ( |
4201 | + EmailAddressStatus, |
4202 | + ) |
4203 | from lp.services.propertycache import clear_property_cache |
4204 | from lp.soyuz.enums import ( |
4205 | ArchivePurpose, |
4206 | @@ -761,761 +716,6 @@ |
4207 | self.assertEqual('(\\u0170-tester)>', displayname) |
4208 | |
4209 | |
4210 | -<<<<<<< TREE |
4211 | -======= |
4212 | -class TestPersonSet(TestCaseWithFactory): |
4213 | - """Test `IPersonSet`.""" |
4214 | - layer = DatabaseFunctionalLayer |
4215 | - |
4216 | - def setUp(self): |
4217 | - super(TestPersonSet, self).setUp() |
4218 | - login(ANONYMOUS) |
4219 | - self.addCleanup(logout) |
4220 | - self.person_set = getUtility(IPersonSet) |
4221 | - |
4222 | - def test_isNameBlacklisted(self): |
4223 | - cursor().execute( |
4224 | - "INSERT INTO NameBlacklist(id, regexp) VALUES (-100, 'foo')") |
4225 | - self.failUnless(self.person_set.isNameBlacklisted('foo')) |
4226 | - self.failIf(self.person_set.isNameBlacklisted('bar')) |
4227 | - |
4228 | - def test_isNameBlacklisted_user_is_admin(self): |
4229 | - team = self.factory.makeTeam() |
4230 | - name_blacklist_set = getUtility(INameBlacklistSet) |
4231 | - self.admin_exp = name_blacklist_set.create(u'fnord', admin=team) |
4232 | - self.store = IStore(self.admin_exp) |
4233 | - self.store.flush() |
4234 | - user = team.teamowner |
4235 | - self.assertFalse(self.person_set.isNameBlacklisted('fnord', user)) |
4236 | - |
4237 | - def test_getByEmail_ignores_case_and_whitespace(self): |
4238 | - person1_email = 'foo.bar@canonical.com' |
4239 | - person1 = self.person_set.getByEmail(person1_email) |
4240 | - self.failIf( |
4241 | - person1 is None, |
4242 | - "PersonSet.getByEmail() could not find %r" % person1_email) |
4243 | - |
4244 | - person2 = self.person_set.getByEmail(' foo.BAR@canonICAL.com ') |
4245 | - self.failIf( |
4246 | - person2 is None, |
4247 | - "PersonSet.getByEmail() should ignore case and whitespace.") |
4248 | - self.assertEqual(person1, person2) |
4249 | - |
4250 | - def test_getPrecachedPersonsFromIDs(self): |
4251 | - # The getPrecachedPersonsFromIDs() method should only make one |
4252 | - # query to load all the extraneous data. Accessing the |
4253 | - # attributes should then cause zero queries. |
4254 | - person_ids = [ |
4255 | - self.factory.makePerson().id |
4256 | - for i in range(3)] |
4257 | - |
4258 | - with StormStatementRecorder() as recorder: |
4259 | - persons = list(self.person_set.getPrecachedPersonsFromIDs( |
4260 | - person_ids, need_karma=True, need_ubuntu_coc=True, |
4261 | - need_location=True, need_archive=True, |
4262 | - need_preferred_email=True, need_validity=True)) |
4263 | - self.assertThat(recorder, HasQueryCount(LessThan(2))) |
4264 | - |
4265 | - with StormStatementRecorder() as recorder: |
4266 | - for person in persons: |
4267 | - person.is_valid_person |
4268 | - person.karma |
4269 | - person.is_ubuntu_coc_signer |
4270 | - person.location |
4271 | - person.archive |
4272 | - person.preferredemail |
4273 | - self.assertThat(recorder, HasQueryCount(LessThan(1))) |
4274 | - |
4275 | - |
4276 | -class KarmaTestMixin: |
4277 | - """Helper methods for setting karma.""" |
4278 | - |
4279 | - def _makeKarmaCache(self, person, product, category_name_values): |
4280 | - """Create a KarmaCache entry with the given arguments. |
4281 | - |
4282 | - In order to create the KarmaCache record we must switch to the DB |
4283 | - user 'karma'. This invalidates the objects under test so they |
4284 | - must be retrieved again. |
4285 | - """ |
4286 | - with dbuser('karma'): |
4287 | - total = 0 |
4288 | - # Insert category total for person and project. |
4289 | - for category_name, value in category_name_values: |
4290 | - category = KarmaCategory.byName(category_name) |
4291 | - self.cache_manager.new( |
4292 | - value, person.id, category.id, product_id=product.id) |
4293 | - total += value |
4294 | - # Insert total cache for person and project. |
4295 | - self.cache_manager.new( |
4296 | - total, person.id, None, product_id=product.id) |
4297 | - |
4298 | - def _makeKarmaTotalCache(self, person, total): |
4299 | - """Create a KarmaTotalCache entry. |
4300 | - |
4301 | - In order to create the KarmaTotalCache record we must switch to the DB |
4302 | - user 'karma'. This invalidates the objects under test so they |
4303 | - must be retrieved again. |
4304 | - """ |
4305 | - with dbuser('karma'): |
4306 | - KarmaTotalCache(person=person.id, karma_total=total) |
4307 | - |
4308 | - |
4309 | -class TestPersonSetMerge(TestCaseWithFactory, KarmaTestMixin): |
4310 | - """Test cases for PersonSet merge.""" |
4311 | - |
4312 | - layer = DatabaseFunctionalLayer |
4313 | - |
4314 | - def setUp(self): |
4315 | - super(TestPersonSetMerge, self).setUp() |
4316 | - self.person_set = getUtility(IPersonSet) |
4317 | - |
4318 | - def _do_premerge(self, from_person, to_person): |
4319 | - # Do the pre merge work performed by the LoginToken. |
4320 | - with celebrity_logged_in('admin'): |
4321 | - email = from_person.preferredemail |
4322 | - email.status = EmailAddressStatus.NEW |
4323 | - store = IMasterStore(EmailAddress) |
4324 | - # EmailAddress.acount and .person need to be updated at the |
4325 | - # same time to prevent the constraints on the account field |
4326 | - # from kicking the change out. |
4327 | - store.execute(""" |
4328 | - UPDATE EmailAddress SET |
4329 | - person = %s, |
4330 | - account = %s |
4331 | - WHERE id = %s |
4332 | - """ % sqlvalues( |
4333 | - to_person.id, to_person.accountID, email.id)) |
4334 | - transaction.commit() |
4335 | - |
4336 | - def _do_merge(self, from_person, to_person, reviewer=None): |
4337 | - # Perform the merge as the db user that will be used by the jobs. |
4338 | - with dbuser(config.IPersonMergeJobSource.dbuser): |
4339 | - self.person_set.merge(from_person, to_person, reviewer=reviewer) |
4340 | - return from_person, to_person |
4341 | - |
4342 | - def _get_testable_account(self, person, date_created, openid_identifier): |
4343 | - # Return a naked account with predictable attributes. |
4344 | - account = removeSecurityProxy(person.account) |
4345 | - account.date_created = date_created |
4346 | - account.openid_identifier = openid_identifier |
4347 | - return account |
4348 | - |
4349 | - def test_delete_no_notifications(self): |
4350 | - team = self.factory.makeTeam() |
4351 | - owner = team.teamowner |
4352 | - transaction.commit() |
4353 | - with dbuser(config.IPersonMergeJobSource.dbuser): |
4354 | - self.person_set.delete(team, owner) |
4355 | - notification_set = getUtility(IPersonNotificationSet) |
4356 | - notifications = notification_set.getNotificationsToSend() |
4357 | - self.assertEqual(0, notifications.count()) |
4358 | - |
4359 | - def test_openid_identifiers(self): |
4360 | - # Verify that OpenId Identifiers are merged. |
4361 | - duplicate = self.factory.makePerson() |
4362 | - duplicate_identifier = removeSecurityProxy( |
4363 | - duplicate.account).openid_identifiers.any().identifier |
4364 | - person = self.factory.makePerson() |
4365 | - person_identifier = removeSecurityProxy( |
4366 | - person.account).openid_identifiers.any().identifier |
4367 | - self._do_premerge(duplicate, person) |
4368 | - login_person(person) |
4369 | - duplicate, person = self._do_merge(duplicate, person) |
4370 | - self.assertEqual( |
4371 | - 0, |
4372 | - removeSecurityProxy(duplicate.account).openid_identifiers.count()) |
4373 | - |
4374 | - merged_identifiers = [ |
4375 | - identifier.identifier for identifier in |
4376 | - removeSecurityProxy(person.account).openid_identifiers] |
4377 | - |
4378 | - self.assertIn(duplicate_identifier, merged_identifiers) |
4379 | - self.assertIn(person_identifier, merged_identifiers) |
4380 | - |
4381 | - def test_karmacache_transferred_to_user_has_no_karma(self): |
4382 | - # Verify that the merged user has no KarmaCache entries, |
4383 | - # and the karma total was transfered. |
4384 | - self.cache_manager = getUtility(IKarmaCacheManager) |
4385 | - product = self.factory.makeProduct() |
4386 | - duplicate = self.factory.makePerson() |
4387 | - self._makeKarmaCache( |
4388 | - duplicate, product, [('bugs', 10)]) |
4389 | - self._makeKarmaTotalCache(duplicate, 15) |
4390 | - # The karma changes invalidated duplicate instance. |
4391 | - duplicate = self.person_set.get(duplicate.id) |
4392 | - person = self.factory.makePerson() |
4393 | - self._do_premerge(duplicate, person) |
4394 | - login_person(person) |
4395 | - duplicate, person = self._do_merge(duplicate, person) |
4396 | - self.assertEqual([], duplicate.karma_category_caches) |
4397 | - self.assertEqual(0, duplicate.karma) |
4398 | - self.assertEqual(15, person.karma) |
4399 | - |
4400 | - def test_karmacache_transferred_to_user_has_karma(self): |
4401 | - # Verify that the merged user has no KarmaCache entries, |
4402 | - # and the karma total was summed. |
4403 | - self.cache_manager = getUtility(IKarmaCacheManager) |
4404 | - product = self.factory.makeProduct() |
4405 | - duplicate = self.factory.makePerson() |
4406 | - self._makeKarmaCache( |
4407 | - duplicate, product, [('bugs', 10)]) |
4408 | - self._makeKarmaTotalCache(duplicate, 15) |
4409 | - person = self.factory.makePerson() |
4410 | - self._makeKarmaCache( |
4411 | - person, product, [('bugs', 9)]) |
4412 | - self._makeKarmaTotalCache(person, 13) |
4413 | - # The karma changes invalidated duplicate and person instances. |
4414 | - duplicate = self.person_set.get(duplicate.id) |
4415 | - person = self.person_set.get(person.id) |
4416 | - self._do_premerge(duplicate, person) |
4417 | - login_person(person) |
4418 | - duplicate, person = self._do_merge(duplicate, person) |
4419 | - self.assertEqual([], duplicate.karma_category_caches) |
4420 | - self.assertEqual(0, duplicate.karma) |
4421 | - self.assertEqual(28, person.karma) |
4422 | - |
4423 | - def test_person_date_created_preserved(self): |
4424 | - # Verify that the oldest datecreated is merged. |
4425 | - person = self.factory.makePerson() |
4426 | - duplicate = self.factory.makePerson() |
4427 | - oldest_date = datetime( |
4428 | - 2005, 11, 25, 0, 0, 0, 0, pytz.timezone('UTC')) |
4429 | - removeSecurityProxy(duplicate).datecreated = oldest_date |
4430 | - self._do_premerge(duplicate, person) |
4431 | - login_person(person) |
4432 | - duplicate, person = self._do_merge(duplicate, person) |
4433 | - self.assertEqual(oldest_date, person.datecreated) |
4434 | - |
4435 | - def test_team_with_active_mailing_list_raises_error(self): |
4436 | - # A team with an active mailing list cannot be merged. |
4437 | - target_team = self.factory.makeTeam() |
4438 | - test_team = self.factory.makeTeam() |
4439 | - self.factory.makeMailingList( |
4440 | - test_team, test_team.teamowner) |
4441 | - self.assertRaises( |
4442 | - AssertionError, self.person_set.merge, test_team, target_team) |
4443 | - |
4444 | - def test_team_with_inactive_mailing_list(self): |
4445 | - # A team with an inactive mailing list can be merged. |
4446 | - target_team = self.factory.makeTeam() |
4447 | - test_team = self.factory.makeTeam() |
4448 | - mailing_list = self.factory.makeMailingList( |
4449 | - test_team, test_team.teamowner) |
4450 | - mailing_list.deactivate() |
4451 | - mailing_list.transitionToStatus(MailingListStatus.INACTIVE) |
4452 | - test_team, target_team = self._do_merge( |
4453 | - test_team, target_team, test_team.teamowner) |
4454 | - self.assertEqual(target_team, test_team.merged) |
4455 | - self.assertEqual( |
4456 | - MailingListStatus.PURGED, test_team.mailing_list.status) |
4457 | - emails = getUtility(IEmailAddressSet).getByPerson(target_team).count() |
4458 | - self.assertEqual(0, emails) |
4459 | - |
4460 | - def test_team_with_purged_mailing_list(self): |
4461 | - # A team with a purges mailing list can be merged. |
4462 | - target_team = self.factory.makeTeam() |
4463 | - test_team = self.factory.makeTeam() |
4464 | - mailing_list = self.factory.makeMailingList( |
4465 | - test_team, test_team.teamowner) |
4466 | - mailing_list.deactivate() |
4467 | - mailing_list.transitionToStatus(MailingListStatus.INACTIVE) |
4468 | - mailing_list.purge() |
4469 | - test_team, target_team = self._do_merge( |
4470 | - test_team, target_team, test_team.teamowner) |
4471 | - self.assertEqual(target_team, test_team.merged) |
4472 | - |
4473 | - def test_team_with_members(self): |
4474 | - # Team members are removed before merging. |
4475 | - target_team = self.factory.makeTeam() |
4476 | - test_team = self.factory.makeTeam() |
4477 | - former_member = self.factory.makePerson() |
4478 | - with person_logged_in(test_team.teamowner): |
4479 | - test_team.addMember(former_member, test_team.teamowner) |
4480 | - test_team, target_team = self._do_merge( |
4481 | - test_team, target_team, test_team.teamowner) |
4482 | - self.assertEqual(target_team, test_team.merged) |
4483 | - self.assertEqual([], list(former_member.super_teams)) |
4484 | - |
4485 | - def test_team_without_super_teams_is_fine(self): |
4486 | - # A team with no members and no super teams |
4487 | - # merges without errors. |
4488 | - test_team = self.factory.makeTeam() |
4489 | - target_team = self.factory.makeTeam() |
4490 | - login_person(test_team.teamowner) |
4491 | - self._do_merge(test_team, target_team, test_team.teamowner) |
4492 | - |
4493 | - def test_team_with_super_teams(self): |
4494 | - # A team with superteams can be merged, but the memberships |
4495 | - # are not transferred. |
4496 | - test_team = self.factory.makeTeam() |
4497 | - super_team = self.factory.makeTeam() |
4498 | - target_team = self.factory.makeTeam() |
4499 | - login_person(test_team.teamowner) |
4500 | - test_team.join(super_team, test_team.teamowner) |
4501 | - test_team, target_team = self._do_merge( |
4502 | - test_team, target_team, test_team.teamowner) |
4503 | - self.assertEqual(target_team, test_team.merged) |
4504 | - self.assertEqual([], list(target_team.super_teams)) |
4505 | - |
4506 | - def test_merge_moves_branches(self): |
4507 | - # When person/teams are merged, branches owned by the from person |
4508 | - # are moved. |
4509 | - person = self.factory.makePerson() |
4510 | - branch = self.factory.makeBranch() |
4511 | - duplicate = branch.owner |
4512 | - self._do_premerge(branch.owner, person) |
4513 | - login_person(person) |
4514 | - duplicate, person = self._do_merge(duplicate, person) |
4515 | - branches = person.getBranches() |
4516 | - self.assertEqual(1, branches.count()) |
4517 | - |
4518 | - def test_merge_with_duplicated_branches(self): |
4519 | - # If both the from and to people have branches with the same name, |
4520 | - # merging renames the duplicate from the from person's side. |
4521 | - product = self.factory.makeProduct() |
4522 | - from_branch = self.factory.makeBranch(name='foo', product=product) |
4523 | - to_branch = self.factory.makeBranch(name='foo', product=product) |
4524 | - mergee = to_branch.owner |
4525 | - duplicate = from_branch.owner |
4526 | - self._do_premerge(duplicate, mergee) |
4527 | - login_person(mergee) |
4528 | - duplicate, mergee = self._do_merge(duplicate, mergee) |
4529 | - branches = [b.name for b in mergee.getBranches()] |
4530 | - self.assertEqual(2, len(branches)) |
4531 | - self.assertContentEqual([u'foo', u'foo-1'], branches) |
4532 | - |
4533 | - def test_merge_moves_recipes(self): |
4534 | - # When person/teams are merged, recipes owned by the from person are |
4535 | - # moved. |
4536 | - person = self.factory.makePerson() |
4537 | - recipe = self.factory.makeSourcePackageRecipe() |
4538 | - duplicate = recipe.owner |
4539 | - # Delete the PPA, which is required for the merge to work. |
4540 | - with person_logged_in(duplicate): |
4541 | - recipe.owner.archive.status = ArchiveStatus.DELETED |
4542 | - self._do_premerge(duplicate, person) |
4543 | - login_person(person) |
4544 | - duplicate, person = self._do_merge(duplicate, person) |
4545 | - self.assertEqual(1, person.recipes.count()) |
4546 | - |
4547 | - def test_merge_with_duplicated_recipes(self): |
4548 | - # If both the from and to people have recipes with the same name, |
4549 | - # merging renames the duplicate from the from person's side. |
4550 | - merge_from = self.factory.makeSourcePackageRecipe( |
4551 | - name=u'foo', description=u'FROM') |
4552 | - merge_to = self.factory.makeSourcePackageRecipe( |
4553 | - name=u'foo', description=u'TO') |
4554 | - duplicate = merge_from.owner |
4555 | - mergee = merge_to.owner |
4556 | - # Delete merge_from's PPA, which is required for the merge to work. |
4557 | - with person_logged_in(merge_from.owner): |
4558 | - merge_from.owner.archive.status = ArchiveStatus.DELETED |
4559 | - self._do_premerge(merge_from.owner, mergee) |
4560 | - login_person(mergee) |
4561 | - duplicate, mergee = self._do_merge(duplicate, mergee) |
4562 | - recipes = mergee.recipes |
4563 | - self.assertEqual(2, recipes.count()) |
4564 | - descriptions = [r.description for r in recipes] |
4565 | - self.assertEqual([u'TO', u'FROM'], descriptions) |
4566 | - self.assertEqual(u'foo-1', recipes[1].name) |
4567 | - |
4568 | - def assertSubscriptionMerges(self, target): |
4569 | - # Given a subscription target, we want to make sure that subscriptions |
4570 | - # that the duplicate person made are carried over to the merged |
4571 | - # account. |
4572 | - duplicate = self.factory.makePerson() |
4573 | - with person_logged_in(duplicate): |
4574 | - target.addSubscription(duplicate, duplicate) |
4575 | - person = self.factory.makePerson() |
4576 | - self._do_premerge(duplicate, person) |
4577 | - login_person(person) |
4578 | - duplicate, person = self._do_merge(duplicate, person) |
4579 | - # The merged person has the subscription, and the duplicate person |
4580 | - # does not. |
4581 | - self.assertTrue(target.getSubscription(person) is not None) |
4582 | - self.assertTrue(target.getSubscription(duplicate) is None) |
4583 | - |
4584 | - def assertConflictingSubscriptionDeletes(self, target): |
4585 | - # Given a subscription target, we want to make sure that subscriptions |
4586 | - # that the duplicate person made that conflict with existing |
4587 | - # subscriptions in the merged account are deleted. |
4588 | - duplicate = self.factory.makePerson() |
4589 | - person = self.factory.makePerson() |
4590 | - with person_logged_in(duplicate): |
4591 | - target.addSubscription(duplicate, duplicate) |
4592 | - with person_logged_in(person): |
4593 | - # The description lets us show that we still have the right |
4594 | - # subscription later. |
4595 | - target.addBugSubscriptionFilter(person, person).description = ( |
4596 | - u'a marker') |
4597 | - self._do_premerge(duplicate, person) |
4598 | - login_person(person) |
4599 | - duplicate, person = self._do_merge(duplicate, person) |
4600 | - # The merged person still has the original subscription, as shown |
4601 | - # by the marker name. |
4602 | - self.assertEqual( |
4603 | - target.getSubscription(person).bug_filters[0].description, |
4604 | - u'a marker') |
4605 | - # The conflicting subscription on the duplicate has been deleted. |
4606 | - self.assertTrue(target.getSubscription(duplicate) is None) |
4607 | - |
4608 | - def test_merge_with_product_subscription(self): |
4609 | - # See comments in assertSubscriptionMerges. |
4610 | - self.assertSubscriptionMerges(self.factory.makeProduct()) |
4611 | - |
4612 | - def test_merge_with_conflicting_product_subscription(self): |
4613 | - # See comments in assertConflictingSubscriptionDeletes. |
4614 | - self.assertConflictingSubscriptionDeletes(self.factory.makeProduct()) |
4615 | - |
4616 | - def test_merge_with_project_subscription(self): |
4617 | - # See comments in assertSubscriptionMerges. |
4618 | - self.assertSubscriptionMerges(self.factory.makeProject()) |
4619 | - |
4620 | - def test_merge_with_conflicting_project_subscription(self): |
4621 | - # See comments in assertConflictingSubscriptionDeletes. |
4622 | - self.assertConflictingSubscriptionDeletes(self.factory.makeProject()) |
4623 | - |
4624 | - def test_merge_with_distroseries_subscription(self): |
4625 | - # See comments in assertSubscriptionMerges. |
4626 | - self.assertSubscriptionMerges(self.factory.makeDistroSeries()) |
4627 | - |
4628 | - def test_merge_with_conflicting_distroseries_subscription(self): |
4629 | - # See comments in assertConflictingSubscriptionDeletes. |
4630 | - self.assertConflictingSubscriptionDeletes( |
4631 | - self.factory.makeDistroSeries()) |
4632 | - |
4633 | - def test_merge_with_milestone_subscription(self): |
4634 | - # See comments in assertSubscriptionMerges. |
4635 | - self.assertSubscriptionMerges(self.factory.makeMilestone()) |
4636 | - |
4637 | - def test_merge_with_conflicting_milestone_subscription(self): |
4638 | - # See comments in assertConflictingSubscriptionDeletes. |
4639 | - self.assertConflictingSubscriptionDeletes( |
4640 | - self.factory.makeMilestone()) |
4641 | - |
4642 | - def test_merge_with_productseries_subscription(self): |
4643 | - # See comments in assertSubscriptionMerges. |
4644 | - self.assertSubscriptionMerges(self.factory.makeProductSeries()) |
4645 | - |
4646 | - def test_merge_with_conflicting_productseries_subscription(self): |
4647 | - # See comments in assertConflictingSubscriptionDeletes. |
4648 | - self.assertConflictingSubscriptionDeletes( |
4649 | - self.factory.makeProductSeries()) |
4650 | - |
4651 | - def test_merge_with_distribution_subscription(self): |
4652 | - # See comments in assertSubscriptionMerges. |
4653 | - self.assertSubscriptionMerges(self.factory.makeDistribution()) |
4654 | - |
4655 | - def test_merge_with_conflicting_distribution_subscription(self): |
4656 | - # See comments in assertConflictingSubscriptionDeletes. |
4657 | - self.assertConflictingSubscriptionDeletes( |
4658 | - self.factory.makeDistribution()) |
4659 | - |
4660 | - def test_merge_with_sourcepackage_subscription(self): |
4661 | - # See comments in assertSubscriptionMerges. |
4662 | - dsp = self.factory.makeDistributionSourcePackage() |
4663 | - self.assertSubscriptionMerges(dsp) |
4664 | - |
4665 | - def test_merge_with_conflicting_sourcepackage_subscription(self): |
4666 | - # See comments in assertConflictingSubscriptionDeletes. |
4667 | - dsp = self.factory.makeDistributionSourcePackage() |
4668 | - self.assertConflictingSubscriptionDeletes(dsp) |
4669 | - |
4670 | - def test_merge_accesspolicygrants(self): |
4671 | - # AccessPolicyGrants are transferred from the duplicate. |
4672 | - person = self.factory.makePerson() |
4673 | - grant = self.factory.makeAccessPolicyGrant() |
4674 | - self._do_premerge(grant.grantee, person) |
4675 | - with person_logged_in(person): |
4676 | - self._do_merge(grant.grantee, person) |
4677 | - self.assertEqual(person, grant.grantee) |
4678 | - |
4679 | - def test_merge_accesspolicygrants_conflicts(self): |
4680 | - # Conflicting AccessPolicyGrants are deleted. |
4681 | - policy = self.factory.makeAccessPolicy() |
4682 | - |
4683 | - person = self.factory.makePerson() |
4684 | - person_grantor = self.factory.makePerson() |
4685 | - person_grant = self.factory.makeAccessPolicyGrant( |
4686 | - grantee=person, grantor=person_grantor, object=policy) |
4687 | - |
4688 | - duplicate = self.factory.makePerson() |
4689 | - duplicate_grantor = self.factory.makePerson() |
4690 | - duplicate_grant = self.factory.makeAccessPolicyGrant( |
4691 | - grantee=duplicate, grantor=duplicate_grantor, object=policy) |
4692 | - |
4693 | - self._do_premerge(duplicate, person) |
4694 | - with person_logged_in(person): |
4695 | - self._do_merge(duplicate, person) |
4696 | - transaction.commit() |
4697 | - |
4698 | - self.assertEqual(person, person_grant.grantee) |
4699 | - self.assertEqual(person_grantor, person_grant.grantor) |
4700 | - self.assertIs( |
4701 | - None, |
4702 | - IStore(AccessPolicyGrant).get( |
4703 | - AccessPolicyGrant, duplicate_grant.id)) |
4704 | - |
4705 | - def test_mergeAsync(self): |
4706 | - # mergeAsync() creates a new `PersonMergeJob`. |
4707 | - from_person = self.factory.makePerson() |
4708 | - to_person = self.factory.makePerson() |
4709 | - login_person(from_person) |
4710 | - job = self.person_set.mergeAsync(from_person, to_person) |
4711 | - self.assertEqual(from_person, job.from_person) |
4712 | - self.assertEqual(to_person, job.to_person) |
4713 | - |
4714 | - |
4715 | -class TestPersonSetCreateByOpenId(TestCaseWithFactory): |
4716 | - layer = DatabaseFunctionalLayer |
4717 | - |
4718 | - def setUp(self): |
4719 | - super(TestPersonSetCreateByOpenId, self).setUp() |
4720 | - self.person_set = getUtility(IPersonSet) |
4721 | - self.store = IMasterStore(Account) |
4722 | - |
4723 | - # Generate some valid test data. |
4724 | - self.account = self.makeAccount() |
4725 | - self.identifier = self.makeOpenIdIdentifier(self.account, u'whatever') |
4726 | - self.person = self.makePerson(self.account) |
4727 | - self.email = self.makeEmailAddress( |
4728 | - email='whatever@example.com', |
4729 | - account=self.account, person=self.person) |
4730 | - |
4731 | - def makeAccount(self): |
4732 | - return self.store.add(Account( |
4733 | - displayname='Displayname', |
4734 | - creation_rationale=AccountCreationRationale.UNKNOWN, |
4735 | - status=AccountStatus.ACTIVE)) |
4736 | - |
4737 | - def makeOpenIdIdentifier(self, account, identifier): |
4738 | - openid_identifier = OpenIdIdentifier() |
4739 | - openid_identifier.identifier = identifier |
4740 | - openid_identifier.account = account |
4741 | - return self.store.add(openid_identifier) |
4742 | - |
4743 | - def makePerson(self, account): |
4744 | - return self.store.add(Person( |
4745 | - name='acc%d' % account.id, account=account, |
4746 | - displayname='Displayname', |
4747 | - creation_rationale=PersonCreationRationale.UNKNOWN)) |
4748 | - |
4749 | - def makeEmailAddress(self, email, account, person): |
4750 | - return self.store.add(EmailAddress( |
4751 | - email=email, |
4752 | - account=account, |
4753 | - person=person, |
4754 | - status=EmailAddressStatus.PREFERRED)) |
4755 | - |
4756 | - def testAllValid(self): |
4757 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4758 | - self.identifier.identifier, self.email.email, 'Ignored Name', |
4759 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4760 | - found = removeSecurityProxy(found) |
4761 | - |
4762 | - self.assertIs(False, updated) |
4763 | - self.assertIs(self.person, found) |
4764 | - self.assertIs(self.account, found.account) |
4765 | - self.assertIs(self.email, found.preferredemail) |
4766 | - self.assertIs(self.email.account, self.account) |
4767 | - self.assertIs(self.email.person, self.person) |
4768 | - self.assertEqual( |
4769 | - [self.identifier], list(self.account.openid_identifiers)) |
4770 | - |
4771 | - def testEmailAddressCaseInsensitive(self): |
4772 | - # As per testAllValid, but the email address used for the lookup |
4773 | - # is all upper case. |
4774 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4775 | - self.identifier.identifier, self.email.email.upper(), |
4776 | - 'Ignored Name', PersonCreationRationale.UNKNOWN, 'No Comment') |
4777 | - found = removeSecurityProxy(found) |
4778 | - |
4779 | - self.assertIs(False, updated) |
4780 | - self.assertIs(self.person, found) |
4781 | - self.assertIs(self.account, found.account) |
4782 | - self.assertIs(self.email, found.preferredemail) |
4783 | - self.assertIs(self.email.account, self.account) |
4784 | - self.assertIs(self.email.person, self.person) |
4785 | - self.assertEqual( |
4786 | - [self.identifier], list(self.account.openid_identifiers)) |
4787 | - |
4788 | - def testNewOpenId(self): |
4789 | - # Account looked up by email and the new OpenId identifier |
4790 | - # attached. We can do this because we trust our OpenId Provider. |
4791 | - new_identifier = u'newident' |
4792 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4793 | - new_identifier, self.email.email, 'Ignored Name', |
4794 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4795 | - found = removeSecurityProxy(found) |
4796 | - |
4797 | - self.assertIs(True, updated) |
4798 | - self.assertIs(self.person, found) |
4799 | - self.assertIs(self.account, found.account) |
4800 | - self.assertIs(self.email, found.preferredemail) |
4801 | - self.assertIs(self.email.account, self.account) |
4802 | - self.assertIs(self.email.person, self.person) |
4803 | - |
4804 | - # Old OpenId Identifier still attached. |
4805 | - self.assertIn(self.identifier, list(self.account.openid_identifiers)) |
4806 | - |
4807 | - # So is our new one. |
4808 | - identifiers = [ |
4809 | - identifier.identifier for identifier |
4810 | - in self.account.openid_identifiers] |
4811 | - self.assertIn(new_identifier, identifiers) |
4812 | - |
4813 | - def testNewEmailAddress(self): |
4814 | - # Account looked up by OpenId identifier and new EmailAddress |
4815 | - # attached. We can do this because we trust our OpenId Provider. |
4816 | - new_email = u'new_email@example.com' |
4817 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4818 | - self.identifier.identifier, new_email, 'Ignored Name', |
4819 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4820 | - found = removeSecurityProxy(found) |
4821 | - |
4822 | - self.assertIs(True, updated) |
4823 | - self.assertIs(self.person, found) |
4824 | - self.assertIs(self.account, found.account) |
4825 | - self.assertEqual( |
4826 | - [self.identifier], list(self.account.openid_identifiers)) |
4827 | - |
4828 | - # The old email address is still there and correctly linked. |
4829 | - self.assertIs(self.email, found.preferredemail) |
4830 | - self.assertIs(self.email.account, self.account) |
4831 | - self.assertIs(self.email.person, self.person) |
4832 | - |
4833 | - # The new email address is there too and correctly linked. |
4834 | - new_email = self.store.find(EmailAddress, email=new_email).one() |
4835 | - self.assertIs(new_email.account, self.account) |
4836 | - self.assertIs(new_email.person, self.person) |
4837 | - self.assertEqual(EmailAddressStatus.NEW, new_email.status) |
4838 | - |
4839 | - def testNewAccountAndIdentifier(self): |
4840 | - # If neither the OpenId Identifier nor the email address are |
4841 | - # found, we create everything. |
4842 | - new_email = u'new_email@example.com' |
4843 | - new_identifier = u'new_identifier' |
4844 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4845 | - new_identifier, new_email, 'New Name', |
4846 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4847 | - found = removeSecurityProxy(found) |
4848 | - |
4849 | - # We have a new Person |
4850 | - self.assertIs(True, updated) |
4851 | - self.assertIsNot(None, found) |
4852 | - |
4853 | - # It is correctly linked to an account, emailaddress and |
4854 | - # identifier. |
4855 | - self.assertIs(found, found.preferredemail.person) |
4856 | - self.assertIs(found.account, found.preferredemail.account) |
4857 | - self.assertEqual( |
4858 | - new_identifier, found.account.openid_identifiers.any().identifier) |
4859 | - |
4860 | - def testNoPerson(self): |
4861 | - # If the account is not linked to a Person, create one. ShipIt |
4862 | - # users fall into this category the first time they log into |
4863 | - # Launchpad. |
4864 | - self.email.person = None |
4865 | - self.person.account = None |
4866 | - |
4867 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4868 | - self.identifier.identifier, self.email.email, 'New Name', |
4869 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4870 | - found = removeSecurityProxy(found) |
4871 | - |
4872 | - # We have a new Person |
4873 | - self.assertIs(True, updated) |
4874 | - self.assertIsNot(self.person, found) |
4875 | - |
4876 | - # It is correctly linked to an account, emailaddress and |
4877 | - # identifier. |
4878 | - self.assertIs(found, found.preferredemail.person) |
4879 | - self.assertIs(found.account, found.preferredemail.account) |
4880 | - self.assertIn(self.identifier, list(found.account.openid_identifiers)) |
4881 | - |
4882 | - def testNoAccount(self): |
4883 | - # EmailAddress is linked to a Person, but there is no Account. |
4884 | - # Convert this stub into something valid. |
4885 | - self.email.account = None |
4886 | - self.email.status = EmailAddressStatus.NEW |
4887 | - self.person.account = None |
4888 | - new_identifier = u'new_identifier' |
4889 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4890 | - new_identifier, self.email.email, 'Ignored', |
4891 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4892 | - found = removeSecurityProxy(found) |
4893 | - |
4894 | - self.assertIs(True, updated) |
4895 | - |
4896 | - self.assertIsNot(None, found.account) |
4897 | - self.assertEqual( |
4898 | - new_identifier, found.account.openid_identifiers.any().identifier) |
4899 | - self.assertIs(self.email.person, found) |
4900 | - self.assertIs(self.email.account, found.account) |
4901 | - self.assertEqual(EmailAddressStatus.PREFERRED, self.email.status) |
4902 | - |
4903 | - def testMovedEmailAddress(self): |
4904 | - # The EmailAddress and OpenId Identifier are both in the |
4905 | - # database, but they are not linked to the same account. The |
4906 | - # identifier needs to be relinked to the correct account - the |
4907 | - # user able to log into the trusted SSO with that email address |
4908 | - # should be able to log into Launchpad with that email address. |
4909 | - # This lets us cope with the SSO migrating email addresses |
4910 | - # between SSO accounts. |
4911 | - self.identifier.account = self.store.find( |
4912 | - Account, displayname='Foo Bar').one() |
4913 | - |
4914 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
4915 | - self.identifier.identifier, self.email.email, 'New Name', |
4916 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
4917 | - found = removeSecurityProxy(found) |
4918 | - |
4919 | - self.assertIs(True, updated) |
4920 | - self.assertIs(self.person, found) |
4921 | - |
4922 | - self.assertIs(found.account, self.identifier.account) |
4923 | - self.assertIn(self.identifier, list(found.account.openid_identifiers)) |
4924 | - |
4925 | - |
4926 | -class TestCreatePersonAndEmail(TestCase): |
4927 | - """Test `IPersonSet`.createPersonAndEmail().""" |
4928 | - layer = DatabaseFunctionalLayer |
4929 | - |
4930 | - def setUp(self): |
4931 | - TestCase.setUp(self) |
4932 | - login(ANONYMOUS) |
4933 | - self.addCleanup(logout) |
4934 | - self.person_set = getUtility(IPersonSet) |
4935 | - |
4936 | - def test_duplicated_name_not_accepted(self): |
4937 | - self.person_set.createPersonAndEmail( |
4938 | - 'testing@example.com', PersonCreationRationale.UNKNOWN, |
4939 | - name='zzzz') |
4940 | - self.assertRaises( |
4941 | - NameAlreadyTaken, self.person_set.createPersonAndEmail, |
4942 | - 'testing2@example.com', PersonCreationRationale.UNKNOWN, |
4943 | - name='zzzz') |
4944 | - |
4945 | - def test_duplicated_email_not_accepted(self): |
4946 | - self.person_set.createPersonAndEmail( |
4947 | - 'testing@example.com', PersonCreationRationale.UNKNOWN) |
4948 | - self.assertRaises( |
4949 | - EmailAddressAlreadyTaken, self.person_set.createPersonAndEmail, |
4950 | - 'testing@example.com', PersonCreationRationale.UNKNOWN) |
4951 | - |
4952 | - def test_invalid_email_not_accepted(self): |
4953 | - self.assertRaises( |
4954 | - InvalidEmailAddress, self.person_set.createPersonAndEmail, |
4955 | - 'testing@.com', PersonCreationRationale.UNKNOWN) |
4956 | - |
4957 | - def test_invalid_name_not_accepted(self): |
4958 | - self.assertRaises( |
4959 | - InvalidName, self.person_set.createPersonAndEmail, |
4960 | - 'testing@example.com', PersonCreationRationale.UNKNOWN, |
4961 | - name='/john') |
4962 | - |
4963 | - |
4964 | ->>>>>>> MERGE-SOURCE |
4965 | class TestPersonRelatedBugTaskSearch(TestCaseWithFactory): |
4966 | |
4967 | layer = DatabaseFunctionalLayer |
4968 | |
4969 | === modified file 'lib/lp/registry/tests/test_personset.py' |
4970 | --- lib/lp/registry/tests/test_personset.py 2012-01-09 13:42:03 +0000 |
4971 | +++ lib/lp/registry/tests/test_personset.py 2012-01-06 15:14:48 +0000 |
4972 | @@ -1,8 +1,4 @@ |
4973 | -<<<<<<< TREE |
4974 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
4975 | -======= |
4976 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
4977 | ->>>>>>> MERGE-SOURCE |
4978 | # GNU Affero General Public License version 3 (see the file LICENSE). |
4979 | |
4980 | """Tests for PersonSet.""" |
4981 | @@ -22,7 +18,6 @@ |
4982 | from zope.component import getUtility |
4983 | from zope.security.proxy import removeSecurityProxy |
4984 | |
4985 | -<<<<<<< TREE |
4986 | from lp.code.tests.helpers import remove_all_sample_data_branches |
4987 | from lp.registry.errors import ( |
4988 | InvalidName, |
4989 | @@ -30,9 +25,6 @@ |
4990 | ) |
4991 | from lp.registry.interfaces.karma import IKarmaCacheManager |
4992 | from lp.registry.interfaces.mailinglist import MailingListStatus |
4993 | -======= |
4994 | -from lp.code.tests.helpers import remove_all_sample_data_branches |
4995 | ->>>>>>> MERGE-SOURCE |
4996 | from lp.registry.interfaces.mailinglistsubscription import ( |
4997 | MailingListAutoSubscribePolicy, |
4998 | ) |
4999 | @@ -40,7 +32,6 @@ |
5000 | from lp.registry.interfaces.person import ( |
Hi Francesco. Thank you for this good work.
I am conditionally approving this, with the changes I note below. If you disagree with my requests, that's fine; we can just work it out when our work hours overlap.
- line 88 of the diff has an XXX that we need to address ("XXX frankban 2011-12-16 further investigation needed"). We need to either address and remove the XXX or follow the XXX policy and give it a bug (see https:/ /dev.launchpad. net/PolicyandPr ocess/XXXPolicy). You said that we could not address the XXX without significant refactoring; and my further understanding was that we did not need the XXX behavior for our goals. Therefore we pursued what would be necessary to follow the XXX policy. I would like to see the following: exclude_ conjoined_ tasks and not (params.milestone or params. milestone_ tag)):" ) to blow up if someone tries to exclude conjoined tasks with a milestone tag, so that it is clear that it is not supported. A better error for this particular case might be NotImplementedE rror. You add an XXX to this error with the same bug number, clarifying that it can be removed when the bug is resolved. ConjoinedBugTas ks to only ask for excluding non-conjoined bug tasks when it is possible. This again would be an XXX with the same number. Alternatively, you could change whatever code uses that function to use a different one, I suppose.
* You file a bug for adding support to exclude conjoined bug tasks from milestone tag searches.
* You add that bug number to the XXX, per the policy, with a comment that states the problem to be solved (not the fact that it needs more investigation).
* You change the ValueError check ("if (params.
* For this to work, we'll need to change the call in getPrecachedNon
I can see an argument for what you have done, which pretends to support excluding the conjoined bug tasks but does not. I really think being clear about it is better. Solving it would be better still, of course. I'd be happy to review the concerns about performing the refactoring if you think that might have a chance of helping; however, I'm also fine trusting the analysis that you and Brad performed.
- You have used tabs instead of spaces, such as in the zcml files (see lines 278 and 279 of the current diff). Please switch to spaces only.
- We have functionality and code to handle user metadata on metadata tags. I think we should probably have a few tests of that.
Lastly, here are comments, notes and suggestions.
- The tests are good. Thank you.
- Our style guide states that our comments should be full sentences. Therefore, "# Circular reference prevention." would be better written as "# Prevent circular references." I don't require this change, but it would be nice.
- I suggest adding comments to getTags and setTags indicating that the tags are not a property because of the "user" argument to setTags, and the Launchpad desire to avoid model code that gets the user from the request or the "launchbag" thread locals.
- On IRC we discussed whether you needed to move milestone_tags to the end of the argument list of BugTaskSearchPa rams's __init__, for backwards compatibility for code that used placeful arguments...