Skip to content

Commit fd6cd8d

Browse files
committed
Refactor survey invitations
+ Introduce Form Object + Set stage for Extract Validator
1 parent 72c2a0d commit fd6cd8d

File tree

9 files changed

+253
-53
lines changed

9 files changed

+253
-53
lines changed
Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,29 @@
11
class InvitationsController < ApplicationController
2-
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
3-
42
def new
53
@survey = Survey.find(params[:survey_id])
4+
@survey_inviter = SurveyInviter.new
65
end
76

87
def create
98
@survey = Survey.find(params[:survey_id])
10-
if valid_recipients? && valid_message?
11-
recipient_list.each do |email|
12-
invitation = Invitation.create(
13-
survey: @survey,
14-
sender: current_user,
15-
recipient_email: email,
16-
status: 'pending'
17-
)
18-
Mailer.invitation_notification(invitation, message)
19-
end
9+
@survey_inviter = SurveyInviter.new(survey_inviter_params)
10+
11+
if @survey_inviter.invite
2012
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
2113
else
22-
@recipients = recipients
23-
@message = message
2414
render 'new'
2515
end
2616
end
2717

2818
private
2919

30-
def valid_recipients?
31-
invalid_recipients.empty?
32-
end
33-
34-
def valid_message?
35-
message.present?
36-
end
37-
38-
def invalid_recipients
39-
@invalid_recipients ||= recipient_list.map do |item|
40-
unless item.match(EMAIL_REGEX)
41-
item
42-
end
43-
end.compact
44-
end
45-
46-
def recipient_list
47-
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
48-
end
49-
50-
def recipients
51-
params[:invitation][:recipients]
52-
end
53-
54-
def message
55-
params[:invitation][:message]
20+
def survey_inviter_params
21+
params.require(:survey_inviter).permit(
22+
:message,
23+
:recipients
24+
).merge(
25+
sender: current_user,
26+
survey: @survey
27+
)
5628
end
5729
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class RecipientList
2+
include Enumerable
3+
4+
def initialize(recipient_string)
5+
@recipient_string = recipient_string
6+
end
7+
8+
def each(&block)
9+
recipients.each(&block)
10+
end
11+
12+
def to_s
13+
@recipient_string
14+
end
15+
16+
private
17+
18+
def recipients
19+
@recipient_string.to_s.gsub(/\s+/, '').split(/[\n,;]+/)
20+
end
21+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class SurveyInviter
2+
include ActiveModel::Model
3+
attr_accessor :recipients, :message, :sender, :survey
4+
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
5+
6+
validates :message, presence: true
7+
validates :recipients, presence: true
8+
validates :sender, presence: true
9+
validates :survey, presence: true
10+
11+
validate :recipient_email_validator
12+
13+
def recipients=(recipients)
14+
unless recipients.blank?
15+
@recipients = RecipientList.new(recipients)
16+
end
17+
end
18+
19+
def invite
20+
if valid?
21+
deliver_invitations
22+
end
23+
end
24+
25+
private
26+
27+
def create_invitations
28+
recipients.map do |recipient_email|
29+
Invitation.create!(
30+
survey: survey,
31+
sender: sender,
32+
recipient_email: recipient_email,
33+
status: 'pending'
34+
)
35+
end
36+
end
37+
38+
def deliver_invitations
39+
create_invitations.each do |invitation|
40+
Mailer.invitation_notification(invitation, message).deliver
41+
end
42+
end
43+
44+
def recipient_email_validator
45+
return if recipients.blank?
46+
47+
recipients.each do |recipient|
48+
unless recipient.match(EMAIL_REGEX)
49+
errors.add(:recipients, "#{recipient} is not a valid email address.")
50+
end
51+
end
52+
end
53+
end
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
<%= simple_form_for :invitation, url: survey_invitations_path(@survey) do |f| %>
2-
<%= f.input :message, as: :text, input_html: { value: @message } %>
3-
<% if @invlid_message %>
4-
<div class="error">Please provide a message</div>
5-
<% end %>
6-
<%= f.input :recipients, as: :text, input_html: { value: @recipients } %>
7-
<% if @invalid_recipients %>
8-
<div class="error">
9-
Invalid email addresses:
10-
<%= @invalid_recipients.join(', ') %>
11-
</div>
12-
<% end %>
1+
<%= simple_form_for @survey_inviter,
2+
url: survey_invitations_path(@survey) do |f| %>
3+
<%= f.input :message, as: :text %>
4+
<%= f.input :recipients, as: :text %>
135
<%= f.button :submit, value: 'Invite' %>
146
<% end %>

example_app/config/application.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Application < Rails::Application
2222
# -- all .rb files in that directory are automatically loaded.
2323

