Archive

Archive for the ‘rails’ Category

Install latest JRuby/Rails on Debian

March 17, 2013 2 comments

The Debian stable distribution (“squeeze”) includes JRuby 1.5.1, which is old.

This is how I installed the latest JRuby:

sudo apt-get install curl
curl -L https://get.rvm.io | bash -s stable
rvm install jruby
rvm use jruby
jruby —S gem install rails
Advertisements
Categories: bash, debian, rails

Rails: Generic drop-down value lists with management UI

February 24, 2013 1 comment

Recently, I had to implement the following feature in a Rails 3.2 application:

  • Certain model attributes have a finite list of possible values.
  • On the web interface users edit the attributes via drop-down lists.
  • Each value list is fully manageable (add, edit, delete) via an admin web interface.
  • Each value list is either alphabetically ordered by name or custom ordered.
  • Values can be “deactivated” by admins so that users can still see but not select them.
  • Some value lists (and corresponding drop-down lists) are “grouped” by category.
  • The groupings are also manageable via admin web interface.
  • The categories themselves are also implemented as manageable value lists.

Example for a grouped value list: Cities where a company is present, grouped by global corporate regions.

The implementation also had to satisfy these design goals:

  • Each value type is represented by a separate Active Record model and database table.
  • Minimize code duplication on the controller, model, view and helper levels.
  • All drop-down lists are generated by one generic form helper method.
  • The implementation must work well with the client_side_validation gem.
  • Use Oracle database with foreign key and uniqueness constraints.

Models

To reduce code duplication, I first tried Active Record bases class inheritance, with the common code in abstract base classes. But this did not work well with the uniqueness checks generated by the client_side_validations gem and so I used modules instead.

I keep all my modules in app/modules. To have them auto-loaded, I add this to config/application.rb:

