mail_form 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,239 @@
1
+ module MailForm::Delivery
2
+ extend ActiveSupport::Concern
3
+
4
+ ACCESSORS = [ :mail_attributes, :mail_subject, :mail_captcha,
5
+ :mail_attachments, :mail_recipients, :mail_sender,
6
+ :mail_headers, :mail_template, :mail_appendable ]
7
+
8
+ included do
9
+ class_inheritable_reader *ACCESSORS
10
+ protected *ACCESSORS
11
+
12
+ # Initialize arrays and hashes
13
+ write_inheritable_array :mail_captcha, []
14
+ write_inheritable_array :mail_appendable, []
15
+ write_inheritable_array :mail_attributes, []
16
+ write_inheritable_array :mail_attachments, []
17
+
18
+ headers({})
19
+ sender {|c| c.email }
20
+ subject{|c| c.class.model_name.human }
21
+ template 'contact'
22
+
23
+ before_create :not_spam?
24
+ after_create :deliver!
25
+
26
+ attr_accessor :request
27
+ alias :deliver :create
28
+ end
29
+
30
+ module ClassMethods
31
+ # Declare your form attributes. All attributes declared here will be appended
32
+ # to the e-mail, except the ones captcha is true.
33
+ #
34
+ # == Options
35
+ #
36
+ # * :validate - A hook to validates_*_of. When true is given, validates the
37
+ # presence of the attribute. When a regexp, validates format. When array,
38
+ # validates the inclusion of the attribute in the array.
39
+ #
40
+ # Whenever :validate is given, the presence is automatically checked. Give
41
+ # :allow_blank => true to override.
42
+ #
43
+ # Finally, when :validate is a symbol, the method given as symbol will be
44
+ # called. Then you can add validations as you do in ActiveRecord (errors.add).
45
+ #
46
+ # * <tt>:attachment</tt> - When given, expects a file to be sent and attaches
47
+ # it to the e-mail. Don't forget to set your form to multitype.
48
+ #
49
+ # * <tt>:captcha</tt> - When true, validates the attributes must be blank
50
+ # This is a simple way to avoid spam
51
+ #
52
+ # == Examples
53
+ #
54
+ # class ContactForm < MailForm
55
+ # attributes :name, :validate => true
56
+ # attributes :email, :validate => /^([^@]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
57
+ # attributes :type, :validate => ["General", "Interface bug"]
58
+ # attributes :message
59
+ # attributes :screenshot, :attachment => true, :validate => :interface_bug?
60
+ # attributes :nickname, :captcha => true
61
+ #
62
+ # def interface_bug?
63
+ # if type == 'Interface bug' && screenshot.nil?
64
+ # self.errors.add(:screenshot, "can't be blank when you are reporting an interface bug")
65
+ # end
66
+ # end
67
+ # end
68
+ #
69
+ def attribute(*accessors)
70
+ options = accessors.extract_options!
71
+ attr_accessor *(accessors - instance_methods.map(&:to_sym))
72
+
73
+ if options[:attachment]
74
+ write_inheritable_array(:mail_attachments, accessors)
75
+ elsif options[:captcha]
76
+ write_inheritable_array(:mail_captcha, accessors)
77
+ else
78
+ write_inheritable_array(:mail_attributes, accessors)
79
+ end
80
+
81
+ validation = options.delete(:validate)
82
+ return unless validation
83
+
84
+ accessors.each do |accessor|
85
+ case validation
86
+ when Symbol, Class
87
+ validate validation
88
+ break
89
+ when Regexp
90
+ validates_format_of accessor, :with => validation, :allow_blank => true
91
+ when Array
92
+ validates_inclusion_of accessor, :in => validation, :allow_blank => true
93
+ when Range
94
+ validates_length_of accessor, :within => validation, :allow_blank => true
95
+ end
96
+
97
+ validates_presence_of accessor unless options[:allow_blank] == true
98
+ end
99
+ end
100
+ alias :attributes :attribute
101
+
102
+ # Declares contact email sender. It can be a string or a proc or a symbol.
103
+ #
104
+ # When a symbol is given, it will call a method on the form object with
105
+ # the same name as the symbol. As a proc, it receives a simple form
106
+ # instance. By default is the class human name.
107
+ #
108
+ # == Examples
109
+ #
110
+ # class ContactForm < MailForm
111
+ # subject "My Contact Form"
112
+ # end
113
+ #
114
+ def subject(duck=nil, &block)
115
+ write_inheritable_attribute(:mail_subject, duck || block)
116
+ end
117
+
118
+ # Declares contact email sender. It can be a string or a proc or a symbol.
119
+ #
120
+ # When a symbol is given, it will call a method on the form object with
121
+ # the same name as the symbol. As a proc, it receives a simple form
122
+ # instance. By default is:
123
+ #
124
+ # sender{ |c| c.email }
125
+ #
126
+ # This requires that your MailForm object have an email attribute.
127
+ #
128
+ # == Examples
129
+ #
130
+ # class ContactForm < MailForm
131
+ # # Change sender to include also the name
132
+ # sender { |c| %{"#{c.name}" <#{c.email}>} }
133
+ # end
134
+ #
135
+ def sender(duck=nil, &block)
136
+ write_inheritable_attribute(:mail_sender, duck || block)
137
+ end
138
+ alias :from :sender
139
+
140
+ # Who will receive the e-mail. Can be a string or array or a symbol or a proc.
141
+ #
142
+ # When a symbol is given, it will call a method on the form object with
143
+ # the same name as the symbol. As a proc, it receives a simple form instance.
144
+ #
145
+ # Both the proc and the symbol must return a string or an array. By default
146
+ # is nil.
147
+ #
148
+ # == Examples
149
+ #
150
+ # class ContactForm < MailForm
151
+ # recipients [ "first.manager@domain.com", "second.manager@domain.com" ]
152
+ # end
153
+ #
154
+ def recipients(duck=nil, &block)
155
+ write_inheritable_attribute(:mail_recipients, duck || block)
156
+ end
157
+ alias :to :recipients
158
+
159
+ # Additional headers to your e-mail.
160
+ #
161
+ # == Examples
162
+ #
163
+ # class ContactForm < MailForm
164
+ # headers { :content_type => 'text/html' }
165
+ # end
166
+ #
167
+ def headers(hash)
168
+ write_inheritable_hash(:mail_headers, hash)
169
+ end
170
+
171
+ # Customized template for your e-mail, if you don't want to use default
172
+ # 'contact' template or need more than one contact form with different
173
+ # template layouts.
174
+ #
175
+ # When a symbol is given, it will call a method on the form object with
176
+ # the same name as the symbol. As a proc, it receives a simple form
177
+ # instance. Both method and proc must return a string with the template
178
+ # name. Defaults to 'contact'.
179
+ #
180
+ # == Examples
181
+ #
182
+ # class ContactForm < MailForm
183
+ # # look for a template in views/mail_form/notifier/my_template.erb
184
+ # template 'my_template'
185
+ # end
186
+ #
187
+ def template(new_template)
188
+ write_inheritable_attribute(:mail_template, new_template)
189
+ end
190
+
191
+ # Values from request object to be appended to the contact form.
192
+ # Whenever used, you have to send the request object when initializing the object:
193
+ #
194
+ # @contact_form = ContactForm.new(params[:contact_form], request)
195
+ #
196
+ # You can get the values to be appended from the AbstractRequest
197
+ # documentation (http://api.rubyonrails.org/classes/ActionController/AbstractRequest.html)
198
+ #
199
+ # == Examples
200
+ #
201
+ # class ContactForm < MailForm
202
+ # append :remote_ip, :user_agent, :session, :cookies
203
+ # end
204
+ #
205
+ def append(*values)
206
+ write_inheritable_array(:mail_appendable, values)
207
+ end
208
+ end
209
+
210
+ # In development, raises an error if the captcha field is not blank. This is
211
+ # is good to remember that the field should be hidden with CSS and shown only
212
+ # to robots.
213
+ #
214
+ # In test and in production, it returns true if all captcha fields are blank,
215
+ # returns false otherwise.
216
+ #
217
+ def spam?
218
+ mail_captcha.each do |field|
219
+ next if send(field).blank?
220
+
221
+ if defined?(Rails) && Rails.env.development?
222
+ raise ScriptError, "The captcha field #{field} was supposed to be blank"
223
+ else
224
+ return true
225
+ end
226
+ end
227
+
228
+ false
229
+ end
230
+
231
+ def not_spam?
232
+ !spam?
233
+ end
234
+
235
+ # Deliver the resource without checking any condition.
236
+ def deliver!
237
+ MailForm.contact(self).deliver
238
+ end
239
+ end
@@ -0,0 +1,50 @@
1
+ require 'active_model'
2
+
3
+ # This the module which makes any class behave like ActiveModel.
4
+ module MailForm::Shim
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ActiveModel::Naming
8
+ extend ActiveModel::Translation
9
+ extend ActiveModel::Callbacks
10
+ include ActiveModel::Validations
11
+ include ActiveModel::Conversion
12
+
13
+ extend MailForm::Shim::ClassMethods
14
+ define_model_callbacks :create
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def i18n_scope
20
+ :mail_form
21
+ end
22
+ end
23
+
24
+ # Initialize assigning the parameters given as hash (just as in ActiveRecord).
25
+ def initialize(params={})
26
+ params.each_pair do |attr, value|
27
+ self.send(:"#{attr}=", value)
28
+ end unless params.blank?
29
+ end
30
+
31
+ # Always return true so when using form_for, the default method will be post.
32
+ def new_record?
33
+ true
34
+ end
35
+
36
+ # Always return nil so when using form_for, the default method will be post.
37
+ def id
38
+ nil
39
+ end
40
+
41
+ # Create just check validity, and if so, trigger callbacks.
42
+ def create
43
+ if valid?
44
+ _run_create_callbacks { true }
45
+ else
46
+ false
47
+ end
48
+ end
49
+ alias :save :create
50
+ end
@@ -1,3 +1,3 @@
1
1
  module MailForm
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "1.1.0".freeze
3
3
  end
