Adding Simple Task Tracking

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.

The Task Model

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 task
  • description: A description of the task (optional)
  • user_id: The member of Acme staff who "owns" the task
  • person_id: The person (client) the task is associated with
  • complete: Whether the task is complete (true/false)
  • start: The start date and time for the task
  • end: 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.

The Tasks Controller

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.

Task Views

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 &quot;*&quot;.</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:

  1. The first highlighted section shows the use of 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.
  2. The 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.

Showing Tasks for a Person

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:

Showing Tasks for a Person

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>

The main changes were:

  1. Place the whole task inside a<div> element with class="task". This will make it easy to style each task when displayed in a list.
  2. Convert references to the @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.
  3. Replace the heading with a<p> element containing the task title.
  4. Remove the Back to index link (irrelevant in the context of a partial).
  5. When creating the Edit and Delete links with 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:

  • A link to add a new task is shown in a paragraph just under the Tasks heading. Note that this link includes the person's ID as the default_person_id option. This means that when creating a new task, we can associate the person with it by default.
  • If a person has no associated tasks, an explanatory message is displayed; if the person does have associated tasks, they are rendered by a loop, which runs the _task.rhtml partial once for each task.
  • The _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).
  • On each iteration, the current 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:

Showing Tasks for a Person

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):

Showing Tasks for a Person

Redirecting to a Person after Adding or Editing a Task

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.

Note

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.

Redirecting after a Deletion

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

Handling the Cancel Link

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>

Setting a Default Person for a New Task

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.54.136