Merge lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749 into lp:launchpad/db-devel

Proposed by Gavin Panella on 2010-09-15
Status: Merged
Approved by: Gavin Panella on 2010-09-16
Approved revision: no longer in the source branch.
Merged at revision: 9825
Proposed branch: lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749
Merge into: lp:launchpad/db-devel
Diff against target: 2507 lines (+988/-246)
52 files modified
database/schema/security.cfg (+46/-0)
lib/canonical/launchpad/emailtemplates/build-request.txt (+1/-0)
lib/lp/bugs/browser/bug.py (+32/-26)
lib/lp/bugs/browser/bugtask.py (+14/-11)
lib/lp/bugs/browser/configure.zcml (+5/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+39/-32)
lib/lp/bugs/configure.zcml (+3/-0)
lib/lp/bugs/doc/bugsubscription.txt (+2/-2)
lib/lp/bugs/doc/initial-bug-contacts.txt (+1/-1)
lib/lp/bugs/interfaces/bug.py (+9/-1)
lib/lp/bugs/interfaces/bugactivity.py (+44/-9)
lib/lp/bugs/interfaces/bugtarget.py (+3/-0)
lib/lp/bugs/interfaces/bugtask.py (+1/-0)
lib/lp/bugs/model/bug.py (+106/-28)
lib/lp/bugs/model/bugsubscriptionfilter.py (+36/-0)
lib/lp/bugs/model/bugsubscriptionfilterimportance.py (+29/-0)
lib/lp/bugs/model/bugsubscriptionfilterstatus.py (+29/-0)
lib/lp/bugs/model/bugsubscriptionfiltertag.py (+29/-0)
lib/lp/bugs/model/bugtarget.py (+16/-15)
lib/lp/bugs/model/bugtask.py (+13/-5)
lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py (+66/-0)
lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py (+54/-0)
lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py (+52/-0)
lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py (+51/-0)
lib/lp/bugs/stories/webservice/xx-bug.txt (+31/-0)
lib/lp/bugs/templates/bug-portlet-actions.pt (+2/-2)
lib/lp/bugs/templates/bugtask-index.pt (+1/-1)
lib/lp/bugs/tests/has-bug-supervisor.txt (+1/-1)
lib/lp/buildmaster/tests/mock_slaves.py (+0/-2)
lib/lp/code/browser/sourcepackagerecipe.py (+6/-0)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+19/-0)
lib/lp/code/mail/sourcepackagerecipebuild.py (+3/-0)
lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py (+18/-6)
lib/lp/code/model/tests/test_recipebuilder.py (+65/-41)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+8/-5)
lib/lp/code/tests/helpers.py (+12/-0)
lib/lp/codehosting/branchdistro.py (+23/-1)
lib/lp/codehosting/tests/test_branchdistro.py (+40/-0)
lib/lp/registry/browser/product.py (+1/-0)
lib/lp/registry/configure.zcml (+1/-0)
lib/lp/registry/interfaces/product.py (+1/-0)
lib/lp/registry/interfaces/projectgroup.py (+2/-0)
lib/lp/registry/model/distributionsourcepackage.py (+3/-0)
lib/lp/registry/model/distroseries.py (+3/-0)
lib/lp/registry/model/milestone.py (+8/-5)
lib/lp/registry/model/person.py (+2/-0)
lib/lp/registry/model/productseries.py (+3/-0)
lib/lp/registry/model/projectgroup.py (+20/-1)
lib/lp/registry/model/sourcepackage.py (+3/-0)
lib/lp/registry/model/structuralsubscription.py (+20/-31)
lib/lp/registry/vocabularies.py (+9/-18)
lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py (+2/-2)
To merge this branch: bzr merge lp:~allenap/launchpad/bug-subscription-filter-models-bug-639749
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code 2010-09-15 Approve on 2010-09-15
Review via email: mp+35546@code.launchpad.net

Commit Message

Add Storm model classes for BugSubscriptionFilter* tables.

Description of the Change

- Add Storm model classes for BugSubscriptionFilter* tables (which
  already exist),

- Add database permissions for the aforementioned tables,

- Clean up some of the code in StructuralSubscriptionTargetMixin.

To post a comment you must log in.
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (5.8 KiB)

Hi Gavin,

This branch looks good, but I have a few comments below.

-Edwin

>=== modified file 'lib/lp/bugs/model/bugsubscription.py'
>--- lib/lp/bugs/model/bugsubscription.py 2010-09-09 21:00:54 +0000
>+++ lib/lp/bugs/model/bugsubscription.py 2010-09-15 18:09:22 +0000
>@@ -66,3 +80,63 @@
> if self.person.is_team:
> return user.inTeam(self.person)
> return user == self.person

Unless something has changed, each model class with its own table
needs to have its own file.

>+class BugSubscriptionFilter(Storm):
>+ """A filter to specialize a *structural* subscription."""
>+
>+ __storm_table__ = "BugSubscriptionFilter"
>+
>+ id = Int(primary=True)
>+
>+ structural_subscription_id = Int("structuralsubscription", allow_none=False)

Long line.