@@ -0,0 +1,38 @@
1
+ <h4 style="text-decoration:underline"><%= mailer.subject %></h4>
2
+
3
+ <% @resource.class.mail_attributes.each do |attribute|
4
+ value = @resource.send(attribute)
5
+ next if value.blank? %>
6
+
7
+ <p><b><%= @resource.class.human_attribute_name(attribute) %>:</b>
8
+ <%= case value
9
+ when /\n/
10
+ raw(simple_format(h(value)))
11
+ when Time, DateTime, Date
12
+ I18n.l value
13
+ else
14
+ value
15
+ end
16
+ %></p>
17
+ <% end %>
18
+
19
+ <% unless @resource.class.mail_appendable.blank? %>
20
+ <br /><h4 style="text-decoration:underline"><%= I18n.t :title, :scope => [ :mail_form, :request ], :default => 'Request information' %></h4>
21
+
22
+ <% @resource.class.mail_appendable.each do |attribute|
23
+ value = @resource.request.send(attribute)
24
+
25
+ value = if value.is_a?(Hash) && !value.empty?
26
+ content_tag(:ul, value.to_a.map{|k,v| content_tag(:li, h("#{k}: #{v.inspect}")) }.join("\n"), :style => "list-style:none;")
27
+ elsif value.is_a?(String)
28
+ h(value)
29
+ else
30
+ h(value.inspect)
31
+ end
32
+ %>
33
+
34
+ <p><b><%= I18n.t attribute, :scope => [ :mail_form, :request ], :default => attribute.to_s.humanize %>:</b>
35
+ <%= value.include?("\n") ? raw(simple_format(value)) : value %></p>
36
+ <% end %>
37
+ <br />
38
+ <% end %>
@@ -0,0 +1,190 @@
1
+ require 'test_helper'
2
+
3
+ class MailFormNotifierTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ @form = ContactForm.new(:name => 'José', :email => 'my.email@my.domain.com', :message => 'Cool')
7
+
8
+ @request = ActionController::TestRequest.new
9
+ @valid_attributes = { :name => 'José', :email => 'my.email@my.domain.com', :message => "Cool\nno?" }
10
+ @advanced = AdvancedForm.new(@valid_attributes)
11
+ @advanced.request = @request
12
+
13
+ test_file = Rack::Test::UploadedFile.new(File.join(File.dirname(__FILE__), 'test_file.txt'))
14
+ @with_file = FileForm.new(:name => 'José', :email => 'my.email@my.domain.com', :message => "Cool", :file => test_file)
15
+
16
+ @template = TemplateForm.new(@valid_attributes)
17
+
18
+ ActionMailer::Base.deliveries = []
19
+ end
20
+
21
+ def test_email_is_sent
22
+ @form.deliver
23
+ assert_equal 1, ActionMailer::Base.deliveries.size
24
+ end
25
+
26
+ def test_subject_defaults_to_class_human_name
27
+ @form.deliver
28
+ assert_equal 'Contact form', first_delivery.subject
29
+ end
30
+
31
+ def test_subject_is_a_string
32
+ @advanced.deliver
33
+ assert_equal 'My Advanced Form', first_delivery.subject
34
+ end
35
+
36
+ def test_sender_defaults_to_form_email
37
+ @form.deliver
38
+ assert_equal ['my.email@my.domain.com'], first_delivery.from
39
+ end
40
+
41
+ def test_error_is_raised_when_recipients_is_nil
42
+ assert_raise ScriptError do
43
+ NullRecipient.new.deliver
44
+ end
45
+ end
46
+
47
+ def test_recipients_is_a_string
48
+ @form.deliver
49
+ assert_equal ['my.email@my.domain.com'], first_delivery.to
50
+ end
51
+
52
+ def test_recipients_is_an_array
53
+ @advanced.deliver
54
+ assert_equal ['my.first@email.com', 'my.second@email.com'], first_delivery.to
55
+ end
56
+
57
+ def test_recipients_is_a_symbold
58
+ @with_file.deliver
59
+ assert_equal ['contact_file@my.domain.com'], first_delivery.to
60
+ end
61
+
62
+ def test_headers_is_a_hash
63
+ @advanced.deliver
64
+ assert_equal 'mypath', first_delivery.header['return-path'].to_s
65
+ end
66
+
67
+ def test_body_contains_subject
68
+ @form.deliver
69
+ assert_match /Contact form/, first_delivery.body.to_s
70
+ end
71
+
72
+ def test_body_contains_attributes_values
73
+ @form.deliver
74
+ assert_match /José/, first_delivery.body.to_s
75
+ assert_match /my.email@my.domain.com/, first_delivery.body.to_s
76
+ assert_match /Cool/, first_delivery.body.to_s
77
+ end
78
+
79
+ def test_body_contains_attributes_names
80
+ @form.deliver
81
+ assert_match /Name:/, first_delivery.body.to_s
82
+ assert_match /Email:/, first_delivery.body.to_s
83
+ assert_match /Message:/, first_delivery.body.to_s
84
+ end
85
+
86
+ def test_body_contains_localized_attributes_names
87
+ I18n.backend.store_translations(:en, :mail_form => { :attributes => { :contact_form => { :message => 'Sent message' } } })
88
+ @form.deliver
89
+ assert_match /Sent message:/, first_delivery.body.to_s
90
+ assert_no_match /Message:/, first_delivery.body.to_s
91
+ end
92
+
93
+ def test_body_mail_format_messages_with_break_line
94
+ @form.deliver
95
+ assert_no_match /<p>Cool/, first_delivery.body.to_s
96
+
97
+ @advanced.deliver
98
+ assert_match /<p>Cool/, last_delivery.body.to_s
99
+ end
100
+
101
+ def test_body_mail_format_dates_with_i18n
102
+ @form.deliver
103
+ assert_no_match /I18n.l(Date.today)/, first_delivery.body.to_s
104
+ end
105
+
106
+ def test_body_does_not_append_request_if_append_is_not_called
107
+ @form.deliver
108
+ assert_no_match /Request information/, first_delivery.body.to_s
109
+ end
110
+
111
+ def test_body_does_append_request_if_append_is_called
112
+ @advanced.deliver
113
+ assert_match /Request information/, last_delivery.body.to_s
114
+ end
115
+
116
+ def test_request_title_is_localized
117
+ I18n.backend.store_translations(:en, :mail_form => { :request => { :title => 'Information about the request' } })
118
+ @advanced.deliver
119
+ assert_no_match /Request information/, last_delivery.body.to_s
120
+ assert_match /Information about the request/, last_delivery.body.to_s
121
+ end
122
+
123
+ def test_request_info_attributes_are_printed
124
+ @advanced.deliver
125
+ assert_match /Remote ip/, last_delivery.body.to_s
126
+ assert_match /User agent/, last_delivery.body.to_s
127
+ end
128
+
129
+ def test_request_info_attributes_are_localized
130
+ I18n.backend.store_translations(:en, :mail_form => { :request => { :remote_ip => 'IP Address' } })
131
+ @advanced.deliver
132
+ assert_match /IP Address/, last_delivery.body.to_s
133
+ assert_no_match /Remote ip/, last_delivery.body.to_s
134
+ end
135
+
136
+ def test_request_info_values_are_printed
137
+ @advanced.deliver
138
+ assert_match /0\.0\.0\.0/, last_delivery.body.to_s
139
+ assert_match /Rails Testing/, last_delivery.body.to_s
140
+ end
141
+
142
+ def test_request_info_hashes_are_print_inside_lis
143
+ @request.session = { :my => :session, :user => "data" }
144
+ @advanced.deliver
145
+ assert_match /<li>my: :session<\/li>/, last_delivery.body.to_s
146
+ assert_match /<li>user: &quot;data&quot;<\/li>/, last_delivery.body.to_s
147
+ end
148
+
149
+ def test_error_is_raised_when_append_is_given_but_no_request_is_given
150
+ assert_raise ScriptError do
151
+ @advanced.request = nil
152
+ @advanced.deliver
153
+ end
154
+ end
155
+
156
+ def test_form_with_file_includes_an_attachment
157
+ @with_file.deliver
158
+ assert_equal 1, first_delivery.attachments.size
159
+ end
160
+
161
+ def test_form_with_file_does_not_output_attachment_as_attribute
162
+ @with_file.deliver
163
+ assert_no_match /File:/, first_delivery.body.to_s
164
+ end
165
+
166
+ def test_form_with_customized_template_render_correct_template
167
+ begin
168
+ previous_view_path = MailForm.view_paths
169
+ MailForm.prepend_view_path File.join(File.dirname(__FILE__), 'views')
170
+ @template.deliver
171
+ assert_match 'Hello from my custom template!', last_delivery.body.to_s
172
+ ensure
173
+ MailForm.view_paths = previous_view_path
174
+ end
175
+ end
176
+
177
+ protected
178
+
179
+ def first_delivery
180
+ ActionMailer::Base.deliveries.first
181
+ end
182
+
183
+ def last_delivery
184
+ ActionMailer::Base.deliveries.last
185
+ end
186
+
187
+ def teardown
188
+ I18n.reload!
189
+ end
190
+ end