mail_form 1.0.0 → 1.1.0

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.
@@ -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