Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Extract dynamic scaffolding into a plugin. Closes #7700.

  • Loading branch information...
commit e1d6dfe0f7b0179e53a96aba88c0ce40882a2d53 0 parents
@jeremy jeremy authored
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2004-2007 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
74 README
@@ -0,0 +1,74 @@
+Scaffolding
+===========
+
+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:
+
+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.
+
+Example
+=======
+
+ class WeblogController < ActionController::Base
+ scaffold :entry
+ end
+
+This tiny piece of code will add all of the following methods to the controller:
+
+ class WeblogController < ActionController::Base
+ verify :method => :post, :only => [ :destroy, :create, :update ],
+ :redirect_to => { :action => :list }
+
+ def index
+ list
+ end
+
+ def list
+ @entries = Entry.find(:all)
+ render_scaffold "list"
+ end
+
+ def show
+ @entry = Entry.find(params[:id])
+ render_scaffold
+ end
+
+ def destroy
+ Entry.find(params[:id]).destroy
+ redirect_to :action => "list"
+ end
+
+ def new
+ @entry = Entry.new
+ render_scaffold
+ end
+
+ def create
+ @entry = Entry.new(params[:entry])
+ if @entry.save
+ flash[:notice] = "Entry was successfully created"
+ redirect_to :action => "list"
+ else
+ render_scaffold('new')
+ end
+ end
+
+ def edit
+ @entry = Entry.find(params[:id])
+ render_scaffold
+ end
+
+ def update
+ @entry = Entry.find(params[:id])
+ @entry.attributes = params[:entry]
+
+ if @entry.save
+ flash[:notice] = "Entry was successfully updated"
+ redirect_to :action => "show", :id => @entry
+ else
+ render_scaffold('edit')
+ end
+ end
+ end
+
+
+Copyright (c) 2004-2007 David Heinemeier Hansson, released under the MIT license
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the scaffolding plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the scaffolding plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Scaffolding'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
1  init.rb
@@ -0,0 +1 @@
+ActionController::Base.send :include, Scaffolding
187 lib/scaffolding.rb
@@ -0,0 +1,187 @@
+module Scaffolding # :nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # 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:
+ #
+ # class WeblogController < ActionController::Base
+ # scaffold :entry
+ # end
+ #
+ # This tiny piece of code will add all of the following methods to the controller:
+ #
+ # class WeblogController < ActionController::Base
+ # verify :method => :post, :only => [ :destroy, :create, :update ],
+ # :redirect_to => { :action => :list }
+ #
+ # def index
+ # list
+ # end
+ #
+ # def list
+ # @entries = Entry.find(:all)
+ # render_scaffold "list"
+ # end
+ #
+ # def show
+ # @entry = Entry.find(params[:id])
+ # render_scaffold
+ # end
+ #
+ # def destroy
+ # Entry.find(params[:id]).destroy
+ # redirect_to :action => "list"
+ # end
+ #
+ # def new
+ # @entry = Entry.new
+ # render_scaffold
+ # end
+ #
+ # def create
+ # @entry = Entry.new(params[:entry])
+ # if @entry.save
+ # flash[:notice] = "Entry was successfully created"
+ # redirect_to :action => "list"
+ # else
+ # render_scaffold('new')
+ # end
+ # end
+ #
+ # def edit
+ # @entry = Entry.find(params[:id])
+ # render_scaffold
+ # end
+ #
+ # def update
+ # @entry = Entry.find(params[:id])
+ # @entry.attributes = params[:entry]
+ #
+ # if @entry.save
+ # flash[:notice] = "Entry was successfully updated"
+ # redirect_to :action => "show", :id => @entry
+ # else
+ # render_scaffold('edit')
+ # end
+ # end
+ # end
+ #
+ # The <tt>render_scaffold</tt> 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.
+ module ClassMethods
+ # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
+ # one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
+ # and @post/@posts for the instance variables.
+ #
+ # It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
+ # make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
+ # instead of just list, show, and post. If suffix is used, then no index method is added.
+ def scaffold(model_id, options = {})
+ options.assert_valid_keys(:class_name, :suffix)
+
+ singular_name = model_id.to_s
+ class_name = options[:class_name] || singular_name.camelize
+ plural_name = singular_name.pluralize
+ suffix = options[:suffix] ? "_#{singular_name}" : ""
+
+ unless options[:suffix]
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def index
+ list
+ end
+ end_eval
+ end
+
+ module_eval <<-"end_eval", __FILE__, __LINE__
+
+ verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
+ :redirect_to => { :action => :list#{suffix} }
+
+
+ def list#{suffix}
+ @#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
+ render#{suffix}_scaffold "list#{suffix}"
+ end
+
+ def show#{suffix}
+ @#{singular_name} = #{class_name}.find(params[:id])
+ render#{suffix}_scaffold
+ end
+
+ def destroy#{suffix}
+ #{class_name}.find(params[:id]).destroy
+ redirect_to :action => "list#{suffix}"
+ end
+
+ def new#{suffix}
+ @#{singular_name} = #{class_name}.new
+ render#{suffix}_scaffold
+ end
+
+ def create#{suffix}
+ @#{singular_name} = #{class_name}.new(params[:#{singular_name}])
+ if @#{singular_name}.save
+ flash[:notice] = "#{class_name} was successfully created"
+ redirect_to :action => "list#{suffix}"
+ else
+ render#{suffix}_scaffold('new')
+ end
+ end
+
+ def edit#{suffix}
+ @#{singular_name} = #{class_name}.find(params[:id])
+ render#{suffix}_scaffold
+ end
+
+ def update#{suffix}
+ @#{singular_name} = #{class_name}.find(params[:id])
+ @#{singular_name}.attributes = params[:#{singular_name}]
+
+ if @#{singular_name}.save
+ flash[:notice] = "#{class_name} was successfully updated"
+ redirect_to :action => "show#{suffix}", :id => @#{singular_name}
+ else
+ render#{suffix}_scaffold('edit')
+ end
+ end
+
+ private
+ def render#{suffix}_scaffold(action=nil)
+ action ||= caller_method_name(caller)
+ # logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
+
+ if template_exists?("\#{self.class.controller_path}/\#{action}")
+ render :action => action
+ else
+ @scaffold_class = #{class_name}
+ @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
+ @scaffold_suffix = "#{suffix}"
+ add_instance_variables_to_assigns
+
+ @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
+
+ if !active_layout.nil?
+ render :file => active_layout, :use_full_path => true
+ else
+ render :file => scaffold_path('layout')
+ end
+ end
+ end
+
+ def scaffold_path(template_name)
+ File.dirname(__FILE__) + "/templates/" + template_name + ".erb"
+ end
+
+ def caller_method_name(caller)
+ caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
+ end
+ end_eval
+ end
+ end
+end
7 lib/templates/edit.erb
@@ -0,0 +1,7 @@
+<h1>Editing <%= @scaffold_singular_name %></h1>
+
+<%= error_messages_for(@scaffold_singular_name) %>
+<%= form(@scaffold_singular_name, :action => "update#{@scaffold_suffix}") %>
+
+<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
69 lib/templates/layout.erb
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Scaffolding</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+
+ .fieldWithErrors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+ }
+
+ #errorExplanation {
+ width: 400px;
+ border: 2px solid red;
+ padding: 7px;
+ padding-bottom: 12px;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+ }
+
+ #errorExplanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ background-color: #c00;
+ color: #fff;
+ }
+
+ #errorExplanation p {
+ color: #333;
+ margin-bottom: 0;
+ padding: 5px;
+ }
+
+ #errorExplanation ul li {
+ font-size: 12px;
+ list-style: square;
+ }
+ </style>
+</head>
+<body>
+
+<p style="color: green"><%= flash[:notice] %></p>
+
+<%= yield %>
+
+</body>
+</html>
27 lib/templates/list.erb
@@ -0,0 +1,27 @@
+<h1>Listing <%= @scaffold_plural_name %></h1>
+
+<table>
+ <tr>
+ <% for column in @scaffold_class.content_columns %>
+ <th><%= column.human_name %></th>
+ <% end %>
+ </tr>
+
+<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
+ <tr>
+ <% for column in @scaffold_class.content_columns %>
+ <td><%= entry.send(column.name) %></td>
+ <% end %>
+ <td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td>
+ <td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td>
+ <td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :method => :post } %></td>
+ </tr>
+<% end %>
+</table>
+
+<%= link_to "Previous page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous %>
+<%= link_to "Next page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.next } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.next %>
+
+<br />
+
+<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>
6 lib/templates/new.erb
@@ -0,0 +1,6 @@
+<h1>New <%= @scaffold_singular_name %></h1>
+
+<%= error_messages_for(@scaffold_singular_name) %>
+<%= form(@scaffold_singular_name, :action => "create#{@scaffold_suffix}") %>
+
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
9 lib/templates/show.erb
@@ -0,0 +1,9 @@
+<% for column in @scaffold_class.content_columns %>
+ <p>
+ <b><%= column.human_name %>:</b>
+ <%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
+ </p>
+<% end %>
+
+<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
+<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
78 test/scaffolding_test.rb
@@ -0,0 +1,78 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
+require 'action_controller/test_process'
+require 'active_record/fixtures'
+
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+
+ActiveRecord::Schema.define(:version => 1) do
+ create_table :entries do |t|
+ t.column :title, :string
+ t.column :body, :text
+ t.column :created_at, :datetime
+ t.column :updated_at, :datetime
+ end
+end
+
+class Entry < ActiveRecord::Base
+end
+
+class WeblogController < ActionController::Base
+ scaffold :entry
+end
+
+class ScaffoldingTest < Test::Unit::TestCase
+ def setup
+ @controller = WeblogController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ @first = Entry.create :title => "Welcome to the weblog", :body => "Such a lovely day"
+ end
+
+ def test_should_get_index
+ get :index
+ assert_response :success
+ end
+
+ def test_should_get_list
+ get :list
+ assert_response :success
+ assert assigns(:entries)
+ end
+
+ def test_should_show_entry
+ get :show, :id => @first.id
+ assert_response :success
+ end
+
+ def test_should_get_new
+ get :new
+ assert_response :success
+ end
+
+ def test_should_create_entry
+ old_count = Entry.count
+ post :create, :entry => { }
+ assert_equal old_count+1, Entry.count
+
+ assert_redirected_to :action => 'list'
+ end
+
+ def test_should_get_edit
+ get :edit, :id => @first.id
+ assert_response :success
+ end
+
+ def test_should_update_entry
+ post :update, :id => @first.id
+ assert_redirected_to :action => 'show', :id => @first.id
+ end
+
+ def test_should_destroy_entry
+ old_count = Entry.count
+ post :destroy, :id => @first.id
+ assert_equal old_count-1, Entry.count
+
+ assert_redirected_to :action => 'list'
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.