Rails 4 Application Development HOTSHOT (2014)

Chapter 3. Creating an Online Social Pinboard

Every now and then, there are changes that alter our perspective of how we do things. One of these things is pinterest.com. The idea of an online pinboard to collect what we like is so appealing. It is a great way to organize personal information. For entrepreneurs, it gives direct insight into the likes and dislikes of a consumer. Hence, pinboards have gained importance and are now becoming specific to interests.

Mission briefing

We will create an online pinboard where users can collect and pin up what they like by uploading pictures. These pictures can also pinned by other users on their own pinboards.

During the course of this project, we will work with some popular jQuery plugins that have common use cases. The grid layout, infinite scroll, and modal box are some of the plugins we will look at. We will also create a mailer daemon that runs a job in the backend to send a weekly mail. Also, we will look at the basics of full-text searching and implement one in our app. Lastly, we will look at some tricks to prevent cross-site scripting and Rails security.

Our finished applications looks as shown in the following screenshot:

Mission briefing

Why is it awesome?

An online platform to pin up things is a great way to look at the kind of fashion, food, design, and photography, among many others, that is trending. It is also a very visual medium to market one's creations. It is more effective than any textual medium as it creates direct impact on the seeker. Repinning a post also allows us to track trends related to various topics as the pins are arranged in boards by an individual's area of interest.

Your Hotshot objectives

We will have to perform the following tasks while building this application:

·        Creating file uploads and image resizing

·        Creating an infinitely scrollable page

·        Creating a responsive grid layout

·        Adding a full-text search

·        Resharing the pins and creating modal boxes using jQuery

·        Enabling the application to send e-mail

·        Securing application from cross-site scripting or XSS

Mission checklist

We need the following installed on the system before we start with our mission:

·        Ruby 1.9.3 / Ruby 2.0.0

·        Rails 4.0.0

·        MySQL 6

·        Bootstrap 3.0

·        Sass

·        Sublime Text

·        Devise

·        Git

·        A tool for mockups

·        jQuery

·        ImageMagick and RMagick

·        Solr

Creating file uploads and image resizing

As seen in previous projects, we will mockup our application page and create a sample layout similar to Pinterest as follows:

Creating file uploads and image resizing

In this section, we will use the carrierwave gem to upload images and resize them into different sizes in order to display them on different pages. For example, we will display thumbnails on listing pages and larger images on individual pages.

Prepare for lift off

Before we start off with creating the upload methods, we will create two models and controllers for board and pin as follows:

$ rails g scaffold board title:string description:text

$rails g scaffold pin name:string image:string board_id:integer

We will create an association between pin and board as follows:

models/pin.rb

  belongs_to :board

models/board.rb

  has_many :pins

A user model is generated using the devise gem. We will also create an association between user and boards:

models/board.rb

  belongs_to :user

models/user.rb

  has_many :boards

We will also use friendly_id to create slugs for board and pin:

models/board.rb

  extend FriendlyId

  friendly_id :title, use: :slugged

models/pin.rb

  extend FriendlyId

  friendly_id :name, use: :slugged

Board is a way to organize pins, so all pins belong to a particular board. Also, these pins are a visual medium and hence full of images. So, we first need to get the images right. We will use the carrierwave gem to build the file uploading methods. It is a very standard method to add file uploads of all kinds.

ImageMagick is a dependency for our project, and we need to install it from source. Detailed installation instructions for ImageMagick can be found at http://www.imagemagick.org/script/advanced-unix-installation.php.

Once ImageMagick is installed and tested, install RMagick:

$ gem install rmagick

Engage thrusters

To create the file uploads, we will perform the following steps:

1.    Add carrierwave to Gemfile and run bundle:

2.  Gemfile

3.  gem 'carrierwave'

4.    Generate the uploader file:

5.  :~/pinpost$ rails g uploader image

6.        create  app/uploaders/image_uploader.rb

This will create a new folder inside the app folder called uploader and generate the file under it.

7.    We will use the filesystem to store and serve files here. The files are renamed to suit the models:

8.  app/uploaders/image_uploader.rb

9.    storage :file

10.  def store_dir

11.    Rails.env.production? ? (environment_folder = "production") : (environment_folder = "test")

12.    "uploads/#{environment_folder}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"

  end

13.          These uploaders are reusable and the same one can be mounted on multiple models in our pin model:

14.app/uploaders/image_uploader.rb

  mount_uploader :image, ImageUploader

15.          At this point, we need to add image attributes to our pin model:

16.$ rails g migration add_image_to_pins image:string

17.      invoke  active_record

18.      create    db/migrate/20140130025412_add_image_to_pins.rb

19.          We will need to whitelist the image attributes so that they can be retrieved from a form and stored in the database.

20.          Add the image parameters to the whitelist in your pins_controller file:

21.app/controllers/pins_controller.rb

22.def pin_params

23.      params.require(:pin).permit(:name, :image, :image_cache, :board_id)

    end

24.          The carrierwave gem maps the f.file_field form helper to the carrierwave uploader method in order to upload the files. So we can add this to our form:

25.app/views/pins/show.html.erb

26.<div class="field">

27.    <%= @pin.image_url if @pin.image? %>

28.    <%= f.file_field :image %>

    <%= f.hidden_field :image_cache %></div>

The form to create a pin looks like what is shown in the following screenshot:

Engage thrusters

29.          Once the images are uploaded, we can display them.

30.          In order to do so, we can directly make a call on the uploader name with a helper method called url to get the image file path:

31.app/views/pins/index.html.erb

<%=link_to(image_tag(pin.image.url, :width=>"200", :height=>"200"), pin) %>

32.          However, instead of manually defining width and height of the image, it's better to have them defined as a geometry and scale them during the time of upload.

33.          Define geometries to resize the images to multiple sizes on different pages:

·        Add the rmagick gem and install it:

·        Gemfile

·        gem 'rmagick'

·        Configure it inside your uploader file:

·        app/uploaders/image_uploader.rb

include CarrierWave::RMagick

·        Define different geometries for your image sizes:

·        app/uploaders/image_uploader.rb

·        # Create different versions of your uploaded files:

·           version :thumb do

·             process :resize_to_fit => [200, 200]

·           end

·           version :normal do

·             process :resize_to_fit => [350, 350]

   end

34.          Now that we have defined different sizes, we need to resize all the existing images and new ones to same sizes. In order to do so, we need a method that allows us to do this in one batch.

35.          After defining the geometries, we need our already uploaded files to be resized to the specified geometries. In order to do so, we will first create a migration:

36.$ rails g migration recreate_old_thumbnails

37.      invoke  active_record

38.      create    db/migrate/20140130033618_recreate_old_thumbnails.rb

39.db/migrate/20140130033618_recreate_old_thumbnails.rb

40.class RecreateOldThumbnails < ActiveRecord::Migration

41.  def up

42.    Pin.all.each {|p| p.image.recreate_versions! if p.image}

43.  end

44.  def down

45. end

46.end

47.          For the index page, modify views to call certain geometries on a certain page:

48.app/views/pins/index.html.erb

<%=link_to(image_tag(pin.image.thumb.url), pin) %>

49.          Similarly for the show page, modify views as explained in the preceding point:

50.app/views/pins/show.html.erb

  <%=image_tag @pin.image.normal.url %>

51.          We will write a test for our uploader file as follows:

52.test/uploaders/image_uploader_test.rb

53.require_relative '../test_helper'

54.require 'rubygems'

55.require 'RMagick'

56.require 'carrierwave'

57.require_relative '../../app/uploaders/image_uploader'

58.class ImageUploaderTest < MiniTest::Unit::TestCase

59.  FILENAME = 'well.jpeg'

60.  STORE_DIR = 'tmp/uploads/store'

61.  CACHE_DIR = 'tmp/uploads/cache'

62.  STORE_PATH = File.join __dir__, '..', '..', STORE_DIR

63.  CACHE_PATH = File.join __dir__, '..', '..', CACHE_DIR

64.  class ::ImageUploader

65.    storage :file

66.    def store_dir; STORE_PATH; end

67.    def cache_dir; CACHE_PATH; end

68.  end

69.  def setup

70.    @file = File.new "#{__dir__}/../test_files/#{FILENAME}"

71.  end

72.  def clear_after_test

73.    FileUtils.rm_rf STORE_PATH

74.    FileUtils.rm_rf CACHE_PATH

75.  end

76.  def test_image_upload

77.    uploader = ImageUploader.new

78.    uploader.store!(@file)

79.    assert_equal Digest::SHA2.file(@file).hexdigest, Digest::SHA2.file("#{STORE_PATH}/#{FILENAME}").hexdigest

80.  end

81.  def after_tests

82.  end

end

Objective complete – mini debriefing

The carrierwave gem creates a separate folder for the upload-related code:

app/uploaders/image_uploader.rb

In many ways, it's a very clean way to keep the uploader-related code abstracted from the rest of the code. The code to upload images is reusable and maintainable:

storage :file

  def store_dir

    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}

  end

The storage rule defines the storage mechanism to store files. We can also use Amazon S3 or Rackspace with the help of the fog gem.

The storage_dir defines the directory where the program stores the image. It generates the directories according to the model class, the type of asset (image, file, and so on), and the record number.

In case the form validation fails, the file field is reset. For the form to remember the filename even when the validation fails, we add a image_cache field in the form. We also add it to the permitted params in our controller.

We then create different versions of the same file during the upload. We use RMagick, which is Ruby's interface for ImageMagick, in order to read and process the image files:

   include CarrierWave::Rmagick

Another option is to use MiniMagick, another interface for ImageMagick, known to consume less memory than RMagick:

include CarrierWave::MiniMagick

The version rule in uploader helps to identify and create versions according to the defined geometry. In order to scale the image to the specified dimensions, we defined the :resize_to_fit method. This method will alter the dimensions of the image:

 version :thumb do

     process :resize_to_fit => [200, 200]

   end

In order to crop a part of the image, we can define the :resize_to_fill method. This will keep the dimensions of the image intact, while cropping out the defined dimensions from the image:

process :resize_to_fill => [200, 200]

In order to display the image, we accessed it via the following rule:

pin.image.thumb.url

It can be read as follows:

Object Name. Uploader Name. Url

To test our uploader, we first load all the required classes, RMagick to resize, carrierwave to upload, and our uploader class:

require_relative '../test_helper'

require 'rubygems'

require 'RMagick'

require 'carrierwave'

require_relative '../../app/uploaders/image_uploader'

We then set up all the parameters required to create the upload method:

class ImageUploaderTest < MiniTest::Unit::TestCase

  FILENAME = 'well.jpeg'

  STORE_DIR = 'tmp/uploads/store'

  CACHE_DIR = 'tmp/uploads/cache'

  STORE_PATH = File.join __dir__, '..', '..', STORE_DIR

  CACHE_PATH = File.join __dir__, '..', '..', CACHE_DIR

We then create our storage directories:

class ::ImageUploader

    storage :file

    def store_dir; STORE_PATH; end

    def cache_dir; CACHE_PATH; end

  end

We add a method to delete the directories after the upload test passes:

 def clear_after_test

    FileUtils.rm_rf STORE_PATH

    FileUtils.rm_rf CACHE_PATH

  end

We actually send the file to upload and match by assertion depending on whether it has been uploaded:

def test_image_upload

    uploader = ImageUploader.new

    uploader.store!(@file)

    assert_equal Digest::SHA2.file(@file).hexdigest, Digest::SHA2.file("#{STORE_PATH}/#{FILENAME}").hexdigest

  end

Creating an infinitely scrollable page

We are creating a social website and hope to attract several users. Very soon, with the increase in data, we will have to figure out how to arrange the data in the form of pages. We will now add pagination and see how to create and fit it in the context of our website. We will first look at creating pagination using Kaminari as the solution. We will then convert it to an infinitely scrollable page by identifying the end of a page and rendering the next page immediately after that.

Engage thrusters

We will now create an infinitely scrollable page for our application:

1.    First add the kaminari gem and set it up.

2.    Add the kaminari gem to your Gemfile and run bundle install:

3.  gem 'kaminari'

4.    Generate the configuration file in the initializers:

5.  rails g kaminari:config

6.    Once it is set up, we will add the pagination methods.

7.    The kaminari gem methods bind to models, so we need to define the per_page method in each mode. This will define the number of records after which a new page will be generated:

8.  app/models/pin.rb

paginates_per 10

9.    In your controller, find and arrange the pins with the latest ones on the top and make a call on the paginates per method.

10.app/controller/pins_controller.rb

11. def index

12.    @pins = Pin.order(:created_at).page(params[:page])

  end

13.          Once the pagination methods are in place, we will render these records into pages by inserting the following at the end of the page:

14.app/views/pins/index.html.erb

<%= paginate @pins %>

15.          We now have a working pagination in our application. We will now create an infinitely scrollable page using the jQuery library called jQuery infinitescroll. We will now have to generate jQuery files and add the jQuery infinitescroll jQuery library to the application.

16.          First generate jQuery files in the Rails public folder using the Rails jQuery generator command:

17.:~/pinpost$ rails g jquery:installl

18.      remove  public/javascripts/prototype.js

19.      remove  public/javascripts/effects.js

20.      remove  public/javascripts/dragdrop.js

21.      remove  public/javascripts/controls.js

22.     copying  jQuery (1.10.2)

23.      create  public/javascripts/jquery.js

24.      create  public/javascripts/jquery.min.js

25.     copying  jQuery UJS adapter (e9e8b8)

26.      remove  public/javascripts/rails.js

27.      create  public/javascripts/jquery_ujs.js

