Home > Articles

Sign Up

This chapter is from the book

7.3 Unsuccessful Signups

Although we’ve briefly examined the HTML for the form in Figure 7.14 (shown in Listing 7.17), we haven’t yet covered any details, and the form is best understood in the context of signup failure. In this section, we’ll create a signup form that accepts an invalid submission and re-renders the signup page with a list of errors, as mocked up in Figure 7.16.

Figure 7.16

Figure 7.16: A mockup of the signup failure page.

7.3.1 A Working Form

Recall from Section 7.1.2 that adding resources :users to the routes.rb file (Listing 7.3) automatically ensures that our Rails application responds to the RESTful URLs from Table 7.1. In particular, it ensures that a POST request to /users is handled by the create action. Our strategy for the create action is to use the form submission to make a new user object using User.new, try (and fail) to save that user, and then render the signup page for possible resubmission. Let’s get started by reviewing the code for the signup form:

<form action="/users" class="new_user" id="new_user" method="post">

As noted in Section 7.2.2, this HTML issues a POST request to the /users URL.

Our first step toward a working signup form is adding the code in Listing 7.18. This listing includes a second use of the render method, which we first saw in the context of partials (Section 5.1.3); as you can see, render works in controller actions as well. Note that we’ve taken this opportunity to introduce an ifelse branching structure, which allows us to handle the cases of failure and success separately based on the value of @user.save, which (as we saw in Section 6.1.3) is either true or false depending on whether or not the save succeeds.

Listing 7.18: A create action that can handle signup failure.

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
  def new
    @user = User.new
  end
  def create
    @user = User.new(params[:user])    # Not the final implementation!
    if @user.save
      # Handle a successful save.
    else
      render 'new', status: :unprocessable_entity
    end
  end
end

Note the comment: This is not the final implementation. But it’s enough to get us started, and we’ll finish the implementation in Section 7.3.2. Also note the use of status: :unprocessable_entity (corresponding to the HTTP status code 422 Unprocessable Entity), which is necessary to render regular HTML when using Turbo (which we’ll install in Section 8.2.4 and cover in more depth in Section 14.2.5). The form will work anyway as long as you included --skip-bundle as in Listing 3.1, but it would fail in Section 8.2.4 after installing Turbo, and it does no harm to include it now.12

The best way to understand how the code in Listing 7.18 works is to submit the form with some invalid signup data. The result appears in Figure 7.17, and the full debug information appears in Figure 7.18.

Figure 7.17

Figure 7.17: Signup failure upon submitting invalid data.

Figure 7.18

Figure 7.18: Signup failure debug information.

To get a better picture of how Rails handles the submission, let’s take a closer look at the user part of the parameters hash from the debug information (Figure 7.18):

"user" => { "name" => "Foo Bar",
            "email" => "foo@invalid",
            "password" => "[FILTERED]",
            "password_confirmation" => "[FILTERED]"
          }

This hash gets passed to the Users controller as part of params, and we saw starting in Section 7.1.2 that the params hash contains information about each request. In the case of a URL like /users/1, the value of params[:id] is the id of the corresponding user (1 in this example). In the case of posting to the signup form, params instead contains a hash of hashes, a construction we first saw in Section 4.3.3, which introduced the strategically named params variable in a console session. The debug information above shows that submitting the form results in a user hash with attributes corresponding to the submitted values, where the keys come from the name attributes of the input tags seen in Listing 7.17. For example, the value of

<input id="user_email" name="user[email]" type="email" />

with name "user[email]" is precisely the email attribute of the user hash.

Although the hash keys appear as strings in the debug output, we can access them in the Users controller as symbols, so that params[:user] is the hash of user attributes—in fact, exactly the attributes needed as an argument to User.new, as first seen in Section 4.4.5 and appearing in Listing 7.18. This means that the line

@user = User.new(params[:user])

is mostly equivalent to

@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")

In previous versions of Rails, using

@user = User.new(params[:user])

actually worked, but it was insecure by default and required a careful and error-prone procedure to prevent malicious users from potentially modifying the application database. In Rails versions later than 4.0, this code raises an error (as seen in Figure 7.17 and Figure 7.18), which means it is secure by default.

7.3.2 Strong Parameters

We mentioned briefly in Section 4.4.5 the idea of mass assignment, which involves initializing a Ruby variable using a hash of values, as in

@user = User.new(params[:user])    # Not the final implementation!

The comment included in Listing 7.18 and reproduced above indicates that this is not the final implementation. The reason is that initializing the entire params hash is extremely dangerous—it arranges to pass to User.new all data submitted by a user. In particular, suppose that, in addition to the current attributes, the User model included an admin attribute used to identify administrative users of the site. (We will implement just such an attribute in Section 10.4.1.) The way to set such an attribute to true is to pass the value admin='1' as part of params[:user], a task that is easy to accomplish using a command-line HTTP client such as curl. The result would be that, by passing in the entire params hash to User.new, we would allow any user of the site to gain administrative access by including admin='1' in the web request.

Previous versions of Rails used a method called attr_accessible in the model layer to solve this problem, and you may still see that method in legacy Rails applications, but as of Rails 4.0 the preferred technique is to use so-called strong parameters in the controller layer. This allows us to specify which parameters are required and which ones are permitted. In addition, passing in a raw params hash as above will cause an error to be raised, so that Rails applications are now immune to mass assignment vulnerabilities by default.

In the present instance, we want to require the params hash to have a :user attribute, and we want to permit the name, email, password, and password confirmation attributes (but no others). We can accomplish this as follows:

params.require(:user).permit(:name, :email, :password, :password_confirmation)

This code returns a version of the params hash with only the permitted attributes (while raising an error if the :user attribute is missing).

To facilitate the use of these parameters, it’s conventional to introduce an auxiliary method called user_params (which returns an appropriate initialization hash) and use it in place of params[:user]:

@user = User.new(user_params)

Since user_params will only be used internally by the Users controller and need not be exposed to external users via the Web, we’ll make it private using Ruby’s private keyword, as shown in Listing 7.19. (We’ll discuss private in more detail in Section 9.1.)

Listing 7.19: Using strong parameters in the create action.

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      # Handle a successful save.
    else
      render 'new', status: :unprocessable_entity
    end
  end
  private
    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

By the way, the extra level of indentation on the user_params method is designed to make it visually apparent which methods are defined after private. (Experience shows that this is a wise practice; in classes with a large number of methods, it is easy to define a private method accidentally, which leads to considerable confusion when it isn’t available to call on the corresponding object.)

At this point, the signup form is working, at least in the sense that it no longer produces an error upon submission. On the other hand, as seen in Figure 7.19, it doesn’t display any feedback on invalid submissions (apart from the development-only debug area), which is potentially confusing. It also doesn’t actually create a new user. We’ll fix the first issue in Section 7.3.3 and the second in Section 7.4.

Figure 7.19

Figure 7.19: The signup form submitted with invalid information.

Exercises

  1. By hitting the URL /signup?admin=1, confirm that the admin attribute appears in the params debug information.

7.3.3 Signup Error Messages

As a final step in handling failed user creation, we’ll add helpful error messages to indicate the problems that prevented successful signup. Conveniently, Rails automatically provides such messages based on the User model validations. For example, consider trying to save a user with an invalid email address and with a password that’s too short:

$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?>                 password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]

Here the errors.full_messages object (which we saw briefly before in Section 6.2.2) contains an array of error messages.

As in the console session above, the failed save in Listing 7.18 generates a list of error messages associated with the @user object. To display the messages in the browser, we’ll render an error-messages partial on the user new page while adding the CSS class form-control (which has special meaning to Bootstrap) to each entry field, as shown in Listing 7.20. It’s worth noting that this error-messages partial is only a first attempt; the final version appears in Section 13.3.2.

