Merge lp:~frankban/launchpad/bug-904335-get-tags into lp:launchpad

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+87489@code.launchpad.net

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 IProjectGroupMilestone are IAbstractMilestone subclasses.
Both IMilestone and IProjectGroupMilestoneTag (a marker interface) are
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 ProjectGroupMilestoneTag model is used to retrieve bugtasks
and specifications.

== Tests ==

bin/test -vv lp.registry.tests.test_milestone
bin/test -vv lp.registry.tests.test_milestonetag
bin/test -vv -t lp.registry.stories.webservice.xx-project-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.

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :
Download full text (3.9 KiB)

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/PolicyandProcess/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:
  * 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.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 NotImplementedError. You add an XXX to this error with the same bug number, clarifying that it can be removed when the bug is resolved.
  * For this to work, we'll need to change the call in getPrecachedNonConjoinedBugTasks 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.

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 BugTaskSearchParams's __init__, for backwards compatibility for code that used placeful arguments...

Read more...

review: Approve
Revision history for this message
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://dev.launchpad.net/LEP/OnlySeriesTasks and the two related escalated bugs are bug 314432 and bug 857109

Revision history for this message
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.getTagsData method that maybe needs your review.

Revision history for this message
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

Revision history for this message
Francesco Banconi (frankban) wrote :

Thank you Gary, fixed typos and switched to isinstance assertion for date_created.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 (
The diff has been truncated for viewing.