sztywny-smsonrails 0.1.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/MIT-LICENSE +20 -0
- data/Manifest +101 -0
- data/README.rdoc +211 -0
- data/Rakefile +109 -0
- data/VERSION +1 -0
- data/app/controllers/admin/sms_on_rails/base_controller.rb +11 -0
- data/app/controllers/admin/sms_on_rails/drafts_controller.rb +75 -0
- data/app/controllers/admin/sms_on_rails/outbounds_controller.rb +117 -0
- data/app/controllers/admin/sms_on_rails/phone_carriers_controller.rb +85 -0
- data/app/controllers/admin/sms_on_rails/phone_numbers_controller.rb +101 -0
- data/app/controllers/sms_on_rails/creation_support.rb +99 -0
- data/app/controllers/sms_on_rails_controller.rb +14 -0
- data/app/helpers/admin/sms_on_rails/drafts_helper.rb +2 -0
- data/app/helpers/admin/sms_on_rails/phone_carriers_helper.rb +2 -0
- data/app/helpers/sms_on_rails/phone_numbers_helper.rb +9 -0
- data/app/helpers/sms_on_rails/sms_helper.rb +44 -0
- data/app/models/sms_on_rails/draft.rb +9 -0
- data/app/models/sms_on_rails/outbound.rb +17 -0
- data/app/models/sms_on_rails/phone_carrier.rb +14 -0
- data/app/models/sms_on_rails/phone_number.rb +8 -0
- data/app/views/admin/sms_on_rails/base/index.html.erb +5 -0
- data/app/views/admin/sms_on_rails/drafts/_show.html.erb +34 -0
- data/app/views/admin/sms_on_rails/drafts/edit.html.erb +36 -0
- data/app/views/admin/sms_on_rails/drafts/index.html.erb +32 -0
- data/app/views/admin/sms_on_rails/drafts/new.html.erb +34 -0
- data/app/views/admin/sms_on_rails/drafts/send_sms.html.erb +3 -0
- data/app/views/admin/sms_on_rails/drafts/show.html.erb +4 -0
- data/app/views/admin/sms_on_rails/outbounds/edit.html.erb +68 -0
- data/app/views/admin/sms_on_rails/outbounds/index.html.erb +37 -0
- data/app/views/admin/sms_on_rails/outbounds/new.html.erb +54 -0
- data/app/views/admin/sms_on_rails/outbounds/show.html.erb +69 -0
- data/app/views/admin/sms_on_rails/phone_carriers/edit.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_carriers/index.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_carriers/new.html.erb +22 -0
- data/app/views/admin/sms_on_rails/phone_carriers/show.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_numbers/edit.html.erb +33 -0
- data/app/views/admin/sms_on_rails/phone_numbers/index.html.erb +28 -0
- data/app/views/admin/sms_on_rails/phone_numbers/new.html.erb +31 -0
- data/app/views/admin/sms_on_rails/phone_numbers/show.html.erb +32 -0
- data/app/views/layouts/sms_on_rails/basic.html.erb +26 -0
- data/app/views/sms_on_rails/_phone_carrier_form_item.html.erb +6 -0
- data/app/views/sms_on_rails/_send_sms.html.erb +33 -0
- data/app/views/sms_on_rails/index.html.erb +8 -0
- data/app/views/sms_on_rails/send_sms.html.erb +3 -0
- data/app/views/sms_on_rails/show.html.erb +29 -0
- data/config/routes.rb +19 -0
- data/db/data/fixtures/sms_phone_carriers.yml +110 -0
- data/db/migrate/sms_on_rails_carrier_tables.rb +9 -0
- data/db/migrate/sms_on_rails_model_tables.rb +48 -0
- data/db/migrate/sms_on_rails_phone_number_tables.rb +11 -0
- data/db/seed_data.rb +16 -0
- data/generators/sms_on_rails/USAGE +31 -0
- data/generators/sms_on_rails/commands/inserts.rb +63 -0
- data/generators/sms_on_rails/commands/timestamps.rb +33 -0
- data/generators/sms_on_rails/runners/add_all_models.rb +6 -0
- data/generators/sms_on_rails/runners/dependencies.rb +1 -0
- data/generators/sms_on_rails/runners/remove_all_models.rb +5 -0
- data/generators/sms_on_rails/runners/sms_on_rails_routes.rb +14 -0
- data/generators/sms_on_rails/sms_on_rails_generator.rb +255 -0
- data/generators/sms_on_rails/templates/configuration/clickatell.rb +6 -0
- data/generators/sms_on_rails/templates/configuration/email_gateway.rb +7 -0
- data/generators/sms_on_rails/templates/migrate/schema_migration.rb +15 -0
- data/generators/sms_on_rails/templates/migrate/sms_on_rails_update_phone_numbers.rb +40 -0
- data/generators/sms_on_rails/templates/phone_number_collision.rb +2 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/sms_on_rails.rb +8 -0
- data/lib/sms_on_rails/activerecord_extensions/acts_as_deliverable.rb +92 -0
- data/lib/sms_on_rails/activerecord_extensions/acts_as_substitutable.rb +80 -0
- data/lib/sms_on_rails/activerecord_extensions/has_a_sms_service_provider.rb +101 -0
- data/lib/sms_on_rails/activerecord_extensions/lockable_record.rb +186 -0
- data/lib/sms_on_rails/all_models.rb +3 -0
- data/lib/sms_on_rails/model_support/draft.rb +178 -0
- data/lib/sms_on_rails/model_support/outbound.rb +136 -0
- data/lib/sms_on_rails/model_support/phone_carrier.rb +77 -0
- data/lib/sms_on_rails/model_support/phone_number.rb +248 -0
- data/lib/sms_on_rails/model_support/phone_number_associations.rb +13 -0
- data/lib/sms_on_rails/schema_helper.rb +51 -0
- data/lib/sms_on_rails/service_providers/base.rb +222 -0
- data/lib/sms_on_rails/service_providers/clickatell.rb +54 -0
- data/lib/sms_on_rails/service_providers/dummy.rb +21 -0
- data/lib/sms_on_rails/service_providers/email_gateway.rb +70 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/errors.rb +20 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer.rb +21 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer/sms_through_gateway.erb +6 -0
- data/lib/sms_on_rails/util/sms_error.rb +12 -0
- data/lib/smsonrails.rb +1 -0
- data/public/images/sms_on_rails/railsYoDawg.jpg +0 -0
- data/public/stylesheets/sms_on_rails.css +137 -0
- data/smsonrails.gemspec +159 -0
- data/sztywny-smsonrails.gemspec +148 -0
- data/tasks/sms_on_rails_tasks.rake +68 -0
- data/test/active_record_extensions/delivery_and_locking_test.rb +84 -0
- data/test/models/draft_test.rb +72 -0
- data/test/models/outbound_test.rb +89 -0
- data/test/models/phone_number_test.rb +131 -0
- data/test/run.rb +19 -0
- data/test/service_providers/abstract_test_support.rb +104 -0
- data/test/service_providers/clickatell_test.rb +37 -0
- data/test/service_providers/email_gateway_test.rb +30 -0
- data/test/test_helper.rb +24 -0
- data/uninstall.rb +1 -0
- metadata +193 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module Draft
|
4
|
+
|
5
|
+
def self.included(base)#:nodoc
|
6
|
+
base.send :accepts_nested_attributes_for, :outbounds
|
7
|
+
#base.acts_as_deliverable :fatal_exception => SmsOnRails::FatalSmsError
|
8
|
+
|
9
|
+
base.validates_presence_of :message
|
10
|
+
base.validate :validates_message_length
|
11
|
+
|
12
|
+
base.class_inheritable_accessor :default_header
|
13
|
+
base.class_inheritable_accessor :default_footer
|
14
|
+
base.class_inheritable_accessor :max_message_length
|
15
|
+
|
16
|
+
base.max_message_length = SmsOnRails::ServiceProviders::Base.max_characters
|
17
|
+
|
18
|
+
base.validates_associated :outbounds
|
19
|
+
|
20
|
+
base.send :include, InstanceMethods
|
21
|
+
base.send :extend, ClassMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
# Create a new sms draft
|
27
|
+
# +message+ - can either be a string, an attribute hash (nested ok) or and ActiveRecord
|
28
|
+
# +phone_numbers+ - array or single text phone number or phone number . If +message+ is
|
29
|
+
# a nested attributes (Rails 2.3), +phone_numbers+ and +options+ should be blank
|
30
|
+
#
|
31
|
+
# ===Options
|
32
|
+
# <tt>:send_immediately</tt> - Send all outbounds now
|
33
|
+
# <tt>:draft</tt> - a hash of attributes for the draft object
|
34
|
+
# <tt>:phone_number</tt> - a hash of attributes applied to all +phone_numbers+
|
35
|
+
# <tt>:outbound</tt> - a hash of attributes applied to all generated +Outbound+ instances
|
36
|
+
# <tt>:keep_failed_outbounds</tt> - typically outbound messages are destroyed if they fail during delivery
|
37
|
+
# <tt>:deliver</tt> - options to pass to the deliver method if <tt>:send_immediately</tt> is used
|
38
|
+
#
|
39
|
+
# ===Example
|
40
|
+
# SmsOnRails::Draft.create_sms('my_message', '9995556667')
|
41
|
+
# SmsOnRails::Draft.create_sms('my_message', ['2065556666', '9995556667'])
|
42
|
+
#
|
43
|
+
# SmsOnRails::Draft.create_sms(params[:draft]) # assume nested with :outbound_attributes
|
44
|
+
# SmsOnRails::Draft.create_sms(params[:draft], params[:phone_numbers], :send_immediately => true
|
45
|
+
def create_sms(message, phone_numbers=nil, options={})
|
46
|
+
|
47
|
+
draft = create_draft(message)
|
48
|
+
draft.attributes = options[:draft] if options[:draft]
|
49
|
+
|
50
|
+
# Update draft with any existing phone numbers
|
51
|
+
draft.outbounds.each {|o| o.assign_existing_phone } if draft.outbounds
|
52
|
+
|
53
|
+
# Generate new phone_numbers
|
54
|
+
draft.create_outbounds_for_phone_numbers(phone_numbers, options) if phone_numbers
|
55
|
+
|
56
|
+
if draft.send(options[:bang] ? :save! : :save) && options[:send_immediately]
|
57
|
+
if !draft.send(options[:bang] ? :deliver! : :deliver, options[:deliver])
|
58
|
+
# this is really crappy but when we are sending multiple messages
|
59
|
+
# locking has to actually create the object.
|
60
|
+
# so if we fail try to delete
|
61
|
+
# this could be terribly slow if there are a lot of outbounds
|
62
|
+
if options[:keep_failed_outbounds]
|
63
|
+
draft.outbounds.each{|o| o.update_attribute(:sms_draft_id, nil ) }
|
64
|
+
else
|
65
|
+
draft.outbounds.each{|o| o.destroy }
|
66
|
+
draft.outbounds = []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
draft
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_sms!(message, phone_numbers=nil, options={})
|
74
|
+
create_sms(message, phone_numbers, options.reverse_merge(:bang => true, :create => :create!))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create the draft object
|
78
|
+
# +options+ - A Draft object, a hash of attributes(can be nested) or a String with the draft message
|
79
|
+
def create_draft(options={})
|
80
|
+
if options.is_a?(ActiveRecord::Base)
|
81
|
+
options.dup
|
82
|
+
elsif options.is_a?(Hash)
|
83
|
+
new(options)
|
84
|
+
elsif options.is_a?(String)
|
85
|
+
new(:message => options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module InstanceMethods
|
91
|
+
|
92
|
+
def initialize(args={})#:nodoc:
|
93
|
+
super args
|
94
|
+
default_headers_and_footers
|
95
|
+
end
|
96
|
+
|
97
|
+
def save_and_deliver(options={})
|
98
|
+
save and deliver(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def save_and_deliver!(options={})
|
102
|
+
save! and deliver!(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
# The length of the message
|
106
|
+
# This does not take into consideration substituted params
|
107
|
+
# TODO: adjust max length for substituted params
|
108
|
+
def message_length; complete_message.length; end
|
109
|
+
def max_message_length; self.class.max_message_length; end
|
110
|
+
|
111
|
+
# default the headers and footers
|
112
|
+
def default_headers_and_footers
|
113
|
+
self.header ||= self.class.default_header
|
114
|
+
self.footer ||= self.class.default_footer
|
115
|
+
end
|
116
|
+
|
117
|
+
# The complete message with header and footer
|
118
|
+
def complete_message
|
119
|
+
complete_message = ""
|
120
|
+
complete_message << "#{header}\n" unless header.blank?
|
121
|
+
complete_message << message
|
122
|
+
complete_message << "\n#{footer}" unless footer.blank?
|
123
|
+
complete_message
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# Deliver all the unsent outbound messages
|
128
|
+
# +error+ - set this option to add an error message to the draft object
|
129
|
+
#
|
130
|
+
# This is locked and safe for individual messages
|
131
|
+
# but the draft object itself is not locked down,
|
132
|
+
# so it can be processed by multiple threads
|
133
|
+
def deliver(options={})
|
134
|
+
options||={}
|
135
|
+
deliver_method = options.delete(:bang) ? :deliver! : :deliver
|
136
|
+
|
137
|
+
error_messages = outbounds.inject([]) do |error_messages, o|
|
138
|
+
next(error_messages) if o.delivered?
|
139
|
+
unless o.send(deliver_method, options)
|
140
|
+
error_messages << "Message could not be delivered to: #{o.phone_number.human_display}"
|
141
|
+
end
|
142
|
+
error_messages
|
143
|
+
end
|
144
|
+
|
145
|
+
self.update_attribute(:status, error_messages.blank? ? 'PROCESSED' : 'FAILED')
|
146
|
+
error_messages.each { |msg| errors.add_to_base(msg) }
|
147
|
+
error_messages.blank?
|
148
|
+
end
|
149
|
+
|
150
|
+
# Deliver all outbound messages safely using optimisitic locking
|
151
|
+
# Progates any exception thrown
|
152
|
+
def deliver!(options={})
|
153
|
+
deliver((options||{}).merge(:bang => true))
|
154
|
+
end
|
155
|
+
|
156
|
+
# Create Outbound Instances based on +phone_numbers+ and +options+
|
157
|
+
# Refer to SmsOnRails::ModelSupport::Outbound.create_outbounds_from_phone
|
158
|
+
def create_outbounds_for_phone_numbers(phone_numbers, options={})
|
159
|
+
self.outbounds = self.class.reflections[:outbounds].klass.create_outbounds_for_phone_numbers(phone_numbers, options)
|
160
|
+
end
|
161
|
+
|
162
|
+
# if there is only one outbound message, return the actual (substituted)
|
163
|
+
# message. Otherwise, returns the draft message with substituted strings if any
|
164
|
+
def actual_message
|
165
|
+
self.outbounds.length == 1 ? outbounds.first.actual_message : message
|
166
|
+
end
|
167
|
+
|
168
|
+
protected
|
169
|
+
|
170
|
+
# validates the length of the message including the header and footer
|
171
|
+
def validates_message_length
|
172
|
+
errors.add(:message, "must be less than #{max_message_length} characters") unless message_length < max_message_length
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module Outbound
|
4
|
+
|
5
|
+
mattr_accessor :default_options
|
6
|
+
self.default_options = {}
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
|
10
|
+
base.has_a_sms_service_provider
|
11
|
+
|
12
|
+
base.acts_as_deliverable :fatal_exception => SmsOnRails::FatalSmsError,
|
13
|
+
:error => 'Unable to send message.'
|
14
|
+
|
15
|
+
|
16
|
+
base.acts_as_substitutable :draft_message,
|
17
|
+
:phone_number_digits => :phone_number_digits,
|
18
|
+
:phone_number => Proc.new{|record| record.phone_number.human_display },
|
19
|
+
:sender_name => :sender_name
|
20
|
+
|
21
|
+
base.send :alias_method, :full_message, :substituted_draft_message
|
22
|
+
base.send :alias_method, :send_immediately, :deliver
|
23
|
+
base.send :alias_method, :send_immediately!, :deliver!
|
24
|
+
|
25
|
+
base.send :cattr_accessor, :default_options
|
26
|
+
|
27
|
+
base.send :accepts_nested_attributes_for, :phone_number
|
28
|
+
|
29
|
+
base.send :include, InstanceMethods
|
30
|
+
base.send :extend, ClassMethods
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def send_immediately(message, phone_number, options={})
|
35
|
+
create_sms(message, phone_number, options.merge(:send_immediately => true))
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_immediately!(message, phone_number, options={})
|
39
|
+
create_sms!(message, phone_number, options.merge(:send_immediately => true))
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_sms(message, number, options={})
|
43
|
+
draft = reflections[:draft].klass.create_sms(message, number, options.reverse_merge(:keep_failed_outbounds => true))
|
44
|
+
number.is_a?(Array) ? draft.outbounds : draft.outbounds.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_sms!(message, number, options={})
|
48
|
+
draft = reflections[:draft].klass.create_sms!(message, number, options)
|
49
|
+
number.is_a?(Array) ? draft.outbounds : draft.outbounds.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_outbounds_for_phone_numbers(phone_numbers, options={})
|
53
|
+
smses = reflections[:phone_number].klass.find_and_create_all_by_numbers(phone_numbers, (options[:find]||{}).reverse_merge(:create => :new)).inject([]) do |smses, phone|
|
54
|
+
phone.attributes = options[:phone_number] if options[:phone_number]
|
55
|
+
phone.carrier = options[:carrier] if options[:carrier]
|
56
|
+
sms = self.new(options[:sms]||{})
|
57
|
+
sms.phone_number = phone
|
58
|
+
sms.service_provider = options[:service_provider] if options[:service_provider]
|
59
|
+
smses << sms
|
60
|
+
smses
|
61
|
+
end
|
62
|
+
smses
|
63
|
+
end
|
64
|
+
|
65
|
+
#Create the object find the existing phone if already stored
|
66
|
+
def create_with_phone(attributes, draft=nil)
|
67
|
+
outbound = new(attributes)
|
68
|
+
transaction {
|
69
|
+
outbound.assign_existing_phone
|
70
|
+
outbound.draft = draft
|
71
|
+
outbound.save
|
72
|
+
}
|
73
|
+
outbound
|
74
|
+
end
|
75
|
+
|
76
|
+
end #ClassMethods
|
77
|
+
|
78
|
+
module InstanceMethods
|
79
|
+
|
80
|
+
def phone_number_digits
|
81
|
+
self['phone_number_digits']||(phone_number ? phone_number.number : nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
def phone_number_digits=(digits)
|
85
|
+
self.phone_number ||= SmsOnRails::PhoneNumber.new
|
86
|
+
self.phone_number.number = digits
|
87
|
+
assign_existing_phone
|
88
|
+
self.phone_number.number
|
89
|
+
end
|
90
|
+
|
91
|
+
def assign_phone_number(phone)
|
92
|
+
self.phone_number = SmsOnRails::PhoneNumber.find_and_create_by_number(phone)
|
93
|
+
end
|
94
|
+
|
95
|
+
def assign_existing_phone; assign_phone_number(self.phone_number); end
|
96
|
+
|
97
|
+
# The actual (not substituted draft message
|
98
|
+
# Substituted message can be obtained with +substituted_draft_message+
|
99
|
+
def draft_message; draft.complete_message if draft; end
|
100
|
+
|
101
|
+
def actual_message
|
102
|
+
read_attribute(:actual_message) || draft_message
|
103
|
+
end
|
104
|
+
|
105
|
+
#only save the actual message if it differs from the draft message
|
106
|
+
def actual_message=(msg)
|
107
|
+
write_attribute(:actual_message, msg) unless substituted_draft_message == draft_message
|
108
|
+
end
|
109
|
+
|
110
|
+
#Todo
|
111
|
+
def sender_name; ''; end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# deliver_message is the actual call to the service provider to send the message
|
116
|
+
# this method is called during deliver in the acts_as_deliverable
|
117
|
+
def deliver_message(options)
|
118
|
+
self.sms_service_provider||= default_service_provider
|
119
|
+
|
120
|
+
# set the actual message if it differs; not a before_save to reduce the
|
121
|
+
# overhead of checking for all commits
|
122
|
+
self.actual_message = substituted_draft_message
|
123
|
+
|
124
|
+
result = (self.sms_service_provider).send_sms(self)
|
125
|
+
self.unique_id = result[:unique_id] if result.is_a?(Hash)
|
126
|
+
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
def log_delivery_error(exc)
|
131
|
+
logger.error "SMS Delivery Error: #{self.phone_number.human_display if self.phone_number}: #{exc}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module PhoneCarrier
|
4
|
+
def self.included(base)
|
5
|
+
base.send :validates_presence_of, :name
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Returns the email address for sms
|
13
|
+
#
|
14
|
+
# * +phone+ - phone number digits or an SmsOnRails::PhoneCarrier
|
15
|
+
# * +carrier+ - the name, instance, or id of a carrier
|
16
|
+
#
|
17
|
+
# SmsOnRails::PhoneCarrier.sms_email_address('12065551111', 1) => '2065551111@att.txt.net'
|
18
|
+
def sms_email_address(phone, carrier)
|
19
|
+
phone_carrier = carrier_by_value(carrier)
|
20
|
+
raise SmsOnRails::SmsError("Invalid carrier: #{carrier}") unless phone_carrier
|
21
|
+
phone_carrier.sms_email_address(phone)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrurns the SmsOnRails::PhoneCarrier object
|
25
|
+
# +carrier+ can be
|
26
|
+
# * symbol name of the object (ex :verizon)
|
27
|
+
# * text name (Ex. 'Verizon')
|
28
|
+
# * SmsOnRails::PhoneCarrier instance returns self
|
29
|
+
# * the id number
|
30
|
+
def carrier_by_value(carrier)
|
31
|
+
phone_carrier = case carrier.class.to_s
|
32
|
+
when 'Symbol', 'String' then find_by_name(carrier)
|
33
|
+
when "#{self.class.to_s}" then carrier
|
34
|
+
when 'Fixnum' then find_by_id(carrier)
|
35
|
+
else nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the number text and carrier obj from an email string
|
40
|
+
# carrier_from_sms_email '12065551234@txt.att.net ' => [12065551234, <SmsOnRails::PhoneCarrier>]
|
41
|
+
def carrier_from_sms_email(address)
|
42
|
+
|
43
|
+
number = address
|
44
|
+
carrier = nil
|
45
|
+
|
46
|
+
if address.match(/^\s*(\d+)@(\S+)\s*$/)
|
47
|
+
number = match[1]
|
48
|
+
carrier_name = match[2]
|
49
|
+
carrier = find_by_email_domain(match[2]) if match[2]
|
50
|
+
end
|
51
|
+
|
52
|
+
[number, carrier]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
module InstanceMethods
|
56
|
+
|
57
|
+
# Returns the email address for sms
|
58
|
+
#
|
59
|
+
# * +phone+ - phone number digits or an SmsOnRails::PhoneNumber
|
60
|
+
# * +options+ - empty space now
|
61
|
+
#
|
62
|
+
# att_carrier.sms_email_address('12065551111') => '2065551111@att.txt.net'
|
63
|
+
#
|
64
|
+
def sms_email_address(phone, options={})
|
65
|
+
email = (phone.is_a?(ActiveRecord::Base) ? phone.digits : phone).dup
|
66
|
+
email.gsub!(/^1/, '')
|
67
|
+
email << '@'
|
68
|
+
email << self.email_domain
|
69
|
+
email
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return the carriers name when stringified
|
73
|
+
def to_s; name; end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module PhoneNumber
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include,InstanceMethods
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :validates_format_of, :phone_number_digits, :with => /^\d{5,30}$/, :message => 'must be a number and have at least 5 digits'
|
8
|
+
base.before_save {|record| record.number = record.digits}
|
9
|
+
base.send :validates_presence_of, :number
|
10
|
+
base.send :attr_reader, :original_number
|
11
|
+
base.class_inheritable_accessor :valid_finder_create_options
|
12
|
+
base.valid_finder_create_options = %w(create keep_duplicates skip_sort)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# adds a sanitize search for numbers for the options
|
19
|
+
# used for finders
|
20
|
+
# Use with find for other ActiveRecord objects
|
21
|
+
# Outbound.find :all, add_number_search([1,2,3], :conditions => 'outbounds.send_priority == 1')
|
22
|
+
def add_number_search(numbers, options={}, merge_options={})
|
23
|
+
number_digits = [numbers].flatten
|
24
|
+
number_digits.collect!{|n| digits(n) } unless merge_options[:skip_sanitize]
|
25
|
+
number_conditions = ['number in (?)', number_digits.uniq.compact]
|
26
|
+
options[:conditions] = merge_conditions(options[:conditions]||{}, number_conditions)
|
27
|
+
options
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find all numbers (sanitized) that match a list of String +numbers+
|
31
|
+
def find_all_by_numbers(numbers, options={}, merge_options={})
|
32
|
+
return [] unless numbers.dup.delete_if{|x| x.blank? }.any?
|
33
|
+
find(:all, add_number_search(numbers, options, merge_options))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find a single number
|
37
|
+
def find_by_number(number, options={})
|
38
|
+
find_all_by_numbers([number], options).first;
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
# Find all numbers and create if it doesn't already exist
|
44
|
+
# +number_list+ - a list of numbers (String, ActiveRecord or attribute hash)
|
45
|
+
# +options+ - additional create options and normal finder options like <tt>:conditions</tt>
|
46
|
+
# === Additional Options
|
47
|
+
# <tt>:create</tt> - <tt>:new</tt>(Default), <tt>:create</tt>, or <tt>:create!</tt>
|
48
|
+
# If the number does not exist, create it with this method. Using <tt>:new</tt> means none of the objects are saved
|
49
|
+
# <tt>:keep_duplicates<tt> - When set to true, duplicates in the list are returned
|
50
|
+
# <tt>:skip_sort</tt> - Default is to sort the list to be in the same order as +number_list+.
|
51
|
+
# Set to false to skip sorting (perf boost). Instance creation or attribute update does not occur
|
52
|
+
# when <tt>skip_sort</tt> is false.
|
53
|
+
def find_and_create_all_by_numbers(number_list, options={})
|
54
|
+
create_options, finder_options = seperate_find_and_create_options(options)
|
55
|
+
|
56
|
+
# Collect a list of digits and a list of attributes
|
57
|
+
attribute_list = []
|
58
|
+
number_digits = [number_list].flatten.inject([]) do |list, n|
|
59
|
+
attribute_list << (new_attributes = value_to_hash(n))
|
60
|
+
digit = digits(new_attributes[:number]||new_attributes['number'])
|
61
|
+
digit = new_attributes[:number]||new_attributes['number'] if digit.blank?
|
62
|
+
list << digit
|
63
|
+
list
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
found_numbers = find_all_by_numbers(number_digits, finder_options, :skip_sanitize => true)
|
68
|
+
# sort the list based on the order of the original input
|
69
|
+
# not found values have nil
|
70
|
+
if create_options[:skip_sort]
|
71
|
+
found_numbers
|
72
|
+
else
|
73
|
+
sorted_numbers = sort_by_numbers(found_numbers, number_digits, create_options)
|
74
|
+
transaction { update_attributes_on_list(sorted_numbers, attribute_list, create_options) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find a number and create if it does not exist
|
79
|
+
# +options+ include those specified in find_all_and_create_by_number
|
80
|
+
def find_and_create_by_number(number, options={})
|
81
|
+
find_and_create_all_by_numbers(number, options.reverse_merge(:create => :new)).first
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Return the phone number with specified carrier if the phone number is an sms email address
|
86
|
+
def find_by_sms_email_address(address, options={})
|
87
|
+
number, carrier = reflections[:carrier].klass.carrier_from_sms_email(address)
|
88
|
+
|
89
|
+
if number
|
90
|
+
phone = find_by_number(number, options)||new(:number => number)
|
91
|
+
phone.carrier = carrier if carrier
|
92
|
+
phone
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# The digits (numbers) only of the phone number
|
100
|
+
# Digits are how phone numbers are stored in the database
|
101
|
+
# The following all return +12065555555+
|
102
|
+
# SmsOnRails::digits(12065555555')
|
103
|
+
# SmsOnRails::digits(206.555.5555')
|
104
|
+
# SmsOnRails::digits(1206-555 5555')
|
105
|
+
def digits(text)
|
106
|
+
return text.digits if text.is_a?(self)
|
107
|
+
number = text.to_s.gsub(/\D/,'')
|
108
|
+
number = "1#{number}" if number.length == 10
|
109
|
+
number
|
110
|
+
end
|
111
|
+
|
112
|
+
# The human display pretty phone number
|
113
|
+
# (206) 555-5555
|
114
|
+
def human_display(number)
|
115
|
+
base_number = digits(number)
|
116
|
+
if base_number.length == 11 && base_number.first == '1'
|
117
|
+
"(#{base_number[1..3]}) #{base_number[4..6]}-#{base_number[7..10]}"
|
118
|
+
elsif base_number.length > 0
|
119
|
+
"+#{base_number}"
|
120
|
+
else
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
# Return create_options hash and finder options hash from all options
|
127
|
+
def seperate_find_and_create_options(options={})#:nodoc:
|
128
|
+
|
129
|
+
seperated_options = (options||{}).inject([{}, {}]) {|map, (k,v)|
|
130
|
+
if valid_finder_create_options.include?(k.to_s)
|
131
|
+
map.first[k.to_sym] = v
|
132
|
+
else
|
133
|
+
map.last[k.to_sym] = v
|
134
|
+
end
|
135
|
+
map
|
136
|
+
}
|
137
|
+
seperated_options.first[:create] = :new if seperated_options.first[:create].is_a?(TrueClass)
|
138
|
+
seperated_options
|
139
|
+
end
|
140
|
+
|
141
|
+
# Convert a PhoneNumber, hash map or string number
|
142
|
+
# into an attribute hash
|
143
|
+
def value_to_hash(digits)#:nodoc:
|
144
|
+
attributes = if digits.is_a?(ActiveRecord::Base)
|
145
|
+
digits.attributes
|
146
|
+
elsif digits.is_a?(Hash)
|
147
|
+
digits.dup
|
148
|
+
elsif digits
|
149
|
+
{:number => digits}
|
150
|
+
end
|
151
|
+
attributes
|
152
|
+
end
|
153
|
+
|
154
|
+
# Return a sorted list of PhoneNumber instances
|
155
|
+
# Will create new records for missing records if attribute_list is specified
|
156
|
+
# +unsorted_list+ - unsorted list of PhoneNumbers
|
157
|
+
# +sorted_digits+ - sorted list of digits (Strings)
|
158
|
+
# +attribute_list+ - optional list of attributes in sorted order
|
159
|
+
# +creation_method+ - :create, :new, or :create!
|
160
|
+
def sort_by_numbers(unsorted_list, sorted_digits, options={})
|
161
|
+
|
162
|
+
unsorted_map = unsorted_list.inject({}) {|map, v| map[v.number] = v; map }
|
163
|
+
|
164
|
+
# First sort the list in the order of the sorted digits
|
165
|
+
# leaving nils for empty spaces
|
166
|
+
sorted_list = if sorted_digits.length > 1
|
167
|
+
sorted = [nil] * sorted_digits.length
|
168
|
+
sorted_digits.each_with_index{|n, idx|
|
169
|
+
sorted[idx] = unsorted_map[n]
|
170
|
+
unsorted_map[n] = :used unless options[:keep_duplicates]
|
171
|
+
}
|
172
|
+
|
173
|
+
sorted.delete_if{|x| x == :used } unless options[:keep_duplicates]
|
174
|
+
sorted
|
175
|
+
else
|
176
|
+
unsorted_list.any? ? unsorted_list.dup : [nil]
|
177
|
+
end
|
178
|
+
sorted_list
|
179
|
+
end
|
180
|
+
|
181
|
+
# create new records for any records not found
|
182
|
+
# update the attributes of the found records
|
183
|
+
# save if :create
|
184
|
+
def update_attributes_on_list(sorted_list, attribute_list, options={})
|
185
|
+
|
186
|
+
unless attribute_list.blank?
|
187
|
+
0.upto(sorted_list.length - 1) {|idx|
|
188
|
+
if sorted_list[idx]
|
189
|
+
if (attr = attribute_list[idx].delete_if{|x, y| x.to_s == 'number'}).any?
|
190
|
+
sorted_list[idx].attributes = attr
|
191
|
+
sorted_list[idx].save if options[:create]
|
192
|
+
sorted_list[idx].save! if options[:create!]
|
193
|
+
end
|
194
|
+
elsif options[:create]
|
195
|
+
sorted_list[idx] = self.send(options[:create], attribute_list[idx])
|
196
|
+
end
|
197
|
+
}
|
198
|
+
end
|
199
|
+
sorted_list
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
module InstanceMethods
|
205
|
+
# The human display pretty phone number
|
206
|
+
# (206) 555-5555
|
207
|
+
def human_display
|
208
|
+
self.class.human_display(self.number)
|
209
|
+
end
|
210
|
+
|
211
|
+
def number=(value)
|
212
|
+
@original_number = value unless value.blank?
|
213
|
+
@digits = self.class.digits(value)
|
214
|
+
write_attribute :number, @digits
|
215
|
+
end
|
216
|
+
|
217
|
+
def number
|
218
|
+
n = read_attribute :number
|
219
|
+
n.blank? ? original_number : n
|
220
|
+
end
|
221
|
+
|
222
|
+
def digits
|
223
|
+
@digits||=self.class.digits(self.number)
|
224
|
+
end
|
225
|
+
|
226
|
+
# return the sms email address from the carrier
|
227
|
+
def sms_email_address
|
228
|
+
carrier.sms_email_address(self) if carrier
|
229
|
+
end
|
230
|
+
|
231
|
+
#Assign the carrier to the phone number if it exists
|
232
|
+
#+carrier+ - can be a ActiveRecord object, a name of the carrier, or carrier id
|
233
|
+
def assign_carrier(carrier)
|
234
|
+
specified_carrier = self.class.reflections[:carrier].klass.carrier_by_value(carrier)
|
235
|
+
self.carrier = specified_carrier if specified_carrier
|
236
|
+
end
|
237
|
+
|
238
|
+
alias_method :phone_number_digits, :digits
|
239
|
+
|
240
|
+
# Returns true if the number is marked as do not send (not blank)
|
241
|
+
# Values could be abuse, bounce, opt-out, etc
|
242
|
+
def do_not_send?
|
243
|
+
!self.do_not_send.blank?
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|