blythedunham-sms_on_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Manifest +101 -0
  3. data/README +163 -0
  4. data/README.rdoc +211 -0
  5. data/Rakefile +76 -0
  6. data/app/controllers/admin/sms_on_rails/base_controller.rb +11 -0
  7. data/app/controllers/admin/sms_on_rails/drafts_controller.rb +75 -0
  8. data/app/controllers/admin/sms_on_rails/outbounds_controller.rb +117 -0
  9. data/app/controllers/admin/sms_on_rails/phone_carriers_controller.rb +85 -0
  10. data/app/controllers/admin/sms_on_rails/phone_numbers_controller.rb +101 -0
  11. data/app/controllers/sms_on_rails/creation_support.rb +99 -0
  12. data/app/controllers/sms_on_rails_controller.rb +14 -0
  13. data/app/helpers/admin/sms_on_rails/drafts_helper.rb +2 -0
  14. data/app/helpers/admin/sms_on_rails/phone_carriers_helper.rb +2 -0
  15. data/app/helpers/sms_on_rails/phone_numbers_helper.rb +9 -0
  16. data/app/helpers/sms_on_rails/sms_helper.rb +44 -0
  17. data/app/models/sms_on_rails/draft.rb +9 -0
  18. data/app/models/sms_on_rails/outbound.rb +17 -0
  19. data/app/models/sms_on_rails/phone_carrier.rb +14 -0
  20. data/app/models/sms_on_rails/phone_number.rb +8 -0
  21. data/app/views/admin/sms_on_rails/base/index.html.erb +5 -0
  22. data/app/views/admin/sms_on_rails/drafts/_show.html.erb +34 -0
  23. data/app/views/admin/sms_on_rails/drafts/edit.html.erb +36 -0
  24. data/app/views/admin/sms_on_rails/drafts/index.html.erb +32 -0
  25. data/app/views/admin/sms_on_rails/drafts/new.html.erb +34 -0
  26. data/app/views/admin/sms_on_rails/drafts/send_sms.html.erb +3 -0
  27. data/app/views/admin/sms_on_rails/drafts/show.html.erb +4 -0
  28. data/app/views/admin/sms_on_rails/outbounds/edit.html.erb +68 -0
  29. data/app/views/admin/sms_on_rails/outbounds/index.html.erb +37 -0
  30. data/app/views/admin/sms_on_rails/outbounds/new.html.erb +54 -0
  31. data/app/views/admin/sms_on_rails/outbounds/show.html.erb +69 -0
  32. data/app/views/admin/sms_on_rails/phone_carriers/edit.html.erb +24 -0
  33. data/app/views/admin/sms_on_rails/phone_carriers/index.html.erb +24 -0
  34. data/app/views/admin/sms_on_rails/phone_carriers/new.html.erb +22 -0
  35. data/app/views/admin/sms_on_rails/phone_carriers/show.html.erb +24 -0
  36. data/app/views/admin/sms_on_rails/phone_numbers/edit.html.erb +33 -0
  37. data/app/views/admin/sms_on_rails/phone_numbers/index.html.erb +28 -0
  38. data/app/views/admin/sms_on_rails/phone_numbers/new.html.erb +31 -0
  39. data/app/views/admin/sms_on_rails/phone_numbers/show.html.erb +32 -0
  40. data/app/views/layouts/sms_on_rails/basic.html.erb +26 -0
  41. data/app/views/sms_on_rails/_phone_carrier_form_item.html.erb +6 -0
  42. data/app/views/sms_on_rails/_send_sms.html.erb +33 -0
  43. data/app/views/sms_on_rails/index.html.erb +8 -0
  44. data/app/views/sms_on_rails/send_sms.html.erb +3 -0
  45. data/app/views/sms_on_rails/show.html.erb +29 -0
  46. data/config/routes.rb +19 -0
  47. data/db/data/fixtures/sms_phone_carriers.yml +110 -0
  48. data/db/migrate/sms_on_rails_carrier_tables.rb +9 -0
  49. data/db/migrate/sms_on_rails_model_tables.rb +48 -0
  50. data/db/migrate/sms_on_rails_phone_number_tables.rb +11 -0
  51. data/db/seed_data.rb +16 -0
  52. data/generators/sms_on_rails/USAGE +31 -0
  53. data/generators/sms_on_rails/commands/inserts.rb +63 -0
  54. data/generators/sms_on_rails/commands/timestamps.rb +33 -0
  55. data/generators/sms_on_rails/runners/add_all_models.rb +6 -0
  56. data/generators/sms_on_rails/runners/dependencies.rb +1 -0
  57. data/generators/sms_on_rails/runners/remove_all_models.rb +5 -0
  58. data/generators/sms_on_rails/runners/sms_on_rails_routes.rb +14 -0
  59. data/generators/sms_on_rails/sms_on_rails_generator.rb +255 -0
  60. data/generators/sms_on_rails/templates/configuration/clickatell.rb +6 -0
  61. data/generators/sms_on_rails/templates/configuration/email_gateway.rb +7 -0
  62. data/generators/sms_on_rails/templates/migrate/schema_migration.rb +15 -0
  63. data/generators/sms_on_rails/templates/migrate/sms_on_rails_update_phone_numbers.rb +40 -0
  64. data/generators/sms_on_rails/templates/phone_number_collision.rb +2 -0
  65. data/init.rb +3 -0
  66. data/install.rb +1 -0
  67. data/lib/sms_on_rails.rb +8 -0
  68. data/lib/sms_on_rails/activerecord_extensions/acts_as_deliverable.rb +92 -0
  69. data/lib/sms_on_rails/activerecord_extensions/acts_as_substitutable.rb +80 -0
  70. data/lib/sms_on_rails/activerecord_extensions/has_a_sms_service_provider.rb +101 -0
  71. data/lib/sms_on_rails/activerecord_extensions/lockable_record.rb +186 -0
  72. data/lib/sms_on_rails/all_models.rb +3 -0
  73. data/lib/sms_on_rails/model_support/draft.rb +178 -0
  74. data/lib/sms_on_rails/model_support/outbound.rb +136 -0
  75. data/lib/sms_on_rails/model_support/phone_carrier.rb +77 -0
  76. data/lib/sms_on_rails/model_support/phone_number.rb +248 -0
  77. data/lib/sms_on_rails/model_support/phone_number_associations.rb +13 -0
  78. data/lib/sms_on_rails/schema_helper.rb +51 -0
  79. data/lib/sms_on_rails/service_providers/base.rb +222 -0
  80. data/lib/sms_on_rails/service_providers/clickatell.rb +52 -0
  81. data/lib/sms_on_rails/service_providers/dummy.rb +19 -0
  82. data/lib/sms_on_rails/service_providers/email_gateway.rb +68 -0
  83. data/lib/sms_on_rails/service_providers/email_gateway_support/errors.rb +20 -0
  84. data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer.rb +21 -0
  85. data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer/sms_through_gateway.erb +6 -0
  86. data/lib/sms_on_rails/util/sms_error.rb +12 -0
  87. data/lib/smsonrails.rb +1 -0
  88. data/public/images/sms_on_rails/railsYoDawg.jpg +0 -0
  89. data/public/stylesheets/sms_on_rails.css +137 -0
  90. data/sms_on_rails.gemspec +32 -0
  91. data/tasks/sms_on_rails_tasks.rake +67 -0
  92. data/test/active_record_extensions/delivery_and_locking_test.rb +84 -0
  93. data/test/models/draft_test.rb +72 -0
  94. data/test/models/outbound_test.rb +89 -0
  95. data/test/models/phone_number_test.rb +131 -0
  96. data/test/run.rb +18 -0
  97. data/test/service_providers/abstract_test_support.rb +104 -0
  98. data/test/service_providers/clickatell_test.rb +39 -0
  99. data/test/service_providers/email_gateway_test.rb +30 -0
  100. data/test/test_helper.rb +24 -0
  101. data/uninstall.rb +1 -0
  102. metadata +187 -0
@@ -0,0 +1,3 @@
1
+ Dir.glob(File.dirname(__FILE__) + "/../../app/models/sms_on_rails/*.rb") {|f| require f;}
2
+
3
+
@@ -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