aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPetteri Räty <petsku@petteriraty.eu>2011-06-25 14:52:19 +0300
committerPetteri Räty <petsku@petteriraty.eu>2011-06-25 14:52:19 +0300
commiteab5375bf3829daa62fa79f16791bec78ec22257 (patch)
tree2d20813439080035865a93fd425c7c074243a6ba
parentAdd #changeitem <no> command to MeetBot (diff)
parentBot receives reminders from webapp (diff)
downloadcouncil-webapp-eab5375bf3829daa62fa79f16791bec78ec22257.tar.gz
council-webapp-eab5375bf3829daa62fa79f16791bec78ec22257.tar.bz2
council-webapp-eab5375bf3829daa62fa79f16791bec78ec22257.zip
Merge remote-tracking branch 'github/time_limit'
Conflicts: bot/tests/run_test.py
-rw-r--r--bot/ircmeeting/agenda.py56
-rw-r--r--bot/ircmeeting/meeting.py21
-rw-r--r--bot/tests/run_test.py75
-rw-r--r--site/app/models/agenda.rb2
-rw-r--r--site/app/models/agenda_item.rb13
-rw-r--r--site/db/schema.rb3
-rw-r--r--site/spec/models/agenda_item_spec.rb19
-rw-r--r--site/spec/models/agenda_spec.rb17
8 files changed, 188 insertions, 18 deletions
diff --git a/bot/ircmeeting/agenda.py b/bot/ircmeeting/agenda.py
index af03c3a..8b9650c 100644
--- a/bot/ircmeeting/agenda.py
+++ b/bot/ircmeeting/agenda.py
@@ -1,7 +1,15 @@
import json
+import threading
import urllib
import re
+class MessageSender:
+ def __init__(self, irc, message):
+ self.irc = irc
+ self.message = message
+ def send_message(self):
+ self.irc.reply(self.message)
+
class Agenda(object):
# Messages
@@ -18,6 +26,10 @@ class Agenda(object):
not_a_number_msg = "Your choice was not recognized as a number. Please retry."
out_of_range_msg = "Your choice was out of range!"
vote_confirm_msg = "You voted for #{} - {}"
+ timelimit_added_msg = 'Added "{}" reminder in {}:{}'
+ timelimit_list_msg = 'Set reminders: "{}"'
+ timelimit_removed_msg = 'Reminder "{}" removed'
+ timelimit_missing_msg = 'No such reminder "{}"'
# Internal
_voters = []
@@ -28,6 +40,7 @@ class Agenda(object):
def __init__(self, conf):
self.conf = conf
+ self.reminders = {}
def get_agenda_item(self):
if not self.conf.manage_agenda:
@@ -37,24 +50,36 @@ class Agenda(object):
else:
return self.empty_agenda_msg
- def next_agenda_item(self):
+ def _swich_agenda_item_to(self, new_item, irc):
+ self._current_item = new_item
+ for reminder in self.reminders.values():
+ reminder.cancel()
+ self.reminders = {}
+ for line in self._agenda[self._current_item][2].split('\n'):
+ match = re.match( '([0-9]+):([0-9]+) (.*)', line)
+ if match:
+ self.add_timelimit(int(match.group(1)), int(match.group(2)),
+ match.group(3), irc)
+ self._agenda[self._current_item][2] = ''
+
+ def next_agenda_item(self, irc):
if not self.conf.manage_agenda:
return('')
if self._vote_open:
return self.voting_open_so_item_not_changed_msg
else:
if (self._current_item + 1) < len(self._agenda):
- self._current_item += 1
+ self._swich_agenda_item_to(self._current_item + 1, irc)
return(self.get_agenda_item())
- def prev_agenda_item(self):
+ def prev_agenda_item(self, irc):
if not self.conf.manage_agenda:
return('')
if self._vote_open:
return self.voting_open_so_item_not_changed_msg
else:
if self._current_item > 0:
- self._current_item -= 1
+ self._swich_agenda_item_to(self._current_item - 1, irc)
return(self.get_agenda_item())
def start_vote(self):
@@ -169,6 +194,29 @@ class Agenda(object):
option = self._agenda[self._current_item][1].pop(opt)
return str.format(self.removed_option_msg, str(opt), option)
+ def add_timelimit(self, minutes, seconds, message, irc):
+ sender = MessageSender(irc, message)
+ reminder = (threading.Timer(60*minutes + seconds, sender.send_message))
+ self.reminders[message] = reminder
+ reminder.start()
+ result = str.format(self.timelimit_added_msg, message, minutes, seconds)
+ return(result)
+
+ def list_timielimits(self):
+ keys = self.reminders.keys()
+ keys_str = '", "'.join(keys)
+ result = str.format(self.timelimit_list_msg, keys_str)
+ return(result)
+
+ def remove_timelimit(self, message):
+ if message in self.reminders:
+ timer = self.reminders.pop(message)
+ timer.cancel()
+ result = str.format(self.timelimit_removed_msg, message)
+ else:
+ result = str.format(self.timelimit_missing_msg, message)
+ return(result)
+
def post_result(self):
if not self.conf.manage_agenda:
return('')
diff --git a/bot/ircmeeting/meeting.py b/bot/ircmeeting/meeting.py
index c01176a..a86c782 100644
--- a/bot/ircmeeting/meeting.py
+++ b/bot/ircmeeting/meeting.py
@@ -33,6 +33,7 @@ import time
import os
import re
import stat
+import threading
import writers
import items
@@ -301,7 +302,6 @@ else:
# Subclass Config and LocalConfig, new type overrides Config.
Config = type('Config', (LocalConfig, Config), {})
-
class MeetingCommands(object):
# Command Definitions
# generic parameters to these functions:
@@ -323,10 +323,25 @@ class MeetingCommands(object):
self.reply(self.config.agenda.get_agenda_item())
def do_nextitem(self, nick, time_, line, **kwargs):
- self.reply(self.config.agenda.next_agenda_item())
+ self.reply(self.config.agenda.next_agenda_item(self))
def do_previtem(self, nick, time_, line, **kwargs):
- self.reply(self.config.agenda.prev_agenda_item())
+ self.reply(self.config.agenda.prev_agenda_item(self))
+
+ def do_timelimit(self, nick, time_, line, **kwargs):
+ reply = 'Usage "#timelimit add <minutes>:<seconds> <message>" or ' +\
+ '"#timelimit list" or "#timelimit remove <message>"'
+ match = re.match( ' *?add ([0-9]+):([0-9]+) (.*)', line)
+ if match:
+ reply = self.config.agenda.add_timelimit(int(match.group(1)),
+ int(match.group(2)), match.group(3), self)
+ elif re.match( ' *?list', line):
+ reply = self.config.agenda.list_timielimits()
+ else:
+ match = re.match( ' *?remove (.*)', line)
+ if(match):
+ reply = self.config.agenda.remove_timelimit(match.group(1))
+ self.reply(reply)
def do_changeitem(self, nick, time_, line, **kwargs):
self.reply(self.config.agenda.change_agenda_item(line))
diff --git a/bot/tests/run_test.py b/bot/tests/run_test.py
index 1358d47..bd116ff 100644
--- a/bot/tests/run_test.py
+++ b/bot/tests/run_test.py
@@ -6,6 +6,8 @@ import re
import shutil
import sys
import tempfile
+import time
+import threading
import unittest
os.environ['MEETBOT_RUNNING_TESTS'] = '1'
@@ -342,7 +344,7 @@ class MeetBotTest(unittest.TestCase):
def get_simple_agenda_test(self):
test = test_meeting.TestMeeting()
test.set_voters(['x', 'z'])
- test.set_agenda([['first item', ['opt1', 'opt2']], ['second item', []], ['third item', []]])
+ test.set_agenda([['first item', ['opt1', 'opt2'], ''], ['second item', [], ''], ['third item', [], '']])
test.M.config.manage_agenda = False
test.answer_should_match("20:13:50 <x> #startmeeting",
@@ -446,6 +448,77 @@ class MeetBotTest(unittest.TestCase):
test.answer_should_match('20:13:50 <x> #vote 0', 'You voted for #0 - opt1')
test.answer_should_match('20:13:50 <z> #vote 0', 'You voted for #0 - opt1. Voting closed.')
+ def test_agenda_time_limit_adding(self):
+ test = self.get_simple_agenda_test()
+ test.answer_should_match('20:13:50 <x> #timelimit', 'Usage "#timelimit ' +\
+ 'add <minutes>:<seconds> <message>" or "' +\
+ '#timelimit list" or "#timelimit remove ' +\
+ '<message>"')
+ test.answer_should_match('20:13:50 <x> #timelimit add 0:1 some other message',
+ 'Added "some other message" reminder in 0:1')
+ test.answer_should_match('20:13:50 <x> #timelimit add 1:0 some message',
+ 'Added "some message" reminder in 1:0')
+ time.sleep(2)
+ last_message = test.log[-1]
+ assert(last_message == 'some other message')
+ reminders = test.M.config.agenda.reminders
+ assert(len(reminders) == 2)
+ for reminder in reminders.values():
+ assert(reminder.__class__ == threading._Timer)
+
+ test.process('20:13:50 <x> #nextitem')
+
+ def test_agenda_time_limit_removing_when_changing_item(self):
+ test = self.get_simple_agenda_test()
+
+ test.process('20:13:50 <x> #timelimit add 0:1 message')
+ assert(len(test.M.config.agenda.reminders) == 1)
+ test.process('20:13:50 <x> #nextitem')
+ assert(len(test.M.config.agenda.reminders) == 0)
+ test.process('20:13:50 <x> #timelimit add 0:1 message')
+ assert(len(test.M.config.agenda.reminders) == 1)
+ test.process('20:13:50 <x> #previtem')
+ assert(len(test.M.config.agenda.reminders) == 0)
+
+ def test_agenda_time_limit_manual_removing(self):
+ test = self.get_simple_agenda_test()
+
+ test.process('20:13:50 <x> #timelimit add 0:1 message')
+ test.process('20:13:50 <x> #timelimit add 0:1 other message')
+ keys = test.M.config.agenda.reminders.keys()
+ keys.sort()
+ assert(keys == ['message', 'other message'])
+
+ test.answer_should_match('20:13:50 <x> #timelimit remove other message', 'Reminder "other message" removed')
+ keys = test.M.config.agenda.reminders.keys()
+ assert(keys == ['message'])
+
+ def test_agenda_time_limit_listing(self):
+ test = self.get_simple_agenda_test()
+ test.process('20:13:50 <x> #timelimit add 0:1 message')
+ test.process('20:13:50 <x> #timelimit add 0:1 other message')
+ test.process('20:13:50 <x> #timelimit add 0:1 yet another message')
+ keys = test.M.config.agenda.reminders.keys()
+ test.answer_should_match('20:13:50 <x> #timelimit list',
+ 'Set reminders: "' + '", "'.join(keys) + '"')
+
+ def test_preset_agenda_time_limits(self):
+ test = self.get_simple_agenda_test()
+ test.M.config.agenda._agenda[0][2] = '1:0 message'
+ test.M.config.agenda._agenda[1][2] = '1:0 another message\n0:10 some other message'
+
+ test.process('20:13:50 <x> #nextitem')
+ keys = test.M.config.agenda.reminders.keys()
+ keys.sort()
+ assert(keys == ['another message', 'some other message'])
+
+ test.process('20:13:50 <x> #previtem')
+ keys = test.M.config.agenda.reminders.keys()
+ keys.sort()
+ assert(keys == ['message'])
+
+ test.process('20:13:50 <x> #nextitem')
+
if __name__ == '__main__':
os.chdir(os.path.join(os.path.dirname(__file__), '.'))
diff --git a/site/app/models/agenda.rb b/site/app/models/agenda.rb
index baaac89..65f45b4 100644
--- a/site/app/models/agenda.rb
+++ b/site/app/models/agenda.rb
@@ -91,7 +91,7 @@ class Agenda < ActiveRecord::Base
def voting_array
agenda_items.collect do |item|
- [item.title, item.voting_options.*.description]
+ [item.title, item.voting_options.*.description, item.timelimits]
end
end
diff --git a/site/app/models/agenda_item.rb b/site/app/models/agenda_item.rb
index f590bb1..0ce60ea 100644
--- a/site/app/models/agenda_item.rb
+++ b/site/app/models/agenda_item.rb
@@ -7,6 +7,7 @@ class AgendaItem < ActiveRecord::Base
discussion :string
body :text
rejected :boolean, :default => false
+ timelimits :text, :null => false, :default => ''
timestamps
end
@@ -14,6 +15,8 @@ class AgendaItem < ActiveRecord::Base
belongs_to :agenda
has_many :voting_options
+ validate :timelimits_entered_properly
+
# --- Permissions --- #
def create_permitted?
return false if acting_user.guest?
@@ -50,4 +53,14 @@ class AgendaItem < ActiveRecord::Base
return false unless agenda.nil?
return acting_user == user if [nil, :title, :discussion, :body].include?(field)
end
+
+ protected
+ def timelimits_entered_properly
+ regexp = /^\d+:\d+( .*)?$/
+ for line in timelimits.split("\n")
+ unless line.match regexp
+ errors.add(:timelimits, "Line '#{line}' doensn't match '<minutes>:<seconds> <message>'")
+ end
+ end
+ end
end
diff --git a/site/db/schema.rb b/site/db/schema.rb
index 071ff84..bc8535a 100644
--- a/site/db/schema.rb
+++ b/site/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20110606170332) do
+ActiveRecord::Schema.define(:version => 20110624141720) do
create_table "agenda_items", :force => true do |t|
t.string "title"
@@ -21,6 +21,7 @@ ActiveRecord::Schema.define(:version => 20110606170332) do
t.datetime "updated_at"
t.integer "user_id"
t.integer "agenda_id"
+ t.text "timelimits", :default => "", :null => false
end
add_index "agenda_items", ["agenda_id"], :name => "index_agenda_items_on_agenda_id"
diff --git a/site/spec/models/agenda_item_spec.rb b/site/spec/models/agenda_item_spec.rb
index 3df2a59..72ee0bb 100644
--- a/site/spec/models/agenda_item_spec.rb
+++ b/site/spec/models/agenda_item_spec.rb
@@ -100,4 +100,23 @@ describe AgendaItem do
a.should_not be_editable_by(u, :agenda)
end
end
+
+ it 'should make sure timelimits are valid' do
+ valid_timelimits = ["", "0:0", "1:1 message", "1:2 longer message",
+ "30:40 a few messages\n5:60 as separate lines"]
+ invalid_timelimits = ["a:0", "1:", "2:a", ":0", " 1:1 message",
+ "30:40 a few messages\n\n5:60 and an empty line",
+ "30:40 a few messages\n5:60 and an wrong line\na:"]
+
+ valid_timelimits.each do |limit|
+ Factory(:agenda_item, :timelimits => limit).should be_valid
+ end
+
+ invalid_timelimits.each do |limit|
+ item = AgendaItem.new :title => 'title', :timelimits => limit
+ item.should_not be_valid
+ item.errors.length.should be_equal(1)
+ item.errors[:timelimits].should_not be_nil
+ end
+ end
end
diff --git a/site/spec/models/agenda_spec.rb b/site/spec/models/agenda_spec.rb
index b9fbd36..34db02f 100644
--- a/site/spec/models/agenda_spec.rb
+++ b/site/spec/models/agenda_spec.rb
@@ -126,9 +126,9 @@ describe Agenda do
Vote.count.should be_equal(9)
- u[0].votes.*.voting_option.*.description.should == ['Yes', 'Yes', 'Dunno']
- u[1].votes.*.voting_option.*.description.should == ['Yes', 'No', 'Dunno']
- u[2].votes.*.voting_option.*.description.should == ['Yes', 'Dunno', 'No']
+ u[0].votes.*.voting_option.*.description.sort.should == ['Dunno', 'Yes', 'Yes']
+ u[1].votes.*.voting_option.*.description.sort.should == ['Dunno', 'No', 'Yes']
+ u[2].votes.*.voting_option.*.description.sort.should == ['Dunno', 'No', 'Yes']
a1.voting_options.*.votes.flatten.*.voting_option.*.description.should == ['Yes', 'Yes', 'Yes']
a2.voting_options.*.votes.flatten.*.voting_option.*.description.should == ['Yes', 'No', 'Dunno']
a3.voting_options.*.votes.flatten.*.voting_option.*.description.should == ['No', 'Dunno', 'Dunno']
@@ -236,15 +236,16 @@ describe Agenda do
it 'should return proper voting_array' do
old_agenda = Factory(:agenda, :state => 'old')
current_agenda = Factory(:agenda)
- i1 = Factory(:agenda_item, :agenda => old_agenda)
- i2 = Factory(:agenda_item, :agenda => current_agenda)
- i3 = Factory(:agenda_item, :agenda => current_agenda)
+ i1 = Factory(:agenda_item, :agenda => old_agenda, :timelimits => '0:0')
+ i2 = Factory(:agenda_item, :agenda => current_agenda, :timelimits => "10:0 Ten minutes passed")
+ i3 = Factory(:agenda_item, :agenda => current_agenda, :timelimits => "0:10 Ten seconds passed")
v11 = Factory(:voting_option, :agenda_item => i1)
v21 = Factory(:voting_option, :agenda_item => i2)
v22 = Factory(:voting_option, :agenda_item => i2, :description => 'other')
- old_agenda.voting_array.should == [[i1.title, [v11.description]]]
- current_agenda.voting_array.should == [[i2.title, [v21.description, v22.description]], [i3.title, []]]
+ old_agenda.voting_array.should == [[i1.title, [v11.description], i1.timelimits]]
+ current_agenda.voting_array.should == [[i2.title, [v21.description, v22.description],
+ i2.timelimits], [i3.title, [], i3.timelimits]]
end
end