Merge lp:~allenap/launchpad/structural-subscriptions-with-filters-3 into lp:launchpad/db-devel

Proposed by Gavin Panella on 2010-10-01
Status: Merged
Approved by: Gavin Panella on 2010-10-01
Approved revision: no longer in the source branch.
Merged at revision: 9866
Proposed branch: lp:~allenap/launchpad/structural-subscriptions-with-filters-3
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~allenap/launchpad/structural-subscriptions-with-filters-2
Diff against target: 350 lines (+296/-0)
4 files modified
lib/lp/bugs/model/bugsubscriptionfilter.py (+144/-0)
lib/lp/bugs/model/bugsubscriptionfiltertag.py (+8/-0)
lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py (+132/-0)
lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py (+12/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/structural-subscriptions-with-filters-3
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2010-10-01 Approve on 2010-10-01
Review via email: mp+37270@code.launchpad.net

Description of the Change

This adds three helper properties to BugSubscriptionFilter - statuses, importances, tags - that will make its use much simpler. They abstract away a lot of the database jiggery pokery that needs to go on when creating or manipulating subscription filters.

To post a comment you must log in.
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
2--- lib/lp/bugs/model/bugsubscriptionfilter.py 2010-09-17 11:14:25 +0000
3+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2010-10-05 09:28:22 +0000
4@@ -6,6 +6,8 @@
5 __metaclass__ = type
6 __all__ = ['BugSubscriptionFilter']
7
8+from itertools import chain
9+
10 from storm.base import Storm
11 from storm.locals import (
12 Bool,
13@@ -14,6 +16,16 @@
14 Unicode,
15 )
16
17+from canonical.launchpad import searchbuilder
18+from canonical.launchpad.interfaces.lpstorm import IStore
19+from lp.bugs.model.bugsubscriptionfilterimportance import (
20+ BugSubscriptionFilterImportance,
21+ )
22+from lp.bugs.model.bugsubscriptionfilterstatus import (
23+ BugSubscriptionFilterStatus,
24+ )
25+from lp.bugs.model.bugsubscriptionfiltertag import BugSubscriptionFilterTag
26+
27
28 class BugSubscriptionFilter(Storm):
29 """A filter to specialize a *structural* subscription."""
30@@ -34,3 +46,135 @@
31 other_parameters = Unicode()
32
33 description = Unicode()
34+
35+ def _get_statuses(self):
36+ """Return a frozenset of statuses to filter on."""
37+ return frozenset(
38+ IStore(BugSubscriptionFilterStatus).find(
39+ BugSubscriptionFilterStatus,
40+ BugSubscriptionFilterStatus.filter == self).values(
41+ BugSubscriptionFilterStatus.status))
42+
43+ def _set_statuses(self, statuses):
44+ """Update the statuses to filter on.
45+
46+ The statuses must be from the `BugTaskStatus` enum, but can be
47+ bundled in any iterable.
48+ """
49+ statuses = frozenset(statuses)
50+ current_statuses = self.statuses
51+ store = IStore(BugSubscriptionFilterStatus)
52+ # Add additional statuses.
53+ for status in statuses.difference(current_statuses):
54+ status_filter = BugSubscriptionFilterStatus()
55+ status_filter.filter = self
56+ status_filter.status = status
57+ store.add(status_filter)
58+ # Delete unused ones.
59+ store.find(
60+ BugSubscriptionFilterStatus,
61+ BugSubscriptionFilterStatus.filter == self,
62+ BugSubscriptionFilterStatus.status.is_in(
63+ current_statuses.difference(statuses))).remove()
64+
65+ statuses = property(
66+ _get_statuses, _set_statuses, doc=(
67+ "A frozenset of statuses filtered on."))
68+
69+ def _get_importances(self):
70+ """Return a frozenset of importances to filter on."""
71+ return frozenset(
72+ IStore(BugSubscriptionFilterImportance).find(
73+ BugSubscriptionFilterImportance,
74+ BugSubscriptionFilterImportance.filter == self).values(
75+ BugSubscriptionFilterImportance.importance))
76+
77+ def _set_importances(self, importances):
78+ """Update the importances to filter on.
79+
80+ The importances must be from the `BugTaskImportance` enum, but can be
81+ bundled in any iterable.
82+ """
83+ importances = frozenset(importances)
84+ current_importances = self.importances
85+ store = IStore(BugSubscriptionFilterImportance)
86+ # Add additional importances.
87+ for importance in importances.difference(current_importances):
88+ importance_filter = BugSubscriptionFilterImportance()
89+ importance_filter.filter = self
90+ importance_filter.importance = importance
91+ store.add(importance_filter)
92+ # Delete unused ones.
93+ store.find(
94+ BugSubscriptionFilterImportance,
95+ BugSubscriptionFilterImportance.filter == self,
96+ BugSubscriptionFilterImportance.importance.is_in(
97+ current_importances.difference(importances))).remove()
98+
99+ importances = property(
100+ _get_importances, _set_importances, doc=(
101+ "A frozenset of importances filtered on."))
102+
103+ def _get_tags(self):
104+ """Return a frozenset of tags to filter on."""
105+ wildcards = []
106+ if self.include_any_tags:
107+ wildcards.append(u"*")
108+ if self.exclude_any_tags:
109+ wildcards.append(u"-*")
110+ tags = (
111+ tag_filter.qualified_tag
112+ for tag_filter in IStore(BugSubscriptionFilterTag).find(
113+ BugSubscriptionFilterTag,
114+ BugSubscriptionFilterTag.filter == self))
115+ return frozenset(chain(wildcards, tags))
116+
117+ def _set_tags(self, tags):
118+ """Update the tags to filter on.
119+
120+ The tags can be qualified with a leading hyphen, and can be bundled in
121+ any iterable.
122+
123+ If they are passed within a `searchbuilder.any` or `searchbuilder.all`
124+ object, the `find_all_tags` attribute will be updated to match.
125+
126+ Wildcard tags - `*` and `-*` - can be given too, and will update
127+ `include_any_tags` and `exclude_any_tags`.
128+ """
129+ # Deal with searchbuilder terms.
130+ if isinstance(tags, searchbuilder.all):
131+ self.find_all_tags = True
132+ tags = frozenset(tags.query_values)
133+ elif isinstance(tags, searchbuilder.any):
134+ self.find_all_tags = False
135+ tags = frozenset(tags.query_values)
136+ else:
137+ # Leave find_all_tags unchanged.
138+ tags = frozenset(tags)
139+ wildcards = frozenset((u"*", u"-*")).intersection(tags)
140+ # Set wildcards.
141+ self.include_any_tags = "*" in wildcards
142+ self.exclude_any_tags = "-*" in wildcards
143+ # Deal with other tags.
144+ tags = tags - wildcards
145+ store = IStore(BugSubscriptionFilterTag)
146+ current_tag_filters = dict(
147+ (tag_filter.qualified_tag, tag_filter)
148+ for tag_filter in store.find(
149+ BugSubscriptionFilterTag,
150+ BugSubscriptionFilterTag.filter == self))
151+ # Remove unused tags.
152+ for tag in set(current_tag_filters).difference(tags):
153+ tag_filter = current_tag_filters.pop(tag)
154+ store.remove(tag_filter)
155+ # Add additional tags.
156+ for tag in tags.difference(current_tag_filters):
157+ tag_filter = BugSubscriptionFilterTag()
158+ tag_filter.filter = self
159+ tag_filter.include = not tag.startswith("-")
160+ tag_filter.tag = tag.lstrip("-")
161+ store.add(tag_filter)
162+
163+ tags = property(
164+ _get_tags, _set_tags, doc=(
165+ "A frozenset of tags filtered on."))
166
167=== modified file 'lib/lp/bugs/model/bugsubscriptionfiltertag.py'
168--- lib/lp/bugs/model/bugsubscriptionfiltertag.py 2010-09-17 11:14:25 +0000
169+++ lib/lp/bugs/model/bugsubscriptionfiltertag.py 2010-10-05 09:28:22 +0000
170@@ -27,3 +27,11 @@
171
172 include = Bool(allow_none=False)
173 tag = Unicode(allow_none=False)
174+
175+ @property
176+ def qualified_tag(self):
177+ """The tag qualified with a hyphen if it is to be omitted."""
178+ if self.include:
179+ return self.tag
180+ else:
181+ return u"-" + self.tag
182
183=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py'
184--- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2010-09-17 12:32:14 +0000
185+++ lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2010-10-05 09:28:22 +0000
186@@ -5,8 +5,13 @@
187
188 __metaclass__ = type
189
190+from canonical.launchpad import searchbuilder
191 from canonical.launchpad.interfaces.lpstorm import IStore
192 from canonical.testing import DatabaseFunctionalLayer
193+from lp.bugs.interfaces.bugtask import (
194+ BugTaskImportance,
195+ BugTaskStatus,
196+ )
197 from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
198 from lp.testing import (
199 login_person,
200@@ -64,3 +69,130 @@
201 self.assertIs(False, bug_subscription_filter.exclude_any_tags)
202 self.assertIs(None, bug_subscription_filter.other_parameters)
203 self.assertIs(None, bug_subscription_filter.description)
204+
205+ def test_statuses(self):
206+ # The statuses property is a frozenset of the statuses that are
207+ # filtered upon.
208+ bug_subscription_filter = BugSubscriptionFilter()
209+ self.assertEqual(frozenset(), bug_subscription_filter.statuses)
210+
211+ def test_statuses_set(self):
212+ # Assigning any iterable to statuses updates the database.
213+ bug_subscription_filter = BugSubscriptionFilter()
214+ bug_subscription_filter.statuses = [
215+ BugTaskStatus.NEW, BugTaskStatus.INCOMPLETE]
216+ self.assertEqual(
217+ frozenset((BugTaskStatus.NEW, BugTaskStatus.INCOMPLETE)),
218+ bug_subscription_filter.statuses)
219+ # Assigning a subset causes the other status filters to be removed.
220+ bug_subscription_filter.statuses = [BugTaskStatus.NEW]
221+ self.assertEqual(
222+ frozenset((BugTaskStatus.NEW,)),
223+ bug_subscription_filter.statuses)
224+
225+ def test_statuses_set_empty(self):
226+ # Assigning an empty iterable to statuses updates the database.
227+ bug_subscription_filter = BugSubscriptionFilter()
228+ bug_subscription_filter.statuses = []
229+ self.assertEqual(frozenset(), bug_subscription_filter.statuses)
230+
231+ def test_importances(self):
232+ # The importances property is a frozenset of the importances that are
233+ # filtered upon.
234+ bug_subscription_filter = BugSubscriptionFilter()
235+ self.assertEqual(frozenset(), bug_subscription_filter.importances)
236+
237+ def test_importances_set(self):
238+ # Assigning any iterable to importances updates the database.
239+ bug_subscription_filter = BugSubscriptionFilter()
240+ bug_subscription_filter.importances = [
241+ BugTaskImportance.HIGH, BugTaskImportance.LOW]
242+ self.assertEqual(
243+ frozenset((BugTaskImportance.HIGH, BugTaskImportance.LOW)),
244+ bug_subscription_filter.importances)
245+ # Assigning a subset causes the other importance filters to be
246+ # removed.
247+ bug_subscription_filter.importances = [BugTaskImportance.HIGH]
248+ self.assertEqual(
249+ frozenset((BugTaskImportance.HIGH,)),
250+ bug_subscription_filter.importances)
251+
252+ def test_importances_set_empty(self):
253+ # Assigning an empty iterable to importances updates the database.
254+ bug_subscription_filter = BugSubscriptionFilter()
255+ bug_subscription_filter.importances = []
256+ self.assertEqual(frozenset(), bug_subscription_filter.importances)
257+
258+ def test_tags(self):
259+ # The tags property is a frozenset of the tags that are filtered upon.
260+ bug_subscription_filter = BugSubscriptionFilter()
261+ self.assertEqual(frozenset(), bug_subscription_filter.tags)
262+
263+ def test_tags_set(self):
264+ # Assigning any iterable to tags updates the database.
265+ bug_subscription_filter = BugSubscriptionFilter()
266+ bug_subscription_filter.tags = [u"foo", u"-bar"]
267+ self.assertEqual(
268+ frozenset((u"foo", u"-bar")),
269+ bug_subscription_filter.tags)
270+ # Assigning a subset causes the other tag filters to be removed.
271+ bug_subscription_filter.tags = [u"foo"]
272+ self.assertEqual(
273+ frozenset((u"foo",)),
274+ bug_subscription_filter.tags)
275+
276+ def test_tags_set_empty(self):
277+ # Assigning an empty iterable to tags updates the database.
278+ bug_subscription_filter = BugSubscriptionFilter()
279+ bug_subscription_filter.tags = []
280+ self.assertEqual(frozenset(), bug_subscription_filter.tags)
281+
282+ def test_tags_set_wildcard(self):
283+ # Setting one or more wildcard tags may update include_any_tags or
284+ # exclude_any_tags.
285+ bug_subscription_filter = BugSubscriptionFilter()
286+ self.assertEqual(frozenset(), bug_subscription_filter.tags)
287+ self.assertFalse(bug_subscription_filter.include_any_tags)
288+ self.assertFalse(bug_subscription_filter.exclude_any_tags)
289+
290+ bug_subscription_filter.tags = [u"*"]
291+ self.assertEqual(frozenset((u"*",)), bug_subscription_filter.tags)
292+ self.assertTrue(bug_subscription_filter.include_any_tags)
293+ self.assertFalse(bug_subscription_filter.exclude_any_tags)
294+
295+ bug_subscription_filter.tags = [u"-*"]
296+ self.assertEqual(frozenset((u"-*",)), bug_subscription_filter.tags)
297+ self.assertFalse(bug_subscription_filter.include_any_tags)
298+ self.assertTrue(bug_subscription_filter.exclude_any_tags)
299+
300+ bug_subscription_filter.tags = [u"*", u"-*"]
301+ self.assertEqual(
302+ frozenset((u"*", u"-*")), bug_subscription_filter.tags)
303+ self.assertTrue(bug_subscription_filter.include_any_tags)
304+ self.assertTrue(bug_subscription_filter.exclude_any_tags)
305+
306+ bug_subscription_filter.tags = []
307+ self.assertEqual(frozenset(), bug_subscription_filter.tags)
308+ self.assertFalse(bug_subscription_filter.include_any_tags)
309+ self.assertFalse(bug_subscription_filter.exclude_any_tags)
310+
311+ def test_tags_with_any_and_all(self):
312+ # If the tags are bundled in a c.l.searchbuilder.any or .all, the
313+ # find_any_tags attribute will also be updated.
314+ bug_subscription_filter = BugSubscriptionFilter()
315+ self.assertEqual(frozenset(), bug_subscription_filter.tags)
316+ self.assertFalse(bug_subscription_filter.find_all_tags)
317+
318+ bug_subscription_filter.tags = searchbuilder.all(u"foo")
319+ self.assertEqual(frozenset((u"foo",)), bug_subscription_filter.tags)
320+ self.assertTrue(bug_subscription_filter.find_all_tags)
321+
322+ # Not using `searchbuilder.any` or `.all` leaves find_all_tags
323+ # unchanged.
324+ bug_subscription_filter.tags = [u"-bar"]
325+ self.assertEqual(frozenset((u"-bar",)), bug_subscription_filter.tags)
326+ self.assertTrue(bug_subscription_filter.find_all_tags)
327+
328+ bug_subscription_filter.tags = searchbuilder.any(u"baz")
329+ self.assertEqual(frozenset((u"baz",)), bug_subscription_filter.tags)
330+ self.assertFalse(bug_subscription_filter.find_all_tags)
331
332=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py'
333--- lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 2010-09-17 12:32:14 +0000
334+++ lib/lp/bugs/model/tests/test_bugsubscriptionfiltertag.py 2010-10-05 09:28:22 +0000
335@@ -49,3 +49,15 @@
336 bug_sub_filter_tag.filter)
337 self.assertIs(True, bug_sub_filter_tag.include)
338 self.assertEqual(u"foo", bug_sub_filter_tag.tag)
339+
340+ def test_qualified_tag(self):
341+ """
342+ `BugSubscriptionFilterTag.qualified_tag` returns a tag with a
343+ preceeding hyphen if `include` is `False`.
344+ """
345+ bug_sub_filter_tag = BugSubscriptionFilterTag()
346+ bug_sub_filter_tag.tag = u"foo"
347+ bug_sub_filter_tag.include = True
348+ self.assertEqual(u"foo", bug_sub_filter_tag.qualified_tag)
349+ bug_sub_filter_tag.include = False
350+ self.assertEqual(u"-foo", bug_sub_filter_tag.qualified_tag)

Subscribers

People subscribed via source and target branches

to status/vote changes: