15 Sep

Dynamic SEO Title and Meta Tags in Rails

This is a simple tutorial for setting up dynamic SEO-friendly title tags and meta description tags in a Rails app. If you want your Rails app to follow SEO best practices and play nicely with web crawlers, it's crucial to have dynamic page titles and meta descriptions that complement the content on each page.

I'm going to use a basic blog app and harness Rails's dynamic templating system to automatically generate title and meta description tags from each blog's title and the first 150 characters of each blog post.

First, I'm going to create two helper methods for the title and meta description tags in app/helpers/application_helper.rb:

def title(blog_title)
  content_for(:title) { blog_title }
end

def meta_description(blog_text)
  content_for(:meta_description) { blog_text }
end

These two methods are pretty straight forward. I can pass them arguments in the form of blog_title and blog_text and call the title and meta_description methods on any view.

For each blog's show page, I want the title tag and meta description tag to properly represent the content on the page. When someone performs a web search for “dynamic seo title tags rails" I want our blog titled “Dynamic Title Tags with Rails" to have the best possible shot of appearing in the search page results. I need to grab each blog's title from the show page and pass it to the helper method as an argument. I have to do the same with the meta description which, if you recall, I am going to generate from the first 150 characters on the blog text itself. The best way to accomplish these two objectives is to call both helper methods on the posts show page template app/views/posts/show.html.erb:

<% title @post.title %>
<% meta_description @post.text.truncate(150, separator: ' ') %>

With these two lines of code I am able to grab each blog's title and the first 150 characters of each post and pass these parameters as arguments in the helper methods. This assumes of course that the Post Controller has @post = Post.find(params[:id]) in its show action.

All I have left to do is to display these dynamic features in the layout file app/views/layouts/application.html.erb. I will set up fallback content for the title and meta description tags through the use of a conditional statement since only blog show pages are covered with this technique. All other pages will default to “Ryan McMahon | Software Development | Ruby on Rails" for the title tag and “Ryan McMahon's blog about Software Engineering, Web Development, and Ruby on Rails" for the meta description:

<title> 
    <% if yield(:title).present? %>
        <%= yield(:title) %>
    <% else %>
        Ryan McMahon | Software Development | Ruby on Rails
    <% end %>
</title>
<% if yield(:meta_description).present? %>
    <meta name="description" content="<%= yield(:meta_description) %>" />
  <% else %>
    <meta name="description" content="Ryan McMahon's blog about Software Engineering, Web Development, and Ruby on Rails" />
  <% end %>

4 Oct

Create a Simple Search Form in Rails

This is an easy tutorial for implementing a keyword search form in a Rails app. Often found in a website's navbar, the search form enhances usability and gives a website or app a more polished feel.

I'm going to implement the search form on a simple recipe-sharing app with a Recipes Controller and Recipe Model. The search form will take the terms entered by a user and look for matches in the recipe titles, ingredients, and cooking instructions. The app will display all of the recipes that contain those keywords or inform the user that there aren't any recipes that contain those terms.

I want to have the search available on every page's navbar so I am going to put the search form in the layout view found in app/views/layouts/application.html.erb.

<%= form_tag(recipes_path, :method => "get", id: "search-form") do %>
    <%= text_field_tag :search, params[:search], placeholder: "Search Recipes" %>
    <%= submit_tag "Search", :name => nil %>
<% end %>

I am using form_tag, a generic form helper, instead of form_for because the latter is intended for situations when you want to edit a model's attributes, but that's not what I am doing here. I just want to search the recipes index and form_tag allows me to do that. After the form_tag I pass it recipes_path, the action I want it to go to along with :method => 'get'. I have to declare the get method because by default the form_tag submits a post request and that's not the functionality I'm looking for.

In the text_field_tag I pass params[:search] as a value and this allows the search query to persist in the search box when the results page loads. This is not critical to the search functionality but it adds a little sugar to the user experience.

Finally, for the submit_tag I want to remove the default name of the search form name="commit" because this appears in the url with every search: www.totaste.org/recipes?search=pancetta&commit=Search. By declaring :name => nil, I end up with prettier urls like www.totaste.org/recipes?search=pancetta

To finish this up I need to put a search method in the model and declare which fields the method should search for matching queries. In /app/models/recipe.rb I put the following code that looks for matches in the name, ingredients, and cooking instructions fields in the database. Note: The LIKE syntax is used for MySQL, but if you are deploying to Heroku or another platform that uses PostgreSQL use the ILIKE syntax instead.

def self.search(search)
  where("name LIKE ? OR ingredients LIKE ? OR cooking_instructions LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%") 
end

And in the Recipes Controller, app/controllers/recipes_controller.rb, I will use the following code to display matching recipes in descending order from the time they were created.

def index
  @recipes = Recipe.all
  if params[:search]
    @recipes = Recipe.search(params[:search]).order("created_at DESC")
  else
    @recipes = Recipe.all.order("created_at DESC")
  end
end

One last thing—if there aren't any recipes that match the search query I want to display a warning on the index page. In /app/views/recipes/index.html.erb I put the following code to alert the user that there aren't any matching recipes. If I skip this step, a user might think something went wrong with the search. Again, it's not a critical function of the search but it makes the user experience a little bit nicer.

<% if @recipes.blank? %>
  <h4>There are no recipes containing the term <%= params[:search] %>.</h4>
<% end %>

5 Nov

Using Markdown for Posts in a Rails Blog

Here is quick tutorial for a Rails blog that allows posts to be written in Markdown.

When I decided to rebuild my static blog website in Rails, I had trouble deciding how to build dynamic show pages for each blog post. Every blog post would have a different HTML structure and I just couldn't wrap my head around how this could work with Rails's templating system. Enter Markdown, the lightweight markup language created by John Gruber and Aaron Swartz, that easily converts to HTML.

Blog posts written in Markdown can be saved directly to the database, and Markdown allows each post to have its own unique combination of headline, paragraph, and link tags. When a Post show action is called, I can parse the Markdown and display it as HTML on the view. I am no longer restricted by the .ERB and HTML in my Posts view.

There are two awesome gems that make using Markdown in an app a breeze. The first is RedCarpet, a Ruby library for converting Markdown to HTML. The other is CodeRay, a gem that provides really cool looking syntax highlighting, a critical feature for any blog about programming.

Using the two gems together is quite simple. First, in app/Gemfile add:

gem 'redcarpet'
gem 'coderay'

Next, in app/helpers/application_helper.rb simply place the following code:

module ApplicationHelper
  class CodeRayify < Redcarpet::Render::HTML
    def block_code(code, language)
      CodeRay.scan(code, language).div
    end
  end

  def markdown(text)
    coderayified = CodeRayify.new(:filter_html => true, :hard_wrap => true)
      options = {
        :fenced_code_blocks => true,
        :no_intra_emphasis => true,
        :autolink => true,
        :strikethrough => true,
        :lax_html_blocks => true,
        :superscript => true
      }
    markdown_to_html = Redcarpet::Markdown.new(coderayified, options)
    markdown_to_html.render(text).html_safe
  end
end

RedCarpet has good documentation on the options hash that can be customized to fit your needs. CodeRay's documentation is thin, but one option that might come in handy is the ability to add line numbers to your code blocks by changing CodeRay.scan(code, language).div to CodeRay.scan(code, language).div(:line_numbers => :table).

All that's left is to parse the Markdown in the view and it's as simple as:

<%= markdown(@post.title) %>
<%= markdown(@post.text) %>

The Markdown content in the title and text fields in the database will now be converted to HTML in my view. For more information on Markdown syntax, check out Daring Fireball or Github.

3 Dec

Rails Image Management with Paperclip and Amazon S3

If you've ever wondered how to combine Rails, the Paperclip gem, and Amazon S3 for image uploads, processing, and storage, look no further.

I recently deployed a simple Rails app called To Taste for my friends and family to share recipes we were always trying to pass around and preserve through emails or handwritten recipe cards. One of the main features I wanted was for users to be able to upload pictures of the cooked dishes. I also wanted the images to be available in different sizes and I didn't want image uploading or storage to strain app performance.

To achieve these three objectives, I decided to use Paperclip and Amazon S3 based on the recommendation of Heroku, where I planned to deploy the app. Paperclip is an easy-to-use file attachment library that treats uploads as model attributes, while Amazon S3 provides off-site image storage that reduces the load on Heroku, making your app faster.

The first thing I need to do is sign up for Amazon S3 and set my security credentials in the dropdown menu link called, you guessed it, “Security Credentials." Here I can set my access key ID and secret access key that will connect my Rails app to the Amazon S3 “bucket" where my images are stored. Clicking the “Create Bucket" button provides detailed instructions for creating and naming your “bucket."

Add Dependencies

Back in my Rails app I need to add the Paperclip, Amazon AWS, and Figaro gems to my Gemfile and then bundle install:

gem 'aws-sdk', '< 2.0'
gem 'paperclip', '~> 4.2'
gem 'figaro'

Figaro makes it easy to manage my credentials for services like Amazon S3. It creates a config/application.yml file where I can store my passwords and adds the file to .gitignore so it is never pushed to Github. All I need to do in config/application.yml is put in the following key/value pairs:

AWS_ACCESS_KEY_ID: my_access_key_id
AWS_SECRET_ACCESS_KEY: my_secret_access_key
AWS_S3_REGION: us-east-1
AWS_S3_BUCKET: the_name_of_my_bucket

Configure Rails and Paperclip with Amazon S3

Next, I have to add the Amazon S3 credentials to the production environment, located in app/config/environments/production.rb:

config.paperclip_defaults = {
  storage: :s3,
  s3_credentials: {
    bucket: ENV['AWS_S3_BUCKET'],
    access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
  }
}

If you want to test this in development, just add the above code to app/config/environments/development.rb too.

Setting up Paperclip

Now, on to Paperclip. The first step is to install an image resizing processor called ImageMagick by running brew install imagemagick. This requires an OS X package manager called Homebrew.

Running which convert displays where ImageMagick is installed, and in my case it's in /usr/local/bin/convert. I need to add this location to config/environments/development.rb) with the following code:

Paperclip.options[:command_path] = "/usr/local/bin/"

I am using a form_for helper to create and update recipes in the New and Edit views, so I need to add { :multipart => true } to the :html hash to enable file attachments in the form, while <%= f.file_field :image %> returns a file upload input tag:

<%= form_for :recipe, :url => recipes_path, :html => { :multipart => true } do |f| %> 
  <div class="field">
    <%= f.label :image %>
    <%= f.file_field :image %>
  </div>
  <div class="field">
    <%= f.label :ingredients %>
    <%= f.text_area :ingredients %>
  </div>
  <div class="field">
    <%= f.label :instructions %>
    <%= f.text_area :instructions %>
  </div>
  <div class="actions">
    <%= f.submit "Create Recipe" %>
  </div>
<% end %>

Define the Image Attribute in the Model

To attach an image to the Recipe model, Paperclip has a helper method called has_attached_file and I just need to add the :image symbol. The has_attached_file also accepts a :styles hash that sets the resized image options. Finally, the validates_attachment method allows me to restrict image uploads to specific formats (good for security) and limit their size (great for storage considerations).

class Recipe < ActiveRecord::Base
  has_attached_file :image, :styles => { :small => "100x100>", :med => "200x200>", :large => "514x386>" }, :default_url => ":style/fallback-recipe-img.png"
  validates_attachment :image, :content_type => { :content_type => ["image/jpeg", "image/gif", "image/png"] }, :size => { :in => 0..3.megabytes }
end

Database Migration Needed

To add the :image attribute to the Recipes table schema, I need to run a migration...

rails generate migration AddImageToRecipes

...and in the migration file I can use Paperclip's add_attachment and remove_attachment helper methods:

class AddImageToRecipes < ActiveRecord::Migration
  def self.up
    add_attachment :recipes, :image
  end

  def self.down
    remove_attachment :recipes, :image
  end
end

I run rake db:migrate and restart the server.

This migration creates the following columns in the Recipes table: image_file_name, image_content_type, image_file_size, and image_updated_at.

Update the Controller

Since To Taste is a Rails 4 app and I'm using strong parameters, I need to update my recipe_params private method to include the :image parameter in the whitelisted params.

def create
  @recipe = Recipe.new(recipe_params)
  @recipe.user_id = current_user.id
  if @recipe.save
    redirect_to @recipe, notice: "Success! You've added a new recipe."
  else
    render 'new'
  end
