Merge lp:~allenap/launchpad/subscribe-to-tag-bug-151129 into lp:launchpad
- subscribe-to-tag-bug-151129
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gavin Panella | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 11972 | ||||
Proposed branch: | lp:~allenap/launchpad/subscribe-to-tag-bug-151129 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
588 lines (+259/-126) 2 files modified
lib/lp/registry/model/structuralsubscription.py (+44/-0) lib/lp/registry/tests/test_structuralsubscriptiontarget.py (+215/-126) |
||||
To merge this branch: | bzr merge lp:~allenap/launchpad/subscribe-to-tag-bug-151129 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Henning Eggers (community) | code | Approve | |
Review via email: mp+40324@code.launchpad.net |
Commit message
[r=henninge]
Description of the change
This adds the ability to subscribe to tags as part of a structural
subscription. It was already possible to subscribe to the presence or
absense of tags, just not specific tags (presence or absense thereof).
The implementation is perhaps interesting. Because set operations need
to be performed, it's not enough to simply left join to the subscribed
tags table (BugSubscriptio
used PostgreSQL ARRAY types.
Robert Collins (lifeless) wrote : | # |
Why use ARRAY rather than an explicit modelling of tag?
We model tags explicitly in bugtag....
-Rob
Gavin Panella (allenap) wrote : | # |
Rob, I don't know! My thinking was already going down one path and I
didn't see the wood for the trees. I'll change it in a follow-on
branch (and I won't land this as it stands).
Gavin Panella (allenap) wrote : | # |
Henning, thank you for the review. I'm going to switch away from
ARRAYs (unless I hit some big problem), but I'll keep the tests of
course. I'm working on your suggestions :)
Gavin Panella (allenap) wrote : | # |
> > + tags_include_
> > + "ARRAY[]::TEXT[] = ARRAY(%s)" % tags_include_expr)
>
> I don't understand why you are not reusing tags_include_array here.
Neither do I. Fixed.
> > +
> > + # Choose the correct expression depending on the find_all_tags flag.
> > + def tags_find_
>
> "The Combinator!" I like that ... ;)
Hehe. I discovered that it's a term from lambda calculus, which I have
never studied. So I'm probably abusing the term and, worse, abusing it
with flagrant ignorance :)
> + SQL("ARRAY[
>
> ... and when you look at that, you'll quickly notice that it matches the
> definition of "tags_include_
Erm, yes, I should. Fixed. Thanks.
> > + # The subscription's excluded tags can be anything.
>
> "..., so no condition is needed for them." This comment at the end got me
> confused at first, so I think it would benefit from the added explanation.
Good suggestion.
> This code from here ...
> > + # If a subscription filter specifies that any of one or more specific
> > + # tags must be present, bugs with any of those tags are matched.
> > + bugtask = self.makeBugTask()
> > +
> > + # Create a new subscription on self.target.
> > + login_person(
> > + subscription = self.target.
> > + self.ordinary_
> > + subscription.
> > + subscription_filter = subscription.
> > +
> > + # Looking for either the "foo" or the "bar" tag.
> > + subscription_
> > + subscription_
>
> ... to here is repeated in the next two tests, with only the tags and
> find_all_tags changing. I think that should go into a
> "makeBugTaskAnd
> changing values.
I've changed the inheritance of various tests and added several new
TestStructuralS
this common stuff in setUp().
> > + # Without either tag the subscription is not found.
> > + subscriptions_
> > + bugtask, BugNotification
> > + self.assertEqua
> > +
>
> Having a factory method makes it also easy to split the test up into smaller
> chunks for the different conditions under which getSubscription
> being tested. I think you can fulfill the "one assert per test" paradigm here
> easily. ;-)
I've moved this into a new assertSubscript
everywhere this pattern appears. That has reduced the line count a
*lot*, more than I realised as I slowly grew this class. Good
suggestion, thanks.
However, I'm going to resist the "one assert per test" thing. I
personally think this is better because it reduces the setup and
teardown costs, makes it no more difficult to ...
Preview Diff
1 | === modified file 'lib/lp/registry/model/structuralsubscription.py' |
2 | --- lib/lp/registry/model/structuralsubscription.py 2010-11-09 11:00:55 +0000 |
3 | +++ lib/lp/registry/model/structuralsubscription.py 2010-11-24 09:39:54 +0000 |
4 | @@ -10,6 +10,7 @@ |
5 | And, |
6 | LeftJoin, |
7 | Or, |
8 | + SQL, |
9 | ) |
10 | from storm.store import Store |
11 | from zope.component import ( |
12 | @@ -487,13 +488,56 @@ |
13 | BugSubscriptionFilter.id)), |
14 | ] |
15 | |
16 | + # An ARRAY[] expression for the given bug's tags. |
17 | + tags_array = "ARRAY[%s]::TEXT[]" % ",".join( |
18 | + quote(tag) for tag in bugtask.bug.tags) |
19 | + |
20 | + # The tags a subscription requests for inclusion. |
21 | + tags_include_expr = ( |
22 | + "SELECT tag FROM BugSubscriptionFilterTag " |
23 | + "WHERE filter = BugSubscriptionFilter.id AND include") |
24 | + tags_include_array = "ARRAY(%s)" % tags_include_expr |
25 | + tags_include_is_empty = SQL( |
26 | + "ARRAY[]::TEXT[] = %s" % tags_include_array) |
27 | + |
28 | + # The tags a subscription requests for exclusion. |
29 | + tags_exclude_expr = ( |
30 | + "SELECT tag FROM BugSubscriptionFilterTag " |
31 | + "WHERE filter = BugSubscriptionFilter.id AND NOT include") |
32 | + tags_exclude_array = "ARRAY(%s)" % tags_exclude_expr |
33 | + tags_exclude_is_empty = SQL( |
34 | + "ARRAY[]::TEXT[] = %s" % tags_exclude_array) |
35 | + |
36 | + # Choose the correct expression depending on the find_all_tags flag. |
37 | + def tags_find_all_combinator(find_all_expr, find_any_expr): |
38 | + return SQL( |
39 | + "CASE WHEN BugSubscriptionFilter.find_all_tags " |
40 | + "THEN (%s) ELSE (%s) END" % (find_all_expr, find_any_expr)) |
41 | + |
42 | if len(bugtask.bug.tags) == 0: |
43 | tag_conditions = [ |
44 | BugSubscriptionFilter.include_any_tags == False, |
45 | + # The subscription's required tags must be an empty set. |
46 | + tags_include_is_empty, |
47 | + # The subscription's excluded tags can be anything so no |
48 | + # condition is needed. |
49 | ] |
50 | else: |
51 | tag_conditions = [ |
52 | BugSubscriptionFilter.exclude_any_tags == False, |
53 | + # The bug's tags must contain the subscription's required tags |
54 | + # if find_all_tags is set, else there must be an intersection. |
55 | + Or(tags_include_is_empty, |
56 | + tags_find_all_combinator( |
57 | + "%s @> %s" % (tags_array, tags_include_array), |
58 | + "%s && %s" % (tags_array, tags_include_array))), |
59 | + # The bug's tags must not contain the subscription's excluded |
60 | + # tags if find_all_tags is set, else there must not be an |
61 | + # intersection. |
62 | + Or(tags_exclude_is_empty, |
63 | + tags_find_all_combinator( |
64 | + "NOT (%s @> %s)" % (tags_array, tags_exclude_array), |
65 | + "NOT (%s && %s)" % (tags_array, tags_exclude_array))), |
66 | ] |
67 | |
68 | conditions = [ |
69 | |
70 | === modified file 'lib/lp/registry/tests/test_structuralsubscriptiontarget.py' |
71 | --- lib/lp/registry/tests/test_structuralsubscriptiontarget.py 2010-10-07 10:07:51 +0000 |
72 | +++ lib/lp/registry/tests/test_structuralsubscriptiontarget.py 2010-11-24 09:39:54 +0000 |
73 | @@ -32,7 +32,6 @@ |
74 | BugTaskImportance, |
75 | BugTaskStatus, |
76 | ) |
77 | -from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter |
78 | from lp.bugs.tests.test_bugtarget import bugtarget_filebug |
79 | from lp.registry.enum import BugNotificationLevel |
80 | from lp.registry.errors import ( |
81 | @@ -66,6 +65,10 @@ |
82 | self.team_owner = self.factory.makePerson() |
83 | self.team = self.factory.makeTeam(owner=self.team_owner) |
84 | |
85 | + |
86 | +class RestrictedStructuralSubscription(StructuralSubscriptionTestBase): |
87 | + # Tests suitable for a target that restricts structural subscriptions. |
88 | + |
89 | def test_target_implements_structural_subscription_target(self): |
90 | self.assertTrue(verifyObject(IStructuralSubscriptionTarget, |
91 | self.target)) |
92 | @@ -124,7 +127,9 @@ |
93 | self.ordinary_subscriber, self.ordinary_subscriber) |
94 | |
95 | |
96 | -class UnrestrictedStructuralSubscription(StructuralSubscriptionTestBase): |
97 | +class UnrestrictedStructuralSubscription(RestrictedStructuralSubscription): |
98 | + # Tests suitable for a target that does not restrict structural |
99 | + # subscriptions. |
100 | |
101 | def test_structural_subscription_by_ordinary_user(self): |
102 | # ordinary users can subscribe themselves |
103 | @@ -170,195 +175,243 @@ |
104 | class FilteredStructuralSubscriptionTestBase(StructuralSubscriptionTestBase): |
105 | """Tests for filtered structural subscriptions.""" |
106 | |
107 | + layer = LaunchpadFunctionalLayer |
108 | + |
109 | + def makeTarget(self): |
110 | + raise NotImplementedError(self.makeTarget) |
111 | + |
112 | def makeBugTask(self): |
113 | return self.factory.makeBugTask(target=self.target) |
114 | |
115 | + def setUp(self): |
116 | + super(FilteredStructuralSubscriptionTestBase, self).setUp() |
117 | + login_person(self.ordinary_subscriber) |
118 | + self.target = self.makeTarget() |
119 | + self.bugtask = self.makeBugTask() |
120 | + self.bug = self.bugtask.bug |
121 | + self.subscription = self.target.addSubscription( |
122 | + self.ordinary_subscriber, self.ordinary_subscriber) |
123 | + self.subscription.bug_notification_level = ( |
124 | + BugNotificationLevel.COMMENTS) |
125 | + |
126 | + def assertSubscriptions( |
127 | + self, expected_subscriptions, level=BugNotificationLevel.NOTHING): |
128 | + observed_subscriptions = list( |
129 | + self.target.getSubscriptionsForBugTask(self.bugtask, level)) |
130 | + self.assertEqual(expected_subscriptions, observed_subscriptions) |
131 | + |
132 | def test_getSubscriptionsForBugTask(self): |
133 | # If no one has a filtered subscription for the given bug, the result |
134 | # of getSubscriptionsForBugTask() is the same as for |
135 | # getSubscriptions(). |
136 | - bugtask = self.makeBugTask() |
137 | subscriptions = self.target.getSubscriptions( |
138 | min_bug_notification_level=BugNotificationLevel.NOTHING) |
139 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
140 | - bugtask, BugNotificationLevel.NOTHING) |
141 | - self.assertEqual(list(subscriptions), list(subscriptions_for_bugtask)) |
142 | + self.assertSubscriptions(list(subscriptions)) |
143 | |
144 | def test_getSubscriptionsForBugTask_with_filter_on_status(self): |
145 | # If a status filter exists for a subscription, the result of |
146 | # getSubscriptionsForBugTask() may be a subset of getSubscriptions(). |
147 | - bugtask = self.makeBugTask() |
148 | - |
149 | - # Create a new subscription on self.target. |
150 | - login_person(self.ordinary_subscriber) |
151 | - subscription = self.target.addSubscription( |
152 | - self.ordinary_subscriber, self.ordinary_subscriber) |
153 | - subscription.bug_notification_level = BugNotificationLevel.COMMENTS |
154 | |
155 | # Without any filters the subscription is found. |
156 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
157 | - bugtask, BugNotificationLevel.NOTHING) |
158 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
159 | + self.assertSubscriptions([self.subscription]) |
160 | |
161 | # Filter the subscription to bugs in the CONFIRMED state. |
162 | - subscription_filter = BugSubscriptionFilter() |
163 | - subscription_filter.structural_subscription = subscription |
164 | + subscription_filter = self.subscription.newBugFilter() |
165 | subscription_filter.statuses = [BugTaskStatus.CONFIRMED] |
166 | |
167 | # With the filter the subscription is not found. |
168 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
169 | - bugtask, BugNotificationLevel.NOTHING) |
170 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
171 | + self.assertSubscriptions([]) |
172 | |
173 | # If the filter is adjusted, the subscription is found again. |
174 | - subscription_filter.statuses = [bugtask.status] |
175 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
176 | - bugtask, BugNotificationLevel.NOTHING) |
177 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
178 | + subscription_filter.statuses = [self.bugtask.status] |
179 | + self.assertSubscriptions([self.subscription]) |
180 | |
181 | def test_getSubscriptionsForBugTask_with_filter_on_importance(self): |
182 | # If an importance filter exists for a subscription, the result of |
183 | # getSubscriptionsForBugTask() may be a subset of getSubscriptions(). |
184 | - bugtask = self.makeBugTask() |
185 | - |
186 | - # Create a new subscription on self.target. |
187 | - login_person(self.ordinary_subscriber) |
188 | - subscription = self.target.addSubscription( |
189 | - self.ordinary_subscriber, self.ordinary_subscriber) |
190 | - subscription.bug_notification_level = BugNotificationLevel.COMMENTS |
191 | |
192 | # Without any filters the subscription is found. |
193 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
194 | - bugtask, BugNotificationLevel.NOTHING) |
195 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
196 | + self.assertSubscriptions([self.subscription]) |
197 | |
198 | # Filter the subscription to bugs in the CRITICAL state. |
199 | - subscription_filter = BugSubscriptionFilter() |
200 | - subscription_filter.structural_subscription = subscription |
201 | + subscription_filter = self.subscription.newBugFilter() |
202 | subscription_filter.importances = [BugTaskImportance.CRITICAL] |
203 | |
204 | # With the filter the subscription is not found. |
205 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
206 | - bugtask, BugNotificationLevel.NOTHING) |
207 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
208 | + self.assertSubscriptions([]) |
209 | |
210 | # If the filter is adjusted, the subscription is found again. |
211 | - subscription_filter.importances = [bugtask.importance] |
212 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
213 | - bugtask, BugNotificationLevel.NOTHING) |
214 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
215 | + subscription_filter.importances = [self.bugtask.importance] |
216 | + self.assertSubscriptions([self.subscription]) |
217 | |
218 | def test_getSubscriptionsForBugTask_with_filter_on_level(self): |
219 | # All structural subscriptions have a level for bug notifications |
220 | # which getSubscriptionsForBugTask() observes. |
221 | - bugtask = self.makeBugTask() |
222 | |
223 | - # Create a new METADATA level subscription on self.target. |
224 | - login_person(self.ordinary_subscriber) |
225 | - subscription = self.target.addSubscription( |
226 | - self.ordinary_subscriber, self.ordinary_subscriber) |
227 | - subscription.bug_notification_level = BugNotificationLevel.METADATA |
228 | + # Adjust the subscription level to METADATA. |
229 | + self.subscription.bug_notification_level = ( |
230 | + BugNotificationLevel.METADATA) |
231 | |
232 | # The subscription is found when looking for NOTHING or above. |
233 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
234 | - bugtask, BugNotificationLevel.NOTHING) |
235 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
236 | + self.assertSubscriptions( |
237 | + [self.subscription], BugNotificationLevel.NOTHING) |
238 | # The subscription is found when looking for METADATA or above. |
239 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
240 | - bugtask, BugNotificationLevel.METADATA) |
241 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
242 | + self.assertSubscriptions( |
243 | + [self.subscription], BugNotificationLevel.METADATA) |
244 | # The subscription is not found when looking for COMMENTS or above. |
245 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
246 | - bugtask, BugNotificationLevel.COMMENTS) |
247 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
248 | + self.assertSubscriptions( |
249 | + [], BugNotificationLevel.COMMENTS) |
250 | |
251 | def test_getSubscriptionsForBugTask_with_filter_include_any_tags(self): |
252 | # If a subscription filter has include_any_tags, a bug with one or |
253 | # more tags is matched. |
254 | - bugtask = self.makeBugTask() |
255 | |
256 | - # Create a new subscription on self.target. |
257 | - login_person(self.ordinary_subscriber) |
258 | - subscription = self.target.addSubscription( |
259 | - self.ordinary_subscriber, self.ordinary_subscriber) |
260 | - subscription.bug_notification_level = BugNotificationLevel.COMMENTS |
261 | - subscription_filter = subscription.newBugFilter() |
262 | + subscription_filter = self.subscription.newBugFilter() |
263 | subscription_filter.include_any_tags = True |
264 | |
265 | # Without any tags the subscription is not found. |
266 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
267 | - bugtask, BugNotificationLevel.NOTHING) |
268 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
269 | + self.assertSubscriptions([]) |
270 | |
271 | # With any tag the subscription is found. |
272 | - bugtask.bug.tags = ["foo"] |
273 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
274 | - bugtask, BugNotificationLevel.NOTHING) |
275 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
276 | + self.bug.tags = ["foo"] |
277 | + self.assertSubscriptions([self.subscription]) |
278 | |
279 | def test_getSubscriptionsForBugTask_with_filter_exclude_any_tags(self): |
280 | # If a subscription filter has exclude_any_tags, only bugs with no |
281 | # tags are matched. |
282 | - bugtask = self.makeBugTask() |
283 | |
284 | - # Create a new subscription on self.target. |
285 | - login_person(self.ordinary_subscriber) |
286 | - subscription = self.target.addSubscription( |
287 | - self.ordinary_subscriber, self.ordinary_subscriber) |
288 | - subscription.bug_notification_level = BugNotificationLevel.COMMENTS |
289 | - subscription_filter = subscription.newBugFilter() |
290 | + subscription_filter = self.subscription.newBugFilter() |
291 | subscription_filter.exclude_any_tags = True |
292 | |
293 | # Without any tags the subscription is found. |
294 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
295 | - bugtask, BugNotificationLevel.NOTHING) |
296 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
297 | + self.assertSubscriptions([self.subscription]) |
298 | |
299 | # With any tag the subscription is not found. |
300 | - bugtask.bug.tags = ["foo"] |
301 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
302 | - bugtask, BugNotificationLevel.NOTHING) |
303 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
304 | + self.bug.tags = ["foo"] |
305 | + self.assertSubscriptions([]) |
306 | + |
307 | + def test_getSubscriptionsForBugTask_with_filter_for_any_tag(self): |
308 | + # If a subscription filter specifies that any of one or more specific |
309 | + # tags must be present, bugs with any of those tags are matched. |
310 | + |
311 | + # Looking for either the "foo" or the "bar" tag. |
312 | + subscription_filter = self.subscription.newBugFilter() |
313 | + subscription_filter.tags = [u"foo", u"bar"] |
314 | + subscription_filter.find_all_tags = False |
315 | + |
316 | + # Without either tag the subscription is not found. |
317 | + self.assertSubscriptions([]) |
318 | + |
319 | + # With either tag the subscription is found. |
320 | + self.bug.tags = ["bar", "baz"] |
321 | + self.assertSubscriptions([self.subscription]) |
322 | + |
323 | + def test_getSubscriptionsForBugTask_with_filter_for_all_tags(self): |
324 | + # If a subscription filter specifies that all of one or more specific |
325 | + # tags must be present, bugs with all of those tags are matched. |
326 | + |
327 | + # Looking for both the "foo" and the "bar" tag. |
328 | + subscription_filter = self.subscription.newBugFilter() |
329 | + subscription_filter.tags = [u"foo", u"bar"] |
330 | + subscription_filter.find_all_tags = True |
331 | + |
332 | + # Without either tag the subscription is not found. |
333 | + self.assertSubscriptions([]) |
334 | + |
335 | + # Without only one of the required tags the subscription is not found. |
336 | + self.bug.tags = ["foo"] |
337 | + self.assertSubscriptions([]) |
338 | + |
339 | + # With both required tags the subscription is found. |
340 | + self.bug.tags = ["foo", "bar"] |
341 | + self.assertSubscriptions([self.subscription]) |
342 | + |
343 | + def test_getSubscriptionsForBugTask_with_filter_for_not_any_tag(self): |
344 | + # If a subscription filter specifies that any of one or more specific |
345 | + # tags must not be present, bugs without any of those tags are |
346 | + # matched. |
347 | + |
348 | + # Looking to exclude the "foo" or "bar" tags. |
349 | + subscription_filter = self.subscription.newBugFilter() |
350 | + subscription_filter.tags = [u"-foo", u"-bar"] |
351 | + subscription_filter.find_all_tags = False |
352 | + |
353 | + # Without either tag the subscription is found. |
354 | + self.assertSubscriptions([self.subscription]) |
355 | + |
356 | + # With either tag the subscription is no longer found. |
357 | + self.bug.tags = ["foo"] |
358 | + self.assertSubscriptions([]) |
359 | + |
360 | + def test_getSubscriptionsForBugTask_with_filter_for_not_all_tags(self): |
361 | + # If a subscription filter specifies that all of one or more specific |
362 | + # tags must not be present, bugs without all of those tags are |
363 | + # matched. |
364 | + |
365 | + # Looking to exclude the "foo" and "bar" tags. |
366 | + subscription_filter = self.subscription.newBugFilter() |
367 | + subscription_filter.tags = [u"-foo", u"-bar"] |
368 | + subscription_filter.find_all_tags = True |
369 | + |
370 | + # Without either tag the subscription is found. |
371 | + self.assertSubscriptions([self.subscription]) |
372 | + |
373 | + # With only one of the excluded tags the subscription is found. |
374 | + self.bug.tags = ["foo"] |
375 | + self.assertSubscriptions([self.subscription]) |
376 | + |
377 | + # With both tags the subscription is no longer found. |
378 | + self.bug.tags = ["foo", "bar"] |
379 | + self.assertSubscriptions([]) |
380 | |
381 | def test_getSubscriptionsForBugTask_with_multiple_filters(self): |
382 | # If multiple filters exist for a subscription, all filters must |
383 | # match. |
384 | - bugtask = self.makeBugTask() |
385 | |
386 | - # Create a new subscription on self.target. |
387 | - login_person(self.ordinary_subscriber) |
388 | - subscription = self.target.addSubscription( |
389 | - self.ordinary_subscriber, self.ordinary_subscriber) |
390 | - subscription.bug_notification_level = BugNotificationLevel.COMMENTS |
391 | + # Add the "foo" tag to the bug. |
392 | + self.bug.tags = ["foo"] |
393 | |
394 | # Filter the subscription to bugs in the CRITICAL state. |
395 | - subscription_filter = BugSubscriptionFilter() |
396 | - subscription_filter.structural_subscription = subscription |
397 | + subscription_filter = self.subscription.newBugFilter() |
398 | subscription_filter.statuses = [BugTaskStatus.CONFIRMED] |
399 | subscription_filter.importances = [BugTaskImportance.CRITICAL] |
400 | |
401 | # With the filter the subscription is not found. |
402 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
403 | - bugtask, BugNotificationLevel.NOTHING) |
404 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
405 | + self.assertSubscriptions([]) |
406 | |
407 | # If the filter is adjusted to match status but not importance, the |
408 | # subscription is still not found. |
409 | - subscription_filter.statuses = [bugtask.status] |
410 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
411 | - bugtask, BugNotificationLevel.NOTHING) |
412 | - self.assertEqual([], list(subscriptions_for_bugtask)) |
413 | + subscription_filter.statuses = [self.bugtask.status] |
414 | + self.assertSubscriptions([]) |
415 | |
416 | # If the filter is adjusted to also match importance, the subscription |
417 | # is found again. |
418 | - subscription_filter.importances = [bugtask.importance] |
419 | - subscriptions_for_bugtask = self.target.getSubscriptionsForBugTask( |
420 | - bugtask, BugNotificationLevel.NOTHING) |
421 | - self.assertEqual([subscription], list(subscriptions_for_bugtask)) |
422 | + subscription_filter.importances = [self.bugtask.importance] |
423 | + self.assertSubscriptions([self.subscription]) |
424 | + |
425 | + # If the filter is given some tag criteria, the subscription is not |
426 | + # found. |
427 | + subscription_filter.tags = [u"-foo", u"bar", u"baz"] |
428 | + subscription_filter.find_all_tags = False |
429 | + self.assertSubscriptions([]) |
430 | + |
431 | + # After removing the "foo" tag and adding the "bar" tag, the |
432 | + # subscription is found. |
433 | + self.bug.tags = ["bar"] |
434 | + self.assertSubscriptions([self.subscription]) |
435 | + |
436 | + # Requiring that all tag criteria are fulfilled causes the |
437 | + # subscription to no longer be found. |
438 | + subscription_filter.find_all_tags = True |
439 | + self.assertSubscriptions([]) |
440 | + |
441 | + # After adding the "baz" tag, the subscription is found again. |
442 | + self.bug.tags = ["bar", "baz"] |
443 | + self.assertSubscriptions([self.subscription]) |
444 | |
445 | |
446 | class TestStructuralSubscriptionForDistro( |
447 | - FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
448 | + RestrictedStructuralSubscription, TestCaseWithFactory): |
449 | |
450 | layer = LaunchpadFunctionalLayer |
451 | |
452 | @@ -421,10 +474,15 @@ |
453 | StructuralSubscription) |
454 | |
455 | |
456 | +class TestStructuralSubscriptionFiltersForDistro( |
457 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
458 | + |
459 | + def makeTarget(self): |
460 | + return self.factory.makeDistribution() |
461 | + |
462 | + |
463 | class TestStructuralSubscriptionForProduct( |
464 | - UnrestrictedStructuralSubscription, |
465 | - FilteredStructuralSubscriptionTestBase, |
466 | - TestCaseWithFactory): |
467 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
468 | |
469 | layer = LaunchpadFunctionalLayer |
470 | |
471 | @@ -433,10 +491,15 @@ |
472 | self.target = self.factory.makeProduct() |
473 | |
474 | |
475 | +class TestStructuralSubscriptionFiltersForProduct( |
476 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
477 | + |
478 | + def makeTarget(self): |
479 | + return self.factory.makeProduct() |
480 | + |
481 | + |
482 | class TestStructuralSubscriptionForDistroSourcePackage( |
483 | - UnrestrictedStructuralSubscription, |
484 | - FilteredStructuralSubscriptionTestBase, |
485 | - TestCaseWithFactory): |
486 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
487 | |
488 | layer = LaunchpadFunctionalLayer |
489 | |
490 | @@ -446,10 +509,15 @@ |
491 | self.target = ProxyFactory(self.target) |
492 | |
493 | |
494 | +class TestStructuralSubscriptionFiltersForDistroSourcePackage( |
495 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
496 | + |
497 | + def makeTarget(self): |
498 | + return self.factory.makeDistributionSourcePackage() |
499 | + |
500 | + |
501 | class TestStructuralSubscriptionForMilestone( |
502 | - UnrestrictedStructuralSubscription, |
503 | - FilteredStructuralSubscriptionTestBase, |
504 | - TestCaseWithFactory): |
505 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
506 | |
507 | layer = LaunchpadFunctionalLayer |
508 | |
509 | @@ -458,15 +526,19 @@ |
510 | self.target = self.factory.makeMilestone() |
511 | self.target = ProxyFactory(self.target) |
512 | |
513 | + |
514 | +class TestStructuralSubscriptionFiltersForMilestone( |
515 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
516 | + |
517 | + def makeTarget(self): |
518 | + return self.factory.makeMilestone() |
519 | + |
520 | def makeBugTask(self): |
521 | - # XXX Should test with target *and* series_target. |
522 | return self.factory.makeBugTask(target=self.target.series_target) |
523 | |
524 | |
525 | class TestStructuralSubscriptionForDistroSeries( |
526 | - UnrestrictedStructuralSubscription, |
527 | - FilteredStructuralSubscriptionTestBase, |
528 | - TestCaseWithFactory): |
529 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
530 | |
531 | layer = LaunchpadFunctionalLayer |
532 | |
533 | @@ -476,10 +548,15 @@ |
534 | self.target = ProxyFactory(self.target) |
535 | |
536 | |
537 | +class TestStructuralSubscriptionFiltersForDistroSeries( |
538 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
539 | + |
540 | + def makeTarget(self): |
541 | + return self.factory.makeDistroSeries() |
542 | + |
543 | + |
544 | class TestStructuralSubscriptionForProjectGroup( |
545 | - UnrestrictedStructuralSubscription, |
546 | - FilteredStructuralSubscriptionTestBase, |
547 | - TestCaseWithFactory): |
548 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
549 | |
550 | layer = LaunchpadFunctionalLayer |
551 | |
552 | @@ -488,15 +565,20 @@ |
553 | self.target = self.factory.makeProject() |
554 | self.target = ProxyFactory(self.target) |
555 | |
556 | + |
557 | +class TestStructuralSubscriptionFiltersForProjectGroup( |
558 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
559 | + |
560 | + def makeTarget(self): |
561 | + return self.factory.makeProject() |
562 | + |
563 | def makeBugTask(self): |
564 | return self.factory.makeBugTask( |
565 | target=self.factory.makeProduct(project=self.target)) |
566 | |
567 | |
568 | class TestStructuralSubscriptionForProductSeries( |
569 | - UnrestrictedStructuralSubscription, |
570 | - FilteredStructuralSubscriptionTestBase, |
571 | - TestCaseWithFactory): |
572 | + UnrestrictedStructuralSubscription, TestCaseWithFactory): |
573 | |
574 | layer = LaunchpadFunctionalLayer |
575 | |
576 | @@ -506,6 +588,13 @@ |
577 | self.target = ProxyFactory(self.target) |
578 | |
579 | |
580 | +class TestStructuralSubscriptionFiltersForProductSeries( |
581 | + FilteredStructuralSubscriptionTestBase, TestCaseWithFactory): |
582 | + |
583 | + def makeTarget(self): |
584 | + return self.factory.makeProductSeries() |
585 | + |
586 | + |
587 | class TestStructuralSubscriptionTargetHelper(TestCaseWithFactory): |
588 | """Tests for implementations of `IStructuralSubscriptionTargetHelper`.""" |
589 |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Hi Gavin,
thank you for extending the Launchpad feature set in such a useful way.
review approve code
There is nothing wrong with the branch but I'd like you to consider some
suggestions of mine, especially with regard to splitting up the tests. I don't
mean to push my coding style on you so I hope you can see what I am trying to
achieve.
Thanks for letting me learn about ARRAY! ;-)
Cheers,
Henning
Am 08.11.2010 14:14, schrieb Gavin Panella: registry/ model/structura lsubscription. py' registry/ model/structura lsubscription. py 2010-10-25 13:24:39 +0000 registry/ model/structura lsubscription. py 2010-11-08 13:14:20 +0000 Filter. id)), FilterTag " Filter. id AND include") is_empty = SQL(
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -10,6 +10,7 @@
> And,
> LeftJoin,
> Or,
> + SQL,
> )
> from storm.store import Store
> from zope.component import (
> @@ -484,13 +485,55 @@
> BugSubscription
> ]
>
> + # An ARRAY[] expression for the given bug's tags.
> + tags_array = "ARRAY[%s]::TEXT[]" % ",".join(
> + quote(tag) for tag in bugtask.bug.tags)
> +
> + # The tags a subscription requests for inclusion.
> + tags_include_expr = (
> + "SELECT tag FROM BugSubscription
> + "WHERE filter = BugSubscription
> + tags_include_array = "ARRAY(%s)" % tags_include_expr
> + tags_include_
> + "ARRAY[]::TEXT[] = ARRAY(%s)" % tags_include_expr)
I don't understand why you are not reusing tags_include_array here.
+ "ARRAY[]::TEXT[] = %s" % tags_include_array)
I think that reads a lot clearer, or am I missing something?
> + FilterTag " Filter. id AND NOT include") is_empty = SQL(
> + # The tags a subscription requests for exclusion.
> + tags_exclude_expr = (
> + "SELECT tag FROM BugSubscription
> + "WHERE filter = BugSubscription
> + tags_exclude_array = "ARRAY(%s)" % tags_exclude_expr
> + tags_exclude_
> + "ARRAY[]::TEXT[] = ARRAY(%s)" % tags_exclude_expr)
Same here.
> + all_combinator( find_all_ expr, find_any_expr):
> + # Choose the correct expression depending on the find_all_tags flag.
> + def tags_find_
"The Combinator!" I like that ... ;)
> + return SQL( Filter. find_all_ tags " bug.tags) == 0: Filter. include_ any_tags == False, array)) ,
> + "CASE WHEN BugSubscription
> + "THEN (%s) ELSE (%s) END" % (find_all_expr, find_any_expr))
> +
> if len(bugtask.
> tag_conditions = [
> BugSubscription
> + # The subscription's required tags must be an empty set.
> + SQL("%s = %s" % (tags_array, tags_include_
I understand that because len(tags) == 0, tags_array is an empty array but had
to wrap my head around it first. Why not use an explicit empty array?
+ SQL("ARRAY[ ]::TEXT[ ] = %s" % tags_include_ array),
... and when you look at that, you'll quickly notice that it matches the is_empty" , so you could just use that here.
definition of "tags_include_
> + # The ...