Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Extract dynamic scaffolding into a plugin. Closes #7700.

  • Loading branch information...
commit e1d6dfe0f7b0179e53a96aba88c0ce40882a2d53 0 parents
Jeremy Kemper jeremy authored
20 MIT-LICENSE
... ... @@ -0,0 +1,20 @@
  1 +Copyright (c) 2004-2007 David Heinemeier Hansson
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining
  4 +a copy of this software and associated documentation files (the
  5 +"Software"), to deal in the Software without restriction, including
  6 +without limitation the rights to use, copy, modify, merge, publish,
  7 +distribute, sublicense, and/or sell copies of the Software, and to
  8 +permit persons to whom the Software is furnished to do so, subject to
  9 +the following conditions:
  10 +
  11 +The above copyright notice and this permission notice shall be
  12 +included in all copies or substantial portions of the Software.
  13 +
  14 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
74 README
... ... @@ -0,0 +1,74 @@
  1 +Scaffolding
  2 +===========
  3 +
  4 +Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come with both controller logic and default templates that through introspection already know which fields to display and which input types to use. Example:
  5 +
  6 +The render_scaffold method will first check to see if you've made your own template (like "weblog/show.erb" for the show action) and if not, then render the generic template for that action. This gives you the possibility of using the scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template and one action at a time while relying on the rest of the scaffolded templates and actions.
  7 +
  8 +Example
  9 +=======
  10 +
  11 + class WeblogController < ActionController::Base
  12 + scaffold :entry
  13 + end
  14 +
  15 +This tiny piece of code will add all of the following methods to the controller:
  16 +
  17 + class WeblogController < ActionController::Base
  18 + verify :method => :post, :only => [ :destroy, :create, :update ],
  19 + :redirect_to => { :action => :list }
  20 +
  21 + def index
  22 + list
  23 + end
  24 +
  25 + def list
  26 + @entries = Entry.find(:all)
  27 + render_scaffold "list"
  28 + end
  29 +
  30 + def show
  31 + @entry = Entry.find(params[:id])
  32 + render_scaffold
  33 + end
  34 +
  35 + def destroy
  36 + Entry.find(params[:id]).destroy
  37 + redirect_to :action => "list"
  38 + end
  39 +
  40 + def new
  41 + @entry = Entry.new
  42 + render_scaffold
  43 + end
  44 +
  45 + def create
  46 + @entry = Entry.new(params[:entry])
  47 + if @entry.save
  48 + flash[:notice] = "Entry was successfully created"
  49 + redirect_to :action => "list"
  50 + else
  51 + render_scaffold('new')
  52 + end
  53 + end
  54 +
  55 + def edit
  56 + @entry = Entry.find(params[:id])
  57 + render_scaffold
  58 + end
  59 +
  60 + def update
  61 + @entry = Entry.find(params[:id])
  62 + @entry.attributes = params[:entry]
  63 +
  64 + if @entry.save
  65 + flash[:notice] = "Entry was successfully updated"
  66 + redirect_to :action => "show", :id => @entry
  67 + else
  68 + render_scaffold('edit')
  69 + end
  70 + end
  71 + end
  72 +
  73 +
  74 +Copyright (c) 2004-2007 David Heinemeier Hansson, released under the MIT license
22 Rakefile
... ... @@ -0,0 +1,22 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/rdoctask'
  4 +
  5 +desc 'Default: run unit tests.'
  6 +task :default => :test
  7 +
  8 +desc 'Test the scaffolding plugin.'
  9 +Rake::TestTask.new(:test) do |t|
  10 + t.libs << 'lib'
  11 + t.pattern = 'test/**/*_test.rb'
  12 + t.verbose = true
  13 +end
  14 +
  15 +desc 'Generate documentation for the scaffolding plugin.'
  16 +Rake::RDocTask.new(:rdoc) do |rdoc|
  17 + rdoc.rdoc_dir = 'rdoc'
  18 + rdoc.title = 'Scaffolding'
  19 + rdoc.options << '--line-numbers' << '--inline-source'
  20 + rdoc.rdoc_files.include('README')
  21 + rdoc.rdoc_files.include('lib/**/*.rb')
  22 +end
1  init.rb
... ... @@ -0,0 +1 @@
  1 +ActionController::Base.send :include, Scaffolding