>+ structural_subscription = Reference(
>+ structural_subscription_id, "StructuralSubscription.id")
>+
>+ find_all_tags = Bool(allow_none=False, default=False)
>+ include_any_tags = Bool(allow_none=False, default=False)
>+ exclude_any_tags = Bool(allow_none=False, default=False)
>+
>+ other_parameters = Unicode()
>+
>+ description = Unicode()
>+
>+
>+class BugSubscriptionFilterStatus(Storm):
>+ """Statuses to filter."""
>+
>+ __storm_table__ = "BugSubscriptionFilterStatus"
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
>+
>+ status = DBEnum(enum=BugTaskStatus, allow_none=False)
>+
>+
>+class BugSubscriptionFilterImportance(Storm):
>+ """Importances to filter."""
>+
>+ __storm_table__ = "BugSubscriptionFilterImportance"
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
>+
>+ importance = DBEnum(enum=BugTaskImportance, allow_none=False)
>+
>+
>+class BugSubscriptionFilterTag(Storm):
>+ """Tags to filter."""
>+
>+ __storm_table__ = "BugSubscriptionFilterTag"
>+
>+ id = Int(primary=True)
>+
>+ filter_id = Int("filter", allow_none=False)
>+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
>+
>+ include = Bool(allow_none=False)
>+ tag = Unicode(allow_none=False)
>
>=== added directory 'lib/lp/bugs/model/tests'
>=== added file 'lib/lp/bugs/model/tests/__init__.py'
>=== added file 'lib/lp/bugs/model/tests/test_bugsubscription.py'
>--- lib/lp/bugs/model/tests/test_bugsubscription.py 1970-01-01 00:00:00 +0000
>+++ lib/lp/bugs/model/tests/test_bugsubscription.py 2010-09-15 18:09:22 +0000
>@@ -0,0 +1,182 @@
>+# Copyright 2010 Canonical Ltd. This software is licensed under the
>+# GNU Affero General Public License version 3 (see the file LICENSE).
>+
>+"""Tests for the bugsubscription module."""
>+
>+__metaclass__ = type
>+
>+from canonical.launchpad.interfaces.lpstorm import IStore
>+from canonical.testing import DatabaseFunctionalLayer
>+from lp.bugs.interfaces.bugtask import (
>+ BugTaskImportance,
>+ BugTaskStatus,
>+ )
>+from lp.bugs.model.bugsubscription import (
>+ BugSubscriptionFilter,
>+ BugSubscriptionFilterImportance,
>+ BugSubscriptionFilterStatus,
>+ BugSubscriptionFilterTa...

Read more...

review: Approve (code)
Gavin Panella (allenap) wrote :

> Unless something has changed, each model class with its own table
> needs to have its own file.

There are at least a couple of other places in bugs/model where more
than one model class is in the same module. Like these new classes,
they're all supporting classes for the first model class in the
module. These BugSubscriptionFilter* classes will almost certainly not
be exposed to browser code (and if they are then I'll move them to
their own modules anyway).

I'll gamble and land this branch as-is, but if you object I'll knock
up another branch to split them up.

Thanks for the review!

Edwin Grubbs (edwin-grubbs) wrote :

Having separate files for separate database classes is still listed in the CodeReviewChecklist, so this should probably be brought up at the reviewers meeting if you think this should be changed.

Here is the reason that I have vivid memories of this rule.
https://lists.ubuntu.com/mailman/private/launchpad-reviews/2007-November/008416.html

Gavin Panella (allenap) wrote :

Okay, I'll split them out anyway. My branch didn't land because of some test failures anyway, so I can do it before landing.

BTW, I can't get see that thread. I suspect I unsubscribed from launchpad-reviews; even though I just changed my password globally on that Mailman I can't authorization is denied for that archive.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2010-09-22 07:12:37 +0000
3+++ database/schema/security.cfg 2010-09-23 11:12:49 +0000
4@@ -539,6 +539,10 @@
5 public.bugnotification = SELECT, INSERT
6 public.bugnotificationrecipient = SELECT, INSERT
7 public.bugsubscription = SELECT
8+public.bugsubscriptionfilter = SELECT
9+public.bugsubscriptionfilterstatus = SELECT
10+public.bugsubscriptionfilterimportance = SELECT
11+public.bugsubscriptionfiltertag = SELECT
12 public.bugtask = SELECT, INSERT, UPDATE
13 public.bugtracker = SELECT, INSERT
14 public.bugtrackeralias = SELECT
15@@ -618,6 +622,10 @@
16 public.bugactivity = SELECT, INSERT
17 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
18 public.bugsubscription = SELECT
19+public.bugsubscriptionfilter = SELECT
20+public.bugsubscriptionfilterstatus = SELECT
21+public.bugsubscriptionfilterimportance = SELECT
22+public.bugsubscriptionfiltertag = SELECT
23 public.bugnotification = SELECT, INSERT
24 public.bugnotificationrecipient = SELECT, INSERT
25 public.structuralsubscription = SELECT
26@@ -630,6 +638,7 @@
27 [branch-distro]
28 type=user
29 public.branch = SELECT, INSERT, UPDATE
30+public.branchrevision = SELECT, INSERT
31 public.branchsubscription = SELECT, INSERT
32 public.distribution = SELECT
33 public.distroseries = SELECT
34@@ -637,6 +646,7 @@
35 public.karmaaction = SELECT
36 public.person = SELECT
37 public.product = SELECT
38+public.revision = SELECT
39 public.seriessourcepackagebranch = SELECT, INSERT, DELETE
40 public.sourcepackagename = SELECT
41 public.teamparticipation = SELECT
42@@ -804,6 +814,10 @@
43 public.bugactivity = SELECT, INSERT
44 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
45 public.bugsubscription = SELECT
46+public.bugsubscriptionfilter = SELECT
47+public.bugsubscriptionfilterstatus = SELECT
48+public.bugsubscriptionfilterimportance = SELECT
49+public.bugsubscriptionfiltertag = SELECT
50 public.bugnotification = SELECT, INSERT
51 public.bugnotificationrecipient = SELECT, INSERT
52 public.bugnomination = SELECT
53@@ -928,6 +942,10 @@
54 public.bugpackageinfestation = SELECT, INSERT, UPDATE
55 public.bugproductinfestation = SELECT, INSERT, UPDATE
56 public.bugsubscription = SELECT, INSERT, UPDATE, DELETE
57+public.bugsubscriptionfilter = SELECT, INSERT, UPDATE, DELETE
58+public.bugsubscriptionfilterstatus = SELECT, INSERT, UPDATE, DELETE
59+public.bugsubscriptionfilterimportance = SELECT, INSERT, UPDATE, DELETE
60+public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE
61 public.bugtask = SELECT, INSERT, UPDATE, DELETE
62 public.bugtracker = SELECT, INSERT, UPDATE, DELETE
63 public.bugtrackeralias = SELECT, INSERT, UPDATE, DELETE
64@@ -1156,6 +1174,10 @@
65 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
66 public.bugjob = SELECT, INSERT
67 public.bugsubscription = SELECT
68+public.bugsubscriptionfilter = SELECT
69+public.bugsubscriptionfilterstatus = SELECT
70+public.bugsubscriptionfilterimportance = SELECT
71+public.bugsubscriptionfiltertag = SELECT
72 public.bugnotification = SELECT, INSERT
73 public.bugnotificationrecipient = SELECT, INSERT
74 public.bugnomination = SELECT
75@@ -1258,6 +1280,10 @@
76 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
77 public.bugjob = SELECT, INSERT
78 public.bugsubscription = SELECT
79+public.bugsubscriptionfilter = SELECT
80+public.bugsubscriptionfilterstatus = SELECT
81+public.bugsubscriptionfilterimportance = SELECT
82+public.bugsubscriptionfiltertag = SELECT
83 public.bugnotification = SELECT, INSERT
84 public.bugnotificationrecipient = SELECT, INSERT
85 public.bugnomination = SELECT
86@@ -1325,6 +1351,10 @@
87 public.bugnotification = SELECT, INSERT, UPDATE
88 public.bugnotificationrecipient = SELECT, INSERT, UPDATE
89 public.bugsubscription = SELECT, INSERT
90+public.bugsubscriptionfilter = SELECT, INSERT
91+public.bugsubscriptionfilterstatus = SELECT, INSERT
92+public.bugsubscriptionfilterimportance = SELECT, INSERT
93+public.bugsubscriptionfiltertag = SELECT, INSERT
94 public.bugnomination = SELECT
95 public.bug = SELECT, INSERT, UPDATE
96 public.bugactivity = SELECT, INSERT
97@@ -1545,6 +1575,10 @@
98 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
99 public.bugjob = SELECT, INSERT
100 public.bugsubscription = SELECT, INSERT
101+public.bugsubscriptionfilter = SELECT, INSERT
102+public.bugsubscriptionfilterstatus = SELECT, INSERT
103+public.bugsubscriptionfilterimportance = SELECT, INSERT
104+public.bugsubscriptionfiltertag = SELECT, INSERT
105 public.bugnotification = SELECT, INSERT
106 public.bugnotificationattachment = SELECT
107 public.bugnotificationrecipient = SELECT, INSERT
108@@ -1553,6 +1587,10 @@
109 public.bugtask = SELECT, INSERT, UPDATE
110 public.bugmessage = SELECT, INSERT
111 public.bugsubscription = SELECT, INSERT, UPDATE, DELETE
112+public.bugsubscriptionfilter = SELECT, INSERT, UPDATE, DELETE
113+public.bugsubscriptionfilterstatus = SELECT, INSERT, UPDATE, DELETE
114+public.bugsubscriptionfilterimportance = SELECT, INSERT, UPDATE, DELETE
115+public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE
116 public.bugtracker = SELECT, INSERT
117 public.bugtrackeralias = SELECT, INSERT
118 public.bugwatch = SELECT, INSERT
119@@ -1812,6 +1850,10 @@
120 public.message = SELECT, INSERT
121 public.messagechunk = SELECT, INSERT
122 public.bugsubscription = SELECT, INSERT
123+public.bugsubscriptionfilter = SELECT, INSERT
124+public.bugsubscriptionfilterstatus = SELECT, INSERT
125+public.bugsubscriptionfilterimportance = SELECT, INSERT
126+public.bugsubscriptionfiltertag = SELECT, INSERT
127 public.bugmessage = SELECT, INSERT
128 public.sourcepackagename = SELECT
129 public.job = SELECT, INSERT, UPDATE
130@@ -1839,6 +1881,10 @@
131 public.bug = SELECT, UPDATE
132 public.bugattachment = SELECT, DELETE
133 public.bugsubscription = SELECT
134+public.bugsubscriptionfilter = SELECT
135+public.bugsubscriptionfilterstatus = SELECT
136+public.bugsubscriptionfilterimportance = SELECT
137+public.bugsubscriptionfiltertag = SELECT
138 public.bugaffectsperson = SELECT
139 public.bugnotification = SELECT, DELETE
140 public.bugnotificationrecipientarchive = SELECT
141
142=== modified file 'lib/canonical/launchpad/emailtemplates/build-request.txt'
143--- lib/canonical/launchpad/emailtemplates/build-request.txt 2010-07-30 16:24:59 +0000
144+++ lib/canonical/launchpad/emailtemplates/build-request.txt 2010-09-23 11:12:49 +0000
145@@ -4,4 +4,5 @@
146 * Distroseries: %(distroseries)s
147 * Duration: %(duration)s
148 * Build Log: %(log_url)s
149+ * Upload Log: %(upload_log_url)s
150 * Builder: %(builder_url)s
151
152=== modified file 'lib/lp/bugs/browser/bug.py'
153--- lib/lp/bugs/browser/bug.py 2010-08-31 11:11:09 +0000
154+++ lib/lp/bugs/browser/bug.py 2010-09-23 11:12:49 +0000
155@@ -299,7 +299,7 @@
156
157 def unlinkcve(self):
158 """Return 'Remove CVE link' Link."""
159- enabled = bool(self.context.bug.cves)
160+ enabled = self.context.bug.has_cves
161 text = 'Remove CVE link'
162 return Link('+unlinkcve', text, icon='remove', enabled=enabled)
163
164@@ -307,31 +307,30 @@
165 """Return the 'Offer mentorship' Link."""
166 text = 'Offer mentorship'
167 user = getUtility(ILaunchBag).user
168- enabled = self.context.bug.canMentor(user)
169+ enabled = False
170 return Link('+mentor', text, icon='add', enabled=enabled)
171
172 def retractmentoring(self):
173 """Return the 'Retract mentorship' Link."""
174 text = 'Retract mentorship'
175 user = getUtility(ILaunchBag).user
176- # We should really only allow people to retract mentoring if the
177- # bug's open and the user's already a mentor.
178- if user and not self.context.bug.is_complete:
179- enabled = self.context.bug.isMentor(user)
180- else:
181- enabled = False
182+ enabled = False
183 return Link('+retractmentoring', text, icon='remove', enabled=enabled)
184
185+ @property
186+ def _bug_question(self):
187+ return self.context.bug.getQuestionCreatedFromBug()
188+
189 def createquestion(self):
190 """Create a question from this bug."""
191 text = 'Convert to a question'
192- enabled = self.context.bug.getQuestionCreatedFromBug() is None
193+ enabled = self._bug_question is None
194 return Link('+create-question', text, enabled=enabled, icon='add')
195
196 def removequestion(self):
197 """Remove the created question from this bug."""
198 text = 'Convert back to a bug'
199- enabled = self.context.bug.getQuestionCreatedFromBug() is not None
200+ enabled = self._bug_question is not None
201 return Link('+remove-question', text, enabled=enabled, icon='remove')
202
203 def activitylog(self):
204@@ -510,29 +509,36 @@
205 else:
206 return 'subscribed-false %s' % dup_class
207
208+ @cachedproperty
209+ def _bug_attachments(self):
210+ """Get a dict of attachment type -> attachments list."""
211+ # Note that this is duplicated with get_comments_for_bugtask
212+ # if you are looking to consolidate things.
213+ result = {BugAttachmentType.PATCH: [],
214+ 'other': []
215+ }
216+ for attachment in self.context.attachments_unpopulated:
217+ info = {
218+ 'attachment': attachment,
219+ 'file': ProxiedLibraryFileAlias(
220+ attachment.libraryfile, attachment),
221+ }
222+ if attachment.type == BugAttachmentType.PATCH:
223+ key = attachment.type
224+ else:
225+ key = 'other'
226+ result[key].append(info)
227+ return result
228+
229 @property
230 def regular_attachments(self):
231 """The list of bug attachments that are not patches."""
232- return [
233- {
234- 'attachment': attachment,
235- 'file': ProxiedLibraryFileAlias(
236- attachment.libraryfile, attachment),
237- }
238- for attachment in self.context.attachments_unpopulated
239- if attachment.type != BugAttachmentType.PATCH]
240+ return self._bug_attachments['other']
241
242 @property
243 def patches(self):
244 """The list of bug attachments that are patches."""
245- return [
246- {
247- 'attachment': attachment,
248- 'file': ProxiedLibraryFileAlias(
249- attachment.libraryfile, attachment),
250- }
251- for attachment in self.context.attachments_unpopulated
252- if attachment.type == BugAttachmentType.PATCH]
253+ return self._bug_attachments[BugAttachmentType.PATCH]
254
255
256 class BugView(LaunchpadView, BugViewMixin):
257
258=== modified file 'lib/lp/bugs/browser/bugtask.py'
259--- lib/lp/bugs/browser/bugtask.py 2010-09-09 17:02:33 +0000
260+++ lib/lp/bugs/browser/bugtask.py 2010-09-23 11:12:49 +0000
261@@ -921,6 +921,9 @@
262 This is particularly useful for views that may render a
263 NullBugTask.
264 """
265+ if self.context.id is not None:
266+ # Fast path for real bugtasks: they have a DB id.
267+ return True
268 params = BugTaskSearchParams(user=self.user, bug=self.context.bug)
269 matching_bugtasks = self.context.target.searchTasks(params)
270 if self.context.productseries is not None:
271@@ -1194,14 +1197,14 @@
272 @property
273 def official_tags(self):
274 """The list of official tags for this bug."""
275- target_official_tags = self.context.target.official_bug_tags
276+ target_official_tags = set(self.context.bug.official_tags)
277 return [tag for tag in self.context.bug.tags
278 if tag in target_official_tags]
279
280 @property
281 def unofficial_tags(self):
282 """The list of unofficial tags for this bug."""
283- target_official_tags = self.context.target.official_bug_tags
284+ target_official_tags = set(self.context.bug.official_tags)
285 return [tag for tag in self.context.bug.tags
286 if tag not in target_official_tags]
287
288@@ -1213,11 +1216,10 @@
289 bug has a task. It is returned as Javascript snippet, to be embedded
290 in the bug page.
291 """
292- available_tags = set()
293- for task in self.context.bug.bugtasks:
294- available_tags.update(task.target.official_bug_tags)
295- return 'var available_official_tags = %s;' % dumps(list(sorted(
296- available_tags)))
297+ # Unwrap the security proxy. - official_tags is a security proxy
298+ # wrapped list.
299+ available_tags = list(self.context.bug.official_tags)
300+ return 'var available_official_tags = %s;' % dumps(available_tags)
301
302 @property
303 def user_is_admin(self):
304@@ -3247,6 +3249,7 @@
305 """Cache the list of bugtasks and set up the release mapping."""
306 # Cache some values, so that we don't have to recalculate them
307 # for each bug task.
308+ # This query is redundant: the publisher also queries all the bugtasks.
309 self.bugtasks = list(self.context.bugtasks)
310 self.many_bugtasks = len(self.bugtasks) >= 10
311 self.cached_milestone_source = CachedMilestoneSourceFactory()
312@@ -3338,7 +3341,6 @@
313
314 upstream_tasks.sort(key=_by_targetname)
315 distro_tasks.sort(key=_by_targetname)
316-
317 all_bugtasks = upstream_tasks + distro_tasks
318
319 # Cache whether the bug was converted to a question, since
320@@ -3402,7 +3404,7 @@
321 # Hide the links when the bug is viewed in a CVE context
322 return self.request.getNearest(ICveSet) == (None, None)
323
324- @property
325+ @cachedproperty
326 def current_user_affected_status(self):
327 """Is the current user marked as affected by this bug?"""
328 return self.context.isUserAffected(self.user)
329@@ -3623,10 +3625,11 @@
330
331 return items
332
333- @property
334+ @cachedproperty
335 def target_has_milestones(self):
336 """Are there any milestones we can target?"""
337- return list(MilestoneVocabulary(self.context)) != []
338+ return MilestoneVocabulary.getMilestoneTarget(
339+ self.context).has_milestones
340
341 def bugtask_canonical_url(self):
342 """Return the canonical url for the bugtask."""
343
344=== modified file 'lib/lp/bugs/browser/configure.zcml'
345--- lib/lp/bugs/browser/configure.zcml 2010-09-06 15:14:17 +0000
346+++ lib/lp/bugs/browser/configure.zcml 2010-09-23 11:12:49 +0000
347@@ -531,6 +531,11 @@
348 permission="launchpad.View"
349 name="+activity"
350 template="../templates/bug-activity.pt"/>
351+ <browser:url
352+ for="lp.bugs.interfaces.bugactivity.IBugActivity"
353+ path_expression="string:activity"
354+ attribute_to_parent="bug"
355+ rootsite="bugs"/>
356 <browser:page
357 for="lp.bugs.interfaces.bugtask.IBugTask"
358 class="lp.bugs.browser.bugtask.BugTaskTableRowView"
359
360=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
361--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-01 01:57:37 +0000
362+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-23 11:12:49 +0000
363@@ -28,6 +28,7 @@
364 setUp,
365 tearDown,
366 )
367+from canonical.launchpad.webapp import canonical_url
368 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
369 from canonical.testing import LaunchpadFunctionalLayer
370 from lp.bugs.browser import bugtask
371@@ -37,11 +38,8 @@
372 BugTasksAndNominationsView,
373 )
374 from lp.bugs.interfaces.bugtask import BugTaskStatus
375-from lp.testing import (
376- person_logged_in,
377- StormStatementRecorder,
378- TestCaseWithFactory,
379- )
380+from lp.testing import TestCaseWithFactory
381+from lp.testing._webservice import QueryCollector
382 from lp.testing.matchers import HasQueryCount
383 from lp.testing.sampledata import (
384 ADMIN_EMAIL,
385@@ -54,16 +52,6 @@
386
387 layer = LaunchpadFunctionalLayer
388
389- def record_view_initialization(self, bugtask, person):
390- self.invalidate_caches(bugtask)
391- # Login first because logging in triggers queries.
392- with nested(person_logged_in(person), StormStatementRecorder()) as (
393- _,
394- recorder):
395- view = BugTaskView(bugtask, LaunchpadTestRequest())
396- view.initialize()
397- return recorder
398-
399 def invalidate_caches(self, obj):
400 store = Store.of(obj)
401 # Make sure everything is in the database.
402@@ -72,24 +60,31 @@
403 # the domain objects)
404 store.invalidate()
405
406- def test_query_counts_constant_with_team_memberships(self):
407+ def test_rendered_query_counts_constant_with_team_memberships(self):
408 login(ADMIN_EMAIL)
409 bugtask = self.factory.makeBugTask()
410- person_no_teams = self.factory.makePerson()
411- person_with_teams = self.factory.makePerson()
412+ person_no_teams = self.factory.makePerson(password='test')
413+ person_with_teams = self.factory.makePerson(password='test')
414 for _ in range(10):
415 self.factory.makeTeam(members=[person_with_teams])
416 # count with no teams
417- recorder = self.record_view_initialization(bugtask, person_no_teams)
418- self.assertThat(recorder, HasQueryCount(LessThan(14)))
419+ url = canonical_url(bugtask)
420+ recorder = QueryCollector()
421+ recorder.register()
422+ self.addCleanup(recorder.unregister)
423+ self.invalidate_caches(bugtask)
424+ self.getUserBrowser(url, person_no_teams)
425+ # This may seem large: it is; there is easily another 30% fat in there.
426+ self.assertThat(recorder, HasQueryCount(LessThan(64)))
427 count_with_no_teams = recorder.count
428 # count with many teams
429- recorder2 = self.record_view_initialization(bugtask, person_with_teams)
430+ self.invalidate_caches(bugtask)
431+ self.getUserBrowser(url, person_with_teams)
432 # Allow an increase of one because storm bug 619017 causes additional
433 # queries, revalidating things unnecessarily. An increase which is
434 # less than the number of new teams shows it is definitely not
435 # growing per-team.
436- self.assertThat(recorder2, HasQueryCount(
437+ self.assertThat(recorder, HasQueryCount(
438 LessThan(count_with_no_teams + 3),
439 ))
440
441@@ -105,23 +100,32 @@
442 self.view = BugTasksAndNominationsView(
443 self.bug, LaunchpadTestRequest())
444
445+ def refresh(self):
446+ # The view caches, to see different scenarios, a refresh is needed.
447+ self.view = BugTasksAndNominationsView(
448+ self.bug, LaunchpadTestRequest())
449+
450 def test_current_user_affected_status(self):
451 self.failUnlessEqual(
452 None, self.view.current_user_affected_status)
453- self.view.context.markUserAffected(self.view.user, True)
454+ self.bug.markUserAffected(self.view.user, True)
455+ self.refresh()
456 self.failUnlessEqual(
457 True, self.view.current_user_affected_status)
458- self.view.context.markUserAffected(self.view.user, False)
459+ self.bug.markUserAffected(self.view.user, False)
460+ self.refresh()
461 self.failUnlessEqual(
462 False, self.view.current_user_affected_status)
463
464 def test_current_user_affected_js_status(self):
465 self.failUnlessEqual(
466 'null', self.view.current_user_affected_js_status)
467- self.view.context.markUserAffected(self.view.user, True)
468+ self.bug.markUserAffected(self.view.user, True)
469+ self.refresh()
470 self.failUnlessEqual(
471 'true', self.view.current_user_affected_js_status)
472- self.view.context.markUserAffected(self.view.user, False)
473+ self.bug.markUserAffected(self.view.user, False)
474+ self.refresh()
475 self.failUnlessEqual(
476 'false', self.view.current_user_affected_js_status)
477
478@@ -148,10 +152,12 @@
479 # logged-in user marked him or herself as affected or not.
480 self.failUnlessEqual(
481 1, self.view.other_users_affected_count)
482- self.view.context.markUserAffected(self.view.user, True)
483+ self.bug.markUserAffected(self.view.user, True)
484+ self.refresh()
485 self.failUnlessEqual(
486 1, self.view.other_users_affected_count)
487- self.view.context.markUserAffected(self.view.user, False)
488+ self.bug.markUserAffected(self.view.user, False)
489+ self.refresh()
490 self.failUnlessEqual(
491 1, self.view.other_users_affected_count)
492
493@@ -161,17 +167,18 @@
494 self.failUnlessEqual(
495 1, self.view.other_users_affected_count)
496 other_user_1 = self.factory.makePerson()
497- self.view.context.markUserAffected(other_user_1, True)
498+ self.bug.markUserAffected(other_user_1, True)
499 self.failUnlessEqual(
500 2, self.view.other_users_affected_count)
501 other_user_2 = self.factory.makePerson()
502- self.view.context.markUserAffected(other_user_2, True)
503+ self.bug.markUserAffected(other_user_2, True)
504 self.failUnlessEqual(
505 3, self.view.other_users_affected_count)
506- self.view.context.markUserAffected(other_user_1, False)
507+ self.bug.markUserAffected(other_user_1, False)
508 self.failUnlessEqual(
509 2, self.view.other_users_affected_count)
510- self.view.context.markUserAffected(self.view.user, True)
511+ self.bug.markUserAffected(self.view.user, True)
512+ self.refresh()
513 self.failUnlessEqual(
514 2, self.view.other_users_affected_count)
515
516
517=== modified file 'lib/lp/bugs/configure.zcml'
518--- lib/lp/bugs/configure.zcml 2010-09-10 16:21:21 +0000
519+++ lib/lp/bugs/configure.zcml 2010-09-23 11:12:49 +0000
520@@ -191,6 +191,7 @@
521 date_fix_released
522 date_left_closed
523 date_closed
524+ productseriesID
525 task_age
526 bug_subscribers
527 is_complete
528@@ -592,6 +593,7 @@
529 isMentor
530 getNullBugTask
531 is_complete
532+ official_tags
533 who_made_private
534 date_made_private
535 userCanView
536@@ -624,6 +626,7 @@
537 initial_message
538 linked_branches
539 watches
540+ has_cves
541 cves
542 cve_links
543 duplicates
544
545=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
546--- lib/lp/bugs/doc/bugsubscription.txt 2010-09-09 21:00:54 +0000
547+++ lib/lp/bugs/doc/bugsubscription.txt 2010-09-23 11:12:49 +0000
548@@ -49,7 +49,7 @@
549 >>> personset = getUtility(IPersonSet)
550
551 >>> linux_source = ubuntu.getSourcePackage("linux-source-2.6.15")
552- >>> linux_source.bug_subscriptions
553+ >>> list(linux_source.bug_subscriptions)
554 []
555 >>> print linux_source.distribution.bug_supervisor
556 None
557@@ -781,7 +781,7 @@
558 contacts:
559
560 >>> evolution = ubuntu.getSourcePackage("evolution")
561- >>> evolution.bug_subscriptions
562+ >>> list(evolution.bug_subscriptions)
563 []
564
565 >>> params = CreateBugParams(
566
567=== modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt'
568--- lib/lp/bugs/doc/initial-bug-contacts.txt 2010-08-20 01:29:08 +0000
569+++ lib/lp/bugs/doc/initial-bug-contacts.txt 2010-09-23 11:12:49 +0000
570@@ -16,7 +16,7 @@
571 >>> debian = getUtility(IDistributionSet).getByName("debian")
572 >>> debian_firefox = debian.getSourcePackage("mozilla-firefox")
573
574- >>> debian_firefox.bug_subscriptions
575+ >>> list(debian_firefox.bug_subscriptions)
576 []
577
578 Adding a package subscription is done with the
579
580=== modified file 'lib/lp/bugs/interfaces/bug.py'
581--- lib/lp/bugs/interfaces/bug.py 2010-08-30 23:50:41 +0000
582+++ lib/lp/bugs/interfaces/bug.py 2010-09-23 11:12:49 +0000
583@@ -70,6 +70,7 @@
584 )
585 from canonical.launchpad.validators.name import name_validator
586 from lp.app.errors import NotFoundError
587+from lp.bugs.interfaces.bugactivity import IBugActivity
588 from lp.bugs.interfaces.bugattachment import IBugAttachment
589 from lp.bugs.interfaces.bugbranch import IBugBranch
590 from lp.bugs.interfaces.bugtask import (
591@@ -243,7 +244,11 @@
592 required=False, default=False, readonly=True))
593 displayname = TextLine(title=_("Text of the form 'Bug #X"),
594 readonly=True)
595- activity = Attribute('SQLObject.Multijoin of IBugActivity')
596+ activity = exported(
597+ CollectionField(
598+ title=_('Log of activity that has occurred on this bug.'),
599+ value_type=Reference(schema=IBugActivity),
600+ readonly=True))
601 initial_message = Attribute(
602 "The message that was specified when creating the bug")
603 bugtasks = exported(
604@@ -269,6 +274,7 @@
605 title=_('CVE entries related to this bug.'),
606 value_type=Reference(schema=ICve),
607 readonly=True))
608+ has_cves = Bool(title=u"True if the bug has cve entries.")
609 cve_links = Attribute('Links between this bug and CVE entries.')
610 subscriptions = exported(
611 doNotSnapshot(CollectionField(
612@@ -406,6 +412,8 @@
613
614 latest_patch = Attribute("The most recent patch of this bug.")
615
616+ official_tags = Attribute("The official bug tags relevant to this bug.")
617+
618 @operation_parameters(
619 subject=optional_message_subject_field(),
620 content=copy_field(IMessage['content']))
621
622=== modified file 'lib/lp/bugs/interfaces/bugactivity.py'
623--- lib/lp/bugs/interfaces/bugactivity.py 2010-08-20 20:31:18 +0000
624+++ lib/lp/bugs/interfaces/bugactivity.py 2010-09-23 11:12:49 +0000
625@@ -15,24 +15,59 @@
626 from zope.interface import Interface
627 from zope.schema import (
628 Datetime,
629- Int,
630 Text,
631 TextLine,
632 )
633
634+from lazr.restful.declarations import (
635+ export_as_webservice_entry,
636+ exported,
637+ )
638+
639+from lp.services.fields import (
640+ BugField,
641+ PersonChoice,
642+ )
643+
644 from canonical.launchpad import _
645
646
647 class IBugActivity(Interface):
648 """A log of all things that have happened to a bug."""
649-
650- bug = Int(title=_('Bug ID'))
651- datechanged = Datetime(title=_('Date Changed'))
652- person = Int(title=_('Person'))
653- whatchanged = TextLine(title=_('What Changed'))
654- oldvalue = TextLine(title=_('Old Value'))
655- newvalue = TextLine(title=_('New Value'))
656- message = Text(title=_('Message'))
657+ export_as_webservice_entry()
658+
659+ bug = exported(
660+ BugField(title=_('Bug'), readonly=True))
661+
662+ datechanged = exported(
663+ Datetime(title=_('Date Changed'),
664+ description=_("The date on which this activity occurred."),
665+ readonly=True))
666+
667+ person = exported(PersonChoice(
668+ title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',
669+ readonly=True, description=_("The person's Launchpad ID or "
670+ "e-mail address.")))
671+
672+ whatchanged = exported(
673+ TextLine(title=_('What Changed'),
674+ description=_("The property of the bug that changed."),
675+ readonly=True))
676+
677+ oldvalue = exported(
678+ TextLine(title=_('Old Value'),
679+ description=_("The value before the change."),
680+ readonly=True))
681+
682+ newvalue = exported(
683+ TextLine(title=_('New Value'),
684+ description=_("The value after the change."),
685+ readonly=True))
686+
687+ message = exported(
688+ Text(title=_('Message'),
689+ description=_("Additional information about what changed."),
690+ readonly=True))
691
692
693 class IBugActivitySet(Interface):
694
695=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
696--- lib/lp/bugs/interfaces/bugtarget.py 2010-08-26 20:22:48 +0000
697+++ lib/lp/bugs/interfaces/bugtarget.py 2010-09-23 11:12:49 +0000
698@@ -363,6 +363,9 @@
699 bugs will be returned.
700 """
701
702+ def _getOfficialTagClause():
703+ """Get the storm clause for finding this targets tags."""
704+
705
706 class IOfficialBugTagTargetPublic(IHasOfficialBugTags):
707 """Public attributes for `IOfficialBugTagTarget`."""
708
709=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
710--- lib/lp/bugs/interfaces/bugtask.py 2010-08-30 19:30:45 +0000
711+++ lib/lp/bugs/interfaces/bugtask.py 2010-09-23 11:12:49 +0000
712@@ -454,6 +454,7 @@
713 title=_('Project'), required=False, vocabulary='Product')
714 productseries = Choice(
715 title=_('Series'), required=False, vocabulary='ProductSeries')
716+ productseriesID = Attribute('The product series ID')
717 sourcepackagename = Choice(
718 title=_("Package"), required=False,
719 vocabulary='SourcePackageName')
720
721=== modified file 'lib/lp/bugs/model/bug.py'
722--- lib/lp/bugs/model/bug.py 2010-09-15 23:40:13 +0000
723+++ lib/lp/bugs/model/bug.py 2010-09-23 11:12:49 +0000
724@@ -70,6 +70,7 @@
725 implements,
726 providedBy,
727 )
728+from zope.security.proxy import removeSecurityProxy
729
730 from canonical.config import config
731 from canonical.database.constants import UTC_NOW
732@@ -161,6 +162,7 @@
733 get_bug_privacy_filter,
734 NullBugTask,
735 )
736+from lp.bugs.model.bugtarget import OfficialBugTag
737 from lp.bugs.model.bugwatch import BugWatch
738 from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
739 from lp.registry.enum import BugNotificationLevel
740@@ -191,6 +193,7 @@
741 from lp.services.propertycache import (
742 cachedproperty,
743 IPropertyCache,
744+ IPropertyCacheManager,
745 )
746
747
748@@ -348,6 +351,21 @@
749 heat_last_updated = UtcDateTimeCol(default=None)
750 latest_patch_uploaded = UtcDateTimeCol(default=None)
751
752+ @cachedproperty
753+ def _subscriber_cache(self):
754+ """Caches known subscribers."""
755+ return set()
756+
757+ @cachedproperty
758+ def _subscriber_dups_cache(self):
759+ """Caches known subscribers to dupes."""
760+ return set()
761+
762+ @cachedproperty
763+ def _unsubscribed_cache(self):
764+ """Cache known non-subscribers."""
765+ return set()
766+
767 @property
768 def latest_patch(self):
769 """See `IBug`."""
770@@ -531,7 +549,7 @@
771 dn += ' ('+self.name+')'
772 return dn
773
774- @property
775+ @cachedproperty
776 def bugtasks(self):
777 """See `IBug`."""
778 result = BugTask.select('BugTask.bug = %s' % sqlvalues(self.id))
779@@ -541,7 +559,8 @@
780 # Do not use the default orderBy as the prejoins cause ambiguities
781 # across the tables.
782 result = result.orderBy("id")
783- return sorted(result, key=bugtask_sort_key)
784+ result = sorted(result, key=bugtask_sort_key)
785+ return result
786
787 @property
788 def default_bugtask(self):
789@@ -665,6 +684,33 @@
790 BugMessage.message == Message.id).order_by('id')
791 return messages.first()
792
793+ @cachedproperty
794+ def official_tags(self):
795+ """See `IBug`."""
796+ # Da circle of imports forces the locals.
797+ from lp.registry.model.distribution import Distribution
798+ from lp.registry.model.product import Product
799+ table = OfficialBugTag
800+ table = LeftJoin(
801+ table,
802+ Distribution,
803+ OfficialBugTag.distribution_id==Distribution.id)
804+ table = LeftJoin(
805+ table,
806+ Product,
807+ OfficialBugTag.product_id==Product.id)
808+ # When this method is typically called it already has the necessary
809+ # info in memory, so rather than rejoin with Product etc, we do this
810+ # bit in Python. If reviewing performance here feel free to change.
811+ clauses = []
812+ for task in self.bugtasks:
813+ clauses.append(
814+ # Storm cannot compile proxied objects.
815+ removeSecurityProxy(task.target._getOfficialTagClause()))
816+ clause = Or(*clauses)
817+ return list(Store.of(self).using(table).find(OfficialBugTag.tag,
818+ clause).order_by(OfficialBugTag.tag).config(distinct=True))
819+
820 def followup_subject(self):
821 """See `IBug`."""
822 return 'Re: '+ self.title
823@@ -698,6 +744,8 @@
824
825 def unsubscribe(self, person, unsubscribed_by):
826 """See `IBug`."""
827+ # Drop cached subscription info.
828+ IPropertyCacheManager(self).clear()
829 if person is None:
830 person = unsubscribed_by
831
832@@ -737,21 +785,11 @@
833
834 def isSubscribed(self, person):
835 """See `IBug`."""
836- if person is None:
837- return False
838-
839- bs = BugSubscription.selectBy(bug=self, person=person)
840- return bool(bs)
841+ return self.personIsDirectSubscriber(person)
842
843 def isSubscribedToDupes(self, person):
844 """See `IBug`."""
845- if person is None:
846- return False
847-
848- return bool(
849- BugSubscription.select("""
850- bug IN (SELECT id FROM Bug WHERE duplicateof = %d) AND
851- person = %d""" % (self.id, person.id)))
852+ return self.personIsSubscribedToDuplicate(person)
853
854 def getDirectSubscriptions(self):
855 """See `IBug`."""
856@@ -868,9 +906,23 @@
857 def getSubscribersForPerson(self, person):
858 """See `IBug."""
859 assert person is not None
860- return Store.of(self).find(
861- # return people
862- Person,
863+ def cache_unsubscribed(rows):
864+ if not rows:
865+ self._unsubscribed_cache.add(person)
866+ def cache_subscriber(row):
867+ _, subscriber, subscription = row
868+ if subscription.bugID == self.id:
869+ self._subscriber_cache.add(subscriber)
870+ else:
871+ self._subscriber_dups_cache.add(subscriber)
872+ return subscriber
873+ return DecoratedResultSet(Store.of(self).find(
874+ # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a
875+ # hack; it does not seem to be possible to express DISTINCT ON
876+ # with Storm.
877+ (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"),
878+ # return people and subscribptions
879+ Person, BugSubscription),
880 # For this bug or its duplicates
881 Or(
882 Bug.id == self.id,
883@@ -890,7 +942,8 @@
884 # bug=https://bugs.edge.launchpad.net/storm/+bug/627137
885 # RBC 20100831
886 SQL("""Person.id = TeamParticipation.team"""),
887- ).order_by(Person.name).config(distinct=True)
888+ ).order_by(Person.name),
889+ cache_subscriber, pre_iter_hook=cache_unsubscribed)
890
891 def getAlsoNotifiedSubscribers(self, recipients=None, level=None):
892 """See `IBug`.
893@@ -1216,6 +1269,11 @@
894 notify(ObjectDeletedEvent(bug_branch, user=user))
895 bug_branch.destroySelf()
896
897+ @cachedproperty
898+ def has_cves(self):
899+ """See `IBug`."""
900+ return bool(self.cves)
901+
902 def linkCVE(self, cve, user):
903 """See `IBug`."""
904 if cve not in self.cves:
905@@ -1314,18 +1372,23 @@
906 question_target = IQuestionTarget(bugtask.target)
907 question = question_target.createQuestionFromBug(self)
908 self.addChange(BugConvertedToQuestion(UTC_NOW, person, question))
909+ IPropertyCache(self)._question_from_bug = question
910
911 notify(BugBecameQuestionEvent(self, question, person))
912 return question
913
914- def getQuestionCreatedFromBug(self):
915- """See `IBug`."""
916+ @cachedproperty
917+ def _question_from_bug(self):
918 for question in self.questions:
919- if (question.owner == self.owner
920+ if (question.ownerID == self.ownerID
921 and question.datecreated == self.datecreated):
922 return question
923 return None
924
925+ def getQuestionCreatedFromBug(self):
926+ """See `IBug`."""
927+ return self._question_from_bug
928+
929 def canMentor(self, user):
930 """See `ICanBeMentored`."""
931 if user is None:
932@@ -1624,10 +1687,13 @@
933
934 def _getTags(self):
935 """Get the tags as a sorted list of strings."""
936- tags = [
937- bugtag.tag
938- for bugtag in BugTag.selectBy(bug=self, orderBy='tag')]
939- return tags
940+ return self._cached_tags
941+
942+ @cachedproperty
943+ def _cached_tags(self):
944+ return list(Store.of(self).find(
945+ BugTag.tag,
946+ BugTag.bugID==self.id).order_by(BugTag.tag))
947
948 def _setTags(self, tags):
949 """Set the tags from a list of strings."""
950@@ -1635,6 +1701,7 @@
951 # and insert the new ones.
952 new_tags = set([tag.lower() for tag in tags])
953 old_tags = set(self.tags)
954+ del IPropertyCache(self)._cached_tags
955 added_tags = new_tags.difference(old_tags)
956 removed_tags = old_tags.difference(new_tags)
957 for removed_tag in removed_tags:
958@@ -1782,12 +1849,17 @@
959
960 def personIsDirectSubscriber(self, person):
961 """See `IBug`."""
962+ if person in self._subscriber_cache:
963+ return True
964+ if person in self._unsubscribed_cache:
965+ return False
966+ if person is None:
967+ return False
968 store = Store.of(self)
969 subscriptions = store.find(
970 BugSubscription,
971 BugSubscription.bug == self,
972 BugSubscription.person == person)
973-
974 return not subscriptions.is_empty()
975
976 def personIsAlsoNotifiedSubscriber(self, person):
977@@ -1803,6 +1875,12 @@
978
979 def personIsSubscribedToDuplicate(self, person):
980 """See `IBug`."""
981+ if person in self._subscriber_dups_cache:
982+ return True
983+ if person in self._unsubscribed_cache:
984+ return False
985+ if person is None:
986+ return False
987 store = Store.of(self)
988 subscriptions_from_dupes = store.find(
989 BugSubscription,
990@@ -2012,13 +2090,13 @@
991
992 # Create the task on a product if one was passed.
993 if params.product:
994- BugTaskSet().createTask(
995+ getUtility(IBugTaskSet).createTask(
996 bug=bug, product=params.product, owner=params.owner,
997 status=params.status)
998
999 # Create the task on a source package name if one was passed.
1000 if params.distribution:
1001- BugTaskSet().createTask(
1002+ getUtility(IBugTaskSet).createTask(
1003 bug=bug, distribution=params.distribution,
1004 sourcepackagename=params.sourcepackagename,
1005 owner=params.owner, status=params.status)
1006
1007=== added file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
1008--- lib/lp/bugs/model/bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
1009+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2010-09-23 11:12:49 +0000
1010@@ -0,0 +1,36 @@
1011+# Copyright 2009 Canonical Ltd. This software is licensed under the
1012+# GNU Affero General Public License version 3 (see the file LICENSE).
1013+
1014+# pylint: disable-msg=E0611,W0212
1015+
1016+__metaclass__ = type
1017+__all__ = ['BugSubscriptionFilter']
1018+
1019+from storm.base import Storm
1020+from storm.locals import (
1021+ Bool,
1022+ Int,
1023+ Reference,
1024+ Unicode,
1025+ )
1026+
1027+
1028+class BugSubscriptionFilter(Storm):
1029+ """A filter to specialize a *structural* subscription."""
1030+
1031+ __storm_table__ = "BugSubscriptionFilter"
1032+
1033+ id = Int(primary=True)
1034+
1035+ structural_subscription_id = Int(
1036+ "structuralsubscription", allow_none=False)
1037+ structural_subscription = Reference(
1038+ structural_subscription_id, "StructuralSubscription.id")
1039+
1040+ find_all_tags = Bool(allow_none=False, default=False)
1041+ include_any_tags = Bool(allow_none=False, default=False)
1042+ exclude_any_tags = Bool(allow_none=False, default=False)
1043+
1044+ other_parameters = Unicode()
1045+
1046+ description = Unicode()
1047
1048=== added file 'lib/lp/bugs/model/bugsubscriptionfilterimportance.py'
1049--- lib/lp/bugs/model/bugsubscriptionfilterimportance.py 1970-01-01 00:00:00 +0000
1050+++ lib/lp/bugs/model/bugsubscriptionfilterimportance.py 2010-09-23 11:12:49 +0000
1051@@ -0,0 +1,29 @@
1052+# Copyright 2009 Canonical Ltd. This software is licensed under the
1053+# GNU Affero General Public License version 3 (see the file LICENSE).
1054+
1055+# pylint: disable-msg=E0611,W0212
1056+
1057+__metaclass__ = type
1058+__all__ = ['BugSubscriptionFilterImportance']
1059+
1060+from storm.base import Storm
1061+from storm.locals import (
1062+ Int,
1063+ Reference,
1064+ )
1065+
1066+from canonical.database.enumcol import DBEnum
1067+from lp.bugs.interfaces.bugtask import BugTaskImportance
1068+
1069+
1070+class BugSubscriptionFilterImportance(Storm):
1071+ """Importances to filter."""
1072+
1073+ __storm_table__ = "BugSubscriptionFilterImportance"
1074+
1075+ id = Int(primary=True)
1076+
1077+ filter_id = Int("filter", allow_none=False)
1078+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
1079+
1080+ importance = DBEnum(enum=BugTaskImportance, allow_none=False)
1081
1082=== added file 'lib/lp/bugs/model/bugsubscriptionfilterstatus.py'
1083--- lib/lp/bugs/model/bugsubscriptionfilterstatus.py 1970-01-01 00:00:00 +0000
1084+++ lib/lp/bugs/model/bugsubscriptionfilterstatus.py 2010-09-23 11:12:49 +0000
1085@@ -0,0 +1,29 @@
1086+# Copyright 2009 Canonical Ltd. This software is licensed under the
1087+# GNU Affero General Public License version 3 (see the file LICENSE).
1088+
1089+# pylint: disable-msg=E0611,W0212
1090+
1091+__metaclass__ = type
1092+__all__ = ['BugSubscriptionFilterStatus']
1093+
1094+from storm.base import Storm
1095+from storm.locals import (
1096+ Int,
1097+ Reference,
1098+ )
1099+
1100+from canonical.database.enumcol import DBEnum
1101+from lp.bugs.interfaces.bugtask import BugTaskStatus
1102+
1103+
1104+class BugSubscriptionFilterStatus(Storm):
1105+ """Statuses to filter."""
1106+
1107+ __storm_table__ = "BugSubscriptionFilterStatus"
1108+
1109+ id = Int(primary=True)
1110+
1111+ filter_id = Int("filter", allow_none=False)
1112+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
1113+
1114+ status = DBEnum(enum=BugTaskStatus, allow_none=False)
1115
1116=== added file 'lib/lp/bugs/model/bugsubscriptionfiltertag.py'
1117--- lib/lp/bugs/model/bugsubscriptionfiltertag.py 1970-01-01 00:00:00 +0000
1118+++ lib/lp/bugs/model/bugsubscriptionfiltertag.py 2010-09-23 11:12:49 +0000
1119@@ -0,0 +1,29 @@
1120+# Copyright 2009 Canonical Ltd. This software is licensed under the
1121+# GNU Affero General Public License version 3 (see the file LICENSE).
1122+
1123+# pylint: disable-msg=E0611,W0212
1124+
1125+__metaclass__ = type
1126+__all__ = ['BugSubscriptionFilterTag']
1127+
1128+from storm.base import Storm
1129+from storm.locals import (
1130+ Bool,
1131+ Int,
1132+ Reference,
1133+ Unicode,
1134+ )
1135+
1136+
1137+class BugSubscriptionFilterTag(Storm):
1138+ """Tags to filter."""
1139+
1140+ __storm_table__ = "BugSubscriptionFilterTag"
1141+
1142+ id = Int(primary=True)
1143+
1144+ filter_id = Int("filter", allow_none=False)
1145+ filter = Reference(filter_id, "BugSubscriptionFilter.id")
1146+
1147+ include = Bool(allow_none=False)
1148+ tag = Unicode(allow_none=False)
1149
1150=== modified file 'lib/lp/bugs/model/bugtarget.py'
1151--- lib/lp/bugs/model/bugtarget.py 2010-09-02 22:08:12 +0000
1152+++ lib/lp/bugs/model/bugtarget.py 2010-09-23 11:12:49 +0000
1153@@ -338,22 +338,26 @@
1154
1155 Using this call in ProjectGroup requires a fix of bug 341203, see
1156 below, class OfficialBugTag.
1157+
1158+ See also `Bug.official_bug_tags` which calculates this efficiently for
1159+ a single bug.
1160 """
1161
1162+ def _getOfficialTagClause(self):
1163+ if IDistribution.providedBy(self):
1164+ return (OfficialBugTag.distribution == self)
1165+ elif IProduct.providedBy(self):
1166+ return (OfficialBugTag.product == self)
1167+ else:
1168+ raise AssertionError(
1169+ '%s is not a valid official bug target' % self)
1170+
1171 def _getOfficialTags(self):
1172 """Get the official bug tags as a sorted list of strings."""
1173 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
1174- if IDistribution.providedBy(self):
1175- target_clause = (OfficialBugTag.distribution == self)
1176- elif IProduct.providedBy(self):
1177- target_clause = (OfficialBugTag.product == self)
1178- else:
1179- raise AssertionError(
1180- '%s is not a valid official bug target' % self)
1181- tags = [
1182- obt.tag for obt
1183- in store.find(OfficialBugTag, target_clause).order_by('tag')]
1184- return tags
1185+ target_clause = self._getOfficialTagClause()
1186+ return list(store.find(
1187+ OfficialBugTag.tag, target_clause).order_by(OfficialBugTag.tag))
1188
1189 def _setOfficialTags(self, tags):
1190 """Set the official bug tags from a list of strings."""
1191@@ -374,10 +378,7 @@
1192 If the tag is not defined for this target, None is returned.
1193 """
1194 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
1195- if IDistribution.providedBy(self):
1196- target_clause = (OfficialBugTag.distribution == self)
1197- else:
1198- target_clause = (OfficialBugTag.product == self)
1199+ target_clause = self._getOfficialTagClause()
1200 return store.find(
1201 OfficialBugTag, OfficialBugTag.tag==tag, target_clause).one()
1202
1203
1204=== modified file 'lib/lp/bugs/model/bugtask.py'
1205--- lib/lp/bugs/model/bugtask.py 2010-09-09 17:02:33 +0000
1206+++ lib/lp/bugs/model/bugtask.py 2010-09-23 11:12:49 +0000
1207@@ -43,7 +43,10 @@
1208 Or,
1209 SQL,
1210 )
1211-from storm.store import EmptyResultSet
1212+from storm.store import (
1213+ EmptyResultSet,
1214+ Store,
1215+ )
1216 from storm.zope.interfaces import (
1217 IResultSet,
1218 ISQLObjectResultSet,
1219@@ -399,6 +402,7 @@
1220 sourcepackagename=None, distribution=None,
1221 distroseries=None):
1222 """Initialize a NullBugTask."""
1223+ self.id = None
1224 self.bug = bug
1225 self.product = product
1226 self.productseries = productseries
1227@@ -756,11 +760,11 @@
1228 conjoined_master = bugtask
1229 break
1230 elif IUpstreamBugTask.providedBy(self):
1231- assert self.product.development_focus is not None, (
1232+ assert self.product.development_focusID is not None, (
1233 'A product should always have a development series.')
1234- devel_focus = self.product.development_focus
1235+ devel_focusID = self.product.development_focusID
1236 for bugtask in bugtasks:
1237- if bugtask.productseries == devel_focus:
1238+ if bugtask.productseriesID == devel_focusID:
1239 conjoined_master = bugtask
1240 break
1241
1242@@ -2367,7 +2371,11 @@
1243 bugtask._syncFromConjoinedSlave()
1244
1245 bugtask.updateTargetNameCache()
1246-
1247+ del IPropertyCache(bug).bugtasks
1248+ # Because of block_implicit_flushes, it is possible for a new bugtask
1249+ # to be queued in appropriately, which leads to Bug.bugtasks not
1250+ # finding the bugtask.
1251+ Store.of(bugtask).flush()
1252 return bugtask
1253
1254 def getStatusCountsForProductSeries(self, user, product_series):
1255
1256=== added directory 'lib/lp/bugs/model/tests'
1257=== added file 'lib/lp/bugs/model/tests/__init__.py'
1258=== added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py'
1259--- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
1260+++ lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2010-09-23 11:12:49 +0000
1261@@ -0,0 +1,66 @@
1262+# Copyright 2010 Canonical Ltd. This software is licensed under the
1263+# GNU Affero General Public License version 3 (see the file LICENSE).
1264+
1265+"""Tests for the bugsubscription module."""
1266+
1267+__metaclass__ = type
1268+
1269+from canonical.launchpad.interfaces.lpstorm import IStore
1270+from canonical.testing import DatabaseFunctionalLayer
1271+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
1272+from lp.testing import (
1273+ login_person,
1274+ TestCaseWithFactory,
1275+ )
1276+
1277+
1278+class TestBugSubscriptionFilter(TestCaseWithFactory):
1279+
1280+ layer = DatabaseFunctionalLayer
1281+
1282+ def setUp(self):
1283+ super(TestBugSubscriptionFilter, self).setUp()
1284+ self.target = self.factory.makeProduct()
1285+ self.subscriber = self.target.owner
1286+ login_person(self.subscriber)
1287+ self.subscription = self.target.addBugSubscription(
1288+ self.subscriber, self.subscriber)
1289+
1290+ def test_basics(self):
1291+ """Test the basic operation of `BugSubscriptionFilter` objects."""
1292+ # Create.
1293+ bug_subscription_filter = BugSubscriptionFilter()
1294+ bug_subscription_filter.structural_subscription = self.subscription
1295+ bug_subscription_filter.find_all_tags = True
1296+ bug_subscription_filter.include_any_tags = True
1297+ bug_subscription_filter.exclude_any_tags = True
1298+ bug_subscription_filter.other_parameters = u"foo"
1299+ bug_subscription_filter.description = u"bar"
1300+ # Flush and reload.
1301+ IStore(bug_subscription_filter).flush()
1302+ IStore(bug_subscription_filter).reload(bug_subscription_filter)
1303+ # Check.
1304+ self.assertIsNot(None, bug_subscription_filter.id)
1305+ self.assertEqual(
1306+ self.subscription.id,
1307+ bug_subscription_filter.structural_subscription_id)
1308+ self.assertEqual(
1309+ self.subscription,
1310+ bug_subscription_filter.structural_subscription)
1311+ self.assertIs(True, bug_subscription_filter.find_all_tags)
1312+ self.assertIs(True, bug_subscription_filter.include_any_tags)
1313+ self.assertIs(True, bug_subscription_filter.exclude_any_tags)
1314+ self.assertEqual(u"foo", bug_subscription_filter.other_parameters)
1315+ self.assertEqual(u"bar", bug_subscription_filter.description)
1316+
1317+ def test_defaults(self):
1318+ """Test the default values of `BugSubscriptionFilter` objects."""
1319+ # Create.
1320+ bug_subscription_filter = BugSubscriptionFilter()
1321+ bug_subscription_filter.structural_subscription = self.subscription
1322+ # Check.
1323+ self.assertIs(False, bug_subscription_filter.find_all_tags)
1324+ self.assertIs(False, bug_subscription_filter.include_any_tags)
1325+ self.assertIs(False, bug_subscription_filter.exclude_any_tags)
1326+ self.assertIs(None, bug_subscription_filter.other_parameters)
1327+ self.assertIs(None, bug_subscription_filter.description)
1328
1329=== added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py'
1330--- lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py 1970-01-01 00:00:00 +0000
1331+++ lib/lp/bugs/model/tests/test_bugsubscriptionfilterimportance.py 2010-09-23 11:12:49 +0000
1332@@ -0,0 +1,54 @@
1333+# Copyright 2010 Canonical Ltd. This software is licensed under the
1334+# GNU Affero General Public License version 3 (see the file LICENSE).
1335+
1336+"""Tests for the bugsubscription module."""
1337+
1338+__metaclass__ = type
1339+
1340+from canonical.launchpad.interfaces.lpstorm import IStore
1341+from canonical.testing import DatabaseFunctionalLayer
1342+from lp.bugs.interfaces.bugtask import BugTaskImportance
1343+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
1344+from lp.bugs.model.bugsubscriptionfilterimportance import (
1345+ BugSubscriptionFilterImportance,
1346+ )
1347+from lp.testing import (
1348+ login_person,
1349+ TestCaseWithFactory,
1350+ )
1351+
1352+
1353+class TestBugSubscriptionFilterImportance(TestCaseWithFactory):
1354+
1355+ layer = DatabaseFunctionalLayer
1356+
1357+ def setUp(self):
1358+ super(TestBugSubscriptionFilterImportance, self).setUp()
1359+ self.target = self.factory.makeProduct()
1360+ self.subscriber = self.target.owner
1361+ login_person(self.subscriber)
1362+ self.subscription = self.target.addBugSubscription(
1363+ self.subscriber, self.subscriber)
1364+ self.subscription_filter = BugSubscriptionFilter()
1365+ self.subscription_filter.structural_subscription = self.subscription
1366+
1367+ def test_basics(self):
1368+ """Test the basics of `BugSubscriptionFilterImportance` objects."""
1369+ # Create.
1370+ bug_sub_filter_importance = BugSubscriptionFilterImportance()
1371+ bug_sub_filter_importance.filter = self.subscription_filter
1372+ bug_sub_filter_importance.importance = BugTaskImportance.HIGH
1373+ # Flush and reload.
1374+ IStore(bug_sub_filter_importance).flush()
1375+ IStore(bug_sub_filter_importance).reload(bug_sub_filter_importance)
1376+ # Check.
1377+ self.assertIsNot(None, bug_sub_filter_importance.id)
1378+ self.assertEqual(
1379+ self.subscription_filter.id,
1380+ bug_sub_filter_importance.filter_id)
1381+ self.assertEqual(
1382+ self.subscription_filter,
1383+ bug_sub_filter_importance.filter)
1384+ self.assertEqual(
1385+ BugTaskImportance.HIGH,
1386+ bug_sub_filter_importance.importance)
1387
1388=== added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py'
1389--- lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py 1970-01-01 00:00:00 +0000
1390+++ lib/lp/bugs/model/tests/test_bugsubscriptionfilterstatus.py 2010-09-23 11:12:49 +0000
1391@@ -0,0 +1,52 @@
1392+# Copyright 2010 Canonical Ltd. This software is licensed under the
1393+# GNU Affero General Public License version 3 (see the file LICENSE).
1394+
1395+"""Tests for the bugsubscription module."""
1396+
1397+__metaclass__ = type
1398+
1399+from canonical.launchpad.interfaces.lpstorm import IStore
1400+from canonical.testing import DatabaseFunctionalLayer
1401+from lp.bugs.interfaces.bugtask import BugTaskStatus
1402+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
1403+from lp.bugs.model.bugsubscriptionfilterstatus import (
1404+ BugSubscriptionFilterStatus,
1405+ )
1406+from lp.testing import (
1407+ login_person,
1408+ TestCaseWithFactory,
1409+ )
1410+
1411+
1412+class TestBugSubscriptionFilterStatus(TestCaseWithFactory):
1413+
1414+ layer = DatabaseFunctionalLayer
1415+
1416+ def setUp(self):
1417+ super(TestBugSubscriptionFilterStatus, self).setUp()
1418+ self.target = self.factory.makeProduct()
1419+ self.subscriber = self.target.owner
1420+ login_person(self.subscriber)
1421+ self.subscription = self.target.addBugSubscription(
1422+ self.subscriber, self.subscriber)
1423+ self.subscription_filter = BugSubscriptionFilter()
1424+ self.subscription_filter.structural_subscription = self.subscription
1425+
1426+ def test_basics(self):
1427+ """Test the basics of `BugSubscriptionFilterStatus` objects."""
1428+ # Create.
1429+ bug_sub_filter_status = BugSubscriptionFilterStatus()
1430+ bug_sub_filter_status.filter = self.subscription_filter
1431+ bug_sub_filter_status.status = BugTaskStatus.NEW
1432+ # Flush and reload.
1433+ IStore(bug_sub_filter_status).flush()
1434+ IStore(bug_sub_filter_status).reload(bug_sub_filter_status)
1435+ # Check.
1436+ self.assertIsNot(None, bug_sub_filter_status.id)
1437+ self.assertEqual(
1438+ self.subscription_filter.id,
1439+ bug_sub_filter_status.filter_id)
1440+ self.assertEqual(
1441+ self.subscription_filter,
1442+ bug_sub_filter_status.filter)
1443+ self.assertEqual(BugTaskStatus.NEW, bug_sub_filter_status.status)
1444
1445=== added file 'lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py'
1446--- lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 1970-01-01 00:00:00 +0000
1447+++ lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 2010-09-23 11:12:49 +0000
1448@@ -0,0 +1,51 @@
1449+# Copyright 2010 Canonical Ltd. This software is licensed under the
1450+# GNU Affero General Public License version 3 (see the file LICENSE).
1451+
1452+"""Tests for the bugsubscription module."""
1453+
1454+__metaclass__ = type
1455+
1456+from canonical.launchpad.interfaces.lpstorm import IStore
1457+from canonical.testing import DatabaseFunctionalLayer
1458+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
1459+from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag
1460+from lp.testing import (
1461+ login_person,
1462+ TestCaseWithFactory,
1463+ )
1464+
1465+
1466+class TestBugSubscriptionFilterTag(TestCaseWithFactory):
1467+
1468+ layer = DatabaseFunctionalLayer
1469+
1470+ def setUp(self):
1471+ super(TestBugSubscriptionFilterTag, self).setUp()
1472+ self.target = self.factory.makeProduct()
1473+ self.subscriber = self.target.owner
1474+ login_person(self.subscriber)
1475+ self.subscription = self.target.addBugSubscription(
1476+ self.subscriber, self.subscriber)
1477+ self.subscription_filter = BugSubscriptionFilter()
1478+ self.subscription_filter.structural_subscription = self.subscription
1479+
1480+ def test_basics(self):
1481+ """Test the basics of `BugSubscriptionFilterTag` objects."""
1482+ # Create.
1483+ bug_sub_filter_tag = BugSubscriptionFilterTag()
1484+ bug_sub_filter_tag.filter = self.subscription_filter
1485+ bug_sub_filter_tag.include = True
1486+ bug_sub_filter_tag.tag = u"foo"
1487+ # Flush and reload.
1488+ IStore(bug_sub_filter_tag).flush()
1489+ IStore(bug_sub_filter_tag).reload(bug_sub_filter_tag)
1490+ # Check.
1491+ self.assertIsNot(None, bug_sub_filter_tag.id)
1492+ self.assertEqual(
1493+ self.subscription_filter.id,
1494+ bug_sub_filter_tag.filter_id)
1495+ self.assertEqual(
1496+ self.subscription_filter,
1497+ bug_sub_filter_tag.filter)
1498+ self.assertIs(True, bug_sub_filter_tag.include)
1499+ self.assertEqual(u"foo", bug_sub_filter_tag.tag)
1500
1501=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
1502--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-09-03 20:28:36 +0000
1503+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-09-23 11:12:49 +0000
1504@@ -18,6 +18,7 @@
1505 15
1506 >>> bug_entries = sorted(bugs['entries'], key=itemgetter('id'))
1507 >>> pprint_entry(bug_entries[0])
1508+ activity_collection_link: u'http://.../bugs/11/activity'
1509 attachments_collection_link: u'http://.../bugs/11/attachments'
1510 bug_tasks_collection_link: u'http://.../bugs/11/bug_tasks'
1511 bug_watches_collection_link: u'http://.../bugs/11/bug_watches'
1512@@ -2084,3 +2085,33 @@
1513 Traceback (most recent call last):
1514 ...
1515 NameError: name 'can_expire' is not defined
1516+
1517+
1518+Bug activity
1519+------------
1520+
1521+Each bug has a collection of activities that have taken place with it.
1522+
1523+ >>> from lazr.restful.testing.webservice import (
1524+ ... pprint_collection, pprint_entry)
1525+ >>> activity = webservice.get(
1526+ ... bug_one['activity_collection_link']).jsonBody()
1527+ >>> pprint_collection(activity)
1528+ next_collection_link: u'http://.../bugs/1/activity?ws.start=5&ws.size=5'
1529+ resource_type_link: u'http://.../#bug_activity-page-resource'
1530+ start: 0
1531+ total_size: 26
1532+ ...
1533+
1534+ >>> bug_nine_activity = webservice.get(
1535+ ... "/bugs/9/activity").jsonBody()
1536+ >>> pprint_entry(bug_nine_activity['entries'][1])
1537+ bug_link: u'http://.../bugs/9'
1538+ datechanged: u'2006-02-23T16:42:40.288553+00:00'
1539+ message: None
1540+ newvalue: u'Confirmed'
1541+ oldvalue: u'Unconfirmed'
1542+ person_link: u'http://.../~name12'
1543+ resource_type_link: u'http://.../#bug_activity'
1544+ self_link: u'http://.../bugs/9/activity'
1545+ whatchanged: u'thunderbird: status'
1546
1547=== modified file 'lib/lp/bugs/templates/bug-portlet-actions.pt'
1548--- lib/lp/bugs/templates/bug-portlet-actions.pt 2010-08-02 17:49:45 +0000
1549+++ lib/lp/bugs/templates/bug-portlet-actions.pt 2010-09-23 11:12:49 +0000
1550@@ -18,8 +18,8 @@
1551 class="menu-link-mark-dupe">Mark as duplicate</a>
1552 </span>
1553 <tal:block
1554- tal:condition="context/duplicates"
1555- tal:define="number_of_dupes context/duplicates/count"
1556+ tal:condition="context/number_of_duplicates"
1557+ tal:define="number_of_dupes context/number_of_duplicates"
1558 >
1559 </tal:block>
1560 <tal:block
1561
1562=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
1563--- lib/lp/bugs/templates/bugtask-index.pt 2010-09-22 19:35:18 +0000
1564+++ lib/lp/bugs/templates/bugtask-index.pt 2010-09-23 11:12:49 +0000
1565@@ -195,7 +195,7 @@
1566 </tal:branches>
1567 </div><!-- bug-branch-container -->
1568
1569- <div tal:condition="context/bug/cves" class="cves">
1570+ <div tal:condition="context/bug/has_cves" class="cves">
1571 <h2>CVE References</h2>
1572 <ul>
1573 <li class="sprite cve" tal:repeat="cve context/bug/cves">
1574
1575=== modified file 'lib/lp/bugs/tests/has-bug-supervisor.txt'
1576--- lib/lp/bugs/tests/has-bug-supervisor.txt 2009-08-25 11:21:05 +0000
1577+++ lib/lp/bugs/tests/has-bug-supervisor.txt 2010-09-23 11:12:49 +0000
1578@@ -5,7 +5,7 @@
1579 structural subscription targets. When the bug supervisor for such an object
1580 is set, a new bug subscription is created as well.
1581
1582- >>> target.bug_subscriptions
1583+ >>> list(target.bug_subscriptions)
1584 []
1585
1586 >>> print target.bug_supervisor
1587
1588=== modified file 'lib/lp/buildmaster/tests/mock_slaves.py'
1589--- lib/lp/buildmaster/tests/mock_slaves.py 2010-09-22 11:27:40 +0000
1590+++ lib/lp/buildmaster/tests/mock_slaves.py 2010-09-23 11:12:49 +0000
1591@@ -122,13 +122,11 @@
1592 return ('1.0', self.arch_tag, 'debian')
1593
1594 def sendFileToSlave(self, sha1, url, username="", password=""):
1595- self.call_log.append('sendFileToSlave')
1596 present, info = self.ensurepresent(sha1, url, username, password)
1597 if not present:
1598 raise CannotFetchFile(url, info)
1599
1600 def cacheFile(self, logger, libraryfilealias):
1601- self.call_log.append('cacheFile')
1602 return self.sendFileToSlave(
1603 libraryfilealias.content.sha1, libraryfilealias.http_url)
1604
1605
1606=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
1607--- lib/lp/code/browser/sourcepackagerecipe.py 2010-09-02 14:28:57 +0000
1608+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-09-23 11:12:49 +0000
1609@@ -60,6 +60,7 @@
1610 BuildAlreadyPending,
1611 NoSuchBranch,
1612 PrivateBranchRecipe,
1613+ TooNewRecipeFormat
1614 )
1615 from lp.code.interfaces.sourcepackagerecipe import (
1616 ISourcePackageRecipe,
1617@@ -333,6 +334,11 @@
1618 data['recipe_text'], data['description'], data['distros'],
1619 data['daily_build_archive'], data['build_daily'])
1620 Store.of(source_package_recipe).flush()
1621+ except TooNewRecipeFormat:
1622+ self.setFieldError(
1623+ 'recipe_text',
1624+ 'The recipe format version specified is not available.')
1625+ return
1626 except ForbiddenInstructionError:
1627 # XXX: bug=592513 We shouldn't be hardcoding "run" here.
1628 self.setFieldError(
1629
1630=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
1631--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-09-20 19:12:35 +0000
1632+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-09-23 11:12:49 +0000
1633@@ -40,6 +40,7 @@
1634 SourcePackageRecipeBuildView,
1635 )
1636 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
1637+from lp.code.tests.helpers import recipe_parser_newest_version
1638 from lp.registry.interfaces.pocket import PackagePublishingPocket
1639 from lp.soyuz.model.processor import ProcessorFamily
1640 from lp.testing import (
1641@@ -304,6 +305,24 @@
1642 self.assertEqual(
1643 get_message_text(browser, 2), 'foo is not a branch on Launchpad.')
1644
1645+ def test_create_recipe_format_too_new(self):
1646+ # If the recipe's format version is too new, we should notify the
1647+ # user.
1648+ product = self.factory.makeProduct(
1649+ name='ratatouille', displayname='Ratatouille')
1650+ branch = self.factory.makeBranch(
1651+ owner=self.chef, product=product, name='veggies')
1652+
1653+ with recipe_parser_newest_version(145.115):
1654+ recipe = dedent(u'''\
1655+ # bzr-builder format 145.115 deb-version 0+{revno}
1656+ %s
1657+ ''') % branch.bzr_identity
1658+ browser = self.createRecipe(recipe, branch)
1659+ self.assertEqual(
1660+ get_message_text(browser, 2),
1661+ 'The recipe format version specified is not available.')
1662+
1663 def test_create_dupe_recipe(self):
1664 # You shouldn't be able to create a duplicate recipe owned by the same
1665 # person with the same name.
1666
1667=== modified file 'lib/lp/code/mail/sourcepackagerecipebuild.py'
1668--- lib/lp/code/mail/sourcepackagerecipebuild.py 2010-08-20 20:31:18 +0000
1669+++ lib/lp/code/mail/sourcepackagerecipebuild.py 2010-09-23 11:12:49 +0000
1670@@ -67,6 +67,7 @@
1671 'duration': '',
1672 'builder_url': '',
1673 'build_url': canonical_url(self.build),
1674+ 'upload_log_url': '',
1675 })
1676 if self.build.builder is not None:
1677 params['builder_url'] = canonical_url(self.build.builder)
1678@@ -75,6 +76,8 @@
1679 params['duration'] = duration_formatter.approximateduration()
1680 if self.build.log is not None:
1681 params['log_url'] = self.build.log.getURL()
1682+ if self.build.upload_log is not None:
1683+ params['upload_log_url'] = self.build.upload_log_url
1684 return params
1685
1686 def _getFooter(self, params):
1687
1688=== modified file 'lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py'
1689--- lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-08-27 11:19:54 +0000
1690+++ lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-09-23 11:12:49 +0000
1691@@ -27,6 +27,7 @@
1692 * Distroseries: distroseries
1693 * Duration: five minutes
1694 * Build Log: %s
1695+ * Upload Log:
1696 * Builder: http://launchpad.dev/builders/bob
1697 """
1698
1699@@ -37,6 +38,7 @@
1700 * Distroseries: distroseries
1701 * Duration:
1702 * Build Log:
1703+ * Upload Log:
1704 * Builder:
1705 """
1706
1707@@ -44,6 +46,11 @@
1708
1709 layer = LaunchpadFunctionalLayer
1710
1711+ def makeStatusEmail(self, build):
1712+ mailer = SourcePackageRecipeBuildMailer.forStatus(build)
1713+ email = build.requester.preferredemail.email
1714+ return mailer.generateEmail(email, build.requester)
1715+
1716 def test_generateEmail(self):
1717 """GenerateEmail produces the right headers and body."""
1718 person = self.factory.makePerson(name='person')
1719@@ -59,9 +66,7 @@
1720 naked_build.builder = self.factory.makeBuilder(name='bob')
1721 naked_build.log = self.factory.makeLibraryFileAlias()
1722 Store.of(build).flush()
1723- mailer = SourcePackageRecipeBuildMailer.forStatus(build)
1724- email = build.requester.preferredemail.email
1725- ctrl = mailer.generateEmail(email, build.requester)
1726+ ctrl = self.makeStatusEmail(build)
1727 self.assertEqual(
1728 u'[recipe build #%d] of ~person recipe in distroseries: '
1729 'Successfully built' % (build.id), ctrl.subject)
1730@@ -93,9 +98,7 @@
1731 recipe=cake, distroseries=secret, archive=pantry,
1732 status=BuildStatus.SUPERSEDED)
1733 Store.of(build).flush()
1734- mailer = SourcePackageRecipeBuildMailer.forStatus(build)
1735- email = build.requester.preferredemail.email
1736- ctrl = mailer.generateEmail(email, build.requester)
1737+ ctrl = self.makeStatusEmail(build)
1738 self.assertEqual(
1739 u'[recipe build #%d] of ~person recipe in distroseries: '
1740 'Build for superseded Source' % (build.id), ctrl.subject)
1741@@ -114,6 +117,15 @@
1742 self.assertEqual(
1743 'SUPERSEDED', ctrl.headers['X-Launchpad-Build-State'])
1744
1745+ def test_generateEmail_upload_failure(self):
1746+ """GenerateEmail works when many fields are NULL."""
1747+ build = self.factory.makeSourcePackageRecipeBuild()
1748+ removeSecurityProxy(build).upload_log = (
1749+ self.factory.makeLibraryFileAlias())
1750+ upload_log_fragment = 'Upload Log: %s' % build.upload_log_url
1751+ ctrl = self.makeStatusEmail(build)
1752+ self.assertTrue(upload_log_fragment in ctrl.body)
1753+
1754
1755 def test_suite():
1756 return TestLoader().loadTestsFromName(__name__)
1757
1758=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
1759--- lib/lp/code/model/tests/test_recipebuilder.py 2010-09-22 11:26:19 +0000
1760+++ lib/lp/code/model/tests/test_recipebuilder.py 2010-09-23 11:12:49 +0000
1761@@ -12,16 +12,20 @@
1762 import unittest
1763
1764 import transaction
1765+from twisted.internet import defer
1766+from twisted.trial.unittest import TestCase as TrialTestCase
1767 from zope.security.proxy import removeSecurityProxy
1768
1769 from canonical.launchpad.scripts.logger import BufferLogger
1770-from canonical.testing import LaunchpadFunctionalLayer
1771+from canonical.testing import (
1772+ LaunchpadFunctionalLayer,
1773+ TwistedLaunchpadZopelessLayer,
1774+ )
1775 from lp.buildmaster.enums import BuildFarmJobType
1776 from lp.buildmaster.interfaces.builder import CannotBuild
1777 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
1778 IBuildFarmJobBehavior,
1779 )
1780-from lp.buildmaster.manager import RecordingSlave
1781 from lp.buildmaster.model.buildqueue import BuildQueue
1782 from lp.buildmaster.tests.mock_slaves import (
1783 MockBuilder,
1784@@ -36,25 +40,16 @@
1785 from lp.soyuz.model.processor import ProcessorFamilySet
1786 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
1787 from lp.testing import (
1788+ ANONYMOUS,
1789+ login_as,
1790+ logout,
1791 person_logged_in,
1792 TestCaseWithFactory,
1793 )
1794-
1795-
1796-class TestRecipeBuilder(TestCaseWithFactory):
1797-
1798- layer = LaunchpadFunctionalLayer
1799-
1800- def test_providesInterface(self):
1801- # RecipeBuildBehavior provides IBuildFarmJobBehavior.
1802- recipe_builder = RecipeBuildBehavior(None)
1803- self.assertProvides(recipe_builder, IBuildFarmJobBehavior)
1804-
1805- def test_adapts_ISourcePackageRecipeBuildJob(self):
1806- # IBuildFarmJobBehavior adapts a ISourcePackageRecipeBuildJob
1807- job = self.factory.makeSourcePackageRecipeBuild().makeJob()
1808- job = IBuildFarmJobBehavior(job)
1809- self.assertProvides(job, IBuildFarmJobBehavior)
1810+from lp.testing.factory import LaunchpadObjectFactory
1811+
1812+
1813+class RecipeBuilderTestsMixin:
1814
1815 def makeJob(self, recipe_registrant=None, recipe_owner=None):
1816 """Create a sample `ISourcePackageRecipeBuildJob`."""
1817@@ -87,6 +82,22 @@
1818 job = IBuildFarmJobBehavior(job)
1819 return job
1820
1821+
1822+class TestRecipeBuilder(TestCaseWithFactory, RecipeBuilderTestsMixin):
1823+
1824+ layer = LaunchpadFunctionalLayer
1825+
1826+ def test_providesInterface(self):
1827+ # RecipeBuildBehavior provides IBuildFarmJobBehavior.
1828+ recipe_builder = RecipeBuildBehavior(None)
1829+ self.assertProvides(recipe_builder, IBuildFarmJobBehavior)
1830+
1831+ def test_adapts_ISourcePackageRecipeBuildJob(self):
1832+ # IBuildFarmJobBehavior adapts a ISourcePackageRecipeBuildJob
1833+ job = self.factory.makeSourcePackageRecipeBuild().makeJob()
1834+ job = IBuildFarmJobBehavior(job)
1835+ self.assertProvides(job, IBuildFarmJobBehavior)
1836+
1837 def test_display_name(self):
1838 # display_name contains a sane description of the job
1839 job = self.makeJob()
1840@@ -241,32 +252,51 @@
1841 job.build, distroarchseries, None)
1842 self.assertEqual(args["archives"], expected_archives)
1843
1844+ def test_getById(self):
1845+ job = self.makeJob()
1846+ transaction.commit()
1847+ self.assertEquals(
1848+ job.build, SourcePackageRecipeBuild.getById(job.build.id))
1849+
1850+
1851+class TestDispatchBuildToSlave(TrialTestCase, RecipeBuilderTestsMixin):
1852+
1853+ layer = TwistedLaunchpadZopelessLayer
1854+
1855+ def setUp(self):
1856+ super(TestDispatchBuildToSlave, self).setUp()
1857+ self.factory = LaunchpadObjectFactory()
1858+ login_as(ANONYMOUS)
1859+ self.addCleanup(logout)
1860+ self.layer.switchDbUser('testadmin')
1861
1862 def test_dispatchBuildToSlave(self):
1863 # Ensure dispatchBuildToSlave will make the right calls to the slave
1864 job = self.makeJob()
1865 test_publisher = SoyuzTestPublisher()
1866 test_publisher.addFakeChroots(job.build.distroseries)
1867- slave = RecordingSlave("i386-slave-1", "http://myurl", "vmhost")
1868+ slave = OkSlave()
1869 builder = MockBuilder("bob-de-bouwer", slave)
1870 processorfamily = ProcessorFamilySet().getByProcessorName('386')
1871 builder.processor = processorfamily.processors[0]
1872 job.setBuilder(builder)
1873 logger = BufferLogger()
1874- job.dispatchBuildToSlave("someid", logger)
1875- logger.buffer.seek(0)
1876- self.assertEquals(
1877- "DEBUG: Initiating build 1-someid on http://fake:0000\n",
1878- logger.buffer.readline())
1879- self.assertEquals(["ensurepresent", "build"],
1880- [call[0] for call in slave.calls])
1881- build_args = slave.calls[1][1]
1882- self.assertEquals(
1883- build_args[0], job.buildfarmjob.generateSlaveBuildCookie())
1884- self.assertEquals(build_args[1], "sourcepackagerecipe")
1885- self.assertEquals(build_args[3], {})
1886- distroarchseries = job.build.distroseries.architectures[0]
1887- self.assertEqual(build_args[4], job._extraBuildArgs(distroarchseries))
1888+ d = defer.maybeDeferred(job.dispatchBuildToSlave, "someid", logger)
1889+ def check_dispatch(ignored):
1890+ logger.buffer.seek(0)
1891+ self.assertEquals(
1892+ "DEBUG: Initiating build 1-someid on http://fake:0000\n",
1893+ logger.buffer.readline())
1894+ self.assertEquals(["ensurepresent", "build"],
1895+ [call[0] for call in slave.call_log])
1896+ build_args = slave.call_log[1][1:]
1897+ self.assertEquals(
1898+ build_args[0], job.buildfarmjob.generateSlaveBuildCookie())
1899+ self.assertEquals(build_args[1], "sourcepackagerecipe")
1900+ self.assertEquals(build_args[3], [])
1901+ distroarchseries = job.build.distroseries.architectures[0]
1902+ self.assertEqual(build_args[4], job._extraBuildArgs(distroarchseries))
1903+ return d.addCallback(check_dispatch)
1904
1905 def test_dispatchBuildToSlave_nochroot(self):
1906 # dispatchBuildToSlave will fail when there is not chroot tarball
1907@@ -277,14 +307,8 @@
1908 builder.processor = processorfamily.processors[0]
1909 job.setBuilder(builder)
1910 logger = BufferLogger()
1911- self.assertRaises(CannotBuild, job.dispatchBuildToSlave,
1912- "someid", logger)
1913-
1914- def test_getById(self):
1915- job = self.makeJob()
1916- transaction.commit()
1917- self.assertEquals(
1918- job.build, SourcePackageRecipeBuild.getById(job.build.id))
1919+ d = defer.maybeDeferred(job.dispatchBuildToSlave, "someid", logger)
1920+ return self.assertFailure(d, CannotBuild)
1921
1922
1923 def test_suite():
1924
1925=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
1926--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-09-21 19:02:44 +0000
1927+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-09-23 11:12:49 +0000
1928@@ -53,6 +53,7 @@
1929 SourcePackageRecipe,
1930 )
1931 from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuildJob
1932+from lp.code.tests.helpers import recipe_parser_newest_version
1933 from lp.registry.interfaces.pocket import PackagePublishingPocket
1934 from lp.services.job.interfaces.job import (
1935 IJob,
1936@@ -246,11 +247,13 @@
1937 old_branches, list(sp_recipe.getReferencedBranches()))
1938
1939 def test_reject_newer_formats(self):
1940- builder_recipe = self.factory.makeRecipe()
1941- builder_recipe.format = 0.3
1942- self.assertRaises(
1943- TooNewRecipeFormat,
1944- self.factory.makeSourcePackageRecipe, recipe=str(builder_recipe))
1945+ with recipe_parser_newest_version(145.115):
1946+ builder_recipe = self.factory.makeRecipe()
1947+ builder_recipe.format = 145.115
1948+ self.assertRaises(
1949+ TooNewRecipeFormat,
1950+ self.factory.makeSourcePackageRecipe,
1951+ recipe=str(builder_recipe))
1952
1953 def test_requestBuild(self):
1954 recipe = self.factory.makeSourcePackageRecipe()
1955
1956=== modified file 'lib/lp/code/tests/helpers.py'
1957--- lib/lp/code/tests/helpers.py 2010-09-15 20:41:46 +0000
1958+++ lib/lp/code/tests/helpers.py 2010-09-23 11:12:49 +0000
1959@@ -14,11 +14,13 @@
1960 ]
1961
1962
1963+from contextlib import contextmanager
1964 from datetime import timedelta
1965 from difflib import unified_diff
1966 from itertools import count
1967 import transaction
1968
1969+from bzrlib.plugins.builder.recipe import RecipeParser
1970 from zope.component import getUtility
1971 from zope.security.proxy import (
1972 isinstance as zisinstance,
1973@@ -301,3 +303,13 @@
1974 make_project_branch_with_revisions(
1975 factory, gen, project, revision_count=num_commits)
1976 transaction.commit()
1977+
1978+
1979+@contextmanager
1980+def recipe_parser_newest_version(version):
1981+ old_version = RecipeParser.NEWEST_VERSION
1982+ RecipeParser.NEWEST_VERSION = version
1983+ try:
1984+ yield
1985+ finally:
1986+ RecipeParser.NEWEST_VERSION = old_version
1987
1988=== modified file 'lib/lp/codehosting/branchdistro.py'
1989--- lib/lp/codehosting/branchdistro.py 2010-09-21 03:47:19 +0000
1990+++ lib/lp/codehosting/branchdistro.py 2010-09-23 11:12:49 +0000
1991@@ -27,7 +27,7 @@
1992 from zope.component import getUtility
1993
1994 from canonical.config import config
1995-from canonical.launchpad.interfaces import ILaunchpadCelebrities
1996+from canonical.launchpad.interfaces import ILaunchpadCelebrities, IMasterStore
1997 from lp.code.enums import BranchLifecycleStatus, BranchType
1998 from lp.code.errors import BranchExists
1999 from lp.code.interfaces.branchcollection import IAllBranches
2000@@ -35,6 +35,7 @@
2001 from lp.code.interfaces.seriessourcepackagebranch import (
2002 IFindOfficialBranchLinks,
2003 )
2004+from lp.code.model.branchrevision import BranchRevision
2005 from lp.codehosting.vfs import branch_id_to_path
2006 from lp.registry.interfaces.distribution import IDistributionSet
2007 from lp.registry.interfaces.pocket import PackagePublishingPocket
2008@@ -350,4 +351,25 @@
2009 switch_branches(
2010 config.codehosting.mirrored_branches_root,
2011 'lp-internal', old_db_branch, new_db_branch)
2012+ # Directly copy the branch revisions from the old branch to the new branch.
2013+ store = IMasterStore(BranchRevision)
2014+ store.execute(
2015+ """
2016+ INSERT INTO BranchRevision (branch, revision, sequence)
2017+ SELECT %s, BranchRevision.revision, BranchRevision.sequence
2018+ FROM BranchRevision
2019+ WHERE branch = %s
2020+ """ % (new_db_branch.id, old_db_branch.id))
2021+
2022+ # Update the scanned details first, that way when hooking into
2023+ # branchChanged, it won't try to create a new scan job.
2024+ tip_revision = old_db_branch.getTipRevision()
2025+ new_db_branch.updateScannedDetails(
2026+ tip_revision, old_db_branch.revision_count)
2027+ new_db_branch.branchChanged(
2028+ '', tip_revision.revision_id,
2029+ old_db_branch.control_format,
2030+ old_db_branch.branch_format,
2031+ old_db_branch.repository_format)
2032+ transaction.commit()
2033 return new_db_branch
2034
2035=== modified file 'lib/lp/codehosting/tests/test_branchdistro.py'
2036--- lib/lp/codehosting/tests/test_branchdistro.py 2010-09-20 04:32:49 +0000
2037+++ lib/lp/codehosting/tests/test_branchdistro.py 2010-09-23 11:12:49 +0000
2038@@ -27,6 +27,8 @@
2039 from bzrlib.transport.chroot import ChrootServer
2040 from lazr.uri import URI
2041 import transaction
2042+from zope.component import getUtility
2043+from zope.security.proxy import removeSecurityProxy
2044
2045 from canonical.config import config
2046 from canonical.launchpad.scripts.logger import (
2047@@ -35,6 +37,7 @@
2048 )
2049 from canonical.testing.layers import LaunchpadZopelessLayer
2050 from lp.code.enums import BranchLifecycleStatus
2051+from lp.code.interfaces.branchjob import IBranchScanJobSource
2052 from lp.codehosting.branchdistro import (
2053 DistroBrancher,
2054 switch_branches,
2055@@ -127,6 +130,7 @@
2056 """Make an official package branch with an underlying bzr branch."""
2057 db_branch = self.factory.makePackageBranch(distroseries=distroseries)
2058 db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner)
2059+ self.factory.makeRevisionsForBranch(db_branch, count=1)
2060
2061 transaction.commit()
2062
2063@@ -241,6 +245,42 @@
2064 self.assertEqual(
2065 BranchLifecycleStatus.MATURE, db_branch.lifecycle_status)
2066
2067+ def test_makeOneNewBranch_avoids_need_for_scan(self):
2068+ # makeOneNewBranch sets the appropriate properties of the new branch
2069+ # so a scan is unnecessary. This can be done because we are making a
2070+ # copy of the source branch.
2071+ db_branch = self.makeOfficialPackageBranch()
2072+ self.factory.makeRevisionsForBranch(db_branch, count=10)
2073+ tip_revision_id = db_branch.last_mirrored_id
2074+ self.assertIsNot(None, tip_revision_id)
2075+ # The makeRevisionsForBranch will create a scan job for the db_branch.
2076+ # We don't really care about that, but what we do care about is that
2077+ # no new jobs are created.
2078+ existing_scan_job_count = len(
2079+ list(getUtility(IBranchScanJobSource).iterReady()))
2080+
2081+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
2082+ brancher.makeOneNewBranch(db_branch)
2083+ new_branch = brancher.new_distroseries.getSourcePackage(
2084+ db_branch.sourcepackage.name).getBranch(RELEASE)
2085+
2086+ self.assertEqual(tip_revision_id, new_branch.last_mirrored_id)
2087+ self.assertEqual(tip_revision_id, new_branch.last_scanned_id)
2088+ # Make sure that the branch revisions have been copied.
2089+ old_ancestry, old_history = removeSecurityProxy(
2090+ db_branch).getScannerData()
2091+ new_ancestry, new_history = removeSecurityProxy(
2092+ new_branch).getScannerData()
2093+ self.assertEqual(old_ancestry, new_ancestry)
2094+ self.assertEqual(old_history, new_history)
2095+ self.assertFalse(new_branch.pending_writes)
2096+ # The script doesn't have permission to create branch jobs, but just
2097+ # to be insanely paradoid.
2098+ transaction.commit()
2099+ self.layer.switchDbUser('launchpad')
2100+ scan_jobs = list(getUtility(IBranchScanJobSource).iterReady())
2101+ self.assertEqual(existing_scan_job_count, len(scan_jobs))
2102+
2103 def test_makeOneNewBranch_inconsistent_branch(self):
2104 # makeOneNewBranch skips over an inconsistent official package branch
2105 # (see `checkConsistentOfficialPackageBranch` for precisely what an
2106
2107=== modified file 'lib/lp/registry/browser/product.py'
2108--- lib/lp/registry/browser/product.py 2010-09-15 16:03:14 +0000
2109+++ lib/lp/registry/browser/product.py 2010-09-23 11:12:49 +0000
2110@@ -468,6 +468,7 @@
2111 # Add the branch configuration in separately.
2112 set_branch = series_menu['set_branch']
2113 set_branch.text = 'Configure project branch'
2114+ set_branch.summary = "Specify the location of this projects code."
2115 set_branch.configured = (
2116 )
2117 config_list.append(
2118
2119=== modified file 'lib/lp/registry/configure.zcml'
2120--- lib/lp/registry/configure.zcml 2010-09-23 02:15:42 +0000
2121+++ lib/lp/registry/configure.zcml 2010-09-23 11:12:49 +0000
2122@@ -453,6 +453,7 @@
2123 getReleasesAndPublishingHistory
2124 upstream_product
2125 target_type_display
2126+ _getOfficialTagClause
2127 official_bug_tags
2128 findRelatedArchives
2129 findRelatedArchivePublications
2130
2131=== modified file 'lib/lp/registry/interfaces/product.py'
2132--- lib/lp/registry/interfaces/product.py 2010-09-09 17:02:33 +0000
2133+++ lib/lp/registry/interfaces/product.py 2010-09-23 11:12:49 +0000
2134@@ -660,6 +660,7 @@
2135 'The series that represents the master or trunk branch. '
2136 'The Bazaar URL lp:<project> points to the development focus '
2137 'series branch.')))
2138+ development_focusID = Attribute("The development focus ID.")
2139
2140 name_with_project = Attribute(_("Returns the product name prefixed "
2141 "by the project name, if a project is associated with this "
2142
2143=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
2144--- lib/lp/registry/interfaces/projectgroup.py 2010-08-21 18:41:57 +0000
2145+++ lib/lp/registry/interfaces/projectgroup.py 2010-09-23 11:12:49 +0000
2146@@ -328,6 +328,8 @@
2147 def getSeries(series_name):
2148 """Return a ProjectGroupSeries object with name `series_name`."""
2149
2150+ product_milestones = Attribute('all the milestones for all the products.')
2151+
2152
2153 class IProjectGroup(IProjectGroupPublic,
2154 IProjectGroupModerate,
2155
2156=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
2157--- lib/lp/registry/model/distributionsourcepackage.py 2010-09-12 17:43:49 +0000
2158+++ lib/lp/registry/model/distributionsourcepackage.py 2010-09-23 11:12:49 +0000
2159@@ -494,6 +494,9 @@
2160 BugTask.sourcepackagename == self.sourcepackagename),
2161 user)
2162
2163+ def _getOfficialTagClause(self):
2164+ return self.distribution._getOfficialTagClause()
2165+
2166 @property
2167 def official_bug_tags(self):
2168 """See `IHasBugs`."""
2169
2170=== modified file 'lib/lp/registry/model/distroseries.py'
2171--- lib/lp/registry/model/distroseries.py 2010-09-21 19:34:45 +0000
2172+++ lib/lp/registry/model/distroseries.py 2010-09-23 11:12:49 +0000
2173@@ -743,6 +743,9 @@
2174 """Customize `search_params` for this distribution series."""
2175 search_params.setDistroSeries(self)
2176
2177+ def _getOfficialTagClause(self):
2178+ return self.distribution._getOfficialTagClause()
2179+
2180 @property
2181 def official_bug_tags(self):
2182 """See `IHasBugs`."""
2183
2184=== modified file 'lib/lp/registry/model/milestone.py'
2185--- lib/lp/registry/model/milestone.py 2010-09-14 15:04:06 +0000
2186+++ lib/lp/registry/model/milestone.py 2010-09-23 11:12:49 +0000
2187@@ -15,6 +15,7 @@
2188
2189 import datetime
2190
2191+from lazr.restful.error import expose
2192 from sqlobject import (
2193 AND,
2194 BoolCol,
2195@@ -28,6 +29,7 @@
2196 And,
2197 Store,
2198 )
2199+from storm.zope import IResultSet
2200 from zope.component import getUtility
2201 from zope.interface import implements
2202
2203@@ -36,7 +38,6 @@
2204 sqlvalues,
2205 )
2206 from canonical.launchpad.webapp.sorting import expand_numbers
2207-from lazr.restful.error import expose
2208 from lp.app.errors import NotFoundError
2209 from lp.blueprints.model.specification import Specification
2210 from lp.bugs.interfaces.bugtarget import IHasBugs
2211@@ -107,8 +108,7 @@
2212 result = store.find(Milestone, self._getMilestoneCondition())
2213 return result.order_by(self._milestone_order)
2214
2215- @property
2216- def milestones(self):
2217+ def _get_milestones(self):
2218 """See `IHasMilestones`."""
2219 store = Store.of(self)
2220 result = store.find(Milestone,
2221@@ -116,6 +116,8 @@
2222 Milestone.active == True))
2223 return result.order_by(self._milestone_order)
2224
2225+ milestones = property(_get_milestones)
2226+
2227
2228 class MultipleProductReleases(Exception):
2229 """Raised when a second ProductRelease is created for a milestone."""
2230@@ -229,7 +231,8 @@
2231 """See `IMilestone`."""
2232 params = BugTaskSearchParams(milestone=self, user=None)
2233 bugtasks = getUtility(IBugTaskSet).search(params)
2234- assert len(self.getSubscriptions()) == 0, (
2235+ subscriptions = IResultSet(self.getSubscriptions())
2236+ assert subscriptions.is_empty(), (
2237 "You cannot delete a milestone which has structural "
2238 "subscriptions.")
2239 assert bugtasks.count() == 0, (
2240@@ -256,7 +259,7 @@
2241 """See lp.registry.interfaces.milestone.IMilestoneSet."""
2242 try:
2243 return Milestone.get(milestoneid)
2244- except SQLObjectNotFound, err:
2245+ except SQLObjectNotFound:
2246 raise NotFoundError(
2247 "Milestone with ID %d does not exist" % milestoneid)
2248
2249
2250=== modified file 'lib/lp/registry/model/person.py'
2251--- lib/lp/registry/model/person.py 2010-09-10 20:27:19 +0000
2252+++ lib/lp/registry/model/person.py 2010-09-23 11:12:49 +0000
2253@@ -1265,6 +1265,8 @@
2254 # The owner is not a member but must retain his rights over
2255 # this team. This person may be a member of the owner, and in this
2256 # case it'll also have rights over this team.
2257+ # Note that this query and the tp query above can be consolidated
2258+ # when we get to a finer grained level of optimisations.
2259 in_team = self.inTeam(team.teamowner)
2260 else:
2261 in_team = False
2262
2263=== modified file 'lib/lp/registry/model/productseries.py'
2264--- lib/lp/registry/model/productseries.py 2010-09-21 19:34:45 +0000
2265+++ lib/lp/registry/model/productseries.py 2010-09-23 11:12:49 +0000
2266@@ -423,6 +423,9 @@
2267 """Customize `search_params` for this product series."""
2268 search_params.setProductSeries(self)
2269
2270+ def _getOfficialTagClause(self):
2271+ return self.product._getOfficialTagClause()
2272+
2273 @property
2274 def official_bug_tags(self):
2275 """See `IHasBugs`."""
2276
2277=== modified file 'lib/lp/registry/model/projectgroup.py'
2278--- lib/lp/registry/model/projectgroup.py 2010-09-09 15:26:11 +0000
2279+++ lib/lp/registry/model/projectgroup.py 2010-09-23 11:12:49 +0000
2280@@ -93,6 +93,7 @@
2281 from lp.registry.model.karma import KarmaContextMixin
2282 from lp.registry.model.mentoringoffer import MentoringOffer
2283 from lp.registry.model.milestone import (
2284+ HasMilestonesMixin,
2285 Milestone,
2286 ProjectMilestone,
2287 )
2288@@ -110,7 +111,8 @@
2289 MakesAnnouncements, HasSprintsMixin, HasAliasMixin,
2290 KarmaContextMixin, BranchVisibilityPolicyMixin,
2291 StructuralSubscriptionTargetMixin,
2292- HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin):
2293+ HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin,
2294+ HasMilestonesMixin):
2295 """A ProjectGroup"""
2296
2297 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,
2298@@ -309,6 +311,11 @@
2299 """Customize `search_params` for this milestone."""
2300 search_params.setProject(self)
2301
2302+ def _getOfficialTagClause(self):
2303+ """See `OfficialBugTagTargetMixin`."""
2304+ And(ProjectGroup.id == Product.projectID,
2305+ Product.id == OfficialBugTag.productID)
2306+
2307 @property
2308 def official_bug_tags(self):
2309 """See `IHasBugs`."""
2310@@ -396,6 +403,11 @@
2311 """
2312 return self.products.count() != 0
2313
2314+ def _getMilestoneCondition(self):
2315+ """See `HasMilestonesMixin`."""
2316+ return And(Milestone.productID == Product.id,
2317+ Product.projectID == self.id)
2318+
2319 def _getMilestones(self, only_active):
2320 """Return a list of milestones for this project group.
2321
2322@@ -445,6 +457,13 @@
2323 return self._getMilestones(True)
2324
2325 @property
2326+ def product_milestones(self):
2327+ """Hack to avoid the ProjectMilestone in MilestoneVocabulary."""
2328+ # XXX: bug=644977 Robert Collins - this is a workaround for
2329+ # insconsistency in project group milestone use.
2330+ return self._get_milestones()
2331+
2332+ @property
2333 def all_milestones(self):
2334 """See `IProjectGroup`."""
2335 return self._getMilestones(False)
2336
2337=== modified file 'lib/lp/registry/model/sourcepackage.py'
2338--- lib/lp/registry/model/sourcepackage.py 2010-09-07 00:51:44 +0000
2339+++ lib/lp/registry/model/sourcepackage.py 2010-09-23 11:12:49 +0000
2340@@ -481,6 +481,9 @@
2341 """Customize `search_params` for this source package."""
2342 search_params.setSourcePackage(self)
2343
2344+ def _getOfficialTagClause(self):
2345+ return self.distroseries._getOfficialTagClause()
2346+
2347 @property
2348 def official_bug_tags(self):
2349 """See `IHasBugs`."""
2350
2351=== modified file 'lib/lp/registry/model/structuralsubscription.py'
2352--- lib/lp/registry/model/structuralsubscription.py 2010-08-20 20:31:18 +0000
2353+++ lib/lp/registry/model/structuralsubscription.py 2010-09-23 11:12:49 +0000
2354@@ -266,47 +266,36 @@
2355 min_blueprint_notification_level=
2356 BlueprintNotificationLevel.NOTHING):
2357 """See `IStructuralSubscriptionTarget`."""
2358- target_clause_parts = []
2359- for key, value in self._target_args.items():
2360+ clauses = [
2361+ "StructuralSubscription.subscriber = Person.id",
2362+ "StructuralSubscription.bug_notification_level "
2363+ ">= %s" % quote(min_bug_notification_level),
2364+ "StructuralSubscription.blueprint_notification_level "
2365+ ">= %s" % quote(min_blueprint_notification_level),
2366+ ]
2367+ for key, value in self._target_args.iteritems():
2368 if value is None:
2369- target_clause_parts.append(
2370- "StructuralSubscription.%s IS NULL " % (key, ))
2371+ clauses.append(
2372+ "StructuralSubscription.%s IS NULL" % (key,))
2373 else:
2374- target_clause_parts.append(
2375- "StructuralSubscription.%s = %s " % (key, quote(value)))
2376- target_clause = " AND ".join(target_clause_parts)
2377- query = target_clause + """
2378- AND StructuralSubscription.subscriber = Person.id
2379- """
2380- all_subscriptions = StructuralSubscription.select(
2381- query,
2382- orderBy='Person.displayname',
2383- clauseTables=['Person'])
2384- subscriptions = [sub for sub
2385- in all_subscriptions
2386- if ((sub.bug_notification_level >=
2387- min_bug_notification_level) and
2388- (sub.blueprint_notification_level >=
2389- min_blueprint_notification_level))]
2390- return subscriptions
2391+ clauses.append(
2392+ "StructuralSubscription.%s = %s" % (key, quote(value)))
2393+ query = " AND ".join(clauses)
2394+ return StructuralSubscription.select(
2395+ query, orderBy='Person.displayname', clauseTables=['Person'])
2396
2397 def getBugNotificationsRecipients(self, recipients=None, level=None):
2398 """See `IStructuralSubscriptionTarget`."""
2399- subscribers = set()
2400 if level is None:
2401 subscriptions = self.bug_subscriptions
2402 else:
2403 subscriptions = self.getSubscriptions(
2404 min_bug_notification_level=level)
2405- for subscription in subscriptions:
2406- if (level is not None and
2407- subscription.bug_notification_level < level):
2408- continue
2409- subscriber = subscription.subscriber
2410- subscribers.add(subscriber)
2411- if recipients is not None:
2412- recipients.addStructuralSubscriber(
2413- subscriber, self)
2414+ subscribers = set(
2415+ subscription.subscriber for subscription in subscriptions)
2416+ if recipients is not None:
2417+ for subscriber in subscribers:
2418+ recipients.addStructuralSubscriber(subscriber, self)
2419 parent = self.parent_subscription_target
2420 if parent is not None:
2421 subscribers.update(
2422
2423=== modified file 'lib/lp/registry/vocabularies.py'
2424--- lib/lp/registry/vocabularies.py 2010-08-31 11:11:09 +0000
2425+++ lib/lp/registry/vocabularies.py 2010-09-23 11:12:49 +0000
2426@@ -1161,16 +1161,18 @@
2427 elif IDistroSeriesBugTask.providedBy(milestone_context):
2428 target = milestone_context.distroseries
2429 elif IProductSeriesBugTask.providedBy(milestone_context):
2430- target = milestone_context.productseries
2431+ target = milestone_context.productseries.product
2432 elif IDistributionSourcePackage.providedBy(milestone_context):
2433 target = milestone_context.distribution
2434 elif ISourcePackage.providedBy(milestone_context):
2435 target = milestone_context.distroseries
2436 elif ISpecification.providedBy(milestone_context):
2437 target = milestone_context.target
2438+ elif IProductSeries.providedBy(milestone_context):
2439+ # Show all the milestones of the product for a product series.
2440+ target = milestone_context.product
2441 elif (IProjectGroup.providedBy(milestone_context) or
2442 IProduct.providedBy(milestone_context) or
2443- IProductSeries.providedBy(milestone_context) or
2444 IDistribution.providedBy(milestone_context) or
2445 IDistroSeries.providedBy(milestone_context)):
2446 target = milestone_context
2447@@ -1196,23 +1198,10 @@
2448 # should be revisited after we've unblocked users.
2449 if target is not None:
2450 if IProjectGroup.providedBy(target):
2451- milestones = shortlist(
2452- (milestone for product in target.products
2453- for milestone in product.milestones),
2454- longest_expected=40)
2455- elif IProductSeries.providedBy(target):
2456- # While some milestones may be associated with a
2457- # productseries, we want to show all milestones for
2458- # the product. Since the database constraint
2459- # "valid_target" ensures that a milestone associated
2460- # with a series is also associated with the product
2461- # itself, we don't need to look up series-related
2462- # milestones.
2463- milestones = shortlist(target.product.milestones,
2464- longest_expected=40)
2465+ milestones_source = target.product_milestones
2466 else:
2467- milestones = shortlist(
2468- target.milestones, longest_expected=40)
2469+ milestones_source = target.milestones
2470+ milestones = shortlist(milestones_source, longest_expected=40)
2471 else:
2472 # We can't use context to reasonably filter the
2473 # milestones, so let's either just grab all of them,
2474@@ -1249,9 +1238,11 @@
2475 product_ids = set(
2476 removeSecurityProxy(milestone).productID
2477 for milestone in milestones)
2478+ product_ids.discard(None)
2479 distro_ids = set(
2480 removeSecurityProxy(milestone).distributionID
2481 for milestone in milestones)
2482+ distro_ids.discard(None)
2483 if len(product_ids) > 0:
2484 list(Product.select("id IN %s" % sqlvalues(product_ids)))
2485 if len(distro_ids) > 0:
2486
2487=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py'
2488--- lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-09-22 11:29:08 +0000
2489+++ lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-09-23 11:12:49 +0000
2490@@ -94,7 +94,7 @@
2491 extra_urls = []
2492
2493 upload_logs = [
2494- ['cacheFile', 'sendFileToSlave', ('ensurepresent', url, '', '')]
2495+ ('ensurepresent', url, '', '')
2496 for url in [chroot.http_url] + extra_urls]
2497
2498 extra_args = {
2499@@ -110,7 +110,7 @@
2500 build_log = [
2501 ('build', build_id, 'binarypackage', chroot.content.sha1,
2502 filemap_names, extra_args)]
2503- return sum(upload_logs, []) + build_log
2504+ return upload_logs + build_log
2505
2506 def startBuild(self, builder, candidate):
2507 builder = removeSecurityProxy(builder)

Subscribers

People subscribed via source and target branches

to status/vote changes: