Editing Multiple Models Simultaneously

So far, we have dealt with the simple case where a controller manages create, update, and delete operations for a single instance of the Person model at a time. However, this is only part of the story.

Each person also optionally has an associated address, stored in the addresses table. We separated addresses from people, as companies also have addresses: it is good practice to store all instances of a certain type of data in a single table. In the case of Intranet, this makes searching for an address far easier, as we only have to search over a single table in the database. On the other hand, it makes managing addresses tougher, as we potentially have two controllers (PeopleController and CompaniesController) that can create and modify Address instances.

While this use case is not rare, it is difficult to find good examples of implementations in the wild. Hopefully, we will address (excuse the pun) this deficiency in the next section. We'll implement a single form, which will enable a user to both add a new person to the database and optionally, at the same time, add a new address.

Adding a New Address for a Person

To insert an address at the same time as we insert or edit a person's record, we need to add some new address fields at the bottom of app/views/people/_form.rhtml. The user can use these fields to enter the address at the same time as they enter a person's details. As we're going to need these address fields when creating companies too, we'll put them into a partial, app/views/address/_form.rhtml, so we can reuse them more easily:

<% if address.errors[:base] -%>
<p><%= error_message_on :address, :base %></p>

<% end -%>
<p><%= label :address, 'Street 1', :required => true %><br />
<%= text_field :address, :street_1 %>
<%= error_message_on :address, :street_1 %></p>

<p><%= label :address, 'Street 2' %><br />
<%= text_field :address, :street_2 %></p>
<p><%= label :address, 'Street 3' %><br />
<%= text_field :address, :street_3 %></p>
<p><%= label :address, 'City' %><br />
<%= text_field :address, :city %></p>
<p><%= label :address, 'County' %><br />
<%= text_field :address, :county %></p>
<p><%= label :address, 'Post code', :required => true %><br />
<%= text_field :address, :post_code %>
<%= error_message_on :address, :post_code %></p>

A few points to note about this form:

  • We are not referencing an @address instance variable when creating the form fields here, but use address instead. We'll pass this address argument in as a local variable (through the :locals option) when we render the partial.
  • We are not going to create a new form to contain the address, but simply place the fields relating to the address inside a surrounding form. In the current case, this will put the address fields inside the person form. This means we can submit data for the person and the address simultaneously.
  • We are using the text_field helpers and specifying :address as the first argument to each method call. This will yield a set of form elements with names in the format address[<field_name>], where<field_name> is the name of a field in the addresses table.
  • The label helper we developed earlier in this chapter is used throughout.
  • The highlighted parts of the code are where validation messages will be shown. Note that the top error message references :base, which is not a field associated with an Address instance. In fact, this refers back to a generic error message we set if there is a validation error on the whole instance; namely, if street_1 and post_code reference an existing address (see the section Validating Addresses in Chapter 4). This message is shown at the top of the form, as it applies to the whole address, and not just to a single field.

Next, we pull this form into the main form for adding a new person (app/views/people/_form.rhtml):

...
<p><%= label :person, 'Notes' %><br />
<%= f.text_area :notes %></p>
<div id="address">
<h2>Enter address details (optional)</h2>
<%= render :partial => 'addresses/form',
:locals => {:address => @address} %>
</div>

<p><%= submit_tag 'Save' %></p>

The new lines are highlighted. These just render the new partial inside a<div> element, passing a local :address variable to the app/views/addresses/_form.rhtml partial. At the moment, :address references an instance variable, @address, which we haven't set yet. Let's do that in the controller (app/controllers/people_controller.rb). While we're doing this, let's add some code to save a person's address and assign it to the person, too:

class PeopleController < ApplicationController
# ... other actions ...
def new
@page_title = "Add a new person"
@person = Person.new
@address = Address.new

end
def create
@person = Person.new(params[:person])
@person.build_address(params[:address])

if @person.save
redirect_to_index 'Person added successfully'
else
@page_title = "Add a new person"
@address = @person.address

render :action => 'new'
end
end
end

We're just creating a new Address instance here, and building it from the :address part of the request parameters (which gathers all the form fields whose names begin with "address" into a hash), using the build_address method automatically added by the association (see the section Associations between Models in Chapter 4). As we're creating a new Person, Rails will save the address to the database when we save the person associated with it.

This works perfectly well if both the person and address validate first time. However, we get problems if we want to add a person without an address. Our code won't let us do this, as it always tries to save the address, which sometimes we don't want to fill in (e.g. if we don't have someone's home address).

To fix this, we'll add a class method to the model, from_street_1_and_post_code, which will give us an address, depending on whether the user has supplied the street_1 and post_code parameters; in cases where they haven't supplied a street_1 or post_code, it returns nil. The method utilizes the Rails find_or_initialize_by_* method to either retrieve an existing address or initialize a new one (without saving it). This method is similar to the find_by_* methods discussed in Chapter 4 (in the section Finding Records Using Attribute-Based Finders), and can be passed multiple fields to initialize a record. There are also find_or_create_by_* methods available to models, which will additionally save records they create. from_street_1_and_post_code also updates the attributes of the retrieved or initialized object from a hash of parameters passed to the method:

class Address < ActiveRecord::Base
# ... other methods ...
# Look up or initialize an address from params
# if street_1 or post_code supplied;
# otherwise return nil; NB does not save the address.
#
# +params+ is a hash of name/value pairs used to set
# the attributes of the Address instance.
def self.from_street_1_and_post_code(params)
params ||= {}
street_1 = params[:street_1]
post_code = params[:post_code]
if street_1.blank? and post_code.blank?
address = nil
else
address = find_or_initialize_by_street_1_and_post_code(street_1, post_code)
address.attributes = params
end
address
end
end

We then modify the create action to use this new model method:

class PeopleController < ApplicationController
# ... other actions ...
def create
@person = Person.new(params[:person])
@person.address = Address.from_street_1_and_post_code(params[ :address])
@address = @person.address || Address.new
if @person.save
redirect_to_index 'Person added successfully'
else
@page_title = "Add a new person"
render :action => 'new'
end
end
end

If no street_1 or post_code parameters are in the request, Address.from_street_1_and_post_code returns nil, and the person's address is set to nil; however, we still need a valid Address instance for use in the view. So, in the second highlighted block, we set the @address instance variable to the person's address; or, if it is nil, to a new Address.

When @person.save is called, the validity of the new person record is checked. Recall from Chapter 4 that a person's address is only valid if it is nil or a valid Address instance. If the person is valid and their address is valid or nil, both will be saved; otherwise, neither is.

Try adding some new people to the application, with valid person and address details, with valid person details only, with valid address details only, and with invalid person and address details. You should only see validation error messages on the address if either street_1 or post_code is set; otherwise, the person should be added without an address (providing the person fields validate).

Using Functional Testing for Complex Actions

The actions defined in the previous section are sufficiently complex to feel nervous about. We need to be sure that the controller responds correctly to different combinations of request parameters: street_1 set, but post_code not; valid address, but invalid person; and so on. In Chapter 4, we saw how unit testing can be used to codify expectations about how models should validate. Functional testing can be used in a similar way for controllers, to store expectations about how they should work and ensure that those expectations aren't broken by changes to the code.

Functional tests effectively interact with the application in the same way that a client browser does, making requests to controller actions and receiving responses; the testing occurs on the responses, where we can check that the correct response codes were received, the response body contained the correct HTML, validation is managed correctly, the client was redirected correctly, and so on.

Each time you generate a controller, Rails adds a functional test skeleton for it to the test/functional directory, with the name<controller name>_controller_test.rb. To write functional tests for the PeopleController class, for example, we need to modify test/functional/people_controller_test.rb. Open this file and delete the test_truth method (which is just a stub to demonstrate the format of testing methods).

We want to test expectations about how the PeopleController's create action should respond to different request parameters, as outlined in the table below:

Person parameters

Address parameters

Expectation

Test method to create

Invalid

Any

Person not created; address not created; form displayed again

test_create_bad_person

Valid

No street_1 and no post_code

Person created with nil address; redirected to index

test_create_person_nil_address

Valid

post_code, but no street_1

Person not created; validation errors returned for the address; form displayed again

test_create_bad_street_1

Valid

street_1, but no post_code

Person not created; validation errors returned for the address; form displayed again

test_create_bad_post_code

Valid

Valid (street_1 and post_code both supplied)

Person and address both created successfully; redirected to index

test_create_person_address

Note that we're mapping each expectation to be tested onto a separate test method. For functional testing, as for unit testing, the test methods are named test_*, a special name which Ruby's testing framework uses to identify methods to include in the test suite. Aside from the requirement to be prefixed with test_, you can give your methods any name you wish: here, the method name includes the name of the action being tested (create) and bad to denote cases where we're testing against invalid request data.

Here's how we code the first test, test_create_bad_person. Add the test inside the PeopleControllerTest class definition in test/functional/people_controller_test.rb:

# Test the create action with bad request parameters
def test_create_bad_person
# Send a post request to the create action with no parameters
post :create
# The response should be rendered using the people/new template
assert_template 'people/new'
# The response should contain a div with class 'formError'
assert_select 'div[class=formError]'
end

Some points to note:

  • The post method used sends a POST request to the specified action on this controller. There is also a get method to send a GET request. Both can be supplied with a parameters hash (see later in this section).
  • assert_template can check whether an action renders a particular template, specified relative to the views directory. Here we make sure that the PeopleController's new template (which shows the form for adding a new person) is rendered.
  • assert_select is a very powerful method for checking the content of the response body. It can be supplied with very fine-grained selectors (similar to CSS or XPath selectors), which attempt to find a matching element in the response body.

Note

Other assert_* methods can also be used inside functional tests: see the section Other Types of Assertion in Chapter 4 for a full list of methods available.

To run the functional tests, call the following from the command line:

$ rake test:functionals
...
Started
..
Finished in 0.103625 seconds.
2 tests, 3 assertions, 0 failures, 0 errors

Any failed tests or errors are reported, along with the details of the test where they occurred. Note that we've run two tests here, as the CompaniesController also has a stub for its functional tests.

Next is test_create_person_nil_address, to test that a POST request with valid person parameters, but no address parameters, correctly creates a person with a nil address:

def test_create_person_nil_address
# Clear out the people table
Person.delete_all
# Post new person details with blank address
post :create, :person => {
:first_name => 'Bob',
:last_name => 'Parks',
:email => '[email protected]',
:gender => 'M'
}
# Check there is one person in the database
person = Person.find(:first)
assert_equal '[email protected]', person.email
# Check their address is nil
assert_equal nil, person.address
# Check we get redirected to the index after creation
assert_redirected_to :action => :index
end

Here we're sending some data in the POST body by passing a hash of parameters to the post method. Note that you have to mirror the structure of the request as it would arrive from the form, meaning that you have to nest all of the person parameters inside a nested hash, keyed by :person. Also note that we can use assert_redirected_to to specify a route we expect the controller to redirect to; the parameters passed to this mirror those used with link_to and url_for. In this case, if a person's record is successfully created, we expect to be redirected back to the index action on this controller.

Similarly, to test requests where partial (invalid) address parameters are supplied, we do:

def test_create_bad_street_1
# Post valid person details, but with empty value for street_1
post :create,
:person => {
:first_name => 'Bob',
:last_name => 'Parks',
:email => '[email protected]',
:gender => 'M'
},
:address => {
:post_code => 'B15 1AU'
}
# Check the new form is shown again
assert_template 'people/new'
# Check we get validation errors for street_1 input element
assert_select 'div[class=fieldWithErrors]' do
assert_select 'input[id=address_street_1]'
end
end

Note that this follows a similar format to the previous test, but that we are also passing an :address key in with the post parameters, specifying an invalid address (missing a street_1 attribute). We then use assert_select to test for the presence of a<div> element with class attribute equal to fieldWithErrors; and nest a further assert_select inside it, to check that the error<div> is wrapping the<input> element with id attribute set to address_street_1. This tests that the form is displayed again, with an error message next to the street_1 form field.

Note

assert_select statements can be nested arbitrarily deep in tests, and selectors can be far more complex than shown here: see the Rails documentation for the HTML::Selector class for details of the full syntax.

test_create_bad_post_code is similar to the previous test, and is not listed here: see the source code for the complete listing.

Our final test, test_create_person_address, checks that if valid person and address data are supplied, both a person and an address are created and correctly associated:

def test_create_person_address
# Clear out the people and addresses tables
Person.delete_all
Address.delete_all
# Send post with valid person and address
post :create,
:person => {
:first_name => 'Bob',
:last_name => 'Parks',
:email => '[email protected]',
:gender => 'M'
},
:address => {
:street_1 => '11 Harley Street',
:post_code => 'B15 1AU'
}
# Check person created
person = Person.find(:first)
assert_equal '[email protected]', person.email
# Check address created
address = Address.find(:first)
assert_equal 'B15 1AU', address.post_code
# Check person's address is the created address
assert_equal person.address_id, address.id
# Check redirected to index
assert_redirected_to :action => :index
end

This test uses many of the same methods as previous tests. The main things we're testing here are that the person and address are created, and that the address_id attribute for the person is set to the id of the newly-created address (highlighted).

This section has been a whirlwind tour of the potential of functional testing, and we've only skated over the surface of its possibilities. As you can see, it can be time-consuming to test every possible combination of request parameters; however, where you are writing mission-critical software, or software where the controller logic is complex, functional testing is a vital technique for ensuring consistency and stability in your application.

Updating a Person and Their Address

Luckily for us (thanks to Rails), the code for editing an existing person and their address is virtually identical to the code for creating a person. The chief difference is that the edit and update actions retrieve an existing person and their address (if available):

class PeopleController < ApplicationController
before_filter :get_person, :only => [:show, :update,
:edit, :confirm, :delete]
verify :method => :post, :only => [:create, :update,
:delete], :redirect_to => {:action => :index}
# ... other methods ...
def edit
@page_title = 'Editing ' + @person.full_name
@address = @person.address || Address.new
end
def update
@person.address = Address.from_street_1_and_post_code(params[:address])
@address = @person.address || Address.new
if @person.update_attributes(params[:person])
redirect_to_index 'Person updated successfully'
else
@page_title = 'Editing ' + @person.full_name
render :action => 'edit'
end
end
end

The only minor issue with this code is that an edit to someone's address will actually add a new address to the database, rather than update the old one. The old one will remain until manually removed from the system. However, it does mean that if someone enters an address that already exists in the database, that address is used instead of creating a new one. There is also the potential for creation of duplicate or near-duplicate addresses, if there are slight differences between how street_1 and post_code are typed.

Finally, we need an app/views/people/edit.rhtml template to present a form for editing a person, which pulls in the existing _form.rhtml partial we created earlier:

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

Summary

The functionality put in place throughout this section gives users all the tools they need to edit people and their addresses. However, this doesn't complete the functionality required in the Intranet application: the next section covers how to build the remaining pieces.

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

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