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.
Files changed (103) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Manifest +101 -0
  3. data/README.rdoc +211 -0
  4. data/Rakefile +109 -0
  5. data/VERSION +1 -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 +54 -0
  81. data/lib/sms_on_rails/service_providers/dummy.rb +21 -0
  82. data/lib/sms_on_rails/service_providers/email_gateway.rb +70 -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/smsonrails.gemspec +159 -0
  91. data/sztywny-smsonrails.gemspec +148 -0
  92. data/tasks/sms_on_rails_tasks.rake +68 -0
  93. data/test/active_record_extensions/delivery_and_locking_test.rb +84 -0
  94. data/test/models/draft_test.rb +72 -0
  95. data/test/models/outbound_test.rb +89 -0
  96. data/test/models/phone_number_test.rb +131 -0
  97. data/test/run.rb +19 -0
  98. data/test/service_providers/abstract_test_support.rb +104 -0
  99. data/test/service_providers/clickatell_test.rb +37 -0
  100. data/test/service_providers/email_gateway_test.rb +30 -0
  101. data/test/test_helper.rb +24 -0
  102. data/uninstall.rb +1 -0
  103. metadata +193 -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