call_center 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|