19aac53b768a340a16fd44bd36364e5ce4720286
[be.git] / becommands / subscribe.py
1 # Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16 """(Un)subscribe to change notification"""
17 from libbe import cmdutil, bugdir, tree, diff
18 import os, copy
19 __desc__ = __doc__
20
21 TAG="SUBSCRIBE:"
22
23 def execute(args, manipulate_encodings=True, restrict_file_access=False):
24     """
25     >>> bd = bugdir.SimpleBugDir()
26     >>> bd.set_sync_with_disk(True)
27     >>> os.chdir(bd.root)
28     >>> a = bd.bug_from_shortname("a")
29     >>> print a.extra_strings
30     []
31     >>> execute(["-s","John Doe <j@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
32     Subscriptions for a:
33     John Doe <j@doe.com>    all    *
34     >>> bd._clear_bugs() # resync our copy of bug
35     >>> a = bd.bug_from_shortname("a")
36     >>> print a.extra_strings
37     ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
38     >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
39     Subscriptions for a:
40     Jane Doe <J@doe.com>    all    a.com,b.net
41     John Doe <j@doe.com>    all    *
42     >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
43     Subscriptions for a:
44     Jane Doe <J@doe.com>    all    a.com,a.edu,b.net
45     John Doe <j@doe.com>    all    *
46     >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
47     Subscriptions for a:
48     Jane Doe <J@doe.com>    all    a.edu,b.net
49     John Doe <j@doe.com>    all    *
50     >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
51     Subscriptions for a:
52     Jane Doe <J@doe.com>    all    *
53     John Doe <j@doe.com>    all    *
54     >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
55     Subscriptions for a:
56     John Doe <j@doe.com>    all    *
57     >>> execute(["-u", "-s","John Doe <j@doe.com>", "a"], manipulate_encodings=False)
58     >>> execute(["-s","Jane Doe <J@doe.com>", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
59     Subscriptions for bug directory:
60     Jane Doe <J@doe.com>    new    *
61     >>> execute(["-s","Jane Doe <J@doe.com>", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE
62     Subscriptions for bug directory:
63     Jane Doe <J@doe.com>    all    *
64     >>> bd.cleanup()
65     """
66     parser = get_parser()
67     options, args = parser.parse_args(args)
68     cmdutil.default_complete(options, args, parser,
69                              bugid_args={0: lambda bug : bug.active==True})
70
71     if len(args) > 1:
72         help()
73         raise cmdutil.UsageError("Too many arguments.")
74
75     bd = bugdir.BugDir(from_disk=True,
76                        manipulate_encodings=manipulate_encodings)
77
78     subscriber = options.subscriber
79     if subscriber == None:
80         subscriber = bd.user_id
81     if options.unsubscribe == True:
82         if options.servers == None:
83             options.servers = "INVALID"
84         if options.types == None:
85             options.types = "INVALID"
86     else:
87         if options.servers == None:
88             options.servers = "*"
89         if options.types == None:
90             options.types = "all"
91     servers = options.servers.split(",")
92     types = options.types.split(",")
93
94     if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions
95         type_root = diff.BUGDIR_TYPE_ALL
96         entity = bd
97         entity_name = "bug directory"
98     else: # bug-specific subscriptions
99         type_root = diff.BUG_TYPE_ALL
100         bug = bd.bug_from_shortname(args[0])
101         entity = bug
102         entity_name = bug.uuid
103     if options.list_all == True:
104         entity_name = "anything in the bug directory"
105
106     types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE,
107                                  default_ok=options.unsubscribe)
108              for name in types]
109     estrs = entity.extra_strings
110     if options.list == True or options.list_all == True:
111         pass
112     else: # alter subscriptions
113         if options.unsubscribe == True:
114             estrs = unsubscribe(estrs, subscriber, types, servers, type_root)
115         else: # add the tag
116             estrs = subscribe(estrs, subscriber, types, servers, type_root)
117         entity.extra_strings = estrs # reassign to notice change
118
119     if options.list_all == True:
120         bd.load_all_bugs()
121         subscriptions = get_bugdir_subscribers(bd, servers[0])
122     else:
123         subscriptions = []
124         for estr in entity.extra_strings:
125             if estr.startswith(TAG):
126                 subscriptions.append(estr[len(TAG):])
127
128     if len(subscriptions) > 0:
129         print "Subscriptions for %s:" % entity_name
130         print '\n'.join(subscriptions)
131
132
133 def get_parser():
134     parser = cmdutil.CmdOptionParser("be subscribe ID")
135     parser.add_option("-u", "--unsubscribe", action="store_true",
136                       dest="unsubscribe", default=False,
137                       help="Unsubscribe instead of subscribing.")
138     parser.add_option("-a", "--list-all", action="store_true",
139                       dest="list_all", default=False,
140                       help="List all subscribers (no ID argument, read only action).")
141     parser.add_option("-l", "--list", action="store_true",
142                       dest="list", default=False,
143                       help="List subscribers (read only action).")
144     parser.add_option("-s", "--subscriber", dest="subscriber",
145                       metavar="SUBSCRIBER",
146                       help="Email address of the subscriber (defaults to bugdir.user_id).")
147     parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS",
148                       help="Servers from which you want notification.")
149     parser.add_option("-t", "--type", dest="types", metavar="TYPES",
150                       help="Types of changes you wish to be notified about.")
151     return parser
152
153 longhelp="""
154 ID can be either a bug id, or blank/"DIR", in which case it refers to the
155 whole bug directory.
156
157 SERVERS specifies the servers from which you would like to receive
158 notification.  Multiple severs may be specified in a comma-separated
159 list, or you can use "*" to match all servers (the default).  If you
160 have not selected a server, it should politely refrain from notifying
161 you of changes, although there is no way to guarantee this behavior.
162
163 Available TYPES:
164   For bugs:
165 %s
166   For %s:
167 %s
168
169 For unsubscription, any listed SERVERS and TYPES are removed from your
170 subscription.  Either the catch-all server "*" or type "%s" will
171 remove SUBSCRIBER entirely from the specified ID.
172
173 This command is intended for use primarily by public interfaces, since
174 if you're just hacking away on your private repository, you'll known
175 what's changed ;).  This command just (un)sets the appropriate
176 subscriptions, and leaves it up to each interface to perform the
177 notification.
178 """ % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID,
179        diff.BUGDIR_TYPE_ALL.string_tree(6),
180        diff.BUGDIR_TYPE_ALL)
181
182 def help():
183     return get_parser().help_str() + longhelp
184
185 # internal helper functions
186
187 def _generate_string(subscriber, types, servers):
188     types = sorted([str(t) for t in types])
189     servers = sorted(servers)
190     return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers))
191
192 def _parse_string(string, type_root):
193     assert string.startswith(TAG), string
194     string = string[len(TAG):]
195     subscriber,types,servers = string.split("\t")
196     types = [diff.type_from_name(name, type_root) for name in types.split(",")]
197     return (subscriber,types,servers.split(","))
198
199 def _get_subscriber(extra_strings, subscriber, type_root):
200     for i,string in enumerate(extra_strings):
201         if string.startswith(TAG):
202             s,ts,srvs = _parse_string(string, type_root)
203             if s == subscriber:
204                 return i,s,ts,srvs # match!
205     return None # no match
206
207 # functions exposed to other modules
208
209 def subscribe(extra_strings, subscriber, types, servers, type_root):
210     args = _get_subscriber(extra_strings, subscriber, type_root)
211     if args == None: # no match
212         extra_strings.append(_generate_string(subscriber, types, servers))
213         return extra_strings
214     # Alter matched string
215     i,s,ts,srvs = args
216     for t in types:
217         if t not in ts:
218             ts.append(t)
219     # remove descendant types
220     all_ts = copy.copy(ts)
221     for t in all_ts:
222         for tt in all_ts:
223             if tt in ts and t.has_descendant(tt):
224                 ts.remove(tt)
225     if "*" in servers+srvs:
226         srvs = ["*"]
227     else:
228         srvs = list(set(servers+srvs))
229     extra_strings[i] = _generate_string(subscriber, ts, srvs)
230     return extra_strings
231
232 def unsubscribe(extra_strings, subscriber, types, servers, type_root):
233     args = _get_subscriber(extra_strings, subscriber, type_root)
234     if args == None: # no match
235         return extra_strings # pass
236     # Remove matched string
237     i,s,ts,srvs = args
238     all_ts = copy.copy(ts)
239     for t in types:
240         for tt in all_ts:
241             if tt in ts and t.has_descendant(tt):
242                 ts.remove(tt)
243     if "*" in servers+srvs:
244         srvs = []
245     else:
246         for srv in servers:
247             if srv in srvs:
248                 srvs.remove(srv)
249     if len(ts) == 0 or len(srvs) == 0:
250         extra_strings.pop(i)
251     else:
252         extra_strings[i] = _generate_string(subscriber, ts, srvs)
253     return extra_strings
254
255 def get_subscribers(extra_strings, type, server, type_root,
256                     match_ancestor_types=False,
257                     match_descendant_types=False):
258     """
259     Set match_ancestor_types=True if you want to find eveyone who
260     cares about your particular type.
261
262     Set match_descendant_types=True if you want to find subscribers
263     who may only care about some subset of your type.  This is useful
264     for generating lists of all the subscribers in a given set of
265     extra_strings.
266
267     >>> def sgs(*args, **kwargs):
268     ...     return sorted(get_subscribers(*args, **kwargs))
269     >>> es = []
270     >>> es = subscribe(es, "John Doe <j@doe.com>", [diff.BUGDIR_TYPE_ALL],
271     ...                ["a.com"], diff.BUGDIR_TYPE_ALL)
272     >>> es = subscribe(es, "Jane Doe <J@doe.com>", [diff.BUGDIR_TYPE_NEW],
273     ...                ["*"], diff.BUGDIR_TYPE_ALL)
274     >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL)
275     ['John Doe <j@doe.com>']
276     >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL,
277     ...     match_descendant_types=True)
278     ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
279     >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL,
280     ...     match_descendant_types=True)
281     ['Jane Doe <J@doe.com>']
282     >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL)
283     ['Jane Doe <J@doe.com>']
284     >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL,
285     ... match_ancestor_types=True)
286     ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
287     """
288     for string in extra_strings:
289         if not string.startswith(TAG):
290             continue
291         subscriber,types,servers = _parse_string(string, type_root)
292         type_match = False
293         if type in types:
294             type_match = True
295         if type_match == False and match_ancestor_types == True:
296             for t in types:
297                 if t.has_descendant(type):
298                     type_match = True
299                     break
300         if type_match == False and match_descendant_types == True:
301             for t in types:
302                 if type.has_descendant(t):
303                     type_match = True
304                     break
305         server_match = False
306         if server in servers or servers == ["*"] or server == "*":
307             server_match = True
308         if type_match == True and server_match == True:
309             yield subscriber
310
311 def get_bugdir_subscribers(bugdir, server):
312     """
313     I have a bugdir.  Who cares about it, and what do they care about?
314     Returns a dict of dicts:
315       subscribers[user][id] = types
316     where id is either a bug.uuid (in the case of a bug subscription)
317     or "%(bugdir_id)s" (in the case of a bugdir subscription).
318
319     Only checks bugs that are currently in memory, so you might want
320     to call bugdir.load_all_bugs() first.
321
322     >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
323     >>> a = bd.bug_from_shortname("a")
324     >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>",
325     ...                [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL)
326     >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>",
327     ...                [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL)
328     >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>",
329     ...                [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL)
330     >>> subscribers = get_bugdir_subscribers(bd, "a.com")
331     >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"]
332     [<SubscriptionType: new>]
333     >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"]
334     [<SubscriptionType: all>]
335     >>> subscribers["John Doe <j@doe.com>"]["a"]
336     [<SubscriptionType: all>]
337     >>> get_bugdir_subscribers(bd, "b.net")
338     {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}}
339     >>> bd.cleanup()
340     """ % {'bugdir_id':diff.BUGDIR_ID}
341     subscribers = {}
342     for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL,
343                                server, diff.BUGDIR_TYPE_ALL,
344                                match_descendant_types=True):
345         i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub,
346                                       diff.BUGDIR_TYPE_ALL)
347         subscribers[sub] = {"DIR":ts}
348     for bug in bugdir:
349         for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL,
350                                    server, diff.BUG_TYPE_ALL,
351                                    match_descendant_types=True):
352             i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub,
353                                           diff.BUG_TYPE_ALL)
354             if sub in subscribers:
355                 subscribers[sub][bug.uuid] = ts
356             else:
357                 subscribers[sub] = {bug.uuid:ts}
358     return subscribers