Listing 7.20: Code to display error messages on the signup form.

app/views/users/new.html.erb

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user) do |f| %>
      <%= render 'shared/error_messages' %>
      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>
      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>
      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

Notice here that we render a partial called 'shared/error_messages'; this reflects the common Rails convention of using a dedicated shared/ directory for partials expected to be used in views across multiple controllers. (We’ll see this expectation fulfilled in Section 10.1.1.)

This means that we have to create a new app/views/shared directory using mkdir and an error-messages partial using (Table 1.1):

$ mkdir app/views/shared

We then need to create the _error_messages.html.erb partial file using touch or the text editor as usual. The contents of the partial appear in Listing 7.21.

Listing 7.21: A partial for displaying form submission error messages.

app/views/shared/_error_messages.html.erb

<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

This partial introduces several new Rails and Ruby constructs, including two methods for Rails error objects. The first method is count, which simply returns the number of errors:

>> user.errors.count
=> 2

The other new method is any?, which (together with empty?) is one of a pair of complementary methods:

>> user.errors.empty?
=> false
>> user.errors.any?
=> true

We see here that the empty? method, which we first saw in Section 4.2.2 in the context of strings, also works on Rails error objects, returning true for an empty object and false otherwise. The any? method is just the opposite of empty?, returning true if there are any elements present and false otherwise. (By the way, all of these methods—count, empty?, and any?—work on Ruby arrays as well. We’ll put this fact to good use starting in Section 13.2.)

The other new idea is the pluralize text helper, which is available in the console via the helper object:

>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(5, "error")
=> "5 errors"

We see here that pluralize takes an integer argument and then returns the number with a properly pluralized version of its second argument. Underlying this method is a powerful inflector that knows how to pluralize a large number of words, including many with irregular English plurals:

>> helper.pluralize(2, "woman")
=> "2 women"
>> helper.pluralize(3, "erratum")
=> "3 errata"

As a result of its use of pluralize, the code

<%= pluralize(@user.errors.count, "error") %>

returns "0 errors", "1 error", "2 errors", and so on, depending on how many errors there are, thereby avoiding ungrammatical phrases such as "1 errors" (a distressingly common mistake in both web and desktop applications).

Note that Listing 7.21 includes the CSS id error_explanation for use in styling the error messages. (Recall from Section 5.1.2 that CSS uses the pound sign # to style ids.) In addition, after an invalid submission Rails automatically wraps the fields with errors in divs with the CSS class field_with_errors. These labels then allow us to style the error messages with the SCSS shown in Listing 7.22, which makes use of Sass’s @extend function to include the functionality of the Bootstrap class has-error.

Listing 7.22: CSS for styling error messages.

app/assets/stylesheets/custom.scss

.
.
.
/* forms */
.
.
.
#error_explanation {
  color: red;
  ul {
    color: red;
    margin: 0 0 30px 0;
  }
}
.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

With the code in Listing 7.20 and Listing 7.21 and the SCSS from Listing 7.22, helpful error messages now appear when submitting invalid signup information, as seen in Figure 7.20. Because the messages are generated by the model validations, they will automatically change if you ever change your mind about, say, the format of email addresses, or the minimum length of passwords. (Note: Because both the presence validation and the has_secure_password validation catch the case of empty (nil) passwords, the signup form currently produces duplicate error messages when the user submits empty passwords. We could manipulate the error messages directly to eliminate such duplicates, but luckily this issue will be fixed automatically by the addition of allow_nil: true in Section 10.1.4.)

Figure 7.20

Figure 7.20: Failed signup with error messages.

Exercises

  1. Confirm by changing the minimum length of passwords to 5 that the error message updates automatically as well.

  2. How does the URL on the unsubmitted signup form (Figure 7.14) compare to the URL for a submitted signup form (Figure 7.20)? Why don’t they match?

7.3.4 A Test for Invalid Submission

In the days before powerful web frameworks with automated testing capabilities, developers had to test forms by hand. For example, to test a signup page manually, we would have to visit the page in a browser and then submit alternately invalid and valid data, verifying in each case that the application’s behavior was correct. Moreover, we would have to remember to repeat the process any time the application changed. This process was painful and error-prone.

Happily, with Rails we can write tests to automate the testing of forms. In this section, we’ll write one such test to verify the correct behavior upon invalid form submission; in Section 7.4.4, we’ll write a corresponding test for valid submission.

To get started, we first generate an integration test file for signing up users, which we’ll call users_signup (adopting the controller convention of a plural resource name):

$ rails generate integration_test users_signup
      invoke  test_unit
      create    test/integration/users_signup_test.rb

(We’ll use this same file in Section 7.4.4 to test a valid signup.)

The main purpose of our test is to verify that clicking the signup button results in not creating a new user when the submitted information is invalid. (Writing a test for the error messages is left as an exercise (Section 7.3.4).) The way to do this is to check the count of users, and under the hood our tests will use the count method available on every Active Record class, including User:

$ rails console
>> User.count
=> 1

(Here User.count is 1 because of the user created in Section 6.3.4, though it may differ if you’ve added or deleted any users in the interim.) As in Section 5.3.4, we’ll use assert_select to test HTML elements of the relevant pages, taking care to check only elements unlikely to change in the future.

We’ll start by visiting the signup path using get:

get signup_path

In order to test the form submission, we need to issue a POST request to the users_path (Table 7.1), which we can do with the post function:

assert_no_difference 'User.count' do
  post users_path, params: { user: { name:  "",
                                     email: "user@invalid",
                                     password:              "foo",
                                     password_confirmation: "bar" } }
end

Here we’ve included the params[:user] hash expected by User.new in the create action (Listing 7.27). (In versions of Rails before 5, params was implicit, and only the user hash would be passed. This practice was deprecated in Rails 5.0, and now the recommended method is to include the full params hash explicitly.)

By wrapping the post in the assert_no_difference method with the string argument 'User.count', we arrange for a comparison between User.count before and after the contents inside the assert_no_difference block. This is equivalent to recording the user count, posting the data, and verifying that the count is the same:

before_count = User.count
post users_path, ...
after_count = User.count
assert_equal before_count, after_count

Although the two are equivalent, using assert_no_difference is cleaner and is more idiomatically correct Rails.

It’s worth noting that the get and post steps above are technically unrelated, and it’s actually not necessary to get the signup path before posting to the users path. I prefer to include both steps, though, both for conceptual clarity and to double-check that the signup form renders without error.

We’ll also include assertions to verify that the right response code is returned and the right template rendered, which we can do as follows:

assert_response :unprocessable_entity
assert_template 'users/new'

Compare the first line with assert_response :success in Listing 3.14 and related tests. This line is useful to verify that the right status is returned by Listing 7.18 and to protect against regressions (Box 3.3).

Putting the above ideas together leads to the test in Listing 7.23. Adding lines to check for the appearance of error messages is left as an exercise (Section 7.3.4).

Listing 7.23: A test for an invalid signup. GREEN

test/integration/users_signup_test.rb

require "test_helper"
class UsersSignupTest < ActionDispatch::IntegrationTest
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_response :unprocessable_entity
    assert_template 'users/new'
  end
end

Because we wrote the application code before the integration test, the test suite should be GREEN:

Listing 7.24: GREEN

$ rails test

Exercises

  1. Write a test for the error messages implemented in Listing 7.20. How detailed you want to make your tests is up to you; a suggested template appears in Listing 7.25.

Listing 7.25: A template for tests of the error messages.

test/integration/users_signup_test.rb

require "test_helper"
class UsersSignupTest < ActionDispatch::IntegrationTest
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name: "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_response :unprocessable_entity
    assert_template 'users/new'
    assert_select 'div#<CSS id for error explanation>'
    assert_select 'div.<CSS class for field with error>'
  end
  .
  .
  .
end

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020