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
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+

Subscribers

People subscribed via source and target branches