Installing the Theme support plugin with Rails 2.0
I have already written an article about the Theme Support Plugin and Rails 1.2. Now I will talk about using it with Ruby on Rails 2.0. We consequently need to update the routing system like we did for the Rails 1.2 version. But you will also encounter a problem within the extension of Actionview::Base. Indeed, it will crash when trying to render any template. I have consequently patched the ActionView::Base#render_file.
alias_method :theme_support_old_render_file, :render_file
# Overrides the default Base#render_file to allow theme-specific views
def render_file(template_path, use_full_path = true, local_assigns = {})
search_path = [
"../themes/#{controller.current_theme}/views",
"../../themes/#{controller.current_theme}/views",
"../../themes/#{controller.current_theme}",
"../../../themes/#{controller.current_theme}/views",
".",
".."
]
if use_full_path
search_path.each do |prefix|
theme_path = prefix +'/'+ template_path
begin
template_extension = pick_template_extension(theme_path)
# Prevent .rhtml (or any other template type) if force_liquid == true
if force_liquid? and
template_extension.to_s != 'liquid' and
prefix != '.'
raise ThemeError.new("Template '#{template_path}' must be a liquid document")
end
local_assigns['active_theme'] = get_current_theme(local_assigns)
rendered_template = theme_support_old_render_file(theme_path, use_full_path, local_assigns)
rescue ActionView::TemplateError => err
raise err
rescue ActionView::ActionViewError => err
next
rescue ThemeError => err
raise err
end
return rendered_template
end
raise ActionViewError, "No #{template_handler_preferences.to_sentence} template found for #{template_path} in #{view_paths.inspect}"
else
theme_support_old_render_file(template_path, use_full_path, local_assigns)
end
end
In fact the plugin was awaiting an exception during the call to pick_template_extension when the path was not valid. But now Rails 2.0 only raise this exception while trying to render the file. I consequently render the template into a variable which may raise an exception.
You may apply the patch manually or if you want you can download the full Theme Support plugin for Rails 2.0 using the following links:
Importing a CSV File from a Form
This article is a translation of the articles I wrote on my other blog during July 2007.
During my placement at Dexem I was mainly developing Voice Publisher: a tool used to create Interactive Voice Responses generated using the VoiceXML language. For this software (developed using Ruby on Rails), I have to import a CSV file into the database. As it is not very well documented, here is what I did to make it work.
To recover a file from a form you just need two things: declaring correctly the form, and a field which will permit you to recover the file:
<%= start_form_tag({:action => 'import_csv', :id => my_object}, :multipart => true) %>
<label for="csv_file">File to Upload</label>
<%= file_field_tag "csv_file" %>
<%= end_form_tag %>
Then in the action used to import the file:
def import_csv
begin
CSV::Reader.parse(params[:csv_file]).each do |row|
unless row[0].nil? or row[1].nil?
MyObject.create(:name => row[0], :value => row[1])
end
end
rescue
flash[:error] = "There was an error parsing your CSV file"
end
end
In order to check the file is specified when the form is submitted, you may want to add the following code to your action:
unless params[:csv][:file] == "" || params[:csv][:file].nil?
csv_file = params[:csv][:file].read
unless csv_file == ""
# read the CSV file from here
# but this time use csv_file instead of params[:csv][:file]
end
end
The first test...
unless params[:csv][:file] == "" || params[:csv][:file].nil?
...will check a file is specified for Firefox and Internet Explorer. The second test...
unless csv_file == ""
...is for Opera. If no file is given, Opera will still give us a StringIO object, which when read will give an empty string. On the contrary, Firefox and Internet Explorer will not give a StringIO if no file is specified, instead they will give an empty string.
That's it ! Now you know how to read a csv file from a form using Ruby on Rails. You may want to have a look at the fastercsv library which will provide, as the name says, faster reading of your csv files :)
A simple users system 2/2
Now that we have a user model and now that Access Control System is updated according to it, we can use it correctly in our controllers and view as usual. If you administrate a part of your application using Active Scaffold, you may edit your controllers as follows:
class CustomerAccountsController < ActionController
active_scaffold
end
class CustomersController < ActionController
access_control :DEFAULT => “admin | reseller”,
[:create, :update, :destroy] => “reseller”
active_scaffold :customers do |config|
config.columns = [:login, :email, :first_name, :last_name, :password, :password_confirmation, :customer_account]
list.columns.exclude :password, :password_confirmation
end
def authorize_create?
permit?(“reseller”)
end
def authorize_update?
permit?(“reseller”)
end
def authorize_delete?
permit?(“reseller”)
end
end
The authorize methods are used by Active Scaffold in views: the links to create, update or delete are not shown when these methods return false. That's it !
Installing the Theme support plugin with Rails 1.2
As usual, install from the subversion repository:
./script/plugin install http://mattmccray.com/svn/rails/plugins/theme_support
Then update the file which overrides your application's routes. It should be in /vendor/plugins/theme_support/lib/patches/routeset_ex.rb, replace its content with:
# Extends ActionController::Routing::RouteSet to automatically add the theme routes
class ActionController::Routing::RouteSet
alias_method :__draw, :draw
# Overrides the default RouteSet#draw to automatically
# include the routes needed by the ThemeController
def draw
clear!
map = Mapper.new(self)
create_theme_routes(map)
yield map
named_routes.install
end
# Creates the required routes for the ThemeController...
def create_theme_routes(map)
map.theme_images "/themes/:theme/images/*filename", :controller => 'theme', :action => 'images'
map.theme_stylesheets "/themes/:theme/stylesheets/*filename", :controller => 'theme', :action => 'stylesheets'
map.theme_javascript "/themes/:theme/javascript/*filename", :controller => 'theme', :action => 'javascript'
map.connect "/themes/*whatever", :controller => 'theme', :action => 'error'
end
end
Then generate a theme using:
./script/generate theme my_theme
You should at that point be able to get for example the following URL: http://localhost:3000/themes/my_theme/images/preview.png Your logs should say that the images action of the themes controller has been used to send the image file.
If that's the case, your installation is successful ! If it is not the case, do not hesitate to leave a comment here and we'll try to see what is the problem.
A simple users system 1/2
At Dexem we are currently working on a new application in which we have to take into account several different users with different permissions. For example we have an admin, several resellers and several customers.
To put that in place, we are gonna use:
- Acts as authenticated to create the users model and the authentication controller.
- Acl System to control access to controllers and actions.
- Active Scaffold to create easily an interface to manage those users.
The default behavior of Acl System is to look for a roles relation in the User model. In the other applications we have at Dexem, a user may have several roles and each role defines access to specific objects of the application.
But for this application, the permission system should be much simpler. That’s why I decided a user should only have one role which will subsequently define a set of permissions. So instead of having a User model plus a Role model and having links between them, I simplified the system to implement inheritance of the User class for each type of user.
class CreateUsers < ActiveRecord::Migration
def self.up
create_table "users", :force => true do |t|
# acts_as_authenticated attributes
t.column :login, :string
t.column :email, :string
t.column :crypted_password, :string, :limit => 40
t.column :salt, :string, :limit => 40
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :remember_token, :string
t.column :remember_token_expires_at, :datetime
# common attributes
t.column :first_name, :string
t.column :last_name, :string
t.column :type, :string
# reseller attributes
t.column :reseller_account_id, :integer
# customer attributes
t.column :customer_account_id, :integer
end
end
def self.down
drop_table "users"
end
end
In the User class defined by Acts as authenticated we add a role method:
def role
self.class.to_s.underscore
end
and in Acl System we modify the role checking method:
module Caboose
class RoleHandler < AccessHandler
def check(key, context)
context[:user].role == key.downcase
end
end
end
The users classes:
class Admin < User
end
class Reseller < User
belongs_to :reseller_account
end
class Customer < User
belongs_to :customer_account
end
With that code you have everything you need to make use of a basic users system. In the next part , I will explain and show how easy it is to integrate it with the use of Active Scaffold.
Obviously, any comments on the design and ways to improve it are welcome :)
Assigning a collection to has_many :through
This first article about Ruby on Rails is quite technical, it explains how to handle assignment of collections to a has_many :through relationship, as it is not the easiest kind of relationship to handle in Rails. I have used some references and made some improvements to the code so that it suits best my needs, but it may not be what you want, don't hesitate to post a comment to suggest some improvements or to ask for more :)
As has_many :through relationship is not handled using proxy collections like has_and_belongs_to_many relationships, we need to be a little tricky to make collection assignments. If you google a little to find a solution, you may find this subject on Ruby-Forum. This solution works well, but it will automatically save your data into the database. If like me you’re an “update_attributes addict”, you would like the affectation not to write into the database until the model is validated. So here is how I handle this assignment...
class Account < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :users, :through => :memberships
attr_reader :old_users
after_save :save_users
# “Reminds” the old users used
# and replaces them by the new ones
def users=(users)
@old_users = Array.new
self.users.each { |user| @old_users << user }
self.users.clear
users.each { |user| self.users << user }
end
private
def save_users
Membership.set_users_for_account self, self.users
memberships.reset
users.reset
end
end
class Users < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :accounts, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :account
belongs_to :user
def Membership.set_users_for_account(account, users)
old_users = account.old_users
# at account creation we would add the users twice otherwise
unless old_users.nil?
delete_from account, (old_users - users)
add_to account, (users - old_users)
end
end
private
def Membership.delete_from(account, users)
unless users.empty?
delete_all ['account_id = ? and user_id in (?)',
account.id, users.collect { |u| u.id }]
end
end
def Membership.add_to(account, users)
unless users.empty?
self.transaction do
users.each do | user|
create! :account => account, :user => user
end
end
end
end
end
This way, when you will make an update_attributes to your account giving a collection of users in parameters (and other account attributes eventually), the list of users associated to the account won’t be updated if the model is not valid.
I’m not the best Ruby on Rails programmer in the world, I would be glad to see your comments on how I could improve this code or even on how I could improve the way I handle collections assignment with has_many :through relationships.
This Article was written using Rails 1.1.6, this way of handling has_many :through relationship doesn't work with Rails 2.0 at creation of object
References
- Many-to-many Dance-off! by Josh Susser
- Why aren't join models proxy collections? by Josh Susser
- Collection assignment to a has_many :through on Ruby-Forum
Is it 2.0 ?
Some of my friends had the good idea to wonder “what is 2.0 ?”. And as curious as it seems to be, they don’t try to answer like most web developers would say: Ajax + Users driven content… they simply ask YOU: “is it 2.0 ?”
Find out more by visiting their very 2.0 website :)
Welcome to Web Driven
Finally I did it ! Here is my new blog. As you can see it is powered by Mephisto using the Ruby on Rails framework. I chose to host this blog on HostingRails.com
Well, as the title says, this blog is about web development. I will try to talk about many different things: mostly including Ruby and Ruby on Rails development, using Prototype and Script.aculo.us with Rails to improve user experience… and all the various things I may find interesting to talk about.
You may realize my english is not that good, but I’ll do my best to improve and make my writings pleasant to read :)