Archive for July, 2009

Polymorphic associations and acts_as_xxx

There are many plugins that allow you to use Rails’ powerful polymorphic relationship. Basically, polymorphic relationship allows you to create for example a Comments module and link to it from any of your classes by using “acts_as_commentable” declaration. How Comment class will not which object it gets linked to? It simply stores 2 fields in the database – xxx_id and xxx_type.

And what if you want to get an object (for example a post) on which somebody commented? It’s actually quite simple. If @comment stores your comment, then @comment.commentable will be the object on which the comment was made. This principle will work with any other acts_as plugin. For acts_as_voteable, you will need to access vote.voteable, etc.

Advertisements

July 26, 2009 at 3:00 pm Leave a comment

Rails books in free access

I don’t know what’s the deal, but Google Books has several Rails / Ruby books available in a very generous preview mode (80-100 pages for free). For those of you, who are just starting out and want to understand if Ruby is the way to go, check out some books here and here, for example. First 3-4 chapters are fully available at the moment.

July 22, 2009 at 11:52 pm Leave a comment

Installing Rails on Windows (3 years later)

**UPDATED June 2010 – just installed on Windows 7 ;)**
**UPDATED October 2010 – now also added Rails3 commands**

So, a lot has changed from the time when I first wrote a short beginner tutorial for installing Ruby on Rails. This is the reincarnation (essentially I needed to write it up from scratch) of an installation process for Rails. There were quirks in Windows installation the last 3 years I was installing Rails, but this time it went really smooth and quick, so I’ve deleted loads of workarounds and the tutorial is now really simple.

Most of the stuff you will do from the command line, to get the command line in windows, go to start menu/run and type “cmd”. You will also need to have an Internet connection during the installation as most stuff is getting updated via the Internet.

Please note that if you see a command in quotes, “like this”, you should disregard the quotes, ie. type the command without them.

1. Ruby – Download and install Ruby installer here. It’s very easy to install. When installing, click checkboxes that ask you to associate Ruby with .rb files and add Ruby to the PATH. Btw, do remember where you installed Ruby, it will come in handy later.

Once installed, from the command line type

ruby -v

and you should see something like this: ruby 1.9.1p378 (2010-01-10 revision 26273) [i386-mingw32]. If you see “Command not found”, make sure that you reload the command (close & open new) window after the install.

2. Update gems. Just in case, from command line run

gem update --system

(that’s two “-“!). It will take 2-3 mins to update.

3. Rails. Simply type in cmd: gem install rails
It will not show any progress for some time, but if your HDD is working, it’s all fine. In my case it installed rails and other related gems (in total 8 gems). After that it installs ri documentation / RDoc documentation, and it takes quite some time (much longer than gems themselves), so be patient. By the way, chances are you will NOT be using local documentation for Rails, as it’s all on the web anyway, so you can run this command instead: gem install rails –no-rdoc –no-ri

4. Create your first project. Now, create a directory for your project. In Windows VISTA (and 7, I think) I do NOT recommend creating it in your root folder or Program Files, as VISTA has some pain-in-the-ass safety mechanism that will not allow many programs access the files you put there. The best way is to create a folder in your “My Documents” folder, you can call it Rails. In your cmd type “cd path/to/your rails folder” (or you can always use an UI-shell like Windows Commander).

Type “rails hello” (in Rails 3 it should be “Rails new hello”) and it should output a bunch of lines “create app/controllers”, etc.

5. Run webserver. Good thing is that Rails comes with its own web server, so no need to set up apache. The default web server is WEBrick, but you can at any point upgrade to Mongrel, which is a production-level server. For now – don’t worry! Just remember, that in order to restart the web server, you need to press Ctrl-Break and retype the “ruby script/server” command (in Rails 3 you now need to type “rails server” instead!).

