The next feature requested for Intranet is a simple task-tracking module. This will enable users of the system to record activities they carry out with clients. Acme staff tend to work on an individual basis with other companies, and therefore would prefer to track activity with people rather than the companies they represent. (However, as people are associated with companies, all the activity related to a company can be aggregated from the tasks carried out with its employees.)
Rory and Jenny decide to implement a task list, which is displayed in a read-only mode alongside a person's record when in the "show" view, arranged in reverse chronological order (newest at the top). That person's tasks will also become editable by clicking on a link next to the task.
As always, the first step is to create a model to represent a task. A task will need the following fields:
title:
The title of the taskdescription:
A description of the task (optional)user_id:
The member of Acme staff who "owns" the taskperson_id:
The person (client) the task is associated withcomplete:
Whether the task is complete (true/false)start:
The start date and time for the taskend:
The end date and time for the task (optional)Note that Rory and Jenny aren't aiming to produce a full-fledged project management system. They are just aiming at a tool for recording activity with clients. Eventually, it might grow into or be replaced by a full project management tool; for now, they have limited time, and want to provide as much functionality as simply as possible.
Generate a model from the command line:
$ ruby script/generate model Task
Write the migration to build the tasks
table (in db/migrate/006_create_tasks.rb):
class CreateTasks < ActiveRecord::Migration def self.up create_table :tasks do |t| t.column :title, :string, :null => false t.column :description, :text t.column :user_id, :integer t.column :person_id, :integer t.column :complete, :boolean, :null => false, :default => false t.column :start, :datetime, :null => false t.column :end, :datetime end end def self.down drop_table :tasks end end
As tasks have relationships to both a Person
and a User
, these relationships must be specified in the Task
model (app/models/task.rb). We also need to validate the fields:
class Task < ActiveRecord::Base belongs_to :person belongs_to :user validates_presence_of :title, :message => 'Please supply a title' validates_associated :person, :message => 'The specified person is invalid' validates_associated :user, :message => 'The specified owner is invalid' validates_presence_of :start, :message => 'Please set a start date and time for the task' end
The other side of the relationship to Person
also needs to be specified (in app/models/person.rb)
, as we'd like to be able to show all the tasks relating to a person. This requires a has_many
method call (highlighted below) inside the Person
class definition:
class Person < ActiveRecord::Base include AddressHandler belongs_to :company belongs_to :address has_many :tasks, :order => 'complete ASC, start DESC', :dependent => :nullify # ... other methods ... end
Notice that the relationship includes the option :order => 'complete ASC, start DESC'
, to ensure that the associated tasks are retrieved in ascending order of whether they are complete (incomplete tasks first), then descending order of their start date-times. This is the desired order for displaying tasks alongside a person's full details. We also include a :dependent => :nullify
option, so that if a person's record is destroyed, any of their dependent tasks have their person_id
set to NULL.
We can also specify a has_many
association in the User
model:
class User < ActiveRecord::Base
has_many :tasks, :order => 'complete ASC, start DESC',
:dependent => :nullify
# ... other methods ...
end
Again, the :dependent => :nullify
option is specified, so that any tasks associated with a user have their user_id
attribute set to NULL, if that user is deleted.
Next, generate the controller that will handle CRUD operations for tasks:
$ ruby script/generate controller tasks
The CRUD actions themselves are simple to add into the controller: as they only have to deal with a single model (Task) in isolation, they don't have the complexity of the previous controllers we've created. The actions look like this:
class TasksController < ApplicationController before_filter :authorize, :except => [:index, :show] before_filter :get_task, :only => [:show, :edit, :update, :confirm, :delete] def index @page_title = "All tasks" @tasks = Task.find(:all, :order => 'title') end def show @page_title = "Task: " + @task.title end def new @page_title = "Adding new task" @task = Task.new end def create @task = Task.new(params[:task]) if @task.save redirect_to_index "Task added successfully" else @page_title = "Adding new task" render :action => 'new' end end def edit @page_title = "Edit " + @task.title end def update if @task.update_attributes(params[:task]) redirect_to_index "Task updated successfully" else @page_title = "Edit " + @task.title render :action => 'edit' end end def confirm confirm_delete(@task, "Are you sure you want to delete " + @task.title + "?") end def delete do_delete(@task) end private def get_task @task = Task.find(params[:id]) end end
We're starting to see the code we've written previously paying off now: the highlighted sections show the use of authentication via the authorize
method; application of a before_filter
to fetch a task if the user is showing, editing or deleting it; use of the generic confirm_delete
and do_delete
methods to delete a task; and use of our generic redirect_to_index
method. This demonstrates the advantages of constant refactoring, and how Rails enables us to create our own powerful macros for common patterns.
We now need some view templates to go with the actions defined in the previous section.
Here's index.rhtml
(to display a table of all tasks):
<h1><%= @page_title %></h1> <table> <tr> <th>Title</th><th>Actions</th> </tr> <% @tasks.each do |task| -%> <tr> <td> <%= link_to task.title, :action => 'show', :id => task.id %> </td> <td> <%= link_to 'Edit', :action => 'edit', :id => task.id %> | <%= link_to 'Delete', :action => 'confirm', :id => task.id %> </td> </tr> <% end -%> </table>
Here's show.rhtml
(to display one task):
<h1><%= @page_title %></h1> <p>(<%= datetime_span(@task.start, @task.end) %>)</p> <%= content_tag('p', @task.description) if @task.description %> <p><%= show_complete(@task) %> | Owner: <%= @task.user.username %></p> <p><%= link_to 'Edit', :action => 'update', :id => @task %> | <%= link_to 'Delete', :action => 'delete', :id => @task %> | <%= link_to 'Back to index', :action => 'index' %></p>
Note that the above template calls a helper method in app/helpers/application_helper.rb
called datetime_span
(first highlighted section). This helper displays a start and (optionally) an end date/time in human-readable form; if both are present," to " is placed between them:
module ApplicationHelper # ... other helpers ... # Display a start/end datetime span in human readable form. If # both are given, ' to ' is placed in the middle of the string. # # +start_datetime+ is a Datetime instance, # +end_datetime+ is optional. def datetime_span(start_datetime, end_datetime=nil) str = start_datetime.strftime('%Y-%m-%d@%H:%M') if end_datetime str += ' to ' + end_datetime.strftime('%Y-%m-%d@%H:%M') end str end end
Another helper, show_complete
(from app/helpers/tasks_helper.rb
, as it works with a task's complete
attribute and is thus specific to tasks) is used to display the complete
status of the task. If the task is complete, this helper returns the string "Complete"; if it is incomplete, the helper returns a<span>
tag with a class
attribute set to "exception" and content "Incomplete". When rendered in the browser, any incomplete tasks appear with a red "Incomplete" message:
module TasksHelper def show_complete(task) if task.complete? 'Complete' else content_tag('span', 'Incomplete', :class => 'exception') end end end
Here's the new.rhtml
template:
<% form_for :task, @task, :url => {:action => 'create'} do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <% end %>
And the template for edit.rhtml:
<% form_for :task, @task, :url => {:action => 'update', :id => @task.id} do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <% end %>
Finally, the most complicated template is _form.rhtml
(called by both edit.rhtml
and new.rhtml)
. This follows the pattern of previous forms, like the one we created for people in Chapter 5 (Creating a Person). As the form needs to show both the owner of the task and the person associated with the task, we first need to retrieve the system users and people to populate the two drop-downs in the TaskController
. We achieve this with get_users
and get_people
methods, which are called using a before_filter:
class TasksController < ApplicationController before_filter :get_people, :only => [:edit, :update, :new, :create] before_filter :get_users, :only => [:edit, :update, :new, :create] # ... other methods ... private def get_people @people = Person.find_all_ordered end private def get_users @users = User.find(:all, :order => 'username') end end
With all the required data made available by the controller, we can now create the form itself in _form.rhtml:
<h1><%= @page_title %></h1> <p>Required fields are marked with "*".</p> <p><%= label :task, 'Title', :required => true %> <%= f.text_field :title %> <%= error_message_on :task, :title %></p> <p><%= label :task, 'Description' %><br/> <%= f.text_area :description, :rows => 5, :cols => 30 %></p> <p><%= label :task, 'Owned by user', :field_name => 'user' %> <select name="task[user_id]"> <%= options_from_collection_for_select @users, :id, :username, session[:user].id %> </select> <%= error_message_on :task, :user %></p> <p><%= label :task, 'Associated with person', :field_name => 'person' %> <%= f.collection_select :person_id, @people, :id, :full_name, :include_blank => true %> <%= error_message_on :task, :person %></p> <p><%= label :task, 'Complete' %> <%= f.check_box :complete %></p> <% this_year = Time.now.year -%> <p><%= label :task, 'Start', :required => true %> <%= f.datetime_select :start, :start_year => this_year - 5, :end_year => this_year + 5 %> <%= error_message_on :task, :start %></p> <p><%= label :task, 'End' %> <%= f.datetime_select :end, :start_year => this_year - 5, :end_year => this_year + 5, :include_blank => true, :default => nil %></p> <p><%= submit_tag 'Save' %> | <%= link_to 'Cancel', :action => 'index' %></p>
Most of this should be self-explanatory (if a little dense). Two areas of code, which may be unfamiliar, are highlighted:
options_from_collection_for_select
method to create the options for the owner drop-down. As we want to specify a default selected option for the owner attributed (set to the logged-in user), we can't use collection_select
, as this method does not allow a default selected option to be supplied. check_box
method creates an HTML<input type="checkbox" ...>
element, which can be set using a Boolean attribute on a model (in our case, the complete
attribute).This completes the basic CRUD controller and views for tasks. To try them out, navigate to: http://localhost:3000/tasks
, create a few tasks, then show, edit, and delete them, to test out all of the actions. Finally, ensure that you add a few tasks to the database so that you have some data to work with in the next section.
To be really useful, Intranet should go beyond these simple views and show tasks attached to a person. This is the context in which tasks are going to be used, so it makes sense to show them with a person's details, rather in an isolated "task administration" area. To accomplish this with whom we need to embed a list of records (tasks) inside their "parent" record's display (the person, the tasks are associated). This is slightly different from what we've done previously, where we've showed a parent record and one associated record simultaneously: for example, in our form, which enabled editing a person and their address at once (see: Editing Multiple Models Simultaneously in Chapter 5). Instead, we now need a way to show multiple tasks associated with a person, and enable users to easily add new tasks or edit/delete the existing ones in the list. The next section describes how to do this.
As tasks are managed in the context of a person, the obvious thing to do is to display them alongside a person's details in app/views/people/show.rhtml
. We'll do this by separating the template into two<div>
elements: one containing the person's details (the current content of the show.rhtml
template), and the other containing the list of tasks associated with them.
First, wrap the entire content of the app/views/people/show.rhtml
template in a<div>
element with id="left_panel":
<div id="left_panel"> <h1><%= @page_title %></h1> <p><strong>Job title:</strong> <%=d @person.job_title %></p> ... <p><%= link_to 'Edit', :action => 'update', :id => @person %> | <%= link_to 'Delete', :action => 'delete', :id => @person %></p> </div>
Next, add the new<div>
element (to hold the task list) at the bottom of the template with id="right_panel":
...
<%= link_to 'Delete', :action => 'delete', :id => @person %></p>
</div>
<div id="right_panel">
<h1>Tasks</h1>
</div>
Now we add some CSS styling (in public/stylesheets/base.css)
to position the two<div>
elements alongside each other:
#left_panel { float: left; width: 60%; } #right_panel { float: right; width: 39%; top: 0em; position: relative; background-color: #EEE; padding-left: 1%; }
Browse to the details for a person to check that the layout is as expected. For example, in Firefox the page looks like this:
Notice the area on the right for listing tasks. Now, create a partial app/views/tasks/_task.rhtml
to show a single task for a person. (As a starting point, you can copy the full show.rhtml
template.) We'll use this partial once for each task associated with a person, and render the results inside the<div id="right_panel">
element of the person's show template. Here's what the task partial looks like:
<div class="task"> <p><strong><%= task.title %></strong></p> <p>(<%= datetime_span(task.start, task.end) %>)</p> <%= content_tag('p', task.description) if task.description %> <p><%= show_complete(task) %> | Owner: <%= task.user.username %></p> <p><%= link_to 'Edit', :controller => 'tasks', :action => 'edit', :id => task.id %> | <%= link_to 'Delete', :controller => 'tasks', :action => 'confirm', :id => task.id %></p> </div>
<div>
element with class="task"
. This will make it easy to style each task when displayed in a list. @task
instance variable into references to a local task
variable. This is because we're going to be calling the partial from inside the show.rhtml
template for the PeopleController
, and setting the task
variable once for each of the person's tasks.<p>
element containing the task title. link_to
, pass a :controller => 'tasks'
option, as deletions and updates will be managed by the TasksController
(not the PeopleController
, which is the context in which the template is rendered).To render a person's tasks inside app/views/people/show.rhtml
, edit the bottom few lines of the template to look like this:
<div id="left_panel">
<h1>Tasks</h1>
<p><%= link_to 'Add a new task', :controller => 'tasks', :action => 'new', :default_person_id => @person.id %>
<% tasks = @person.tasks -%>
<% if tasks.empty? -%>
<p><strong>No tasks are associated with this person</strong></p>
<% else -%>
<% for task in tasks -%>
<%= render :partial => 'tasks/task', :locals => {:task => task} %>
<% end -%>
<% end -%>
</div>
Points to note:
default_person_id
option. This means that when creating a new task, we can associate the person with it by default. _task.rhtml
partial once for each task. _task.rhtml
partial is referenced using'tasks/task'
, as we are rendering it from within the PeopleController
. Therefore, we need to specify the "absolute" path of the partial (relative to app/views)
. task
is passed into the rendered partial.However, if you try to display a person's details at this point, you get this error:
undefined method 'show_complete' for #<#<Class:0xb72309f8>:0xb72309d0>
(NB you will get a different<Class>
string.) Why is our show_complete
helper causing this error to be thrown? Remember, we are trying to render a task in the context of PeopleController
. By default, the PeopleController
class only knows about application-level helpers (defined in app/helpers/application.rb)
and its own helpers (defined in app/helpers/people_helper.rb)
; and show_complete
is a task helper (in app/helpers/tasks_helper.rb)
, making it inaccessible to PeopleController
.
To use the task helpers inside the PeopleController
, simply add a line to the class definition (in app/controllers/people_controller.rb)
to include the TasksHelper
module in the controller:
class PeopleController < ApplicationController helper TasksHelper # ... other methods ... end
Now, when you display a person's details (provided they have an associated task), you should see something like:
This is close to what we want. One remaining issue is that the tasks are a bit spread out: we can afford to style them in a more compact way, so add a few more lines to public/stylesheets/base.css
to reduce the white space between the paragraphs:
.task { padding: 0.5em 1em 0.5em 1em; margin-top: 0.5em; font-size: 0.9em; } .task p { margin: 0em; }
This gives us a slightly better layout (notice how the tasks list is more compact):
The application now lists the tasks associated with a person alongside the person's details. When a user clicks on an Edit link for a task, or Add a new task, they are taken through to the tasks controller and the appropriate action. However, once their edits are completed, they are redirected back to the tasks index, rather than to the person associated with the task.
What we need to do instead is redirect the user back to the show
action for the person associated with the task instead of to the list
action. To do this, we first need to alter the create and update methods in the TasksController
class, which otherwise just redirect to the TasksController
index page:
class TasksController < ApplicationController # ... other methods ... def create @task = Task.new(params[:task]) if @task.save flash[:notice] = "Task added successfully" redirect_to_person @task.person_id else @page_title = "Adding new task" render :action => 'new' end end def update if @task.update_attributes(params[:task]) flash[:notice] = "Task added successfully" redirect_to_person @task.person_id else @page_title = "Edit " + @task.title render :action => 'edit' end end private def redirect_to_person(person_id) if person_id redirect_to :controller => 'people', :action => 'show', :id => person_id else redirect_to :action => 'index' end end end
Rather than redirecting to the index action, these two actions now call the redirect_to_person
method, defined as a private method on this controller. If a person has been assigned to the task, a redirect to that person's record is performed; if not, the tasks index is displayed instead. Now, if you create or update a task, you will be redirected back to the associated person's record when the save completes. If the task doesn't validate, you'll see the form again with validation errors, as per usual.
In the next few sections, we'll see how to really polish up the redirections, including redirecting correctly after deletions, handling the Cancel link, and setting a default person for new tasks.
Alternatives to the "external edit with redirect" approach
While we took the approach of editing a person's tasks on a separate page then redirecting back to the person's details, there are a couple of other approaches we could have taken instead.
Integrated forms This is the approach we took with addresses: we incorporated the address form into the person (and company) form. However, we were only dealing with a single address at a time there. In the case of people and tasks, we could potentially have multiple tasks to edit, and we don't want to display an editing form for each individual task.
In-place editors We could attach an in-place editor to each task so that it becomes editable in the page when clicked (see Chapter 7 Improving the User Experience for examples of in-place editing). This is an elegant solution, as the user never leaves the page, so there are no lengthy round-trips to the server. However, it only works if AJAX is available.
The advantages of the external edit with redirect approach (used here) are that it requires no JavaScript, but remains reasonably clean to implement, as we don't need a separate edit form for each task.
Recall the actions we added to ApplicationController
, to manage generic deletions for any controller (see A Shared View to Confirm Deletions in Chapter 5). While they work fine, they assume that after a deletion we want to return to the index
action of the controller. In the current case, we would prefer to redirect back to the PeopleController's show
action, for the person whose task we just deleted.
The solution is to make the do_delete
(in app/controllers/application.rb)
action more generic, so that it will accept a hash which specifies a URL to redirect to:
class ApplicationController < ActionController::Base # ... other methods ... private def do_delete(object, redirect_options=nil) if 'yes' == params[:confirm] object.destroy object_name = object.class.name.humanize flash[:notice] = object_name + ' deleted successfully' redirect_options ||= {:action => 'index'} redirect_to redirect_options end end end
The changes are highlighted. Note how we can now pass in a redirect_options
argument, which defaults to nil
. Within the method, we set redirect_options
to a hash just containing {:action => 'index'}
if it hasn't been passed in as an argument (that's what ||=
does). Finally, we use redirect_to
and pass it this hash. Note that this change doesn't break any of the previous calls to this method in other controllers, as we set a default for the redirect_options
parameter (nil) in the method definition.
To use this in the TasksController
, we just modify the delete action so that it redirects back to the person associated with the task just deleted:
class TasksController < ApplicationController # ... other methods ... def delete redirect_options = { :controller => 'people', :action => 'show', :id => @task.person_id } do_delete(@task, redirect_options) end end
The Cancel link available when creating or updating a task should return the user to that person's details. We can manage this by redirecting to a person's details if the task has been assigned to a person, or to the tasks index if not. Edit the Cancel link in app/views/tasks/_form.rhtml:
<p><%= submit_tag 'Save' %> | <% if @task.person_id cancel_url_options = {:controller => 'people', :action => 'show', :id => @task.person_id} else cancel_url_options = {:action => 'index'} end -%> <%= link_to 'Cancel', cancel_url_options %></p>
The last step is a small one, but can make quite a difference to usability. What we'll do is set a default person for new tasks, based on the ID of the person whose tasks we're editing.
This is actually very simple, and just requires a small change to the TasksController's new
action (highlighted):
class TasksController < ApplicationController
# ... other methods ...
def new
@page_title = "Adding new task"
@task = Task.new
if params[:default_person_id]
@task.person = Person.find(params[:default_person_id])
end
end
end
If a default_person_id
has been supplied in the querystring, the appropriate Person
instance is retrieved from the database and assigned to @task
(via the person=
method). Then, when the form for creating a task is displayed, the person is pre-selected from the Associated with person drop-down box, as the person has already been assigned to the task. If a task is being created without a person ID specified, the drop-down box for selecting the person defaults to the blank option at the top.
This solution is still not perfect. For example, if you update a person's details, their tasks aren't even mentioned. It may be necessary to include the task listing in the edit.rhtml
template, as well as in show.rhtml
. Also, every time we edit, delete, or add a task we are redirected back to the record of the person associated with the task: in some cases, we may just want to bulk-edit tasks, and redirect back to the task index instead. These refinements are possible, but the system we've built so far covers the common case, where tasks are edited in the context of a person.
However, the simple task manager we've built is fairly flexible and intuitive for users. More importantly, writing it has enabled us to explore techniques for managing a list of child objects from inside their parent, without adding too much complexity and retaining cross-browser compatibility. This is a common use case, and one for which there is little guidance elsewhere.
3.142.199.184