end

def update
  @recipe = Recipe.find(params[:id])
  if @recipe.update(recipe_params)
    redirect_to @recipe
  else
    render 'edit'   
  end
end

private
  def recipe_params
    params.require(:recipe).permit(:ingredients, :instructions, :image)
  end

Displaying Images in the Views

Images uploaded with Paperclip are now stored in Amazon S3, but metadata such as the image name and location in S3 are stored in the database. All that's left is to call an image in a view through the .url method that provides access to the image's url on Amazon (http://your_bucket_name.s3.amazonaws.com/...) and specify one of the styles I included in has_attached_file method.

<%= image_tag recipe.image.url(:med) %>

Images are now being served from Amazon S3 and they won't interfere with other requests, allowing pages to load quicker than if they were served directly from the app itself.

18 Dec

Automating Jobs with Rails Rake Tasks and Heroku Scheduler

I recently implemented Heroku Scheduler and Rails Rake tasks to automatically make API calls and update data in a side project I have called Power Pol. Rails and Heroku make it really easy to setup and deploy this functionality in minutes.

Install Heroku Scheduler

The first step is to install Heroku Scheduler by cd-ing into my app and typing this command in terminal: heroku addons:create scheduler:standard.

Create a Custom Rake Task

Next, I need to create the custom Rake tasks and I do this in lib/tasks/scheduler.rake. Originally created to manage software build processes, Rake enables me to define a set of tasks and their dependencies, and it automatically runs custom code when a given task is called. Each task consists of three things:

  • A description
  • A task name, represented by a Ruby symbol, that identifies the task
  • The code the task will execute in a Ruby do...end block
desc "This task calls the NYT API"
task :nyt_api_votes => :environment do
  politicians = Politician.all
  request = Unirest.get("http://api.nytimes.com/svc/politics/v3/us/legislative/congress/114/senate/members/current.json?api-key=#{Figaro.env.nyt_api_key}").body
    request["results"][0]["members"].each do |dw|
        politician = Politician.find_by(:bio_guide => dw["id"])
        if politician
        politician.update(:votes_with_party_pct => dw["votes_with_party_pct"], :missed_votes_pct => dw["missed_votes_pct"])
        end
    end
end

Every time I call the :nyt_api_votes task, the code in the block sends a request to the New York Times Congress API, updates the data for the politicians in my app, and saves it to the database.

Test Locally Before Deployment

I can test my Rake task locally before deploying it to Heroku, and this is as simple as firing up http://localhost:3000 and running heroku run rake nyt_api_votes from the command line. Heroku scheduler uses the same one-off dynos that Heroku run uses to execute jobs, so if my task works with with Heroku run, it will work with the scheduler.

Schedule Jobs in Heroku

Scheduling a job is as easy as going to https://scheduler.heroku.com/dashboard, clicking “Add a Job", and providing the name of my task rake nyt_api_votes. I also need to select a dyno size (for a simple app like mine 1X is fine), choose the frequency at which I want the task to run (daily, hourly, or every 10 minutes), and at what time I want the first task to execute.

With these simple steps, the politicians' data in my app will update automatically, eliminating the hassle of manually calling the API from Rails console.

Additional Info

The code block in the Rake task uses two gems I highly recommend: Unirest for making API calls and Figaro for handling sensitive information (like API keys and passwords) as environment variables.

30 Dec

Automatic Email Notifications for a Rails Comments Section

When I built the comments section for this blog I wanted to include functionality that automatically sent me an email notification when someone left a comment. This tutorial explains how to use Rails Action Mailer and a Gmail account to generate emails when a visitor posts a comment.

Generate a Mailer

Action Mailer allows a Rails app to send emails using mailer classes and views, and its functionality is similar to a controller. To install a mailer in your app run rails generate mailer CommentsMailer, a command that creates app/mailers/comment_mailer.rb, app/views/comment_mailer, and test files.

Edit the Mailer

Similar to a controller, a mailer has methods called “actions," its instance variables are available in the email views, and it creates messages that are emailed to a recipient—similar to how a controller generates HTML to be sent back to the client. In app/mailers/comment_mailer.rb, use the default hash default :from 'example@gmail.com' to set the email address that will generate an email notification when a visitor leaves a comment.

Add a method inside the CommentMailer class that describes the action taking place—something like “comment_notification." Call the mail method and pass it hash arguments in the form of common email headers. The only hash headers truly required are to:, for declaring the email address that will receive the email notifications (this should be different than the :from address), and subject:, but there are other options available such as cc: and bcc:.

class CommentMailer < ApplicationMailer
    default from: 'example@gmail.com'

    def comment_notification
        mail(to: 'another_email_address@gmail.com', subject: 'New Blog Comment')
    end
end

Send Email When a Comment is Saved

To trigger the Action Mailer email notification, put a conditional statement in the create action in app/controllers/comments_controller. It calls the comment_notification method on the CommentMailer class only when a comment is saved. You can also pass arguments such as @article and @comment to make them available in the email view.

def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.new(comments_params)
    if @comment.save
        CommentMailer.comment_notification(@comment, @article).deliver
        flash[:success] = "Thanks for commenting!"
        redirect_to article_path(@article)
    else
        flash[:danger] = "All fields must be filled in to post a comment."
        redirect_to article_path(@article)
    end
end

Add Arguments to the Mailer Class Method

To make the @article and @comment instance variables available in the view, add arguments to the comment_notification method in app/mailers/comment_mailer.rb and set the arguments to instance variables.

def comment_notification(comment, article)
    @comment = comment
    @article = article 
    mail(to: 'example@gmail.com', subject: 'Blog Comment Recorded')
end

Create an Action Mailer View

For the email view, create a file in app/views/comment_mailer/ and name it comment_notification.html.erb, ensuring the name matches the Mailer class method created earlier. Build out the email template with any HTML or ERB you like, but the one I use is as simple as this:

<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <p>Ryan, there is a new comment on the post:</p> 
    <p><h3><%= @article.title %></h3></p>
    <p><strong><%= @comment.name %></strong> wrote:</p>
    <p> "<%= @comment.body %>"</p>
  </body>
</html>

When Action Mailer sends a comment-notification email, the template I created renders the title of the article the visitor commented on, the commenter's name, and their entire comment. Pretty slick!

Setup Gmail for Sending Emails

The final steps are to make sure Gmail settings are setup properly in both development and production environments. In development, assuming you are using WEBrick and localhost:3000, place these lines in config/environments/development.rb to test Action Mailer.

config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    address: "smtp.gmail.com",
    port: "587",
    domain: "gmail.com",
    authentication: "plain",
    enable_starttls_auto: true,  
    user_name: ENV["GMAIL_USERNAME"],
    password: ENV["GMAIL_PASSWORD"]
}

The “user_name" and “password" keys are for the Gmail account I am sending emails from. I use the Figaro gem for the ENV["GMAIL_USERNAME"] and ENV["GMAIL_PASSWORD"]values to prevent my Gmail username and password from being pushed to Github. There are other ways to do this but I think the Figaro gem is easier to use. The Figaro documentation is great if you wan to use it too.

I deploy my blog via Heroku and these are the settings I use in config/enviroments/production.rb:

config.active_record.dump_schema_after_migration = false
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = false
  config.action_mailer.default_url_options = { :host => 'www.your_app.com' }
  config.action_mailer.default :charset => "utf-8"
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address: "smtp.gmail.com",
    port: "587",
    domain: "gmail.com",
    authentication: "plain",
    enable_starttls_auto: true,  
    user_name: ENV["GMAIL_USERNAME"],
    password: ENV["GMAIL_PASSWORD"]
  }

Conclusion

With this code in place, you will no longer have to check your blog to see if people are commenting and interacting with your posts. It's a great way to get instant feedback on your writing and connect with people who share the same interests.

25 Mar

Importing CSV Data to a Heroku Database

I was recently asked to migrate data from a CSV file to a Heroku database that was already in use for a Rails app. For a couple of years my company was using a Google form as a way for people to apply to our program but we recently deployed a Rails form so we needed to migrate all of the Google form data to the Heroku database. My first thought was, “No big deal," but I quickly found out this task was more complex than it appeared.

Baby Steps