28.          Once this is done, download infinite scroll (https://github.com/paulirish/infinite-scroll) and add the jQuery infinitescroll library to the application:

29.:~/pinpost/vendor/assets/javascripts$ ls

jquery.infinitescroll.js

30.          Then require the infinitescroll library in application.js:

31.app/assets/application.js

//= require jquery.infinitescroll

32.          We need this script to only run on the page where we have to display all the pins. Thus, we add the following script to pins.js.coffee:

33.app/assets/pins.js.coffee

34.$(document).ready ->

35.  $("#posts").infinitescroll

36.    navSelector: "nav.pagination"

37.    nextSelector: "nav.pagination a[rel=next]"

    itemSelector: "#posts tr.post"

38.          The next item selector binds to a particular tag inside your tag structure. The nav.pagination method will fire the next page to bring the next batch of records in order to display them.

39.          We will now find the end of the page, generate a div element, and append it to the page.

40.          For the page to look infinitely scrollable, we will have to identify the end of the page and generate a div element in order to display the next page. We will create an index.js.erb file inside our views/pins folder:

41.app/views/pins/index.js.erb

$("#posts").append("<div class='page'><%= escape_javascript(render(@pins)) %></div>");

42.          Finally, we will modify the index page to display pagination.

43.          We will assign an ID called posts to table, so the index.js.erb file can bind to it. Each page will have a tbody class page and each pin will bind to a post class:

44.app/views/pins/index.html.erb

45.<table id="posts">

46.  <tbody class="page">

47.    <% @pins.each do |pin| %>

48.      <tr class="post">

49.        <td> <%=link_to(image_tag(pin.image.thumb.url), pin) %><p>

50.        <strong>Board:</strong><%= pin.board.title %><br/><%= pin.board.user.email %></p></td>

51.      </tr>

52.    <% end %>

53.  </tbody>

</table>

54.          We will add a validation to make sure title is present. User e-mail is a mandate to create the account, so the devise gem has already taken care of it:

55.app/models/board.rb

validates :title, presence: true

56.          This is all we need need to create an infinitely scrollable page.

Objective complete – mini debriefing

We just created a page with endless pagination. We looked at normal pagination that sorts several records page-wise. We used the kaminari gem to create the pagination inside our application. When we generate the configuration, a kaminari_config.rb file is generated:

config/initializers/kaminari_config.rb

Kaminari.configure do |config|

  # config.default_per_page = 25

  # config.max_per_page = nil

  # config.window = 4

  # config.outer_window = 0

  # config.left = 0

  # config.right = 0

  # config.page_method_name = :page

  # config.param_name = :page

end

The config.param_name option changes the page name required for pagination. By default, it is page. Then, we defined the paginates_per method to limit the number of records to be displayed in a page:

paginates_per 10

In order to render the pagination, we add the partial call in our view:

<%= paginate @pins %>

We then looked at making these pages paginate one after another and append at the end of each page. We used JavaScript in order to create the infinite scroll. We used a combination of jQuery and CoffeeScript in order to create the infinite scroll. It is noteworthy that CoffeeScript is a language that compiles to JavaScript. So jQuery-related code or any other code related to the JavaScript framework can be written as CoffeeScript and then compiled to JavaScript. Also, it is neatly integrated with the Rails framework, so all controllers have a CoffeeScript associated with them.

There are several libraries that provide similar functionalities, for example, sausage.js is a simple jQuery library with similar functions. Also, Masonry and Wookmark come with in-built methods to generate infinite scrolls. We used a jQuery plugin calledjquery_infinitescroll in order to implement it. The plugin can be downloaded from GitHub (http://www.infinite-scroll.com/infinite-scroll-jquery-plugin/).

The first selector is meant for the page navigation and this will be hidden:

navSelector: "nav.pagination"

The next page is automatically identified by nextSelector and it looks for the next set of posts to render:

    nextSelector: "nav.pagination a[rel=next]"

Also, ItemSelector will render the next page or the next set of posts right after the end of the page is reached:

    itemSelector: "#posts tr.post"

In Rails, to bind a JavaScript to a controller method, we have to create an action-specific JavaScript file. Thus, we create index.js.erb. We will be able to retrieve the posts and append them to the bottom of the page:

$("#posts").append("<div class='page'><%= escape_javascript(render(@pins)) %></div>");

In order to bind the JavaScript method to HTML, we have to create a table (as tr and td are inside the table HTML attribute) and call the post class on it. This will make the infinite scroll method applicable to the HTML:

<table id="posts">

  <tbody class="page">

    <% @pins.each do |pin| %>

      <tr class="post">

        <td> <%=link_to(image_tag(pin.image.thumb.url), pin) %><p>

        <strong>Board:</strong><%= pin.board.title %><br/><%= pin.board.user.email %></p></td>

      </tr>

    <% end %>

  </tbody>

</table>

Creating a responsive grid layout

One of the most eye catching features of Pinterest and several other online pinboards is the way pins are displayed. They are arranged as a grid of images alongside each other. This is one of the greatest innovations and turning points in the creation of user experience. As previously mentioned, Masonry and Wookmark are some of the libraries that generate these kind of grids.

Prepare for lift off

Download the Wookmark from its GitHub repository (https://github.com/GBKS/Wookmark-jQuery). Place the jquery.wookmark.js file in the app/assets folder.

Engage thrusters

We will add the Pinterest-style grid layout in this task:

1.    First add the Wookmark library to the JavaScript files.

2.    Add the jquery.wookmark.js file in the JavaScript files and require in application.js:

:~/pinpost/app/assets/javascripts$  jquery.wookmark.js

3.    In application.js, add the following line:

  //= require jquery.wookmark

4.    Then initiate the JavaScript and generate a grid.

5.    Initiate the function and bind it to the tiles ID. Also, bind it to a td so that we have all the images inside the td. We will also handle clicks and randomize the height of an image so that it looks like the images flow into one another. This will also help to resize images in a responsive format:

6.  app/views/pins/index.html.erb

7.  <script type="text/javascript">

8.        var $handler = $('#tiles td');

9.        $handler.wookmark({

10.          autoResize: true,

11.          container: $('#main'),

12.          offset: 5,

13.          outerOffset: 10,

14.          itemWidth: 210

15.      });

16.      $handler.click(function(){

17.        var newHeight = $('img', this).height() + Math.round(Math.random() * 300 + 30);

18.        $(this).css('height', newHeight+'px');

19.        // Update the layout.

20.        $handler.wookmark();

21.      });

</script>

22.          Next, we will create the div element with ID as main and call the grid inside it.

23.          Create a div element called main as mentioned in the Wookmark initializer and bind tbody to tiles. The <td> tags under it will inherit the styles from this class:

24.app/views/pins/index.html.erb

25.<div id="main" role="main">

26.<table id="posts">

27.  <tbody id="tiles" class="page">

28.    <% @pins.each do |pin| %>

29.      <tr class="post">

30.        <td> <%=link_to(image_tag(pin.image.thumb.url), pin) %><p>

31.         <strong>Board:</strong><%= pin.board.title %><br/><%= pin.board.user.email %></p></td>

32.      </tr>

33.    <% end %>

34.  </tbody>

35.</table>

36.<%= paginate @pins %>

</div>

Objective complete – mini debriefing

In order to generate the grid layout to display all the pins, we used a jQuery plugin called wookmark.js. We first created a variable called handler that binds to the td element of the table, which is each cell of the row:

var $handlerr = $('#tiles td');

Then, we defined the variables required for each cell to be generated using wookmark. Container is the element based on which the width of each column is calculated. The offset element is used to define the distance between the two objects in a row:

      $handler.wookmark({

          autoResize: true,

          container: $('#main'),

          offset: 5,

          outerOffset: 10,

          itemWidth: 210

      });

Then, we created an event that randomizes the event size and creates grid variable-sized images:

$handler.click(function(){

        var newHeight = $('img', this).height() + Math.round(Math.random() * 300 + 30);

        $(this).css('height', newHeight+'px');

        // Update the layout.

        $handler.wookmark();

      });

As soon as we created the td element, our JavaScript automatically identified the element and generated it:

<td> <%=link_to(image_tag(pin.image.thumb.url), pin) %><p>

         <strong>Board:</strong><%= pin.board.title %><br/><%= pin.board.user.email %></p></td>

We have successfully generated the grid layout, which looks like that of Pinterest and is shown in the following screenshot:

Objective complete – mini debriefing

The preceding layout is responsive too, so we will resize our browser as shown in the following screenshot and check it:

Objective complete – mini debriefing

The layout being responsive also depends on Bootstrap as it contains media queries as a part of the CSS; however, Wookmark automates image resizing and grid size required for different window sizes. Hence, it is a completely responsive layout.

Adding a full-text search

Search is one of the most important functionalities today. Because sites are targeted at millions of users, there is a much larger volume of content. For a user to find what he or she is looking for, a full-text search is created. The idea of a search is to call the text, break it word by word, and match it with the key term supplied to it. We will use Apache Solr to create our search engine. In this section, we add a search option to our models using Sunspot, a Ruby-based library for Solr, indexing, and search methods in our Rails application.

Prepare for lift off

We will need to install Solr and Tomcat Solr before we start working with it. Solr relies on Java, so you need to have an updated version of OpenJDK before you proceed. Solr is generally a process bound with the sunspot gem and can be initiated using Rake. However, the Solr server depends on Tomcat and JDK, so they need to be installed before we start using Solr:

$ sudo apt-get install openjdk-6-jdk

Then install Tomcat and start the server:

sudo apt-get install solr-tomcat

sudo service tomcat6 start

Engage thrusters

We will add a full-text search engine to our application:

1.    Add sunspot and the supporting libraries to Gemfile and bundle install:

2.  gem 'sunspot', :require => 'sunspot'

3.  gem 'sunspot_rails'

4.  gem 'sunspot_solr'

5.    The main library Sunspot; sunspot_rails is specific to the interface with Rails applications and attaches it to the models. Sunspot Solr provides a Solr-related configuration interface.

6.    Generate the configuration file:

7.  :~/pinpost$ rails generate sunspot_rails:install

8.        create  config/sunspot.yml

9.    The file looks like the following:

10.config/sunspot.yml

11.production:

12.  solr:

13.    hostname: localhost

14.    port: 8983

15.    log_level: WARNING

16.    # read_timeout: 2

17.    # open_timeout: 0.5

18.development:

19.  solr:

20.    hostname: localhost

21.    port: 8982

22.    log_level: INFO

23.test:

24.  solr:

25.    hostname: localhost

26.    port: 8981

    log_level: WARNING

27.          In case your Solr server is running on a different port, edit the port number in this file to match that. This will allow Solr to bind to that port and run on it.

28.          We will load the Rake tasks manually. In Rails 4, the Rake tasks for Solr are not loaded by default. Thus, we will need to add them to our Rake file:

29.require 'sunspot/solr/tasks'

30.          Start the Solr server using the Rake task:

31.:~/pinpost$ rake sunspot:solr:start

32.java version "1.7.0_25"

33.OpenJDK Runtime Environment (IcedTea 2.3.10) (7u25-2.3.10-1ubuntu0.13.04.2)

34.OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode)

35.Removing stale PID file at /home/user/pinpost/solr/pids/development/sunspot-solr-development.pid

36.Successfully started Solr

37.          Solr is now up and running on your system. Let's go ahead and add indexes on the fields we need to search.

38.          Sunspot Solr accesses the database for full-text search through the models. We need to define these in our board and pin models:

39.models/board.rb

40.   searchable do

41.     text :title, :description

42.     integer :user_id

43.   end

44.models/pin.rb

45.   searchable do

46.     text :name, :image

47.     integer :board_id

   endOnce

48.          The indices are set up; we will index the data to Solr.

49.          Indexing is a Rake task of sunspot, so just run the following Rake command:

50.:~/pinpost$ rake sunspot:reindex

51.*Note: the reindex task will remove your current indexes and start from scratch.

52.If you have a large dataset, reindexing can take a very long time, possibly weeks.

53.This is not encouraged if you have anywhere near or over 1 million rows.

54.Are you sure you want to drop your indexes and completely reindex? (y/n)

55.y

56.[##################################################################################################################################################] [88/88] [100.00%] [00:01] [00:00] [67.55/s]

57.          We will now write our search methods in our model. We will then add a search method in order to search our indexed data.

58.          In order to search the indexed data, Sunspot provides us with a search method. We will create a class method in our model to search through them. Our board model will look as follows:

59.app/models/board.rb

60.def self.search_board(search_key)

61.   @search = self.search do

62.      fulltext "#{search_key}"

63.    end

64.    @search.results

  end

65.          Our pin model will look like as shown in the following code snippet:

66.app/models/pin.rb

67.def self.search_pin(search_key)

68.    @search = self.search do

69.      fulltext "#{search_key}"

70.    end

71.    @search.results

  end

72.          We will call the search and fulltext methods as a self class method using a search term in the search_key variable. The @search object cannot be inspected. Hence, we call @search.results to output our results in the form of an object. Call the search methodthrough the controller.

73.          We will create a home controller in order to set up a home page for our application:

74.$ rails g controller home index

75.          We will add our search method to the home controller as we want to create a site-wide search. First, check if the search term is blank. Also, we will check for a condition in which there are no results, and then display the message; else, we will add the results of the board and pin into one object:

76.app/controllers/home_controller.rb

77.def search

78.    if params[:search].blank?

79.      flash[:notice] = "Please Supply a Search term"

80.If it is present, then search for board and pin

81.  else

82.    @board_results = Board.search_board(params[:search])

83.    @pin = Pin.search_pin(params[:search])

84.    if @board.nil? && @pin.nil?

85.      flash[:notice] = "No Results Found matching your query"

86.    else

87.      flash[:notice] = "Following are the search results"

88.      @search = @board + @pin

89.    end

90.    end

  end

91.          We now have the search results. In order to search from a form, we need a route. Our search method route will look like the following:

92.config/routes.rb

get :search, :to => 'home#search', :as => 'search'

93.          However, to search from a form, we need to create a search form in layouts/application.html.erb.

94.          This form will send the search term as params[:search], which will be passed to the controller method:

95.app/views/layouts/application.html.erb

96.<%= form_tag(search_path, :class=>"navbar-form navbar-left", :method => :get) do%>

97.        <div class="form-group">

98.          <%=text_field_tag :search, params[:search], :class => 'form-control', :placeholder => 'Search'%>

99.        </div>

100.           <button type="submit" class="btn btn-default">

101.           <i class="icon-search"></i>

102.           </button>

      <%end%>

We have used the Font Awesome icon to create the search icon. We can now see a Search bar on the top of our page as shown in the following screenshot:

Engage thrusters

103.     We will now display the search results by creating a search results page.

104.     We can run the loop over our search object and identify if the class name is Board or Pin. In this way, we can differentiate between the different search results:

105.   app/views/layouts/application.html.erb

106.   <% @search.each do |s|%>

107.          <%if s.kind_of?(Board)%>

108.            <li><p><%= s.title%><br/><%= s.description%><br/><%= s.user.email%></p></li>

109.         <%else if s.class.name == "Pin" %>

110.            <li><%=image_tag s.image.url%><p><%= s.board.title%></p></li>

111.         <%end%>

      <%end%>

This is how it looks after applying the Wookmark grid layout to the view:

Engage thrusters

Objective complete – mini debriefing

In this task, we added a full-text search engine to our application. We used Solr as our choice of search engine. Solr's website defines it as follows:

Note

SolrTM is the popular, blazing, fast, open source, enterprise search platform in the Apache LuceneTM project. Its major features include powerful full-text search, hit highlighting, faceted search, near real-time indexing, dynamic clustering, database integration, rich document (for example, Word, PDF, and so on) handling, and geospatial search.

Although Solr has more dependencies than other counterparts (Sphinx and Elasticsearch), it is highly scalable and can handle complex queries with ease.

We first defined an index in the pin model:

  searchable do

   text :name, :image

   integer :board_id

  end

In a search engine, indexing is a process to collect, parse, and store data such that it is matched and retrieved really quickly using the defined matching algorithms. A query on an index is much faster than a query on the database because an index remembers only a particular set of data, not the entire data set. An index is generally a data structure. Solr uses an inverted index data structure technique (a hash table or a binary tree) to implement the indexing function.

Once the index is defined, Sunspot generates an incremental index as soon as a new record is saved.

The fulltext method performs the search on the index created using the search term:

  @search = self.search do

      fulltext "#{search_key}"

    end

The results are returned as objects inside @search. In order to get the search results as a hash from the object, we called the following method:

    @search.results

The terminal shows the Solr query once we try to search according to the search term:

Started GET "/search?utf8=%E2%9C%93&search=blade" for 127.0.0.1 at 2014-02-02 16:02:15 +0800

Processing by HomeController#search as HTML

Parameters: {"utf8"=>"  ", "search"=>"blade"}

SOLR Request (96.9ms) [ path=#<RSolr::Client:0x007f352c0d5008> parameters={data: fq=type%3ABoard&q=blade&fl=%2A+score&qf=title_text+description_text&defType=dismax&start=0&rows=30, method: post, params: {:wt=>:ruby}, query: wt=ruby, headers: {"Content-Type"=>"application/x-www-form-urlencoded; charset=UTF-8"}, path: select, uri: http://localhost:8982/solr/select?wt=ruby, open_timeout: , read_timeout: , retry_503: , retry_after_limit: } ]

SOLR Request (27.0ms) [ path=#<RSolr::Client:0x007f352c0d5008> parameters={data: fq=type%3APin&q=blade&fl=%2A+score&qf=name_text+image_text&defType=dismax&start=0&rows=30, method: post, params: {:wt=>:ruby}, query: wt=ruby, headers: {"Content-Type"=>"application/x-www-form-urlencoded; charset=UTF-8"}, path: select, uri: http://localhost:8982/solr/select?wt=ruby, open_timeout: , read_timeout: , retry_503: , retry_after_limit: } ]

Tip

Rails 4.2 upgrade tip

In Rails 4.2, the require path, active_support/core_ext/object/to_json, is depricated instead of active_support/core_ext/object/json.

Sunspot Rails gives the following deprication warning:

DEPRECATION WARNING: You have required `active_support/core_ext/object/to_json`. This file will be removed in Rails 4.2. You should require `active_support/core_ext/object/json` instead. (called from <top (required)> at /home/rwub/rails4-book/book/6294OS_Chapter_03/project-3/config/application.rb:7)

sunspot_rails/lib/sunspot_rails.rb

require 'active_support/core_ext/object/json'

Resharing the pins and creating modal boxes using jQuery

One of the most important features in our application is resharing. This is the most attractive feature businesswise and a USP of our application. If a user likes an image or pin, he or she would like to pin it up on their board. In this section, we will look at creating this functionality. This is the social aspect and also the business model. How do we check the most trending items? The number of times a pin has been shared can serve as a strong metric when suggesting a trending topic.

A user should be able to select the board on which you have put up the pin. These are the users' own boards. We can do this by creating a modal box with a list of users' boards in it.

Engage thrusters

In this task, we will add the functionality to repin the post:

1.    Create a pin_post method in the pins controller:

·        This method will call the pin and find it using the pin ID. Create a new pin and assign values to various attributes. We can save this pin once we build the complete object:

·        app/controllers/pins_controller.rb

·          def pin_post

·            @current_pin = Pin.friendly.find(params[:id])

·            @pin = @current_pin.repin_post (params[:board_id])

·            respond_to do |format|

·               if @pin.save

·            format.js {render :layout => false}

·               else

·             format.js

·               end

·             end

  end

2.    We will create a new pin in the pin model:

3.  app/models/pin.rb

4.  def repin_post(board_id)

5.     pin = Pin.new

6.     pin.name = self.name

7.     pin.board_id = board_id

8.     pin.image = self.image

9.     pin.save

end

10.          Create a route to access this method from the controller.

11.          We will pass the ID of the pin along with the route:

12.config/route.rb

 post 'pin_post/:id', :to => 'pins#pin_post', :as => 'pin_post'

13.          We will add a modal box using jQuery Facebox:

·        Add the Facebox jQuery plugin using the facebox-rails gem:

·        Gemfile

·            gem 'facebox-rails'

·        Add JavaScript to the application.js file:

·        app/assets/application.js

//= require jquery.facebox

·        Also add the stylesheet to the application.css file

·        app/assets/application.css

 *= require jquery.facebox

·        Initiate facebox and ask it to bind to a tag with rel="facebox":

·        app/views/pins/index.html.erb

·        <script>

·        jQuery(document).ready(function($) {

·          $('a[rel*=facebox]').facebox()

·        })

</script>

14.          In order to make this form reusable, create a partial called pin_post.html.erb:

15.app/views/pins/_pin_post.html.erb

16.<%= form_tag(pin_post_path(pin)) do%>

17.   <p>Select a board for Pinning</p>

18.<% if current_user.boards%>

19. <% current_user.boards.each do |b|%>

20.    <%= radio_button_tag :board_id, b.id %>

21.    <%=  b.title %><br/>

22.   <%end%>

23.  <%= submit_tag 'Save', :class => "btn btn-primary"%>

24. <%end%>

<%end%>

25.          Create a link to the info box to display the boards.

26.          We can call the Pin This link and use the share icon from the Font Awesome icon library:

27.app/views/pins/index.html.erb

 <a href="#info" rel="facebox"><i class="icon-share-sign"></i>Pin This</a>

28.          Add a hidden div and call the partial pin_post.html.erb in the div:

29.app/views/pins/index.html.erb

30.<div id="info" style="display:none;">

31.      <%= render partial: 'pin_post', locals: {pin: pin}%>

        </div>

Objective complete – mini debriefing

We have now added the modal box and allowed users to repin a particular pin they like on their own boards. The idea behind a repin is that users will save the same pin on the board that's associated with them. So, in order to repin, we created a class method in the model. We called the current pin in an object using ID:

@current_pin = Pin.friendly.find(params[:id])

Then, we called a method to generate a new pin based on the information of the existing pin:

def repin_post(board_id)

    pin = Pin.new

    pin.name = self.name

    pin.board_id = board_id

    pin.image = self.image

    pin.save

end

We displayed the boards in a modal box. We used the jQuery Facebox plugin as the modal box. We used Facebox (https://github.com/defunkt/facebox) to load a partial with a form that contains the list of boards selectable using radio buttons. The form_tag binds to the controller action via the route. The remote=>true enables the AJAX form submission. Also, radio_button_tag generates the radio button to select the board value:

<%= form_tag(pin_post_path(pin), :remote=>true) do%>

   <p>Select a board for Pinning</p>

   <% Board.my_board(current_user).each do |b| %>

    <%= radio_button_tag :board_id, b.id %>

    <%=  b.title %><br/>

   <%end%>

  <%= submit_tag 'Save', :class => "btn btn-primary" %>

<%end%>

The modal box for resharing a pin looks like what is shown in the following screenshot:

Objective complete – mini debriefing

Enabling the application to send a mail

Mailers are the oldest way of marketing and still prove to be one of the most effective ways to reach out to users. As a part of the user engagement model, we can create a weekly mailer with a list of recent pins. This will keep the user updated with the latest information posted on our website and enhance users' engagement.

Engage thrusters

In the coming steps, we will create a mailer service for our application. To do so, we need to set up Action Mailer and use the Google apps e-mail service to send the mails via our application:

1.    Add the following lines inside your development.rb/production.rb file:

2.  config/environments/development.rb

3.  config.action_mailer.smtp_settings = {

4.      :enable_starttls_auto => true,

5.      :address => "smtp.gmail.com",

6.      :port => '587',

7.      :domain => "smtp.gmail.com",

8.      :authentication => "plain",

9.      :user_name => "foobar@pinpost.com",

    :password => "myawesomepw" }

10.          We will have to make sure that we do not commit the credentials of our mailer system in our version control; you can use dummy credentials. We can also avoid sending out mail in the development mode by using MailCatcher. We can install it using the gemcommand first:

11.$ gem install mailcatcher

12.Fetching: skinny-0.2.3.gem (100%)

13.Fetching: mailcatcher-0.5.12.gem (100%)

14.Successfully installed skinny-0.2.3

15.Successfully installed mailcatcher-0.5.12

16.2 gems installed

Then, start the MailCatcher service using the mailcatcher command:

$ mailcatcher

Starting MailCatcher

==> smtp://127.0.0.1:1025

==> http://127.0.0.1:1080

*** MailCatcher runs as a daemon by default. Go to the web interface to quit.

17.          Now we will edit our mailer settings in our environments/development.rb file:

18.config/environments/development.rb

19.config.action_mailer.delivery_method = :smtp

config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }

20.          We will then generate a mailer called newsletter:

21.:~/pinpost$ rails g mailer newsletter

22.      create  app/mailers/newsletter.rb

23.      invoke  erb

24.      create    app/views/newsletter

25.      invoke  test_unit

26.      create    test/mailers/newsletter_test.rb

27.          We will add a mailer method called letter in order to send the e-mail.

28.          Then, we will pass the user e-mail and pin as the argument and pick up the e-mail from it:

29.app/mailers/newsletter.rb

30.class Newsletter < ActionMailer::Base

31.  default from: "noreply@pinpost.com"

32.  def letter(user, pin)

33.    @user = user

34.    @pins = pin

35.    mail(:to => @user.email, :subject => "Latest Pins from Our Users")

36.  end

end

37.          Next up, we will create a controller method to retrieve these objects and pass them to the mailer method.

38.          We will create a pins_newsletter method, calling the last five pins into an object and looping them over all users:

39.app/models/pin.rb

40.  def self.send_newsletter

41.    @user = User.all

42.    @user.each do |u|

43.      @pins = self.all(:limit => 5)

44.      Newsletter.letter(u, @pins).deliver

45.    end

  end

46.          We will need to add e-mail views to the newsletter folder located under views.

47.          The newsletter folder was created when we generated the mailer earlier. This folder will hold the views for the e-mail, that is, how the e-mail will look. In our case, we will call the last five pins and directly link to them via e-mail and call the letter.text.erb file:

48.app/Views/newsletter/letter.text.erb

49. <h3>Our Latest Pins</h3>

50.<% @pins.each do |p|%>

51. <%=link_to p.name, p %>

<%end%>

52.          Now that we are ready with our methods, we need to create a Rake task as we want this functionality to run in the backend.

53.          Create a newsletter.rake file in lib/tasks.

54.          This will basically invoke a new method with an instance called pins_newsletter and send an e-mail to all the users:

55.lib/tasks/newsletter.rake

56.namespace :newsletter do

57.  desc "Send Newsletter"

58.  task :send => :environment do

59.    Pin.send_newsletter

  end

We will bind the preceding Rake task to cron using the whenever gem.

60.          Add the whenever gem to Gemfile and bundle:

61.gem 'whenever', :require => false

62.          Generate the configuration file called schedule.rb:

63.:~/pinpost$ wheneverize .

64.[add] writing `./config/schedule.rb'

65.[done] wheneverized!

66.          Configure the Rake task to run every seven days in order to send a weekly mail:

67.config/schedule.rb

68.every 7.days do

69.   rake "newsletter:send"

70. end

71.          Update the crontab file:

72.:~/pinpost$ whenever --update-crontab store

73.[write] crontab file updated

74.          Check if the crontab file has been updated by listing the cron jobs:

75.:~/pinpost$ crontab -l

76.# Begin Whenever generated tasks for: store

77.0 0 1,8,15,22 * * /bin/bash -l -c 'cd /home/rwub/rails4-book/book/6294OS_Chapter_03/project-3 && bin/rails runner -e production '\''Send Newsletter Email'\'''

78.0 0 1,8,15,22 * * /bin/bash -l -c 'cd /home/rwub/rails4-book/book/6294OS_Chapter_03/project-3 && RAILS_ENV=production bundle exec rake newsletter:send --silent'

Objective complete – mini debriefing

In this task, we saw how to create a mailer using Action Mailer and bind it to cron in order to send weekly e-mails.

We first created a mailer in our application. The mailer then creates a class under the mailers folder. We defined the default from e-mail in our mailer class. Next, as a part of the e-mail, we defined a method with user and pins as attributes. Finally, we added the subject of our e-mail and sent it to all the users in the system:

app/mailers/newsletter.rb

class Newsletter < ActionMailer::Base

  default from: "noreply@pinpost.com"

  def letter(user, pin)

    @user = user

    @pins = pin

    mail(:to => @user.email, :subject => "Latest Pins from Our Users")

  end

end

Then we defined the text for the e-mail to be sent out. In that, we displayed the list of the last five pins that have been created:

app/views/newsletter/letter.text.erb

<h3>Our Latest Pins</h3>

<% @pins.each do |p|%>

 <%=link_to p.name, p %>

<%end%>

In order to fetch this information and fire the method, we created a class method in the Pin controller. This method will fetch all the users and the last five pins. We will pass the user object and pins object to the Newsletter mailer class. Also, Model.all is depricated in Rails 4. A direct replacement for the Model.all call is Model.to_a. In order to limit the number of records to be selected in the query, we will first have to pass the order argument and then the limit argument:

  def self.send_newsletter

    @user = User.all.to_a

    @user.each do |u|

      @pins = self.order('id ASC').limit(5)

      Newsletter.letter(u, @pins).deliver

    end

  end

We fired this using a Rake task that directly calls the send_newsletter method in the Pin model:

namespace :newsletter do

  desc "Send Newsletter"

  task :send => :environment do

    Pin.send_newsletter

  end

end

In order to send e-mails on a periodic basis, we added the cron jobs to our application and used the whenever gem for it. The whenever gem uses a file called schedule.rb to define that our task will run every seven days:

Config/schedule.rb

every 7.days do

   rake "newsletter:send"

 end

The whenever gem edits the Linux cron jobs in order to send e-mails from time to time. The e-mails in our terminal look like the following:

Sent mail to myawesomeuser@gmail.com (15.8ms)

Date: Mon, 02 Sep 2013 07:11:29 +0800

From: noreply@pinpost.com

To: myawesomeuser@gmail.com

Message-ID: <5223c9a1d7189_ed53fb18a55acb44652d@rwub.mail>

Subject: Latest Pins from Our Users

Mime-Version: 1.0

Content-Type: text/plain;

charset=UTF-8

Content-Transfer-Encoding: 7bit

<h3>Our Latest Pins</h3>

<a href="/pins/zombie-t-shirt">Zombie Tshirt</a>

<a href="/pins/kyoto-ginkakuji-temple">Kyoto Ginkakuju Temple</a>

<a href="/pins/spaghetti-cheese">Spaghetti Cheese</a>

<a href="/pins/pasta-aut-gratin">Pasta Au Gratin</a>

<a href="/pins/long-road">Long road</a>

We can also browse to localhost:1080 to see the sent e-mails in our MailCatcher web console. The screenshot shows the output of the previous code:

Objective complete – mini debriefing

We can set weekly mail as a background job as well using Sidekiq or Resque. We will look at Sidekiq in later projects. It is a much advanced version of creating background jobs and job queues and is used in cases where there are several asynchronous jobs to be run in the background.

Securing an application from cross-site scripting or XSS

The Internet comes with its share of security concerns. There are several types of attacks you will have to avoid while working with your Rails application: session hacking, cookie stealing, SQL injections, and cross-site scripting. In this section, we will only look at cross-site scripting and how to avoid it in our application.

Engage thrusters

The following steps will give us some security tips:

1.    Check for vulnerability by adding a simple alert box in your text area field.

2.    Create a new board and add the following code in your description area:

3.  <h1>Board</h1>

4.    <p>This is a vulnerability test</p>

  <p><script>alert('This is a vulnerability test!');</script></p>

5.    If your application gives out an alert whenever the page loads, your site is vulnerable to cross-site scripting:

Engage thrusters

6.    This can occur even if we apply the html_safe filters and allow HTML to be passed as part of the text. By default, Rails sanitizes all HTML to text and uses the latest HTML5 standards in order to do so.

7.    We need to escape HTML in order to stop the JavaScript execution.

8.    We will use the Rails HTML escape helper in order to escape the HTML in textboxes and prevent the execution of JavaScript in our text area. This will ensure the security of our application from JavaScript attacks:

    <%=(@board.description) %>%>

Objective complete – mini debriefing

We have sanitized the HTML so that JavaScript is not inserted into our textboxes and executed every time the page is loaded. Cross-site scripting is a very serious issue. It could lead to concerns such as session and cookie stealing. A malicious user can enter such a JavaScript in our database and steal session information every time the page is loaded.

Rails has several forms of security built into the framework; let's be smart enough and use them.

Mission accomplished

In this project, we have created a simple social sharing website. We discussed creating pins and boards and resharing the pins. We looked at various jQuery libraries—Infinite scroll, Facebox, and Wookmark—and how to quickly use them to our advantages. We also used Solr to create a full-text search engine for our website.

We created a weekly mailer to increase our user engagement and used the cron job to make it a periodic task that runs in the background. Last but not least, we looked at potential security vulnerabilities and a simple way to fix these issues.

Hotshot challenges

Great! We have achieved a lot at the end of this project. Give yourself a pat on your back. Now it's time to take these concepts ahead and try out new things with what we've seen in this project:

·        Use Amazon S3 instead of the filesystem to upload files

·        Count the number of repins for each pin

·        Add facets

·        Write integration tests for the search option using minitest

·        Create a mailer with the five most shared pins