August 10th, 8:48am 0 comments

with_scope is protected

I've seen a lot of people run into trouble using with_scope on edge. It's now a protected method Ticket: http://dev.rubyonrails.org/ticket/8524.

The solution? Don't use it or use it sparingly. They made it protected for a reason - to protect people from abusing it - but if you absolutely must have at it then you can do something like this:

klass.send(:with_scope, :find => { :conditions => conditions }) { yield }

So say you want to scope your finders to retrieve only MyModel objects of type 'super':

def my_super_duper_scoping(klass)
  klass.send(:with_scope, :find => { :conditions => "type = 'super'" }) { yield }
end

my_super_models = my_super_duper_scoping(MyModel) do
    MyModel.find(:all)
  end

my_super_models_named_david = my_super_duper_scoping(MyModel) do
    MyModel.find(:all, :conditions => ["name = '?'", 'david')
  end

Using self.class.send is a way to get around public and private checking in ruby 1.8. Use with caution.

Filed under rails ruby
Posted
May 22nd, 12:06pm 0 comments

Tracking Views in Rails

ActsAsViewable is plugin that allows you to track page and asset views in your Rails application. For example, you can use it to track how many times a page is visited or how many times a particular image is viewed.


Trac: http://trac.intridea.com/trac/public/wiki/ActsAsViewable


Subversion repository: http://svn.intridea.com/svn/public/acts_as_viewable


Installation:

script/plugin install http://svn.intridea.com/svn/public/acts_as_viewable

OR


cd vendor/pluginssvn co http://svn.intridea.com/svn/public/acts_as_viewable

Create the tables where views will be tracked:


class CreateViewings < ActiveRecord::Migration  def self.up    create_table :viewings do |t|      t.column :viewable_type,  :string      t.column :viewable_id,    :integer      t.column :views,          :integer,   :default => 0      t.column :created_at,     :datetime, :null => false      t.column :updated_at,     :datetime    end  end  def self.down    drop_table :viewings  endend

Set the objects you want to track views for:

class SomeAsset < ActiveRecord::Base  acts_as_viewableend

Now you can increment views for these objects wherever you need to. For example in the show action of our SomeAssetController:


class SomeAssetController < ApplicationController  def show    @some_asset = SomeAsset.find(params[:id])    @some_asset.increment_views  endend

To get the number of views:


@some_asset.views
Posted
May 22nd, 11:59am 0 comments

Automatically Expiring Sessions in Rails

SessionExpiration is plugin that allows you to expire sessions after X seconds of
inactivity. Useful for when you want to automatically log out users if they’re idle.


Trac: http://trac.intridea.com/trac/public


Subversion repository: http://svn.intridea.com/svn/public/session_expiration/


Installation:

script/plugin install http://svn.intridea.com/svn/public/session_expiration

OR


cd vendor/pluginssvn co http://svn.intridea.com/svn/public/session_expiration

Specify when to expire session in your ApplicationController to do it site wide or you can do it for specific controllers:


class ApplicationController  expire_session_in 5.minutesend

If you want to run a method when the session expires use this:


class ApplicationController  expire_session_in 5.minutes, :after_expiration => :some_method  def some_method    flash[:notice] = "You have been logged out due to inactivity"  endend
Posted
December 11th, 1:38am 1 comment

Ajax uploads? Image manipulation & drag-and-drop sorting.

Wouldn’t it be nice to allow uploads in a cool Ajaxy way? Well, because of security restrictions it’s just not possible. There are however ways to create the same effect.


Here’s a quick demo of an ajax-ish image upload as well as some image manipulation functionality, and drag and drop sorting. I’m not sure this will work on all browsers but it’s been tested successfully with most. This was created about 4 months ago and I never had time to polish any of it up so take what you can from it.


http://www.naffis.com/demos/image_demo


First our layout (layouts/image_demo.rhtml):


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/strict.dtd"><html>    <head>        <title>Image Demo</title>        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">        <%= javascript_include_tag :defaults %>        <%= stylesheet_link_tag 'image_demo' %>    </head>    <body>        <div id="titlebar">Image Demo</div>        <%= render :partial => "upload_form" %>        <div id="centercontent">            <%= yield %>        </div>        <div id="next">            <%= link_to "Create Animated Gif", :action => "animate" %>        </div>        <div id="bottom">            &copy; naffis.com 2006        </div>    </body></html>

We’re going to extend the form_remote_tag to handle file uploads.


Drop this in your lib directory (lib/remote_uploads.rb):


module ActionView  module Helpers    module PrototypeHelper      alias_method :form_remote_tag_old, :form_remote_tag      def form_remote_tag(options = {})        if options[:html] && options[:html][:multipart]          uid = "a#{Time.now.to_f.hash}"          <<-STR                    <form method="post" action="#{url_for options[:url].update({:iframe_remote => true})}" enctype="multipart/form-data" target="#{uid}" #{%(onsubmit="#{options[:loading]}") if options[:loading]}>          STR        else          form_remote_tag_old(options)        end      end    end  endend

Add the require in your environment.rb:


require 'remote_uploads.rb'

This will create a custom form for file uploads (multipart => true) that submits to a hidden iframe. If it’s not a file upload then it will revert to the standard form_remote_tag of PrototypeHelper.


Some boring half baked styles for our demo:


body {  background-color:        #FFFFFF;  background-image:        url(/maps/images/gradient.jpg);  background-repeat:    no-repeat;  color:                            #666666;  font-family:                arial, sans;  font-size:                    100%;  line-height:                1.7em;  margin:                            1em 2em;}#titlebar {  font-size:                     1.2em;  border-bottom:                    2px solid #333333;  margin-bottom:                    1em;  padding-bottom:                1em;}h2 {  font-size:                     1.2em;}ul.navigation {  background-color:        #333333;  padding:                        0em 0.5em;  list-style-type:        none;}ul.navigation li {  border-right:                1px solid #666666;  display:                        inline;}.navigation a {  color:                            #FFFFFF;  padding:                        0.5em;}.description {  font-size:                    1.2em;}.upload {  font-size:                    1.2em;}strong {  background-color:        #FFFF99;}#centercontent {  width: 100%;  text-align: center;  margin-bottom:                1em;  padding-bottom:                1em;  margin-top:                    1em;  padding-top:                1em;}#bottom {    width: 100%;    float: left;    text-align: center;    border-top:                    2px solid #333333;    margin-top:                    1em;    padding-top:                1em;}div.float {  width: 120px;  padding: 10px;  float: left;}div.spacer {  clear: both;}div.float img {  margin-left: 5px;  }div.float p {  font-size: 9px;  text-align: center;  }#image-list ul {  list-style: none;}#image-list ul li {  list-style: none;  display: inline;    float: left;    width: 120px;    height: 120px;    padding: 10px;  border: 1px solid #000;}

We’re using Sean Treadway’s responds_to_parent plugin (http://sean.treadway.info/svn/plugins/responds_to_parent/) to execute our RJS generated javascript in the parent window instead of the iframe which the file upload is submitted to. There are other ways of doing this that use less code but the plugin is simple so why not use it?


Everything from this point on is pretty self explanitory. I can expand on it later but here’s the rest of the code.


Our index:

<div id="image-list">    <ul id="sortable_list">        <% for @asset in @assets %>            <%= render :partial => "image_container", :locals => { :asset => @asset } %>        <% end %>    </ul></div><%= sortable_element('sortable_list', :constraint => false, :url => {:action => :update_positions}) %>


Some partials used above:


_image_container.rhtml

<li id="item_<%= @asset.id %>" class="float">    <%= render :partial => "image_thumb", :locals => { :asset => @asset } %></li>


_image_thumb.rhtml

<%= image_tag @asset.thumbnail, :border => 2 %>    <br>    <%= link_to_remote(image_tag("arrow_rotate_anticlockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "left"} ) %>    &nbsp;    <%= link_to_remote(image_tag("cross.png", :border => 0), :url => {:action => "remove", :id => @asset.id} ) %>    &nbsp;    <%= link_to_remote(image_tag("arrow_rotate_clockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "right"} ) %>


_upload_form.rhtml

<%= form_remote_tag(:url => {        :controller => "image_demo",        :action => "create" },        :html => {:multipart => true}) %>    <b>Picture:</b>&nbsp;    <%= file_field_tag "asset" %>&nbsp;    <%= submit_tag "Upload" %>&nbsp;<%= end_form_tag %>


Our RJS to handle the create, remove, and rotate.


create.rjs

if @asset.new_record?  page.alert "There was a problem uploading your file:\n" +  @asset.errors.full_messages.join("\n")else  page.insert_html :top, 'sortable_list', :partial => 'image_container', :locals => { :asset => @asset }  page.visual_effect :highlight, "item_#{@asset.id}"  page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }end


remove.rjs

page.remove "item_#{@asset_id}"page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }


rotate.rjs

page.replace_html "item_#{@asset.id}", :partial => 'image_thumb', :locals => { :asset => @asset }page.visual_effect :highlight, "item_#{@asset.id}"page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }


Our controller:

class ImageDemoController < ApplicationController  layout 'image_demo'  def index    session[:uid] = Time.now.to_i unless session[:uid]    @assets = Asset.find(:all,                         :conditions => ["user_id = ?", session[:uid].to_i],    :order => "position")  end  def create    @asset = Asset.new()    @asset.uploaded_file = params['asset']    @asset.position = 0    @asset.user_id = session[:uid].to_i    @asset.save    responds_to_parent do      render :action => 'create.rjs'    end    return  end  def list    @assets = Asset.find(:all,                         :conditions => ["user_id = ?", session[:uid].to_i],    :order => "position")  end  def update_positions    params[:sortable_list].each_with_index do |id, position|      Asset.update(id, :position => position)    end    render :nothing => true  end  def rotate    @asset = Asset.find(params[:id])    degrees = params[:direction] == "left" ? -90 : 90    @asset.rotate(degrees)  end  def remove    @asset_id = params[:id]    Asset.delete(@asset_id)  endend


Our asset model:

require 'RMagick'class Asset < ActiveRecord::Base  def uploaded_file=(incoming_file)    content_type = incoming_file.content_type.chomp    if content_type.rindex(/image\/[(jpe?g)||(gif)]/)      self.name = base_part_of(incoming_file.original_filename)      base_dir = "/some/path/you/like"      # save original file      self.original = "image_demo_assets/o_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name      File.open(base_dir+self.original,File::CREAT|File::TRUNC|File::WRONLY,0666){ |f|        f.write(incoming_file.read)      }      self.resized = "image_demo_assets/r_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name      resized = Magick::Image.read(base_dir+self.original).first      resized.change_geometry!('500x500') { |cols, rows, img|        img.resize!(cols, rows)      }      resized.write(base_dir+self.resized)      self.thumbnail = "image_demo_assets/t_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name      thumb = Magick::Image.read(base_dir+self.original).first      thumb.change_geometry!('100x100') { |cols, rows, img|        img.resize!(cols, rows)      }      thumb.write(base_dir+self.thumbnail)      self.save    end  end  def rotate(degrees)    base_dir = "/some/path/you/like"    #main photo    image = Magick::ImageList.new(base_dir+self.original)    image = image.rotate(degrees)    image.write(base_dir+self.original)    # resized    resized = Magick::ImageList.new(base_dir+self.resized)    resized = resized.rotate(degrees)    resized.write(base_dir+self.resized)    # thumb    thumb = Magick::ImageList.new(base_dir+self.thumbnail)    thumb = thumb.rotate(degrees)    thumb.write(base_dir+self.thumbnail)  end  private  def base_part_of(file_name)    name = File.basename(file_name)    name.gsub(/[^W._-]/, '')    sanitize_filename(name)  end  # Fixes a 'feature' of IE where it passes the entire path instead of just the filename  def sanitize_filename(value)    #get only the filename (not the whole path)    just_filename = value.gsub(/^.*(\\|\/)/, '')    just_filename.gsub(/[^\w\.\-]/,'_')  endend