187 lib/scaffolding.rb
... ... @@ -0,0 +1,187 @@
  1 +module Scaffolding # :nodoc:
  2 + def self.included(base)
  3 + base.extend(ClassMethods)
  4 + end
  5 +
  6 + # Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
  7 + # for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
  8 + # with both controller logic and default templates that through introspection already know which fields to display
  9 + # and which input types to use. Example:
  10 + #
  11 + # class WeblogController < ActionController::Base
  12 + # scaffold :entry
  13 + # end
  14 + #
  15 + # This tiny piece of code will add all of the following methods to the controller:
  16 + #
  17 + # class WeblogController < ActionController::Base
  18 + # verify :method => :post, :only => [ :destroy, :create, :update ],
  19 + # :redirect_to => { :action => :list }
  20 + #
  21 + # def index
  22 + # list
  23 + # end
  24 + #
  25 + # def list
  26 + # @entries = Entry.find(:all)
  27 + # render_scaffold "list"
  28 + # end
  29 + #
  30 + # def show
  31 + # @entry = Entry.find(params[:id])
  32 + # render_scaffold
  33 + # end
  34 + #
  35 + # def destroy
  36 + # Entry.find(params[:id]).destroy
  37 + # redirect_to :action => "list"
  38 + # end
  39 + #
  40 + # def new
  41 + # @entry = Entry.new
  42 + # render_scaffold
  43 + # end
  44 + #
  45 + # def create
  46 + # @entry = Entry.new(params[:entry])
  47 + # if @entry.save
  48 + # flash[:notice] = "Entry was successfully created"
  49 + # redirect_to :action => "list"
  50 + # else
  51 + # render_scaffold('new')
  52 + # end
  53 + # end
  54 + #
  55 + # def edit
  56 + # @entry = Entry.find(params[:id])
  57 + # render_scaffold
  58 + # end
  59 + #
  60 + # def update
  61 + # @entry = Entry.find(params[:id])
  62 + # @entry.attributes = params[:entry]
  63 + #
  64 + # if @entry.save
  65 + # flash[:notice] = "Entry was successfully updated"
  66 + # redirect_to :action => "show", :id => @entry
  67 + # else
  68 + # render_scaffold('edit')
  69 + # end
  70 + # end
  71 + # end
  72 + #
  73 + # The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.erb" for
  74 + # the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
  75 + # scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
  76 + # and one action at a time while relying on the rest of the scaffolded templates and actions.
  77 + module ClassMethods
  78 + # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
  79 + # one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
  80 + # and @post/@posts for the instance variables.
  81 + #
  82 + # It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
  83 + # make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
  84 + # instead of just list, show, and post. If suffix is used, then no index method is added.
  85 + def scaffold(model_id, options = {})
  86 + options.assert_valid_keys(:class_name, :suffix)
  87 +
  88 + singular_name = model_id.to_s
  89 + class_name = options[:class_name] || singular_name.camelize
  90 + plural_name = singular_name.pluralize
  91 + suffix = options[:suffix] ? "_#{singular_name}" : ""
  92 +
  93 + unless options[:suffix]
  94 + module_eval <<-"end_eval", __FILE__, __LINE__
  95 + def index
  96 + list
  97 + end
  98 + end_eval
  99 + end
  100 +
  101 + module_eval <<-"end_eval", __FILE__, __LINE__
  102 +
  103 + verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
  104 + :redirect_to => { :action => :list#{suffix} }
  105 +
  106 +
  107 + def list#{suffix}
  108 + @#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
  109 + render#{suffix}_scaffold "list#{suffix}"
  110 + end
  111 +
  112 + def show#{suffix}
  113 + @#{singular_name} = #{class_name}.find(params[:id])
  114 + render#{suffix}_scaffold
  115 + end
  116 +
  117 + def destroy#{suffix}
  118 + #{class_name}.find(params[:id]).destroy
  119 + redirect_to :action => "list#{suffix}"
  120 + end
  121 +
  122 + def new#{suffix}
  123 + @#{singular_name} = #{class_name}.new
  124 + render#{suffix}_scaffold
  125 + end
  126 +
  127 + def create#{suffix}
  128 + @#{singular_name} = #{class_name}.new(params[:#{singular_name}])
  129 + if @#{singular_name}.save
  130 + flash[:notice] = "#{class_name} was successfully created"
  131 + redirect_to :action => "list#{suffix}"
  132 + else
  133 + render#{suffix}_scaffold('new')
  134 + end
  135 + end
  136 +
  137 + def edit#{suffix}
  138 + @#{singular_name} = #{class_name}.find(params[:id])
  139 + render#{suffix}_scaffold
  140 + end
  141 +
  142 + def update#{suffix}
  143 + @#{singular_name} = #{class_name}.find(params[:id])
  144 + @#{singular_name}.attributes = params[:#{singular_name}]
  145 +
  146 + if @#{singular_name}.save
  147 + flash[:notice] = "#{class_name} was successfully updated"
  148 + redirect_to :action => "show#{suffix}", :id => @#{singular_name}
  149 + else
  150 + render#{suffix}_scaffold('edit')
  151 + end
  152 + end
  153 +
  154 + private
  155 + def render#{suffix}_scaffold(action=nil)
  156 + action ||= caller_method_name(caller)
  157 + # logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
  158 +
  159 + if template_exists?("\#{self.class.controller_path}/\#{action}")
  160 + render :action => action
  161 + else
  162 + @scaffold_class = #{class_name}
  163 + @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
  164 + @scaffold_suffix = "#{suffix}"
  165 + add_instance_variables_to_assigns
  166 +
  167 + @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
  168 +
  169 + if !active_layout.nil?
  170 + render :file => active_layout, :use_full_path => true
  171 + else
  172 + render :file => scaffold_path('layout')
  173 + end
  174 + end
  175 + end
  176 +
  177 + def scaffold_path(template_name)
  178 + File.dirname(__FILE__) + "/templates/" + template_name + ".erb"
  179 + end
  180 +
  181 + def caller_method_name(caller)
  182 + caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
  183 + end
  184 + end_eval
  185 + end
  186 + end
  187 +end
7 lib/templates/edit.erb
... ... @@ -0,0 +1,7 @@
  1 +<h1>Editing <%= @scaffold_singular_name %></h1>
  2 +
  3 +<%= error_messages_for(@scaffold_singular_name) %>
  4 +<%= form(@scaffold_singular_name, :action => "update#{@scaffold_suffix}") %>
  5 +
  6 +<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
  7 +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
69 lib/templates/layout.erb
... ... @@ -0,0 +1,69 @@
  1 +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  2 + "http://www.w3.org/TR/html4/loose.dtd">
  3 +<html>
  4 +<head>
  5 + <title>Scaffolding</title>
  6 + <style>
  7 + body { background-color: #fff; color: #333; }
  8 +
  9 + body, p, ol, ul, td {
  10 + font-family: verdana, arial, helvetica, sans-serif;
  11 + font-size: 13px;
  12 + line-height: 18px;
  13 + }
  14 +
  15 + pre {
  16 + background-color: #eee;
  17 + padding: 10px;
  18 + font-size: 11px;
  19 + }
  20 +
  21 + a { color: #000; }
  22 + a:visited { color: #666; }
  23 + a:hover { color: #fff; background-color:#000; }
  24 +
  25 + .fieldWithErrors {
  26 + padding: 2px;
  27 + background-color: red;
  28 + display: table;
  29 + }
  30 +
  31 + #errorExplanation {
  32 + width: 400px;
  33 + border: 2px solid red;
  34 + padding: 7px;
  35 + padding-bottom: 12px;
  36 + margin-bottom: 20px;
  37 + background-color: #f0f0f0;
  38 + }
  39 +
  40 + #errorExplanation h2 {
  41 + text-align: left;
  42 + font-weight: bold;
  43 + padding: 5px 5px 5px 15px;
  44 + font-size: 12px;
  45 + margin: -7px;
  46 + background-color: #c00;
  47 + color: #fff;
  48 + }
  49 +
  50 + #errorExplanation p {
  51 + color: #333;
  52 + margin-bottom: 0;
  53 + padding: 5px;
  54 + }
  55 +
  56 + #errorExplanation ul li {
  57 + font-size: 12px;
  58 + list-style: square;
  59 + }
  60 + </style>
  61 +</head>
  62 +<body>
  63 +
  64 +<p style="color: green"><%= flash[:notice] %></p>
  65 +
  66 +<%= yield %>
  67 +
  68 +</body>
  69 +</html>
27 lib/templates/list.erb
... ... @@ -0,0 +1,27 @@
  1 +<h1>Listing <%= @scaffold_plural_name %></h1>
  2 +
  3 +<table>
  4 + <tr>
  5 + <% for column in @scaffold_class.content_columns %>
  6 + <th><%= column.human_name %></th>
  7 + <% end %>
  8 + </tr>
  9 +
  10 +<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
  11 + <tr>
  12 + <% for column in @scaffold_class.content_columns %>
  13 + <td><%= entry.send(column.name) %></td>
  14 + <% end %>
  15 + <td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td>
  16 + <td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td>
  17 + <td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :method => :post } %></td>
  18 + </tr>
  19 +<% end %>
  20 +</table>
  21 +
  22 +<%= link_to "Previous page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous %>
  23 +<%= link_to "Next page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.next } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.next %>
  24 +
  25 +<br />
  26 +
  27 +<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>
