aboutsummaryrefslogtreecommitdiffstats
path: root/libbe/command/subscribe.py
blob: 6001e033933ad91084a158893e119a01d535da6c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# Copyright (C) 2009-2010 Gianluca Montecchi <gian@grys.it>
#                         W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
# Bugs Everywhere 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.
#
# Bugs Everywhere 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 Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.

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 your 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