Some suggestions:


  • Use form_for and get rid of some ugliness in the controller by using Asset.new(params[:asset]) instead of setting each value individually.

  • Use simply_helpful for generiting your DOM id’s.

  • Use acts_as_attachment for handing the storing of files.

  • Better validations (aaa will handle that too).

  • Rewrite the whole thing.

Again, this is a VERY quick-and-dirty demo written in about 20 minutes with so much room for improvement. If I had the time I would, but alas I hope it helps.

Posted
October 21st, 2:18pm 0 comments

Problem with has_many :through

I recently ran into a problem using has_many :through relationships. The edge code works fine when using standard id’s but for those using legacy databases or non-standard id’s in your join table the code fails when trying to add or delete an association.


Something like this would fail:


create_table :books, :force => true do |t|  t.column :name, :stringendcreate_table :citations, :id => false, :force => true do |t|  t.column :book1_id, :integer  t.column :book2_id, :integerendclass Book < ActiveRecord::Base  has_many :citations, :foreign_key => 'book1_id'  has_many :references, :through => :citations, :source => :reference_of, :uniq => trueendclass Citation < ActiveRecord::Base  belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id  belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id  belongs_to :book2, :class_name => "Book", :foreign_key => :book2_idendawdr = Book.create!(:name => "Agile Web Development with Rails")rfr = Book.create!(:name => "Ruby for Rails")awdr.references << rfrawdr.delete(rfr)

