Permalink
Browse files

Refactor survey invitations

+ Introduce Form Object
+ Set stage for Extract Validator
  • Loading branch information...
harlow committed Dec 18, 2012
1 parent 72c2a0d commit fd6cd8d5a6273c4b4edf6b8b84942d6342b37b5f
@@ -1,57 +1,29 @@
class InvitationsController < ApplicationController
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

def new
@survey = Survey.find(params[:survey_id])
@survey_inviter = SurveyInviter.new
end

def create
@survey = Survey.find(params[:survey_id])
if valid_recipients? && valid_message?
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, message)
end
@survey_inviter = SurveyInviter.new(survey_inviter_params)

if @survey_inviter.invite
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
@recipients = recipients
@message = message
render 'new'
end
end

private

def valid_recipients?
invalid_recipients.empty?
end

def valid_message?
message.present?
end

def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
end

def recipient_list
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end

def recipients
params[:invitation][:recipients]
end

def message
params[:invitation][:message]
def survey_inviter_params
params.require(:survey_inviter).permit(
:message,
:recipients
).merge(
sender: current_user,
survey: @survey
)
end
end
@@ -0,0 +1,21 @@
class RecipientList
include Enumerable

def initialize(recipient_string)
@recipient_string = recipient_string
end

def each(&block)
recipients.each(&block)
end

def to_s
@recipient_string
end

private

def recipients
@recipient_string.to_s.gsub(/\s+/, '').split(/[\n,;]+/)
end
end
@@ -0,0 +1,53 @@
class SurveyInviter
include ActiveModel::Model
attr_accessor :recipients, :message, :sender, :survey
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i

validates :message, presence: true
validates :recipients, presence: true
validates :sender, presence: true
validates :survey, presence: true

validate :recipient_email_validator

def recipients=(recipients)
unless recipients.blank?
@recipients = RecipientList.new(recipients)
end
end

def invite
if valid?
deliver_invitations
end
end

private

def create_invitations
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending'
)
end
end

def deliver_invitations
create_invitations.each do |invitation|
Mailer.invitation_notification(invitation, message).deliver
end
end

def recipient_email_validator
return if recipients.blank?

recipients.each do |recipient|
unless recipient.match(EMAIL_REGEX)
errors.add(:recipients, "#{recipient} is not a valid email address.")
end
end
end
end
@@ -1,14 +1,6 @@
<%= simple_form_for :invitation, url: survey_invitations_path(@survey) do |f| %>
<%= f.input :message, as: :text, input_html: { value: @message } %>
<% if @invlid_message %>
<div class="error">Please provide a message</div>
<% end %>
<%= f.input :recipients, as: :text, input_html: { value: @recipients } %>
<% if @invalid_recipients %>
<div class="error">
Invalid email addresses:
<%= @invalid_recipients.join(', ') %>
</div>
<% end %>
<%= simple_form_for @survey_inviter,
url: survey_invitations_path(@survey) do |f| %>
<%= f.input :message, as: :text %>
<%= f.input :recipients, as: :text %>
<%= f.button :submit, value: 'Invite' %>
<% end %>
@@ -22,7 +22,7 @@ class Application < Rails::Application
# -- all .rb files in that directory are automatically loaded.

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

# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
@@ -0,0 +1,97 @@
module ActiveModel

# == Active \Model Basic \Model
#
# Includes the required interface for an object to interact with
# <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
# It includes model name introspections, conversions, translations and
# validations. Besides that, it allows you to initialize the object with a
# hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
#
# A minimal implementation could be:
#
# class Person
# include ActiveModel::Model
# attr_accessor :name, :age
# end
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => 'bob'
# person.age # => 18
#
# Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
# to return +false+, which is the most common case. You may want to override
# it in your class to simulate a different scenario:
#
# class Person
# include ActiveModel::Model
# attr_accessor :id, :name
#
# def persisted?
# self.id == 1
# end
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.persisted? # => true
#
# Also, if for some reason you need to run code on <tt>initialize</tt>, make
# sure you call +super+ if you want the attributes hash initialization to
# happen.
#
# class Person
# include ActiveModel::Model
# attr_accessor :id, :name, :omg
#
# def initialize(attributes={})
# super
# @omg ||= true
# end
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.omg # => true
#
# For more detailed information on other functionalities available, please
# refer to the specific modules included in <tt>ActiveModel::Model</tt>
# (see below).
module Model
def self.included(base) #:nodoc:
base.class_eval do
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
end
end

# Initializes a new model with the given +params+.
#
# class Person
# include ActiveModel::Model
# attr_accessor :name, :age
# end
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => 18
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
end if params
end

# Indicates if the model is persisted. Default is +false+.
#
# class Person
# include ActiveModel::Model
# attr_accessor :id, :name
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.persisted? # => false
def persisted?
false
end
end
end
@@ -0,0 +1,28 @@
require 'spec_helper'

describe '#map' do
it 'parses recipients separated by commas' do
recipient_list('one@example.com, two@example.com').should
eq ['one@example.com', 'two@example.com']
end

it 'parses recipients separated by newlines' do
recipient_list("one@example.com\ntwo@example.com").should
eq ['one@example.com', 'two@example.com']
end

it 'parses recipients separated by semi-colons' do
recipient_list("one@example.com; two@example.com").should
eq ['one@example.com', 'two@example.com']
end

def recipient_list(string)
RecipientList.new(string).map(&:to_s)
end
end

describe RecipientList, '#to_s' do
it 'returns the original string' do
RecipientList.new('some string').to_s.should eq 'some string'
end
end
@@ -0,0 +1,36 @@
require 'spec_helper'

describe SurveyInviter, 'Validations' do
it { should validate_presence_of(:message) }
it { should validate_presence_of(:recipients) }
it { should validate_presence_of(:sender) }
it { should validate_presence_of(:survey) }
end

describe SurveyInviter, '#invite' do
it 'invites a valid recipient' do
SurveyInviter.new(valid_params).invite

Invitation.count.should eq 1
ActionMailer::Base.deliveries.count.should eq 1
end

it 'returns false for an invalid recipient' do
SurveyInviter.new(invalid_params).invite.should be_false
end

def valid_params
{
survey: build(:survey),
sender: build(:sender),
message: 'Take my survey!',
recipients: 'valid@example.com'
}
end

def invalid_params
valid_params.merge(
recipients: 'invalid_email'
)
end
end
@@ -16,4 +16,5 @@
config.mock_framework = :mocha
config.include FactoryGirl::Syntax::Methods
config.include Features, type: :feature
config.before(:each) { ActionMailer::Base.deliveries.clear }
end

0 comments on commit fd6cd8d

Please sign in to comment.