2424
# Custom directories with classes and modules you want to be autoloadable.
25-
# config.autoload_paths += %W(#{config.root}/extras)
25+
config.autoload_paths += %W(#{config.root}/lib)
2626

2727
# Only load the plugins named here, in the order given (default is alphabetical).
2828
# :all can be used as a placeholder for all plugins not explicitly named.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
module ActiveModel
2+
3+
# == Active \Model Basic \Model
4+
#
5+
# Includes the required interface for an object to interact with
6+
# <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
7+
# It includes model name introspections, conversions, translations and
8+
# validations. Besides that, it allows you to initialize the object with a
9+
# hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
10+
#
11+
# A minimal implementation could be:
12+
#
13+
# class Person
14+
# include ActiveModel::Model
15+
# attr_accessor :name, :age
16+
# end
17+
#
18+
# person = Person.new(name: 'bob', age: '18')
19+
# person.name # => 'bob'
20+
# person.age # => 18
21+
#
22+
# Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
23+
# to return +false+, which is the most common case. You may want to override
24+
# it in your class to simulate a different scenario:
25+
#
26+
# class Person
27+
# include ActiveModel::Model
28+
# attr_accessor :id, :name
29+
#
30+
# def persisted?
31+
# self.id == 1
32+
# end
33+
# end
34+
#
35+
# person = Person.new(id: 1, name: 'bob')
36+
# person.persisted? # => true
37+
#
38+
# Also, if for some reason you need to run code on <tt>initialize</tt>, make
39+
# sure you call +super+ if you want the attributes hash initialization to
40+
# happen.
41+
#
42+
# class Person
43+
# include ActiveModel::Model
44+
# attr_accessor :id, :name, :omg
45+
#
46+
# def initialize(attributes={})
47+
# super
48+
# @omg ||= true
49+
# end
50+
# end
51+
#
52+
# person = Person.new(id: 1, name: 'bob')
53+
# person.omg # => true
54+
#
55+
# For more detailed information on other functionalities available, please
56+
# refer to the specific modules included in <tt>ActiveModel::Model</tt>
57+
# (see below).
58+
module Model
59+
def self.included(base) #:nodoc:
60+
base.class_eval do
61+
extend ActiveModel::Naming
62+
extend ActiveModel::Translation
63+
include ActiveModel::Validations
64+
include ActiveModel::Conversion
65+
end
66+
end
67+
68+
# Initializes a new model with the given +params+.
69+
#
70+
# class Person
71+
# include ActiveModel::Model
72+
# attr_accessor :name, :age
73+
# end
74+
#
75+
# person = Person.new(name: 'bob', age: '18')
76+
# person.name # => "bob"
77+
# person.age # => 18
78+
def initialize(params={})
79+
params.each do |attr, value|
80+
self.public_send("#{attr}=", value)
81+
end if params
82+
end
83+
84+
# Indicates if the model is persisted. Default is +false+.
85+
#
86+
# class Person
87+
# include ActiveModel::Model
88+
# attr_accessor :id, :name
89+
# end
90+
#
91+
# person = Person.new(id: 1, name: 'bob')
92+
# person.persisted? # => false
93+
def persisted?
94+
false
95+
end
96+
end
97+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'spec_helper'
2+
3+
describe '#map' do
4+
it 'parses recipients separated by commas' do
5+
recipient_list('one@example.com, two@example.com').should
6+
eq ['one@example.com', 'two@example.com']
7+
end
8+
9+
it 'parses recipients separated by newlines' do
10+
recipient_list("one@example.com\ntwo@example.com").should
11+
eq ['one@example.com', 'two@example.com']
12+
end
13+
14+
it 'parses recipients separated by semi-colons' do
15+
recipient_list("one@example.com; two@example.com").should
16+
eq ['one@example.com', 'two@example.com']
17+
end
18+
19+
def recipient_list(string)
20+
RecipientList.new(string).map(&:to_s)
21+
end
22+
end
23+
24+
describe RecipientList, '#to_s' do
25+
it 'returns the original string' do
26+
RecipientList.new('some string').to_s.should eq 'some string'
27+
end
28+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require 'spec_helper'
2+
3+
describe SurveyInviter, 'Validations' do
4+
it { should validate_presence_of(:message) }
5+
it { should validate_presence_of(:recipients) }
6+
it { should validate_presence_of(:sender) }
7+
it { should validate_presence_of(:survey) }
8+
end
9+
10+
describe SurveyInviter, '#invite' do
11+
it 'invites a valid recipient' do
12+
SurveyInviter.new(valid_params).invite
13+
14+
Invitation.count.should eq 1
15+
ActionMailer::Base.deliveries.count.should eq 1
16+
end
17+
18+
it 'returns false for an invalid recipient' do
19+
SurveyInviter.new(invalid_params).invite.should be_false
20+
end
21+
22+
def valid_params
23+
{
24+
survey: build(:survey),
25+
sender: build(:sender),
26+
message: 'Take my survey!',
27+
recipients: 'valid@example.com'
28+
}
29+
end
30+
31+
def invalid_params
32+
valid_params.merge(
33+
recipients: 'invalid_email'
34+
)
35+
end
36+
end

example_app/spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
config.mock_framework = :mocha
1717
config.include FactoryGirl::Syntax::Methods
1818
config.include Features, type: :feature
19+
config.before(:each) { ActionMailer::Base.deliveries.clear }
1920
end

0 commit comments

Comments
 (0)