How to create PDF’s and Images from your website in Rails 4

Posted by Randy on July 29, 2010

I am going to show you how to generate both a pdf and image from a single action in a controller using the awesome, wkhtmltopdf library. This also uses PDFKit and WebSnap gems available on GitHub.

This example assumes the following:

  • wkhtmltopdf and wkhtmltoimage are already installed and accessible on in the PATH.
  • You have an html page setup to display the record.
  • You have created a pdf CSS file to help display the pdf, if you so choose.
  # config/initializers/mime_types.rb
  Mime::Type.register "application/pdf", :pdf
  Mime::Type.register "image/png", :png
 
  # app/controllers/items_controller.rb
  def show
    @item = Item.find(params[:id])
 
    respond_to do |format|
      format.html { }
      format.pdf {
        html = render :action => "show.html.erb"
        kit  = PDFKit.new( html, :zoom => 0.75 )
        kit.stylesheets << File.join( RAILS_ROOT, "public", "stylesheets", "pdf.css" )
 
        send_data kit.to_pdf, :filename => "item.pdf", :type => 'application/pdf', :disposition => 'inline'
      }
      format.png {
        html = render :action => "show.html.erb", :layout => "application.html.erb"
 
        # I am nil'ing these options out because my version of wkhtmltoimage does
        # not support the scale options and I do not want to crop the image at all.
        snap = WebSnap.new(html, :format => 'png', :'scale-h' => nil, :'scale-w' => nil,
          :'crop-h' => nil, :'crop-w' => nil, :quality => 100, :'crop-x' => nil, :'crop-y' => nil)
 
        send_data snap.to_bytes, :filename => "item.png", :type => "image/png", :disposition => 'inline'
      }
    end

Now you should be able to access three distinct views, each producing a different result

  http://example.com/items/1 # => Generates an html page.
  http://example.com/items/1.pdf # => Generates a pdf of the html page.
  http://example.com/items/1.png # => Generates a png of the html page.

You could easily also add more image types by just created another block for each format, and
changing the :format to whatever one you would like.

Request formats, filters, and functional tests…

Posted by Randy on March 04, 2010

I recently had to write some tests against a controller that was filtering based on the requesting format. In this case, I wanted to allow xml requests only, and redirect to login on everything else. This was fine when browsing or using curl by doing a simple:

skip_before_filter :login_required, :only => [:create], :if => Proc.new {|c| c.request.format.xml?}

My problem came when I was trying to create tests to verify that both html and xml requests did in fact produce the correct response. After many hours of messing around, I came up with a simple solution. First I skip the filters for every request, then I have another filter to re-enable them on anything but the xml request:

skip_before_filter :login_required, :only => [:create]
before_fiilter :only => [:create] do |c|
  c.send(:login_required) unless c.request.format.xml?
end

Voila!!! This allowed me to continue with my rails testing and browser and curl work appropriately. (I use curl to test the xml request).

def test_not_logged_in_normal_post
  post :create, :login => "test@test.com", :password => "test"
  assert_response :redirect
end
 
def test_not_logged_in_xml_post
  post :create, :format => 'xml', :login => "test@test.com", :password => "test"
  assert_response :success
end

Including methods and associations in a JSON Data set with Rails 1

Posted by Randy on October 02, 2009

I was poking around while working with creating an application specifically for web services. We decided to use JSON as the methods of transportation of data, but the problem came when I wanted to include custom methods, or associations in my data set. The solution was fairly simple, using the to_json method.

Suppose you have the following classes:

class Client < ActiveRecord::Base
  has_many :employees
end
 
class Employee < ActiveRecord::Base
  belongs_to :client
 
  def full_name
    "#{first_name} #{last_name}"
  end
end

We want the controller to return a client with association employees and the full name in the database. Here is how we would go about doing that:

def show
  @client = Client.find(params[:id])
 
  respond_to do |format|
    format.json { render :json => @client.to_json(
      :include => {
        :employee => {
          :only => :email,
          :methods => [ :full_name ]
        }
      }
    ) }
  end
