Merge lp:~xavier-antoviaque/ibid/meetingagenda-931774 into lp:ibid
- meetingagenda-931774
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stefano Rivera | Needs Fixing | ||
Jonathan Hitchcock | Approve | ||
Review via email: mp+93247@code.launchpad.net |
Commit message
Description of the change
Also see the test script for a sample use: https:/
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).
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.
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
XavierAntoviaque (xavier-antoviaque) wrote : | # |
Ok : )
Done too, along with the addition of the unit tests.
Stefano Rivera (stefanor) wrote : | # |
The test suite doesn't pass:
=======
[FAIL]
Traceback (most recent call last):
File "/home/
u' \* test 2')
File "/home/
self.fail("No response in matches regex %r" % regex, event)
File "/home/
unittest.
twisted.
{'account': None, 'responses': [{'reply': u"My notes of user's report:", 'address': False, 'target': u'testchan', 'conflate': True, 'source': u'test_
ibid.test.
-------
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/IrcBotServi ce/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] topicsMove to the next agenda topic
[take up] next topic
[open] next topic
move to next topicOpen 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 topicDefer a topic on the agenda
skip [this] topicA sample usage can be found at http://
paste.pocoo. org/show/ 551514/
Preview Diff
1 | === modified file 'AUTHORS' |
2 | --- AUTHORS 2011-03-23 16:11:13 +0000 |
3 | +++ AUTHORS 2012-02-17 17:23:18 +0000 |
4 | @@ -24,4 +24,5 @@ |
5 | * Kevin Woodland |
6 | * Guy Halse |
7 | * Dominic Cleal |
8 | - * Keegan Carruthers-Smith |
9 | \ No newline at end of file |
10 | + * Keegan Carruthers-Smith |
11 | + * Xavier Antoviaque |
12 | \ No newline at end of file |
13 | |
14 | === modified file 'ibid/plugins/meetings.py' |
15 | --- ibid/plugins/meetings.py 2012-02-13 22:04:05 +0000 |
16 | +++ ibid/plugins/meetings.py 2012-02-17 17:23:18 +0000 |
17 | @@ -1,4 +1,4 @@ |
18 | -# Copyright (c) 2009-2010, Stefano Rivera and Max Rabkin |
19 | +# Copyright (c) 2009-2012, Stefano Rivera, Max Rabkin and Xavier Antoviaque |
20 | # Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
21 | |
22 | from datetime import datetime, timedelta |
23 | @@ -9,6 +9,7 @@ |
24 | import re |
25 | from urllib import quote |
26 | from xmlrpclib import ServerProxy |
27 | +from copy import copy |
28 | |
29 | from dateutil.parser import parse |
30 | from dateutil.tz import tzlocal, tzutc |
31 | @@ -33,10 +34,23 @@ |
32 | usage = u""" |
33 | (start | end) meeting [about <title>] |
34 | I am <True Name> |
35 | - topic <topic> |
36 | (agreed | idea | accepted | rejected) <statement> |
37 | minutes so far |
38 | meeting title is <title> |
39 | + topic+ <text> |
40 | + topic+ <text> '['<proposer>']' |
41 | + what's on the agenda? |
42 | + topic- <number> |
43 | + the agenda order is a [, b]... [, m-[n]]... |
44 | + rename topic <number> to <newtext> |
45 | + move to next topic |
46 | + move to topic <number|pattern> |
47 | + what topic are we on? |
48 | + close this topic |
49 | + close topic <number|pattern> |
50 | + skip this topic |
51 | + topic <text> |
52 | + topic <text> '['<proposer>']' |
53 | """ |
54 | features = ('meeting',) |
55 | permission = u'chairmeeting' |
56 | @@ -75,6 +89,7 @@ |
57 | .logging_name(event.channel), |
58 | 'title': title, |
59 | 'attendees': {}, |
60 | + 'agenda': [], |
61 | 'minutes': [{ |
62 | 'time': event.time, |
63 | 'type': 'started', |
64 | @@ -88,6 +103,18 @@ |
65 | event.addresponse(u'gets out his memo-pad and cracks his knuckles', |
66 | action=True) |
67 | |
68 | + def locate_meeting(self, event): |
69 | + """Attempt to find the meeting corresponding to the event""" |
70 | + if not event.public: |
71 | + event.addresponse(u'Sorry, must be done in public') |
72 | + return |
73 | + if (event.source, event.channel) not in meetings: |
74 | + event.addresponse(u'Sorry, no meeting in progress.') |
75 | + return |
76 | + |
77 | + meeting = meetings[(event.source, event.channel)] |
78 | + return meeting |
79 | + |
80 | @match(r'^i\s+am\s+(.+)$') |
81 | def ident(self, event, name): |
82 | if not event.public or (event.source, event.channel) not in meetings: |
83 | @@ -99,14 +126,13 @@ |
84 | event.addresponse(True) |
85 | |
86 | @authorise() |
87 | - @match(r'^(topic|idea|agreed|accepted|rejected)\s+(.+)$') |
88 | + @match(r'^(idea|agreed|accepted|rejected)\s+(.+)$') |
89 | def identify(self, event, action, subject): |
90 | - if not event.public or (event.source, event.channel) not in meetings: |
91 | + meeting = self.locate_meeting(event) |
92 | + if not meeting: |
93 | return |
94 | |
95 | action = action.lower() |
96 | - |
97 | - meeting = meetings[(event.source, event.channel)] |
98 | meeting['minutes'].append({ |
99 | 'time': event.time, |
100 | 'type': action, |
101 | @@ -114,9 +140,7 @@ |
102 | 'nick': event.sender['nick'], |
103 | }) |
104 | |
105 | - if action == 'topic': |
106 | - message = u'Current Topic: %s' |
107 | - elif action == 'idea': |
108 | + if action == 'idea': |
109 | message = u'Idea recorded: %s' |
110 | elif action == 'agreed': |
111 | message = u'Agreed: %s' |
112 | @@ -129,25 +153,296 @@ |
113 | @authorise() |
114 | @match(r'^meeting\s+title\s+is\s+(.+)$') |
115 | def set_title(self, event, title): |
116 | - if not event.public: |
117 | - event.addresponse(u'Sorry, must be done in public') |
118 | - return |
119 | - if (event.source, event.channel) not in meetings: |
120 | - event.addresponse(u'Sorry, no meeting in progress.') |
121 | - return |
122 | - meeting = meetings[(event.source, event.channel)] |
123 | + meeting = self.locate_meeting(event) |
124 | + if not meeting: |
125 | + return |
126 | + |
127 | meeting['title'] = title |
128 | event.addresponse(True) |
129 | |
130 | + @match(r'^(?:agenda|topic)\+\s+([^\[]+)(?:\s+\[(.+)\])?$') |
131 | + def add_agenda_topic(self, event, topic_name, proposer): |
132 | + meeting = self.locate_meeting(event) |
133 | + if not meeting: |
134 | + return |
135 | + |
136 | + meeting['agenda'].append({'name': topic_name, |
137 | + 'state': 'unopened', |
138 | + 'proposer': proposer or event.sender['nick']}) |
139 | + event.addresponse(u'Ok, I added "%(name)s" to the agenda', |
140 | + {'name': topic_name}) |
141 | + |
142 | + @authorise() |
143 | + @match(r'^topic\s+([^\[]+)(?:\s+\[(.+)\])?$') |
144 | + def add_and_open_agenda_topic(self, event, topic_name, proposer): |
145 | + meeting = self.locate_meeting(event) |
146 | + if not meeting: |
147 | + return |
148 | + |
149 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
150 | + if open_topic_nb is None: |
151 | + new_topic_nb = len(meeting['agenda']) |
152 | + else: |
153 | + new_topic_nb = open_topic_nb + 1 |
154 | + |
155 | + meeting['agenda'].insert(new_topic_nb, {'name': topic_name, |
156 | + 'state': 'unopened', |
157 | + 'proposer': proposer or event.sender['nick']}) |
158 | + self.open_agenda_topic(event, str(new_topic_nb + 1)) |
159 | + |
160 | + @match(r'^(?:what(?:\s+is|\'s)\s+(?:on\s+)?(?:the\s+)?agenda\??|what\s+are\s+the\s+topics\??|' |
161 | + r'(?:show|list)\s+(?:the\s+)?(?:agenda|topics)|agenda\??|topics\??)$') |
162 | + def list_agenda(self, event): |
163 | + meeting = self.locate_meeting(event) |
164 | + if not meeting: |
165 | + return |
166 | + |
167 | + if len(meeting['agenda']) < 1: |
168 | + event.addresponse(u'Well, the agenda is empty. Tell me what I ' |
169 | + u'should add there with "topic+ <name>"') |
170 | + return |
171 | + |
172 | + nb = 0 |
173 | + event.addresponse(u'Here is the current agenda:') |
174 | + for topic in meeting['agenda']: |
175 | + nb += 1 |
176 | + |
177 | + if topic['state'] == 'unopened': |
178 | + state = u'' |
179 | + elif topic['state'] == 'open': |
180 | + state = u'(current)' |
181 | + elif topic['state'] == 'closed': |
182 | + state = u'(closed)' |
183 | + |
184 | + event.addresponse(u' %(nb)d. %(name)s %(state)s', |
185 | + {'nb': nb, |
186 | + 'state': state, |
187 | + 'name': topic['name']}) |
188 | + |
189 | + @authorise() |
190 | + @match(r'^rename\s+topic\s+(\d+)\s*to\s*(.+)$') |
191 | + def rename_agenda_topic(self, event, topic_nb, topic_name): |
192 | + meeting = self.locate_meeting(event) |
193 | + if not meeting: |
194 | + return |
195 | + |
196 | + try: |
197 | + topic = meeting['agenda'][int(topic_nb) - 1] |
198 | + except IndexError: |
199 | + event.addresponse(u"Did you count right? I don't see any " |
200 | + u'topic on the agenda with the number %(topic_nb)s', \ |
201 | + {'topic_nb': topic_nb}) |
202 | + return |
203 | + |
204 | + topic_previous_name = topic['name'] |
205 | + topic['name'] = topic_name |
206 | + event.addresponse(u'Ok, I renamed it from "%(previous_name)s" ' |
207 | + u'to "%(new_name)s"', |
208 | + {'previous_name': topic_previous_name, |
209 | + 'new_name': topic_name}) |
210 | + |
211 | + @authorise() |
212 | + @match(r'^(?:(?:agenda-|topic-)|(?:delete|drop|forget|remove)\s+' |
213 | + r'(?:agenda|topic))\s+(\d+)$') |
214 | + def delete_agenda_topic(self, event, topic_nb): |
215 | + meeting = self.locate_meeting(event) |
216 | + if not meeting: |
217 | + return |
218 | + |
219 | + try: |
220 | + topic_name = meeting['agenda'][int(topic_nb) - 1]['name'] |
221 | + del meeting['agenda'][int(topic_nb) - 1] |
222 | + except IndexError: |
223 | + event.addresponse(u'Um, there is no topic %(topic_nb)s', \ |
224 | + {'topic_nb': topic_nb}) |
225 | + return |
226 | + |
227 | + event.addresponse(u'Ok, I removed "%(name)s" from the agenda', |
228 | + {'name': topic_name}) |
229 | + |
230 | + @authorise() |
231 | + @match(r'^(?:please\s+)?clear\s+(?:the\s+)?agenda$') |
232 | + def clear_agenda(self, event): |
233 | + meeting = self.locate_meeting(event) |
234 | + if not meeting: |
235 | + return |
236 | + |
237 | + meeting['agenda'] = [] |
238 | + event.addresponse(u'Better start over from scratch, yes. All neat and clean.') |
239 | + |
240 | + @authorise() |
241 | + @match(r'^(?:the\s+)?(?:agenda|topics|topic)\s+order\s+(?:is\s+)([\d\s,]+)$') |
242 | + def reorder_agenda(self, event, agenda_order_str): |
243 | + meeting = self.locate_meeting(event) |
244 | + if not meeting: |
245 | + return |
246 | + |
247 | + reordered_agenda = [] |
248 | + remaining_agenda = copy(meeting['agenda']) |
249 | + for topic_nb in agenda_order_str.split(','): |
250 | + topic_nb = int(topic_nb.strip()) |
251 | + try: |
252 | + reordered_agenda.append(meeting['agenda'][topic_nb - 1]) |
253 | + remaining_agenda.remove(meeting['agenda'][topic_nb - 1]) |
254 | + except IndexError: |
255 | + event.addresponse(u'Um, there is no topic %(topic_nb)s', \ |
256 | + {'topic_nb': topic_nb}) |
257 | + return |
258 | + reordered_agenda += remaining_agenda |
259 | + meeting['agenda'] = reordered_agenda |
260 | + |
261 | + event.addresponse(u'First things first!') |
262 | + |
263 | + @authorise() |
264 | + @match(r'^(?:(?:take\s+up|open|move\s+to)\s+)?next\s+topic$') |
265 | + def next_agenda_topic(self, event): |
266 | + meeting = self.locate_meeting(event) |
267 | + if not meeting: |
268 | + return |
269 | + |
270 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
271 | + if open_topic_nb is None: |
272 | + open_topic_nb = 0 |
273 | + else: |
274 | + meeting['agenda'][open_topic_nb]['state'] = 'closed' |
275 | + |
276 | + next_topic = None |
277 | + for topic in meeting['agenda'][open_topic_nb:] \ |
278 | + + meeting['agenda'][:open_topic_nb]: |
279 | + if topic['state'] == 'unopened': |
280 | + next_topic = topic |
281 | + break |
282 | + |
283 | + if not next_topic: |
284 | + event.addresponse(u"That's all folks!") |
285 | + else: |
286 | + next_topic_nb = meeting['agenda'].index(next_topic) + 1 |
287 | + self.open_agenda_topic(event, str(next_topic_nb)) |
288 | + |
289 | + @authorise() |
290 | + @match(r'^(?:take\s+up|open|move\s+to)\s+topic\s+(.+)$') |
291 | + def open_agenda_topic(self, event, topic_nb_or_pattern): |
292 | + meeting = self.locate_meeting(event) |
293 | + if not meeting: |
294 | + return |
295 | + |
296 | + next_topic = self.get_agenda_topic_by_nb_or_pattern(event, topic_nb_or_pattern) |
297 | + if not next_topic: |
298 | + return |
299 | + |
300 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
301 | + if open_topic_nb is not None: |
302 | + meeting['agenda'][open_topic_nb]['state'] = 'closed' |
303 | + |
304 | + next_topic['state'] = 'open' |
305 | + meeting['minutes'].append({ |
306 | + 'time': event.time, |
307 | + 'type': 'topic', |
308 | + 'subject': next_topic['name'], |
309 | + 'nick': next_topic['proposer'], |
310 | + }) |
311 | + event.addresponse(u'The current topic is now "%(name)s" (from %(proposer)s)', |
312 | + {'name': next_topic['name'], |
313 | + 'proposer': next_topic['proposer']}) |
314 | + |
315 | + def get_agenda_topic_by_nb_or_pattern(self, event, topic_nb_or_pattern): |
316 | + meeting = self.locate_meeting(event) |
317 | + if not meeting: |
318 | + return |
319 | + |
320 | + try: |
321 | + topic_nb = int(topic_nb_or_pattern.strip()) - 1 |
322 | + found_topic = meeting['agenda'][topic_nb] |
323 | + except ValueError: |
324 | + found_topic = None |
325 | + for topic in meeting['agenda']: |
326 | + if re.search(topic_nb_or_pattern.strip(), topic['name']): |
327 | + found_topic = topic |
328 | + break |
329 | + if found_topic is None: |
330 | + event.addresponse(u"I don't see this topic in the agenda") |
331 | + return found_topic |
332 | + |
333 | + @match(r'^(?:(?:what|current)\s+topic(?:\s+(?:is\s+this|is\s+open|are\s+we\s+on)\s*)?\??|topic\??)$') |
334 | + def current_agenda_topic(self, event): |
335 | + meeting = self.locate_meeting(event) |
336 | + if not meeting: |
337 | + return |
338 | + |
339 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
340 | + if open_topic_nb is None: |
341 | + event.addresponse(u"I don't think we're discussing anything from the agenda") |
342 | + else: |
343 | + current_topic = meeting['agenda'][open_topic_nb] |
344 | + event.addresponse(u'We\'re discussing "%(name)s" (from %(proposer)s)', |
345 | + {'name': current_topic['name'], |
346 | + 'proposer': current_topic['proposer']}) |
347 | + |
348 | + @authorise() |
349 | + @match(r'^close\s+(?:this\s+|the\s+current\s+|current\s+)?topic$') |
350 | + def close_current_agenda_topic(self, event): |
351 | + meeting = self.locate_meeting(event) |
352 | + if not meeting: |
353 | + return |
354 | + |
355 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
356 | + if open_topic_nb is None: |
357 | + event.addresponse(u"I don't think we're discussing anything from the agenda") |
358 | + else: |
359 | + open_topic = meeting['agenda'][open_topic_nb] |
360 | + open_topic['state'] = 'closed' |
361 | + event.addresponse(u"Yes, let's stop talking about %(name)s", |
362 | + {'name': open_topic['name'].lower()}) |
363 | + |
364 | + @authorise() |
365 | + @match(r'^close\s+topic\s+(.+)$') |
366 | + def close_agenda_topic(self, event, topic_nb_or_pattern): |
367 | + meeting = self.locate_meeting(event) |
368 | + if not meeting: |
369 | + return |
370 | + |
371 | + topic = self.get_agenda_topic_by_nb_or_pattern(event, topic_nb_or_pattern) |
372 | + if not topic: |
373 | + return |
374 | + |
375 | + if topic['state'] == 'closed': |
376 | + event.addresponse(u"We've already closed that topic") |
377 | + else: |
378 | + topic['state'] = 'closed' |
379 | + event.addresponse(u"We don't need to talk about %(name)s anymore", |
380 | + {'name': topic['name'].lower()}) |
381 | + |
382 | + @authorise() |
383 | + @match(r'^skip\s+(?:this\s+)?topic$') |
384 | + def skip_current_agenda_topic(self, event): |
385 | + meeting = self.locate_meeting(event) |
386 | + if not meeting: |
387 | + return |
388 | + |
389 | + open_topic_nb = self.get_open_agenda_topic_nb(meeting) |
390 | + if open_topic_nb is None: |
391 | + event.addresponse(u"I don't think we're discussing anything from the agenda") |
392 | + else: |
393 | + open_topic = meeting['agenda'][open_topic_nb] |
394 | + event.addresponse(u"Ok, we'll discuss %(name)s later", |
395 | + {'name': open_topic['name'].lower()}) |
396 | + self.next_agenda_topic(event) |
397 | + open_topic['state'] = 'unopened' |
398 | + |
399 | + def get_open_agenda_topic_nb(self, meeting): |
400 | + nb = 0 |
401 | + for topic in meeting['agenda']: |
402 | + if topic['state'] == 'open': |
403 | + return nb |
404 | + nb += 1 |
405 | + return None |
406 | + |
407 | @match(r'^minutes(?:\s+(?:so\s+far|please))?$') |
408 | def write_minutes(self, event): |
409 | - if not event.public: |
410 | - event.addresponse(u'Sorry, must be done in public') |
411 | - return |
412 | - if (event.source, event.channel) not in meetings: |
413 | - event.addresponse(u'Sorry, no meeting in progress.') |
414 | - return |
415 | - meeting = meetings[(event.source, event.channel)] |
416 | + meeting = self.locate_meeting(event) |
417 | + if not meeting: |
418 | + return |
419 | + |
420 | meeting['attendees'].update((e['nick'], None) for e in meeting['log'] |
421 | if e['nick'] not in meeting['attendees'] |
422 | and e['nick'] != ibid.config['botname']) |
423 | @@ -215,13 +510,9 @@ |
424 | @authorise() |
425 | @match(r'^end\s+meeting$') |
426 | def end_meeting(self, event): |
427 | - if not event.public: |
428 | - event.addresponse(u'Sorry, must be done in public') |
429 | - return |
430 | - if (event.source, event.channel) not in meetings: |
431 | - event.addresponse(u'Sorry, no meeting in progress.') |
432 | - return |
433 | - meeting = meetings[(event.source, event.channel)] |
434 | + meeting = self.locate_meeting(event) |
435 | + if not meeting: |
436 | + return |
437 | |
438 | meeting['endtime'] = event.time |
439 | meeting['log'].append({ |
440 | |
441 | === added file 'ibid/test/plugins/test_meetings.py' |
442 | --- ibid/test/plugins/test_meetings.py 1970-01-01 00:00:00 +0000 |
443 | +++ ibid/test/plugins/test_meetings.py 2012-02-17 17:23:18 +0000 |
444 | @@ -0,0 +1,194 @@ |
445 | +# Copyright \(c) 2010-2011, Max Rabkin, Stefano Rivera |
446 | +# Released under terms of the MIT/X/Expat Licence. See COPYING for details. |
447 | + |
448 | +import logging |
449 | + |
450 | +import ibid.test |
451 | + |
452 | +class MeetingsTest(ibid.test.PluginTestCase): |
453 | + load = ['meetings'] |
454 | + public = True |
455 | + |
456 | + def test_meeting_agenda(self): |
457 | + # Meeting start |
458 | + self.assertResponseMatches(u'Ibid: start meeting', |
459 | + u'gets out his memo-pad and cracks his knuckles') |
460 | + self.assertResponseMatches(u'Ibid: what\'s the agenda?', |
461 | + u'user: Well, the agenda is empty') |
462 | + |
463 | + # Adding topics |
464 | + self.assertResponseMatches(u'Ibid: agenda+ test', |
465 | + u'user: Ok, I added "test" to the agenda') |
466 | + self.assertResponseMatches(u'Ibid: topic+ test 2', |
467 | + u'user: Ok, I added "test 2" to the agenda') |
468 | + self.assertResponseMatches(u'Ibid: what is on the agenda?', |
469 | + u'user: Here is the current agenda:',) |
470 | + self.assertResponseMatches(u'Ibid: what is on the agenda?', |
471 | + u'user: 1. test') |
472 | + self.assertResponseMatches(u'Ibid: what is on the agenda?', |
473 | + u'user: 2. test 2') |
474 | + |
475 | + # Renaming |
476 | + self.assertResponseMatches(u'Ibid: rename topic 2 to test modified 2', |
477 | + u'user: Ok, I renamed it from "test 2" to "test modified 2"') |
478 | + self.assertResponseMatches(u'Ibid: what is on the agenda?', |
479 | + u'user: 1. test') |
480 | + self.assertResponseMatches(u'Ibid: what is on the agenda?', |
481 | + u'user: 2. test modified 2') |
482 | + self.failIfResponseMatches(u'Ibid: what is on the agenda?', |
483 | + u'user: 2. test 2') |
484 | + self.assertResponseMatches(u'Ibid: rename topic 3 to bogus agenda topic', |
485 | + u'user: Did you count right\? I don\'t see any topic on the agenda with the number 3') |
486 | + |
487 | + # Removing topics |
488 | + self.assertResponseMatches(u'Ibid: agenda- 1', |
489 | + u'user: Ok, I removed "test" from the agenda') |
490 | + self.assertResponseMatches(u'Ibid: show the agenda', |
491 | + u'user: 1. test modified 2') |
492 | + self.failIfResponseMatches(u'Ibid: what is on the agenda?', |
493 | + u'user: 1. test$') |
494 | + |
495 | + # Clearing the agenda |
496 | + self.assertResponseMatches(u'Ibid: agenda+ the 3rd item', |
497 | + u'user: Ok, I added "the 3rd item" to the agenda') |
498 | + self.assertResponseMatches(u'Ibid: what is the agenda', |
499 | + u'user: 2. the 3rd item') |
500 | + self.assertResponseMatches(u'Ibid: clear the agenda', |
501 | + u'user: Better start over from scratch, yes. All neat and clean.') |
502 | + self.assertResponseMatches(u'Ibid: what is the agenda', |
503 | + u'user: Well, the agenda is empty.') |
504 | + |
505 | + # Closing topics |
506 | + self.assertResponseMatches(u'Ibid: topic+ test', |
507 | + u'user: Ok, I added "test" to the agenda') |
508 | + self.assertResponseMatches(u'Ibid: topic+ test 2', |
509 | + u'user: Ok, I added "test 2" to the agenda') |
510 | + self.assertResponseMatches(u'Ibid: what are the topics?', |
511 | + u'user: 1. test') |
512 | + self.assertResponseMatches(u'Ibid: what are the topics?', |
513 | + u'user: 2. test 2') |
514 | + self.assertResponseMatches(u'Ibid: topic+ test blah', |
515 | + u'user: Ok, I added "test blah" to the agenda') |
516 | + self.assertResponseMatches(u'Ibid: topic+ test 4 [toto]', |
517 | + u'user: Ok, I added "test 4" to the agenda') |
518 | + self.assertResponseMatches(u'Ibid: topic+ test 5', |
519 | + u'user: Ok, I added "test 5" to the agenda') |
520 | + self.assertResponseMatches(u'Ibid: topic+ test 6', |
521 | + u'user: Ok, I added "test 6" to the agenda') |
522 | + self.assertResponseMatches(u'Ibid: topic+ test seven', |
523 | + u'user: Ok, I added "test seven" to the agenda') |
524 | + self.assertResponseMatches(u'Ibid: close topic 6', |
525 | + u'user: We don\'t need to talk about test 6 anymore') |
526 | + self.assertResponseMatches(u'Ibid: close topic seven', |
527 | + u'user: We don\'t need to talk about test seven anymore') |
528 | + self.assertResponseMatches(u'Ibid: list the topics', |
529 | + u'user: 1. test') |
530 | + self.assertResponseMatches(u'Ibid: list the topics', |
531 | + u'user: 2. test 2') |
532 | + self.assertResponseMatches(u'Ibid: list the topics', |
533 | + u'user: 3. test blah') |
534 | + self.assertResponseMatches(u'Ibid: list the topics', |
535 | + u'user: 4. test 4') |
536 | + self.assertResponseMatches(u'Ibid: list the topics', |
537 | + u'user: 5. test 5') |
538 | + self.assertResponseMatches(u'Ibid: list the topics', |
539 | + u'user: 6. test 6 \(closed\)') |
540 | + self.assertResponseMatches(u'Ibid: list the topics', |
541 | + u'user: 7. test seven \(closed\)') |
542 | + |
543 | + # Changing the agenda order |
544 | + self.assertResponseMatches(u'Ibid: the agenda order is 4, 5 ,3, 1,2', |
545 | + u'user: First things first!') |
546 | + self.assertResponseMatches(u'Ibid: show topics', |
547 | + u'user: 1. test 4') |
548 | + self.assertResponseMatches(u'Ibid: show topics', |
549 | + u'user: 2. test 5') |
550 | + self.assertResponseMatches(u'Ibid: show topics', |
551 | + u'user: 3. test blah') |
552 | + self.assertResponseMatches(u'Ibid: show topics', |
553 | + u'user: 4. test') |
554 | + self.assertResponseMatches(u'Ibid: show topics', |
555 | + u'user: 5. test 2') |
556 | + self.assertResponseMatches(u'Ibid: show topics', |
557 | + u'user: 6. test 6 \(closed\)') |
558 | + self.assertResponseMatches(u'Ibid: show topics', |
559 | + u'user: 7. test seven \(closed\)') |
560 | + |
561 | + # Moving through topics |
562 | + self.assertResponseMatches(u'Ibid: take up topic 4', |
563 | + u'user: The current topic is now "test" \(from user\)') |
564 | + self.assertResponseMatches(u'Ibid: skip this topic', |
565 | + u'user: Ok, we\'ll discuss test later') |
566 | + self.assertResponseMatches(u'Ibid: topics?', |
567 | + u'user: 1. test 4') |
568 | + self.assertResponseMatches(u'Ibid: topics?', |
569 | + u'user: 2. test 5') |
570 | + self.assertResponseMatches(u'Ibid: topics?', |
571 | + u'user: 3. test blah') |
572 | + self.assertResponseMatches(u'Ibid: topics?', |
573 | + u'user: 4. test') |
574 | + self.assertResponseMatches(u'Ibid: topics?', |
575 | + u'user: 5. test 2 \(current\)') |
576 | + self.assertResponseMatches(u'Ibid: topics?', |
577 | + u'user: 6. test 6 \(closed\)') |
578 | + self.assertResponseMatches(u'Ibid: topics?', |
579 | + u'user: 7. test seven \(closed\)') |
580 | + self.assertResponseMatches(u'Ibid: move to topic bl.h', |
581 | + u'user: The current topic is now "test blah" \(from user\)') |
582 | + self.assertResponseMatches(u'Ibid: show the topics', |
583 | + u'user: 1. test 4') |
584 | + self.assertResponseMatches(u'Ibid: show the topics', |
585 | + u'user: 2. test 5') |
586 | + self.assertResponseMatches(u'Ibid: show the topics', |
587 | + u'user: 3. test blah \(current\)') |
588 | + self.assertResponseMatches(u'Ibid: show the topics', |
589 | + u'user: 4. test') |
590 | + self.assertResponseMatches(u'Ibid: show the topics', |
591 | + u'user: 5. test 2 \(closed\)') |
592 | + self.assertResponseMatches(u'Ibid: show the topics', |
593 | + u'user: 6. test 6 \(closed\)') |
594 | + self.assertResponseMatches(u'Ibid: show the topics', |
595 | + u'user: 7. test seven \(closed\)') |
596 | + |
597 | + # Inserting and opening a topic immediately |
598 | + self.assertResponseMatches(u'Ibid: topic A new topic', |
599 | + u'user: The current topic is now "A new topic" \(from user\)') |
600 | + self.assertResponseMatches(u'Ibid: topics?', |
601 | + u'user: 1. test 4') |
602 | + self.assertResponseMatches(u'Ibid: topics?', |
603 | + u'user: 2. test 5') |
604 | + self.assertResponseMatches(u'Ibid: topics?', |
605 | + u'user: 3. test blah \(closed\)') |
606 | + self.assertResponseMatches(u'Ibid: topics?', |
607 | + u'user: 4. A new topic \(current\)') |
608 | + self.assertResponseMatches(u'Ibid: topics?', |
609 | + u'user: 5. test') |
610 | + self.assertResponseMatches(u'Ibid: topics?', |
611 | + u'user: 6. test 2 \(closed\)') |
612 | + self.assertResponseMatches(u'Ibid: topics?', |
613 | + u'user: 7. test 6 \(closed\)') |
614 | + self.assertResponseMatches(u'Ibid: topics?', |
615 | + u'user: 8. test seven \(closed\)') |
616 | + self.assertResponseMatches(u'Ibid: topic?', |
617 | + u'user: We\'re discussing "A new topic" \(from user\)') |
618 | + |
619 | + # Moving throught topics \(closed topics, looping back to skipped topics) |
620 | + self.assertResponseMatches(u'Ibid: open next topic', |
621 | + u'user: The current topic is now "test" \(from user\)') |
622 | + self.assertResponseMatches(u'Ibid: current topic?', |
623 | + u'user: We\'re discussing "test" \(from user\)') |
624 | + self.assertResponseMatches(u'Ibid: close this topic', |
625 | + u'user: Yes, let\'s stop talking about test') |
626 | + self.assertResponseMatches(u'Ibid: move to next topic', |
627 | + u'user: The current topic is now "test 4" \(from toto\)') |
628 | + self.assertResponseMatches(u'Ibid: what topic is open?', |
629 | + u'user: We\'re discussing "test 4" \(from toto\)') |
630 | + self.assertResponseMatches(u'Ibid: next topic', |
631 | + u'user: The current topic is now "test 5" \(from user\)') |
632 | + self.assertResponseMatches(u'Ibid: what topic is this?', |
633 | + u'user: We\'re discussing "test 5" \(from user\)') |
634 | + self.assertResponseMatches(u'Ibid: take up next topic', |
635 | + u'user: That\'s all folks!') |
636 | + self.assertResponseMatches(u'Ibid: what topic are we on?', |
637 | + u'user: I don\'t think we\'re discussing anything from the agenda') |
638 | + |
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/IrcBotServi ce/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>