Models represent real-world things. For instance, let's say you're working with dogs. Let's create a class to represent a dog.
class Dog < ActiveRecord::Base
attr_accessible :id, :name, :breed, :weight
end
For our use, all we want to keep up with is the dog's name, breed, and weight.
We need a way to interact with our dog model. The way we do that is by routing certain HTTP requests to certain methods of a controller. We'll get to the controller below, but let's talk about the routes we need first. There are many ways of setting up routes, but we prefer to use RESTful URLs for all of our models. This means that each instance of a model has it's own unique URL and that you interact with the models by sending different types of HTTP requests to the unique URL.
For our dog model example, we'll use two main URLs:
/admin/dogs
/admin/dogs/:id
Here's the route file for this:
MyExample::Application.routes.draw do
get "/admin/dogs" => "dogs#admin_index"
get "/admin/dogs/:id" => "dogs#admin_edit"
put "/admin/dogs/:id" => "dogs#admin_update"
post "/admin/dogs" => "dogs#admin_add"
delete "/admin/dogs/:id" => "dogs#admin_delete"
end
As you can see, the above route file covers viewing a list of dogs, adding to the list of dogs, editing a dog, updating a dog, and deleting a dog. Now let's add a few more that we'll talk about later in the controllers.
MyExample::Application.routes.draw do
get "/admin/dogs" => "dogs#admin_index"
get "/admin/dogs" => "dogs#admin_json"
get "/admin/dogs" => "dogs#admin_json_single"
get "/admin/dogs/:id" => "dogs#admin_edit"
put "/admin/dogs/bulk" => "dogs#admin_bulk_update"
put "/admin/dogs/:id" => "dogs#admin_update"
post "/admin/dogs/bulk" => "dogs#admin_bulk_add"
post "/admin/dogs" => "dogs#admin_add"
delete "/admin/dogs/bulk" => "dogs#admin_bulk_delete"
delete "/admin/dogs/:id" => "dogs#admin_delete"
end
Let's start with a simple controller that implements the standard admin methods.
class DogsController < Caboose::ApplicationController
def admin_index() end
def admin_json() end
def admin_json_single() end
def admin_edit() end
def admin_update() end
def admin_bulk_update() end
def admin_add() end
def admin_bulk_add() end
def admin_delete() end
def admin_bulk_delete() end
end
And now let's fill in the details for each method.
The admin_index method simply makes sure the user is allowed to get to the page. This is done because the model_binder/index_table javascript will go get the list of dogs.
# GET /admin/dogs
def admin_index
return if !user_is_allowed('dogs', 'view')
render :layout => 'caboose/admin'
end
The admin_json method is what the index_table javascript will call to get an array of all the dogs. As you can see, there are a few parameters that can be passed that will filter the dogs that are returned.
# GET /admin/dogs/json
def admin_json
return if !user_is_allowed('dogs', 'view')
pager = Caboose::PageBarGenerator.new(params, {
'name_like' => '',
'breed_like' => '',
'weight_like' => '',
}, {
'model' => 'Dog',
'sort' => 'name',
'desc' => false,
'base_url' => '/admin/dogs',
'items_per_page' => 25,
'use_url_params' => false
})
render :json => {
:pager => pager,
:models => pager.items
}
end
The admin_json_single method is what the index_table javascript uses to refresh a single dog from the database.
# GET /admin/dogs/:id/json
def admin_json_single
return if !user_is_allowed('dogs', 'view')
d = Dog.find(params[:id])
render :json => d
end
The admin_edit method just gets the dog from the database corresponding to the id given in the URL, then sends it on to the admin edit view.
# GET /admin/dogs/:id
def admin_edit
return if !user_is_allowed('dogs', 'edit')
@dog = Dog.find(params[:id])
render :layout => 'caboose/admin'
end
The admin_update method updates the given attribute for the object corresponding to the given id. This method is usually called from javascript ModelBinder object.
# PUT /admin/dogs/:id
def admin_update
return if !user_is_allowed('dogs', 'edit')
resp = Caboose::StdClass.new
dog = Dog.find(params[:id])
save = true
params.each do |k,v|
case name
when 'name' then dog.name = value
when 'breed' then dog.breed = value
when 'weight' then dog.weight = value
end
end
resp.success = save && d.save
render :json => resp
end
The admin_bulk_update method does the same thing as the admin_update method, but it update each dog correspoding to every id given. This method is called from the ModelBinder/IndexTable javascript object.
# PUT /admin/dogs/bulk
def admin_bulk_update
return unless user_is_allowed_to('edit', 'dogs')
resp = Caboose::StdClass.new
dogs = params[:model_ids].collect{ |dog_id| Dog.find(dog_id) }
save = true
params.each do |k,v|
case k
when 'name' then dogs.each{ |dog| dog.name = v }
when 'breed' then dogs.each{ |dog| dog.breed = v }
when 'weight' then dogs.each{ |dog| dog.weight = v }
end
end
dogs.each{ |dog| dog.save }
resp.success = true
render :json => resp
end
The admin_add method takes the given data, creates a new dog, then returns the id of the new dog. This method is often called from the ModelBinder/IndexTable javascript object, but may also be called from a custom new model page.
# POST /admin/dogs
def admin_add
return if !user_is_allowed('dogs', 'add')
resp = Caboose::StdClass.new
if params[:name].length == 0 then resp.error = "The name cannot be empty."
elsif params[:breed].length == 0 then resp.error = "The breed cannot be empty."
elsif params[:weight].length == 0 then resp.error = "The weight cannot be empty."
else
d = Dog.new(
:name => params[:name],
:breed => params[:breed],
:weight => params[:weight]
)
d.save
resp.new_id = d.id
resp.redirect = "/admin/dogs/#{d.id}"
end
render :json => resp
end
The admin_bulk_add method takes CSV data, checks it, and upon a successful check, creates objects for each row of data. This method is usually called from the ModelBinder/IndexTable javascript object.
# POST /admin/dogs/bulk
def admin_bulk_add
return if !user_is_allowed('dogs', 'add')
resp = Caboose::StdClass.new
# Check for data integrity first
CSV.parse(params[:csv_data]).each do |row|
if row[0].length == 0 then resp.error = "The name cannot be empty." and break
elsif row[1].length == 0 then resp.error = "The breed cannot be empty." and break
elsif row[2].length == 0 then resp.error = "The weight cannot be empty." and break
end
end
if resp.error.nil?
CSV.parse(params[:csv_data]).each do |row|
Dog.create(
:name => row[0],
:breed => row[1],
:weight => row[2]
)
end
resp.success = true
end
render :json => resp
end
The admin_delete method simply deletes the object corresponding to the given id.
# DELETE /admin/dogs/:id
def admin_delete
return if !user_is_allowed('dogs', 'delete')
Dog.find(params[:id]).destroy
render :json => { :success => true }
end
The admin_bulk_delete method deletes the objects corresponding to the given model ids. This method is usually called from the ModelBinder/IndexTable javascript object.
# DELETE /admin/dogs/bulk
def admin_bulk_delete
return if !user_is_allowed('dogs', 'delete')
resp = Caboose::StdClass.new
params[:model_ids].each do |dog_id|
Dog.find(dog_id).destroy
end
resp.success = true
render :json => resp
end
Because the ModelBinder/IndexTable does so much, for most models, there are only two views that need to be written, one for the admin_index method and another for the admin_edit method.
The admin_index view sets up the ModelBinder/IndexTable javascript object.
<h1>Dogs</h1>
<div id='dogs'></div>
<div id='message'></div>
<% content_for :caboose_js do %>
<%= javascript_include_tag "caboose/model/all" %>
<script type='text/javascript'>
$(document).ready(function() {
var that = this;
var table = new IndexTable({
form_authenticity_token: '<%= form_authenticity_token %>',
container: 'dogs',
base_url: '/admin/dogs',
allow_bulk_edit: true,
allow_bulk_delete: true,
allow_duplicate: false,
allow_advanced_edit: true,
fields: [
{ show: true , name: 'name' , nice_name: 'Name' , sort: 'name' , type: 'text' , value: function(d) { return d.name; }, width: 75, align: 'left', bulk_edit: true },
{ show: true , name: 'breed' , nice_name: 'Breed' , sort: 'breed' , type: 'text' , value: function(d) { return d.breed; }, width: 75, align: 'left', bulk_edit: true },
{ show: true , name: 'weight' , nice_name: 'Weight' , sort: 'weight' , type: 'text' , value: function(d) { return d.weight; }, width: 75, align: 'left', bulk_edit: true }
],
new_model_text: 'New Dog',
new_model_fields: [
{ name: 'name' , nice_name: 'Name' , type: 'text' },
{ name: 'breed' , nice_name: 'Breed' , type: 'text' },
{ name: 'weight' , nice_name: 'Weight' , type: 'text' }
],
bulk_import_fields: ['name', 'breed', 'weight'],
bulk_import_url: '/admin/dogs/bulk'
});
});
</script>
<% end %>
The admin_edit method is not completely necessary because the ModelBinder/IndexTable javascript objects allows models to be edited inline directly on the admin_index page. However, there are times where it's easier for the user to edit models on another page. In this case, the admin_edit view will be needed. The admin_edit view takes the model from the admin_edit controller method and sets up a ModelBinder javascript object. The admin_edit view also sets up a delete button that sends a delete ajax call.
<%
d = @dog
%>
<h1>Edit Dog</h1>
<p><div id="dog_<%= d.id %>_name" ></div></p>
<p><div id="dog_<%= d.id %>_breed" ></div></p>
<p><div id="dog_<%= d.id %>_weight" ></div></p>
<div id="message"></div>
<p>
<input type="button" value="< Back" onclick="window.location='/admin/dogs';" />
<input type="button" value="Delete Dog" onclick="delete_dog(<%= d.id %>)" />
</p>
<% content_for :caboose_js do %>
<%= javascript_include_tag "caboose/model/all" %>
<script type='text/javascript'>
$(document).ready(function() {
m = new ModelBinder({
name: 'Dog',
id: <%= d.id %>,
update_url: '/admin/dogs/<%= d.id %>',
authenticity_token: '<%= form_authenticity_token %>',
attributes: [
{ name: 'name' , nice_name: 'Name' , type: 'text' , value: <%= raw Caboose.json(d.name) %> , width: 400 },
{ name: 'breed' , nice_name: 'Breed' , type: 'text' , value: <%= raw Caboose.json(d.breed) %> , width: 400 },
{ name: 'weight' , nice_name: 'Weight' , type: 'text' , value: <%= raw Caboose.json(d.weight) %> , width: 400 }
]
});
});
function delete_dog(dog_id, confirm)
{
if (!confirm)
{
var p = $('').addClass('note error')
.append("Are you sure you want to delete the dog? ")
.append($('').attr('type', 'button').val('Yes').click(function() { delete_dog(dog_id, true); })).append(' ')
.append($('').attr('type', 'button').val('No').click(function() { $('#message').empty(); }));
$('#message').empty().append(p);
return;
}
$('#message').html("Deleting the dog...
");
$.ajax({
url: '/admin/dogs/' + dog_id,
type: 'delete',
success: function(resp) {
if (resp.error) $('#message').html("" + resp.error + "
");
if (resp.redirect) window.location = resp.redirect;
}
});
}
</script>
<% end %>