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.
Add comment July 26, 2009
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.
Add comment July 22, 2009
Installing Rails on Windows (3 years later)
**UPDATED 30 August**
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. Unfortunately, Windows dev environments are not that popular and there are some quirks and errors you will most probably encounter when installing, which will make the process much more frustrating than it should be! But fear not, I’m here to help you out.
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 latest Ruby one-click installer for Windows from here. Although Rails (currently latest v.2.3.3, released just today) seems to work with the latest Ruby (1.9.1), the one-click installer for it is currently called “technology preview”, so I’m not sure of it’s robustness (but if you want to get it – get it here). In any case, you will also be ok with the 1.8.6 release.
When installing you need to include the installation of Ruby gems (Ruby’s extensions installation package) and I recommend you to get rid of the default ScITE editor, you most probably won’t use it. 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.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]. If you see “Command not found”, make sure that you reload the command (close & open new) window after the install.
2. Update gems. 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.
4. Create your first project. Now, create a directory for your project. In Windows VISTA 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”… and it should output a bunch of lines “create app/controllers”, etc.
4.1. OPTIONAL – Fixing an issue in Ruby Gems (Windows) to prevent the “load” error.
Note: This got fixed in the last Ruby Gems release (1.3.5), so if you have this problem – just update your gems! Leaving it here for historical reasons only.
The latest version of Ruby Gems has a bug in it (thanks to Matt Jones for pointing that out), and it won’t work correctly if the path to your Ruby bin directory has spaces in it (like c:\Program Files). It actually gave an error to me – “error in ‘load’ no such file to load“.
If you have the same problem, fear not, here’s the fix, until the official update to Gems becomes available:
In your Ruby/bin directory (eg. c:\Program Files\Ruby\bin or c:\Ruby\bin) , find the “rails” file (that’s the file without an extension, not the rails.bat file), open it with a text editor (like notepad) and find the line that says Gem.bin_path(‘rails’, ‘rails’, version). Comment that line by putting “#” in front of it and on a new line type load ‘rails’
Save the file and go to your cmd window and type the “rails hello” command again. It should work now!
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 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“. 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 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 (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.16″ 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”.
7.1.1 Problems with installing sqlite3 gem (Failed to build gem native extension ERROR)
Note: This got fixed in August, so most probably you will not need to run this trick, but as it’s actually an error that you will often meet, I am leaving it here as well.
So, you might get into problems, as the latest version of sqlite3-ruby didn’t exist pre-compiled for the Win32 platform, so you might get an error “ERROR: Failed to build gem native extension“.
Fear not, run this command
gem list --remote --all sqlite3-ruby
and check which versions are available. Currently the 1.2.4 version doesn’t work for windows, so install the previous version by running this command: “gem install sqlite3-ruby -v 1.2.3“
It should now say something like “1 gem installed” and continue installing the documentation. The process is over fairly quickly.
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
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. If you get an error “no such file to load” again, follow the steps you did when editing the rails file, but now for the rake file (and the line you need to add is load ‘rake’ now.
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. Forgot to include it originally, but it’s 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). At this stage you might also want to install SVN, although I discourage you – in the last couple of months I found all the plugins I needed on Github.
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.
24 comments July 20, 2009
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.
Add comment July 20, 2009
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
#
rder - 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,
rder, :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)]) if options[:end_at]
type_and_context = "#{Vote.table_name}.voteable_type = #{quote_value(base_class.name)}"
conditions = [
type_and_context,
options[:conditions],
start_at,
end_at
]
conditions = conditions.compact.join(' AND ')
conditions = merge_conditions(conditions, scope[:conditions]) if scope
joins = ["LEFT OUTER JOIN #{Vote.table_name} ON #{Vote.table_name}.voteable_id = #{table_name}.#{primary_key}"]
joins << scope[:joins] if scope && scope[:joins]
at_least = sanitize_sql(["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)]) if options[:at_most]
having = [at_least, at_most].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
])
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
New acts_as_voter.rb:
# 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
])
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.
5 comments July 19, 2009
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
Add comment July 4, 2009