aboutsummaryrefslogtreecommitdiffstats
path: root/libbe/command/subscribe.py
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-12-31 15:54:12 -0500
committerW. Trevor King <wking@drexel.edu>2009-12-31 15:54:12 -0500
commitb0b5341c4045dd27cfbb3e2585cb2614ed9ad903 (patch)
tree37c7c2d011617ccd7a6f28a24ea77bb1b3cddfe7 /libbe/command/subscribe.py
parenta06030436d3940dddfba37b344f90651366d67e1 (diff)
parent2d1562d951e763fed71fe60e77cc9921be9abdc9 (diff)
downloadbugseverywhere-b0b5341c4045dd27cfbb3e2585cb2614ed9ad903.tar.gz
Merged be.restructure, major internal reorganization.
Added a bunch of classes to make the commands, user interfaces, and storage backends more abstract and distinct. This should make it much easier to extend and maintain BE. Features: * Directory restructured: becommands/ -> libbe/commands submods sorted by functionality. * Lots of new classes: Option, Argument, Command InputOutput, StorageCallbacks, UserInterface Storage * Consolidated ID handling in libbe.util.id * Transitioned VCS backends for Python-based VCSs from subprocess calss to internal python calls. Plus the user-visible changes: * New bugdir/bug/comment ID format replaces old bug:comment format. * Deprecated support for `be diff` on Arch and Darcs <= 2.3.1. A new backend abstraction (Storage) makes the former implementation ungainly. * Improved command completion. * Removed commands close, open, email_bugs, * Flipped some arguments `be assign BUG-ID [ASSIGNEE]` -> `be status ASSIGNED BUG-ID ...` `be severity BUG-ID SEVERITY` -> `be severity SEVERITY BUG-ID ...` `be status BUG-ID STATUS` -> `be status STATUS BUG-ID ...` In the merge: * Added 'commit' to list of pagerless commands. * Updated doc/README.dev See #bea86499-824e-4e77-b085-2d581fa9ccab/1100c966-9671-4bc6-8b68-6d408a910da1# for a discussion of why the changes were made and some of the difficulties en-route.
Diffstat (limited to 'libbe/command/subscribe.py')
-rw-r--r--libbe/command/subscribe.py385
1 files changed, 385 insertions, 0 deletions
diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py
new file mode 100644
index 0000000..78d6fe0
--- /dev/null
+++ b/libbe/command/subscribe.py
@@ -0,0 +1,385 @@
+# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import copy
+import os
+
+import libbe
+import libbe.bug
+import libbe.command
+import libbe.diff
+import libbe.command.util
+import libbe.util.id
+import libbe.util.tree
+
+
+TAG="SUBSCRIBE:"
+
+
+class Subscribe (libbe.command.Command):
+ """(Un)subscribe to change notification
+
+ >>> import sys
+ >>> import libbe.bugdir
+ >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
+ >>> io = libbe.command.StringInputOutput()
+ >>> io.stdout = sys.stdout
+ >>> ui = libbe.command.UserInterface(io=io)
+ >>> ui.storage_callbacks.set_bugdir(bd)
+ >>> cmd = Subscribe(ui=ui)
+
+ >>> a = bd.bug_from_uuid('a')
+ >>> print a.extra_strings
+ []
+ >>> ret = ui.run(cmd, {'subscriber':'John Doe <j@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ John Doe <j@doe.com> all *
+ >>> bd.flush_reload()
+ >>> a = bd.bug_from_uuid('a')
+ >>> print a.extra_strings
+ ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
+ >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com,b.net'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ Jane Doe <J@doe.com> all a.com,b.net
+ John Doe <j@doe.com> all *
+ >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ Jane Doe <J@doe.com> all a.com,a.edu,b.net
+ John Doe <j@doe.com> all *
+ >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ Jane Doe <J@doe.com> all a.edu,b.net
+ John Doe <j@doe.com> all *
+ >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ Jane Doe <J@doe.com> all *
+ John Doe <j@doe.com> all *
+ >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for abc/a:
+ John Doe <j@doe.com> all *
+ >>> ret = ui.run(cmd, {'unsubscribe':True, 'subscriber':'John Doe <j@doe.com>'}, ['/a'])
+ >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>', 'types':'new'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for bug directory:
+ Jane Doe <J@doe.com> new *
+ >>> ret = ui.run(cmd, {'subscriber':'Jane Doe <J@doe.com>'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE
+ Subscriptions for bug directory:
+ Jane Doe <J@doe.com> all *
+ >>> ui.cleanup()
+ >>> bd.cleanup()
+ """
+ name = 'subscribe'
+
+ def __init__(self, *args, **kwargs):
+ libbe.command.Command.__init__(self, *args, **kwargs)
+ self.options.extend([
+ libbe.command.Option(name='unsubscribe', short_name='u',
+ help='Unsubscribe instead of subscribing'),
+ libbe.command.Option(name='list-all', short_name='a',
+ help='List all subscribers (no ID argument, read only action)'),
+ libbe.command.Option(name='list', short_name='l',
+ help='List subscribers (read only action).'),
+ libbe.command.Option(name='subscriber', short_name='s',
+ help='Email address of the subscriber (defaults to bugdir.user_id).',
+ arg=libbe.command.Argument(
+ name='subscriber', metavar='EMAIL')),
+ libbe.command.Option(name='servers', short_name='S',
+ help='Servers from which you want notification.',
+ arg=libbe.command.Argument(
+ name='servers', metavar='STRING')),
+ libbe.command.Option(name='types', short_name='t',
+ help='Types of changes you wish to be notified about.',
+ arg=libbe.command.Argument(
+ name='types', metavar='STRING')),
+ ])
+ self.args.extend([
+ libbe.command.Argument(
+ name='id', metavar='ID', default=tuple(),
+ optional=True, repeatable=True,
+ completion_callback=libbe.command.util.complete_bug_comment_id),
+ ])
+
+ def _run(self, **params):
+ bugdir = self._get_bugdir()
+ if params['list-all'] == True or params['list'] == True:
+ writeable = bugdir.storage.writeable
+ bugdir.storage.writeable = False
+ if params['list-all'] == True:
+ assert len(params['id']) == 0, params['id']
+ subscriber = params['subscriber']
+ if subscriber == None:
+ subscriber = self._get_user_id()
+ if params['unsubscribe'] == True:
+ if params['servers'] == None:
+ params['servers'] = 'INVALID'
+ if params['types'] == None:
+ params['types'] = 'INVALID'
+ else:
+ if params['servers'] == None:
+ params['servers'] = '*'
+ if params['types'] == None:
+ params['types'] = 'all'
+ servers = params['servers'].split(',')
+ types = params['types'].split(',')
+
+ if len(params['id']) == 0:
+ params['id'] = [libbe.diff.BUGDIR_ID]
+ for _id in params['id']:
+ if _id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions
+ type_root = libbe.diff.BUGDIR_TYPE_ALL
+ entity = bugdir
+ entity_name = 'bug directory'
+ else: # bug-specific subscriptions
+ type_root = libbe.diff.BUG_TYPE_ALL
+ bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
+ bugdir, _id)
+ entity = bug
+ entity_name = bug.id.user()
+ if params['list-all'] == True:
+ entity_name = 'anything in the bug directory'
+ types = [libbe.diff.type_from_name(name, type_root, default=libbe.diff.INVALID_TYPE,
+ default_ok=params['unsubscribe'])
+ for name in types]
+ estrs = entity.extra_strings
+ if params['list'] == True or params['list-all'] == True:
+ pass
+ else: # alter subscriptions
+ if params['unsubscribe'] == True:
+ estrs = unsubscribe(estrs, subscriber, types, servers, type_root)
+ else: # add the tag
+ estrs = subscribe(estrs, subscriber, types, servers, type_root)
+ entity.extra_strings = estrs # reassign to notice change
+
+ if params['list-all'] == True:
+ bugdir.load_all_bugs()
+ subscriptions = get_bugdir_subscribers(bugdir, servers[0])
+ else:
+ subscriptions = []
+ for estr in entity.extra_strings:
+ if estr.startswith(TAG):
+ subscriptions.append(estr[len(TAG):])
+
+ if len(subscriptions) > 0:
+ print >> self.stdout, 'Subscriptions for %s:' % entity_name
+ print >> self.stdout, '\n'.join(subscriptions)
+ if params['list-all'] == True or params['list'] == True:
+ bugdir.storage.writeable = writeable
+ return 0
+
+ def _long_help(self):
+ return """
+ID can be either a bug id, or blank/"DIR", in which case it refers to the
+whole bug directory.
+
+SERVERS specifies the servers from which you would like to receive
+notification. Multiple severs may be specified in a comma-separated
+list, or you can use "*" to match all servers (the default). If you
+have not selected a server, it should politely refrain from notifying
+you of changes, although there is no way to guarantee this behavior.
+
+Available TYPES:
+ For bugs:
+%s
+ For %s:
+%s
+
+For unsubscription, any listed SERVERS and TYPES are removed from your
+subscription. Either the catch-all server "*" or type "%s" will
+remove SUBSCRIBER entirely from the specified ID.
+
+This command is intended for use primarily by public interfaces, since
+if you're just hacking away on your private repository, you'll known
+what's changed ;). This command just (un)sets the appropriate
+subscriptions, and leaves it up to each interface to perform the
+notification.
+""" % (libbe.diff.BUG_TYPE_ALL.string_tree(6), libbe.diff.BUGDIR_ID,
+ libbe.diff.BUGDIR_TYPE_ALL.string_tree(6),
+ libbe.diff.BUGDIR_TYPE_ALL)
+
+
+# internal helper functions
+
+def _generate_string(subscriber, types, servers):
+ types = sorted([str(t) for t in types])
+ servers = sorted(servers)
+ return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers))
+
+def _parse_string(string, type_root):
+ assert string.startswith(TAG), string
+ string = string[len(TAG):]
+ subscriber,types,servers = string.split("\t")
+ types = [libbe.diff.type_from_name(name, type_root) for name in types.split(",")]
+ return (subscriber,types,servers.split(","))
+
+def _get_subscriber(extra_strings, subscriber, type_root):
+ for i,string in enumerate(extra_strings):
+ if string.startswith(TAG):
+ s,ts,srvs = _parse_string(string, type_root)
+ if s == subscriber:
+ return i,s,ts,srvs # match!
+ return None # no match
+
+# functions exposed to other modules
+
+def subscribe(extra_strings, subscriber, types, servers, type_root):
+ args = _get_subscriber(extra_strings, subscriber, type_root)
+ if args == None: # no match
+ extra_strings.append(_generate_string(subscriber, types, servers))
+ return extra_strings
+ # Alter matched string
+ i,s,ts,srvs = args
+ for t in types:
+ if t not in ts:
+ ts.append(t)
+ # remove descendant types
+ all_ts = copy.copy(ts)
+ for t in all_ts:
+ for tt in all_ts:
+ if tt in ts and t.has_descendant(tt):
+ ts.remove(tt)
+ if "*" in servers+srvs:
+ srvs = ["*"]
+ else:
+ srvs = list(set(servers+srvs))
+ extra_strings[i] = _generate_string(subscriber, ts, srvs)
+ return extra_strings
+
+def unsubscribe(extra_strings, subscriber, types, servers, type_root):
+ args = _get_subscriber(extra_strings, subscriber, type_root)
+ if args == None: # no match
+ return extra_strings # pass
+ # Remove matched string
+ i,s,ts,srvs = args
+ all_ts = copy.copy(ts)
+ for t in types:
+ for tt in all_ts:
+ if tt in ts and t.has_descendant(tt):
+ ts.remove(tt)
+ if "*" in servers+srvs:
+ srvs = []
+ else:
+ for srv in servers:
+ if srv in srvs:
+ srvs.remove(srv)
+ if len(ts) == 0 or len(srvs) == 0:
+ extra_strings.pop(i)
+ else:
+ extra_strings[i] = _generate_string(subscriber, ts, srvs)
+ return extra_strings
+
+def get_subscribers(extra_strings, type, server, type_root,
+ match_ancestor_types=False,
+ match_descendant_types=False):
+ """
+ Set match_ancestor_types=True if you want to find eveyone who
+ cares about your particular type.
+
+ Set match_descendant_types=True if you want to find subscribers
+ who may only care about some subset of your type. This is useful
+ for generating lists of all the subscribers in a given set of
+ extra_strings.
+
+ >>> def sgs(*args, **kwargs):
+ ... return sorted(get_subscribers(*args, **kwargs))
+ >>> es = []
+ >>> es = subscribe(es, "John Doe <j@doe.com>", [libbe.diff.BUGDIR_TYPE_ALL],
+ ... ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
+ >>> es = subscribe(es, "Jane Doe <J@doe.com>", [libbe.diff.BUGDIR_TYPE_NEW],
+ ... ["*"], libbe.diff.BUGDIR_TYPE_ALL)
+ >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
+ ['John Doe <j@doe.com>']
+ >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
+ ... match_descendant_types=True)
+ ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
+ >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "b.net", libbe.diff.BUGDIR_TYPE_ALL,
+ ... match_descendant_types=True)
+ ['Jane Doe <J@doe.com>']
+ >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
+ ['Jane Doe <J@doe.com>']
+ >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
+ ... match_ancestor_types=True)
+ ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
+ """
+ for string in extra_strings:
+ if not string.startswith(TAG):
+ continue
+ subscriber,types,servers = _parse_string(string, type_root)
+ type_match = False
+ if type in types:
+ type_match = True
+ if type_match == False and match_ancestor_types == True:
+ for t in types:
+ if t.has_descendant(type):
+ type_match = True
+ break
+ if type_match == False and match_descendant_types == True:
+ for t in types:
+ if type.has_descendant(t):
+ type_match = True
+ break
+ server_match = False
+ if server in servers or servers == ["*"] or server == "*":
+ server_match = True
+ if type_match == True and server_match == True:
+ yield subscriber
+
+def get_bugdir_subscribers(bugdir, server):
+ """
+ I have a bugdir. Who cares about it, and what do they care about?
+ Returns a dict of dicts:
+ subscribers[user][id] = types
+ where id is either a bug.uuid (in the case of a bug subscription)
+ or "%(bugdir_id)s" (in the case of a bugdir subscription).
+
+ Only checks bugs that are currently in memory, so you might want
+ to call bugdir.load_all_bugs() first.
+
+ >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
+ >>> a = bd.bug_from_shortname("a")
+ >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>",
+ ... [libbe.diff.BUGDIR_TYPE_ALL], ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
+ >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>",
+ ... [libbe.diff.BUGDIR_TYPE_NEW], ["*"], libbe.diff.BUGDIR_TYPE_ALL)
+ >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>",
+ ... [libbe.diff.BUG_TYPE_ALL], ["a.com"], libbe.diff.BUG_TYPE_ALL)
+ >>> subscribers = get_bugdir_subscribers(bd, "a.com")
+ >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"]
+ [<SubscriptionType: new>]
+ >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"]
+ [<SubscriptionType: all>]
+ >>> subscribers["John Doe <j@doe.com>"]["a"]
+ [<SubscriptionType: all>]
+ >>> get_bugdir_subscribers(bd, "b.net")
+ {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}}
+ >>> bd.cleanup()
+ """ % {'bugdir_id':libbe.diff.BUGDIR_ID}
+ subscribers = {}
+ for sub in get_subscribers(bugdir.extra_strings, libbe.diff.BUGDIR_TYPE_ALL,
+ server, libbe.diff.BUGDIR_TYPE_ALL,
+ match_descendant_types=True):
+ i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub,
+ libbe.diff.BUGDIR_TYPE_ALL)
+ subscribers[sub] = {"DIR":ts}
+ for bug in bugdir:
+ for sub in get_subscribers(bug.extra_strings, libbe.diff.BUG_TYPE_ALL,
+ server, libbe.diff.BUG_TYPE_ALL,
+ match_descendant_types=True):
+ i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub,
+ libbe.diff.BUG_TYPE_ALL)
+ if sub in subscribers:
+ subscribers[sub][bug.uuid] = ts
+ else:
+ subscribers[sub] = {bug.uuid:ts}
+ return subscribers