Fleshing Out Companies and Addresses

The Intranet application is missing some functionality before it will be really useful. It still needs to:

  • Provide create, update, and delete actions for companies (we already have an index view for companies, which we can continue to use).
  • Enable users to associate a person's company (from the person update and create views).
  • Delete addresses if they are no longer attached to a person or company.

In this section, we will briefly see how to implement this functionality. The techniques should be familiar from the previous sections, but extra detail is given for any new techniques.

Managing Companies

The remaining actions we need for companies are delete, new, create, edit, and update. Action methods go into app/controllers/companies_controller.rb; views go into app/views/companies.

Stubbing Out the Navigation

We want the index view to link to the edit and delete actions for each company. Add a new Action column to app/views/companies/index.rhtml, and add two links for each company row:

<td>
<%= link_to 'Edit', :action => 'update', :id => company.id %> |
<%= link_to 'Delete', :action => 'delete', :id => company.id %>
</td>

At the moment, these links don't lead anywhere; but having them available makes it easier to find your way around while building up the remaining functionality.

A Shared View to Confirm Deletions

It turns out that the view and action for confirming company deletion are virtually identical to those for confirming deletion of a person. Here is another refactoring opportunity.

First, move app/views/people/confirm.rhtml to app/views/shared/confirm.rhtml. This makes the view for confirming a deletion into a shared template, easily usable by any controller. We need to make this view generic, so it will work with any object (currently, it references @person). Modify it like this (@object replaces @person in the highlighted line):

<h1><%= @page_title %></h1>
<% form_for :action => 'delete', :id => @object.id do %>

<%= hidden_field_tag 'confirm', 'yes' %>
<p><%= submit_tag 'Yes' %> |
<%= link_to 'Cancel', request.referer %> </p>
<% end %>

Next, modify the confirm and delete actions in app/controllers/people_controller.rb, extracting the code that is common to those actions for any controller into separate private methods (confirm_delete and do_delete):

def confirm
prompt = "Do you really want to delete #{@person.full_name}?"
confirm_delete(@person, prompt)
end
def delete
do_delete(@person)
end
private
def confirm_delete(object, prompt)
@object = object
@page_title = prompt
render :template => 'shared/confirm'
end
private
def do_delete(object)
if 'yes' == params[:confirm]
object.destroy
object_name = object.class.name.humanize
redirect_to_index object_name + ' deleted successfully'
end
end

Now the confirm_delete method manages confirmation of the deletion of any generic object, and the confirm action in PeopleController just invokes this method, passing in the @person instance variable and @person.full_name as a prompt. The prompt is displayed in the confirmation screen, which is constructed by rendering the shared/confirm.rhtml template. (The :template option will render any template, including ones for controllers other than the one currently being invoked.)

The delete action calls the do_delete method, which in turn calls the object's destroy method; a bit of class reflection is used to create the message for the flash (#{object.class.name.humanize} yields a human-readable version of the class name of the object).

Move the confirm_delete and do_delete methods into the ApplicationController class so they are available to every controller.

Finally, add delete and confirm actions to CompaniesController too, referencing the generic confirm_delete and do_delete methods:

class CompaniesController < ApplicationController
# ... other methods ...
def confirm
@company = Company.find(params[:id])
prompt = "Do you really want to delete #{@company.name}?"
confirm_delete(@company, prompt)
end
def delete
@company = Company.find(params[:id])
do_delete(@company)
end
end

The final thing to do is add some delete links to the app/views/companies/index.rhtml template:

<%= link_to('Delete', :action => 'confirm', :id => company.id) %>

The beauty of this refactoring is that it makes it trivial to add delete and confirm actions to any controller from this point on. The confirm action should pass the object we're trying to delete plus some prompt to the confirm_delete method; the delete action should call do_delete with the object to delete; and Rails will do the rest.

Attaching a Person to a Company

A likely scenario would be for a person to change company: someone at Acme would then search for a person's record to modify their company association. Adding this functionality to the person edit form is actually very simple, as we are simply setting the company_id for a record in the people table. As this is a simple attribute, we merely need to add a drop-down box of companies to the form for editing a person. First, we need to get the list of companies to populate the drop-down by adding another before_filter for the PeopleController's edit, new, update, and create actions (any action that is expected to render the form):

class PeopleController < ApplicationController
before_filter :get_companies, :only => [:edit, :new, :update,
:create]
# ... other methods ...
private
def get_companies
@companies = Company.find(:all, :order => 'name')
end
end

Then add the drop-down box to the app/views/people/_form.rhtml partial, before the address entry part of the form:

<h2>Company (optional)</h2>
<p><%= f.collection_select(:company_id, @companies, :id, :name, :include_blank => true)
%></p>

The collection_select helper creates a<select> element around the output from options_from_collection_for_select. As a company is optional for a person, the :include_blank option is set to true so that a blank option is displayed at the top of the drop-down. Rails will now manage this attribute as it manages the other simple attributes for a person (like first_name, last_name, etc.).

We can also amend the show view for a person (in app/views/people/show.rhtml) to display the name of the company they work for:

<p><strong>Company:</strong>
<%= (@person.company ? @person.company.name : d) %></p>

Creating and Updating Companies

The basic actions for creating or updating a company (without its address) are similar to the initial actions created earlier in this chapter, and fairly trivial. We'll skip those and go straight to the more complex case: creating or updating a company and an address from a single form.

Unlike a person, where the address is optional, a company must always be assigned an address. The validation code on the Company model we wrote in Chapter 4 ensures that an address must be supplied, and that the address is itself valid. So providing we assign an address and check the validity of the company, Rails will cascade validation to the associated address. If the company validates, it means the address is valid too, and we can save both safely.

Below is the code for the new, create, edit, and update actions. It again uses the Address.from_street_1_and_post_code method we defined in the section Adding a New Address for a Person, to either find an existing address to assign to a company or create a new one from the supplied :address parameters:

class CompaniesController < ApplicationController
def new
@page_title = 'Add a new company'
@company = Company.new
@address = Address.new
end
def create
@company = Company.new(params[:company])
@company.address = Address.from_street_1_and_post_code(params[ :address])
# @company.address might be nil,
# so set a sensible default if it is
@address = @company.address || Address.new
if @company.save
redirect_to_index 'Company added successfully'
else
@page_title = 'Add a new company'
render :action => :new
end
end
def edit
@company = Company.find(params[:id])
@page_title = 'Editing ' + @company.name
@address = @company.address
end
def update
@company = Company.find(params[:id])
@company.address = Address.from_street_1_and_post_code(params[ :address])
@address = @company.address || Address.new
if @company.update_attributes(params[:company])
redirect_to_index 'Person updated successfully'
else
@page_title = 'Editing ' + @company.name
render :action => 'edit'
end
end
end

ActiveRecord handles validation of the address associated with the company, so we can be certain that @company.save only succeeds when both the company and its address are valid.

Here is the form partial, which goes with these actions (app/views/companies/_form.rhtml):

<h1><%= @page_title %></h1>
<p>Required fields are marked with &quot;*&quot;.</p>
<% if @company.errors[:address] -%>
<p><%= error_message_on :company, :address %></p>
<% end -%>
<p><%= label :company, 'Company name', :field_name => 'name',
:required => true %><br />
<%= f.text_field :name %>
<%= error_message_on :company, :name %></p>
<p><%= label :company, 'Telephone' %><br />
<%= f.text_field :telephone %></p>
<p><%= label :company, 'Fax' %><br />
<%= f.text_field :fax %></p>
<p><%= label :company, 'Website' %><br />
<%= f.text_field :website %></p>
<h2>Address*</h2>
<div id="address">
<h3>Enter address details</h3>
<%= render :partial => 'addresses/form', :locals => {:address => @address} %>
</div>
<p><%= submit_tag 'Save' %> | <%= link_to 'Cancel', :action => 'index' %></p>

Note that any validation errors to do with the company's address are at the top of the form (highlighted). If the address fails to validate, the error message will appear here.

We will also need a template for the new action (app/views/companies/new.rhtml):

<% form_for :company, @company,
:url => {:action => 'create'} do |f| %>
<%= render :partial => 'form', :locals => {:f => f} %>
<% end %>

and one for the edit action (app/views/companies/edit.rhtml):

<% form_for :company, @company,
:url => {:action => 'update', :id => @company.id} do |f| %>
<%= render :partial => 'form', :locals => {:f => f} %>
<% end %>

Finally, add a link to the menu (in app/views/layouts/application.rhtml) to create a new company:

<%= link_to 'Add a company', :controller => 'companies', :action => 'new' %>

That completes the functionality for adding and updating companies. Again, you may want to ensure that this works as expected by adding some functional tests for this controller: an example is given in the code repository for the book.

Managing Addresses

Earlier we saw that addresses don't really have a life of their own: they are always associated with a company or a person, and don't need to be treated as entities in their own right. So, it seems a wasted effort to spend too long writing administration pages for them (and time is running out for Rory and co.).

However, it is clear that addresses may become orphans if there are no longer companies or people associated with them. At the moment, there is no way to tidy up if a company is deleted and its address orphaned.

There are several ways to approach this issue. Here are a few suggestions:

  1. Create a scaffold for the Address model. This will quickly supply CRUD operations for the model, but won't make it simple to identify orphaned addresses without some modification (e.g. displaying entities associated with each address).
  2. Run a scheduled task to clear up the database. Using cron or similar, scan the database every evening for addresses that no longer have associated companies or people and delete them.
  3. Cascade deletions to addresses. Each time a person or company is deleted, check and delete any addresses that have become orphans as a consequence.

The last of these is the most interesting and tidiest: rather than managing addresses through their own administration screens and having to manually identify orphans, we can have them deleted automatically when they become orphans.

Note

This approach is not suitable if you want to maintain addresses regardless of whether they are associated with any other entities in the database.

Adding a Callback to Company Deletions

To manage addresses that no longer have an associated company or person, we an add a callback handler for the Person and Company model classes. Each time an instance of either is deleted, we check whether any associated address is orphaned as a result, and, if it is, delete it too.

A callback enables you to trigger events in response to actions on records in the database, such as a new record being added, a record being updated, or a record being deleted. There are about a dozen callback points available (see the documentation for ActiveRecord::Callbacks), but the one we're interested in is after_destroy, which enables you to specify an action to trigger after a record is destroyed. In this case, it will enable us to clear out orphaned addresses after a company or person is destroyed.

First off, create a callback handler in one of the two models—Company is as good as any. This is defined by creating a new method called after_destroy in the Company class definition:

class Company < ActiveRecord::Base
# ... validation methods etc. ...
def after_destroy
unless address_id.blank?
address = Address.find address_id
if address.people.empty? and address.company.nil?
address.destroy
end
end
end
end

Note that, despite the callback being triggered after the Company instance has been destroyed, it still has access to the attributes the model instance had before the record was destroyed. This means we can reference the company's address_id and use it to look up its associated address. The callback handler checks whether the potentially-orphaned address still has either an associated company, or one or more associated people, by querying the database. If the address is not associated with any other records, it can be safely deleted.

Rather than copying this callback handler into the Person class definition, it would be better to keep things DRY and have the handler in a single location. But where should we put it? In the case of controllers, we have the ApplicationController class we can use for any methods we want to make available to all controllers. However, we don't have a similar "superclass" for our models, so there is no obvious location to put the callback to make it accessible to both Person and Company.

Instead, we can use an observer as the location for the callback handler. Observers are special classes that wait for and then respond to lifecycle events on ActiveRecord classes, such as addition of new records, updates to records, or deletion of records. You specify which events trigger the observer by creating methods named after the type of event, such as the after_destroy method we just saw.

Either an observer can be assigned to a single model (in which case, you just need to create a class named after the model, e.g. a PersonObserver observer would observe events occurring on the Person model); or, you can create an observer with an arbitrary class name and instruct it to watch for events across multiple models using the observe method. We'll take the latter approach and write one observer that manages addresses in response to events occurring on the Person or Company model.

