Archived entries for Rails

Backbone.js and Rails has_many :through Relationships

The has_many :through relationship is a core relationship for most rails apps. Implementing a Backbone front-end into your app requires mirroring your many-to-many back-end model relationship in Backbone. While Backbone-relational provides a great framework for handling model relationships in Backbone, I wanted to see if I could get it working on my own with only the bare, necessary functionality.

First, the Rails models. I will use a typical shopping cart example:

class Order < ActiveRecord::Base
  has_many :line_items
  has_many :products, :through => :line_items
  accepts_nested_attributes_for :line_items, :allow_destroy => true
end
 
class Prodcts < ActiveRecord::Base
end
 
class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
end

So an Order has many Products through the LineItem join model. In this many-to-many scenario I only care about the Products an Order has, and do not need to know the inverse–what Orders a specific Product has been in. For this reason, I am not setting up any relationships in the Product model.

Now to set up the Backbone model structure. In Backbone, the model relationships will not be a 1-to-1 mapping of the rails model relationships. In Backbone we simply need to worry about an Order containing a collection of Products. No need for a join model. See this post for a more detailed explanation.

Backbone models (in CoffeeScript):

class Product extends Backbone.Model
  paramRoot: 'region'
 
class ProductsCollection extends Backbone.Collection
  model: Product
  url: '/products'
 
class Order extends Backbone.Model
  initialize: ->
    @products = new ProductsCollection()
 
  line_items_attributes: ->
    @products.map (p) ->
      {product_id: p.get("id")}
 
  toJSON: ->
    json = {order : _.clone(@attributes)}
    _.extend(json.order, {line_items_attributes: @line_items_attributes()})

Two important points here. Our Order instances initialize to have an attribute @products, which is just a Backbone Collection to hold all of its related products. This will emulate a one-to-many relationship.

Second, we are overwriting toJSON() in order to structure the JSON data so that it’s useful to rails. More on that later.

Next let’s put the Backbone models to use. I can add Products to an Order using the add() funciton.

@order = new Order()
@order.products.add([@product1, @product2])
@order.save()

When the order is saved, the overwritten toJSON() function will also include the order’s product data. The JSON data needs to be structured in such a way as to tell rails to create a new Order record, and new LineItem records for each product in the order.

{"order"=>
  {"user_id"=>99, "line_items_attributes"=>
    [{"product_id"=>6}, {"product_id"=>11}, {"product_id"=>26}]
  }
}

Setting

accepts_nested_attributes_for :line_items

in the Order moddel allows it to create both Order records and associated LineItem records in one JSON post.

Since rails only needs to know the ids of each Product we wish to associate to an Order, the products.add() call can be simplified to only contain id data.

@order = new Order()
@order.products.add([{product_id: @product1.id}, {product_id: @product2.id}])
@order.save()

This will ensure our JSON data is as clean and simple as it needs to be in order to create the association records.

This is a very rough first pass at getting Backbone to play well with a Rails has_many :through set-up in one direction. I haven’t covered how to load existing Orders into Backbone for view rendering. This will require customizing the as_json method in rails to include it’s related Product data. I hope to cover that in a part 2 post. Please fill me in on other approaches you have come up with to handle Backbone model associations.

Extracting Geolocation Image Data with Carrierwave and RMagick on Heroku

Many smartphones embed latitude and longitude EXIF data relative to where the photo was taken. So why not extract that data and store it to the database when a user uploads an image?

I am currently using this technique in an API I built which allows users to post a picture of an item they would like to sell. Next an ad is created for the item with the location populated by the image’s embedded geolocation data.

On Heroku we would like the exif data extracted from the temporary image file before it is uploaded to S3. To do this we can call a method to extract the exif date before_save. In the model file:

app/models/item_image.rb
class ItemImage < ActiveRecord::Base
  belongs_to :item
 
  mount_uploader :image, ImageUploader
 
  before_save :extract_geolocation
 
  def extract_geolocation
    img = Magick::Image.read(image)[0] rescue nil
 
    return unless img
    img_lat = img.get_exif_by_entry('GPSLatitude')[0][1].split(', ') rescue nil
    img_lng = img.get_exif_by_entry('GPSLongitude')[0][1].split(', ') rescue nil
 
    lat_ref = img.get_exif_by_entry('GPSLatitudeRef')[0][1] rescue nil
    lng_ref = img.get_exif_by_entry('GPSLongitudeRef')[0][1] rescue nil
 
    return unless img_lat && img_lng && lat_ref && lng_ref
 
    latitude = to_frac(img_lat[0]) + (to_frac(img_lat[1])/60) + (to_frac(img_lat[2])/3600)
    longitude = to_frac(img_lng[0]) + (to_frac(img_lng[1])/60) + (to_frac(img_lng[2])/3600)
 
    latitude = latitude * -1 if lat_ref == 'S'  # (N is +, S is -)
    longitude = longitude * -1 if lng_ref == 'W'   # (W is -, E is +)
 
    self.lat = latitude
    self.lng = longitude
  end
 
  def to_frac(strng)
    numerator, denominator = strng.split('/').map(&:to_f)
    denominator ||= 1
    numerator/denominator
  end
end

The extract_geolocation method uses RMagick to access the image EXIF data. If it contains latitude and longitude data expressed as linear units (degree, minute, second), but we would like it in decimal format so that it can be fed into the Google Maps API and returned as a city, state and zipcode. This is what the calculations in the method are handling.

With a gem like Geocoder we can easily wire in geocode look up and store the city, state, and zipcode in the image record.

  if geo = Geocoder.search("#{latitude},#{longitude}").first
    self.city = geo.city
    self.state = geo.state
    self.zipcode = geo.postal_code
  end

Putting it all together:

Feedback on how to improve the process is welcome.

How To Setup Twitter Style Following In Your User Model

Setting up a Twitter-like following scheme can be a little tricky at first glance. The reason being that your User model will need to refer to itself in order for a user to follow another user. Here’s how to look at the relationships:

user-follows

We can build this relationship using self-referential associations, and a model as a join table. We’ll call the joining model Follow.

db/migrate/create_follows.rb
class CreateFollows < ActiveRecord::Migration
  def self.up
    create_table :follows do |t|
      t.integer :follower_id
      t.integer :followed_id
      ...

Next, we can add the follower and followed associations to the our new Follow model.

app/models/follow.rb
class Follow < ActiveRecord::Base
  belongs_to :follower, :class_name => "User"
  belongs_to :followed, :class_name => "User"
end

Lastly, we can set up the self-referential associations in the User model.

app/models/user.rb
class User < ActiveRecord::Base
  has_many :follows, :foreign_key => "follower_id", :class_name => "Follow", :dependent => :destroy
  has_many :users_followed, :through => :follows, :source => :followed
 
  has_many :followings, :foreign_key => "followed_id", :class_name => "Follow", :dependent => :destroy
  has_many :users_following, :through => :followings, :source => :follower
end

This allows the User to be both followed and a follower. See this blog post for a good explanation of self-referential associations.

Your controller can look basically like this:

app/controllers/follows_controller.rb
class FollowsController < ApplicationController
 
  def create
    @follow = current_user.follows.build(:followed_id => params[:followed_id])
    if @follow.save
      flash[:notice] = "You are now following #{@follow.followed.name}"
      redirect_to user_path(@follow.followed)
    else
      flash[:error] = "Unable to follow."
      redirect_to user_path(@follow.followed)
    end
  end
 
  def destroy
    @follow = current_user.follows.find_by_followed_id(params[:followed_id])
    @follow.destroy
    flash[:notice] = "Removed follow."
    redirect_to user_path(params[:followed_id])
  end
 
end


Copyright © 2004–2009. All rights reserved.

RSS Feed. This blog is proudly powered by Wordpress and uses Modern Clix, a theme by Rodrigo Galindez.