end

You will end up with the following data set:

{ client: { name: "Some client", employee: { email: "test@test.com", full_name: "John Doe" } } }

Forgive me if I messed up the json output…doing it from memory :) There are of course way easier uses for this too, but I just decided to spit out a more complex one.

Will paginate – how to pass custom parameters 1

Posted by Randy on July 31, 2009

I came across and issue today that involved my pagination not working correctly. What was happening was a user would do a search with various criteria and the result would show with pagination links. Once they clicked on a new page, it would return them to a list of results, but that wasn’t using any criteria. The solution was fairly simple, but not obviously documented. (Unless you look in the source files)

<%= will_paginate @object, :params => { :custom_param => @custom_param_value } %>

Here are some additional custom params you can use:

Display options

  • :previous_label — default: “<< Previous" (this parameter is called :prev_label in versions 2.3.2 and older!)
  • :next_label — default: “Next >>”
  • :page_links — when false, only previous/next links are rendered (default: true)
  • :inner_window — how many links are shown around the current page (default: 4)
  • :o uter_window — how many links are around the first and the last page (default: 1)
  • :separator — string separator for page HTML elements (default: single space)

HTML options

  • :class — CSS class name for the generated DIV (default: “pagination”)
  • :container — toggles rendering of the DIV container for pagination links, set to false only when you are rendering your own pagination markup (default: true)
  • :id — HTML ID for the container (default: nil). Pass +true+ to have the ID automatically generated from the class name of objects in collection: for example, paginating ArticleComment models would yield an ID of “article_comments_pagination”.

Advanced options

  • :param_name — parameter name for page number in URLs (default: :page)
  • :params — additional parameters when generating pagination links (eg. :controller => “foo”, :action => nil)
  • :renderer — class name, class or instance of a link renderer (default: WillPaginate::LinkRenderer)

acts_as_similar – A basic similarity activerecord plugin

Posted by Randy on June 04, 2009

I had a need to find items that were similar to an item. I looked at acts_as_recommendable, and while this was very nice plugin, it was too slow when working with large data sets. I also decided that the results I needed didn’t need to be scientifically accurate, just a rough match. So I created this plugin to allow for a very elementary way to find items that are similar to the object you are working with. This works best when used with a has_many :through relationship.

Basically all it is doing is looking for other objects of the same class, that have some related value.

Git it here: http://github.com/freezzo/acts_as_similar/tree/master

Example 1
=========
Look for similar playlists, using the videos as what defines similarity.
This will look for all playlists that have a similar video as the playlist you are looking against.

class Playlist
  has_many :playlists_videos
  has_many :videos, :through => :playlists_videos
 
  acts_as_similar :videos
end

Example 2
=========
Look for similar playlists, using the title of the playlist as the similarity item.
This will look for all playlists that have a similar title as the playlist you are looking against.

class Playlist
  has_many :playlists_videos
  has_many :videos, :through => :playlists_videos
 
  acts_as_similar :field => :title
end

Example 3
=========
Look for similar playlists, using the video_id of the playlists_videos as the similarity item.
This will look for all playlists that have a similar video_id as the playlist you are looking against.

class Playlist
  has_many :playlists_videos
  has_many :videos, :through => :playlists_videos
 
  acts_as_similar :playlists_videos, :field => :video_id
end

Execute
=========

@playlist = Playlist.first
@playlist.similar

Note: This is a work in progress.

Some Possibly less know ActiveRecord configuration options

Posted by Randy on March 19, 2009

I am going to list from the guides.rubyonrails.com site, some possibly less know, but useful configuration options for ruby on rails. I didn’t know these existed until I had to figure out an elegant way to prefix tables with something, and I didn’t want to just type it into the migrations and models. Here is how you set the variables:

In your config/environments/development.rb (or production.rb, or whatever.rb) do the following:

config.active_record.table_name_prefix = "whatever_"

You can also select any of the following options instead of table_name_prefix to configure active record globally. Here is the list:

ActiveRecord::Base includes a variety of configuration options:

* logger accepts a logger conforming to the interface of Log4r or the default Ruby 1.8.x Logger class, which is then passed on to any new database connections made. You can retrieve this logger by calling logger on either an ActiveRecord model class or an ActiveRecord model instance. Set to nil to disable logging.

* primary_key_prefix_type lets you adjust the naming for primary key columns. By default, Rails assumes that primary key columns are named id (and this configuration option doesn’t need to be set.) There are two other choices:
o :table_name would make the primary key for the Customer class customerid
o :table_name_with_underscore would make the primary key for the Customer class customer_id

* table_name_prefix lets you set a global string to be prepended to table names. If you set this to northwest_, then the Customer class will look for northwest_customers as its table. The default is an empty string.

* table_name_suffix lets you set a global string to be appended to table names. If you set this to _northwest, then the Customer class will look for customers_northwest as its table. The default is an empty string.

* pluralize_table_names specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the customers table. If set to false, then the Customers class will use the customer table.

* colorize_logging (true by default) specifies whether or not to use ANSI color codes when logging information from ActiveRecord.

* default_timezone determines whether to use Time.local (if set to :local) or Time.utc (if set to :utc) when pulling dates and times from the database. The default is :local.

* schema_format controls the format for dumping the database schema to a file. The options are :ruby (the default) for a database-independent version that depends on migrations, or :sql for a set of (potentially database-dependent) SQL statements.

* timestamped_migrations controls whether migrations are numbered with serial integers or with timestamps. The default is true, to use timestamps, which are preferred if there are multiple developers working on the same application.

* lock_optimistically controls whether ActiveRecord will use optimistic locking. By default this is true.

Paperclip and getting the original file path before its uploaded

Posted by Randy on March 17, 2009

Back to some rails stuff. Here is a little tip that helped me out with reading in the file that was being uploaded before I saved the record. I was doing this because I wanted to validate the data in the file before I save the record. Here is what I did:

class SomeModel < ActiveRecord::Base
  has_attachment :some_file
 
  def validate
    file = self.some_file.to_file(:original)
    data = File.read(file)
    # Do some validation on data
    errors.add_to_base "File format invalid" if data.nil?
  end
end

NoMethodError on nil.each?

Posted by Randy on March 12, 2009

Have you ever have to iterate over an array from say the params hash? You probably have added some code to make sure that data exists in the hash element first before doing the loop. This is a cool little trick to help condense some code. Normally I would write this:

if params[:accounts] and params[:accounts].size > 0
  params[:accounts].each do |account|
    # Do something with account here
  end
end

As you can see, kind of ugly. Here is the change:

(params[:accounts] || []).each do |account|
  # Do something with account
end

AHH!!! Must nicer!

Moving files in Rails using String 1

Posted by Randy on March 05, 2009

Since ruby allow for really easy overriding of objects and classes, I decided to make it easier and cleaner to move my files. Here is how you currently do it:

FileUtils.move("/path/to/file", "/path/to/new/file")

I know this isn’t a huge deal, but I would like to clean it up a little bit. I used an initializer (RAILS_ROOT/config/initializers/core_extensions.rb) to override the String class as such:

class String
   def move(to = nil)
     if to
       if File.exist?(self)
         FileUtils.move(self, to)
       end
     end
   end
end

This allows me to do the following to move files:

"/path/to/file".move("/path/to/new/file")

I though it was pretty cool!

Rails sessions round two – Turn off by request type 1

Posted by Randy on January 22, 2009

This is a follow up to my previous post about clearing out sessions. It turns out I was pulling a lot of crap sessions from rss feeds and xml requests which I didn’t need. I had over 14k new sessions in less than one day. To help combat this, I came up with a solution to turn sessions off based on a request type. I added this to application.rb file:

session :off, :if => Proc.new { |req| ['xml', 'rss'].include?(req.params[:format]) }

You could ideally just add this to an individual controller, and add :o nly or :except to it as well, but I wanted to eliminate all of the controllers and actions.