In step 4 you have created the project called hello. You will see that within your chosen directory for rails projects a directory “hello” was created and a bunch of subdirectories as well (like “app”, “config”, “db”, etc.). Go into the “hello” directory (still in your cmd) and run the webserver command “ruby script/server” (or in Rails3 use “rails server” . You should see something like “Booting WEBrick”.

6. Now, test the installation in your browser. Use your favorite browser and type in the address: http://127.0.0.1:3000

You should see a standard Rails splash screen, it will say “Welcome aboard”. Rails is installed!

7. Finally, install the database. For your dev box sqlite3 is the best choice, it’s lightweight (like, less than 1mb!) and easy to install. Go to the project’s site and get 2 zip archives precompiled binaries for Windows section. You need to download sqlite3 command line program (here’s the current version AT THE TIME OF WRITING) AND sqlite dlls (you do not need TCL bindings) – here’s the current version AT THE TIME OF WRITING. Download and extract the files to your Ruby bin directory (!).

Test that it works correctly – run sqlite3 in the command line. It should say something like “SQLite version 3.6.23” and show you a prompt (you can type SQL commands there, but don’t do it now). Now just type “.exit” or press Ctrl-Break to exit.

7.1. Now, you need to install the sqlite3 gem. Simply run “gem install sqlite3-ruby”.

8. Write code. The installation is now complete, but why stop where all the fun should begin? Let’s create our first program in Rails now!

The good thing about sqlite3 is that it doesn’t require passwords or setting up of your database, so you can start coding almost straight away. Now let’s try out the (in)famous Rails scaffold script. Run the following command in your “hello” directory:

ruby script/generate scaffold post title:string description:text

Or in Rails3:

rails generate scaffold post title:string description:text

This command creates a Post scaffold, a model/controller/view ready to be updated. Note – from now on majority of commands that you run, you should run in cmd from within your “hello” project root directory. If you go to sub-directory or a directory above your project’s root, commands will not work.

The post model will have 2 fields – a title which is a single line of text and a description, which is multi-line text. Note, that it’s a Rails notation that models should be written in singular.

Now, in your browser go to the http://127.0.0.1:3000/posts. It should give you an error ActiverRecord::StatementInvalid in PostsController#index.

9. Run the database migration. This is because after creating the scaffold, the database is not automatically updated with the new blog model, you need to do it manually by running a rake db:migrate command. Do it now.

A note on using SQL tools. In my previous post on working with Rails on Windows I’ve suggested to use MySQL Query Designer or phpadmin or something similar for creating tables. Now, with the updated tools of Rails 2.3.x you should NOT work with the database at such low level, you should work with migrations and migrations only! Migrations are easy to understand, and once you understand them, you will never want to create tables in any other ways. It also gives you other benefits, like ability to migrate up or down, etc.

Ok, so a successful rake db:migrate command will show something like CreatePosts: migrating, create_table(:posts) and show time it took to run the migration.

Before going to the next step, kill your command line window that runs the web server and restart it again by typing “ruby script/server” (in Rails 3 “rails server”). You will need to do this every time you install a new gem, as the gems are loaded at the start. If you don’t restart your server, you will see an error (We’re sorry, but something went wrong error) on the next step. These are errors that are difficult to catch, even by looking at the logs (online logs are shown in the cmd window of your web server). So, don’t forget to try and reload the server if you’re lost.

10. Show me the money… Ok, lets check the browser again – you should now see the page saying “Listing posts” and you can add new posts, edit them and delete. If you want to play around with the code, you will find the code in the hello/app directory.

11. Git. Definitely a requirement if you want to use Rails seriously. Git is a version-control system that has become a de facto standard for rails developers. Most of the latest plugins are hosted on Git. In order to use Git hosted plugins you will need to install a Git client. It’s easily installable and is available from here (you need to download a full non-portable installer, don’t install the installer that says “download if you want to hack on Git”). When installing Git, you can select different “types” of installation, select the one that says “Bash only”.

12. Learning Ruby. Now, it’s probably time to learn Ruby and Ruby on Rails. I’ve learned Ruby and Rails from 3 great books that I link to on my blog. If you are looking for free Rails web resources, I posted about them here.

13. Next, you should think about your development environment You should select the best tools for editing your rails files, running scripts, etc. straight away. You can check my dev environment here.

14. Create you first project. Now that the technical part is over, you should be dying to do something useful in Rails. As a very basic example you can follow this 5 minute Rails tutorial.

15. Plugins Finally, if you are wondering which plugins / extensions you should use, I wrote about Rails plugins here.

July 20, 2009 at 11:57 pm 79 comments

Great plugin list

Coming from PHP background, I actually find that there are not as many plugins / extensions on Rails as I am used to. One reason would be of course that RoR makes writing your own code much easier, the other – that not enough time yet passed and not every building block is written. One thing that I found a bit annoying is that you can find a plugin on Agile website (more or less official directory of RoR plugins) that is not the best one to use. You will download it, install and find out 2-3 hours later that it doesn’t do what’s needed (and usually was last updated couple of years ago). 1-2 weeks later still, after doing some modifications, you will actually come across a better plugin.

As a rule of thumb, if the plugin is not on github and was not updated in the last 2-3 months, think twice before installing it.

Another thing to do is to check out the popularity of the plugin on this great site – Ruby-toolbox.com. It does not list all plugin types, but most of the time based on my current experience it suggest the best plugin to use. And you actually might find out that somebody already made the hard work building the functionality you’re now starting to create.

July 20, 2009 at 7:26 pm 12 comments

Voting plugin

I needed to create a voting functionality for my project and first used act_as_voteable plugin. However, there is a newer and better alternative (which is still supported as well!), called vote_fu, git page is here from Peter Jackson.

The link above gives all the required info on how to install use vote_fu. However, the plugin has a problem – it puts votes as boolean (true/false) in the model, and this does not allow you to use it to show the most popular items sorted by total number of positive votes minus total number of negative votes, it can only count positive votes.

What’s needed is to change the plugin to insert integers as vote value (change column vote to integer). This will allow you to do many things, like quickly calculating the sum of votes and sort results based on that. It can also give you easy functionality for allowing multi-point voting system (anything less than 0 becomes a negative post, anything over – positive).

You can use the modified migration below, line 4 is changed from the original (you can just save it to your migration directory and run it, it will overrun the existing votes table – HOWEVER all historical data will be lost!):

class VoteFuChange < ActiveRecord::Migration
  def self.up
    create_table :votes, :force => true do |t|
      t.integer    :vote, :default => -1
      t.references :voteable, :polymorphic => true, :null => false
      t.references :voter,    :polymorphic => true
      t.timestamps      
    end

    add_index :votes, ["voter_id", "voter_type"],       :name => "fk_voters"
    add_index :votes, ["voteable_id", "voteable_type"], :name => "fk_voteables"

    # If you want to enfore "One Person, One Vote" rules in the database, uncomment the index below
    #add_index :votes, ["voter_id", "voter_type", "voteable_id", "voteable_type"], :unique => true, :name => "uniq_one_vote_only"
  end

  def self.down
    drop_table :votes
  end

end

You also need to do some changes to the plugin itself (in directory your rails project/vendor/plugins/lib). What’s needed is to change the methods for calculating the totals. Instead of using count(*), you should use the sum of the vote fields of the required records. As you changed the type of the vote from boolean to integer, you need to also do minor corrections to methods for inserting new votes (instead of true it will now place +1, instead of false -1).

New acts_as_voteable.rb

# ActsAsVoteable
module Juixe
  module Acts #:nodoc:
    module Voteable #:nodoc:

      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def acts_as_voteable
          has_many :votes, :as => :voteable, :dependent => :nullify

          include Juixe::Acts::Voteable::InstanceMethods
          extend  Juixe::Acts::Voteable::SingletonMethods
        end        
      end
      
      # This module contains class methods
      module SingletonMethods
        
        # Calculate the vote counts for all voteables of my type.
        def tally(options = {})
          find(:all, options_for_tally(options.merge({:order =>"count DESC" })))
        end

        # 
        # Options:
        #  :start_at    - Restrict the votes to those created after a certain time
        #  :end_at      - Restrict the votes to those created before a certain time
        #  :conditions  - A piece of SQL conditions to add to the query
        #  :limit       - The maximum number of voteables to return
        #  :order       - A piece of SQL to order by. Eg 'votes.count desc' or 'voteable.created_at desc'
        #  :at_least    - Item must have at least X votes
        #  :at_most     - Item may not have more than X votes
        def options_for_tally (options = {})
            options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit

            scope = scope(:find)
            start_at = sanitize_sql(["#{Vote.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
            end_at = sanitize_sql(["#{Vote.table_name}.created_at <= ?", options.delete(:end_at)&#93;) if options&#91;:end_at&#93;

            type_and_context = "#{Vote.table_name}.voteable_type = #{quote_value(base_class.name)}"

            conditions = &#91;
              type_and_context,
              options&#91;:conditions&#93;,
              start_at,
              end_at
            &#93;

            conditions = conditions.compact.join(' AND ')
            conditions = merge_conditions(conditions, scope&#91;:conditions&#93;) if scope

            joins = &#91;"LEFT OUTER JOIN #{Vote.table_name} ON #{Vote.table_name}.voteable_id = #{table_name}.#{primary_key}"&#93;
            joins << scope&#91;:joins&#93; if scope && scope&#91;:joins&#93;
            at_least  = sanitize_sql(&#91;"COUNT(#{Vote.table_name}.id) >= ?", options.delete(:at_least)]) if options[:at_least]
            at_most   = sanitize_sql(["COUNT(#{Vote.table_name}.id) <= ?", options.delete(:at_most)&#93;) if options&#91;:at_most&#93;
            having    = &#91;at_least, at_most&#93;.compact.join(' AND ')
            group_by  = "#{Vote.table_name}.voteable_id"
            group_by << " AND #{having}" unless having.blank?

            { :select     => "#{table_name}.*, SUM(#{Vote.table_name}.vote) AS count", 
              :joins      => joins.join(" "),
              :conditions => conditions,
              :group      => group_by
            }.update(options)          
        end
     end
      # This module contains instance methods
      module InstanceMethods
        def votes_for
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ? AND vote > 0",
            id, self.class.name
          ])
        end
        
        def votes_against
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ? AND vote < 0",
            id, self.class.name
          &#93;)
        end

        def votes_total
          Vote.sum(:vote, :conditions => [
            "voteable_id = ? AND voteable_type = ?",
            id, self.class.name
          ])
        end
        
        # Same as voteable.votes.size
        def votes_count
          self.votes.size
        end
        
        def voters_who_voted
          voters = []
          self.votes.each { |v|
            voters << v.voter
          }
          voters
        end
        
        def voted_by?(voter)
          rtn = false
          if voter
            self.votes.each { |v|
              rtn = true if (voter.id == v.voter_id && voter.class.name == v.voter_type)
            }
          end
          rtn
        end

        def vote_by?(voter)
          rtn = 0
          if voter
            self.votes.each { |v|
              rtn = v.vote if (voter.id == v.voter_id && voter.class.name == v.voter_type)
            }
          end
          rtn
        end
        
        
      end
    end
  end
end
&#91;/sourcecode&#93;

New acts_as_voter.rb:

&#91;sourcecode language="ruby"&#93;
# ActsAsVoter
module PeteOnRails
  module Acts #:nodoc:
    module Voter #:nodoc:

      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def acts_as_voter
          has_many :votes, :as => :voter, :dependent => :nullify  # If a voting entity is deleted, keep the votes. 
          include PeteOnRails::Acts::Voter::InstanceMethods
          extend  PeteOnRails::Acts::Voter::SingletonMethods
        end
      end
      
      # This module contains class methods
      module SingletonMethods
      end
      
      # This module contains instance methods
      module InstanceMethods
        
        # Usage user.vote_count(true)  # All +1 votes
        #       user.vote_count(false) # All -1 votes
        #       user.vote_count()      # All votes
        
        def vote_count(for_or_against = "all")
          where = (for_or_against == "all") ? 
            ["voter_id = ? AND voter_type = ?", id, self.class.name ] : 
            ["voter_id = ? AND voter_type = ? AND vote = ?", id, self.class.name, for_or_against ]
                        
          Vote.count(:all, :conditions => where)

        end
                
        def voted_for?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND vote > 0 AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   ])
         end

         def voted_against?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND vote < 0 AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   &#93;)
         end

         def voted_on?(voteable)
           0 < Vote.count(:all, :conditions => [
                   "voter_id = ? AND voter_type = ? AND voteable_id = ? AND voteable_type = ?",
                   self.id, self.class.name, voteable.id, voteable.class.name
                   ])
         end
                
        def vote_for(voteable)
          self.vote(voteable, 1)
        end
    
        def vote_against(voteable)
          self.vote(voteable, -1)
        end

        def vote(voteable, vote)
          vote = Vote.new(:vote => vote, :voteable => voteable, :voter => self)
          vote.save
        end

      end
    end
  end
end

Now, just restart the server and you should use the tally method as you used before (btw, order attribute doesn’t work in the version I used, so you need to modify the code further, if your order is not the default one). Also, you have a new method votes_total, which is equal to votes_for – votes_against, but uses a single DB query.

One final note – the tally will not show any objects that do not have at least one vote, even though the left join is used. I need to research it further, but one workaround is to provide a system vote automatically on the creation of the object.

July 19, 2009 at 10:32 am 8 comments

Create an object of a type (class) stored in a string

So, you have a class of object stored in a string, and you want to create an object of that class. You cannot assign a type of an object directly (by say @myvar.class = xxx), and the class conversions are limited to the basic built-in classes like to_s, etc. But you can use this trick:

@classtype = "Artist" 
@myvar = Kernel.const_get(@classtype).find(@id)

Another (quite similar) option is to use the Rails constantize method (that in turn uses const_get). This is how for example acts_as_commentable plugin can provide a method to get the commentable object back by passing the type and id of it:

  def self.find_commentable(commentable_str, commentable_id)
    commentable_str.constantize.find(commentable_id)
  end

July 4, 2009 at 10:15 pm Leave a comment


Recent posts

Starting to learn Rails?

Kindle

Get Kindle - the best e-book reader, that I personally use, and the only one that you can read on the beach - very useful: Kindle Wireless Reading Device (6" Display, Global Wireless, Latest Generation)