Google forms store all of the submitted data on spreadsheets so the first step was to make sure the Google column headers matched the column names on the Heroku database. For instance, “Timestamp" on the Google form had to be changed to “created_at" to match our database column and I had to convert all of these timestamps from the default 7/1/2014 9:8:42 to a form accepted by Postgres: 2014-07-01 09:08:26. Once all of the data and columns were fixed I simply exported the document to CSV and stored it locally.

Rake Task

My plan was to create a rake task that would create new application records using the CSV data that I could parse with the CSV library. In application.rb I added require 'csv' so I'd have access to the CSV library throughout the Rails app.

Next, I created a rake task that took the CSV file I had stored locally and called Application.create while specifying which columns I wanted to import by using the slice method and an array of desired columns:

namespace :acltc_website do
  desc 'Import applicant data from CSV to database'
  task :import_applicant_data => :environment do
    CSV.foreach('/Users/desktop/application-responses.csv', :headers => true) do |row|
      Application.create(row.to_hash.slice(*%w[first_name last_name email phone programming_experience preferred_work_location capstone_idea created_at]))
    end
  end
end

This worked flawlessly in development on localhost: all of the applicant data from the CSV was migrated to the correct Postgres columns. I thought I was home free until I tried to push this code to Heroku.

Heroku is not Local

I quickly realized I couldn't implement this strategy because Heroku does not have access to my local environment. Sure, I could have pushed this CSV data to GitHub and grabbed it from there but that would've exposed the personal data of hundreds of people to world. What I needed was a secure place to store the CSV data AND Heroku had to be able to access it. Enter Amazon S3.

Amazon S3 Saves the Day

I created a bucket in my Amazon S3 account and uploaded the CSV file and then added the 'aws-sdk', '~> 2' gem to my Gemfile. I stored my AWS credentials in my .env file, pushed them to Heroku, and after a lot of trial and error I was able to get the rake task to work with the following code:

namespace :acltc_website do
  desc 'Import applicant data from CSV to database'
  task :import_applicant_data => :environment do
    s3 = Aws::S3::Resource.new(
      region: 'region',
      access_key_id: ENV['AWS_ACCESS_KEY_ID'],
      secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
    )
    obj = s3.bucket(ENV['S3_BUCKET_TITLE']).object('key').get
    CSV.parse(obj.body, :headers => true) do |row|
      Application.create(row.to_hash.slice(*%w[first_name last_name email phone programming_experience preferred_work_location capstone_idea created_at]))
    end
  end
end

There is a lot going on in that rake task so I'll walk you through it. I create a new instance of the Aws::S3:Resource and pass in my credentials. Then, I set the obj variable to the name of my S3 bucket and the “key," which is just their term for the name of my CSV file. The interesting thing about this is that obj is a string, not an array of arrays like a typical CSV, so I can't use CSV.foreach to access the data. I need to call CSV.parse and specify obj.body as an argument because that's where the CSV data is stored.

Now I can run heroku run bundle exec rake acltc_website:import_applicant_data and boom—all of the CSV data is migrated to the Heroku database.

20 Apr

How to Add an Invitation Code to Devise and Rails

Ever wonder how to restrict account sign up to invited guests using Devise and Rails? This easy-to-implement technique adds an invitation code field to the Devise gem's sign-up form, ensuring only visitors who were sent an invitation code can create an account.

Customize the Devise Sign-up View

The first step is to create an :invite field in the Devise sign-up view. To do that I need to generate the Devise views by hitting rails generate devise:views and then add the :invite field to app/views/devise/registrations/new.html.erb:

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>
    <div class="form-group">
      <%= f.label :email %>
      <%= f.email_field :email, autofocus: true %>
    </div>
    <div class="form-group">
      <%= f.label :invite, "Invitation Code" %>
      <%= f.text_field :invite, placeholder: "Input invitation code" %>
    </div>
    <div class="form-group">
      <%= f.label :password %>
      <% if @minimum_password_length %>
      <em class="devise-label">(<%= @minimum_password_length %> characters minimum)</em>
      <% end %><br />
      <%= f.password_field :password, autocomplete: "off" %>
    </div>
    <div class="form-group">
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation, autocomplete: "off" %>
    </div>
  <div class="actions form-group">
    <%= f.submit "Sign up" %>
  </div>
<% end %>
<%= render "devise/shared/links" %>