Locate the observer in app/models/address_owner_observer.rb and cut and paste the after_destroy callback method from the Company class into it. The other slight modification required is that the callback should accept an object to be manipulated (here called record): this will represent the just-deleted person or company. You also need to declare the class, inheriting from the ActiveRecord::Observer class:

class AddressOwnerObserver < ActiveRecord::Observer
observe Person, Company
def after_destroy(record)
unless record.address_id.blank?

address = Address.find address_id
if address.people.empty? and address.company.nil?
address.destroy
end
end
end
end

We need to add the new check on the address_id (highlighted), as an address is optional for people, but not for companies.

The final step is to "switch on" the observer by adding it to the application's configuration inside config/environment.rb. Note that there is already a commented-out line inside the configuration that sets the value for config.active_record.observers. Uncomment this line and set the value to the name of your observer, e.g. (highlighted)

Rails::Initializer.run do |config|
# ... other settings ...
# Activate observers that should always be running
config.active_record.observers = :address_owner_observer

# ... yet more settings ...
end

The observer now works as a callback handler for both model classes. The easiest way to see it in effect is to use the console:

$ script/console
Loading development environment.
>> a = Address.new(:street_1 => '78 Blink Street',
:post_code => 'B14 2QQ')
=> #<Address:0xb75c7b0c ...}>
>> a.save # Save the address to the database
=> true
>> c = Company.new(:name => 'Charming Pottery')
=> #<Company:0xb759ba84 ...}>
>> c.address = a # Associate the address with the company
=> #<Address:0xb75c7b0c ...>
>> c.save # Save the company to the database
=> true
>> c.address_id # ID of the address associated with the company
=> 16
>> c.destroy # The callback is triggered by this method call
=> #<Company:0xb759ba84 ...>
>> Address.find 16 # Try to retrieve the address from the database
ActiveRecord::RecordNotFound: Couldn't find Address with ID=16
...

As you can see, the callback handler deleted the new address we just created, in response to deletion of the company it was attached to.

Unit Testing for Callbacks

Unit tests are a good way to encapsulate the kind of callback checks we performed manually with the console (above), making sure that they behave as expected. For example, to test the this callback, the unit test could:

  1. Create an address
  2. Create a person
  3. Create a company
  4. Assign the address to the person
  5. Assign the address to the company
  6. Delete the company
  7. Verify that the address still exists in the database

Conversely, you could write a test to verify that an orphaned address is correctly deleted. For example:

# Test that the after_destroy call-back for Company
# correctly triggers deletion of an orphaned address.
def test_deletes_orphaned_address
@acme.destroy
assert_raise(ActiveRecord::RecordNotFound) { Address.find 1 }
end

See the source code repository (test/unit/company_test.rb) for example unit tests, which perform more callback testing.

A Very Quick Interface for Addresses

While we are managing addresses via their associated companies and people, it is sometimes useful to get an overview of all the addresses in the system. But, because we're doing most of the management in other pages, it seems a waste of effort to build a whole interface. One of the beauties of Rails is that we can very quickly add a management interface for a model, which we can flesh out later or just leave it as it is. We saw the scaffold generator in the last chapter, which adds all the files to implement a CRUD interface for a model. However, there is an even simpler way to implement a scaffold for a model (in our case, the Address model).

First, create a controller for the model:

$ script/generate controller addresses

Next, edit the controller (app/controllers/addresses.rb):

class AddressesController < ApplicationController
scaffold :address
end

The scaffold method builds all the actions for the controller invisibly: simply pass it the name of the model to scaffold for. It also generates the required views without creating any files in your application. If you create any actions with the standard scaffold names (e.g. create, new, edit, update) inside the controller, your method will override the default scaffold ones. Using the scaffold command inside your controller class definition can be a good first step to getting a controller up and running: any actions you haven't yet defined for your controller are provided by the scaffold defaults.

Next, start the server (if it's not already running) and browse to http://localhost:3000/addresses. You should see a list of addresses in the system. That's all you need to do to get a quick interface for addresses up and running; you could even add it to the menu system (though, remember that we set up companies as dependent on addresses: if you delete an address, any associated company is also destroyed).

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

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