From part 4 of the tutorial, we have set up our polymorphic associations. Now it’s time to be able to give users the ability to CRUD (create, read, update, and delete) some tasks.
Sidebar: Using Git
I’ll be using the git subversion control system throughout this tutorial, but you don’t have to. Michael Hartl talks uses it throughout the Rails tutorial (available for free online) or you can search for it.
git checkout -b part-5-crud
Getting Ready to Create a Task
First, we need to migrate the database. So at the command prompt, type:
bundle exec rake db:migrate
You should see something like the following:
== CreateTasks: migrating ====================================================
-- create_table(:tasks)
-> 0.0526s
-- add_index(:tasks, [:taskable_id, :taskable_type])
-> 0.0005s
== CreateTasks: migrated (0.0533s) ===========================================
== CreateFolders: migrating ==================================================
-- create_table(:folders)
-> 0.0006s
== CreateFolders: migrated (0.0006s) =========================================
We need a controller with some methods to create and display tasks
At the command prompt type:
rails g controller tasks index new
Because we want to be able to let a user (or folder) create tasks, we need to modify our routes.rb file so that tasks are a nested resource under todousers and folders.
I comment out all the “get” tasks lines and add nested resources so that the config/routes.rb file looks as follows:
Tododemo::Application.routes.draw do<br />
#get "tasks/index"
#get "tasks/new"
devise_for :todousers
get "pages/home"
get "pages/about"
resources :todousers do
resources :tasks
end
resources :folders do
resources :tasks
end
root :to=>"pages#home"
:
end
By nesting the routes this way, we are telling our rails application to direct actions to the TasksController we generated.
To display a task, we start by remembering that a task could have been created by a folder or Todouser
So how do we locate tasks to display in the TasksController index method?
At the top of our TasksController, we add a before_filter method, :find_taskable.
def find_taskable
klass = [Todouser, Folder].detect { |x| params["#{x.name.underscore}_id"]}
@taskable = klass.find(params["#{klass.name.underscore}_id"])
end
Explanation of find_taskable
The detect method is a ruby method (see the Ruby docs on the Enumerable class) that returns the first item in the [Todouser, Folder] array for which the block (i.e., {…}), is not false. Essentially, we’re looking for the class to which a task belongs when we call our index controller action.
Then we use that result to find the class object instance that corresponds to the id returned by the params hash.
More fun with Twitter Bootstrap
We’re going to need to start displaying some information soon, so we might as well modify our application layout file with a Twitter Bootstrap layout to display things more nicely. So we’ll borrow a template from the railsapps site and paste the following code in app/views/layouts/application.html.erb.
<p>Tododemo</p>
<p><%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %><br />
<%= csrf_meta_tags %>
<header class="navbar navbar-fixed-top">
<nav class="navbar-inner">
<div class="container">
<%= render "layouts/navigation" %>
</div>
</nav>
</header>
<div id="main">
<div class="container">
<div class="content">
<div class="row">
<div class="span12">
<%= render "layouts/messages" %><br /> <%= yield %><br /> <%= params.inspect %>
</div>
</div><footer></footer>
</div>
</div>
<p>
<!--! end of .container -->
</p>
</div>
<!--! end of #main -->
You’ll notice we’re changing the application layout file a bit from part 3 by moving our flash messages code into a rails view partial. This will help us tidy things up a bit in our view code.
Adding the flash messages partial
Create the file app/views/layouts/_messages.html.erb. Paste in the following code:
<% flash.each do |name, msg| %>
<%= content_tag :div, msg, class: “#{flash_class(name)}” %>
<% end %>
Add a navigation link to the task controller index view
Now a task can be created by a user or a folder. For simplicity’s sake, we’lll modify our navigation menu to allow the user to navigate to a page showing the tasks they created*.*
Make sure your app/views/layouts/_navigation.html.erb file looks like below with the bold code added to the original navigation code:
<ul class="nav">
<% if todouser_signed_in? %></p>
<li>
<%= link_to("Logout", destroy_todouser_session_path, :method=>"delete") %>
</li>
<li>
<%= link_to("Show Tasks", [current_todouser, :tasks]) %>
</li>
</ul>
:
Let the user add a new task from the task controller index view
Paste the following code into app/views/tasks/index.html.erb
# Tasks#index
<div>
<% @tasks.each do |task| %>
</div>
<div>
<%= simple_format task.description+" "+task.duedate %>
</div>
<% end %>
<%= link_to "New Task", [:new, @taskable, :task] %>
The link_to helper allows rails to dynamically generate the path link for a new task action since @taskable can be a Todouser or Folder type. The @tasks.each block lists out the tasks associated with the user. simple_format is a rails helper function that transforms text into html using simple formatting rules.
Modify new and add a create action
Clicking the “New Task” link will take us to the TasksController’s new action so here is the TasksController code to create a new task via the tasks association:
class TasksController < ApplicationController
before_filter :find_taskable
def index
@tasks = @taskable.tasks
end
def new
@task = @taskable.tasks.new
end
def create
@task = @taskable.tasks.new(params[:task])
if @task.save
redirect_to [@taskable, :tasks], notice: "Task created."
else
render :new
end
end
def edit
@task = @taskable.tasks.find(params[:id])
end
def update
@task = @taskable.tasks.find(params[:id])
if @task.update_attributes(params[:task])
redirect_to [@taskable, :tasks], notice: "Task updated."
else
render :edit
end
end
def destroy
@task = @taskable.tasks.find(params[:id])
if @task.destroy
redirect_to root_path, notice: "Task deleted."
else
redirect_to [@taskable, :tasks], notice: "Could not delete task for some reason."
end
end
private
def find_taskable
klass = [Todouser, Folder].detect { |x| params["#{x.name.underscore}_id"]}
@taskable = klass.find(params["#{klass.name.underscore}_id"])
end
end
Add the new view
Paste or type the following code in app/tasks/views/new.html.erb:
# New Task
<%= form_for [@taskable, @task] do |f| %>
<% if @task.errors.any? %>
<div class="#{flash_class(:error)}">
<h2>
Please correct the following errors.
</h2>
<ul>
<% @task.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label "Task Description" %>
<%= f.text_area :description %>
</div>
<div class="field">
<%= f.label "Due Date" %>
<%= f.text_field :duedate %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
This is a fairly standard form for a rails application. One thing to note is the array passed in to the form_for helper to let rails know how to generate the URL correctly for the polymorphic association. Whew, all that work just to let the Todouser create a new task.
Editing a todouser task
To our TasksController, we need to add edit and update methods so that we can edit our task descriptions and due dates.
def edit
@task = @taskable.tasks.find(params[:id])
end
def update
@task = @taskable.tasks.find(params[:id])
if @task.update_attributes(params[:task])
redirect_to [@taskable, :tasks], notice: “Task updated.”
else
render :edit
end
end
Create the edit view
Paste or type all the code from app/tasks/views/new.html.erb into a new file app/tasks/views/edit.html.erb
Change the <h1>
tag up at the top to:
<h1>Edit Task</h1>
You’re done.
Modify the index view to let the user edit a task
Open app/tasks/views/index.html.erb and edit your code as follows by adding a link_to helper underneath the task description:
<h1>Tasks#index</h1>
:
<%= simple_format task.description+" "+task.duedate %>
<%= link_to "Edit Task", [:edit, current_todouser, task] %>
:
Reading a task
Now in most rails applications, you can have the ability to show an individual task all by itself. This means we would add a show method in our controller. In the context of our application though, it doesn’t make sense since most of our task descriptions will be relatively short and don’t require an entire page to show each individual task. So we’ll skip adding a show method for now.
Delete a task
To our TasksController, we need to add a destroy method so we can delete our task.
def destroy
@task = @taskable.tasks.find(params[:id])
if @task.destroy
redirect_to root_path, notice: "Task deleted."
else
redirect_to [@taskable, :tasks], notice: "Could not delete task for some reason."
end
end
Modify the index view to allow a user to delete a task
Open app/tasks/views/index.html.erb and edit your code as follows by adding a link_to helper underneath the task description:
<h1>Tasks#index</h1>
:
<%= simple_format task.description+" "+task.duedate %>
<%= link_to "Edit Task", [:edit, current_todouser, task] %>
<%= link_to "Delete Task", [current_todouser, task], :confirm=>"Are you sure?", :method=>:delete %>
:
Sidebar: Using Git
Finally, we commit our work to git and merge it into the master branch.
git add .
git commit -m “added polymorphic associations”
git checkout master
git merge part-5-crud
Summary
We’ve now given a user the ability to create, update, and delete tasks. We turn our attention now to writing Cucumber specs.
Resources:
I’m keeping a public repo of this application at my github account. You can download the source code there.
Sources:
- http://railsapps.github.com/twitter-bootstrap-rails.html by Daniel Kehoe