diff options
author | Petteri Räty <petsku@petteriraty.eu> | 2011-06-18 18:44:53 +0300 |
---|---|---|
committer | Petteri Räty <petsku@petteriraty.eu> | 2011-06-18 18:44:53 +0300 |
commit | dd32ce1ebac8c9b2fd3a3b715c955f965066e770 (patch) | |
tree | 1f54babdc1e14e79afb434afe4a8e21de89c9cb1 | |
parent | Remove some duplication (diff) | |
parent | Add '#option remove' command to MeetBot (diff) | |
download | council-webapp-dd32ce1ebac8c9b2fd3a3b715c955f965066e770.tar.gz council-webapp-dd32ce1ebac8c9b2fd3a3b715c955f965066e770.tar.bz2 council-webapp-dd32ce1ebac8c9b2fd3a3b715c955f965066e770.zip |
Merge remote-tracking branch 'github/new_bot_commands'
-rw-r--r-- | bot/Reminder/plugin.py | 6 | ||||
-rw-r--r-- | bot/Reminder/run_test.py | 28 | ||||
-rw-r--r-- | bot/ircmeeting/agenda.py | 90 | ||||
-rw-r--r-- | bot/ircmeeting/meeting.py | 11 | ||||
-rw-r--r-- | bot/tests/run_test.py | 159 | ||||
-rw-r--r-- | bot/tests/test_meeting.py | 63 |
6 files changed, 266 insertions, 91 deletions
diff --git a/bot/Reminder/plugin.py b/bot/Reminder/plugin.py index b7005a0..3b04ad3 100644 --- a/bot/Reminder/plugin.py +++ b/bot/Reminder/plugin.py @@ -41,11 +41,11 @@ import json import supybot.ircmsgs as ircmsgs class Reminder(callbacks.Plugin): - def __init__(self, irc): + def __init__(self, irc, sleep = 10): self.__parent = super(Reminder, self) self.__parent.__init__(irc) self.irc = irc - self.sleep = 10 + self.sleep = sleep self.source_url = 'http://localhost:3000/agendas/reminders' self.last_remind_time = time.gmtime(0) self.data = {} @@ -102,9 +102,7 @@ class Reminder(callbacks.Plugin): msg = self.data['message'] - print msg for nick in self.data['users']: - print nick self.irc.sendMsg(ircmsgs.privmsg(str(nick), str(msg))) Class = Reminder diff --git a/bot/Reminder/run_test.py b/bot/Reminder/run_test.py new file mode 100644 index 0000000..73bc394 --- /dev/null +++ b/bot/Reminder/run_test.py @@ -0,0 +1,28 @@ +import unittest +from plugin import Reminder +import urllib +import time + +class FakeIrc: + msgs = [] + + def sendMsg(self, msg): + self.msgs.append(msg) + +def do_nothing(): + pass +class TestSequenceFunctions(unittest.TestCase): + def test_ping_with_newer_stamp(self): + logger = FakeIrc() + testee = Reminder(logger, sleep = 0) + testee.get_data = do_nothing + testee.data = {"users":["nick1","nick2"],"remind_time":u"Wed Jun 08 20:15:04 2011","message":u"Test message"} + time.sleep(1) + assert(len(logger.msgs) == 2) + for i in range(2): + assert(logger.msgs[i].command == 'PRIVMSG') + assert(logger.msgs[i].args[0] == 'nick' + str(i+1)) + assert(logger.msgs[i].args[1] == u"Test message") + +if __name__ == '__main__': + unittest.main() diff --git a/bot/ircmeeting/agenda.py b/bot/ircmeeting/agenda.py index 2269f6f..928ff5f 100644 --- a/bot/ircmeeting/agenda.py +++ b/bot/ircmeeting/agenda.py @@ -1,17 +1,20 @@ import json import urllib +import re class Agenda(object): # Messages + added_option_msg = "You added new voting option: {}" empty_agenda_msg = "Agenda is empty so I can't help you manage meeting (and voting)." current_item_msg = "Current agenda item is {}." + removed_option_msg = "You removed voting option {}: {}" voting_already_open_msg = "Voting is already open. You can end it with #endvote." - voting_open_msg = "Voting started. Your choices are: {} Vote #vote <option number>.\n End voting with #endvote." - voting_close_msg = "Voting is closed." + voting_open_msg = "Voting started. {}Vote #vote <option number>.\nEnd voting with #endvote." + voting_close_msg = "Voting closed." voting_already_closed_msg = "Voting is already closed. You can start it with #startvote." voting_open_so_item_not_changed_msg = "Voting is currently open so I didn't change item. Please #endvote first" - can_not_vote_msg = "You can not vote. Only {} can vote" + can_not_vote_msg = "You can not vote or change agenda. Only {} can." not_a_number_msg = "Your vote was not recognized as a number. Please retry." out_of_range_msg = "Your vote was out of range!" vote_confirm_msg = "You voted for #{} - {}" @@ -27,43 +30,52 @@ class Agenda(object): self.conf = conf def get_agenda_item(self): + if not self.conf.manage_agenda: + return('') if self._current_item < len(self._agenda): return str.format(self.current_item_msg, self._agenda[self._current_item][0]) else: return self.empty_agenda_msg def next_agenda_item(self): + if not self.conf.manage_agenda: + return('') if self._vote_open: - return voting_open_so_item_not_changed_msg + return self.voting_open_so_item_not_changed_msg else: if (self._current_item + 1) < len(self._agenda): self._current_item += 1 return(self.get_agenda_item()) def prev_agenda_item(self): + if not self.conf.manage_agenda: + return('') if self._vote_open: - return voting_open_so_item_not_changed_msg + return self.voting_open_so_item_not_changed_msg else: if self._current_item > 0: self._current_item -= 1 return(self.get_agenda_item()) def start_vote(self): + if not self.conf.manage_agenda: + return('') if self._vote_open: return self.voting_already_open_msg self._vote_open = True - options = "\n" - for i in range(len(self._agenda[self._current_item][1])): - options += str.format("{}. {}\n", i, self._agenda[self._current_item][1][i]) - return str.format(self.voting_open_msg, options) + return str.format(self.voting_open_msg, self.options()) def end_vote(self): + if not self.conf.manage_agenda: + return('') if self._vote_open: self._vote_open = False - return voting_already_closed_msg - return voting_close_msg + return self.voting_close_msg + return self.voting_already_closed_msg def get_data(self): + if not self.conf.manage_agenda: + return('') self._voters = self._get_json(self.conf.voters_url) self._agenda = self._get_json(self.conf.agenda_url) self._votes = { } @@ -71,15 +83,14 @@ class Agenda(object): self._votes[i[0]] = { } def vote(self, nick, line): + if not self.conf.manage_agenda: + return('') if not nick in self._voters: return str.format(self.can_not_vote_msg, ", ".join(self._voters)) - if not line.isdigit(): - return self.not_a_number_msg - opt = int(line) - - if opt < 0 or opt >= len(self._agenda[self._current_item][1]): - return self.out_of_range_msg + opt = self._to_voting_option_number(line) + if opt.__class__ is not int: + return(opt) self._votes[self._agenda[self._current_item][0]][nick] = self._agenda[self._current_item][1][opt] return str.format(self.vote_confirm_msg, opt, self._agenda[self._current_item][1][opt]) @@ -90,7 +101,52 @@ class Agenda(object): result = json.loads(str) return result + def _to_voting_option_number(self, line): + if not line.isdigit(): + return self.not_a_number_msg + opt = int(line) + if opt < 0 or opt >= len(self._agenda[self._current_item][1]): + return self.out_of_range_msg + return(opt) + + def options(self): + options_list = self._agenda[self._current_item][1] + n = len(options_list) + if n == 0: + return 'No voting options available.' + else: + options = "Available voting options are:\n" + for i in range(n): + options += str.format("{}. {}\n", i, options_list[i]) + return options + + def add_option(self, nick, line): + if not self.conf.manage_agenda: + return('') + if not nick in self._voters: + return str.format(self.can_not_vote_msg, ", ".join(self._voters)) + options_list = self._agenda[self._current_item][1] + option_text = re.match( ' *?add (.*)', line).group(1) + options_list.append(option_text) + return str.format(self.added_option_msg, option_text) + + def remove_option(self, nick, line): + if not self.conf.manage_agenda: + return('') + if not nick in self._voters: + return str.format(self.can_not_vote_msg, ", ".join(self._voters)) + + opt_str = re.match( ' *?remove (.*)', line).group(1) + opt = self._to_voting_option_number(opt_str) + if opt.__class__ is not int: + return(opt) + + option = self._agenda[self._current_item][1].pop(opt) + return str.format(self.removed_option_msg, str(opt), option) + def post_result(self): + if not self.conf.manage_agenda: + return('') data = urllib.quote(json.dumps([self._votes])) result_url = str.format(self.conf.result_url, self.conf.voting_results_user, diff --git a/bot/ircmeeting/meeting.py b/bot/ircmeeting/meeting.py index 108ae1d..84949ed 100644 --- a/bot/ircmeeting/meeting.py +++ b/bot/ircmeeting/meeting.py @@ -105,6 +105,7 @@ class Config(object): # Credentials for posting voting results voting_results_user = 'user' voting_results_password = 'password' + manage_agenda = False def enc(self, text): return text.encode(self.output_codec, 'replace') @@ -339,6 +340,16 @@ class MeetingCommands(object): for messageline in self.config.agenda.vote(nick, line).split('\n'): self.reply(messageline) + def do_option(self, nick, time_, line, **kwargs): + if re.match( ' *?list', line): + result = self.config.agenda.options() + elif re.match( ' *?add .*', line): + result = self.config.agenda.add_option(nick, line) + elif re.match( ' *?remove .*', line): + result = self.config.agenda.remove_option(nick, line) + for messageline in result.split('\n'): + self.reply(messageline) + def do_endmeeting(self, nick, time_, **kwargs): """End the meeting.""" if not self.isChair(nick): return diff --git a/bot/tests/run_test.py b/bot/tests/run_test.py index f304a2b..9808ee6 100644 --- a/bot/tests/run_test.py +++ b/bot/tests/run_test.py @@ -7,19 +7,14 @@ import shutil import sys import tempfile import unittest -import time os.environ['MEETBOT_RUNNING_TESTS'] = '1' import ircmeeting.meeting as meeting import ircmeeting.writers as writers -running_tests = True +import test_meeting -def parse_time(time_): - try: return time.strptime(time_, "%H:%M:%S") - except ValueError: pass - try: return time.strptime(time_, "%H:%M") - except ValueError: pass +running_tests = True def process_meeting(contents, extraConfig={}, dontSave=True, filename='/dev/null'): @@ -344,69 +339,93 @@ class MeetBotTest(unittest.TestCase): assert M.config.filename().endswith('somechannel-blah1234'),\ "Filename not as expected: "+M.config.filename() - def test_agenda(self): - """ Test agenda management - """ - logline_re = re.compile(r'\[?([0-9: ]*)\]? *<[@+]?([^>]+)> *(.*)') - loglineAction_re = re.compile(r'\[?([0-9: ]*)\]? *\* *([^ ]+) *(.*)') - - M = process_meeting('#startmeeting') - log = [] - M._sendReply = lambda x: log.append(x) - M.config.agenda._voters = ['x', 'z'] - M.config.agenda._agenda = [['first item', ['opt1', 'opt2']], ['second item', []]] - M.config.agenda._votes = { } - for i in M.config.agenda._agenda: - M.config.agenda._votes[i[0]] = { } - - - M.starttime = time.gmtime(0) - contents = """20:13:50 <x> #nextitem - 20:13:50 <x> #nextitem - 20:13:50 <x> #previtem - 20:13:50 <x> #previtem - 20:13:50 <x> #startvote - 20:13:50 <x> #vote 10 - 20:13:50 <x> #vote 1 - 20:13:50 <y> #vote 0 - 20:13:50 <z> #vote 0 - 20:13:50 <x> #endvote - 20:13:50 <x> #endmeeting""" - - for line in contents.split('\n'): - # match regular spoken lines: - m = logline_re.match(line) - if m: - time_ = parse_time(m.group(1).strip()) - nick = m.group(2).strip() - line = m.group(3).strip() - if M.owner is None: - M.owner = nick ; M.chairs = {nick:True} - M.addline(nick, line, time_=time_) - # match /me lines - m = loglineAction_re.match(line) - if m: - time_ = parse_time(m.group(1).strip()) - nick = m.group(2).strip() - line = m.group(3).strip() - M.addline(nick, "ACTION "+line, time_=time_) - - self.assert_(M.config.agenda._votes == {'first item': {u'x': 'opt2', u'z': 'opt1'}, 'second item': {}}) - - answers = ['Current agenda item is second item.', - 'Current agenda item is second item.', - 'Current agenda item is first item.', - 'Current agenda item is first item.', - 'Voting started. Your choices are: ', - '0. first item', - "1. ['opt1', 'opt2']", - ' Vote #vote <option number>.', - ' End voting with #endvote.', - 'Your vote was out of range!', - "You voted for #1 - ['opt1', 'opt2']", - 'You can not vote. Only x, z can vote', - 'You voted for #0 - first item'] - self.assert_(log[0:len(answers)] == answers) + def get_simple_agenda_test(self): + test = test_meeting.TestMeeting() + test.set_voters(['x', 'z']) + test.set_agenda([['first item', ['opt1', 'opt2']], ['second item', []]]) + test.M.config.manage_agenda = False + + test.answer_should_match("20:13:50 <x> #startmeeting", + "Meeting started .*\nUseful Commands: #action #agreed #help #info #idea #link #topic.\n") + test.M.config.manage_agenda = True + + return(test) + + def test_agenda_item_changing(self): + test = self.get_simple_agenda_test() + + # Test changing item before vote + test.answer_should_match('20:13:50 <x> #nextitem', 'Current agenda item is second item.') + test.answer_should_match('20:13:50 <x> #nextitem', 'Current agenda item is second item.') + test.answer_should_match('20:13:50 <x> #previtem', 'Current agenda item is first item.') + test.answer_should_match('20:13:50 <x> #previtem', 'Current agenda item is first item.') + + # Test changing item during vote + test.process('20:13:50 <x> #startvote') + test.answer_should_match('20:13:50 <x> #nextitem', 'Voting is currently ' +\ + 'open so I didn\'t change item. Please #endvote first') + test.answer_should_match('20:13:50 <x> #previtem', 'Voting is currently ' +\ + 'open so I didn\'t change item. Please #endvote first') + + # Test changing item after vote + test.process('20:13:50 <x> #endvote') + test.answer_should_match('20:13:50 <x> #nextitem', 'Current agenda item is second item.') + test.answer_should_match('20:13:50 <x> #nextitem', 'Current agenda item is second item.') + test.answer_should_match('20:13:50 <x> #previtem', 'Current agenda item is first item.') + test.answer_should_match('20:13:50 <x> #previtem', 'Current agenda item is first item.') + + def test_agenda_option_listing(self): + test = self.get_simple_agenda_test() + + test.answer_should_match('20:13:50 <x> #option list', 'Available voting options ' +\ + 'are:\n0. opt1\n1. opt2\n') + test.process('20:13:50 <x> #nextitem') + test.answer_should_match('20:13:50 <x> #option list', 'No voting options available.') + test.process('20:13:50 <x> #previtem') + test.answer_should_match('20:13:50 <x> #option list', 'Available voting options ' +\ + 'are:\n0. opt1\n1. opt2\n') + + def test_agenda_option_adding(self): + test = self.get_simple_agenda_test() + test.process('20:13:50 <x> #nextitem') + test.answer_should_match('20:13:50 <not_allowed> #option add first option', + 'You can not vote or change agenda. Only x, z can.') + test.answer_should_match('20:13:50 <x> #option add first option', + 'You added new voting option: first option') + test.answer_should_match('20:13:50 <x> #option list', 'Available voting options ' +\ + 'are:\n0. first option') + + def test_agenda_option_removing(self): + test = self.get_simple_agenda_test() + test.answer_should_match('20:13:50 <not_allowed> #option remove 1', + 'You can not vote or change agenda. Only x, z can.') + test.answer_should_match('20:13:50 <x> #option remove 1', + 'You removed voting option 1: opt2') + test.answer_should_match('20:13:50 <x> #option list', 'Available voting options ' +\ + 'are:\n0. opt1') + + def test_agenda_voting(self): + test = self.get_simple_agenda_test() + test.answer_should_match('20:13:50 <x> #startvote', 'Voting started\. ' +\ + 'Available voting options are:\n0. opt1\n1. opt2\nVote ' +\ + '#vote <option number>.\nEnd voting with #endvote.') + test.answer_should_match('20:13:50 <x> #startvote', 'Voting is already open. ' +\ + 'You can end it with #endvote.') + test.answer_should_match('20:13:50 <x> #vote 10', 'Your vote was out of range\!') + test.answer_should_match('20:13:50 <x> #vote 0', 'You voted for #0 - opt1') + test.answer_should_match('20:13:50 <x> #vote 1', 'You voted for #1 - opt2') + test.answer_should_match('20:13:50 <z> #vote 0', 'You voted for #0 - opt1') + test.answer_should_match('20:13:50 <x> #option list', 'Available voting options ' +\ + 'are:\n0. opt1\n1. opt2\n') + test.answer_should_match('20:13:50 <x> #endvote', 'Voting closed.') + test.answer_should_match('20:13:50 <x> #endvote', 'Voting is already closed. ' +\ + 'You can start it with #startvote.') + + test.M.config.manage_agenda = False + test.answer_should_match('20:13:50 <x> #endmeeting', 'Meeting ended ' +\ + '.*\nMinutes:.*\nMinutes \(text\):.*\nLog:.*') + + assert(test.votes() == {'first item': {u'x': 'opt2', u'z': 'opt1'}, 'second item': {}}) if __name__ == '__main__': os.chdir(os.path.join(os.path.dirname(__file__), '.')) diff --git a/bot/tests/test_meeting.py b/bot/tests/test_meeting.py new file mode 100644 index 0000000..238ba2f --- /dev/null +++ b/bot/tests/test_meeting.py @@ -0,0 +1,63 @@ +import ircmeeting.meeting as meeting +import ircmeeting.writers as writers +import re +import time +class TestMeeting: + logline_re = re.compile(r'\[?([0-9: ]*)\]? *<[@+]?([^>]+)> *(.*)') + loglineAction_re = re.compile(r'\[?([0-9: ]*)\]? *\* *([^ ]+) *(.*)') + log = [] + + def __init__(self): + self.M = meeting.process_meeting(contents = '', + channel = "#none", filename = '/dev/null', + dontSave = True, safeMode = False, + extraConfig = {}) + self.M._sendReply = lambda x: self.log.append(x) + self.M.starttime = time.gmtime(0) + + def set_voters(self, voters): + self.M.config.agenda._voters = voters + + def set_agenda(self, agenda): + self.M.config.agenda._agenda = agenda + self.M.config.agenda._votes = { } + for i in agenda: + self.M.config.agenda._votes[i[0]] = { } + + + def parse_time(self, time_): + try: return time.strptime(time_, "%H:%M:%S") + except ValueError: pass + try: return time.strptime(time_, "%H:%M") + except ValueError: pass + + def process(self, content): + for line in content.split('\n'): + # match regular spoken lines: + m = self.logline_re.match(line) + if m: + time_ = self.parse_time(m.group(1).strip()) + nick = m.group(2).strip() + line = m.group(3).strip() + if self.M.owner is None: + self.M.owner = nick ; self.M.chairs = {nick:True} + self.M.addline(nick, line, time_=time_) + # match /me lines + self.m = self.loglineAction_re.match(line) + if m: + time_ = self.parse_time(m.group(1).strip()) + nick = m.group(2).strip() + line = m.group(3).strip() + self.M.addline(nick, "ACTION "+line, time_=time_) + + def answer_should_match(self, line, answer_regexp): + self.log = [] + self.process(line) + answer = '\n'.join(self.log) + error_msg = "Answer for:\n\t'" + line + "'\n was \n\t'" + answer +\ + "'\ndid not match regexp\n\t'" + answer_regexp + "'" + answer_matches = re.match(answer_regexp, answer) + assert answer_matches, error_msg + + def votes(self): + return(self.M.config.agenda._votes) |