Installing the Theme support plugin with Rails 2.0

written by damien on January 9th, 2008 @ 12:13 PM

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

written by damien on December 6th, 2007 @ 04:04 PM

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

written by damien on December 6th, 2007 @ 11:43 AM

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

written by damien on November 27th, 2007 @ 02:58 PM

You may have wanted to install the Theme support plugin in your Ruby on Rails application. If you are using Rails 1.1, you won't have any problem, but the plugin as not been updated since Rails 1.2 is out. If you follow these steps it will work nicely:

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

written by damien on November 25th, 2007 @ 06:09 PM

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:

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

written by damien on October 28th, 2007 @ 06:45 PM

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

Is it 2.0 ?

written by damien on October 23rd, 2007 @ 10:10 PM

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

written by damien on October 22nd, 2007 @ 08:38 PM

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 :)

Options:

Size

Colors