cicloid-conversational 0.3.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +21 -0
- data/README.markdown +240 -0
- data/Rakefile +2 -0
- data/conversational.gemspec +22 -0
- data/features/configure_blank_topic.feature +9 -0
- data/features/configure_exclusion_conversations.feature +20 -0
- data/features/configure_unknown_topic.feature +9 -0
- data/features/find_existing_conversation.feature +21 -0
- data/features/find_or_create_with.feature +33 -0
- data/features/retrieve_conversation_details.feature +13 -0
- data/features/step_definitions/conversation_steps.rb +60 -0
- data/features/step_definitions/pickle_steps.rb +87 -0
- data/features/support/conversation.rb +2 -0
- data/features/support/email_spec.rb +1 -0
- data/features/support/env.rb +58 -0
- data/features/support/mail.rb +6 -0
- data/features/support/paths.rb +33 -0
- data/features/support/pickle.rb +24 -0
- data/features/support/sample_conversation.rb +3 -0
- data/lib/conversational.rb +3 -0
- data/lib/conversational/active_record_additions.rb +122 -0
- data/lib/conversational/conversation.rb +285 -0
- data/lib/conversational/version.rb +4 -0
- data/lib/generators/conversational/migration/USAGE +5 -0
- data/lib/generators/conversational/migration/migration_generator.rb +23 -0
- data/lib/generators/conversational/migration/templates/migration.rb +14 -0
- data/lib/generators/conversational/skeleton/USAGE +6 -0
- data/lib/generators/conversational/skeleton/skeleton_generator.rb +17 -0
- data/lib/generators/conversational/skeleton/templates/conversation.rb +3 -0
- data/spec/active_record_additions_spec.rb +120 -0
- data/spec/conversation_definition_spec.rb +248 -0
- data/spec/conversation_spec.rb +34 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/conversation.rb +3 -0
- metadata +113 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2010 David Wilkie
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/README.markdown
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
# Conversational
|
2
|
+
|
3
|
+
Conversational makes it easier to accept incoming text messages (SMS) and respond to them in your application. You could also use Conversational to respond to incoming email or IM.
|
4
|
+
|
5
|
+
Conversational allows you to have have *stateful* or *stateless* interactions with your users.
|
6
|
+
|
7
|
+
## Stateless Conversations
|
8
|
+
|
9
|
+
This is the most common way of dealing with incoming messages in SMS apps. Stateless conversations don't know anything about a previous request and therefore require you to text in an explicit command. Let's look at an example:
|
10
|
+
|
11
|
+
Say you have an application which allows you to interact with Facebook over SMS. You want be able to write on on your friends wall and change your status by sending an SMS to your app. You also want to receive SMS alerts when a friend writes on your wall or you have a new friend request.
|
12
|
+
|
13
|
+
Let's tackle the incoming messages first.
|
14
|
+
|
15
|
+
### Incoming Text Messages
|
16
|
+
|
17
|
+
Say your commands are:
|
18
|
+
|
19
|
+
* us is ready for a beer
|
20
|
+
* wow johnny wanna go 4 a beer?
|
21
|
+
|
22
|
+
Where "us" is update status and "wow" is write on wall
|
23
|
+
|
24
|
+
Assuming you have set up a controller in your application to accept the incoming text messages when they are posted from your SMS gateway you could use Conversational as follows:
|
25
|
+
|
26
|
+
class Conversation
|
27
|
+
include Conversational::Conversation
|
28
|
+
converse do |with, message|
|
29
|
+
OutgoingTextMessage.send(with, message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class UsConversation < Conversation
|
34
|
+
def move_along(message)
|
35
|
+
# code to update status
|
36
|
+
say "Successfully updated your status"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class WowConversation < Conversation
|
41
|
+
def move_along(message)
|
42
|
+
# Code to write on wall
|
43
|
+
say "Successfully wrote on wall"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class IncomingTextMessageController
|
48
|
+
def create
|
49
|
+
message = params[:message]
|
50
|
+
topic = params[:message].split(" ").first
|
51
|
+
number = params[:number]
|
52
|
+
Conversation.new(:with => number, :topic => topic).details.move_along(message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
There's quite a bit going on here so let's have a bit more of a look.
|
57
|
+
|
58
|
+
In the controller a new Conversation is created with the number of the incoming message and the topic as the first word of the text message. In our case the topic will be either "us" or "wow". Calling `details` on an instance of Conversation will try and return an instance of a class in your project that subclasses your main Conversation class and has the same name as the topic. In our case our main Conversation class *is* called Conversation so a topic of "us" will map to `UsConversation`. Similarly "wow" maps to `WowConversation`. So say we text in "us is ready for a beer" an instance of `UsConversation` is returned and move_along is then called on the instance.
|
59
|
+
|
60
|
+
Inside our subclassed conversations there is a method available to us called `say`. `say` executes the converse block you set up inside your main Conversation class.In our case this will call `OutgoingTextMessage.send` passing in the number and the message. Obviously this example doesn't contain any error checking. If we text in something starting with other than "us" or "wow" we'll get an error because `details` will return `nil` and `move_along` will be called on `nil`
|
61
|
+
|
62
|
+
### Outgoing Text Messages
|
63
|
+
|
64
|
+
To handle your Facebook alerts you can simply make use of Conversation's `say` method: You might do something like this:
|
65
|
+
|
66
|
+
class FacebookAlert < Conversation
|
67
|
+
def wrote_on_wall(facebook_notification)
|
68
|
+
# Code to get the wall details
|
69
|
+
say "Someone wrote on your wall..."
|
70
|
+
end
|
71
|
+
|
72
|
+
def friend_request(facebook_notification)
|
73
|
+
# Code to get the name of the person who befriended you
|
74
|
+
say "You have a new friend request from ..."
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Then when you get a facebook alert simply do either:
|
79
|
+
|
80
|
+
FacebookAlert.new(:with => "your number").wrote_on_wall(facebook_notification)
|
81
|
+
FacebookAlert.new(:with => "your number").friend_request(facebook_notification)
|
82
|
+
|
83
|
+
## Stateful Conversations
|
84
|
+
|
85
|
+
Conversational also allows you to have *stateful* conversations. A stateful conversation knows about prevous interactions and therefore allows you to respond differently. Note this is currently only supported in Rails.
|
86
|
+
|
87
|
+
Let's build on the prevous example using stateful conversations.
|
88
|
+
|
89
|
+
Our application so far is *stateless*. Currently if we get a friend request we have no way of accepting or rejecting it. If we were to continue building a stateless application we would simply add a couple of new commands such as:
|
90
|
+
|
91
|
+
* "afr <friend>"
|
92
|
+
* "rfr <friend>"
|
93
|
+
where "afr" is accept friend request and "rfr" is reject friend request.
|
94
|
+
|
95
|
+
But instead of doing that let's allow us to respond to a friend request notification with yes or no.
|
96
|
+
|
97
|
+
So we'll change our existing friend request alert to: "You have a new friend request from Johnnie Cash. Do you want to accept? Text yes or no."
|
98
|
+
|
99
|
+
With the current stateless implementation if they text "yes" then `details` will look to see if `YesConversation` is defined in our project, won't be able to find and return `nil` So here's how to make our application stateful.
|
100
|
+
|
101
|
+
class Conversation < ActiveRecord::Base
|
102
|
+
include Conversational::Conversation
|
103
|
+
converse do |with, message|
|
104
|
+
OutgoingTextMessage.send(with, message)
|
105
|
+
end
|
106
|
+
protected
|
107
|
+
def finish
|
108
|
+
state = "finished"
|
109
|
+
self.save!
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class IncomingTextMessageController
|
114
|
+
def create
|
115
|
+
message = params[:message]
|
116
|
+
topic = params[:message].split(" ").first
|
117
|
+
number = params[:number]
|
118
|
+
Conversation.find_or_create_with(number, topic).move_along(message)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class FacebookAlertConversation < Conversation
|
123
|
+
def move_along(message)
|
124
|
+
if message == "yes"
|
125
|
+
# code to accept friend request
|
126
|
+
say "You successfully accepted the friend request"
|
127
|
+
elsif message == "no"
|
128
|
+
# say "You successfully rejected the friend request"
|
129
|
+
else
|
130
|
+
say "Invalid response. Reply with yes or no"
|
131
|
+
end
|
132
|
+
finish
|
133
|
+
end
|
134
|
+
|
135
|
+
def friend_request(facebook_notification)
|
136
|
+
# Code to get the name of the person who befriended you
|
137
|
+
say "You have a new friend request from ...Do you want to accept? Text yes or no."
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
The first change you'll notice is that our Conversation base class now extends from ActiveRecord::Base. This does a couple of things. But first we will need a migration file (which can be generated with `rails g conversational:migration`). Once we have that we can simply migrate our database `rake db:migrate`. Extending from ActiveRecord::Base adds a couple of methods for us which i'll describe in some more detail later. For our example we only care about one of them.
|
142
|
+
|
143
|
+
Jumping over to our controller you can see that now we are calling `Conversation.find_or_create_with`. This method was added when we extended Conversation from ActiveRecord::Base. The method tries to return the last *recent*, *open* conversation with this number. By *open* we mean that it's state is *not* finished and by *recent* we mean that it was updated within the last 24 hours. More on how you can override this later. If it finds one it will return an instance of this conversation subclass. If it doesn't it will create a new conversation with the topic specified and return it as an instance of its subclass (just like `details`).
|
144
|
+
|
145
|
+
Now take a look at our `FacebookAlert` class. The first thing is that we renamed it to `FacebookAlertConversation`. This is important so that it is now recognised as a type of conversation.
|
146
|
+
|
147
|
+
There is also a new method `move_along` which looks at the message to see if the user replied with "yes" or "no" and responds appropriately. Notice it also calls `finish`.
|
148
|
+
|
149
|
+
If we jump back and take a look at our main Conversation class we see that `finish` marks the conversation state as finished so it won't be found by `find_or_create_with`. It is important that you remember to call `finish` on all conversations where you don't expect a response.
|
150
|
+
|
151
|
+
So how does this all tie together?
|
152
|
+
|
153
|
+
The application receives an alert from Facebook with a new friend request and calls:
|
154
|
+
|
155
|
+
FacebookAlert.create!(
|
156
|
+
:with => "your number", :topic => "facebook_alert"
|
157
|
+
).friend_request(facebook_notification)
|
158
|
+
|
159
|
+
This sends the following message to you: "You have a new friend request from...Do you want to accept? Text yes or no."
|
160
|
+
|
161
|
+
Notice that it calls `create!` and *not* `new` as we want to save this conversation. Also notice that topic is set to "facebook_alert" which is the name of the class minus "Conversation". This is important so `find_or_create_with` can find the conversation. Also notice that `friend_request` does *not* call finish. This conversation isn't over yet!
|
162
|
+
|
163
|
+
Now sometime later you reply with "yes". The controller calls
|
164
|
+
`Conversation.find_or_create_with` which finds your open conversation and returns an instance as a `FacebookAlertConversation`.
|
165
|
+
|
166
|
+
The controller then calls `move_along` on this conversation which looks at your message, sees that you replied with "yes" and replies with "You successfully accepted the friend request". It also calls `finish` which marks the conversation as finished, so that next time you text something in it won't find any open conversations.
|
167
|
+
|
168
|
+
There are still a few more things we need to do in order to make this application work properly. Right now if we text in something other than "us", "wow" or "yes" we will still get an exception. Let's fix it in the following section.
|
169
|
+
|
170
|
+
## Configuration
|
171
|
+
|
172
|
+
In our example application we are responding to requests based on the first word of the incoming text message. But what if we text in something unknown or something blank?
|
173
|
+
|
174
|
+
You can configure Conversation to deal with this situation as follows:
|
175
|
+
|
176
|
+
class Conversation
|
177
|
+
unknown_topic_subclass = UnknownTopicNotification
|
178
|
+
blank_topic_subclass = BlankTopicNotification
|
179
|
+
end
|
180
|
+
|
181
|
+
class UnknownTopicNotification < Conversation
|
182
|
+
def move_along
|
183
|
+
say "Sorry. Unknown Command"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
class BlankTopicNotification < Conversation
|
188
|
+
def move_along
|
189
|
+
say "Hey. What do you want?"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
Now when we text in "hey jonnie", `details` will try and find a conversation defined as `HeyConversation`, won't be able to find it and will return an instance of `UnknownTopicConversation` instead.
|
194
|
+
|
195
|
+
`move_along` then causes a message to be sent saying "Sorry. Unknown Command."
|
196
|
+
|
197
|
+
The same thing happens for a blank conversation.
|
198
|
+
|
199
|
+
There is one more subtle issue with our application. What if we text in "facebook_alert"? The reply will be: "Invalid response. Reply with yes or no" when it should actually be "Sorry. Unknown Command". This is because if `find_or_create_with` cannot find an existing conversation it will try and create one with the topic "facebook_alert" if `FacebookAlertConversation` is defined in our application (which it is). To solve this problem we can use `exclude`.
|
200
|
+
|
201
|
+
class Conversation
|
202
|
+
exclude FacebookAlertConversation
|
203
|
+
end
|
204
|
+
|
205
|
+
Now we'll get "Sorry. Unknown Command."
|
206
|
+
|
207
|
+
## Overriding Defaults
|
208
|
+
|
209
|
+
When you extend your base conversation class from ActiveRecord::Base in addition to `find_or_create_with` you will also get the following class methods:
|
210
|
+
|
211
|
+
`converser("someone")` returns all conversations with "someone"
|
212
|
+
|
213
|
+
`in_progress` returns all conversations that are not "finished".
|
214
|
+
|
215
|
+
`recent(time)` returns all conversations in the last *time* or within the last 24 if *time* is not suppied
|
216
|
+
|
217
|
+
`with("someone")` a convienience method for `converse("someone").in_progress.recent`
|
218
|
+
|
219
|
+
|
220
|
+
## Installation
|
221
|
+
|
222
|
+
Add the following to your Gemfile: `gem "conversational"`
|
223
|
+
|
224
|
+
## Setup
|
225
|
+
|
226
|
+
`rails g conversational:skeleton`
|
227
|
+
Generates a base conversation class under app/conversations
|
228
|
+
|
229
|
+
`rails g conversational:migration`
|
230
|
+
Generates a migration file if you want to use Conversational with Rails
|
231
|
+
|
232
|
+
## More Examples
|
233
|
+
|
234
|
+
Here's an [example](http://github.com/dwilkie/drinking) *stateful* conversation app about drinking
|
235
|
+
|
236
|
+
## Notes
|
237
|
+
|
238
|
+
Conversational is only compatible with Rails 3 however you do not need Rails if you are not using stateful conversations.
|
239
|
+
|
240
|
+
Copyright (c) 2010 David Wilkie, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "conversational/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cicloid-conversational"
|
7
|
+
s.version = Conversational::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["David Wilkie", "Gustavo Barrón"]
|
10
|
+
s.email = ["dwilkie@gmail.com", "cicloid@42fu.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Have conversations based on topic}
|
13
|
+
s.description = %q{Have stateful or stateless conversations based a topic}
|
14
|
+
|
15
|
+
s.rubyforge_project = "conversational"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Feature: Specify a blank conversation topic template
|
2
|
+
In order to converse when the topic is blank
|
3
|
+
As a Conversation user
|
4
|
+
I want to be able to specify a conversation template to use when the topic is blank
|
5
|
+
|
6
|
+
Scenario: Specify a blank conversation topic
|
7
|
+
Given I configured Conversation with the following: Conversation.blank_topic_subclass = SampleConversation
|
8
|
+
When I start up a conversation with an blank topic
|
9
|
+
Then the conversation details should be a SampleConversation
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Configure exclusion conversations
|
2
|
+
In order to allow conversations to exist in a project that should not be found
|
3
|
+
when using Conversation.details or Conversation.find_or_create_with
|
4
|
+
As a Conversation user
|
5
|
+
I want to be able to configure which conversations should be excluded
|
6
|
+
|
7
|
+
Scenario Outline: Configure exclusion conversations
|
8
|
+
Given I configured Conversation with the following: <configuration>
|
9
|
+
And a conversation exists with topic: "sample", with: "someone@example.com"
|
10
|
+
Then the conversation details should be nil
|
11
|
+
|
12
|
+
Examples:
|
13
|
+
| configuration |
|
14
|
+
| Conversation.exclude [SampleConversation] |
|
15
|
+
| Conversation.exclude SampleConversation |
|
16
|
+
| Conversation.exclude "sample_conversation" |
|
17
|
+
| Conversation.exclude "SampleConversation" |
|
18
|
+
| Conversation.exclude :sample_conversation |
|
19
|
+
| Conversation.exclude /sample/i |
|
20
|
+
| Conversation.exclude [/sample/, "sample_conversation", SampleConversation] |
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Feature: Specify an unknown conversation topic template
|
2
|
+
In order to converse when the topic is unknown
|
3
|
+
As a Conversation user
|
4
|
+
I want to be able to specify a conversation template to use when the topic is unknown
|
5
|
+
|
6
|
+
Scenario: Specify an unknown conversation topic
|
7
|
+
Given I configured Conversation with the following: Conversation.unknown_topic_subclass = SampleConversation
|
8
|
+
When I start up a conversation with an unknown topic
|
9
|
+
Then the conversation details should be a SampleConversation
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Feature: Find existing conversations
|
2
|
+
In order to find an active conversation with a user
|
3
|
+
As a Conversation user
|
4
|
+
I want to be able to find the conversation easily
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given a conversation exists with topic: "sample", with: "someone@example.com"
|
8
|
+
|
9
|
+
Scenario: Find an open conversation updated within the last 24 hours
|
10
|
+
Given 23 hours have elapsed since the conversation was last updated
|
11
|
+
Then I should be able to find a conversation with: "someone@example.com"
|
12
|
+
|
13
|
+
Scenario: Find an open conversation updated more than that 24 hours
|
14
|
+
Given 24 hours have elapsed since the conversation was last updated
|
15
|
+
Then I should not be able to find a conversation with: "someone@example.com"
|
16
|
+
|
17
|
+
Scenario: Find a finished conversation
|
18
|
+
Given 1 hour has elapsed since the conversation was last updated
|
19
|
+
And the conversation is finished
|
20
|
+
Then I should not be able to find a conversation with: "someone@example.com"
|
21
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Feature: Find existing conversation or create a new one
|
2
|
+
In order to easily create a new conversation if an existing one cannot be found
|
3
|
+
As a Conversation user
|
4
|
+
I want to be able to call find_or_create_with supplying who with and the topic
|
5
|
+
|
6
|
+
Scenario: No conversations exist
|
7
|
+
Given no conversations exist with: "someone"
|
8
|
+
When I call find_or_create_with("someone", "sample")
|
9
|
+
Then a conversation should exist with topic: "sample", with: "someone"
|
10
|
+
|
11
|
+
Scenario: A recent conversation exists
|
12
|
+
Given a conversation exists with topic: "sample", with: "someone"
|
13
|
+
When I call find_or_create_with("someone", "sample")
|
14
|
+
Then 1 conversations should exist
|
15
|
+
|
16
|
+
Scenario: An old conversation exists
|
17
|
+
Given a conversation exists with topic: "sample", with: "someone"
|
18
|
+
And 24 hours have elapsed since the conversation was last updated
|
19
|
+
When I call find_or_create_with("someone", "sample")
|
20
|
+
Then 2 conversations should exist
|
21
|
+
|
22
|
+
Scenario: An finished conversation exists
|
23
|
+
Given a conversation exists with topic: "sample", with: "someone"
|
24
|
+
And the conversation is finished
|
25
|
+
When I call find_or_create_with("someone", "sample")
|
26
|
+
Then 2 conversations should exist
|
27
|
+
|
28
|
+
Scenario: No conversation exists and the subclass has been excluded
|
29
|
+
Given a conversation exists with topic: "sample", with: "someone"
|
30
|
+
And I configured Conversation with the following: Conversation.exclude [SampleConversation]
|
31
|
+
When I call find_or_create_with("someone", "sample")
|
32
|
+
Then 1 conversations should exist
|
33
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Get the conversation specifics
|
2
|
+
In order to get the specific conversation based on the topic and then move it along
|
3
|
+
As a Conversation user
|
4
|
+
I want to be able to get the specific conversation from the general one based off the topic
|
5
|
+
|
6
|
+
Scenario: Get the details from a conversation instance
|
7
|
+
Given a conversation exists with topic: "sample", with: "someone@example.com"
|
8
|
+
Then the conversation details should be a SampleConversation
|
9
|
+
|
10
|
+
Scenario: Get the details from a conversation instance
|
11
|
+
Given a conversation exists with topic: "sample", with: "someone@example.com"
|
12
|
+
And I configured Conversation with the following: Conversation.exclude [SampleConversation]
|
13
|
+
Then conversation.details(:include_all => true) should be a SampleConversation
|
@@ -0,0 +1,60 @@
|
|
1
|
+
Given /^(an|\d+) hours? (?:has|have) elapsed since #{capture_model} was last updated$/ do |time, conversation|
|
2
|
+
conversation = model!(conversation)
|
3
|
+
time = parse_email_count(time)
|
4
|
+
Conversation.record_timestamps = false
|
5
|
+
conversation.updated_at = time.hours.ago
|
6
|
+
conversation.save!
|
7
|
+
Conversation.record_timestamps = true
|
8
|
+
end
|
9
|
+
|
10
|
+
Given /^#{capture_model} is (.+)$/ do |conversation, state|
|
11
|
+
conversation = model!(conversation)
|
12
|
+
conversation.state = state
|
13
|
+
conversation.save!
|
14
|
+
end
|
15
|
+
|
16
|
+
Given /^no conversations exist with: "([^\"]*)"/ do |with|
|
17
|
+
find_models("conversation", "with: \"#{with}\"").each do |instance|
|
18
|
+
instance.destroy
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Given /^I configured Conversation with the following: (.+)$/ do |configuration|
|
23
|
+
instance_eval(configuration)
|
24
|
+
end
|
25
|
+
|
26
|
+
When /^I start a new conversation(?: with #{capture_fields})?$/ do |fields|
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
When /^I call find_or_create_with\("([^\"]*)", "([^\"]*)"\)$/ do |with, topic|
|
31
|
+
Conversation.find_or_create_with(with, topic)
|
32
|
+
end
|
33
|
+
|
34
|
+
When /^I start up a conversation with an? (blank|unknown) topic$/ do |template_type|
|
35
|
+
topic = ""
|
36
|
+
topic = "unknown" if template_type == "unknown"
|
37
|
+
Given "a conversation exists with topic: \"#{topic}\", with: \"someone\""
|
38
|
+
end
|
39
|
+
|
40
|
+
Then /^the conversation details should be a (\w+)$/ do |template_name|
|
41
|
+
model!("conversation").details.class.should == template_name.constantize
|
42
|
+
end
|
43
|
+
|
44
|
+
Then /^I should (not )?be able to find a conversation with: "([^\"]*)"$/ do |negative, with|
|
45
|
+
conversation = Conversation.with(with).last
|
46
|
+
unless negative
|
47
|
+
conversation.should == model!("conversation")
|
48
|
+
else
|
49
|
+
conversation.should be_nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Then /^#{capture_model} details should (?:be|have) (?:an? )?#{capture_predicate}$/ do |name, predicate|
|
54
|
+
model!(name).details.should send("be_#{predicate.gsub(' ', '_')}")
|
55
|
+
end
|
56
|
+
|
57
|
+
Then /^conversation\.details\(:include_all => true\) should be a SampleConversation$/ do
|
58
|
+
model!("conversation").details(:include_all => true).class.should == SampleConversation
|
59
|
+
end
|
60
|
+
|