There’s further information at http://dev.rubyonrails.org/ticket/6466


If you’re running into this problem you can patch your local version of rails. First freeze edge in your tree. Then create the file has_many_through_patch.rb in your lib directory with the following code:


module ActiveRecord  class HasManyThroughCantDisassociateNewRecords < ActiveRecordError #:nodoc:    def initialize(owner, reflection)      super("Cannot disassociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")    end  end  module Associations    class HasManyThroughAssociation      # Construct attributes for :through pointing to owner and associate.      def construct_join_attributes(associate)        construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)      end      # Remove +records+ from this association.  Does not destroy +records+.      def delete(*records)         return if records.empty?         records.each { |associate| raise_on_type_mismatch(associate) }         through = @reflection.through_reflection         raise ActiveRecord::HasManyThroughCantDisassociateNewRecords.new(@owner, through) if @owner.new_record?         load_target         klass = through.klass         klass.transaction do           flatten_deeper(records).each do |associate|             raise_on_type_mismatch(associate)             raise ActiveRecord::HasManyThroughCantDisassociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?             @owner.send(@reflection.through_reflection.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))             @target.delete(associate)           end         end         self       end    end  endend

Then in your environment.rb add the following:


require 'has_many_through_patch'

You should be able to add and delete now until the patch is committed.

Filed under has_many patch rails ruby
Posted
August 31st, 11:29am 0 comments

Ruby on Rails, Ajax & CSS Star Rating System

I’m sure everyone by now has seen those oh-so-Web 2.0 star rating features on hundreds of websites. Well I needed to implement one for a site I’m working on and I couldn’t find a complete example anywhere (not in RoR). So here it is. A complete Rails based Ajax and CSS star ratings sytem with some RJS thrown in for good measure.


I used Rogie’s very elegant CSS only star rating system found here CSS Star Rating Part Deux. I also used Chris Ingrassia’s acts_as_rateable plugin.


So here we go.


Get the CSS and change the image url’s


First figure out which version of the CSS ratings you like. I used this example.


/*             styles for the star rater                */    .star-rating{        list-style:none;        margin: 0px;        padding:0px;        width: 150px;        height: 30px;        position: relative;        background: url(/images/star_rating.gif) top left repeat-x;    }    .star-rating li{        padding:0px;        margin:0px;        /*\*/        float: left;        /* */    }    .star-rating li a{        display:block;        width:30px;        height: 30px;        text-decoration: none;        text-indent: -9000px;        z-index: 20;        position: absolute;        padding: 0px;    }    .star-rating li a:hover{        background: url(/images/star_rating.gif) left center;        z-index: 2;        left: 0px;        border:none;    }    .star-rating a.one-star{        left: 0px;    }    .star-rating a.one-star:hover{        width:30px;    }    .star-rating a.two-stars{        left:30px;    }    .star-rating a.two-stars:hover{        width: 60px;    }    .star-rating a.three-stars{        left: 60px;    }    .star-rating a.three-stars:hover{        width: 90px;    }    .star-rating a.four-stars{        left: 90px;    }    .star-rating a.four-stars:hover{        width: 120px;    }    .star-rating a.five-stars{        left: 120px;    }    .star-rating a.five-stars:hover{        width: 150px;    }    .star-rating li.current-rating{        background: url(/images/star_rating.gif) left bottom;        position: absolute;        height: 30px;        display: block;        text-indent: -9000px;        z-index: 1;    }

Make sure you change your image url’s so that your Rails app can find them.


Get the images for your CSS


Grab the images used in your CSS and put them in your images directory. Here are both


.


Install the acts_as_rateable plugin.


Run the following from the root of your Rails app to install the plugin.


script/plugin install http://juixe.com/svn/acts_as_rateable

Create the tables used by acts_as_rateable


Create a file db/migrate/xxx_create_ratings.rb (xxx is 001 if it’s the first migration file you have).


class CreateRatings< ActiveRecord::Migration  def self.up    create_table :ratings, :force => true do |t|      t.column :rating, :integer, :default => 0      t.column :created_at, :datetime, :null => false      t.column :rateable_type, :string, :limit => 15,      :default => "", :null => false      t.column :rateable_id, :integer, :default => 0, :null => false      t.column :user_id, :integer, :default => 0, :null => false    end    add_index :ratings, ["user_id"], :name => "fk_ratings_user"  end  def self.down    drop_table :ratings  endend

