Merge lp:~xavier-antoviaque/ibid/meetingagenda-931774 into lp:ibid

Proposed by XavierAntoviaque
Status: Needs review
Proposed branch: lp:~xavier-antoviaque/ibid/meetingagenda-931774
Merge into: lp:ibid
Diff against target: 638 lines (+517/-31)
3 files modified
AUTHORS (+2/-1)
ibid/plugins/meetings.py (+321/-30)
ibid/test/plugins/test_meetings.py (+194/-0)
To merge this branch: bzr merge lp:~xavier-antoviaque/ibid/meetingagenda-931774
Reviewer Review Type Date Requested Status
Stefano Rivera Needs Fixing
Jonathan Hitchcock Approve
Review via email: mp+93247@code.launchpad.net

Description of the change

Also see the test script for a sample use: https://bugs.launchpad.net/ibid/+bug/931774/comments/1

To post a comment you must log in.
Revision history for this message
XavierAntoviaque (xavier-antoviaque) wrote :

Btw, I didn't implement some commands, mostly those related to timers. Would be good to discuss a simple way to implement those though, as I understand that the large timer feature planned is a little big. : )

== Missing compared to https://wiki.koumbit.net/IrcBotService/ToDo#Agenda_tracking : ==

If an agendum is currently open it will be closed unless it has been open for less than a minute.
(This reduces confusion if several people simultaneously instruct Zakim-bot to move to the next agendum.)

Time items
    allow this [agenda] item <n> minutes
    allow this agendum <n> minutes

Program a reminder alarm
    [please] ping [me|us] in <interval> [minutes|hours]
    [please] remind [me|us] in <interval> [minutes|hours] [about|to|that <text>]

Start/stop tracking agenda
    [please] track [the] agenda
    [please] ignore [the] agenda

Read agenda from another source
    [please] read agenda from <uri>

Revision history for this message
Jonathan Hitchcock (vhata) wrote :

Nice, comprehensive, and mostly ibiddy (there are a couple of "syntaxy" commands like "agenda-", but they have english equivalents, which is great).

review: Approve
Revision history for this message
XavierAntoviaque (xavier-antoviaque) wrote :

Thanks for the review!

Do want me to change/remove the non-ibiddy commands? Is this only agenda/topic[+/-] or do you see other commands that don't feel right? I can easily come up with more ibiddy alternatives, ie "add topic <xxx>" and "add topic <xxx> from <proposer>" for example.

Revision history for this message
Stefano Rivera (stefanor) wrote :

Oh, please add yourself to AUTHORS

1046. By XavierAntoviaque

Implements #931774 - Agenda tracking for meetings (adds tests)

Adding unit test for the meeting agenda feature, as per the code review.

1047. By XavierAntoviaque

Adding myself to the AUTHORS file, as requested :p

Revision history for this message
XavierAntoviaque (xavier-antoviaque) wrote :

Ok : )

Done too, along with the addition of the unit tests.

Revision history for this message
Stefano Rivera (stefanor) wrote :

The test suite doesn't pass:
===============================================================================
[FAIL]
Traceback (most recent call last):
  File "/home/stefanor/bzr/ibid/xavier-antoviaque/scrummeeting-934376/ibid/test/plugins/test_meetings.py", line 214, in test_meeting_progress_report
    u' \* test 2')
  File "/home/stefanor/bzr/ibid/xavier-antoviaque/scrummeeting-934376/ibid/test/__init__.py", line 206, in assertResponseMatches
    self.fail("No response in matches regex %r" % regex, event)
  File "/home/stefanor/bzr/ibid/xavier-antoviaque/scrummeeting-934376/ibid/test/__init__.py", line 187, in fail
    unittest.TestCase.fail(self, message)
