call_center 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +52 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +159 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/call_center.gemspec +97 -0
- data/init.rb +1 -0
- data/lib/call_center.rb +100 -0
- data/lib/call_center/core_ext/object_instance_exec.rb +21 -0
- data/lib/call_center/state_machine_ext.rb +26 -0
- data/lib/call_center/test/dsl.rb +69 -0
- data/test/call_center_test.rb +261 -0
- data/test/core_ext_test.rb +18 -0
- data/test/examples/call.rb +46 -0
- data/test/examples/legacy_call.rb +26 -0
- data/test/examples/multiple_flow_call.rb +21 -0
- data/test/examples/non_standard_call.rb +12 -0
- data/test/helper.rb +40 -0
- metadata +278 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# From Active Support
|
2
|
+
class Object
|
3
|
+
unless defined? instance_exec
|
4
|
+
def instance_exec(*args, &block)
|
5
|
+
begin
|
6
|
+
old_critical, Thread.critical = Thread.critical, true
|
7
|
+
n = 0
|
8
|
+
n += 1 while respond_to?(method_name = "__instance_exec#{n}")
|
9
|
+
InstanceExecMethods.module_eval { define_method(method_name, &block) }
|
10
|
+
ensure
|
11
|
+
Thread.critical = old_critical
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
send(method_name, *args)
|
16
|
+
ensure
|
17
|
+
InstanceExecMethods.module_eval { remove_method(method_name) } rescue nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Extension for StateMachine::Machine to store and provide render blocks
|
2
|
+
class StateMachine::Machine
|
3
|
+
attr_accessor :render_blocks
|
4
|
+
attr_accessor :flow_to_blocks
|
5
|
+
|
6
|
+
def on_render(state_name, &blk)
|
7
|
+
@render_blocks ||= {}
|
8
|
+
@render_blocks[state_name] = blk
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_flow_to(state_name, &blk)
|
12
|
+
@flow_to_blocks ||= {}
|
13
|
+
@flow_to_blocks[state_name] = blk
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Extension for StateMachine::AlternateMachine to provide render blocks inside a state definition
|
18
|
+
class StateMachine::AlternateMachine
|
19
|
+
def on_render(state_name = nil, &blk)
|
20
|
+
if @from_state
|
21
|
+
@queued_sends << [[:on_render, @from_state], blk]
|
22
|
+
else
|
23
|
+
@queued_sends << [[:on_render, state_name], blk]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module CallCenter
|
2
|
+
module Test
|
3
|
+
module DSL
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.class_eval do
|
7
|
+
def response_from_page_or_rjs_with_body
|
8
|
+
HTML::Document.new(CGI.unescapeHTML(@body)).root
|
9
|
+
end
|
10
|
+
alias_method_chain :response_from_page_or_rjs, :body
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def body(text, debug = false)
|
15
|
+
puts text if debug
|
16
|
+
@body = text
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def should_flow(options, &block)
|
21
|
+
event = options.delete(:on)
|
22
|
+
setup_block = options.delete(:when)
|
23
|
+
setup_block_line = setup_block.to_s.match(/.*@(.*):([0-9]+)>/)[2] if setup_block
|
24
|
+
state_field = options.delete(:state) || :state
|
25
|
+
from, to = options.to_a.first
|
26
|
+
description = ":#{from} => :#{to} via #{event}!#{setup_block_line.present? ? " when:#{setup_block_line}" : nil}"
|
27
|
+
context "" do
|
28
|
+
should "transition #{description}" do
|
29
|
+
self.instance_eval(&setup_block) if setup_block
|
30
|
+
@call.send(:"#{state_field}=", from.to_s)
|
31
|
+
@call.send(:"#{event}")
|
32
|
+
assert_equal to, @call.send(:"#{state_field}_name"), "status should be :#{to}, not :#{@call.send(state_field)}"
|
33
|
+
end
|
34
|
+
|
35
|
+
if block.present?
|
36
|
+
context "#{description} and :#{to}" do
|
37
|
+
setup do
|
38
|
+
self.instance_eval(&setup_block) if setup_block
|
39
|
+
@call.send(:"#{state_field}=", from.to_s)
|
40
|
+
@call.send(:"#{event}")
|
41
|
+
body(@call.render) if @call.respond_to?(:render)
|
42
|
+
end
|
43
|
+
|
44
|
+
self.instance_eval(&block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def should_also(&block)
|
51
|
+
line = block.to_s.match(/.*@(.*):([0-9]+)>/)[2]
|
52
|
+
should "also satisfy block on line #{line}" do
|
53
|
+
self.instance_eval(&block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
alias_method :and_also, :should_also
|
57
|
+
|
58
|
+
def should_render(&block)
|
59
|
+
line = block.to_s.match(/.*@(.*):([0-9]+)>/)[2]
|
60
|
+
should "render selector on line #{line}" do
|
61
|
+
args = [self.instance_eval(&block)].flatten
|
62
|
+
assert_select *args
|
63
|
+
end
|
64
|
+
end
|
65
|
+
alias_method :and_render, :should_render
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
require 'call_center/test/dsl'
|
4
|
+
|
5
|
+
require 'test/examples/legacy_call'
|
6
|
+
require 'test/examples/call'
|
7
|
+
require 'test/examples/non_standard_call'
|
8
|
+
require 'test/examples/multiple_flow_call'
|
9
|
+
|
10
|
+
class CallCenterTest < Test::Unit::TestCase
|
11
|
+
include CallCenter::Test::DSL
|
12
|
+
|
13
|
+
[:call, :legacy_call].each do |call_type|
|
14
|
+
context "#{call_type.to_s.gsub('_', ' ')} workflow" do
|
15
|
+
setup do
|
16
|
+
klass = call_type.to_s.gsub('_', ' ').titleize.gsub(' ', '').constantize
|
17
|
+
@call = klass.new
|
18
|
+
@call.stubs(:notify)
|
19
|
+
@call.stubs(:flow_url).returns('the_flow')
|
20
|
+
end
|
21
|
+
|
22
|
+
context "agents available" do
|
23
|
+
setup do
|
24
|
+
@call.stubs(:agents_available?).returns(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "transition to routing" do
|
28
|
+
@call.incoming_call!
|
29
|
+
assert_equal 'routing', @call.state
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "no agents available "do
|
34
|
+
setup do
|
35
|
+
@call.stubs(:agents_available?).returns(false)
|
36
|
+
end
|
37
|
+
|
38
|
+
should "transition to voicemail" do
|
39
|
+
@call.incoming_call!
|
40
|
+
assert_equal 'voicemail', @call.state
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "in voicemail" do
|
45
|
+
setup do
|
46
|
+
@call.stubs(:agents_available?).returns(false)
|
47
|
+
@call.incoming_call!
|
48
|
+
end
|
49
|
+
|
50
|
+
context "and customer hangs up" do
|
51
|
+
should "transition to voicemail_completed" do
|
52
|
+
@call.customer_hangs_up!
|
53
|
+
assert @call.voicemail_completed?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "something crazy happens" do
|
59
|
+
# It's going to try to call the after transition method, but since it doesn't exist...
|
60
|
+
should "be ok" do
|
61
|
+
@call.something_crazy_happens!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "cancelled" do
|
66
|
+
should "stay in cancelled" do
|
67
|
+
@call.stubs(:cancelled)
|
68
|
+
@call.state = 'cancelled'
|
69
|
+
|
70
|
+
@call.customer_hangs_up!
|
71
|
+
|
72
|
+
assert @call.cancelled?
|
73
|
+
assert_received(@call, :cancelled) { |e| e.never }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "using test DSL:" do
|
78
|
+
should_flow :on => :incoming_call, :initial => :routing, :when => Proc.new {
|
79
|
+
@call.stubs(:agents_available?).returns(true)
|
80
|
+
}
|
81
|
+
|
82
|
+
should_flow :on => :incoming_call, :initial => :voicemail, :when => Proc.new {
|
83
|
+
@call.stubs(:agents_available?).returns(false)
|
84
|
+
} do
|
85
|
+
should_flow :on => :customer_hangs_up, :voicemail => :voicemail_completed
|
86
|
+
end
|
87
|
+
|
88
|
+
should_flow :on => :something_crazy_happens, :initial => :uh_oh
|
89
|
+
|
90
|
+
should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
|
91
|
+
should_also { assert_received(@call, :cancelled) { |e| e.never } }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "call" do
|
98
|
+
setup do
|
99
|
+
@call = Call.new
|
100
|
+
end
|
101
|
+
|
102
|
+
should "render xml for initial state" do
|
103
|
+
@call.expects(:notify).with(:rendering_initial)
|
104
|
+
body @call.render
|
105
|
+
assert_select "Response>Say", "Hello World"
|
106
|
+
end
|
107
|
+
|
108
|
+
should "render xml for voicemail state" do
|
109
|
+
@call.stubs(:agents_available?).returns(false)
|
110
|
+
@call.incoming_call!
|
111
|
+
@call.expects(:notify).with(:rendering_voicemail)
|
112
|
+
@call.expects(:flow_url).with(:voicemail_complete).returns('the_flow')
|
113
|
+
|
114
|
+
body @call.render
|
115
|
+
assert_select "Response>Say"
|
116
|
+
assert_select "Response>Record[action=the_flow]"
|
117
|
+
end
|
118
|
+
|
119
|
+
should "render noop when no render block" do
|
120
|
+
@call.stubs(:agents_available?).returns(true)
|
121
|
+
@call.incoming_call!
|
122
|
+
|
123
|
+
body @call.render
|
124
|
+
assert_select "Response"
|
125
|
+
end
|
126
|
+
|
127
|
+
should "respond when flow to state (once)" do
|
128
|
+
@call.state = 'routing'
|
129
|
+
@call.expects(:notify).with(:cancelled).once
|
130
|
+
@call.customer_hangs_up!
|
131
|
+
assert @call.cancelled?
|
132
|
+
@call.customer_hangs_up!
|
133
|
+
assert @call.cancelled?
|
134
|
+
@call.customer_hangs_up!
|
135
|
+
assert @call.cancelled?
|
136
|
+
end
|
137
|
+
|
138
|
+
should "asynchronously perform event" do
|
139
|
+
@call.stubs(:agents_available?).returns(true)
|
140
|
+
@call.incoming_call!
|
141
|
+
@call.expects(:redirect_to).with(:start_conference)
|
142
|
+
|
143
|
+
@call.redirect_and_start_conference!
|
144
|
+
end
|
145
|
+
|
146
|
+
should "asynchronously perform event with options" do
|
147
|
+
@call.stubs(:agents_available?).returns(true)
|
148
|
+
@call.incoming_call!
|
149
|
+
@call.expects(:redirect_to).with(:start_conference, :status => 'completed')
|
150
|
+
|
151
|
+
@call.redirect_and_start_conference!(:status => 'completed')
|
152
|
+
end
|
153
|
+
|
154
|
+
should "raise error on missing method" do
|
155
|
+
assert_raises {
|
156
|
+
@call.i_am_missing!
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
should "draw state machine digraph" do
|
161
|
+
Call.state_machines[:state].expects(:draw).with(:name => 'call_workflow', :font => 'Helvetica Neue')
|
162
|
+
@call.draw_call_flow(:name => 'call_workflow', :font => 'Helvetica Neue')
|
163
|
+
end
|
164
|
+
|
165
|
+
context "using test DSL:" do
|
166
|
+
should_flow :on => :incoming_call, :initial => :voicemail, :when => Proc.new {
|
167
|
+
@call.stubs(:agents_available?).returns(false)
|
168
|
+
@call.stubs(:notify)
|
169
|
+
@call.stubs(:flow_url).returns('the_flow')
|
170
|
+
} do
|
171
|
+
should_also { assert_received(@call, :notify) { |e| e.with(:rendering_voicemail) } }
|
172
|
+
and_also { assert_received(@call, :flow_url) { |e| e.with(:voicemail_complete) } }
|
173
|
+
and_render { "Response>Say" }
|
174
|
+
and_render { "Response>Record[action=the_flow]" }
|
175
|
+
end
|
176
|
+
|
177
|
+
should_flow :on => :incoming_call, :initial => :routing, :when => Proc.new {
|
178
|
+
@call.stubs(:agents_available?).returns(true)
|
179
|
+
} do
|
180
|
+
should_render { "Response" }
|
181
|
+
end
|
182
|
+
|
183
|
+
should_flow :on => :customer_hangs_up, :routing => :cancelled, :when => Proc.new {
|
184
|
+
@call.stubs(:notify)
|
185
|
+
} do
|
186
|
+
should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } }
|
187
|
+
and_also { assert @call.cancelled? }
|
188
|
+
|
189
|
+
should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
|
190
|
+
should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } } # For above
|
191
|
+
and_also { assert @call.cancelled? }
|
192
|
+
|
193
|
+
should_flow :on => :customer_hangs_up, :cancelled => :cancelled do
|
194
|
+
should_also { assert_received(@call, :notify) { |e| e.with(:cancelled).once } } # For above
|
195
|
+
and_also { assert @call.cancelled? }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "non-standard call" do
|
203
|
+
setup do
|
204
|
+
@call = NonStandardCall.new
|
205
|
+
end
|
206
|
+
|
207
|
+
should "render xml for initial state" do
|
208
|
+
assert_equal 'ready', @call.status
|
209
|
+
body @call.render
|
210
|
+
assert_select "Response>Say", "Hello World"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context "cache call" do
|
215
|
+
should "re-apply state machine and render xml for initial state" do
|
216
|
+
Object.send(:remove_const, :NonStandardCall)
|
217
|
+
Object.const_set(:NonStandardCall, Class.new)
|
218
|
+
NonStandardCall.class_eval do
|
219
|
+
include CallCenter
|
220
|
+
call_flow :status, :initial => :ready do
|
221
|
+
raise Exception, "Should not be called"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
@call = NonStandardCall.new
|
226
|
+
|
227
|
+
assert_equal 'ready', @call.status
|
228
|
+
body @call.render
|
229
|
+
assert_select "Response>Say", "Hello World"
|
230
|
+
assert @call.go!
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
context "cache multiple call flows" do
|
235
|
+
should "re-apply state machine and render xml for initial state" do
|
236
|
+
Object.send(:remove_const, :MultipleFlowCall)
|
237
|
+
Object.const_set(:MultipleFlowCall, Class.new)
|
238
|
+
MultipleFlowCall.class_eval do
|
239
|
+
include CallCenter
|
240
|
+
call_flow :status, :initial => :ready do
|
241
|
+
raise Exception, "Should not be called"
|
242
|
+
end
|
243
|
+
call_flow :outgoing_status, :initial => :outgoing_ready do
|
244
|
+
raise Exception, "Should not be called"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
@call = MultipleFlowCall.new
|
249
|
+
|
250
|
+
assert_equal 'ready', @call.status
|
251
|
+
body @call.render
|
252
|
+
assert_select "Response>Say", "Hello World"
|
253
|
+
assert @call.go!
|
254
|
+
|
255
|
+
assert_equal 'outgoing_ready', @call.outgoing_status
|
256
|
+
body @call.render(:outgoing_status)
|
257
|
+
assert_select "Response>Say", "Hello Outgoing World"
|
258
|
+
assert @call.outgoing_go!
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class MyObject
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
class CoreExtTest < Test::Unit::TestCase
|
8
|
+
should "use existing" do
|
9
|
+
obj = MyObject.new
|
10
|
+
$capture = nil
|
11
|
+
block = lambda { |a|
|
12
|
+
$capture = [self, a]
|
13
|
+
}
|
14
|
+
obj.instance_exec(true, &block)
|
15
|
+
|
16
|
+
assert_equal [obj, true], $capture
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Call
|
2
|
+
include CallCenter
|
3
|
+
include CommonCallMethods
|
4
|
+
|
5
|
+
call_flow :state, :initial => :initial do
|
6
|
+
state :initial do
|
7
|
+
event :incoming_call, :to => :voicemail, :unless => :agents_available?
|
8
|
+
event :incoming_call, :to => :routing, :if => :agents_available?
|
9
|
+
event :something_crazy_happens, :to => :uh_oh
|
10
|
+
end
|
11
|
+
|
12
|
+
state :voicemail do
|
13
|
+
event :customer_hangs_up, :to => :voicemail_completed
|
14
|
+
end
|
15
|
+
|
16
|
+
state :routing do
|
17
|
+
event :customer_hangs_up, :to => :cancelled
|
18
|
+
event :start_conference, :to => :in_conference
|
19
|
+
end
|
20
|
+
|
21
|
+
state :cancelled do
|
22
|
+
event :customer_hangs_up, :to => same
|
23
|
+
end
|
24
|
+
|
25
|
+
# =================
|
26
|
+
# = Render Blocks =
|
27
|
+
# =================
|
28
|
+
|
29
|
+
state :initial do
|
30
|
+
on_render do |call, x| # To allow defining render blocks within a state
|
31
|
+
call.notify(:rendering_initial)
|
32
|
+
x.Say "Hello World"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
on_render(:voicemail) do |call, x| # To allow defining render blocks outside a state
|
37
|
+
notify(:rendering_voicemail)
|
38
|
+
x.Say "Hello World"
|
39
|
+
x.Record :action => flow_url(:voicemail_complete)
|
40
|
+
end
|
41
|
+
|
42
|
+
on_flow_to(:cancelled) do |call, transition|
|
43
|
+
notify(:cancelled)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|