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 ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7-2011.03@twilio_flow
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'shoulda', '>= 0'
5
+ gem 'bundler', '~> 1.0.0'
6
+ gem 'jeweler', '~> 1.6.4'
7
+ gem 'rcov', '>= 0'
8
+ gem 'test-unit', :require => 'test/unit'
9
+ gem 'guard'
10
+ gem 'guard-test'
11
+ gem 'actionpack', '~> 2.3.10'
12
+ gem 'mocha'
13
+ gem 'bourne'
14
+ gem 'pre-commit'
15
+ end
16
+
17
+ gem 'builder'
18
+ gem 'hsume2-state_machine', '~> 1.0.5', :require => 'state_machine'
data/Gemfile.lock ADDED
@@ -0,0 +1,52 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionpack (2.3.12)
5
+ activesupport (= 2.3.12)
6
+ rack (~> 1.1.0)
7
+ activesupport (2.3.12)
8
+ bourne (1.0)
9
+ mocha (= 0.9.8)
10
+ builder (3.0.0)
11
+ execjs (1.2.0)
12
+ multi_json (~> 1.0)
13
+ git (1.2.5)
14
+ guard (0.7.0)
15
+ thor (~> 0.14.6)
16
+ guard-test (0.3.0)
17
+ guard (>= 0.2.2)
18
+ test-unit (~> 2.2)
19
+ hsume2-state_machine (1.0.5)
20
+ jeweler (1.6.4)
21
+ bundler (~> 1.0)
22
+ git (>= 1.2.5)
23
+ rake
24
+ mocha (0.9.8)
25
+ rake
26
+ multi_json (1.0.3)
27
+ pre-commit (0.1.13)
28
+ execjs
29
+ rack (1.1.2)
30
+ rake (0.9.2)
31
+ rcov (0.9.9)
32
+ shoulda (2.11.3)
33
+ test-unit (2.3.0)
34
+ thor (0.14.6)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ actionpack (~> 2.3.10)
41
+ bourne
42
+ builder
43
+ bundler (~> 1.0.0)
44
+ guard
45
+ guard-test
46
+ hsume2-state_machine (~> 1.0.5)
47
+ jeweler (~> 1.6.4)
48
+ mocha
49
+ pre-commit
50
+ rcov
51
+ shoulda
52
+ test-unit
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'test' do
2
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch('test/helper.rb') { "test" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Henry Hsu and Zendesk, Inc
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.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ Call Center
2
+ ===========
3
+
4
+ Support for defining call center workflows.
5
+
6
+ Overview
7
+ --------
8
+ Call Center streamlines the process of defining multi-party call workflows in your application. Particularly, with [Twilio](http://www.twilio.com/docs) in mind.
9
+
10
+ [Twilio](http://www.twilio.com/docs) provides a two-part API for managing phone calls, and is mostly driven by callbacks. Call Center DRYs up the application logic dealing with a callback driven API so you can focus on the business logic of your call center.
11
+
12
+ ### Not DRY
13
+ Twilio requests your application to return [TwiML](http://www.twilio.com/docs/api/twiml/) that describes the call workflow. TwiML contains commands which Twilio then executes. It is essentially an application-to-application API, synonymous to making a REST call.
14
+
15
+ In the context of "[Skinny Controller, Fat Model](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model)", outgoing REST calls for the function of business logic are not a view concern but a model concern. Therefore, so is TwiML.
16
+
17
+ Twilio supports callbacks URLs and redirects to URLs that also render TwiML as a method of modifying live calls. Incoming callbacks are handled by the controller first, but the response is still a model concern.
18
+
19
+ Terminology
20
+ -----------
21
+
22
+ * **Call** - An application resource of yours that encapsulates a phone call. Phone calls are then acted on: answered, transferred, declined, etc.
23
+ * **Event** - Is something that happens outside or inside your application in relation to a **Call**. Someone picks up, hangs up, presses a button, etc.
24
+ * **State** - Is the status a **Call** is in which is descriptive of what's happened so far and what are the next things that should happen. (e.g. a call on hold is waiting for the agent to return)
25
+ * **CallFlow** - Is a definition of the process a **Call** goes through. **Events** drive the flow between **States**. (e.g. a simple workflow is when noone answers the call, send the call to voicemail)
26
+ * **Render** - Is the ability of the **CallFlow** to return TwiML to bring the call into the **State** or modify the live call through a **Redirect**.
27
+ * **Redirect** - Is a way of modifying a live call outside of a TwiML response (e.g. background jobs)
28
+
29
+ Usage
30
+ -----
31
+
32
+ class Call
33
+ include CallCenter
34
+
35
+ call_flow :state, :intial => :answered do
36
+ state :answered do
37
+ event :incoming_call, :to => :voicemail, :if => :not_during_business_hours?
38
+ event :incoming_call, :to => :sales
39
+ end
40
+
41
+ state :voicemail do
42
+ event :customer_hangs_up, :to => :voicemail_completed
43
+ end
44
+
45
+ on_render(:sales) do |call, x|
46
+ x.Say "This is Sales!"
47
+ end
48
+
49
+ on_render(:voicemail) do |call, x|
50
+ x.Say "Leave a voicemail!"
51
+ end
52
+
53
+ on_flow_to(:voicemail) do |call, transition|
54
+ call.notify(:voicemail)
55
+ end
56
+ end
57
+ end
58
+
59
+ Benefits of **CallCenter** is that it's backed by [state_machine](https://github.com/pluginaweek/state_machine). Which means you can interact with events the same you do in state_machine.
60
+
61
+ @call.incoming_call!
62
+ @call.voicemail?
63
+ @call.sales?
64
+ @call.render # See Rendering
65
+
66
+ Flow
67
+ ----
68
+
69
+ The general application flow for a **CallFlow** is like this:
70
+
71
+ 1. An incoming call is posted to your application
72
+ * You create a **Call**
73
+ * You execute an initial event
74
+ * You respond by rendering TwiML. Your TwiML contains callbacks to events or redirects
75
+ 2. Something happens and Twilio posts an event to your application
76
+ * You find the **Call**
77
+ * You store any new information
78
+ * You execute the posted event
79
+ * You respond by rendering TwiML. Your TwiML contains callbacks to events or redirects
80
+ 3. Repeat 2.
81
+
82
+ Rendering
83
+ ---------
84
+
85
+ Rendering is your way of interacting with Twilio. Thus, it provides two facilities: access to an XML builder and access to your call.
86
+
87
+ on_render(:sales) do |the_call, xml_builder|
88
+ xml_builder.Say "This is Sales!"
89
+
90
+ the_call.flag! # You can access the call explicitly
91
+ flag! # Or access it implicitly
92
+ end
93
+
94
+ Renders:
95
+
96
+ <?xml version="1.0" encoding="UTF-8"?>
97
+ <Response>
98
+ <Say>This is Sales!</Say>
99
+ </Response>
100
+
101
+ Callbacks
102
+ ---------
103
+
104
+ If you ever want to do something special after entering a state, but only if it's a new transition (e.g. NOT from :voicemail => :voicemail), you can do this:
105
+
106
+ on_flow_to(:voicemail) do |the_call, the_transition|
107
+ the_call.notify(transition.event) # Explicitly
108
+ notify(transition.event) # Implicitly
109
+ end
110
+
111
+ Redirects
112
+ ---------
113
+
114
+ Redirects are a request made to the [Twilio REST API](http://www.twilio.com/docs/api/rest/) that points to a callback URL which returns TwiML to be executed on a call. It is up to you how you want to perform this (e.g. with your favority http libraries, or with [Twilio Libraries](http://www.twilio.com/docs/libraries/)).
115
+
116
+ Redirect to events look like this:
117
+
118
+ ...
119
+ call_flow :state, :intial => :answered do
120
+ state :answered do
121
+ ...
122
+ end
123
+
124
+ state :ending_call do
125
+ event :end_call, :to => :ended_call
126
+ end
127
+
128
+ on_render(:ending_call) do
129
+ redirect_and_end_call!(:status => 'completed')
130
+ end
131
+ end
132
+ ...
133
+
134
+ For your **Call** to support this syntax, it must adhere to the following API:
135
+
136
+ class Call
137
+ def redirect_to(event, *args)
138
+ # where:
139
+ # event #=> :end_call
140
+ # args #=> [:status => 'completed]
141
+ @account.calls.get(self.sid).update({:url => "http://myapp.com/call_flow?event=#{event}"})
142
+ end
143
+ end
144
+
145
+ Tools
146
+ -----
147
+
148
+ ### Drawing ###
149
+
150
+ Should you be interested in what your call center workflow looks like, you can draw.
151
+
152
+ Call.state_machines[:status].draw(:font => 'Helvetica Neue')
153
+ # OR
154
+ @call.draw_call_flow(:font => 'Helvetica Neue')
155
+
156
+ Future
157
+ ------
158
+
159
+ * Integrate making new calls into the **CallFlow** DSL
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "call_center"
18
+ gem.homepage = "http://github.com/zendesk/call_center"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Support for describing call center workflows}
21
+ gem.description = %Q{Support for describing call center workflows}
22
+ gem.email = "hhsu@zendesk.com"
23
+ gem.authors = ["Henry Hsu"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "twilio_flow #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,97 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{call_center}
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Henry Hsu"]
12
+ s.date = %q{2011-09-21}
13
+ s.description = %q{Support for describing call center workflows}
14
+ s.email = %q{hhsu@zendesk.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rvmrc",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "Guardfile",
25
+ "LICENSE.txt",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "call_center.gemspec",
30
+ "init.rb",
31
+ "lib/call_center.rb",
32
+ "lib/call_center/core_ext/object_instance_exec.rb",
33
+ "lib/call_center/state_machine_ext.rb",
34
+ "lib/call_center/test/dsl.rb",
35
+ "test/call_center_test.rb",
36
+ "test/core_ext_test.rb",
37
+ "test/examples/call.rb",
38
+ "test/examples/legacy_call.rb",
39
+ "test/examples/multiple_flow_call.rb",
40
+ "test/examples/non_standard_call.rb",
41
+ "test/helper.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/zendesk/call_center}
44
+ s.licenses = ["MIT"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.5.3}
47
+ s.summary = %q{Support for describing call center workflows}
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
54
+ s.add_runtime_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
55
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
56
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
57
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
58
+ s.add_development_dependency(%q<rcov>, [">= 0"])
59
+ s.add_development_dependency(%q<test-unit>, [">= 0"])
60
+ s.add_development_dependency(%q<guard>, [">= 0"])
61
+ s.add_development_dependency(%q<guard-test>, [">= 0"])
62
+ s.add_development_dependency(%q<actionpack>, ["~> 2.3.10"])
63
+ s.add_development_dependency(%q<mocha>, [">= 0"])
64
+ s.add_development_dependency(%q<bourne>, [">= 0"])
65
+ s.add_development_dependency(%q<pre-commit>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<builder>, [">= 0"])
68
+ s.add_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
69
+ s.add_dependency(%q<shoulda>, [">= 0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ s.add_dependency(%q<test-unit>, [">= 0"])
74
+ s.add_dependency(%q<guard>, [">= 0"])
75
+ s.add_dependency(%q<guard-test>, [">= 0"])
76
+ s.add_dependency(%q<actionpack>, ["~> 2.3.10"])
77
+ s.add_dependency(%q<mocha>, [">= 0"])
78
+ s.add_dependency(%q<bourne>, [">= 0"])
79
+ s.add_dependency(%q<pre-commit>, [">= 0"])
80
+ end
81
+ else
82
+ s.add_dependency(%q<builder>, [">= 0"])
83
+ s.add_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
84
+ s.add_dependency(%q<shoulda>, [">= 0"])
85
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
86
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
87
+ s.add_dependency(%q<rcov>, [">= 0"])
88
+ s.add_dependency(%q<test-unit>, [">= 0"])
89
+ s.add_dependency(%q<guard>, [">= 0"])
90
+ s.add_dependency(%q<guard-test>, [">= 0"])
91
+ s.add_dependency(%q<actionpack>, ["~> 2.3.10"])
92
+ s.add_dependency(%q<mocha>, [">= 0"])
93
+ s.add_dependency(%q<bourne>, [">= 0"])
94
+ s.add_dependency(%q<pre-commit>, [">= 0"])
95
+ end
96
+ end
97
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'call_center'
@@ -0,0 +1,100 @@
1
+ require 'call_center/core_ext/object_instance_exec'
2
+ require 'state_machine'
3
+ require 'call_center/state_machine_ext'
4
+
5
+ module CallCenter
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :cached_state_machines
13
+ end
14
+ self.cached_state_machines ||= {}
15
+
16
+ def self.cache(klass, state_machine)
17
+ self.cached_state_machines["#{klass.name}_#{state_machine.name}"] ||= state_machine
18
+ end
19
+
20
+ def self.cached(klass, state_machine_name)
21
+ self.cached_state_machines["#{klass.name}_#{state_machine_name}"]
22
+ end
23
+
24
+ module ClassMethods
25
+ attr_accessor :call_flow_state_machine_name
26
+
27
+ # Calls state_machine ... with :syntax => :alternate
28
+ def call_flow(*args, &blk)
29
+ options = args.last.is_a?(Hash) ? args.pop : {}
30
+ args << options.merge(:syntax => :alternate)
31
+ state_machine_name = args.first || :state
32
+ if state_machine = CallCenter.cached(self, state_machine_name)
33
+ state_machine = state_machine.duplicate_to(self)
34
+ else
35
+ state_machine = state_machine(*args, &blk)
36
+ state_machine.instance_eval do
37
+ after_transition any => any do |call, transition|
38
+ call.flow_to(transition) if transition.from_name != transition.to_name
39
+ end
40
+ end
41
+ CallCenter.cache(self, state_machine)
42
+ end
43
+ self.call_flow_state_machine_name ||= state_machine.name
44
+ state_machine
45
+ end
46
+
47
+ def current_state_machine
48
+ self.state_machines[self.call_flow_state_machine_name]
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ def render(state_machine_name = self.class.call_flow_state_machine_name)
54
+ xml = Builder::XmlMarkup.new
55
+ render_block = current_block_accessor(:render_blocks, state_machine_name)
56
+
57
+ xml.instruct!
58
+ xml.Response do
59
+ self.instance_exec(self, xml, &render_block) if render_block
60
+ end
61
+ xml.target!
62
+ end
63
+
64
+ def flow_to(transition, state_machine_name = self.class.call_flow_state_machine_name)
65
+ block = current_block_accessor(:flow_to_blocks, state_machine_name)
66
+ self.instance_exec(self, transition, &block) if block
67
+ end
68
+
69
+ def draw_call_flow(*args)
70
+ current_state_machine.draw(*args)
71
+ end
72
+
73
+ private
74
+
75
+ def current_block_accessor(accessor, state_machine_name)
76
+ csm = self.class.state_machines[state_machine_name]
77
+ return unless csm.respond_to?(accessor)
78
+ blocks, name = csm.send(accessor), csm.name
79
+ blocks[current_flow_state(state_machine_name)] if blocks
80
+ end
81
+
82
+ def current_state_machine
83
+ self.class.current_state_machine
84
+ end
85
+
86
+ def current_flow_state(state_machine_name)
87
+ send(state_machine_name).to_sym
88
+ end
89
+
90
+ def method_missing(*args, &blk)
91
+ method_name = args.first.to_s
92
+ if method_name =~ /^redirect_and_(.+)!$/
93
+ args.shift
94
+ redirect_to($1.to_sym, *args)
95
+ else
96
+ super
97
+ end
98
+ end
99
+ end
100
+ end