diff options
author | 2011-06-25 14:52:19 +0300 | |
---|---|---|
committer | 2011-06-25 14:52:19 +0300 | |
commit | eab5375bf3829daa62fa79f16791bec78ec22257 (patch) | |
tree | 2d20813439080035865a93fd425c7c074243a6ba | |
parent | Add #changeitem <no> command to MeetBot (diff) | |
parent | Bot receives reminders from webapp (diff) | |
download | council-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.py | 56 | ||||
-rw-r--r-- | bot/ircmeeting/meeting.py | 21 | ||||
-rw-r--r-- | bot/tests/run_test.py | 75 | ||||
-rw-r--r-- | site/app/models/agenda.rb | 2 | ||||
-rw-r--r-- | site/app/models/agenda_item.rb | 13 | ||||
-rw-r--r-- | site/db/schema.rb | 3 | ||||
-rw-r--r-- | site/spec/models/agenda_item_spec.rb | 19 | ||||
-rw-r--r-- | site/spec/models/agenda_spec.rb | 17 |
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 |