config.autoload_paths += %W(#{config.root}/app/modules)

app/modules/

combo_common.rb

module ComboCommon
  extend ActiveSupport::Concern

  included do
    attr_accessible :active, :description, :name, :order

    default_scope order('"ORDER", name')

    validates :name, :presence => true, :uniqueness => true

    validates :order, :presence => true,
              :numericality => {:only_integer => true, :greater_than => 0}
  end

  def is_active
    active ? "Yes" : "No"
  end

  module ClassMethods
    def title
      name.demodulize.titleize
    end
  end
end

combo_with_category.rb

module ComboWithCategory
  extend ActiveSupport::Concern

  included do
    attr_accessible :category_id, :category
    validates :category_id, :presence => true
  end

end

app/models/

Example : cities grouped by region

combo/city.rb

class Combo::City < ActiveRecord::Base
  include ComboCommon, ComboWithCategory

  def self.category_type
    Combo::Region
  end

  belongs_to :category, :class_name => category_type.name

end

combo/region.rb

class Combo::Region < ActiveRecord::Base
  include ComboCommon

  has_many :category_members, class_name: Combo::Location.name,
           foreign_key: "category_id", readonly: true
end

app/controllers/

combo/controller_base.rb

class Combo::ControllerBase < ApplicationController

  before_filter :ensure_admin

  def init_type(type)
    @type = type
  end

  def index
    @items = @type.all
    render "combo/index"
  end

  def new
    @item = @type.new(active: true, order: 100)
    render :template => "combo/new", :layout => "popup"
  end

  def edit
    @item = @type.find(params[:id])
    render :template => "combo/edit", :layout => "popup"
  end

  def create
    @item = @type.new(params[type_symbol])
    if @item.save
      redirect_to_index "was successfully created."
    else
      render "combo/new"
    end
  end

  def update
    @item = @type.find(params[:id])
    if @item.update_attributes(params[type_symbol])
      redirect_to_index "was successfully updated."
    else
      redirect_to_index "was NOT successfully updated."
    end
  end

  def destroy
    item = @type.find(params[:id])
    begin
      item.destroy if item
      redirect_to_index("was successfully deleted.")
    rescue ActiveRecord::StatementInvalid => e
      msg = e.message
      # See http://docs.oracle.com/cd/E11882_01/server.112/e17766/e2100.htm
      # for reference documentation of Oracle error codes
      if msg.include?("ORA-02292") and msg.include?("child record found")
        redirect_to_index("could not be deleted because it is still in use! ")
      else
        raise
      end
    end
  end

  private

  def type_symbol
    @type.name.delete("::").underscore.to_sym
  end

  def redirect_to_index(message)
    redirect_to polymorphic_path(@type), :notice => @type.title + " " + message
  end

end

Example : cities grouped by region

combo/cities_controller.rb

class Combo::CitiesController < Combo::ControllerBase

  before_filter { init_type(Combo::City) }

end

combo/regions_controller.rb

class Combo::RegionsController < Combo::ControllerBase

  before_filter { init_type(Combo::Region) }

end

app/views

combo/index.html.erb

<div class="heading clearfix">
    <h2><%= @type.title.pluralize %></h2>

    <div class="actions">
        <%= link_to 'New Entry', new_polymorphic_path(@type),
                    :class => "button colorbox" %>
    </div>
</div>

<table id="index" class="display">
    <thead>
    <tr>
        <th>Name</th>
        <th>Description</th>
        <% if @type.respond_to? :category_type %>
            <th>Category</th>
        <% end %>
        <th>Order</th>
        <th>Active</th>
        <th>Created</th>
        <th>Modified</th>
        <th class="no-search no-sort"></th>
        <th class="no-search no-sort"></th>
    </tr>
    </thead>

    <tbody>
    <% @items.each do |item| %>
        <tr>
            <td><%= item.name %></td>
            <td><%= item.description %></td>
            <% if item.respond_to? :category %>
                <td><%= item.category.name %>
            <% end %>
            <td><%= item.order %></td>
            <td><%= item.is_active %></td>
            <td><%= item.created_at.to_s(:date) %></td>
            <td><%= item.updated_at.to_s(:date) %></td>
            <td><%= link_to 'Edit', edit_polymorphic_path(item),
                            :class => "colorbox" %></td>
            <td><%= link_to 'Delete', item,
                            :confirm => 'Are you sure?',
                            :method => :delete %>
            </td>
        </tr>
    <% end %>
    </tbody>

</table>

combo/edit.html.erb

<h3>Editing <%= @type.title %></h3>

<%= render "combo/form" %>

combo/new.html.erb

<h3>New <%= @type.title %></h3>

<%= render "combo/form" %>

combo/_form.html.erb

<%= form_for @item, :validate => true do |f| %>
    <% if @item.errors.any? %>
        <div id="error_explanation">
            <h2><%= pluralize(@item.errors.count, "error") %>
                prohibited this value list item from being saved:</h2>
            <ul>
                <% @item.errors.full_messages.each do |msg| %>
                    <li><%= msg %></li>
                <% end %>
            </ul>
        </div>
    <% end %>

    <table class="no-borders">
        <tr>
            <td><%= label_for f, :name %></td>
            <td><%= f.text_field :name, :class => "required" %></td>
        </tr>
        <tr>
            <td><%= label_for f, :description %></td>
            <td><%= f.text_field :description %></td>
        </tr>
        <% if @type.respond_to? :category_type %>
            <tr>
                <td><%= label_for f, :category_id, "Category" %></td>
                <td><%= combo(f, :category_id, @type.category_type) %>
                </td>
            </tr>
        <% end %>
        <tr>
            <td><%= label_for f, :order %></td>
            <td><%= f.number_field :order %></td>
        </tr>
        <tr>
            <td><%= label_for f, :active %></td>
            <td><%= f.check_box :active %></td>
        </tr>
    </table>

    <br/>

    <div class="actions">
        <%= f.submit %>
        <%= link_to 'Cancel', polymorphic_path(@type), :class => "button" %>
    </div>
<% end %>

helper methods

app/helper/application_helper.rb

module ApplicationHelper
  include ComboUtil
end

app/modules/combo_util.rb

module ComboUtil

  def combo_link(type)
    link_to simple_plural(type).titleize, polymorphic_path(type)
  end

  def combo(form, attr, type, options = {}, html_options = {})
    form.select(attr, combo_options(type, form.object.send(attr)), options,
                html_options)
  end

  def combo_filter(type)
    render 'combo_filter', type: type
  end

  private

  def simple_plural(type)
    type.name.demodulize.pluralize
  end

  def combo_options(type, id)
    if type.respond_to? :category_type
      grouped_options_for_select(all_grouped_options(type),
                                 disabled: inactive_options(type),
                                 selected: id)
    else
      options_for_select(all_options(type),
                         disabled: inactive_options(type),
                         selected: id)
    end
  end

  def all_grouped_options(type)
    type.category_type.all.map do |cat|
      [cat.name, cat.category_members.map { |m| [m.name, m.id] }]
    end
  end

  def all_options(type)
    type.all.map { |m| [m.name, m.id] }
  end

  def inactive_options(type)
    type.where(active: false).map { |m| m.id }
  end

end
Categories: rails Tags: