diff --git a/plugins/Todo/README.txt b/plugins/Todo/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/plugins/Todo/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/plugins/Todo/__init__.py b/plugins/Todo/__init__.py new file mode 100644 index 000000000..8af066349 --- /dev/null +++ b/plugins/Todo/__init__.py @@ -0,0 +1,61 @@ +### +# Copyright (c) 2003-2005, Daniel DiPaolo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +The Todo plugin allows registered users to keep their own personal list of +tasks to do, with an optional priority for each. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "%%VERSION%%" + +__author__ = supybot.authors.strike + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Todo/config.py b/plugins/Todo/config.py new file mode 100644 index 000000000..896e69d5f --- /dev/null +++ b/plugins/Todo/config.py @@ -0,0 +1,48 @@ +### +# Copyright (c) 2003-2005, Daniel DiPaolo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Todo', True) + + +Todo = conf.registerPlugin('Todo') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(Todo, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78 diff --git a/plugins/Todo/plugin.py b/plugins/Todo/plugin.py new file mode 100644 index 000000000..3b088ec76 --- /dev/null +++ b/plugins/Todo/plugin.py @@ -0,0 +1,265 @@ +### +# Copyright (c) 2003-2005, Daniel DiPaolo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import os +import re +import time +import fnmatch +import operator + +import supybot.dbi as dbi +import supybot.conf as conf +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks + +class TodoRecord(dbi.Record): + __fields__ = [ + 'priority', + 'at', + 'task', + 'active', + ] + +dataDir = conf.supybot.directories.data + +class FlatTodoDb(object): + def __init__(self): + self.directory = dataDir.dirize('Todo') + if not os.path.exists(self.directory): + os.mkdir(self.directory) + self.dbs = {} + + def _getDb(self, uid): + dbfile = os.path.join(self.directory, str(uid)) + if uid not in self.dbs: + self.dbs[uid] = dbi.DB(dbfile, Record=TodoRecord) + return self.dbs[uid] + + def close(self): + for db in self.dbs.itervalues(): + db.close() + + def get(self, uid, tid): + db = self._getDb(uid) + return db.get(tid) + + def getTodos(self, uid): + db = self._getDb(uid) + L = [R for R in db.select(lambda r: r.active)] + if not L: + raise dbi.NoRecordError + return L + + def add(self, priority, now, uid, task): + db = self._getDb(uid) + return db.add(TodoRecord(priority=priority, at=now, + task=task, active=True)) + + def remove(self, uid, tid): + db = self._getDb(uid) + t = db.get(tid) + t.active = False + db.set(tid, t) + + def select(self, uid, criteria): + db = self._getDb(uid) + def match(todo): + for p in criteria: + if not p(todo.task): + return False + return True + todos = db.select(lambda t: match(t)) + if not todos: + raise dbi.NoRecordError + return todos + + def setpriority(self, uid, tid, priority): + db = self._getDb(uid) + t = db.get(tid) + t.priority = priority + db.set(tid, t) + + def change(self, uid, tid, replacer): + db = self._getDb(uid) + t = db.get(tid) + t.task = replacer(t.task) + db.set(tid, t) + +class Todo(callbacks.Plugin): + def __init__(self, irc): + self.__parent = super(Todo, self) + self.__parent.__init__(irc) + self.db = FlatTodoDb() + + def die(self): + self.__parent.die() + self.db.close() + + def _shrink(self, s): + return utils.str.ellipsisify(s, 50) + + def todo(self, irc, msg, args, user, taskid): + """[ []|] + + Retrieves a task for the given task id. If no task id is given, it + will return a list of task ids that that user has added to their todo + list. + """ + # List the active tasks for the given user + if not taskid: + try: + tasks = self.db.getTodos(user.id) + utils.sortBy(operator.attrgetter('priority'), tasks) + tasks = [format('#%i: %s', t.id, self._shrink(t.task)) + for t in tasks] + Todo = 'Todo' + if len(tasks) != 1: + Todo = 'Todos' + irc.reply(format('%s for %s: %L', + Todo, user.name, tasks)) + except dbi.NoRecordError: + irc.reply('That user has no todos.') + return + # Reply with the user's task + else: + try: + t = self.db.get(user.id, taskid) + if t.active: + active = 'Active' + else: + active = 'Inactive' + if t.priority: + t.task += format(', priority: %i', t.priority) + at = time.strftime(conf.supybot.reply.format.time(), + time.localtime(t.at)) + s = format('%s todo for %s: %s (Added at %s)', + active, user.name, t.task, at) + irc.reply(s) + except dbi.NoRecordError: + irc.errorInvalid('task id', taskid) + todo = wrap(todo, [first('otherUser', 'user'), additional(('id', 'task'))]) + + def add(self, irc, msg, args, user, optlist, text, now): + """[--priority=] + + Adds as a task in your own personal todo list. The optional + priority argument allows you to set a task as a high or low priority. + Any integer is valid. + """ + priority = 0 + for (option, arg) in optlist: + if option == 'priority': + priority = arg + todoId = self.db.add(priority, now, user.id, text) + irc.replySuccess(format('(Todo #%i added)', todoId)) + add = wrap(add, ['user', getopts({'priority': ('int', 'priority')}), + 'text', 'now']) + + def remove(self, irc, msg, args, user, tasks): + """ [ ...] + + Removes from your personal todo list. + """ + invalid = [] + for taskid in tasks: + try: + self.db.get(user.id, taskid) + except dbi.NoRecordError: + invalid.append(taskid) + if invalid and len(invalid) == 1: + irc.error(format('Task %i could not be removed either because ' + 'that id doesn\'t exist or it has been removed ' + 'already.', invalid[0])) + elif invalid: + irc.error(format('No tasks were removed because the following ' + 'tasks could not be removed: %L.', invalid)) + else: + for taskid in tasks: + self.db.remove(user.id, taskid) + irc.replySuccess() + remove = wrap(remove, ['user', many(('id', 'task'))]) + + def search(self, irc, msg, args, user, optlist, globs): + """[--{regexp} ] [ ...] + + Searches your todos for tasks matching . If --regexp is given, + its associated value is taken as a regexp and matched against the + tasks. + """ + if not optlist and not globs: + raise callbacks.ArgumentError + criteria = [] + for (option, arg) in optlist: + if option == 'regexp': + criteria.append(arg.search) + for glob in globs: + glob = fnmatch.translate(glob) + criteria.append(re.compile(glob[:-1]).search) + try: + tasks = self.db.select(user.id, criteria) + L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks] + irc.reply(format('%L', L)) + except dbi.NoRecordError: + irc.reply('No tasks matched that query.') + search = wrap(search, + ['user', getopts({'regexp': 'regexpMatcher'}), any('glob')]) + + def setpriority(self, irc, msg, args, user, id, priority): + """ + + Sets the priority of the todo with the given id to the specified value. + """ + try: + self.db.setpriority(user.id, id, priority) + irc.replySuccess() + except dbi.NoRecordError: + irc.errorInvalid('task id', id) + setpriority = wrap(setpriority, + ['user', ('id', 'task'), ('int', 'priority')]) + + def change(self, irc, msg, args, user, id, replacer): + """ + + Modify the task with the given id using the supplied regexp. + """ + try: + self.db.change(user.id, id, replacer) + irc.replySuccess() + except dbi.NoRecordError: + irc.errorInvalid('task id', id) + change = wrap(change, ['user', ('id', 'task'), 'regexpReplacer']) + + +Class = Todo + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Todo/test.py b/plugins/Todo/test.py new file mode 100644 index 000000000..fe108a039 --- /dev/null +++ b/plugins/Todo/test.py @@ -0,0 +1,135 @@ +### +# Copyright (c) 2003-2005, Daniel DiPaolo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +from supybot.test import * + +class TodoTestCase(PluginTestCase): + plugins = ('Todo', 'User') + _user1 = 'foo!bar@baz' + _user2 = 'bar!foo@baz' + def setUp(self): + PluginTestCase.setUp(self) + # Create a valid user to use + self.prefix = self._user2 + self.assertNotError('register testy oom') + self.prefix = self._user1 + self.assertNotError('register tester moo') + + def testTodo(self): + # Should not error, but no tasks yet. + self.assertNotError('todo') + self.assertRegexp('todo', 'has no Todos') + # Add a task + self.assertNotError('todo add wash my car') + self.assertRegexp('todo', '#1: wash my car') + # Check that task + self.assertRegexp('todo 1', + 'Todo for tester: wash my car \(Added .*?\)') + # Check that it lists all my tasks when given my name + self.assertResponse('todo tester', + 'Todo for tester: #1: wash my car') + # Check pluralization + self.assertNotError('todo add moo') + self.assertRegexp('todo tester', + 'Todos for tester: #1: wash my car and #2: moo') + # Check error + self.assertError('todo asfas') + self.assertRegexp('todo asfas', + 'Error: \'asfas\' is not a valid task') + # Check priority sorting + self.assertNotError('todo setpriority 1 100') + self.assertNotError('todo setpriority 2 10') + self.assertRegexp('todo', '#2: moo and #1: wash my car') + + def testAddtodo(self): + self.assertNotError('todo add code a new plugin') + self.assertNotError('todo add --priority=1000 fix all bugs') + + def testRemovetodo(self): + self.nick = 'testy' + self.prefix = self._user2 + self.assertNotError('todo add do something') + self.assertNotError('todo add do something else') + self.assertNotError('todo add do something again') + self.assertNotError('todo remove 1') + self.assertNotError('todo 1') + self.nick = 'tester' + self.prefix = self._user1 + self.assertNotError('todo add make something') + self.assertNotError('todo add make something else') + self.assertNotError('todo add make something again') + self.assertNotError('todo remove 1 3') + self.assertRegexp('todo 1', r'Inactive') + self.assertRegexp('todo 3', r'Inactive') + self.assertNotError('todo') + + def testSearchtodo(self): + self.assertNotError('todo add task number one') + self.assertRegexp('todo search task*', '#1: task number one') + self.assertRegexp('todo search number', '#1: task number one') + self.assertNotError('todo add task number two is much longer than' + ' task number one') + self.assertRegexp('todo search task*', + '#1: task number one and #2: task number two is ' + 'much longer than task number...') + self.assertError('todo search --regexp s/bustedregex') + self.assertRegexp('todo search --regexp m/task/', + '#1: task number one and #2: task number two is ' + 'much longer than task number...') + + def testSetPriority(self): + self.assertNotError('todo add --priority=1 moo') + self.assertRegexp('todo 1', + 'moo, priority: 1 \(Added at .*?\)') + self.assertNotError('setpriority 1 50') + self.assertRegexp('todo 1', + 'moo, priority: 50 \(Added at .*?\)') + self.assertNotError('setpriority 1 0') + self.assertRegexp('todo 1', 'moo \(Added at .*?\)') + + def testChangeTodo(self): + self.assertNotError('todo add moo') + self.assertError('todo change 1 asdfas') + self.assertError('todo change 1 m/asdfaf//') + self.assertNotError('todo change 1 s/moo/foo/') + self.assertRegexp('todo 1', 'Todo for tester: foo \(Added .*?\)') + + def testActiveInactiveTodo(self): + self.assertNotError('todo add foo') + self.assertNotError('todo add bar') + self.assertRegexp('todo 1', 'Active') + self.assertRegexp('todo 2', 'Active') + self.assertNotError('todo remove 1') + self.assertRegexp('todo 1', 'Inactive') + self.assertRegexp('todo 2', 'Active') + self.assertNotError('todo remove 2') + self.assertRegexp('todo 2', 'Inactive') + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/setup.py b/setup.py index 0f609557c..32e32b305 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ plugins = [ 'Status', 'String', 'Time', + 'Todo', 'Topic', 'URL', 'User',