1###
2# Copyright (c) 2003-2005, Daniel DiPaolo
3# Copyright (c) 2010, James McCoy
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions, and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions, and the following disclaimer in the
13#     documentation and/or other materials provided with the distribution.
14#   * Neither the name of the author of this software nor the name of
15#     contributors to this software may be used to endorse or promote products
16#     derived from this software without specific prior written consent.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29###
30
31import os
32import re
33import time
34import operator
35
36import supybot.dbi as dbi
37import supybot.conf as conf
38import supybot.ircdb as ircdb
39import supybot.utils as utils
40from supybot.commands import *
41import supybot.plugins as plugins
42import supybot.ircutils as ircutils
43import supybot.callbacks as callbacks
44from supybot import commands
45from supybot.i18n import PluginInternationalization, internationalizeDocstring
46_ = PluginInternationalization('Todo')
47
48class TodoRecord(dbi.Record):
49    __fields__ = [
50          ('priority', int),
51          'at',
52          'task',
53          'active',
54          ]
55
56dataDir = conf.supybot.directories.data
57
58class FlatTodoDb(object):
59    def __init__(self):
60        self.directory = dataDir.dirize('Todo')
61        if not os.path.exists(self.directory):
62            os.mkdir(self.directory)
63        self.dbs = {}
64
65    def _getDb(self, uid):
66        dbfile = os.path.join(self.directory, str(uid))
67        if uid not in self.dbs:
68            self.dbs[uid] = dbi.DB(dbfile, Record=TodoRecord)
69        return self.dbs[uid]
70
71    def close(self):
72        for db in self.dbs.values():
73            db.close()
74
75    def get(self, uid, tid):
76        db = self._getDb(uid)
77        return db.get(tid)
78
79    def getTodos(self, uid):
80        db = self._getDb(uid)
81        L = [R for R in db.select(lambda r: r.active)]
82        if not L:
83            raise dbi.NoRecordError
84        return L
85
86    def add(self, priority, now, uid, task):
87        db = self._getDb(uid)
88        return db.add(TodoRecord(priority=priority, at=now,
89                                 task=task, active=True))
90
91    def remove(self, uid, tid):
92        db = self._getDb(uid)
93        t = db.get(tid)
94        t.active = False
95        db.set(tid, t)
96
97    def select(self, uid, criteria):
98        db = self._getDb(uid)
99        def match(todo):
100            for p in criteria:
101                if not p(todo.task):
102                    return False
103            return True
104        todos = db.select(lambda t: match(t))
105        if not todos:
106            raise dbi.NoRecordError
107        return todos
108
109    def setpriority(self, uid, tid, priority):
110        db = self._getDb(uid)
111        t = db.get(tid)
112        t.priority = priority
113        db.set(tid, t)
114
115    def change(self, uid, tid, replacer):
116        db = self._getDb(uid)
117        t = db.get(tid)
118        t.task = replacer(t.task)
119        db.set(tid, t)
120
121class Todo(callbacks.Plugin):
122    """This plugin allows you to create your own personal to-do list on
123    the bot."""
124    def __init__(self, irc):
125        self.__parent = super(Todo, self)
126        self.__parent.__init__(irc)
127        self.db = FlatTodoDb()
128
129    def die(self):
130        self.__parent.die()
131        self.db.close()
132
133    def _shrink(self, s):
134        return utils.str.ellipsisify(s, 50)
135
136    @internationalizeDocstring
137    def todo(self, irc, msg, args, user, taskid):
138        """[<username>] [<task id>]
139
140        Retrieves a task for the given task id.  If no task id is given, it
141        will return a list of task ids that that user has added to their todo
142        list.
143        """
144        try:
145            u = ircdb.users.getUser(msg.prefix)
146        except KeyError:
147            u = None
148        if u != user and not self.registryValue('allowThirdpartyReader'):
149            irc.error(_('You are not allowed to see other users todo-list.'))
150            return
151        # List the active tasks for the given user
152        if not taskid:
153            try:
154                tasks = self.db.getTodos(user.id)
155                utils.sortBy(operator.attrgetter('priority'), tasks)
156                tasks = [format(_('#%i: %s'), t.id, self._shrink(t.task))
157                         for t in tasks]
158                Todo = 'Todo'
159                if len(tasks) != 1:
160                    Todo = 'Todos'
161                irc.reply(format(_('%s for %s: %L'),
162                                 Todo, user.name, tasks))
163            except dbi.NoRecordError:
164                if u != user:
165                    irc.reply(_('That user has no tasks in their todo list.'))
166                else:
167                    irc.reply(_('You have no tasks in your todo list.'))
168                return
169        # Reply with the user's task
170        else:
171            try:
172                t = self.db.get(user.id, taskid)
173                if t.active:
174                    active = _('Active')
175                else:
176                    active = _('Inactive')
177                if t.priority:
178                    t.task += format(_(', priority: %i'), t.priority)
179                at = time.strftime(conf.supybot.reply.format.time(),
180                                   time.localtime(t.at))
181                s = format(_('%s todo for %s: %s (Added at %s)'),
182                           active, user.name, t.task, at)
183                irc.reply(s)
184            except dbi.NoRecordError:
185                irc.errorInvalid(_('task id'), taskid)
186    todo = wrap(todo, [first('otherUser', 'user'), additional(('id', 'task'))])
187
188    @internationalizeDocstring
189    def add(self, irc, msg, args, user, optlist, text, now):
190        """[--priority=<num>] <text>
191
192        Adds <text> as a task in your own personal todo list.  The optional
193        priority argument allows you to set a task as a high or low priority.
194        Any integer is valid.
195        """
196        priority = 0
197        for (option, arg) in optlist:
198            if option == 'priority':
199                priority = arg
200        todoId = self.db.add(priority, now, user.id, text)
201        irc.replySuccess(format(_('(Todo #%i added)'), todoId))
202    add = wrap(add, ['user', getopts({'priority': ('int', 'priority')}),
203                     'text', 'now'])
204
205    @internationalizeDocstring
206    def remove(self, irc, msg, args, user, tasks):
207        """<task id> [<task id> ...]
208
209        Removes <task id> from your personal todo list.
210        """
211        invalid = []
212        for taskid in tasks:
213            try:
214                self.db.get(user.id, taskid)
215            except dbi.NoRecordError:
216                invalid.append(taskid)
217        if invalid and len(invalid) == 1:
218            irc.error(format(_('Task %i could not be removed either because '
219                             'that id doesn\'t exist or it has been removed '
220                             'already.'), invalid[0]))
221        elif invalid:
222            irc.error(format(_('No tasks were removed because the following '
223                             'tasks could not be removed: %L.'), invalid))
224        else:
225            for taskid in tasks:
226                self.db.remove(user.id, taskid)
227            irc.replySuccess()
228    remove = wrap(remove, ['user', many(('id', 'task'))])
229
230    @internationalizeDocstring
231    def search(self, irc, msg, args, user, optlist, globs):
232        """[--{regexp} <value>] [<glob> <glob> ...]
233
234        Searches your todos for tasks matching <glob>.  If --regexp is given,
235        its associated value is taken as a regexp and matched against the
236        tasks.
237        """
238        if not optlist and not globs:
239            raise callbacks.ArgumentError
240        criteria = []
241        for (option, arg) in optlist:
242            if option == 'regexp':
243                criteria.append(lambda s:
244                                regexp_wrapper(s, reobj=arg, timeout=0.1,
245                                               plugin_name=self.name(),
246                                               fcn_name='search'))
247        for glob in globs:
248            glob = utils.python.glob2re(glob)
249            criteria.append(re.compile(glob).search)
250        try:
251            tasks = self.db.select(user.id, criteria)
252            L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks]
253            irc.reply(format('%L', L))
254        except dbi.NoRecordError:
255            irc.reply(_('No tasks matched that query.'))
256    search = wrap(search,
257                  ['user', getopts({'regexp': 'regexpMatcher'}), any('glob')])
258
259    @internationalizeDocstring
260    def setpriority(self, irc, msg, args, user, id, priority):
261        """<id> <priority>
262
263        Sets the priority of the todo with the given id to the specified value.
264        """
265        try:
266            self.db.setpriority(user.id, id, priority)
267            irc.replySuccess()
268        except dbi.NoRecordError:
269            irc.errorInvalid(_('task id'), id)
270    setpriority = wrap(setpriority,
271                       ['user', ('id', 'task'), ('int', 'priority')])
272
273    @internationalizeDocstring
274    def change(self, irc, msg, args, user, id, replacer):
275        """<task id> <regexp>
276
277        Modify the task with the given id using the supplied regexp.
278        """
279        try:
280            self.db.change(user.id, id, replacer)
281            irc.replySuccess()
282        except dbi.NoRecordError:
283            irc.errorInvalid(_('task id'), id)
284    change = wrap(change, ['user', ('id', 'task'), 'regexpReplacer'])
285
286
287Class = Todo
288
289
290# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
291