Usually I'd create a migration and add the :invite attribute to the Users table, but I don't need to go through all of that trouble here. I just need to validate what the visitor enters in the :invite field is indeed the invitation code I sent to them. And to do that, all I need is a virtual attribute.

Virtual Attribute Validation

In app/models/user.rb I add the virtual attribute :invite to the User model via attr_accessor:

class User < ActiveRecord::Base
  attr_accessor :invite
end

Now, I need a method I can call to validate the :invite field and I only want to trigger this validation on the create action:

class User < ActiveRecord::Base
  validate :validate_invite, :on => :create
  attr_accessor :invite

  def validate_invite
    if self.invite != “my_invite_code"
      self.errors[:base] << "The Invitation Code is Incorrect"
    end
  end
end

If the value of self.invite is not equal to “my_invite_code", an exception is raised, the visitor sees the error message “The Invitation Code is Incorrect", and a new User will not be created. If the two values are the same, the new User will be created.

Of course, I can't hardcode the invitation code into the conditional and push it to GitHub. For situations like this, I like to use the Figaro gem to keep my passwords or API keys out of version control. Figaro creates an application.yml file were I can store the invitation code as a key-value pair: invitation_code: "my_invitation_code". Figaro automatically adds application.yml to .gitignore, ensuring my sensitive data is not pushed to GitHub. The ready-for-production method now looks like this:

def validate_invite
  if self.invite != Figaro.env.invitation_code
    self.errors[:base] << "The Invitation Code is Incorrect"
  end
end

Configure Strong Params

Last and certainly not least, I need to update the strong parameters for the Devise sign-up form so the :invite field will be permitted. In app/controllers/application_controller.rb I'll use the before_action to configure Devise's standard parameters:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, :invite) }
  end
end

Admittedly, this technique might not be suited for all apps since every new user will be sent the same invitation code. Some apps may require dynamic invitation codes that are different for every user (hey, another blog post!) but this technique is more than capable of handling simple invitation-only app sign ups.

20 Jun

Rails Full-Text Search Form with AJAX

My most popular blogpost, with over 19,000 pageviews in just 18 months, is Create a Simple Search Form with Rails. Nearly all of the traffic (92%) was from Google because the post consistently ranked in the top 3 Google search page results for the term "rails search form". I guess a lot of Rails devs are looking for tutorials on search forms.

I received many comments on the blogpost (some were even nice) and a few people asked how they could make the simple search form more sophisticated. In response to those requests, I thought I'd write an updated post that demonstrates how to build a complex, full-text search form that submits via AJAX for that smooth, modern app feel.

Let's create a simple blog app with a form that allows visitors to perform full-text searches of the blogpost titles and body text while rendering the results without a full-page refresh. I am using Rails 5.2, Ruby 2.4.1, and PostgreSQL 9.6.3 for this demo.

The Setup

Create a new Rails app with PostgreSQL as the database (Postgres is required for the full-text search):

$ rails new blog --database=postgresql

Generate some scaffolding for the Post model with title and body attributes:

$ rails g scaffold Post title:string body:text

Create and migrate the database:

$ rails db:create && rails db:migrate

Start the Rails server and go to http://localhost:3000/posts/ to render the Posts index page.

We are going to need a few gems—Faker for adding fake data to the development database, jQuery (it was removed from Rails in 5.0), and pg_search for using PostgreSQL’s full-text search. In the Gemfile add:

gem 'faker', :git => 'https://github.com/stympy/faker.git', :branch => 'master'
gem 'jquery-rails'
gem 'pg_search'

Run $ bundle install.

Open app/assets/javascripts/application.js and add //= require jquery3 so jQuery is available in the asset pipeline. The file should look like this:

//= require rails-ujs
//= require jquery3
//= require activestorage
//= require turbolinks
//= require_tree .

Seed the Database

Let's add some records to the database using the Faker gem. Go to app/db , open seeds.rb, and add:

100.times do
  Post.create(
    title: Faker::Hipster.sentence,
    body: Faker::Hipster.paragraphs(6)
  )
end

Run $ rails db:seed and you'll see 100 hipster-themed blogposts on the index page.

Create the Search Form

Let's use a form_tag for the search form since we aren't saving data to the model.

