aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPetteri Räty <petsku@petteriraty.eu>2011-06-18 18:44:53 +0300
committerPetteri Räty <petsku@petteriraty.eu>2011-06-18 18:44:53 +0300
commitdd32ce1ebac8c9b2fd3a3b715c955f965066e770 (patch)
tree1f54babdc1e14e79afb434afe4a8e21de89c9cb1
parentRemove some duplication (diff)
parentAdd '#option remove' command to MeetBot (diff)
downloadcouncil-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.py6
-rw-r--r--bot/Reminder/run_test.py28
-rw-r--r--bot/ircmeeting/agenda.py90
-rw-r--r--bot/ircmeeting/meeting.py11
-rw-r--r--bot/tests/run_test.py159
-rw-r--r--bot/tests/test_meeting.py63
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)