twisted.trial.unittest.FailTest: No response in matches regex u' \\* test 2'
{'account': None, 'responses': [{'reply': u"My notes of user's report:", 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}, {'reply': u' - What user worked on:', 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}, {'reply': u' * the next thing,', 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}, {'reply': u' - What user will be working on next:', 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}, {'reply': u' * test dsf,,test 2', 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}, {'reply': u' * test.test', 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_source_50090256'}], 'channel': u'testchan', 'source': u'test_source_50090256', 'addressed': u'Ibid', 'processed': True, 'time': datetime.datetime(2012, 2, 25, 19, 46, 48, 68327), 'message': {'raw': u'Ibid: show me the report from user', 'deaddressed': u'show me the report from user', 'clean': u'show me the report from user', 'stripped': u'Ibid: show me the report from user'}, 'type': u'message', 'public': True, 'identity': 1, 'sender': {'nick': u'user', 'connection': u'user', 'id': u'user'}}

ibid.test.plugins.test_meetings.MeetingsTest.test_meeting_progress_report
-------------------------------------------------------------------------------

review: Needs Fixing

Unmerged revisions

1047. By XavierAntoviaque

Adding myself to the AUTHORS file, as requested :p

1046. By XavierAntoviaque

Implements #931774 - Agenda tracking for meetings (adds tests)

Adding unit test for the meeting agenda feature, as per the code review.

1045. By XavierAntoviaque

Implements #931774 - Agenda tracking for meetings

* Implementation based on https://wiki.koumbit.net/IrcBotService/ToDo#Agenda_tracking
  For the complete list of supported commands, see below.

* Integrated the "topic <text>" command as part of the agenda tracking
  feature, in a manner which keeps backward compatibility, but allows
  to take advantage of the new features. It can be used alone, the same
  way as before, and will then create agenda topics and open them
  immediately, closing the previous one if any. If used with pre-defined
  agenda items, it will insert the new topic immediately after the current
  one, and move to it.

* Factored the meeting identification based on the current event in a single
  method

== List of implemented commands ==

Add a topic to the agenda
    agenda+ <text>
    topic+ <text>
    agenda+ <text> '['<proposer>']'
    topic+ <text> '['<proposer>']'

Add a topic and open it immediately
    topic <text>
    topic <text> '['<proposer>']'

Display the topics on the agenda
    what is [on] [the] agenda?
    what's [on] [the] agenda?
    list [the] agenda
    show [the] agenda
    agenda?
    what are the topics?
    list [the] topics
    show [the] topics
    topics?

Rename an agenda topic
    rename topic <number> to <newtext>

Drop a topic from the agenda
    agenda- <number>
    topic- <number>
    delete topic <number>
    drop topic <number>
    forget topic <number>
    remove topic <number>

Reorder the agenda
    [the] agenda order [is] a [, b]... [, m-[n]]...
    [the] topic[s] order [is] a [, b]... [, m-[n]]...

Clear the agenda
    [please] clear [the] agenda
    [please] clear [the] topics

Move to the next agenda topic
    [take up] next topic
    [open] next topic
    move to next topic

Open the discussion about an agenda topic
    take up topic <number|pattern>
    open topic <number|pattern>
    move to topic <number|pattern>

What is the current agenda topic?
    what topic [is open]?
    what topic is this?
    what topic are we on?
    current topic?
    topic?

Close an agenda topic
    close topic <number|pattern>
    close this topic
    close [the] current topic

Defer a topic on the agenda
    skip [this] topic

A sample usage can be found at http://paste.pocoo.org/show/551514/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'AUTHORS'
--- AUTHORS 2011-03-23 16:11:13 +0000
+++ AUTHORS 2012-02-17 17:23:18 +0000
@@ -24,4 +24,5 @@
24 * Kevin Woodland24 * Kevin Woodland
25 * Guy Halse25 * Guy Halse
26 * Dominic Cleal26 * Dominic Cleal
27 * Keegan Carruthers-Smith
28\ No newline at end of file27\ No newline at end of file
28 * Keegan Carruthers-Smith
29 * Xavier Antoviaque
29\ No newline at end of file30\ No newline at end of file
3031
=== modified file 'ibid/plugins/meetings.py'
--- ibid/plugins/meetings.py 2012-02-13 22:04:05 +0000
+++ ibid/plugins/meetings.py 2012-02-17 17:23:18 +0000
@@ -1,4 +1,4 @@
1# Copyright (c) 2009-2010, Stefano Rivera and Max Rabkin1# Copyright (c) 2009-2012, Stefano Rivera, Max Rabkin and Xavier Antoviaque
2# Released under terms of the MIT/X/Expat Licence. See COPYING for details.2# Released under terms of the MIT/X/Expat Licence. See COPYING for details.
33
4from datetime import datetime, timedelta4from datetime import datetime, timedelta
@@ -9,6 +9,7 @@
9import re9import re
10from urllib import quote10from urllib import quote
11from xmlrpclib import ServerProxy11from xmlrpclib import ServerProxy
12from copy import copy
1213
13from dateutil.parser import parse14from dateutil.parser import parse
14from dateutil.tz import tzlocal, tzutc15from dateutil.tz import tzlocal, tzutc
@@ -33,10 +34,23 @@
33 usage = u"""34 usage = u"""
34 (start | end) meeting [about <title>]35 (start | end) meeting [about <title>]
35 I am <True Name>36 I am <True Name>
36 topic <topic>
37 (agreed | idea | accepted | rejected) <statement>37 (agreed | idea | accepted | rejected) <statement>
38 minutes so far38 minutes so far
39 meeting title is <title>39 meeting title is <title>
40 topic+ <text>
41 topic+ <text> '['<proposer>']'
42 what's on the agenda?
43 topic- <number>
44 the agenda order is a [, b]... [, m-[n]]...
45 rename topic <number> to <newtext>
46 move to next topic
47 move to topic <number|pattern>
48 what topic are we on?
49 close this topic
50 close topic <number|pattern>
51 skip this topic
52 topic <text>
53 topic <text> '['<proposer>']'
40 """54 """
41 features = ('meeting',)55 features = ('meeting',)
42 permission = u'chairmeeting'56 permission = u'chairmeeting'
@@ -75,6 +89,7 @@
75 .logging_name(event.channel),89 .logging_name(event.channel),
76 'title': title,90 'title': title,
77 'attendees': {},91 'attendees': {},
92 'agenda': [],
78 'minutes': [{93 'minutes': [{
79 'time': event.time,94 'time': event.time,
80 'type': 'started',95 'type': 'started',
@@ -88,6 +103,18 @@
88 event.addresponse(u'gets out his memo-pad and cracks his knuckles',103 event.addresponse(u'gets out his memo-pad and cracks his knuckles',
89 action=True)104 action=True)
90105
106 def locate_meeting(self, event):
107 """Attempt to find the meeting corresponding to the event"""
108 if not event.public:
109 event.addresponse(u'Sorry, must be done in public')
110 return
111 if (event.source, event.channel) not in meetings:
112 event.addresponse(u'Sorry, no meeting in progress.')
113 return
114
115 meeting = meetings[(event.source, event.channel)]
116 return meeting
117
91 @match(r'^i\s+am\s+(.+)$')118 @match(r'^i\s+am\s+(.+)$')
92 def ident(self, event, name):119 def ident(self, event, name):
93 if not event.public or (event.source, event.channel) not in meetings:120 if not event.public or (event.source, event.channel) not in meetings:
@@ -99,14 +126,13 @@
99 event.addresponse(True)126 event.addresponse(True)
100127
101 @authorise()128 @authorise()
102 @match(r'^(topic|idea|agreed|accepted|rejected)\s+(.+)$')129 @match(r'^(idea|agreed|accepted|rejected)\s+(.+)$')
103 def identify(self, event, action, subject):130 def identify(self, event, action, subject):
104 if not event.public or (event.source, event.channel) not in meetings:131 meeting = self.locate_meeting(event)
132 if not meeting:
105 return133 return
106134
107 action = action.lower()135 action = action.lower()
108
109 meeting = meetings[(event.source, event.channel)]
110 meeting['minutes'].append({136 meeting['minutes'].append({
111 'time': event.time,137 'time': event.time,
112 'type': action,138 'type': action,
@@ -114,9 +140,7 @@
114 'nick': event.sender['nick'],140 'nick': event.sender['nick'],
115 })141 })
116142
117 if action == 'topic':143 if action == 'idea':
118 message = u'Current Topic: %s'
119 elif action == 'idea':
120 message = u'Idea recorded: %s'144 message = u'Idea recorded: %s'
121 elif action == 'agreed':145 elif action == 'agreed':
122 message = u'Agreed: %s'146 message = u'Agreed: %s'
@@ -129,25 +153,296 @@
129 @authorise()153 @authorise()
130 @match(r'^meeting\s+title\s+is\s+(.+)$')154 @match(r'^meeting\s+title\s+is\s+(.+)$')
131 def set_title(self, event, title):155 def set_title(self, event, title):
132 if not event.public:156 meeting = self.locate_meeting(event)
133 event.addresponse(u'Sorry, must be done in public')157 if not meeting:
134 return158 return
135 if (event.source, event.channel) not in meetings:159
136 event.addresponse(u'Sorry, no meeting in progress.')
137 return
138 meeting = meetings[(event.source, event.channel)]
139 meeting['title'] = title160 meeting['title'] = title
140 event.addresponse(True)161 event.addresponse(True)
141162
163 @match(r'^(?:agenda|topic)\+\s+([^\[]+)(?:\s+\[(.+)\])?$')
164 def add_agenda_topic(self, event, topic_name, proposer):
165 meeting = self.locate_meeting(event)
166 if not meeting:
167 return
168
169 meeting['agenda'].append({'name': topic_name,
170 'state': 'unopened',
171 'proposer': proposer or event.sender['nick']})
172 event.addresponse(u'Ok, I added "%(name)s" to the agenda',
173 {'name': topic_name})
174
175 @authorise()
176 @match(r'^topic\s+([^\[]+)(?:\s+\[(.+)\])?$')
177 def add_and_open_agenda_topic(self, event, topic_name, proposer):
178 meeting = self.locate_meeting(event)
179 if not meeting:
180 return
181
182 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
183 if open_topic_nb is None:
184 new_topic_nb = len(meeting['agenda'])
185 else:
186 new_topic_nb = open_topic_nb + 1
187
188 meeting['agenda'].insert(new_topic_nb, {'name': topic_name,
189 'state': 'unopened',
190 'proposer': proposer or event.sender['nick']})
191 self.open_agenda_topic(event, str(new_topic_nb + 1))
192
193 @match(r'^(?:what(?:\s+is|\'s)\s+(?:on\s+)?(?:the\s+)?agenda\??|what\s+are\s+the\s+topics\??|'
194 r'(?:show|list)\s+(?:the\s+)?(?:agenda|topics)|agenda\??|topics\??)$')
195 def list_agenda(self, event):
196 meeting = self.locate_meeting(event)
197 if not meeting:
198 return
199
200 if len(meeting['agenda']) < 1:
201 event.addresponse(u'Well, the agenda is empty. Tell me what I '
202 u'should add there with "topic+ <name>"')
203 return
204
205 nb = 0
206 event.addresponse(u'Here is the current agenda:')
207 for topic in meeting['agenda']:
208 nb += 1
209
210 if topic['state'] == 'unopened':
211 state = u''
212 elif topic['state'] == 'open':
213 state = u'(current)'
214 elif topic['state'] == 'closed':
215 state = u'(closed)'
216
217 event.addresponse(u' %(nb)d. %(name)s %(state)s',
218 {'nb': nb,
219 'state': state,
220 'name': topic['name']})
221
222 @authorise()
223 @match(r'^rename\s+topic\s+(\d+)\s*to\s*(.+)$')
224 def rename_agenda_topic(self, event, topic_nb, topic_name):
225 meeting = self.locate_meeting(event)
226 if not meeting:
227 return
228
229 try:
230 topic = meeting['agenda'][int(topic_nb) - 1]
231 except IndexError:
232 event.addresponse(u"Did you count right? I don't see any "
233 u'topic on the agenda with the number %(topic_nb)s', \
234 {'topic_nb': topic_nb})
235 return
236
237 topic_previous_name = topic['name']
238 topic['name'] = topic_name
239 event.addresponse(u'Ok, I renamed it from "%(previous_name)s" '
240 u'to "%(new_name)s"',
241 {'previous_name': topic_previous_name,
242 'new_name': topic_name})
243
244 @authorise()
245 @match(r'^(?:(?:agenda-|topic-)|(?:delete|drop|forget|remove)\s+'
246 r'(?:agenda|topic))\s+(\d+)$')
247 def delete_agenda_topic(self, event, topic_nb):
248 meeting = self.locate_meeting(event)
249 if not meeting:
250 return
251
252 try:
253 topic_name = meeting['agenda'][int(topic_nb) - 1]['name']
254 del meeting['agenda'][int(topic_nb) - 1]
255 except IndexError:
256 event.addresponse(u'Um, there is no topic %(topic_nb)s', \
257 {'topic_nb': topic_nb})
258 return
259
260 event.addresponse(u'Ok, I removed "%(name)s" from the agenda',
261 {'name': topic_name})
262
263 @authorise()
264 @match(r'^(?:please\s+)?clear\s+(?:the\s+)?agenda$')
265 def clear_agenda(self, event):
266 meeting = self.locate_meeting(event)
267 if not meeting:
268 return
269
270 meeting['agenda'] = []
271 event.addresponse(u'Better start over from scratch, yes. All neat and clean.')
272
273 @authorise()
274 @match(r'^(?:the\s+)?(?:agenda|topics|topic)\s+order\s+(?:is\s+)([\d\s,]+)$')
275 def reorder_agenda(self, event, agenda_order_str):
276 meeting = self.locate_meeting(event)
277 if not meeting:
278 return
279
280 reordered_agenda = []
281 remaining_agenda = copy(meeting['agenda'])
282 for topic_nb in agenda_order_str.split(','):
283 topic_nb = int(topic_nb.strip())
284 try:
285 reordered_agenda.append(meeting['agenda'][topic_nb - 1])
286 remaining_agenda.remove(meeting['agenda'][topic_nb - 1])
287 except IndexError:
288 event.addresponse(u'Um, there is no topic %(topic_nb)s', \
289 {'topic_nb': topic_nb})
290 return
291 reordered_agenda += remaining_agenda
292 meeting['agenda'] = reordered_agenda
293
294 event.addresponse(u'First things first!')
295
296 @authorise()
297 @match(r'^(?:(?:take\s+up|open|move\s+to)\s+)?next\s+topic$')
298 def next_agenda_topic(self, event):
299 meeting = self.locate_meeting(event)
300 if not meeting:
301 return
302
303 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
304 if open_topic_nb is None:
305 open_topic_nb = 0
306 else:
307 meeting['agenda'][open_topic_nb]['state'] = 'closed'
308
309 next_topic = None
310 for topic in meeting['agenda'][open_topic_nb:] \
311 + meeting['agenda'][:open_topic_nb]:
312 if topic['state'] == 'unopened':
313 next_topic = topic
314 break
315
316 if not next_topic:
317 event.addresponse(u"That's all folks!")
318 else:
319 next_topic_nb = meeting['agenda'].index(next_topic) + 1
320 self.open_agenda_topic(event, str(next_topic_nb))
321
322 @authorise()
323 @match(r'^(?:take\s+up|open|move\s+to)\s+topic\s+(.+)$')
324 def open_agenda_topic(self, event, topic_nb_or_pattern):
325 meeting = self.locate_meeting(event)
326 if not meeting:
327 return
328
329 next_topic = self.get_agenda_topic_by_nb_or_pattern(event, topic_nb_or_pattern)
330 if not next_topic:
331 return
332
333 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
334 if open_topic_nb is not None:
335 meeting['agenda'][open_topic_nb]['state'] = 'closed'
336
337 next_topic['state'] = 'open'
338 meeting['minutes'].append({
339 'time': event.time,
340 'type': 'topic',
341 'subject': next_topic['name'],
342 'nick': next_topic['proposer'],
343 })
344 event.addresponse(u'The current topic is now "%(name)s" (from %(proposer)s)',
345 {'name': next_topic['name'],
346 'proposer': next_topic['proposer']})
347
348 def get_agenda_topic_by_nb_or_pattern(self, event, topic_nb_or_pattern):
349 meeting = self.locate_meeting(event)
350 if not meeting:
351 return
352
353 try:
354 topic_nb = int(topic_nb_or_pattern.strip()) - 1
355 found_topic = meeting['agenda'][topic_nb]
356 except ValueError:
357 found_topic = None
358 for topic in meeting['agenda']:
359 if re.search(topic_nb_or_pattern.strip(), topic['name']):
360 found_topic = topic
361 break
362 if found_topic is None:
363 event.addresponse(u"I don't see this topic in the agenda")
364 return found_topic
365
366 @match(r'^(?:(?:what|current)\s+topic(?:\s+(?:is\s+this|is\s+open|are\s+we\s+on)\s*)?\??|topic\??)$')
367 def current_agenda_topic(self, event):
368 meeting = self.locate_meeting(event)
369 if not meeting:
370 return
371
372 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
373 if open_topic_nb is None:
374 event.addresponse(u"I don't think we're discussing anything from the agenda")
375 else:
376 current_topic = meeting['agenda'][open_topic_nb]
377 event.addresponse(u'We\'re discussing "%(name)s" (from %(proposer)s)',
378 {'name': current_topic['name'],
379 'proposer': current_topic['proposer']})
380
381 @authorise()
382 @match(r'^close\s+(?:this\s+|the\s+current\s+|current\s+)?topic$')
383 def close_current_agenda_topic(self, event):
384 meeting = self.locate_meeting(event)
385 if not meeting:
386 return
387
388 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
389 if open_topic_nb is None:
390 event.addresponse(u"I don't think we're discussing anything from the agenda")
391 else:
392 open_topic = meeting['agenda'][open_topic_nb]
393 open_topic['state'] = 'closed'
394 event.addresponse(u"Yes, let's stop talking about %(name)s",
395 {'name': open_topic['name'].lower()})
396
397 @authorise()
398 @match(r'^close\s+topic\s+(.+)$')
399 def close_agenda_topic(self, event, topic_nb_or_pattern):
400 meeting = self.locate_meeting(event)
401 if not meeting:
402 return
403
404 topic = self.get_agenda_topic_by_nb_or_pattern(event, topic_nb_or_pattern)
405 if not topic:
406 return
407
408 if topic['state'] == 'closed':
409 event.addresponse(u"We've already closed that topic")
410 else:
411 topic['state'] = 'closed'
412 event.addresponse(u"We don't need to talk about %(name)s anymore",
413 {'name': topic['name'].lower()})
414
415 @authorise()
416 @match(r'^skip\s+(?:this\s+)?topic$')
417 def skip_current_agenda_topic(self, event):
418 meeting = self.locate_meeting(event)
419 if not meeting:
420 return
421
422 open_topic_nb = self.get_open_agenda_topic_nb(meeting)
423 if open_topic_nb is None:
424 event.addresponse(u"I don't think we're discussing anything from the agenda")
425 else:
426 open_topic = meeting['agenda'][open_topic_nb]
427 event.addresponse(u"Ok, we'll discuss %(name)s later",
428 {'name': open_topic['name'].lower()})
429 self.next_agenda_topic(event)
430 open_topic['state'] = 'unopened'
431
432 def get_open_agenda_topic_nb(self, meeting):
433 nb = 0
434 for topic in meeting['agenda']:
435 if topic['state'] == 'open':
436 return nb
437 nb += 1
438 return None
439
142 @match(r'^minutes(?:\s+(?:so\s+far|please))?$')440 @match(r'^minutes(?:\s+(?:so\s+far|please))?$')
143 def write_minutes(self, event):441 def write_minutes(self, event):
144 if not event.public:442 meeting = self.locate_meeting(event)
145 event.addresponse(u'Sorry, must be done in public')443 if not meeting:
146 return444 return
147 if (event.source, event.channel) not in meetings:445
148 event.addresponse(u'Sorry, no meeting in progress.')
149 return
150 meeting = meetings[(event.source, event.channel)]
151 meeting['attendees'].update((e['nick'], None) for e in meeting['log']446 meeting['attendees'].update((e['nick'], None) for e in meeting['log']
152 if e['nick'] not in meeting['attendees']447 if e['nick'] not in meeting['attendees']
153 and e['nick'] != ibid.config['botname'])448 and e['nick'] != ibid.config['botname'])
@@ -215,13 +510,9 @@
215 @authorise()510 @authorise()
216 @match(r'^end\s+meeting$')511 @match(r'^end\s+meeting$')
217 def end_meeting(self, event):512 def end_meeting(self, event):
218 if not event.public:513 meeting = self.locate_meeting(event)
219 event.addresponse(u'Sorry, must be done in public')514 if not meeting:
220 return515 return
221 if (event.source, event.channel) not in meetings:
222 event.addresponse(u'Sorry, no meeting in progress.')
223 return
224 meeting = meetings[(event.source, event.channel)]
225516
226 meeting['endtime'] = event.time517 meeting['endtime'] = event.time
227 meeting['log'].append({518 meeting['log'].append({
228519
=== added file 'ibid/test/plugins/test_meetings.py'
--- ibid/test/plugins/test_meetings.py 1970-01-01 00:00:00 +0000
+++ ibid/test/plugins/test_meetings.py 2012-02-17 17:23:18 +0000
@@ -0,0 +1,194 @@
1# Copyright \(c) 2010-2011, Max Rabkin, Stefano Rivera
2# Released under terms of the MIT/X/Expat Licence. See COPYING for details.
3
4import logging
5
6import ibid.test
7
8class MeetingsTest(ibid.test.PluginTestCase):
9 load = ['meetings']
10 public = True
11
12 def test_meeting_agenda(self):
13 # Meeting start
14 self.assertResponseMatches(u'Ibid: start meeting',
15 u'gets out his memo-pad and cracks his knuckles')
16 self.assertResponseMatches(u'Ibid: what\'s the agenda?',
17 u'user: Well, the agenda is empty')
18
19 # Adding topics
20 self.assertResponseMatches(u'Ibid: agenda+ test',
21 u'user: Ok, I added "test" to the agenda')
22 self.assertResponseMatches(u'Ibid: topic+ test 2',
23 u'user: Ok, I added "test 2" to the agenda')
24 self.assertResponseMatches(u'Ibid: what is on the agenda?',
25 u'user: Here is the current agenda:',)
26 self.assertResponseMatches(u'Ibid: what is on the agenda?',
27 u'user: 1. test')
28 self.assertResponseMatches(u'Ibid: what is on the agenda?',
29 u'user: 2. test 2')
30
31 # Renaming
32 self.assertResponseMatches(u'Ibid: rename topic 2 to test modified 2',
33 u'user: Ok, I renamed it from "test 2" to "test modified 2"')
34 self.assertResponseMatches(u'Ibid: what is on the agenda?',
35 u'user: 1. test')
36 self.assertResponseMatches(u'Ibid: what is on the agenda?',
37 u'user: 2. test modified 2')
38 self.failIfResponseMatches(u'Ibid: what is on the agenda?',
39 u'user: 2. test 2')
40 self.assertResponseMatches(u'Ibid: rename topic 3 to bogus agenda topic',
41 u'user: Did you count right\? I don\'t see any topic on the agenda with the number 3')
42
43 # Removing topics
44 self.assertResponseMatches(u'Ibid: agenda- 1',
45 u'user: Ok, I removed "test" from the agenda')
46 self.assertResponseMatches(u'Ibid: show the agenda',
47 u'user: 1. test modified 2')
48 self.failIfResponseMatches(u'Ibid: what is on the agenda?',
49 u'user: 1. test$')
50
51 # Clearing the agenda
52 self.assertResponseMatches(u'Ibid: agenda+ the 3rd item',
53 u'user: Ok, I added "the 3rd item" to the agenda')
54 self.assertResponseMatches(u'Ibid: what is the agenda',
55 u'user: 2. the 3rd item')
56 self.assertResponseMatches(u'Ibid: clear the agenda',
57 u'user: Better start over from scratch, yes. All neat and clean.')
58 self.assertResponseMatches(u'Ibid: what is the agenda',
59 u'user: Well, the agenda is empty.')
60
61 # Closing topics
62 self.assertResponseMatches(u'Ibid: topic+ test',
63 u'user: Ok, I added "test" to the agenda')
64 self.assertResponseMatches(u'Ibid: topic+ test 2',
65 u'user: Ok, I added "test 2" to the agenda')
66 self.assertResponseMatches(u'Ibid: what are the topics?',
67 u'user: 1. test')
68 self.assertResponseMatches(u'Ibid: what are the topics?',
69 u'user: 2. test 2')
70 self.assertResponseMatches(u'Ibid: topic+ test blah',
71 u'user: Ok, I added "test blah" to the agenda')
72 self.assertResponseMatches(u'Ibid: topic+ test 4 [toto]',
73 u'user: Ok, I added "test 4" to the agenda')
74 self.assertResponseMatches(u'Ibid: topic+ test 5',
75 u'user: Ok, I added "test 5" to the agenda')
76 self.assertResponseMatches(u'Ibid: topic+ test 6',
77 u'user: Ok, I added "test 6" to the agenda')
78 self.assertResponseMatches(u'Ibid: topic+ test seven',
79 u'user: Ok, I added "test seven" to the agenda')
80 self.assertResponseMatches(u'Ibid: close topic 6',
81 u'user: We don\'t need to talk about test 6 anymore')
82 self.assertResponseMatches(u'Ibid: close topic seven',
83 u'user: We don\'t need to talk about test seven anymore')
84 self.assertResponseMatches(u'Ibid: list the topics',
85 u'user: 1. test')
86 self.assertResponseMatches(u'Ibid: list the topics',
87 u'user: 2. test 2')
88 self.assertResponseMatches(u'Ibid: list the topics',
89 u'user: 3. test blah')
90 self.assertResponseMatches(u'Ibid: list the topics',
91 u'user: 4. test 4')
92 self.assertResponseMatches(u'Ibid: list the topics',
93 u'user: 5. test 5')
94 self.assertResponseMatches(u'Ibid: list the topics',
95 u'user: 6. test 6 \(closed\)')
96 self.assertResponseMatches(u'Ibid: list the topics',
97 u'user: 7. test seven \(closed\)')
98
99 # Changing the agenda order
100 self.assertResponseMatches(u'Ibid: the agenda order is 4, 5 ,3, 1,2',
101 u'user: First things first!')
102 self.assertResponseMatches(u'Ibid: show topics',
103 u'user: 1. test 4')
104 self.assertResponseMatches(u'Ibid: show topics',
105 u'user: 2. test 5')
106 self.assertResponseMatches(u'Ibid: show topics',
107 u'user: 3. test blah')
108 self.assertResponseMatches(u'Ibid: show topics',
109 u'user: 4. test')
110 self.assertResponseMatches(u'Ibid: show topics',
111 u'user: 5. test 2')
112 self.assertResponseMatches(u'Ibid: show topics',
113 u'user: 6. test 6 \(closed\)')
114 self.assertResponseMatches(u'Ibid: show topics',
115 u'user: 7. test seven \(closed\)')
116
117 # Moving through topics
118 self.assertResponseMatches(u'Ibid: take up topic 4',
119 u'user: The current topic is now "test" \(from user\)')
120 self.assertResponseMatches(u'Ibid: skip this topic',
121 u'user: Ok, we\'ll discuss test later')
122 self.assertResponseMatches(u'Ibid: topics?',
123 u'user: 1. test 4')
124 self.assertResponseMatches(u'Ibid: topics?',
125 u'user: 2. test 5')
126 self.assertResponseMatches(u'Ibid: topics?',
127 u'user: 3. test blah')
128 self.assertResponseMatches(u'Ibid: topics?',
129 u'user: 4. test')
130 self.assertResponseMatches(u'Ibid: topics?',
131 u'user: 5. test 2 \(current\)')
132 self.assertResponseMatches(u'Ibid: topics?',
133 u'user: 6. test 6 \(closed\)')
134 self.assertResponseMatches(u'Ibid: topics?',
135 u'user: 7. test seven \(closed\)')
136 self.assertResponseMatches(u'Ibid: move to topic bl.h',
137 u'user: The current topic is now "test blah" \(from user\)')
138 self.assertResponseMatches(u'Ibid: show the topics',
139 u'user: 1. test 4')
140 self.assertResponseMatches(u'Ibid: show the topics',
141 u'user: 2. test 5')
142 self.assertResponseMatches(u'Ibid: show the topics',
143 u'user: 3. test blah \(current\)')
144 self.assertResponseMatches(u'Ibid: show the topics',
145 u'user: 4. test')
146 self.assertResponseMatches(u'Ibid: show the topics',
147 u'user: 5. test 2 \(closed\)')
148 self.assertResponseMatches(u'Ibid: show the topics',
149 u'user: 6. test 6 \(closed\)')
150 self.assertResponseMatches(u'Ibid: show the topics',
151 u'user: 7. test seven \(closed\)')
152
153 # Inserting and opening a topic immediately
154 self.assertResponseMatches(u'Ibid: topic A new topic',
155 u'user: The current topic is now "A new topic" \(from user\)')
156 self.assertResponseMatches(u'Ibid: topics?',
157 u'user: 1. test 4')
158 self.assertResponseMatches(u'Ibid: topics?',
159 u'user: 2. test 5')
160 self.assertResponseMatches(u'Ibid: topics?',
161 u'user: 3. test blah \(closed\)')
162 self.assertResponseMatches(u'Ibid: topics?',
163 u'user: 4. A new topic \(current\)')
164 self.assertResponseMatches(u'Ibid: topics?',
165 u'user: 5. test')
166 self.assertResponseMatches(u'Ibid: topics?',
167 u'user: 6. test 2 \(closed\)')
168 self.assertResponseMatches(u'Ibid: topics?',
169 u'user: 7. test 6 \(closed\)')
170 self.assertResponseMatches(u'Ibid: topics?',
171 u'user: 8. test seven \(closed\)')
172 self.assertResponseMatches(u'Ibid: topic?',
173 u'user: We\'re discussing "A new topic" \(from user\)')
174
175 # Moving throught topics \(closed topics, looping back to skipped topics)
176 self.assertResponseMatches(u'Ibid: open next topic',
177 u'user: The current topic is now "test" \(from user\)')
178 self.assertResponseMatches(u'Ibid: current topic?',
179 u'user: We\'re discussing "test" \(from user\)')
180 self.assertResponseMatches(u'Ibid: close this topic',
181 u'user: Yes, let\'s stop talking about test')
182 self.assertResponseMatches(u'Ibid: move to next topic',
183 u'user: The current topic is now "test 4" \(from toto\)')
184 self.assertResponseMatches(u'Ibid: what topic is open?',
185 u'user: We\'re discussing "test 4" \(from toto\)')
186 self.assertResponseMatches(u'Ibid: next topic',
187 u'user: The current topic is now "test 5" \(from user\)')
188 self.assertResponseMatches(u'Ibid: what topic is this?',
189 u'user: We\'re discussing "test 5" \(from user\)')
190 self.assertResponseMatches(u'Ibid: take up next topic',
191 u'user: That\'s all folks!')
192 self.assertResponseMatches(u'Ibid: what topic are we on?',
193 u'user: I don\'t think we\'re discussing anything from the agenda')
194

Subscribers

People subscribed via source and target branches