<%= form_tag(posts_path, method: "get") do %>
  <%= text_field_tag :search, params[:search], placeholder: "Enter search term" %>
  <%= submit_tag "Search" %>
<% end %>

Add pg_search to the Post Model

First, let's define the difference between a simple search and a full-text search. Let's say we have a recipe app that allows users to search for recipes by their name and one of the most popular recipes is "Penne with Arrabiata". With a simple search, a phrase such as "penne arrabiata" returns zero matches because the search phrase did not include the word "with." With full-text search, however, searching for "penne arrabiata" will return the "Penne with Arrabiata" recipe and all other recipes with either of those words in the title. Full-text search is more useful and it's the kind of search visitors are expecting from modern apps.

Let's include the pg_search module in the model we want to search. After it's included, create a scope and choose the attributes you want the search to use to look for matches (in our case, :title and :body. Setting :tsearch => {:prefix => true} will give us the full-text search we desire and it will allow searches for partial words, so a search for "pen" will return "Penne with Arrabiata".

class Post < ApplicationRecord
  include PgSearch
  pg_search_scope :search_by_title_and_body, :against => [:title, :body],
    using: {
      :tsearch => {:prefix => true}
    }
end

Filter the Search Params in the Controller

In the controller, let’s create a conditional that displays the search results when the search form is submitted or it displays all of the blogposts in all other circumstances.

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts
  # GET /posts.json
  def index
    if params[:search]
      @search_results_posts = Post.search_by_title_and_body(params[:search])
    else
      @posts = Post.all
    end
  end
  etc...
end

Add AJAX

Submitting a form with AJAX allows us to insert search results into the DOM without reloading the entire page. This is a nice UX touch for users who may perform multiple searches during their visit. To submit a form via AJAX in Rails, add remote: true to the form_tag arguments.

<%= form_tag(posts_path, method: "get", remote: true) do %>
  <%= text_field_tag :search, params[:search], placeholder: "Enter search term" %>
  <%= submit_tag "Search" %>
<% end %>

Next, we need a respond_to block so the index action can respond to the AJAX call with JavaScript, since we are no longer submitting the form via HTML. The respond_to block is going to render a partial called "search-results" that we will create shortly.

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts
  # GET /posts.json
  def index
    if params[:search]
      @search_results_posts = Post.search_by_title_and_body(params[:search])
      respond_to do |format|
        format.js { render partial: 'search-results'}
      end
    else
      @posts = Post.all
    end
  end

On the index page, we need to wrap a <div> around the table and assign it an id of "blogpost-table". We also need to place an empty <div> with an id of "search-results" immediately after the closing tag of the first <div>. Using jQuery, we are going to hide the "blogpost-table" <div> when a search is performed and insert the results in the "search-results" <div> via JavaScript.

<div id="blogpost-table">
  <table>
    <thead>
      <tr>
        <th>Title</th>
        <th>Body</th>
        <th colspan="3"></th>
      </tr>
    </thead>

    <tbody>
      <% @posts.each do |post| %>
        <tr>
          <td><%= post.title %></td>
          <td><%= post.body %></td>
          <td><%= link_to 'Show', post %></td>
          <td><%= link_to 'Edit', edit_post_path(post) %></td>
          <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
<div id="search-results">

</div>

Sprinkles of jQuery

In app/views/posts/ create _search-results.js.erb, a file that will store the jQuery that will hide the unfiltered blogpost-table <div> and display the search results in the search-results <div>. Add the following jQuery:

$("#blogpost-table").hide();
$("#search-results").html("<%= escape_javascript(render :partial => 'results') %>");

The above code renders another partial called "results". It will hold the ERB that will display the results of our search and gets injected into <div id="search-results">. Create _results.html.erb in app/views/posts/ and add the same code for the table in views/posts/index.html.erb, but be sure to change @posts to @search_results_posts inside the loop:

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @search_results_posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td><%= post.body %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

That's it! Try searching for random hipster phrases and watch the matching blogposts appear without a full-page reload! The pg_search gem has a bunch of other options you can add and you can enhance this feature even further by adding autocomplete or a site-wide search that searches multiple models (maybe that’s v3 of this post?)

Check out a live demo of the full-text AJAX search form at https://rails-full-text-search-form.herokuapp.com/posts