Run your migration.


rake migrate

You should now have the appropriate tables.


Make one of your models rateable


I was trying to add a rating system for the model Asset. Yours can obviously be whatever you like but from here on out I’ll be using Asset. So add acts_as_rateable to your model.


class Asset < ActiveRecord::Base  acts_as_rateable  ...end

Create a controller to handle the rating submissions


Create the file /controllers/rating_controller.rb


class RatingController < ApplicationController  def rate    @asset = Asset.find(params[:id])    Rating.delete_all(["rateable_type = 'Asset' AND rateable_id = ? AND user_id = ?",      @asset.id, current_user.id])    @asset.add_rating Rating.new(:rating => params[:rating],      :user_id => current_user.id)  endend

Two things to note here. First I’m associating ratings to users. I’ve already implemented a user/permission system for my site using the model User. Use whatever is appropriate for you. You can modify this whole example to work without associating ratings to users, the acts_as_rateable plugin will handle it just fine. However, I’m not going to get into that here.


Since I am associating ratings to users it would be bad to have a user skew the results by storing multiple ratings for a single Asset. Hence the delete. I’m telling it to delete all ratings for the rateable_type ‘Asset’ and the id (rateable_id) of the Asset. The rateable_type of Asset is handled by the plugin and stored in the ratings table.


Create your views


Create the partial /views/rating/_rating.rhtml


<%= number_with_precision(asset.rating, 1) %>/5 Stars<br><ul class='star-rating'>    <li class='current-rating' style='width:<%= (asset.rating * 30).to_i -%>px;'>          Currently <%= number_with_precision(asset.rating, 1) %>/5 Stars.        </li>    <li>        <%= link_to_remote( "1", {:url => { :controller => "rating_demo",            :action => "rate", :id => asset.id, :rating => 1}},            :class => 'one-star', :name => '1 star out of 5') %>    </li>    <li>        <%= link_to_remote( "2", {:url => { :controller => "rating_demo",            :action => "rate", :id => asset.id, :rating => 2}},            :class => 'two-stars', :name => '2 stars out of 5') %>    </li>    <li>        <%= link_to_remote( "3", {:url => { :controller => "rating_demo",            :action => "rate", :id => asset.id, :rating => 3}},            :class => 'three-stars', :name => '3 stars out of 5') %>    </li>    <li>        <%= link_to_remote( "4", {:url => { :controller => "rating_demo",            :action => "rate", :id => asset.id, :rating => 4}},            :class => 'four-stars', :name => '4 stars out of 5') %>    </li>    <li>        <%= link_to_remote( "5", {:url => { :controller => "rating_demo",            :action => "rate", :id => asset.id, :rating => 5}},            :class => 'five-stars', :name => '5 stars out of 5') %>    </li></ul>

Obviously it’s using Ajax with the prototype helper link_to_remote to submit the user’s rating. One thing to note. Where you see width:<= (asset.rating * 30).to_i ->px;’ you’ll have to modify this to correspond with the images you chose to use. The one I’m using has images which are 30px wide. If you chose the smaller star images then you’ll have to modify this calculation to correspond to your image width. By the way, this is the line that handles the display of the current rating.


And now a little RJS


Create the file /views/rating/rate.rjs


page.replace_html "star-ratings-block", :partial => 'rating/rating', :locals => { :asset => @asset }

This will replace the star ratings with the partial we created previously in order to reflect any rating changes made by the submission.


And finally put it on your page


Render the partial in one of your views.


<div id="star-ratings-block">    <%= render :partial => "rating/rating", :locals => { :asset => @asset } %></div>

This needs @asset (or whatver you’re going to be using) in order to function.


Done


Now wasn’t that easy? Gotta love rails. 10 minutes of coding and you have a complete Ajax and CSS star rating system just like the pros use. Here’s a demo.


I could very well have skipped something so let me know if you have any problems.

Posted
July 6th, 3:17am 0 comments

WhatMyFriendsLike.com

In addition to MySpace Maps I also created WhatMyFriendsLike.com


It lets you create a ranked list of all the music, movies and books your MySpace friends like.


This one was also written entirely in Ruby on Rails sharing most of the code with MySpace Maps.

Filed under Projects myspace rails ruby
Posted