1 - Custom Rake tasts #66

(1/4/13)

Task dependency syntax is '=>' in task definition line

Requires and runs dependent task before running current task.
'[]', ie array, can be used to supply multiple dependencies

2 - Rails 3 Screencasts - ActionDispatch

(11/4/13)

Refactor of ActionController stack

http
middleware
routing

Nicer syntax for routing cfg

'session#new' = 'session' controller and 'new' action
Redirects available direct from routing cfg file
Access Rack direct from routing cfg file

3 - Factories not Fixtures #158

(10/5/14)

Make RSpec aware of Factory by adding a 'require' line to '/spec/spec_helper.rb'

Factories are an excellent way to improve the tests in your Rails application

4 - What is new in Rails 4 #400

(11/11/14)

Rails 4 has support for native datatypes in Postgres.

$ rails g scaffold article name content:text published_on:date tags properties:hstore

Then alter migration:

* Add 'execute "create extension hstore"'.
* Add the 'array' option to the ':tags' column and setting it to 'true':
  ...
  "t.string :tags, array: true"
  ...

When creating a new Article, we can pass in a Ruby array for 'tags' which will be converted to a Postgres array.

While for the 'properties' we can pass in a hash.

>> Article.create! name:"Hello", tags: %w[ruby rails], properties: {author:"Eifion"}

We could do something similar by serializing a text column but this approach allows us to interact with these columns, in a much more efficient way!

ActiveRecord features:

ActiveModel::Model

It’s especially great if you want to pass a custom class into a 'form_for' call.

Views

Routes

Misc

5 - Importing CSV and Excel #396

(1/12/14)

form_tag

Use 'form_tag' instead of 'form_for' since don't have object to handle importing yet

Form submitted to a new 'import' action on the 'ProductsController' (set by 'routes' file addition)

Set 'multipart' option so that form can handle file uploads

/app/views/products/index.html.erb

<h2>Import Products</h2>
<%= form_tag import_products_path, multipart: true do %>
  <%= file_field_tag :file %>
  <%= submit_tag "Import" %>
<% end %>

Controller action

ProductsController::import

Importing CSV data

App config file contains “ require 'csv' ” so can use Ruby's built-in CSV library

/app/models/product.rb

def self.import(file)
  CSV.foreach(file.path, headers: true) do |row|
    Product.create! row.to_hash
  end
end

This will yield to the block for each line of data that’s found.

'headers' option means the first line of data will be expected to hold each column’s name which will be used to name the data.

Product created by passing 'row.to_hash'

As long as the column names map to attributes in Product a new record will be created for each row

Modifying Existing Records

If we had 'id' column in CSV data then could update existing records (rather than just adding new ones)

Then use 'find_by_id' and only update certain attributes (listed by model's 'attr_accessible' list)

/app/models/product.rb

def self.import(file)
  CSV.foreach(file.path, headers: true) do |row|
    product = find_by_id(row["id"]) || new
    product.attributes = row.to_hash.slice(*accessible_attributes)
    product.save!
  end
end

Before 'products.csv':

  name,price,released_on
  Christmas Music Album,12.99,2012-12-06
  Unicorn Action Figure,5.85,2012-12-06

Downloaded existing products:

  id,name,released_on,price,created_at,updated_at
  6,Christmas Music Album,2012-12-06,12.99,2012-12-29 20:55:29 UTC,2012-12-29 20:55:29 UTC
  7,Unicorn Action Figure,2012-12-06,5.85,2012-12-29 20:55:29 UTC,2012-12-29 20:55:29 UTC
  ...

Importing Excel spreadsheets

Roo gem (other gems are available…)

/Gemfile
gem 'roo'

/config/application.rb
require 'iconv'

Need header columns as keys to hash so get array of headers + array of current row + call 'transpose' to alter rows & cols.

Altered: /app/models/product.rb

def self.import(file)
  spreadsheet = open_spreadsheet(file)
  header = spreadsheet.row(1)
  (2..spreadsheet.last_row).each do |i|
    row = Hash[[header, spreadsheet.row(i)].transpose]
    product = find_by_id(row["id"]) || new
    product.attributes = row.to_hash.slice(*accessible_attributes)
    product.save!
  end
end

'original_filename' used on the uploaded file because it’s stored in a temporary file which doesn’t have an extension.
3rd option tells Roo not to raise an exception if the file name doesn't match the type.

def self.open_spreadsheet(file)
  case File.extname(file.original_filename)
  when '.csv' then Roo::Csv.new(file.path, nil, :ignore)
  when '.xls' then Roo::Excel.new(file.path, nil, :ignore)
  when '.xlsx' then Roo::Excelx.new(file.path, nil, :ignore)
  else raise "Unknown file type: #{file.original_filename}"
  end
end

(One issue with this solution is an exception is raised when we try to import files exported from our app, not Excel.)

Validating data - using form_for not form_tag

Use 'form_for' so that we can easily display any validation errors just like we would with any other model.

This model isn’t stored in the database, however, it’s a simple Ruby class.

We use ActiveModel here to simulate ActiveRecord.

See github.com/railscasts/396-importing-csv-and-excel/tree/master/store-with-validations