= DRY CRUD
DRY CRUD generates simple and extendable controller, views and helpers that support you to DRY up the CRUD code in your Rails project. Start with these elements and build a clean base to efficiently develop your application upon. First, you need to install the gem with
gem install dry_crud
In order to use the generator, you have to register the gem in your Rails application's +Gemfile+. Add the following lines:
group :development do
gem 'dry_crud'
end
Don't worry, it's a simple development dependency. You may even savely remove it after you have run in once. So now, you should run the generator to get the goodies:
rails generate dry_crud
To integrate DRY CRUD into your code, only a few additions are required:
* For uniform CRUD functionality, just subclass your controllers from +CrudController+.
* To use standard formatting, tables and forms throughout your application, add helper :standard to your +ApplicationController+ and benefit everywhere from these little helper methods.
* Add a :label method to your models for a human-friendly representation.
Version 1.0.0 and higher are built for Rails 3. If you need a version for Rails 2.3, please get version 0.6.0 of the gem or go to the rails-2.3 branch on Github.
== Overview
In most Rails applications, you have some models that require basic CRUD (create, read, update, delete) functionality. There are various possibilities like Rails scaffolding, {Inherited Resources}[https://github.com/josevalim/inherited_resources] or {Dry Scaffold}[http://github.com/grimen/dry_scaffold]. Still, various parts in your application remain duplicated. While you might pull up common methods into a common superclass controller, most views still contain very similar code.
Enter DRY CRUD.
The main idea of DRY CRUD is to concentrate basic functionality of your application, like CRUD actions, uniform formatting, forms and tables into specifically extendable units. DRY CRUD generates various foundation classes that you may freely adapt to your application's needs. For each model, you may transparently customize arbitrary parts or just fallback to the general behavior. This applies not only for controllers, but also for view templates and helpers. There is no black box your code depends on. You lay the foundation that fits your application best.
A core element of DRY CRUD is the +RenderInheritable+ module. This gives you inheritable views and partials. In the default case, a template is searched in the current controller's view folder. If it is not found there, the template with the same name in the view folder of the superclass controller is used. This lookup path might be customized as well. RenderInheritable is also available as a stand-alone gem at http://github.com/codez/render_inheritable.
DRY CRUD is a Rails generator. All code resides in your application and is open for you to inspect and to extend. You may pick whatever you consider useful or adapt what is not sufficient. Even if you do not require any CRUD functionality, you might find some helpers simplifying your work. There are no runtime dependencies to the dry_crud gem. Having said this, DRY CRUD does not want to provide a maximum of functionality that requires a lot of configuration, but rather a clean and lightweight foundation to build your application's requirements upon. This is why DRY CRUD comes as a generator and not as a Rails plugin.
DRY CRUD does not depend on any other plugins, but easily allows you to integrate them in order to unify the behavior of your CRUD controllers. You might even use the plugins mentioned above to adapt your generated +CrudController+ base class. All classes come with thorough tests that provide you with a solid foundation for implementing your own adaptions.
See the Examples section for some use cases and the Generated Files section below for details on the single classes and templates.
== Examples
=== Controller with CRUD functionality
Say you want to manage a +Person+ model. Create the following controller and add a :label method to your model for a human-friendly representation used in page titles.
app/controllers/people_controller.rb:
class PeopleController < CrudController
end
app/models/person.rb:
class Person
def label
"#{lastname} #{firstname}"
end
end
That's it. You have a sortable overview of all people, detail pages and forms to edit and create persons. Oh, and of course, you may delete persons as well. By default, all attributes are displayed and formatted according to their column type wherever they appear. This holds for the input fields as well.
==== Customize single views
Well, maybe there are certain attributes you do not want to display in the people list, or others that are not editable. No problem, simply create a _list partial in app/views/people/_list.html.erb to customize this:
<%= crud_table [:lastname, :firstname, :city, :sex] %>
This only displays these three attributes in the table. All other templates, as well as the main index view, fallback to the ones in app/views/crud.
==== Adapt general behavior
Next, let's adapt a part of the general behavior used in all CRUD controllers. As an example, we include pagination with will_paginate[http://wiki.github.com/mislav/will_paginate/] in all our overview tables:
In app/controllers/list_controller.rb, change the index action to
def index
@entries = list_entries.paginate(:page => params[:page])
respond_with @entries
end
In app/views/list/index.html.erb, add the following line for the pagination links:
<%= will_paginate @entries %>
And we are done again. All our controllers inheriting from +ListController+, including above +PeopleController+, now have paginated index views. Because our customization for the people table is in the separate _list partial, no further modifications are required.
If the current page should be remembered while viewing or editing an entry, just add :page to the remembered_params in ListController::Memory:
controller.remember_params = [:q, :sort, :sort_dir, :page]
==== Special formatting for selected attributes
Sometimes, the default formatting provided by +:format_attr+ will not be sufficient. We have a boolean column sex in our model, but would like to display 'male' or 'female' for it (instead of 'no' or 'yes', which is a bit cryptic). Just define a method in your view helper starting with format_, followed by the attribute name:
In app/helpers/people.rb:
def format_sex(person)
person.sex ? 'female' : 'male'
end
By the way: The method +:f+ in +StandardHelper+ uniformly formats arbitrary values according to their class.
==== Filtering the index list
There is a simple search functionality (based on SQL LIKE queries) implemented in the +CrudController+. Define an array of columns in your controller's +search_columns+ class variable to make the entries searchable by these fields:
In app/controllers/people_controller.rb:
self.search_columns = [:firstname, :lastname]
If you have columns defined, a search box will be displayed in the index view that let's you filter the displayed entries.
==== CRUD controller callbacks
As a last example, let's say we have added a custom input field that must specially processed. Instead of overwriting the entire update action, it is possible to register callbacks for the +create+, +update+, +save+ (= +create+ and +update+) and +destroy+ actions. They work very similarliy like the callbacks on ActiveRecord. For each action, before and after callbacks are run. Before callbacks may also prevent the action from being executed when returning false. Here is some code:
In app/controllers/people_controller.rb:
after_save :upload_picture
before_destroy :delete_picture
def upload_picture
store_file(params[:person][:picture]) if params[:person][:picture]
end
def delete_picture
if !perform_delete_picture(@entry.picture)
flash.alert = 'Could not delete picture'
false
end
end
Beside these "action" callbacks, there is also a set of "before render" callbacks that are called whenever a certain view is rendered. They are available for the +index+, +show+, +new+, +edit+ and +form+ (= +new+ and +edit+) views. These callbacks are not only called for the corresponding action, but, for example, also when the +new+ view is going to be rendered from an unsuccessfull +create+ action. Say you need to prepare additional variables whenever the form is rendered:
In app/controllers/people_controller.rb:
before_render_form :set_hometowns
def set_hometowns
@hometowns = City.where(:country => @entry.country)
end
=== Standard Tables and Forms
DRY CRUD also provides two builder classes for update/create forms and tables for displaying entries of one model. They may be used all over your application to DRY up the form and table code. Normally, they are used with the corresponding methods from +StandardHelper+. When you define a view for a subclass of +CrudController+, you may also use the slightly enhanced +crud_table+ and +crud_form+ methods from +CrudHelper+.
==== Tables
This is the code to define a table with some attribute columns for a list of same-type entries. Columns get a header corresponding to the attribute name:
<%= table(@people) do |t|
t.sortable_attrs :lastname, :firstname
end %>
If entries is empty, a basic 'No entries found' message is rendered instead of the table.
To render custom columns, use the :col method:
<%= table(@people) do |t|
t.sortable_attrs :lastname, :firstname
t.col('', :class => 'center') {|entry| image_tag(entry.picture) }
t.attr :street
t.col('Map') {|entry| link_to(entry.city, "http://maps.google.com/?q=#{entry.city}" }
end %>
==== Forms
Forms work very similar. In the most simple case, you just have to specify which attributes of a model to create input fields for, and you get a complete form with error messages, labeled input fields according the column types and a save button:
<%= standard_form(@person, [:firstname, :lastname, :age, :city] -%>
Of course, custom input fields may be defined as well:
<%= standard_form(@person, [], :url => {:action => 'custom_update', :id => @person.id}) do |f| %>
<%= f.labeled_input_fields :firstname, :lastname %>
<%= f.labeled(:sex) do %>
<%= f.radio_button :sex, true %> female
<%= f.radio_button :sex, false %> male
<% end %>
<%= f.labeled_integer_field :age %>
<%= f.labeled_file_field :picture %>
<% end %>
Even +belongs_to+ associations are automatically rendered with a select field. By default, all entries from the associated model are used as options. To customize this, either define an instance variable with the same name as the association in your controller, or pass a :list option:
<%= f.belongs_to_field :hometown, :list => City.where(:country => @person.country) %>
Yes, it's bad practice to use finder logic in your views! Define the variable @hometowns in your controller instead (as shown in the example above), and you do not even have to specify the :list option.
== Generated Files
All generated files are supposed to provide a reasonable foundation for the CRUD functionality. You are encouraged to adapt them to fit the needs of your application. They're yours!
=== Controller:
{controller/crud_controller.rb}[http://codez.ch/dry_crud/?q=CrudController]:: Abstract controller providing basic CRUD actions. This implementation mainly follows the one of the Rails scaffolding controller and responses to HTML and XML requests. Some enhancements were made to ease extendability. Several protected helper methods are there to be (optionally) overriden by subclasses. With the help of additional callbacks, it is possible to hook into the action procedures without overriding the entire method. This class is based on ListController.
{controller/list_controller.rb}[http://codez.ch/dry_crud/?q=ListController]:: Abstract controller providing a basic list action. There are two sub-modules that provide search and sort functionality for the table displayed in the list action. A third sub-module remembers the list parameters in order to return to an identical list.
{controller/render_inheritable.rb}[http://codez.ch/dry_crud/?q=RenderInheritable]:: A controller enhancement that allows one to render inheritable views and partials. If no view file is found for the current controller, the corresponding file is looked up in its superclass hierarchy. Thus, only views or partials that look differently have to be overwritten.
=== Helpers:
{helpers/standard_helper.rb}[http://codez.ch/dry_crud/?q=StandardHelper]:: A view helper to standardize often used functions like formatting, tables, forms or action links. This helper is ideally defined in the ApplicationController. It is required to use the StandardTableBuilder and the StandardFormBuilder.
{helpers/crud_helper.rb}[http://codez.ch/dry_crud/?q=CrudHelper]:: A small helper for CrudController to render tables and forms with a default set of attributes.
{helpers/list_helper.rb}[http://codez.ch/dry_crud/?q=ListHelper]:: A small helper for ListController to render the list table with a default set of attributes.
{helpers/standard_table_builder.rb}[http://codez.ch/dry_crud/?q=StandardTableBuilder]:: A simple helper object to easily define tables listing several rows of the same data type.
{helpers/standard_form_builder.rb}[http://codez.ch/dry_crud/?q=StandardFormBuilder]:: A form builder that automatically selects the corresponding input element for ActiveRecord columns. Input elements are rendered with a corresponding label by default.
=== Views:
All templates in the +crud+ folder may be 'overriden' individually in a respective view folder. Define the basic structure of your CRUD views here and adapt it as required for each single model.
views/crud/show.html.erb:: The show view displaying all the attributes of one entry and the various actions to perform on it.
views/crud/_attrs.html.erb:: A partial defining the attributes to be displayed in the show view.
views/crud/_list.html.erb:: A partial defining the table in the index view. To change the displayed attributes for your list model, just create an own _list.html.erb in your controller's view directory.
views/crud/new.html.erb:: The view to create a new entry.
views/crud/edit.html.erb:: The view to edit an existing entry.
views/crud/_form.html.erb:: The form used to create and edit entries. If you would like to customize this form for various models, just create an own _form.html.erb in your controller's view directory.
views/crud/_actions_index.html.erb:: The action links available in the index view.
views/crud/_actions_show.html.erb:: The action links available in the show view.
views/crud/_actions_edit.html.erb:: The action links available in the edit view.
views/list/index.html.erb:: The index view displaying a sortable table with all entries. If you have +search_columns+ defined for your controller, then a search box is rendered as well.
views/list/_list.html.erb:: A partial defining the table in the index view. To change the displayed attributes for your list model, just create an own _list.html.erb in your controller's view directory.
views/list/_search.html.erb:: A partial defining a simple search form that is displayed when +search_columns+ are defined in a subclassing controller.
views/list/_actions_index.html.erb:: The action links available in the index view. None by default.
views/shared/_labeled.html.erb:: Partial to define the layout for an arbitrary content with a label.
views/layouts/crud.html.erb:: An example layout showing how to use the @title and +flash+. Most probably you want to merge this with your application.html.erb or adapt the main CRUD templates, so you wont need this file.
views/layouts/_menu.html.erb:: An empty file to put your menu items into. Included from +list.html.erb+.
public/stylesheets/crud.css:: A simple CSS with all the classes and ids used in the CRUD code.
=== Tests:
{test/crud_test_model.rb}[http://codez.ch/dry_crud/?q=CrudTestHelper]:: A dummy model to run CRUD tests against.
{test/custom_assertions.rb}[http://codez.ch/dry_crud/?q=CustomAssertions]:: A handful of convenient assertions. Include this module into your test_helper.rb file.
{test/functionals/crud_controller_test_helper.rb}[http://codez.ch/dry_crud/?q=CrudControllerTestHelper]:: A module to include into the functional tests for your CrudController subclasses. Contains a handful of CRUD functionality tests for the provided implementation. So for each new CRUD controller, you get 20 tests for free.
test/several other tests:: Testing the provided implementation and a great base to test your adaptions of the CRUD code.