February 18, 2011
#44 A Contact Form - Implementation
In this post, we'll add some configuration options to our contact_form gem that we created in Post 43. Then we will incorporate the gem into our app.
ContactForm Configuration With Blocks
Before we add our contact_form gem, let's create two configuration options. The first will allow us to set a before filter for our ContactController so that we can require a user to login before contacting us. The second configuration will allow us to set the Form email attribute if we have that information available.
To do this, we will borrow a technique from the RailsAdmin gem. We will create two module methods for ContactForm. Each method will act as both an accessor and a getter depending on whether a block is given to the method. If a block is given, the block is stored. If a block is not given, the stored block is returned. (Depending on your preference, you can write these methods with an explicit block parameter or not.)
In our ContactController, we can reference these configuration options. The trick here is that the blocks that we pass to our configuration methods will contain code that may reference methods defined in ApplicationController, which is defined in our Rails app. Therefore, we need to use #instance_eval to specify the scope/context of the code in the block to reference ContactController (and thus ApplicationController.)
For example, when we call ContactForm.before_filter with a block, that block will be stored as a Proc object in @filter. Later when we call ContactForm.before_filter without a block, the saved Proc object is returned. When this Proc object is called prefixed with an ampersand, '&', the Proc object is turned back into a block. This block is then evaluated in the context of the ContactController instance that is generated by Rails. Thus the code in the block can make reference to instance methods defined in ApplicationController.
# contact_form.rb module ContactForm ... def self.before_filter(&block) @filter = block if block @filter || Proc.new {} end def self.form_email @email = Proc.new if block_given? @email || Proc.new {} end end _____________________________________________________ # contact_controller.rb module ContactForm class ContactController < ::ApplicationController before_filter :_before_filter def new @form = ContactForm::Form.new(:email => _email) ... private def _before_filter instance_eval &ContactForm.before_filter end def _email instance_eval &ContactForm.form_email end
Setting Up ContactFrom
With the ContactForm configurations available, we can add contact_form to our application's Gemfile and install it. (While here, we will upgrade to Rails 3.0.4. BTW: During development of a gem, we can use our local gem source code by specifying a :path option.)
In order to configure ContactForm, we create an initializer file. We will restrict access to our contact form to users who are logged in. (#logged_in? will be defined in ApplicationController.) If the user is not logged in, we redirect him to the login page. Before we redirect, we set a session variable with information that we'll use to redirect the user to the contact form page after he has logged in.
Depending on which strategy the user chooses to login with, we may have the user's email available to us. If so, we'll store that email in another session variable. Then we can populate that field in the form.
Lastly, we overwrite some of the default ContactForm YAML settings.
# Gemfile ... gem 'rails', '3.0.4' gem 'contact_form', :git => "git://github.com/arailsdemo/contact_form.git" # gem 'contact_form', :path => "/local/path/to/contact_form" _____________________________________________________ # Terminal $ bundle _____________________________________________________ # contact_form.rb ContactForm.before_filter do unless logged_in? session[:redirect_after_login] = { :url => new_contact_form_url, :message => 'Thanks for logging in.' } redirect_to login_path, :notice => 'Please login first in order to contact us.' end end ContactForm.form_email do session[:email] end _____________________________________________________ # en.yml en: contact_form: headers: to: 'arailsdemo@example.com' redirect_url: 'posts_url'
The Controllers
In our ApplicationController, we define the #logged_in? method. A user is logged in if he has a :provider session variable. This variable is set after he has successfully authenticated through OmniAuth (see Post #19.)
In our SessionsController#create action, we save the session[:redirect_after_login] information to a local variable before resetting the session. After saving the relevant user information retrieved by OmniAuth, we can redirect the user to the contact form page.
# application_controller.rb class ApplicationController < ActionController::Base ... protected def logged_in? session[:provider] end _____________________________________________________ # sessions_controller.rb class SessionsController < ApplicationController ... def create redirect_after_login = session[:redirect_after_login] reset_session info = request.env["omniauth.auth"] session[:email] = info["email"] session[:provider] = info["proivider"] # store other OmniAuth related user information here if redirect_after_login redirect_to redirect_after_login[:url], :notice => redirect_after_login[:message] else redirect_to posts_url, :notice => "Welcome #{session[:name]}!" end end ... protected def handle_unverified_request true end
CSRF Protection In Rails 3.0.4
Rails 3.0.4 addressed a security vulnerability that could allow CSRF. The authenticity "token will now be required for all non-GET requests." If it is not present, the default behavior will be to reset the session. This affects our ability to persist session information when using certain OmniAuth strategies. For our app, using Google via OpenId would clear our session information. Authenticating through Twitter does not reset the session. (We're using OmniAuth version 0.1.6 here.)
Since we are resetting the session explicitly in our SessionsController, we can remove the call to #reset_session in #handle_unverified_request for that controller (see previous snippet.)
(Another approach to dealing with this issue would be to set the :redirect_after_login information in a cookie and avoid session variables.)
# request_forgery_protection.rb module ActionController module RequestForgeryProtection ... protected # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token verified_request? || handle_unverified_request end def handle_unverified_request reset_session end
Sendgrid
Since our app is on Heroku, we can use the Sendgrid add-on to enable emails to be sent via ActionMailer through Sendgrid. With the add-on in place, our production environment is automatically configured to work properly. However, we will need to setup some settings in development.
When we installed the Sendgrid add-on, it created some Heroku configuration variables which we can find using the 'heroku' command. We can store this information in a YAML file which we can load when our Rails app boots up. (See Railscasts #85.) For convenience, we can use a Hashie::Mash object to access our YAML data instead of a hash object. (The Twitter gem that we have in our Gemfile has a dependency on the Hashie gem, so we didn't have to explicitly include the Hashie gem.)
With our Sendgrid credentials available in development, we manually configure ActionMailer in another initializer file. (Note that we're using port 587 instead of 25.)
The final thing we do is have Rails raise delivery errors in development.
# Terminal $ heroku addons:add sendgrid:free $ heroku config vars SENDGRID_DOMAIN => your_domain SENDGRID_PASSWORD => your_password SENDGRID_USERNAME => your_username _____________________________________________________ # config.yml development: sendgrid: domain: your_domain password: your_password user_name: your_username _____________________________________________________ # load_config.rb CONFIG = Hashie::Mash.new(YAML.load_file("#{Rails.root}/config/config.yml")[Rails.env]) _____________________________________________________ # setup_mail.rb if Rails.env.development? ActionMailer::Base.smtp_settings = { :address => "smtp.sendgrid.net", :port => 587, :authentication => :plain, :user_name => CONFIG.sendgrid.username, :password => CONFIG.sendgrid.password, :domain => CONFIG.sendgrid.domain } end _____________________________________________________ # development.rb Arailsdemo::Application.configure do config.action_mailer.raise_delivery_errors = true
The View
-# _side_bar.html.haml %h3#contact= link_to 'Contact', new_contact_form_url _____________________________________________________ # application.sass #contact :position fixed :right -22px :right -42px\9 // target IE8 and below :*right 0 // target IE7 and below :top 45% :display block :background $logo :height 25px :padding 0 6px 2px :z-index 10001 :border inset 1px $rust :border-bottom none +transform(1, -90deg) :filter progid:DXImageTransform.Microsoft.BasicImage(rotation=3) a :color $bkg1 :font :size 16px a:hover :background none #contact:hover :height 30px
(Please login to submit comments.)
Comments