Thursday, January 2, 2014

Replacing the scaffold_controller Generator in Rails 4

Background... 

If you're like me, when you start working on a project for hire the pressure to accomplish something quickly often leads to poor software development practices. When I'm running the project I can usually avoid the poor practices and help strike a balance between getting things done and setting things up for future success with some good development practices. On a recent project I found myself in the boat of continuing poor practices already in place by others on the project. The "when in Rome" approach. Although not the worst of practices, one such practice was something I think a lot of us do on Rails projects. We copy around whole sets of CRUD views or partials around and even the controller code and then tweak it to do the next thing we need. This doesn't feel very DRY and every time I do it I think to myself "I should make a Rails Generator to make this easier and less error prone.

Enter a new project... 

A good friend and I have been working on a new project together using Rails 4, Ruby 2.0, and Twitter Bootstrap 3.0. We created the initial model, agreed on general application layout and on using certain javascript components for the UI.  I created some initial views and a controller to get things working like we think they should.  Normally this is the point when the copy / paste / renaming starts happening but this time I made myself do something different.  I decided that rather than creating the next Controller and Views for a new feature I would try to get the Rails scaffold_controller to generate the controller, views, and a presenter we need.  This turned out to be quite fun and a good learning experience so I decided to capture the highlights.

How to replace the scaffold_controller in Rails 4 with your own custom version.


I could have just created my own Generator but I opted to create a replacement for the scaffold controller because I need to generate a new "presenter" class file and I wanted to keep all the nice stuff (in my opinion) that the current scaffold_controller generator can do.

Where does the code go?

All of your new code will go somewhere specific in your applications "lib" directory.

The templates for your CRUD views will go into lib/templates/haml/scaffold  OR  if your using ERB  lib/templates/erb/scaffold.

The template for the controller should be placed in lib/templates/rails/scaffold_controller

The actual scaffold generator code should be placed in lib/generators/scaffold_controller

It should all look something like the directory structure in the following image.

When it came down to getting the initial source for these I just used my own views as a starting point for the CRUD templates. Then I pulled the scaffold_controller_generator.rb and controller.rb  from the Rails source.  You can find them in your
railties-4.0.x/ directory here:

lib/rails/generators/rails/scaffold_controller/
lib/rails/generators/rails/scaffold_controller/templates

If you need to get starting points for the CRUD templates you'll have to pull them from one of two places depending on your template engine.  If you using ERB then you can find them in the railties-4.0.x/ directory here:

lib/rails/generators/erb/scaffold/templates

Or if you're like me and prefer the HAML templates then you'll want to pull them from your haml-rails-0.x directory here:

lib/generators/haml/scaffold/templates

Note: That datatable.rb in the templates/rails/scaffold_controller directory is my own custom template I use to create a class that helps me render code for jQuery Datatables.  (ie. so you won't find that in the Rails source)

By placing the code in these locations Rails will successfully use the as overrides to the existing scaffold_controller generator.

Basic scaffold templates...

To bring things a little more into perspective.   Here's my example _form.html.haml template:

.row
  .col-xs-12
    = render 'shared/show_record_errors', record: @<%= singular_table_name %>
    .col-xs-12
      = form_for @<%= singular_table_name %>, :html => {class: 'form-horizontal', role: 'form'} do |f|
<% class_name.constantize.content_columns.each do |attribute| -%>
        .form-group
          = f.label :<%= attribute.name %>
          = f.text_field :<%= attribute.name %>, class: 'form-control'
<% end -%>
        .form-group
          = bootstrap_save_button(f)


I'm merely showing this as an example of what I'm doing. Might not fit your workflow. I'm using class_name.constantize.content_columns because I'm almost always going to be working with an existing model when using this generator. That's a bit different then the original scaffold/scaffold_controller implementation on this so you probably should look at the original template for a comparison.  Also, don't be fooled by the file extension.  This is ERB that is outputting HAML.

Basic scaffold_controller generator replacement...


Here's my scaffold_controller_generator.rb file.


require 'rails/generators/resource_helpers'

module Rails
  module Generators
    class ScaffoldControllerGenerator < NamedBase # :nodoc:
      desc "========================================================================\n" +
           "NOTE!!!!!\n" +
           "This is an OVERRIDE of the original Rails Scaffold Generator!\n" +
           "Used to create Twitter Bootstrap 3.0 HAML views and custom\n" +
           "extensions specifically for this project.\n\n"+
           "It is assumed that you have already created the associated model and the\n" +
           "model will be referenced for creating any field related source.\n" +
           "Source can be found in lib/generators/scaffold_controller\n" +
           "The template can be found in lib/templates/rails/scaffold_controller\n" +
           "========================================================================\n"

      include ResourceHelpers

      check_class_collision suffix: "Controller"

      class_option :orm, banner: "NAME", type: :string, required: true,
                   desc: "ORM to generate the controller for"

      argument :attributes, type: :array, default: [], banner: "field:type field:type"

      class_option :disable_common, type: :boolean, default: true, desc: 'SETS: --no-helper --no-view-specs --no-request-specs --no-routing-specs'

      def create_controller_files
        if @options[:disable_common]
          puts 'Project Overrides'
          puts 'Disabling: helpers, view-specs, request-specs, and route-specs'
          @options = @options.merge(helper: false)
        end
        template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
        template "datatable.rb", File.join('app/datatables', controller_class_path, "#{controller_file_name}_datatable.rb")
        needed_route = <<-FILE
  resources :#{plural_table_name}
  namespace :#{plural_table_name} do
    post :act_on_list
  end
FILE
        route(needed_route)
      end

      hook_for :template_engine, :test_framework, as: :scaffold do |invoked|
        if @options[:disable_common]
          invoke invoked, [ controller_name ], {view_specs: false, request_specs: false, routing_specs: false}
        else
          invoke invoked, [ controller_name ]
        end
      end

      # Invoke the helper using the controller name (pluralized)
      hook_for :helper, as: :scaffold do |invoked|
        invoke invoked, [ controller_name ]
      end

    end
  end
end

Again, this code is highly specific to my project. I suspect that really is the only good use of generators because otherwise the code becomes to generic an needs significant modifications to create something useful. In my opinion that almost defeats the purpose of generated code.  You really should review the original Rails code for this generator and compare it to what I have for the best picture.   However, to help shed some light on that this is doing here's a quick review. Most of the work is handled in the create_controller_files method.  This method is in the original Rails source and so I just added my code.  First thing I'm doing is overriding the generation of "helpers" as I don't find them particularly useful to create the specs right away.  I'd rather not have the empty / pending files around as the odds are I won't use most of them.   You can also see that I'm using my datatable.rb template to create a file in they app/datatable folder. Then because I want to establish some initial routes I'm adding some route information to my routes.rb.  Moving on to the hook_for event handlers you can see that I've basically added some code that will override the creation of other spec files I don't want to create initially.

Running the generator...  

This is the best part of overriding this generator.  I don't have to remember a new generator name or how to run it since I basically have the same generator but it's using my Bootstrap 3.0 related view templates and my own custom presenter for jQuery Datatables.    

bundle exec rails g scaffold_controller ModelClassName

References...


1 comment:

  1. This comment has been removed by the author.

    ReplyDelete