Merge lp:~allenap/launchpad/bugsubscription-to-storm into lp:launchpad

Proposed by Gavin Panella on 2010-09-15
Status: Merged
Approved by: Gavin Panella on 2010-09-17
Approved revision: no longer in the source branch.
Merged at revision: 11646
Proposed branch: lp:~allenap/launchpad/bugsubscription-to-storm
Merge into: lp:launchpad
Diff against target: 598 lines (+123/-88)
17 files modified
lib/lp/bugs/browser/bug.py (+2/-2)
lib/lp/bugs/browser/bugsubscription.py (+4/-1)
lib/lp/bugs/browser/tests/bug-portlet-subscribers-content.txt (+2/-2)
lib/lp/bugs/browser/tests/bug-views.txt (+3/-3)
lib/lp/bugs/browser/tests/bugs-views.txt (+1/-1)
lib/lp/bugs/browser/tests/person-bug-views.txt (+1/-1)
lib/lp/bugs/doc/bugnotification-sending.txt (+4/-4)
lib/lp/bugs/doc/bugsubscription.txt (+7/-7)
lib/lp/bugs/doc/bugtask-expiration.txt (+1/-1)
lib/lp/bugs/doc/milestones-from-bugtask-search.txt (+1/-1)
lib/lp/bugs/model/bug.py (+33/-32)
lib/lp/bugs/model/bugsubscription.py (+37/-23)
lib/lp/bugs/model/bugtask.py (+2/-2)
lib/lp/bugs/tests/bugs-emailinterface.txt (+1/-1)
lib/lp/bugs/tests/test_bug.py (+17/-0)
lib/lp/hardwaredb/doc/hwdb-device-tables.txt (+5/-5)
lib/lp/hardwaredb/model/hwdb.py (+2/-2)
To merge this branch: bzr merge lp:~allenap/launchpad/bugsubscription-to-storm
Reviewer Review Type Date Requested Status
Michael Nelson (community) code 2010-09-15 Approve on 2010-09-16
Review via email: mp+35537@code.launchpad.net

Commit Message

Change BugSubscription to a regular Storm model object; it no longer uses the SQLObject shim.

Description of the Change

This converts BugSubscription to inherit from Storm instead of SQLObject.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

We can't really do this till we have a clean base class that hooks
into the IPropertyCache clear. :(

Gavin Panella (allenap) wrote :

@Rob BugSubscription doesn't use any cached properties, so I don't follow. We can switch the base class at a later date, en masse, when one is available. Would that work? I suspect I'm missing something.

Robert Collins (lifeless) wrote :

On Thu, Sep 16, 2010 at 8:36 PM, Gavin Panella
<email address hidden> wrote:
> @Rob BugSubscription doesn't use any cached properties, so I don't follow. We can switch the base class at a later date, en masse, when one is available. Would that work? I suspect I'm missing something.

That would be fine; I'm just sensitised to the lack and hoping someone
else will do it ;)

-Rob

Michael Nelson (michael.nelson) wrote :
Download full text (3.7 KiB)

Hi Gavin,

r=me, great to see it being stormified :) I've got one thought that you might want to try below.

Cheers,
Michael

> === modified file 'lib/lp/bugs/model/bug.py'
> --- lib/lp/bugs/model/bug.py 2010-09-09 17:02:33 +0000
> +++ lib/lp/bugs/model/bug.py 2010-09-16 12:35:16 +0000
> @@ -730,27 +737,28 @@
> """See `IBug`."""
> if self.private:
> return []
> -
> - duplicate_subscriptions = set(
> - BugSubscription.select("""
> - BugSubscription.bug = Bug.id AND
> - Bug.duplicateof = %d""" % self.id,
> - prejoins=["person"], clauseTables=["Bug"]))
> -
> - # Only add a subscriber once to the list.
> - duplicate_subscribers = set(
> - sub.person for sub in duplicate_subscriptions)
> - subscriptions = []
> - for duplicate_subscriber in duplicate_subscribers:
> - for duplicate_subscription in duplicate_subscriptions:
> - if duplicate_subscription.person == duplicate_subscriber:
> - subscriptions.append(duplicate_subscription)
> - break
> -
> - def get_person_displayname(subscription):
> - return subscription.person.displayname
> -
> - return sorted(subscriptions, key=get_person_displayname)
> + # Get bug subscribers and subscriptions for duplicates.
> + subscribers_and_subscriptions = (
> + IStore(BugSubscription).find(
> + (Person, BugSubscription),
> + BugSubscription.person_id == Person.id,
> + BugSubscription.bug_id == Bug.id,
> + Bug.duplicateof == self).order_by(
> + Person.id, BugSubscription.id))
> + # Generator for the subscription objects alone. The subscribers
> + # (Person objects) are loaded to warm up the cache only.
> + subscriptions = (
> + subscription for subscriber, subscription in (
> + subscribers_and_subscriptions))
> + # Group by the subscriber. The results are ordered by subscriber and
> + # subscription id, so we can grab the earliest subscription for a
> + # subscriber (and that the results of this method are stable).
> + subscriptions = (
> + list(subscriptions)[0] for person_id, subscriptions in groupby(
> + subscriptions, key=operator.attrgetter("person_id")))
> + # Sort by the subscriber's display name.
> + return sorted(
> + subscriptions, key=operator.attrgetter("person.displayname"))

We chatted about this:
{{{
14:42 < noodles775> allenap: what order of subscriptions are returned by getSubscriptionsFromDuplicates()?
14:43 * allenap looks
14:44 < noodles775> sorry, I meant how many subscriptions - and whether it's currently a method with performance issues or not.
14:46 < allenap> noodles775: I don't know if it has performance problems, but the code certainly appears sub-optimal. It should return one subscription per subscriber. Each subscriber could have more than one subscription so I've ensured that the earliest is always returned. I /think/ it was undefined before.
}}}...

Read more...

review: Approve (code)
Robert Collins (lifeless) wrote :

ugh, nooo

just do
DISTINCT ON

If the contract is 'return on subscription per subscriber' your
DISTINCT ON clause will want to be (subscriber.id) [e.g. Person.id, or
TeamParticipation.person, or whatever].

-Rob

Michael Nelson (michael.nelson) wrote :

On Fri, Sep 17, 2010 at 2:09 AM, Robert Collins
<email address hidden> wrote:
> ugh, nooo
>
> just do
> DISTINCT ON
>
> If the contract is 'return on subscription per subscriber' your
> DISTINCT ON clause will want to be (subscriber.id) [e.g. Person.id, or
> TeamParticipation.person, or whatever].

Even better - I'd not seen DISTINCT ON before. Just read a bit more
about this exact issue here:
http://www.postgresonline.com/journal/archives/4-Using-Distinct-ON-to-return-newest-order-for-each-customer.html

Cheers,
M.

Gavin Panella (allenap) wrote :

Oh yes, thanks Rob. I feel a bit foolish; I've used it before :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bug.py'
2--- lib/lp/bugs/browser/bug.py 2010-09-21 10:03:47 +0000
3+++ lib/lp/bugs/browser/bug.py 2010-09-24 14:31:45 +0000
4@@ -455,10 +455,10 @@
5 """
6 if IBug.providedBy(self.context):
7 dupe_subs = self.context.getSubscriptionsFromDuplicates()
8- return set([sub.person for sub in dupe_subs])
9+ return set(sub.person for sub in dupe_subs)
10 elif IBugTask.providedBy(self.context):
11 dupe_subs = self.context.bug.getSubscriptionsFromDuplicates()
12- return set([sub.person for sub in dupe_subs])
13+ return set(sub.person for sub in dupe_subs)
14 else:
15 raise NotImplementedError(
16 'duplicate_subscribers is not implemented for %s' % self)
17
18=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
19--- lib/lp/bugs/browser/bugsubscription.py 2010-08-23 09:16:37 +0000
20+++ lib/lp/bugs/browser/bugsubscription.py 2010-09-24 14:31:45 +0000
21@@ -95,7 +95,10 @@
22 """Get the list of subscriptions to duplicates of this bug."""
23 return [
24 SubscriptionAttrDecorator(subscription)
25- for subscription in self.context.getSubscriptionsFromDuplicates()]
26+ for subscription in sorted(
27+ self.context.getSubscriptionsFromDuplicates(),
28+ key=(lambda subscription: subscription.person.displayname))
29+ ]
30
31
32 class BugPortletSubcribersIds(LaunchpadView, BugViewMixin):
33
34=== modified file 'lib/lp/bugs/browser/tests/bug-portlet-subscribers-content.txt'
35--- lib/lp/bugs/browser/tests/bug-portlet-subscribers-content.txt 2009-11-06 17:15:31 +0000
36+++ lib/lp/bugs/browser/tests/bug-portlet-subscribers-content.txt 2010-09-24 14:31:45 +0000
37@@ -12,9 +12,9 @@
38 >>> bug = factory.makeBug(owner=reporter)
39 >>> view = BugPortletSubcribersContents(bug, None)
40 >>> bug.subscribe(view.user, view.user)
41- <BugSubscription at ...>
42+ <lp.bugs.model.bugsubscription.BugSubscription ...>
43 >>> bug.subscribe(view.user.teams_participated_in[0], view.user)
44- <BugSubscription at ...>
45+ <lp.bugs.model.bugsubscription.BugSubscription ...>
46 >>> for subscription in view.sorted_direct_subscriptions:
47 ... print '%s %s' % (
48 ... subscription.person.displayname,
49
50=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
51--- lib/lp/bugs/browser/tests/bug-views.txt 2010-08-07 14:54:40 +0000
52+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-09-24 14:31:45 +0000
53@@ -325,7 +325,7 @@
54 If we subscribe Foo Bar, 'Unsubscribe' is shown.
55
56 >>> bug_one.subscribe(foo_bar, foo_bar)
57- <BugSubscription at ...>
58+ <lp.bugs.model.bugsubscription.BugSubscription ...>
59 >>> bug_menu = BugContextMenu(bug_one_bugtask)
60 >>> bug_menu.subscription().text
61 'Unsubscribe'
62@@ -340,7 +340,7 @@
63 >>> foo_bar.inTeam(launchpad_team)
64 True
65 >>> bug_one.subscribe(launchpad_team, launchpad_team)
66- <BugSubscription at ...>
67+ <lp.bugs.model.bugsubscription.BugSubscription ...>
68 >>> bug_menu = BugContextMenu(bug_one_bugtask)
69 >>> bug_menu.subscription().text
70 'Unsubscribe'
71@@ -416,7 +416,7 @@
72
73 >>> ubuntu_team = getUtility(IPersonSet).getByName("ubuntu-team")
74 >>> bug_two.subscribe(ubuntu_team, ubuntu_team)
75- <BugSubscription...>
76+ <lp.bugs.model.bugsubscription.BugSubscription ...>
77
78 >>> bug_two.markAsDuplicate(bug_three)
79 >>> syncUpdate(bug_two)
80
81=== modified file 'lib/lp/bugs/browser/tests/bugs-views.txt'
82--- lib/lp/bugs/browser/tests/bugs-views.txt 2009-06-12 16:36:02 +0000
83+++ lib/lp/bugs/browser/tests/bugs-views.txt 2010-09-24 14:31:45 +0000
84@@ -98,7 +98,7 @@
85 >>> bug_two.subscribe(
86 ... person_set.getByEmail('test@canonical.com'),
87 ... person_set.getByEmail('foo.bar@canonical.com'))
88- <BugSubscription...>
89+ <lp.bugs.model.bugsubscription.BugSubscription ...>
90 >>> login('test@canonical.com')
91 >>> bugs_view = MaloneView(MaloneApplication(), LaunchpadTestRequest())
92 >>> bugs_view.initialize()
93
94=== modified file 'lib/lp/bugs/browser/tests/person-bug-views.txt'
95--- lib/lp/bugs/browser/tests/person-bug-views.txt 2010-06-16 16:46:06 +0000
96+++ lib/lp/bugs/browser/tests/person-bug-views.txt 2010-09-24 14:31:45 +0000
97@@ -621,7 +621,7 @@
98 Once new_user has subscribed, the related milestones appear.
99
100 >>> bug.subscribe(new_user, new_user)
101- <BugSubscription at ...>
102+ <lp.bugs.model.bugsubscription.BugSubscription ...>
103
104 >>> print pretty(subscribed_bugs_view.getMilestoneWidgetValues())
105 [{'checked': False,
106
107=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
108--- lib/lp/bugs/doc/bugnotification-sending.txt 2010-09-02 16:33:43 +0000
109+++ lib/lp/bugs/doc/bugnotification-sending.txt 2010-09-24 14:31:45 +0000
110@@ -858,13 +858,13 @@
111 ... displayname='Verbose Person', email='verbose@example.com')
112 >>> verbose_person.verbose_bugnotifications = True
113 >>> bug.subscribe(verbose_person, verbose_person)
114- <BugSubscription...>
115+ <lp.bugs.model.bugsubscription.BugSubscription ...>
116
117 >>> concise_person = factory.makePerson(
118 ... displayname='Concise Person', email='concise@example.com')
119 >>> concise_person.verbose_bugnotifications = False
120 >>> bug.subscribe(concise_person, concise_person)
121- <BugSubscription...>
122+ <lp.bugs.model.bugsubscription.BugSubscription ...>
123
124
125 Concise Team doesn't want verbose notifications, while Concise Team
126@@ -880,7 +880,7 @@
127 >>> ignored = concise_team.addMember(
128 ... concise_team_person, concise_team_person)
129 >>> bug.subscribe(concise_team, concise_team_person)
130- <BugSubscription...>
131+ <lp.bugs.model.bugsubscription.BugSubscription ...>
132
133 Verbose Team wants verbose notifications, while Verbose Team Person, a
134 member, does not.
135@@ -895,7 +895,7 @@
136 >>> ignored = verbose_team.addMember(
137 ... verbose_team_person, verbose_team_person)
138 >>> bug.subscribe(verbose_team, verbose_team_person)
139- <BugSubscription...>
140+ <lp.bugs.model.bugsubscription.BugSubscription ...>
141
142 We'll expire all existing notifications since we're not interested in
143 them:
144
145=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
146--- lib/lp/bugs/doc/bugsubscription.txt 2010-09-09 21:00:54 +0000
147+++ lib/lp/bugs/doc/bugsubscription.txt 2010-09-24 14:31:45 +0000
148@@ -49,7 +49,7 @@
149 >>> personset = getUtility(IPersonSet)
150
151 >>> linux_source = ubuntu.getSourcePackage("linux-source-2.6.15")
152- >>> linux_source.bug_subscriptions
153+ >>> list(linux_source.bug_subscriptions)
154 []
155 >>> print linux_source.distribution.bug_supervisor
156 None
157@@ -78,7 +78,7 @@
158 >>> mark = personset.getByName("mark")
159
160 >>> linux_source_bug.subscribe(mark, mark)
161- <BugSubscription ...>
162+ <lp.bugs.model.bugsubscription.BugSubscription ...>
163
164 >>> print_displayname(linux_source_bug.getDirectSubscribers())
165 Foo Bar
166@@ -527,7 +527,7 @@
167 False
168
169 >>> bug_six.subscribe(sample_person, sample_person)
170- <BugSubscription...>
171+ <lp.bugs.model.bugsubscription.BugSubscription...>
172
173 >>> bug_five.isSubscribedToDupes(sample_person)
174 True
175@@ -781,7 +781,7 @@
176 contacts:
177
178 >>> evolution = ubuntu.getSourcePackage("evolution")
179- >>> evolution.bug_subscriptions
180+ >>> list(evolution.bug_subscriptions)
181 []
182
183 >>> params = CreateBugParams(
184@@ -858,7 +858,7 @@
185 ... owner=mark)
186 >>> ff_bug = firefox.createBug(params)
187 >>> ff_bug.subscribe(lifeless, mark)
188- <BugSubscription at ...>
189+ <lp.bugs.model.bugsubscription.BugSubscription ...>
190 >>> for subscription in ff_bug.getDirectSubscriptions():
191 ... print '%s (%s)' % (
192 ... subscription.person.displayname,
193@@ -873,10 +873,10 @@
194 >>> dupe_ff_bug.markAsDuplicate(ff_bug)
195 >>> dupe_ff_bug.syncUpdate()
196 >>> dupe_ff_bug.subscribe(foobar, lifeless)
197- <BugSubscription at ...>
198+ <lp.bugs.model.bugsubscription.BugSubscription ...>
199 >>> for subscription in ff_bug.getSubscriptionsFromDuplicates():
200 ... print '%s (%s)' % (
201 ... subscription.person.displayname,
202 ... subscription.display_duplicate_subscribed_by)
203+ Scott James Remnant (Self-subscribed to bug 28)
204 Foo Bar (Subscribed to bug 28 by Robert Collins)
205- Scott James Remnant (Self-subscribed to bug 28)
206
207=== modified file 'lib/lp/bugs/doc/bugtask-expiration.txt'
208--- lib/lp/bugs/doc/bugtask-expiration.txt 2010-06-29 15:48:57 +0000
209+++ lib/lp/bugs/doc/bugtask-expiration.txt 2010-09-24 14:31:45 +0000
210@@ -398,7 +398,7 @@
211 ... unless he's subscribed to the bug.
212
213 >>> private_bug.subscribe(no_priv, sample_person)
214- <BugSubscription at ...>
215+ <lp.bugs.model.bugsubscription.BugSubscription ...>
216 >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
217 ... 0, user=no_priv, target=ubuntu)
218 >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
219
220=== modified file 'lib/lp/bugs/doc/milestones-from-bugtask-search.txt'
221--- lib/lp/bugs/doc/milestones-from-bugtask-search.txt 2010-06-16 16:46:06 +0000
222+++ lib/lp/bugs/doc/milestones-from-bugtask-search.txt 2010-09-24 14:31:45 +0000
223@@ -17,7 +17,7 @@
224 >>> bugtask = factory.makeBugTask(target=milestone.product)
225 >>> bugtask.milestone = milestone
226 >>> bugtask.subscribe(person, person)
227- <BugSubscription ...>
228+ <lp.bugs.model.bugsubscription.BugSubscription ...>
229 >>> transaction.commit()
230
231 The results of a search for all bug tasks related to a person can be
232
233=== modified file 'lib/lp/bugs/model/bug.py'
234--- lib/lp/bugs/model/bug.py 2010-09-23 12:27:33 +0000
235+++ lib/lp/bugs/model/bug.py 2010-09-24 14:31:45 +0000
236@@ -51,6 +51,7 @@
237 In,
238 LeftJoin,
239 Max,
240+ Min,
241 Not,
242 Or,
243 Select,
244@@ -328,10 +329,6 @@
245 cve_links = SQLMultipleJoin('BugCve', joinColumn='bug', orderBy='id')
246 mentoring_offers = SQLMultipleJoin(
247 'MentoringOffer', joinColumn='bug', orderBy='id')
248- # XXX: kiko 2006-09-23: Why is subscriptions ordered by ID?
249- subscriptions = SQLMultipleJoin(
250- 'BugSubscription', joinColumn='bug', orderBy='id',
251- prejoins=["person"])
252 duplicates = SQLMultipleJoin(
253 'Bug', joinColumn='duplicateof', orderBy='id')
254 specifications = SQLRelatedJoin('Specification', joinColumn='bug',
255@@ -791,6 +788,16 @@
256 """See `IBug`."""
257 return self.personIsSubscribedToDuplicate(person)
258
259+ @property
260+ def subscriptions(self):
261+ """The set of `BugSubscriptions` for this bug."""
262+ # XXX: kiko 2006-09-23: Why is subscriptions ordered by ID?
263+ results = Store.of(self).find(
264+ (Person, BugSubscription),
265+ BugSubscription.person_id == Person.id,
266+ BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
267+ return DecoratedResultSet(results, operator.itemgetter(1))
268+
269 def getDirectSubscriptions(self):
270 """See `IBug`."""
271 # Cache valid persons so that <person>.is_valid_person can
272@@ -801,14 +808,14 @@
273 valid_persons = Store.of(self).find(
274 (Person, ValidPersonCache),
275 Person.id == ValidPersonCache.id,
276- ValidPersonCache.id == BugSubscription.personID,
277+ ValidPersonCache.id == BugSubscription.person_id,
278 BugSubscription.bug == self)
279 # Suck in all the records so that they're actually cached.
280 list(valid_persons)
281 # Do the main query.
282 return Store.of(self).find(
283 BugSubscription,
284- BugSubscription.personID == Person.id,
285+ BugSubscription.person_id == Person.id,
286 BugSubscription.bug == self).order_by(
287 Func('person_sort_key', Person.displayname, Person.name))
288
289@@ -852,27 +859,21 @@
290 """See `IBug`."""
291 if self.private:
292 return []
293-
294- duplicate_subscriptions = set(
295- BugSubscription.select("""
296- BugSubscription.bug = Bug.id AND
297- Bug.duplicateof = %d""" % self.id,
298- prejoins=["person"], clauseTables=["Bug"]))
299-
300- # Only add a subscriber once to the list.
301- duplicate_subscribers = set(
302- sub.person for sub in duplicate_subscriptions)
303- subscriptions = []
304- for duplicate_subscriber in duplicate_subscribers:
305- for duplicate_subscription in duplicate_subscriptions:
306- if duplicate_subscription.person == duplicate_subscriber:
307- subscriptions.append(duplicate_subscription)
308- break
309-
310- def get_person_displayname(subscription):
311- return subscription.person.displayname
312-
313- return sorted(subscriptions, key=get_person_displayname)
314+ # For each subscription to each duplicate of this bug, find the
315+ # earliest subscription for each subscriber. Eager load the
316+ # subscribers.
317+ return DecoratedResultSet(
318+ IStore(BugSubscription).find(
319+ # XXX: GavinPanella 2010-09-17 bug=374777: This SQL(...) is a
320+ # hack; it does not seem to be possible to express DISTINCT ON
321+ # with Storm.
322+ (SQL("DISTINCT ON (BugSubscription.person) 0 AS ignore"),
323+ Person, BugSubscription),
324+ Bug.duplicateof == self,
325+ BugSubscription.bug_id == Bug.id,
326+ BugSubscription.person_id == Person.id).order_by(
327+ BugSubscription.person_id),
328+ operator.itemgetter(2))
329
330 def getSubscribersFromDuplicates(self, recipients=None, level=None):
331 """See `IBug`.
332@@ -887,10 +888,10 @@
333 Store.of(self).find(
334 (Person, Bug),
335 BugSubscription.person == Person.id,
336- BugSubscription.bug == Bug.id,
337+ BugSubscription.bug_id == Bug.id,
338 Bug.duplicateof == self.id))
339
340- dupe_subscribers = set([person for person in dupe_details.keys()])
341+ dupe_subscribers = set(dupe_details)
342
343 # Direct and "also notified" subscribers take precedence over
344 # subscribers from dupes. Note that we don't supply recipients
345@@ -914,7 +915,7 @@
346 self._unsubscribed_cache.add(person)
347 def cache_subscriber(row):
348 _, subscriber, subscription = row
349- if subscription.bugID == self.id:
350+ if subscription.bug_id == self.id:
351 self._subscriber_cache.add(subscriber)
352 else:
353 self._subscriber_dups_cache.add(subscriber)
354@@ -931,7 +932,7 @@
355 Bug.id == self.id,
356 Bug.duplicateof == self.id),
357 # Get subscriptions for these bugs
358- BugSubscription.bug == Bug.id,
359+ BugSubscription.bug_id == Bug.id,
360 # Filter by subscriptions to any team person is in.
361 # Note that teamparticipation includes self-participation entries
362 # (person X is in the team X)
363@@ -1890,7 +1891,7 @@
364 subscriptions_from_dupes = store.find(
365 BugSubscription,
366 Bug.duplicateof == self,
367- BugSubscription.bugID == Bug.id,
368+ BugSubscription.bug_id == Bug.id,
369 BugSubscription.person == person)
370
371 return not subscriptions_from_dupes.is_empty()
372
373=== modified file 'lib/lp/bugs/model/bugsubscription.py'
374--- lib/lp/bugs/model/bugsubscription.py 2010-09-09 21:00:54 +0000
375+++ lib/lp/bugs/model/bugsubscription.py 2010-09-24 14:31:45 +0000
376@@ -6,41 +6,55 @@
377 __metaclass__ = type
378 __all__ = ['BugSubscription']
379
380-from sqlobject import ForeignKey
381+import pytz
382+from storm.base import Storm
383+from storm.locals import (
384+ DateTime,
385+ Int,
386+ Reference,
387+ )
388 from zope.interface import implements
389
390 from canonical.database.constants import UTC_NOW
391-from canonical.database.datetimecol import UtcDateTimeCol
392-from canonical.database.enumcol import EnumCol
393-from canonical.database.sqlbase import SQLBase
394+from canonical.database.enumcol import DBEnum
395 from lp.bugs.interfaces.bugsubscription import IBugSubscription
396 from lp.registry.enum import BugNotificationLevel
397 from lp.registry.interfaces.person import validate_person
398
399
400-class BugSubscription(SQLBase):
401+class BugSubscription(Storm):
402 """A relationship between a person and a bug."""
403
404 implements(IBugSubscription)
405
406- _table = 'BugSubscription'
407-
408- person = ForeignKey(
409- dbName='person', foreignKey='Person',
410- storm_validator=validate_person,
411- notNull=True
412- )
413- bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
414- bug_notification_level = EnumCol(
415+ __storm_table__ = 'BugSubscription'
416+
417+ id = Int(primary=True)
418+
419+ person_id = Int(
420+ "person", allow_none=False, validator=validate_person)
421+ person = Reference(person_id, "Person.id")
422+
423+ bug_id = Int("bug", allow_none=False)
424+ bug = Reference(bug_id, "Bug.id")
425+
426+ bug_notification_level = DBEnum(
427 enum=BugNotificationLevel,
428 default=BugNotificationLevel.COMMENTS,
429- notNull=True)
430- date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW)
431- subscribed_by = ForeignKey(
432- dbName='subscribed_by', foreignKey='Person',
433- storm_validator=validate_person,
434- notNull=True
435- )
436+ allow_none=False)
437+
438+ date_created = DateTime(
439+ allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
440+
441+ subscribed_by_id = Int(
442+ "subscribed_by", allow_none=False, validator=validate_person)
443+ subscribed_by = Reference(subscribed_by_id, "Person.id")
444+
445+ def __init__(self, bug=None, person=None, subscribed_by=None):
446+ super(BugSubscription, self).__init__()
447+ self.bug = bug
448+ self.person = person
449+ self.subscribed_by = subscribed_by
450
451 @property
452 def display_subscribed_by(self):
453@@ -54,9 +68,9 @@
454 def display_duplicate_subscribed_by(self):
455 """See `IBugSubscription`."""
456 if self.person == self.subscribed_by:
457- return u'Self-subscribed to bug %s' % (self.bugID)
458+ return u'Self-subscribed to bug %s' % (self.bug_id)
459 else:
460- return u'Subscribed to bug %s by %s' % (self.bugID,
461+ return u'Subscribed to bug %s by %s' % (self.bug_id,
462 self.subscribed_by.displayname)
463
464 def canBeUnsubscribedByUser(self, user):
465
466=== modified file 'lib/lp/bugs/model/bugtask.py'
467--- lib/lp/bugs/model/bugtask.py 2010-09-21 23:58:12 +0000
468+++ lib/lp/bugs/model/bugtask.py 2010-09-24 14:31:45 +0000
469@@ -2159,8 +2159,8 @@
470 tables.append(BugAffectsPerson)
471 if params.hardware_owner_is_subscribed_to_bug:
472 bug_link_clauses.append(
473- And(BugSubscription.personID == HWSubmission.ownerID,
474- BugSubscription.bugID == Bug.id))
475+ And(BugSubscription.person_id == HWSubmission.ownerID,
476+ BugSubscription.bug_id == Bug.id))
477 tables.append(BugSubscription)
478 if params.hardware_is_linked_to_bug:
479 bug_link_clauses.append(
480
481=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
482--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-08-27 13:15:43 +0000
483+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-09-24 14:31:45 +0000
484@@ -772,7 +772,7 @@
485 [u'Sample Person', u'Ubuntu Team']
486
487 >>> bug_six.subscribe(no_priv, no_priv)
488- <BugSubscription...>
489+ <lp.bugs.model.bugsubscription.BugSubscription ...>
490
491 >>> sorted([subscriber.displayname
492 ... for subscriber in bug_five.getIndirectSubscribers()])
493
494=== modified file 'lib/lp/bugs/tests/test_bug.py'
495--- lib/lp/bugs/tests/test_bug.py 2010-09-21 16:38:53 +0000
496+++ lib/lp/bugs/tests/test_bug.py 2010-09-24 14:31:45 +0000
497@@ -9,6 +9,7 @@
498 from lp.registry.interfaces.person import PersonVisibility
499 from lp.registry.model.structuralsubscription import StructuralSubscription
500 from lp.testing import (
501+ login_person,
502 person_logged_in,
503 TestCaseWithFactory,
504 )
505@@ -67,6 +68,22 @@
506 set([person, team1, team2]),
507 set(real_bug.getSubscribersForPerson(person)))
508
509+ def test_getSubscriptionsFromDuplicates(self):
510+ # getSubscriptionsFromDuplicates() will return only the earliest
511+ # subscription if a user is subscribed to a bug via more than one
512+ # duplicate.
513+ user = self.factory.makePerson()
514+ login_person(user)
515+ bug = self.factory.makeBug(owner=user)
516+ dupe1 = self.factory.makeBug(owner=user)
517+ dupe1.markAsDuplicate(bug)
518+ subscription = dupe1.subscribe(user, user)
519+ dupe2 = self.factory.makeBug(owner=user)
520+ dupe2.markAsDuplicate(bug)
521+ dupe2.subscribe(user, user)
522+ self.assertEqual(
523+ [subscription], list(bug.getSubscriptionsFromDuplicates()))
524+
525 def test_get_also_notified_subscribers_with_private_team(self):
526 product = self.factory.makeProduct()
527 bug = self.factory.makeBug(product=product)
528
529=== modified file 'lib/lp/hardwaredb/doc/hwdb-device-tables.txt'
530--- lib/lp/hardwaredb/doc/hwdb-device-tables.txt 2010-02-02 17:12:29 +0000
531+++ lib/lp/hardwaredb/doc/hwdb-device-tables.txt 2010-09-24 14:31:45 +0000
532@@ -2310,7 +2310,7 @@
533 for bug subscribers who own a given device.
534
535 >>> bug_one.subscribe(foo_bar, subscribed_by=foo_bar)
536- <BugSubscription at ...
537+ <lp.bugs.model.bugsubscription.BugSubscription ...>
538 >>> for person in submission_set.deviceDriverOwnersAffectedByBugs(
539 ... bus=HWBus.IDE, vendor_id='SEAGATE', product_id='ST3250820NS ',
540 ... bug_ids=[1], subscribed_to_bug=True):
541@@ -2420,7 +2420,7 @@
542 >>> LaunchpadZopelessLayer.switchDbUser('launchpad')
543 >>> no_priv = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
544 >>> bug_one.subscribe(no_priv, subscribed_by=no_priv)
545- <BugSubscription at ...
546+ <lp.bugs.model.bugsubscription.BugSubscription ...>
547 >>> for person in submission_set.deviceDriverOwnersAffectedByBugs(
548 ... bus=HWBus.IDE, vendor_id='SEAGATE', product_id='ST3250820NS ',
549 ... bug_ids=[1], subscribed_to_bug=True, user=no_priv):
550@@ -2474,7 +2474,7 @@
551
552 >>> bug_one.markUserAffected(foo_bar, False)
553 >>> bug_one.subscribe(foo_bar, subscribed_by=foo_bar)
554- <BugSubscription at ...
555+ <lp.bugs.model.bugsubscription.BugSubscription ...>
556 >>> for person in submission_set.deviceDriverOwnersAffectedByBugs(
557 ... driver_name='sd', bug_ids=[1], subscribed_to_bug=True):
558 ... print person.displayname
559@@ -2664,7 +2664,7 @@
560 We can also check for when the user is subscribed to the bug.
561
562 >>> bug_one.subscribe(foo_bar, subscribed_by=foo_bar)
563- <BugSubscription ...
564+ <lp.bugs.model.bugsubscription.BugSubscription ...>
565
566 >>> for entry in submission_set.hwInfoByBugRelatedUsers(
567 ... bug_ids=[1], subscribed_to_bug=True):
568@@ -2688,7 +2688,7 @@
569 The owner of a private submission can see his or her submission.
570
571 >>> bug_one.subscribe(no_priv, subscribed_by=no_priv)
572- <BugSubscription ...
573+ <lp.bugs.model.bugsubscription.BugSubscription ...>
574
575 >>> for entry in submission_set.hwInfoByBugRelatedUsers(
576 ... bug_ids=[1], subscribed_to_bug=True, user=no_priv):
577
578=== modified file 'lib/lp/hardwaredb/model/hwdb.py'
579--- lib/lp/hardwaredb/model/hwdb.py 2010-08-23 09:10:10 +0000
580+++ lib/lp/hardwaredb/model/hwdb.py 2010-09-24 14:31:45 +0000
581@@ -467,7 +467,7 @@
582
583 if subscribed_to_bug:
584 subscriber_clauses = [
585- BugSubscription.personID == HWSubmission.ownerID,
586+ BugSubscription.person_id == HWSubmission.ownerID,
587 BugSubscription.bug == Bug.id,
588 ]
589 subscriber_query = Select(
590@@ -531,7 +531,7 @@
591 person_clauses = [Bug.ownerID == HWSubmission.ownerID]
592 if subscribed_to_bug:
593 person_clauses.append(
594- And(BugSubscription.personID == HWSubmission.ownerID,
595+ And(BugSubscription.person_id == HWSubmission.ownerID,
596 BugSubscription.bug == Bug.id))
597 tables.append(BugSubscription)
598 if affected_by_bug: