1 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
2 # Gianluca Montecchi <gian@grys.it>
3 # W. Trevor King <wking@tremily.us>
5 # This file is part of Bugs Everywhere.
7 # Bugs Everywhere is free software: you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the Free
9 # Software Foundation, either version 2 of the License, or (at your option) any
12 # Bugs Everywhere is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17 # You should have received a copy of the GNU General Public License along with
18 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
27 import libbe.command.util
29 import libbe.util.tree
35 class Subscribe (libbe.command.Command):
36 """(Un)subscribe to change notification
39 >>> import libbe.bugdir
40 >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
41 >>> io = libbe.command.StringInputOutput()
42 >>> io.stdout = sys.stdout
43 >>> ui = libbe.command.UserInterface(io=io)
44 >>> ui.storage_callbacks.set_storage(bd.storage)
45 >>> cmd = Subscribe(ui=ui)
47 >>> a = bd.bug_from_uuid('a')
48 >>> print a.extra_strings
50 >>> ret = ui.run(cmd, {'subscriber':'John Doe <j@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
51 Subscriptions for abc/a:
52 John Doe <j@doe.com> all *
54 >>> a = bd.bug_from_uuid('a')
55 >>> print a.extra_strings
56 ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
57 >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com,b.net'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
58 Subscriptions for abc/a:
59 Jane Doe <J@doe.com> all a.com,b.net
60 John Doe <j@doe.com> all *
61 >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
62 Subscriptions for abc/a:
63 Jane Doe <J@doe.com> all a.com,a.edu,b.net
64 John Doe <j@doe.com> all *
65 >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
66 Subscriptions for abc/a:
67 Jane Doe <J@doe.com> all a.edu,b.net
68 John Doe <j@doe.com> all *
69 >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
70 Subscriptions for abc/a:
71 Jane Doe <J@doe.com> all *
72 John Doe <j@doe.com> all *
73 >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
74 Subscriptions for abc/a:
75 John Doe <j@doe.com> all *
76 >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'John Doe <j@doe.com>'}, ['/a'])
78 ... {'subscriber':'Jane Doe <J@doe.com>', 'types':'new'},
79 ... [bd.uuid[:3]]) # doctest: +NORMALIZE_WHITESPACE
80 Subscriptions for abc:
81 Jane Doe <J@doe.com> new *
83 ... {'subscriber':'Jane Doe <J@doe.com>'},
84 ... [bd.uuid]) # doctest: +NORMALIZE_WHITESPACE
85 Subscriptions for abc:
86 Jane Doe <J@doe.com> all *
92 def __init__(self, *args, **kwargs):
93 libbe.command.Command.__init__(self, *args, **kwargs)
95 libbe.command.Option(name='unsubscribe', short_name='u',
96 help='Unsubscribe instead of subscribing'),
97 libbe.command.Option(name='list-all', short_name='a',
98 help='List all subscribers (no ID argument, read only action)'),
99 libbe.command.Option(name='list', short_name='l',
100 help='List subscribers (read only action).'),
101 libbe.command.Option(name='subscriber', short_name='s',
102 help='Email address of the subscriber (defaults to your user id).',
103 arg=libbe.command.Argument(
104 name='subscriber', metavar='EMAIL')),
105 libbe.command.Option(name='servers', short_name='S',
106 help='Servers from which you want notification.',
107 arg=libbe.command.Argument(
108 name='servers', metavar='STRING')),
109 libbe.command.Option(name='types', short_name='t',
110 help='Types of changes you wish to be notified about.',
111 arg=libbe.command.Argument(
112 name='types', metavar='STRING')),
115 libbe.command.Argument(
116 name='id', metavar='ID', default=tuple(),
117 optional=True, repeatable=True,
118 completion_callback=libbe.command.util.complete_bug_comment_id),
121 def _run(self, **params):
122 storage = self._get_storage()
123 bugdirs = self._get_bugdirs()
124 if params['list-all'] == True or params['list'] == True:
125 writeable = storage.writeable
126 storage.writeable = False
127 if params['list-all'] == True:
128 assert len(params['id']) == 0, params['id']
129 subscriber = params['subscriber']
130 if subscriber == None:
131 subscriber = self._get_user_id()
132 if params['unsubscribe'] == True:
133 if params['servers'] == None:
134 params['servers'] = 'INVALID'
135 if params['types'] == None:
136 params['types'] = 'INVALID'
138 if params['servers'] == None:
139 params['servers'] = '*'
140 if params['types'] == None:
141 params['types'] = 'all'
142 servers = params['servers'].split(',')
143 types = params['types'].split(',')
145 if len(params['id']) == 0:
146 params['id'] = bugdirs.keys()
147 for _id in params['id']:
148 p = libbe.util.id.parse_user(bugdirs, _id)
149 if p['type'] == 'bugdir':
150 type_root = libbe.diff.BUGDIR_TYPE_ALL
151 entity = bugdirs[p['bugdir']]
152 else: # bug-specific subscriptions
153 type_root = libbe.diff.BUG_TYPE_ALL
154 bugdir,bug,comment = (
155 libbe.command.util.bugdir_bug_comment_from_user_id(
158 entity_name = entity.id.user()
159 if params['list-all'] == True:
160 entity_name = 'anything in the bug directory'
161 types = [libbe.diff.type_from_name(name, type_root, default=libbe.diff.INVALID_TYPE,
162 default_ok=params['unsubscribe'])
164 estrs = entity.extra_strings
165 if params['list'] == True or params['list-all'] == True:
167 else: # alter subscriptions
168 if params['unsubscribe'] == True:
169 estrs = unsubscribe(estrs, subscriber, types, servers, type_root)
171 estrs = subscribe(estrs, subscriber, types, servers, type_root)
172 entity.extra_strings = estrs # reassign to notice change
174 if params['list-all'] == True:
176 for bugdir in bugdirs.values():
177 bugdir.load_all_bugs()
178 subscriptions.extend(
179 get_bugdir_subscribers(bugdir, servers[0]))
182 for estr in entity.extra_strings:
183 if estr.startswith(TAG):
184 subscriptions.append(estr[len(TAG):])
186 if len(subscriptions) > 0:
187 print >> self.stdout, 'Subscriptions for %s:' % entity_name
188 print >> self.stdout, '\n'.join(subscriptions)
189 if params['list-all'] == True or params['list'] == True:
190 storage.writeable = writeable
193 def _long_help(self):
195 ID can be either a bug ID, a bugdir ID, or blank, in which case it
196 refers to all known bugdirs.
198 SERVERS specifies the servers from which you would like to receive
199 notification. Multiple severs may be specified in a comma-separated
200 list, or you can use "*" to match all servers (the default). If you
201 have not selected a server, it should politely refrain from notifying
202 you of changes, although there is no way to guarantee this behavior.
210 For unsubscription, any listed SERVERS and TYPES are removed from your
211 subscription. Either the catch-all server "*" or type "%s" will
212 remove SUBSCRIBER entirely from the specified ID.
214 This command is intended for use primarily by public interfaces, since
215 if you're just hacking away on your private repository, you'll known
216 what's changed ;). This command just (un)sets the appropriate
217 subscriptions, and leaves it up to each interface to perform the
219 """ % (libbe.diff.BUG_TYPE_ALL.string_tree(6), libbe.diff.BUGDIR_ID,
220 libbe.diff.BUGDIR_TYPE_ALL.string_tree(6),
221 libbe.diff.BUGDIR_TYPE_ALL)
224 # internal helper functions
226 def _generate_string(subscriber, types, servers):
227 types = sorted([str(t) for t in types])
228 servers = sorted(servers)
229 return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers))
231 def _parse_string(string, type_root):
232 assert string.startswith(TAG), string
233 string = string[len(TAG):]
234 subscriber,types,servers = string.split("\t")
235 types = [libbe.diff.type_from_name(name, type_root) for name in types.split(",")]
236 return (subscriber,types,servers.split(","))
238 def _get_subscriber(extra_strings, subscriber, type_root):
239 for i,string in enumerate(extra_strings):
240 if string.startswith(TAG):
241 s,ts,srvs = _parse_string(string, type_root)
243 return i,s,ts,srvs # match!
244 return None # no match
246 # functions exposed to other modules
248 def subscribe(extra_strings, subscriber, types, servers, type_root):
249 args = _get_subscriber(extra_strings, subscriber, type_root)
250 if args == None: # no match
251 extra_strings.append(_generate_string(subscriber, types, servers))
253 # Alter matched string
258 # remove descendant types
259 all_ts = copy.copy(ts)
262 if tt in ts and t.has_descendant(tt):
264 if "*" in servers+srvs:
267 srvs = list(set(servers+srvs))
268 extra_strings[i] = _generate_string(subscriber, ts, srvs)
271 def unsubscribe(extra_strings, subscriber, types, servers, type_root):
272 args = _get_subscriber(extra_strings, subscriber, type_root)
273 if args == None: # no match
274 return extra_strings # pass
275 # Remove matched string
277 all_ts = copy.copy(ts)
280 if tt in ts and t.has_descendant(tt):
282 if "*" in servers+srvs:
288 if len(ts) == 0 or len(srvs) == 0:
291 extra_strings[i] = _generate_string(subscriber, ts, srvs)
294 def get_subscribers(extra_strings, type, server, type_root,
295 match_ancestor_types=False,
296 match_descendant_types=False):
298 Set match_ancestor_types=True if you want to find eveyone who
299 cares about your particular type.
301 Set match_descendant_types=True if you want to find subscribers
302 who may only care about some subset of your type. This is useful
303 for generating lists of all the subscribers in a given set of
306 >>> def sgs(*args, **kwargs):
307 ... return sorted(get_subscribers(*args, **kwargs))
309 >>> es = subscribe(es, "John Doe <j@doe.com>", [libbe.diff.BUGDIR_TYPE_ALL],
310 ... ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
311 >>> es = subscribe(es, "Jane Doe <J@doe.com>", [libbe.diff.BUGDIR_TYPE_NEW],
312 ... ["*"], libbe.diff.BUGDIR_TYPE_ALL)
313 >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
314 ['John Doe <j@doe.com>']
315 >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
316 ... match_descendant_types=True)
317 ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
318 >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "b.net", libbe.diff.BUGDIR_TYPE_ALL,
319 ... match_descendant_types=True)
320 ['Jane Doe <J@doe.com>']
321 >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
322 ['Jane Doe <J@doe.com>']
323 >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
324 ... match_ancestor_types=True)
325 ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
327 for string in extra_strings:
328 if not string.startswith(TAG):
330 subscriber,types,servers = _parse_string(string, type_root)
334 if type_match == False and match_ancestor_types == True:
336 if t.has_descendant(type):
339 if type_match == False and match_descendant_types == True:
341 if type.has_descendant(t):
345 if server in servers or servers == ["*"] or server == "*":
347 if type_match == True and server_match == True:
350 def get_bugdir_subscribers(bugdir, server):
352 I have a bugdir. Who cares about it, and what do they care about?
353 Returns a dict of dicts:
354 subscribers[user][id] = types
355 where id is either a bug.uuid (in the case of a bug subscription)
356 or "%(bugdir_id)s" (in the case of a bugdir subscription).
358 Only checks bugs that are currently in memory, so you might want
359 to call bugdir.load_all_bugs() first.
361 >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
362 >>> a = bd.bug_from_shortname("a")
363 >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>",
364 ... [libbe.diff.BUGDIR_TYPE_ALL], ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
365 >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>",
366 ... [libbe.diff.BUGDIR_TYPE_NEW], ["*"], libbe.diff.BUGDIR_TYPE_ALL)
367 >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>",
368 ... [libbe.diff.BUG_TYPE_ALL], ["a.com"], libbe.diff.BUG_TYPE_ALL)
369 >>> subscribers = get_bugdir_subscribers(bd, "a.com")
370 >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"]
371 [<SubscriptionType: new>]
372 >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"]
373 [<SubscriptionType: all>]
374 >>> subscribers["John Doe <j@doe.com>"]["a"]
375 [<SubscriptionType: all>]
376 >>> get_bugdir_subscribers(bd, "b.net")
377 {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}}
379 """ % {'bugdir_id':libbe.diff.BUGDIR_ID}
381 for sub in get_subscribers(bugdir.extra_strings, libbe.diff.BUGDIR_TYPE_ALL,
382 server, libbe.diff.BUGDIR_TYPE_ALL,
383 match_descendant_types=True):
384 i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub,
385 libbe.diff.BUGDIR_TYPE_ALL)
386 subscribers[sub] = {"DIR":ts}
388 for sub in get_subscribers(bug.extra_strings, libbe.diff.BUG_TYPE_ALL,
389 server, libbe.diff.BUG_TYPE_ALL,
390 match_descendant_types=True):
391 i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub,
392 libbe.diff.BUG_TYPE_ALL)
393 if sub in subscribers:
394 subscribers[sub][bug.uuid] = ts
396 subscribers[sub] = {bug.uuid:ts}