The Intranet application is missing some functionality before it will be really useful. It still needs to:
create, update
, and delete
actions for companies (we already have an index
view for companies, which we can continue to use). update
and create
views).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.
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
.
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.
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.
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>
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 "*".</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.
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:
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.
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 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:
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.
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).
3.144.47.218