6 lib/templates/new.erb
... ... @@ -0,0 +1,6 @@
  1 +<h1>New <%= @scaffold_singular_name %></h1>
  2 +
  3 +<%= error_messages_for(@scaffold_singular_name) %>
  4 +<%= form(@scaffold_singular_name, :action => "create#{@scaffold_suffix}") %>
  5 +
  6 +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
9 lib/templates/show.erb
... ... @@ -0,0 +1,9 @@
  1 +<% for column in @scaffold_class.content_columns %>
  2 + <p>
  3 + <b><%= column.human_name %>:</b>
  4 + <%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
  5 + </p>
  6 +<% end %>
  7 +
  8 +<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
  9 +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
78 test/scaffolding_test.rb
... ... @@ -0,0 +1,78 @@
  1 +require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
  2 +require 'action_controller/test_process'
  3 +require 'active_record/fixtures'
  4 +
  5 +ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
  6 +
  7 +ActiveRecord::Schema.define(:version => 1) do
  8 + create_table :entries do |t|
  9 + t.column :title, :string
  10 + t.column :body, :text
  11 + t.column :created_at, :datetime
  12 + t.column :updated_at, :datetime
  13 + end
  14 +end
  15 +
  16 +class Entry < ActiveRecord::Base
  17 +end
  18 +
  19 +class WeblogController < ActionController::Base
  20 + scaffold :entry
  21 +end
  22 +
  23 +class ScaffoldingTest < Test::Unit::TestCase
  24 + def setup
  25 + @controller = WeblogController.new
  26 + @request = ActionController::TestRequest.new
  27 + @response = ActionController::TestResponse.new
  28 +
  29 + @first = Entry.create :title => "Welcome to the weblog", :body => "Such a lovely day"
  30 + end
  31 +
  32 + def test_should_get_index
  33 + get :index
  34 + assert_response :success
  35 + end
  36 +
  37 + def test_should_get_list
  38 + get :list
  39 + assert_response :success
  40 + assert assigns(:entries)
  41 + end
  42 +
  43 + def test_should_show_entry
  44 + get :show, :id => @first.id
  45 + assert_response :success
  46 + end
  47 +
  48 + def test_should_get_new
  49 + get :new
  50 + assert_response :success
  51 + end
  52 +
  53 + def test_should_create_entry
  54 + old_count = Entry.count
  55 + post :create, :entry => { }
  56 + assert_equal old_count+1, Entry.count
  57 +
  58 + assert_redirected_to :action => 'list'
  59 + end
  60 +
  61 + def test_should_get_edit
  62 + get :edit, :id => @first.id
  63 + assert_response :success
  64 + end
  65 +
  66 + def test_should_update_entry
  67 + post :update, :id => @first.id
  68 + assert_redirected_to :action => 'show', :id => @first.id
  69 + end
  70 +
  71 + def test_should_destroy_entry
  72 + old_count = Entry.count
  73 + post :destroy, :id => @first.id
  74 + assert_equal old_count-1, Entry.count
  75 +
  76 + assert_redirected_to :action => 'list'
  77 + end
  78 +end

0 comments on commit e1d6dfe

Please sign in to comment.
Something went wrong with that request. Please try again.