About This E-Book

EPUB is an open, industry-standard format for e-books. However, support for EPUB and its many features varies across reading devices and applications. Use your device or app settings to customize the presentation to your liking. Settings that you can customize often include font, font size, single or double column, landscape or portrait mode, and figures that you can click or tap to enlarge. For additional information about the settings and features on your reading device or app, visit the device manufacturer’s Web site.

Many titles include programming code or configuration examples. To optimize the presentation of these elements, view the e-book in single-column, landscape mode and adjust the font size to the smallest setting. In addition to presenting code and configurations in the reflowable text format, we have included images of the code that mimic the presentation found in the print book; therefore, where the reflowable format may compromise the presentation of the code listing, you will see a “Click here to view code image” link. Click the link to view the print-fidelity code image. To return to the previous page viewed, click the Back button on your device or app.

Ruby on Rails™ Tutorial

Learn Web Development with Rails

Fourth Edition

Michael Hartl

Boston • Columbus • Indianapolis • New York • San Francisco • Amsterdam • Cape Town
Dubai • London • Madrid • Milan • Munich • Paris • Montreal • Toronto • Delhi • Mexico City
São Paulo • Sydney • Hong Kong • Seoul • Singapore • Taipei • Tokyo

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.

The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.

For information about buying this title in bulk quantities, or for special sales opportunities (which may include electronic versions; custom cover designs; and content particular to your business, training goals, marketing focus, or branding interests), please contact our corporate sales department at corpsales@pearsoned.com or (800) 382–3419.

For government sales inquiries, please contact governmentsales@pearsoned.com.

For questions about sales outside the United States, please contact intlcs@pearson.com.

Visit us on the web: informit.com/aw

Library of Congress Control Number: 2016950287

Copyright © 2017 Michael Hartl

All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, request forms and the appropriate contacts within the Pearson Education Global Rights & Permissions Department, please visit www.pearsoned.com/permissions/.

The source code in Ruby on Rails™ Tutorial, Fourth Edition, is released under the MIT License.

ISBN 13: 978-0-13-459862-8
ISBN 10: 0-13-459862-8
Text printed in the United States of America.
1    16

Editor-in-Chief
Mark L. Taub

Acquisitions Editor
Debra Williams Cauley

Managing Editor
Sandra Schroeder

Full-Service Production Manager
Julie B. Nahil

Project Editor
Rachel Paul

Copy Editor
Chris Zahn

Indexer
Richard Evans

Proofreader
Rachel Paul

Assistant Editor
Kim Boedigheimer

Cover Designer
Chuti Prasertsith

Compositor
diacriTech

Praise for Michael Hartl’s Books and Videos on Ruby on Rails

“My former company (CD Baby) was one of the first to loudly switch to Ruby on Rails, and then even more loudly switch back to PHP. (Google me to read about the drama.) This book by Michael Hartl came so highly recommended that I had to try it, and the Ruby on Rails™ Tutorial is what I used to switch back to Rails again.”

—From the Foreword by Derek Sivers (sivers.org)
Formerly: founder of CD Baby
Currently: founder of Thoughts Ltd.

“Michael Hartl’s Rails Tutorial book is the #1 (and only, in my opinion) place to start when it comes to books about learning Rails. . . . It’s an amazing piece of work and, unusually, walks you through building a Rails app from start to finish with testing. If you want to read just one book and feel like a Rails master by the end of it, pick the Ruby on Rails™ Tutorial.”

—Peter Cooper, editor, Ruby Inside

“For the self-motivated reader who responds well to the ‘learn by doing’ method and is prepared to put in the effort, this comes highly recommended.”

—Ian Elliot, reviewer, I Programmer

“Ruby on Rails™ Tutorial is a lot of work, but if you’re careful and patient, you’ll learn a lot.”

—Jason Shen, tech entrepreneur, blogger at The Art of Ass-Kicking

“Michael Hartl’s Ruby on Rails™ Tutorial seamlessly taught me about not only Ruby on Rails, but also the underlying Ruby language, HTML, CSS, a bit of JavaScript, and even some SQL—but most importantly it showed me how to build a web application (Twitter) in a short amount of time.”

—Mattan Griffel, co-founder & CEO of One Month

“Although I’m a Python/Django developer by trade, I can’t stress enough how much this book has helped me. As an undergraduate, completely detached from industry, this book showed me how to use version control, how to write tests, and, most importantly— despite the steep learning curve for setting up and getting stuff running—how the end result of perseverance is extremely gratifying. It made me fall in love with technology all over again. This is the book I direct all my friends to who want to start learning programming/building stuff. Thank you Michael!”

—Prakhar Srivastav, software engineer, Xcite.com, Kuwait

“It doesn’t matter what you think you will be developing with in the future or what the framework du jour is; if you want to learn how to build something, there is no better place to start than with this tutorial. And for all the ‘non-technical’ people out there who want to see their ideas come to life, who are considering hiring contractors, paying for a class, or ‘founder dating’ in the search for a technical co-founder: stop. Take a step back. Forget about your idea for a short while and immerse yourself in this tutorial to learn what it takes to put something together. You and your software-related projects will be better for it.”

—Vincent C., entrepreneur and developer

“It has to be the best-written book of its type I’ve ever seen, and I can’t recommend it enough.”

—Daniel Hollands, administrator of Birmingham.IO

“For those wanting to learn Ruby on Rails, Hartl’s Ruby on Rails™ Tutorial is (in my opinion) the best way to do it.”

—David Young, software developer and author at deepinthecode.com

“This is a great tutorial for a lot of reasons, because aside from just teaching Rails, Hartl is also teaching good development practices.”

—Michael Denomy, full-stack web developer

“Without a doubt, the best way I learned Ruby on Rails was by building an actual working app. I used Michael Hartl’s Ruby on Rails™ Tutorial, which showed me how to get a very basic Twitter-like app up and running from scratch. I cannot recommend this tutorial enough; getting something up and going fast was key; it beats memorization by a mile.”

—James Fend, serial entrepreneur, JamesFend.com

“The book gives you the theory and practice, while the videos focus on showing you in person how it’s done. Highly recommended combo.”

—Antonio Cangiano, software engineer, IBM

“The author is clearly an expert at the Ruby language and the Rails framework, but more than that, he is a working software engineer who introduces best practices throughout the text.”

—Greg Charles, senior software developer, Fairway Technologies

“Overall, [Hartl’s] video tutorials should be a great resource for anyone new to Rails.”

—Michael Morin, ruby.about.com

“Hands-down, I would recommend this book to anyone wanting to get into Ruby on Rails development.”

—Michael Crump, Microsoft MVP

Contents

Foreword to the First Edition by Derek Sivers

Foreword to the Third Edition by Obie Fernandez

Acknowledgments

About the Author

Chapter 1 From Zero to Deploy

1.1 Introduction

1.1.1 Prerequisites

1.1.2 Conventions Used in This Book

1.2 Up and Running

1.2.1 Development Environment

1.2.2 Installing Rails

1.3 The First Application

1.3.1 Bundler

1.3.2 rails server

1.3.3 Model-View-Controller (MVC)

1.3.4 Hello, World!

1.4 Version Control with Git

1.4.1 Installation and Setup

1.4.2 What Good Does Git Do You?

1.4.3 Bitbucket

1.4.4 Branch, Edit, Commit, Merge

1.5 Deploying

1.5.1 Heroku Setup

1.5.2 Heroku Deployment, Step One

1.5.3 Heroku Deployment, Step Two

1.5.4 Heroku Commands

1.6 Conclusion

1.6.1 What We Learned in This Chapter

Chapter 2 A Toy App

2.1 Planning the Application

2.1.1 A Toy Model for Users

2.1.2 A Toy Model for Microposts

2.2 The Users Resource

2.2.1 A User Tour

2.2.2 MVC in Action

2.2.3 Weaknesses of this Users Resource

2.3 The Microposts Resource

2.3.1 A Micropost Microtour

2.3.2 Putting the Micro in Microposts

2.3.3 A User has_many Microposts

2.3.4 Inheritance Hierarchies

2.3.5 Deploying the Toy App

2.4 Conclusion

2.4.1 What We Learned in This Chapter

Chapter 3 Mostly Static Pages

3.1 Sample App Setup

3.2 Static Pages

3.2.1 Generated Static Pages

3.2.2 Custom Static Pages

3.3 Getting Started with Testing

3.3.1 Our First Test

3.3.2 Red

3.3.3 Green

3.3.4 Refactor

3.4 Slightly Dynamic Pages

3.4.1 Testing Titles (Red)

3.4.2 Adding Page Titles (Green)

3.4.3 Layouts and Embedded Ruby (Refactor)

3.4.4 Setting the Root Route

3.5 Conclusion

3.5.1 What We Learned in This Chapter

3.6 Advanced Testing Setup

3.6.1 Minitest Reporters

3.6.2 Automated Tests with Guard

Chapter 4 Rails-Flavored Ruby

4.1 Motivation

4.1.1 Built-In Helpers

4.1.2 Custom Helpers

4.2 Strings and Methods

4.2.1 Comments

4.2.2 Strings

4.2.3 Objects and Message Passing

4.2.4 Method Definitions

4.2.5 Back to the Title Helper

4.3 Other Data Structures

4.3.1 Arrays and Ranges

4.3.2 Blocks

4.3.3 Hashes and Symbols

4.3.4 CSS Revisited

4.4 Ruby Classes

4.4.1 Constructors

4.4.2 Class Inheritance

4.4.3 Modifying Built-in Classes

4.4.4 A Controller Class

4.4.5 A User Class

4.5 Conclusion

4.5.1 What We Learned in This Chapter

Chapter 5 Filling in the Layout

5.1 Adding Some Structure

5.1.1 Site Navigation

5.1.2 Bootstrap and Custom CSS

5.1.3 Partials

5.2 Sass and the Asset Pipeline

5.2.1 The Asset Pipeline

5.2.2 Syntactically Awesome Stylesheets

5.3 Layout Links

5.3.1 Contact Page

5.3.2 Rails Routes

5.3.3 Using Named Routes

5.3.4 Layout Link Tests

5.4 User Signup: A First Step

5.4.1 Users Controller

5.4.2 Signup URL

5.5 Conclusion

5.5.1 What We Learned in This Chapter

Chapter 6 Modeling Users

6.1 User Model

6.1.1 Database Migrations

6.1.2 The Model File

6.1.3 Creating User Objects

6.1.4 Finding User Objects

6.1.5 Updating User Objects

6.2 User Validations

6.2.1 A Validity Test

6.2.2 Validating Presence

6.2.3 Length Validation

6.2.4 Format Validation

6.2.5 Uniqueness Validation

6.3 Adding a Secure Password

6.3.1 A Hashed Password

6.3.2 User Has Secure Password

6.3.3 Minimum Password Standards

6.3.4 Creating and Authenticating a User

6.4 Conclusion

6.4.1 What We Learned in This Chapter

Chapter 7 Sign Up

7.1 Showing Users

7.1.1 Debug and Rails Environments

7.1.2 A Users Resource

7.1.3 Debugger

7.1.4 A Gravatar Image and a Sidebar

7.2 Signup Form

7.2.1 Using form_for

7.2.2 Signup form HTML

7.3 Unsuccessful Signups

7.3.1 A Working Form

7.3.2 Strong Parameters

7.3.3 Signup Error Messages

7.3.4 A Test for Invalid Submission

7.4 Successful Signups

7.4.1 The Finished Signup Form

7.4.2 The Flash

7.4.3 The First Signup

7.4.4 A Test for Valid Submission

7.5 Professional-Grade Deployment

7.5.1 SSL in Production

7.5.2 Production Webserver

7.5.3 Production Deployment

7.6 Conclusion

7.6.1 What We Learned in This Chapter

Chapter 8 Basic Login

8.1 Sessions

8.1.1 Sessions Controller

8.1.2 Login Form

8.1.3 Finding and Authenticating a User

8.1.4 Rendering with a Flash Message

8.1.5 A Flash Test

8.2 Logging In

8.2.1 The log_in Method

8.2.2 Current User

8.2.3 Changing the Layout Links

8.2.4 Testing Layout Changes

8.2.5 Login Upon Signup

8.3 Logging Out

8.4 Conclusion

8.4.1 What We Learned in This Chapter

Chapter 9 Advanced Login

9.1 Remember Me

9.1.1 Remember Token and Digest

9.1.2 Login with Remembering

9.1.3 Forgetting Users

9.1.4 Two Subtle Bugs

9.2 “Remember Me” Checkbox

9.3 Remember Tests

9.3.1 Testing the “Remember Me” Box

9.3.2 Testing the Remember Branch

9.4 Conclusion

9.4.1 What We Learned in This Chapter

Chapter 10 Updating, Showing, and Deleting Users

10.1 Updating Users

10.1.1 Edit Form

10.1.2 Unsuccessful Edits

10.1.3 Testing Unsuccessful Edits

10.1.4 Successful Edits (with TDD)

10.2 Authorization

10.2.1 Requiring Logged-in Users

10.2.2 Requiring the Right User

10.2.3 Friendly Forwarding

10.3 Showing All Users

10.3.1 Users Index

10.3.2 Sample Users

10.3.3 Pagination

10.3.4 Users Index Test

10.3.5 Partial Refactoring

10.4 Deleting Users

10.4.1 Administrative Users

10.4.2 The destroy Action

10.4.3 User Destroy Tests

10.5 Conclusion

10.5.1 What We Learned in This Chapter

Chapter 11 Account Activation

11.1 Account Activations Resource

11.1.1 Account Activations Controller

11.1.2 Account Activation Data Model

11.2 Account Activation Emails

11.2.1 Mailer Templates

11.2.2 Email Previews

11.2.3 Email Tests

11.2.4 Updating the Users create Action

11.3 Activating the Account

11.3.1 Generalizing the authenticated? Method

11.3.2 Activation edit Action

11.3.3 Activation Test and Refactoring

11.4 Email in Production

11.5 Conclusion

11.5.1 What We Learned in This Chapter

Chapter 12 Password Reset

12.1 Password Resets Resource

12.1.1 Password Resets Controller

12.1.2 New Password Resets

12.1.3 Password Reset create Action

12.2 Password Reset Emails

12.2.1 Password Reset Mailer and Templates

12.2.2 Email Tests

12.3 Resetting the Password

12.3.1 Reset edit Action

12.3.2 Updating the Reset

12.3.3 Password Reset Test

12.4 Email in Production (Take Two)

12.5 Conclusion

12.5.1 What We Learned in This Chapter

12.6 Proof of Expiration Comparison

Chapter 13 User Microposts

13.1 A Micropost Model

13.1.1 The Basic Model

13.1.2 Micropost Validations

13.1.3 User/Micropost Associations

13.1.4 Micropost Refinements

13.2 Showing Microposts

13.2.1 Rendering Microposts

13.2.2 Sample Microposts

13.2.3 Profile Micropost Tests

13.3 Manipulating Microposts

13.3.1 Micropost Access Control

13.3.2 Creating Microposts

13.3.3 A Proto-Feed

13.3.4 Destroying Microposts

13.3.5 Micropost Tests

13.4 Micropost Images

13.4.1 Basic Image Upload

13.4.2 Image Validation

13.4.3 Image Resizing

13.4.4 Image Upload in Production

13.5 Conclusion

13.5.1 What We Learned in This Chapter

Chapter 14 Following Users

14.1 The Relationship Model

14.1.1 A Problem with the Data Model (and a Solution)

14.1.2 User/Relationship Associations

14.1.3 Relationship Validations

14.1.4 Followed Users

14.1.5 Followers

14.2 A Web Interface for Following Users

14.2.1 Sample Following Data

14.2.2 Stats and a Follow Form

14.2.3 Following and Followers Pages

14.2.4 A Working Follow Button the Standard Way

14.2.5 A Working Follow Button with Ajax

14.2.6 Following Tests

14.3 The Status Feed

14.3.1 Motivation and Strategy

14.3.2 A First Feed Implementation

14.3.3 Subselects

14.4 Conclusion

14.4.1 Guide to Further Resources

14.4.2 What We Learned in This Chapter

Index

Foreword to the First Edition

My former company (CD Baby) was one of the first to loudly switch to Ruby on Rails, and then even more loudly switch back to PHP. (Google me to read about the drama.) This book by Michael Hartl came so highly recommended that I had to try it, and the Ruby on Rails™ Tutorial is what I used to switch back to Rails again.

Though I’ve worked my way through many Rails books, this is the one that finally made me “get” it. Everything is done very much “the Rails way”—a way that felt very unnatural to me before, but now after reading this book finally feels natural. This is also the only Rails book that does test-driven development the entire time, an approach highly recommended by the experts but which has never been so clearly demonstrated before. Finally, by including Git, GitHub, and Heroku in the demo examples, the author really gives you a feel for what it’s like to do a real-world project. The tutorial’s code examples are not in isolation.

The linear narrative is such a great format. Personally, I powered through the Ruby on Rails™ Tutorial in three long days,* doing all the examples and challenges at the end of each chapter. Do it from start to finish, without jumping around, and you’ll get the ultimate benefit.

Enjoy!

—Derek Sivers (sivers.org)
Founder, CD Baby

This is not typical! Getting through the entire book usually takes much longer than three days.

Foreword to the Third Edition

Rails is now ten years old, and adoption shows no sign of slowing down. Along with the perpetual growth, we’ve seen a paradoxical tragedy unfolding. According to many people, Rails is now one of the hardest tech stacks for beginners to adopt. The complexity of choices to make when starting out is very high, and there are ten years of blog posts and books out there, most of which are obsolete and broken to some degree or another. The supreme irony is that getting started with Rails today involves a lot of configuration, perhaps not of Rails itself, but of the myriad libraries that are recommended for use with it. In case you’ve forgotten, or weren’t paying attention in 2005 when Rails debuted, the main goal was to achieve convention over configuration.

To some extent, we’ve replicated the Java web beast that we once fought hard to slay. Argh.

Don’t get too depressed, though, because that would be missing the point. The good news is that once you’re past the daunting learning curve, Rails remains one of the most powerful and efficient stacks available for building API back-ends and content-driven websites.

Now maybe you’re considering using this book to kick off your journey up the Rails learning curve. Trust me, it’s the right choice. I’ve known Michael Hartl for almost ten years now, and he is a highly intelligent man. Just look for his credentials elsewhere in the book and you’ll see what I mean. But never mind the prestigious degrees; the approach he has adopted for this latest edition of our best-selling Ruby on Rails™ Tutorial proves just how smart he is. Instead of doubling down on the opinionated approach (like another series author I know, ahem), he’s gone in the opposite direction! By getting less opinionated, he has lowered the barrier for Rails newcomers in significant ways.

First of all, he dispenses with any sort of local installation or configuration. He also eschews complex configuration options (like Spork and RubyTest), which are likely to trip up novices. All code examples run in a standardized cloud-based environment accessible via a simple web browser.

Second, he throws out tons of content from the previous edition and embraces the Rails “default stack”, including its built-in MiniTest testing framework. The resulting elimination of many external dependencies (RSpec, Cucumber, Capybara, Factory Girl) makes the Rails learning curve quite a bit easier to climb, at the expense of having to rewrite big swaths of the book.

Over the years, in no small part due to his work on this book franchise, Michael has become a master of writing training materials grounded in practical, useful knowledge. And as in the past, this edition includes basics of vital tools such as Git and GitHub. Testing is front and center, which most would agree is the proper emphasis for beginners. Michael’s well-polished examples always utilize small, bite-sized pieces of code—simple enough to understand and novel enough to be challenging. By the time you finish the book and are playing around with your very own little Twitter clone, you’re sure to possess a deeper, more flexible knowledge of Rails. Most importantly, you’ll have a foundation flexible enough to get you coding up nearly any type of web application.

Godspeed!

—Obie Fernandez,
Series Editor

Acknowledgments

The Ruby on Rails™ Tutorial owes a lot to my previous Rails book, RailsSpace, and hence to my coauthor Aurelius Prochazka. I’d like to thank Aure both for the work he did on that book and for his support of this one. I’d also like to thank Debra Williams Cauley, my editor on both RailsSpace and the Ruby on Rails™ Tutorial; as long as she keeps taking me to baseball games, I’ll keep writing books for her.

I’d like to acknowledge a long list of Rubyists who have taught and inspired me over the years: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Mark Bates, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, Sandi Metz, Ryan Davis, Aaron Patterson, the good people at Pivotal Labs, the Heroku gang, the thoughtbot guys, and the GitHub crew.

I’d like to thank technical reviewer Andrew Thal for his careful reading of the manuscript and for his helpful suggestions. I’d also like to thank my cofounders at Learn Enough to Be Dangerous, Nick Merwin and Lee Donahoe, for all their help in preparing this tutorial.

Finally, many, many readers—far too many to list—have contributed a huge number of bug reports and suggestions during the writing of this book, and I gratefully acknowledge their help in making it as good as it can be.

About the Author

Michael Hartl is the author of the Ruby on Rails™ Tutorial, one of the leading introductions to web development, and is a founder of the educational website Learn Enough to Be Dangerous (learnenough.com). His prior experience includes writing and developing RailsSpace, an extremely obsolete Rails tutorial book, and developing Insoshi, a once-popular and now-obsolete social networking platform in Ruby on Rails. In 2011, Michael received a Ruby Hero Award for his contributions to the Ruby community. He is a graduate of Harvard College, has a Ph.D. in Physics from Caltech, and is an alumnus of the Y Combinator entrepreneur program.

Chapter 1. From Zero to Deploy

Welcome to Ruby on Rails™ Tutorial: Learn Web Development with Rails. The purpose of this book is to teach you how to develop custom web applications, and our tool of choice is the popular Ruby on Rails web framework. In addition to focusing on general principles of web development (rather than on Rails specifically), the Ruby on Rails™ Tutorial teaches the broader skill of technical sophistication (Box 1.1), which is a principal theme developed by the Learn Enough to Be Dangerous tutorials.¹ In particular, the Learn Enough introductory sequence consists of a series of tutorials that are suitable as prerequisites to the Ruby on Rails™ Tutorial, starting with Learn Enough Command Line to Be Dangerous,² which (unlike the present tutorial) is aimed at complete beginners.

1. learnenough.com/story

2. learnenough.com/command-line

Box 1.1: Technical Sophistication

The Ruby on Rails™ Tutorial is part of the Learn Enough to Be Dangerous family of tutorials, which develop the theme of technical sophistication: the combination of hard and soft skills that make it seem like you can magically solve any technical problem. Web development, and computer programming in general, are essential components of technical sophistication, but there’s more to it than that—you also have to know how to click around menu items to learn the capabilities of a particular application, how to clarify a confusing error message by Googling it, or when to give up and just reboot the darn thing.

Because web applications have so many moving parts, they offer ample opportunities to develop your technical sophistication. In the context of Rails web development, some specific examples of technical sophistication include making sure you’re using the right Ruby gem versions, running bundle install or bundle update, and restarting the local webserver if something doesn’t work. (Don’t worry if all this sounds like gibberish; we’ll cover everything mentioned here in the course of completing this tutorial.)

As you proceed through this tutorial, in all likelihood you will occasionally be tripped up by things not immediately working as expected. Although some particularly tricky steps are explicitly highlighted in the text, it is impossible to anticipate all the things that can go wrong. I recommend you embrace these inevitable stumbling blocks as opportunities to work on improving your technical sophistication. Or, as we say in geek speak: It’s not a bug; it’s a feature!

The Ruby on Rails™ Tutorial is designed to give you a thorough introduction to web application development, including a basic grounding in Ruby, Rails, HTML & CSS, databases, version control, testing, and deployment—sufficient to launch you on a career as a web developer or technology entrepreneur. If you already know web development, this book will quickly teach you the essentials of the Rails framework, including MVC and REST, generators, migrations, routing, and embedded Ruby. In any case, when you finish the Ruby on Rails™ Tutorial you will be in a position to benefit from the many more advanced books, blogs, and screencasts that are part of the thriving programming educational ecosystem.³ One such offering is the Ruby on Rails Tutorial LiveLessons screencasts, available at informit.com/rubyrailsvid.

3. The most up-to-date version of the Ruby on Rails™ Tutorial can be found on the book’s website at http://www.railstutorial.org/. If you are reading this book offline, be sure to check the online version of the Rails Tutorial book at http://www.railstutorial.org/book for the latest updates.

The Ruby on Rails™ Tutorial takes an integrated approach to web development by building three example applications of increasing sophistication, starting with a minimal hello app (Section 1.3), a slightly more capable toy app (Chapter 2), and a real sample app (Chapter 3 through Chapter 14). As implied by their generic names, the applications developed in the Ruby on Rails™ Tutorial are not specific to any particular kind of website. The final sample application bears more than a passing resemblance to Twitter (which, coincidentally, was also originally written in Rails), but the emphasis throughout the tutorial is on general principles, so you will have a solid foundation no matter what kinds of web applications you want to build.

In this first chapter, we’ll get started with Ruby on Rails by installing all the necessary software and by setting up our development environment (Section 1.2). We’ll then create our first Rails application, called hello_app. The Rails Tutorial emphasizes good software development practices, so immediately after creating our fresh new Rails project we’ll put it under version control with Git (Section 1.4). And, believe it or not, in this chapter we’ll even put our first app on the wider web by deploying it to production (Section 1.5).

In Chapter 2, we’ll make a second project, whose purpose is to demonstrate the basic workings of a Rails application. To get up and running quickly, we’ll build this toy app (called toy_app) using scaffolding (Box 1.2) to generate code; because this code is both ugly and complex, Chapter 2 will focus on interacting with the toy app through its URIs (often called URLs)⁴ using a web browser.

4. URI stands for Uniform Resource Identifier, while the slightly less general URL stands for Uniform Resource Locator. In practice, the URL is usually equivalent to “the thing you see in the address bar of your browser.”

The rest of the tutorial focuses on developing a single large real sample application (called sample_app), writing all the code from scratch. We’ll develop the sample app using a combination of mockups, test-driven development (TDD), and integration tests. We’ll get started in Chapter 3 by creating static pages and then add a little dynamic content. We’ll take a quick detour in Chapter 4 to learn a little about the Ruby language underlying Rails. Then, in Chapter 5 through Chapter 12, we’ll complete the foundation for the sample application by making a site layout, a user data model, and a full registration and authentication system (including account activation and password resets). Finally, in Chapter 13 and Chapter 14 we’ll add microblogging and social features to make a working example site.

Box 1.2: Scaffolding: Quicker, Easier, More Seductive

From the beginning, Rails has benefited from a palpable sense of excitement, starting with the famous 15-minute weblog video by Rails creator David Heinemeier Hansson. That video and its successors are a great way to get a taste of Rails’ power, and I recommend watching them. But be warned: they accomplish their amazing fifteen-minute feat using a feature called scaffolding, which relies heavily on generated code, magically created by the Rails generate scaffold command.

When writing a Ruby on Rails tutorial, it is tempting to rely on the scaffolding approach—it’s quicker, easier, more seductive. But the complexity and sheer amount of code in the scaffolding can be utterly overwhelming to a beginning Rails developer; you may be able to use it, but you probably won’t understand it. Following the scaffolding approach risks turning you into a virtuoso script generator with little (and brittle) actual knowledge of Rails.

In the Ruby on Rails™ Tutorial, we’ll take the (nearly) polar opposite approach: although Chapter 2 will develop a small toy app using scaffolding, the core of the Rails Tutorial is the sample app, which we’ll start writing in Chapter 3. At each stage of developing the sample application, we will write small, bite-sized pieces of code—simple enough to understand, yet novel enough to be challenging. The cumulative effect will be a deeper, more flexible knowledge of Rails, giving you a good background for writing nearly any type of web application.

1.1 Introduction

Ruby on Rails (or just “Rails” for short) is a web development framework written in the Ruby programming language. Since its debut in 2004, Ruby on Rails has rapidly become one of the most powerful and popular tools for building dynamic web applications. Rails is used by companies as varied as Airbnb, Basecamp, Disney, GitHub, Hulu, Kickstarter, Shopify, Twitter, and the Yellow Pages. There are also many web development shops that specialize in Rails, such as ENTP, thoughtbot, Pivotal Labs, Hashrocket, and HappyFunCorp, plus innumerable independent consultants, trainers, and contractors.

What makes Rails so great? First of all, Ruby on Rails is 100% open-source, available under the generous MIT License, and as a result it also costs nothing to download or use. Rails also owes much of its success to its elegant and compact design; by exploiting the malleability of the underlying Ruby language, Rails effectively creates a domain-specific language for writing web applications. As a result, many common web programming tasks—such as generating HTML, making data models, and routing URLs—are easy with Rails, and the resulting application code is concise and readable.

Rails also adapts rapidly to new developments in web technology and framework design. For example, Rails was one of the first frameworks to fully digest and implement the REST architectural style for structuring web applications (which we’ll be learning about throughout this tutorial). And when other frameworks develop successful new techniques, Rails creator David Heinemeier Hansson and the Rails core team don’t hesitate to incorporate their ideas. Perhaps the most dramatic example is the merger of Rails and Merb, a rival Ruby web framework, so that Rails now benefits from Merb’s modular design, stable API, and improved performance.

Finally, Rails benefits from an unusually enthusiastic and supportive community. The results include thousands of open-source contributors, fun and informative conferences, a huge number of gems (self-contained solutions to specific problems such as pagination and image upload), a rich variety of informative blogs, and a cornucopia of discussion forums and IRC channels. The large number of Rails programmers also makes it easier to handle the inevitable application errors: the “Google the error message” algorithm nearly always produces a relevant blog post or discussion-forum thread.

1.1.1 Prerequisites

There are no formal prerequisites to this book, and the Ruby on Rails™ Tutorial contains integrated tutorials not only for Rails, but also for the underlying Ruby language, the default Rails testing framework (minitest), the Unix command line, HTML, CSS, a small amount of JavaScript, and even a little SQL. That’s a lot of material to absorb, though, and I generally recommend having some HTML and programming background before starting this tutorial. If you’re new to software development, I recommend starting with the tutorials at Learn Enough to Be Dangerous instead:⁵

5. As of this writing, the command line, text editor, Git and HTML tutorials are complete, and the other tutorials are in development.

1. Developer Fundamentals

(a) Learn Enough Command Line to Be Dangerous

(b) Learn Enough Text Editor to Be Dangerous

(c) Learn Enough Git to Be Dangerous

2. Web Basics

(a) Learn Enough HTML to Be Dangerous

(b) Learn Enough CSS & Layout to Be Dangerous

(c) Learn Enough JavaScript to Be Dangerous

3. Intro Ruby Web Development

(a) Learn Enough Ruby to Be Dangerous

(b) Learn Enough Sinatra to Be Dangerous

(c) Learn Enough Ruby on Rails to Be Dangerous

4. Professional Ruby Web Development

(a) Ruby on Rails™ Tutorial

One common question when learning Rails is whether to learn Ruby first. The answer depends on your personal learning style and how much programming experience you already have. If you prefer to learn everything systematically from the ground up, or if you have never programmed before, then learning Ruby first might work well for you, and in this case I recommend following the full Learn Enough sequence listed above. On the other hand, many beginning Rails developers are excited about making web applications, and would rather not wait to finish a whole book on Ruby before ever writing a single web page. In this case, I recommend giving this tutorial a go and switching to the Learn Enough sequence if it proves too challenging.

At the end of this tutorial, no matter where you started, you should be ready for the many more intermediate-to-advanced Rails resources out there. Here are some I particularly recommend:

• The Learn Enough Society: Premium subscription service that includes a special enhanced version of the Ruby on Rails™ Tutorial book and 15+ hours of streaming screencast lessons filled the kind of with tips, tricks, and live demos that you can’t get from reading a book. Also includes text and videos for the other Learn Enough tutorials. Scholarship discounts are available.

• Code School: Good interactive online programming courses

• The Turing School of Software & Design: A full-time, 27-week training program in Denver, Colorado

• Bloc: An online bootcamp with a structured curriculum, personalized mentorship, and a focus on learning through concrete projects. Use the coupon code BLOCLOVESHARTL to get $500 off the enrollment fee.

• Launch School: A good online Rails development bootcamp (includes advanced material)

• Firehose Project: A mentor-driven, online coding bootcamp focused on real-world programming skills like test-driven development, algorithms, and building an advanced web application as part of an agile team. Two-week free intro course.

• Thinkful: An online class that pairs you with a professional engineer as you work through a project-based curriculum

• Pragmatic Studio: Online Ruby and Rails courses from Mike and Nicole Clark. Along with Programming Ruby author Dave Thomas, Mike taught the first Rails course I took, way back in 2006.

• RailsApps: A large variety of detailed topic-specific Rails projects and tutorials

• Rails Guides: Topical and up-to-date Rails references

Exercises

The Ruby on Rails™ Tutorial contains a large number of exercises. Solving them as you proceed through the tutorial is strongly recommended. Exercise solutions are available to book purchasers at http://railstutorial.org/solutions.

In order to keep the main discussion independent of the exercises, the solutions are not generally incorporated into subsequent code listings. (In the rare circumstance that an exercise solution is used subsequently, it is explicitly solved in the main text.) This means that over time your code may diverge from the code shown in the tutorial due to differences introduced in the exercises. Learning how to resolve such discrepancies is a valuable exercise in technical sophistication (Box 1.1).

To record your answers and see solutions, you can join the Learn Enough Society, a subscription service from Learn Enough to Be Dangerous that includes a special enhanced version of the Ruby on Rails™ Tutorial.

Many of the exercises are challenging, but we’ll start out with some easy ones just to get warmed up:

1. Which website hosts the Ruby gem for Ruby on Rails? Hint: When in doubt, Google it.

2. What is the current version number of Rails?

3. As of this moment, how many total times has Ruby on Rails been downloaded?

1.1.2 Conventions Used in This Book

The conventions used in this book are mostly self-explanatory. In this section, we’ll go over some that may not be.

Many examples in this book use command-line commands. For simplicity, all command line examples use a Unix-style command line prompt (a dollar sign), as follows:

Click here to view code image

$ echo "hello, world"
hello, world

Rails comes with many commands that can be run at the command line. For example, in Section 1.3.2 we’ll run a local development webserver with the rails server command:

$ rails server

As with the command-line prompt, the Rails Tutorial uses the Unix convention for directory separators (i.e., a forward slash /). For example, the sample application production.rb configuration file appears as follows:

Click here to view code image

config/environments/production.rb

This file path should be understood as being relative to the application’s root directory, which will vary by system; on the cloud IDE (Section 1.2.1), it looks like this:

Click here to view code image

/home/ubuntu/workspace/sample_app/

Thus, the full path to production.rb is

Click here to view code image

/home/ubuntu/workspace/sample_app/config/environments/production.rb

I will typically omit the application path and just write config/environments/production.rb for short.

The Rails Tutorial often shows output from various programs. Because of the innumerable small differences between different computer systems, the output you see may not always agree exactly with what is shown in the text, but this is not cause for concern. In addition, some commands may produce errors depending on your system; rather than attempt the Sisyphean task of documenting all such errors in this tutorial, I will delegate to the “Google the error message” algorithm, which among other things is good practice for real-life software development (Box 1.1). If you run into any problems while following the tutorial, I suggest consulting the resources listed in the Rails Tutorial help page.⁶

6. railstutorial.org/help

Because the Rails Tutorial covers testing of Rails applications, it is often helpful to know if a particular piece of code causes the test suite to fail (indicated by the color red) or pass (indicated by the color green). For convenience in the digital editions of this book, code resulting in a failing test is thus indicated with RED, while code resulting in a passing test is indicated with GREEN.

Finally, for convenience the Ruby on Rails™ Tutorial adopts two conventions designed to make the many code samples easier to understand. First, some code listings include one or more highlighted lines, as seen below:

Click here to view code image

class User < ApplicationRecord
  validates :name,  presence: true
  validates :email, presence: true

end

Such highlighted lines typically indicate the most important new code in the given sample, and often (though not always) represent the difference between the present code listing and previous listings. Second, for brevity and simplicity many of the book’s code listings include vertical dots, as follows:

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

These dots represent omitted code and should not be copied literally.

1.2 Up and Running

Even for experienced Rails developers, installing Ruby, Rails, and all the associated supporting software can be an exercise in frustration. Compounding the problem is the multiplicity of environments: different operating systems, version numbers, preferences in text editor and integrated development environment (IDE), etc. The Ruby on Rails™ Tutorial offers two recommended solutions to this problem. One possibility is to follow the full Learn Enough intro sequence mentioned in Section 1.1.1, which will automatically lead to a system configured for this tutorial.

The other possibility, recommended for newer users, is to sidestep such installation and configuration issues by using a cloud integrated development environment, or cloud IDE. The cloud IDE used in this tutorial runs inside an ordinary web browser, and hence works the same across different platforms, which is especially useful for operating systems (such as Windows) on which Rails development has historically been difficult. It also maintains the current state of your work, so you can take a break from the tutorial and come back to the system just as you left it.

1.2.1 Development Environment

Considering various idiosyncratic customizations, there are probably as many development environments as there are Rails programmers. To avoid this complexity, the Ruby on Rails™ Tutorial standardizes on the excellent cloud development environment Cloud9. In particular, I am pleased to be partnering with Cloud9 to offer a free development environment specifically tailored to the needs of this tutorial. The resulting Rails Tutorial Cloud9 workspace comes pre-configured with most of the software needed for professional-grade Rails development, including Ruby, RubyGems, and Git. (Indeed, the only big piece of software we’ll install separately is Rails itself, and this is intentional [Section 1.2.2].)

The cloud IDE also includes the three essential components needed to develop web applications: a text editor, a filesystem navigator, and a command-line terminal (Figure 1.1). Among other features, the cloud IDE text editor supports the “Find in Files” global search that I consider essential to navigating any large Ruby or Rails project.⁷ Finally, even if you decide not to use the cloud IDE exclusively in real life (and I certainly recommend learning other tools as well), it provides an excellent introduction to the general capabilities of text editors and other development tools.

7. For example, to find the definition of a function called foo, you can do a global search for “def foo”.

Figure 1.1: The anatomy of the cloud IDE.

Here are the steps for getting started with the cloud development environment:

1. Sign up for a free account at Cloud9.⁸ In order to prevent abuse, Cloud9 requires a valid credit card for signup, but the Rails Tutorial workspace is 100% free, and your card will not be charged.

8. c9.io/signup

2. Click on “Go to your Dashboard”.

3. Select “Create New Workspace”.

4. As shown in Figure 1.2, create a workspace called “rails-tutorial” (not “rails_tutorial”), set it to “Private to the people I invite”, and select the icon for the Rails Tutorial (not the icon for Ruby on Rails).

Figure 1.2: Creating a new workspace at Cloud9.

5. Click “Create workspace”.

6. After Cloud9 has finished provisioning the workspace, it should start automatically.

Because using two spaces for indentation is a near-universal convention in Ruby, I also recommend changing the editor to use two spaces instead of the default four. As shown in Figure 1.3, you can do this by clicking the gear icon in the upper right and then selecting “Code Editor (Ace)” to edit the “Soft Tabs” setting. (Note that this takes effect immediately; you don’t need to click a “Save” button.)

1.2.2 Installing Rails

The development environment from Section 1.2.1 includes all the software we need to get started, except for Rails itself. To install Rails, we’ll use the gem command provided by the RubyGems package manager, which involves typing the command shown in Listing 1.1 into your command-line terminal. (If developing on your local system, this means using a regular terminal window; if using the cloud IDE, this means using the command-line area shown in Figure 1.1)

Figure 1.3: Setting Cloud9 to use two spaces for indentation.

Listing 1.1: Installing Rails with a specific version number.

$ gem install rails -v 5.0.0

Here the -v flag ensures that the specified version of Rails gets installed, which is important for getting results consistent with this tutorial.

1.3 The First Application

Following a long tradition in computer programming, our goal for the first application is to write a “hello, world” program. In particular, we will create a simple application that displays the string “hello, world!” on a web page, both on our development environment (Section 1.3.4) and on the live web (Section 1.5).

Virtually all Rails applications start the same way, by running the rails new command. This handy command creates a skeleton Rails application in a directory of your choice. To get started, users not using the Cloud9 IDE recommended in Section 1.2.1 should make a workspace directory for your Rails projects if it doesn’t already exist (Listing 1.2) and then change into the directory. (Listing 1.2 uses the Unix commands cd and mkdir; see Box 1.3 if you are not already familiar with these commands.)

Listing 1.2: Making a workspace directory for Rails projects (unnecessary in the cloud).

Click here to view code image

$ cd                # Change to the home directory.
$ mkdir workspace   # Make a workspace directory.
$ cd workspace/     # Change into the workspace directory.

Box 1.3: A Crash Course on the Unix Command Line

For readers coming from Windows or (to a lesser but still significant extent) Macintosh OS X, the Unix command line may be unfamiliar. Luckily, if you are using the recommended cloud environment, you automatically have access to a Unix (Linux) command line running a standard shell command-line interface known as Bash.

The basic idea of the command line is simple: by issuing short commands, users can perform a large number of operations, such as creating directories (mkdir), moving and copying files (mv and cp), and navigating the filesystem by changing directories (cd). Although the command line may seem primitive to users mainly familiar with graphical user interfaces (GUIs), appearances are deceiving: the command line is one of the most powerful tools in the developer’s toolbox. Indeed, you will rarely see the desktop of an experienced developer without several open terminal windows running command-line shells.

The general subject is deep, but for the purposes of this tutorial we will need only a few of the most common Unix command-line commands, as summarized in Table 1.1. For a more thorough introduction to the Unix command line, see the first of the Learn Enough tutorials, Learn Enough Command Line to Be Dangerous.⁹

9. learnenough.com/command-line

The next step on both local systems and the cloud IDE is to create the first application using the command in Listing 1.3. Note that Listing 1.3 explicitly includes the Rails version number as part of the command. This ensures that the same version of Rails we installed in Listing 1.1 is used to create the first application’s file structure. (If the command in Listing 1.3 returns an error like “Could not find 'railties'”, it means you don’t have the right version of Rails installed, and you should double-check that you followed the command in Listing 1.1 exactly as written.)

Table 1.1: Some common Unix commands.

Listing 1.3: Running rails new (with a specific version number).

Click here to view code image

$ cd ˜/workspace
$ rails _5.0.0_ new hello_app
      create
      create README.md
      create Rakefile
      create config.ru
      create .gitignore
      create Gemfile
      create app
      create app/assets/config/manifest.js
      create app/assets/javascripts/application.js
      create app/assets/javascripts/cable.js
      create app/assets/stylesheets/application.css
      create app/channels/application_cable/channel.rb
      create app/channels/application_cable/connection.rb
      create app/controllers/application_controller.rb
      .
      .
      .
      create tmp/cache/assets
      create vendor/assets/javascripts
      create vendor/assets/javascripts/.keep
      create vendor/assets/stylesheets
      create vendor/assets/stylesheets/.keep
      remove config/initializers/cors.rb
         run bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Installing rake 11.1.2
Using concurrent-ruby 1.0.2
.
.
.
Your bundle is complete!
Use 'bundle show [gemname]' to see where a bundled gem is installed.
         run bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

As seen at the end of Listing 1.3, running rails new automatically runs the bundle install command after the file creation is done. We’ll discuss what this means in more detail starting in Section 1.3.1.

Figure 1.4: The directory structure for a newly created Rails app.

Notice how many files and directories the rails command creates. This standard directory and file structure (Figure 1.4) is one of the many advantages of Rails: it immediately gets you from zero to a functional (if minimal) application. Moreover, since the structure is common to all Rails apps, you can immediately get your bearings when looking at someone else’s code.

A summary of the default Rails files appears in Table 1.2. We’ll learn about most of these files and directories throughout the rest of this book. In particular, starting in Section 5.2.1 we’ll discuss the app/assets directory, part of the asset pipeline that makes it easy to organize and deploy assets such as cascading style sheets and JavaScript files.

Table 1.2: A summary of the default Rails directory structure.

1.3.1 Bundler

After creating a new Rails application, the next step is to use Bundler to install and include the gems needed by the app. As noted briefly in Section 1.3, Bundler is run automatically (via bundle install) by the rails command, but in this section we’ll make some changes to the default application gems and run Bundler again. This involves opening the Gemfile with a text editor. (With the cloud IDE, this involves clicking the arrow in the file navigator to open the sample app directory and double-clicking the Gemfile icon.) Although the exact version numbers and details may differ slightly, the results should look something like Figure 1.5 and Listing 1.4. (The code in this file is Ruby, but don’t worry at this point about the syntax; Chapter 4 will cover Ruby in more depth.) If the files and directories don’t appear as shown in Figure 1.5, click on the file navigator’s gear icon and select “Refresh File Tree”. (As a general rule, you should refresh the file tree any time files or directories don’t appear as expected.)¹⁰

10. This is a typical example of technical sophistication (Box 1.1).

Figure 1.5: The default Gemfile open in a text editor.

Listing 1.4: The default Gemfile in the hello_app directory.

Click here to view code image

source 'https://rubygems.org'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '˜> 5.0.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '˜> 3.0'
# Use SCSS for stylesheets
gem 'sass-rails', '˜> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '˜> 4.2'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes navigating your web application faster.
# Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '˜> 5.x'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '˜> 2.0'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '˜> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '˜> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution
  # and get a debugger console
  gem 'byebug', platform: :mri
end

group :development do
  # Access an IRB console on exception pages or by using
  # <%= console %> anywhere in the code.
  gem 'web-console'
  gem 'listen', '˜> 3.0.5'
  # Spring speeds up development by keeping your application running
  # in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '˜> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Many of these lines are commented out with the hash symbol # (Section 4.2.1); they are there to show you some commonly needed gems and to give examples of the Bundler syntax. For now, we won’t need any gems other than the defaults.

Unless you specify a version number to the gem command, Bundler will automatically install the latest requested version of the gem. This is the case, for example, in the code

Click here to view code image

gem 'sqlite3'

There are also two common ways to specify a gem version range, which allows us to exert some control over the version used by Rails. The first looks like this:

gem 'uglifier', '>= 1.3.0'

This installs the latest version of the uglifier gem (which handles file compression for the asset pipeline) as long as it’s greater than or equal to version 1.3.0—even if it’s, say, version 7.2. The second method looks like this:

gem 'coffee-rails', '˜> 4.0.0'

This installs the gem coffee-rails as long as it’s newer than version 4.0.0 and not newer than 4.1. In other words, the >= notation always installs the latest gem, whereas the ˜> 4.0.0 notation only installs updated gems where the last digit differs (e.g., from 4.0.0 to 4.0.1), but the digits before that releases (e.g., from 4.0 to 4.1).¹¹ Unfortunately, experience shows that even minor point releases can break the application, so for the Ruby on Rails™ Tutorial we’ll err on the side of caution by including exact version numbers for all gems. You are welcome to use the most up-to-date version of any gem, including using the ˜> construction in the Gemfile (which I generally recommend for more advanced users), but be warned that this may cause the tutorial to act unpredictably.

11. Similarly, ˜> 4.0 would install version 4.9 of a gem but not 5.0. This is especially useful if the project in question uses semantic versioning (also called “semver”), which is a convention for numbering releases designed to minimize the chances of breaking software dependencies.

Converting the Gemfile in Listing 1.4 to use exact gem versions results in the code shown in Listing 1.5. (You can determine the exact version number for each gem by running gem list <gem name> at the command line, but Listing 1.5 saves you the trouble.) Note that we’ve also taken this opportunity to arrange for the sqlite3 gem to be included only in a development or test environment (Section 7.1.1), which prevents potential conflicts with the database used by Heroku (Section 1.5). Important note: For all the Gemfiles in this book, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed below (although they should be identical if you are reading this online).

Listing 1.5: A Gemfile with an explicit version for each Ruby gem.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',        '5.0.0'
gem 'puma',         '3.4.0'
gem 'sass-rails',   '5.0.5'
gem 'uglifier',     '3.0.0'
gem 'coffee-rails', '4.2.1'
gem 'jquery-rails', '4.1.1'
gem 'turbolinks',   '5.0.0'
gem 'jbuilder',     '2.4.1'

group :development, :test do
  gem 'sqlite3', '1.3.11'
  gem 'byebug',  '9.0.0', platform: :mri
end

group :development do
  gem 'web-console',           '3.1.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '1.7.2'
  gem 'spring-watcher-listen', '2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Once you’ve placed the contents of Listing 1.5 into the application’s Gemfile, install the gems using bundle install:¹²

12. As noted in Table 3.1, you can even leave off install, as the bundle command by itself is an alias for bundle install.

Click here to view code image

$ cd hello_app/
$ bundle install
Fetching source index for https://rubygems.org/
.
.
.

The bundle install command might take a few moments, but when it’s done our application will be ready to run.

By the way, when you run bundle install it’s possible that you’ll get a message saying you need to run bundle update first. In this case you should run bundle update first. (Learning not to panic when things don’t go exactly as planned is a key part of technical sophistication, and you’ll be amazed at how often the “error” message contains the exact instructions you need to fix the problem at hand.)

1.3.2 rails server

Thanks to running rails new in Section 1.3 and bundle install in Section 1.3.1, we already have an application we can run—but how? Happily, Rails comes with a command-line program, or script, that runs a local webserver to assist us in developing our application. The exact command depends on the environment you’re using: on a local system, you just run rails server (Listing 1.6), whereas on Cloud9 you need to supply an additional IP binding address and port number to tell the Rails server the address it can use to make the application visible to the outside world (Listing 1.7).¹³ (Cloud9 uses the special environment variables $IP and $PORT to assign the IP address and port number dynamically. If you want to see the values of these variables, type echo $IP or echo $PORT at the command line.)

13. Normally, websites run on port 80, but this usually requires special privileges, so it’s conventional to use a less restricted higher-numbered port for the development server.

If your system complains about the lack of a JavaScript runtime, visit the execjs page at GitHub for a list of possibilities. I particularly recommend installing Node.js.

Listing 1.6: Running the Rails server on a local machine.

Click here to view code image

$ cd ˜/workspace/hello_app/
$ rails server
=> Booting Puma
=> Rails application starting on http://localhost:3000
=> Run 'rails server -h' for more startup options
=> Ctrl-C to shutdown server

Listing 1.7: Running the Rails server on the cloud IDE.

Click here to view code image

$ cd ˜/workspace/hello_app/
$ rails server -b $IP -p $PORT
=> Booting Puma
=> Rails application starting on http://0.0.0.0:8080
=> Run 'rails server -h' for more startup options
=> Ctrl-C to shutdown server

Whichever option you choose, I recommend running the rails server command in a second terminal tab so that you can still issue commands in the first tab, as shown in Figure 1.6 and Figure 1.7. (If you already started a server in your first tab, press Ctrl-C to shut it down.)¹⁴ On a local server, paste the URL http://0.0.0.0:3000 into the address bar of your browser; on the cloud IDE, go to Share and click on the Application address to open it (Figure 1.8). In either case, the result should look something like Figure 1.9.

14. Here “C” refers to the character on the keyboard, not the capital letter, so there’s no need to hold down the Shift key to get a capital “C”.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. According to the default Rails page, what is the version of Ruby on your system? Confirm by running ruby -v at the command line.

2. What is the version of Rails? Confirm that it matches the version installed in Listing 1.1.

Figure 1.6: Opening a new terminal tab.

Figure 1.7: Running the Rails server in a separate tab.

Figure 1.8: Sharing the local server running on the cloud workspace.

1.3.3 Model-View-Controller (MVC)

Even at this early stage, it’s helpful to get a high-level overview of how Rails applications work, as illustrated in Figure 1.10. You might have noticed that the standard Rails application structure (Figure 1.4) has an application directory called app/ containing three subdirectories: models, views, and controllers. This is a hint that Rails follows the model-view-controller (MVC) architectural pattern, which enforces a separation between the data in the application (such as user information) and the code used to display it, which is a common way of structuring a graphical user interface (GUI).

When interacting with a Rails application, a browser sends a request that is received by a webserver and passed on to a Rails controller, which is in charge of what to do next. In some cases, the controller will immediately render a view, which is a template that gets converted to HTML and sent back to the browser. More commonly for dynamic sites, the controller interacts with a model, which is a Ruby object that represents an element of the site (such as a user) and is in charge of communicating with the database. After invoking the model, the controller then renders the view and returns the complete web page to the browser as HTML.

Figure 1.9: The default Rails page served by rails server.

If this discussion seems a bit abstract right now, don’t worry; we’ll cover these ideas in more detail later in this book. In particular, Section 1.3.4 shows a first tentative application of MVC, while Section 2.2.2 includes a more detailed discussion of MVC in the context of the toy app. Finally, the full sample app will use all aspects of MVC: we’ll cover controllers and views starting in Section 3.2, models starting in Section 6.1, and we’ll see all three working together in Section 7.1.2.

Figure 1.10: A schematic representation of the model-view-controller (MVC) architecture.

1.3.4 Hello, World!

As a first application of the MVC framework, we’ll make a wafer-thin change to the first app by adding a controller action to render the string “hello, world!” to replace the default Rails page from Figure 1.9. (We’ll learn more about controller actions starting in Section 2.2.2.)

As implied by their name, controller actions are defined inside controllers. We’ll call our action hello and place it in the Application controller. Indeed, at this point the Application controller is the only controller we have, which you can verify by running to view the current controllers. (We’ll start creating our own controllers in Chapter 2.) Listing 1.8 shows the resulting definition of hello, which uses the render function to return the HTML text “hello, world!”. (Don’t worry about the Ruby syntax right now; it will be covered in more depth in Chapter 4.)

Click here to view code image

$ ls app/controllers/*_controller.rb

Listing 1.8: Adding a hello action to the Application controller.
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
    render html: "hello, world!"
  end
end

Having defined an action that returns the desired string, we need to tell Rails to use that action instead of the default page in Figure 1.9. To do this, we’ll edit the Rails router, which sits in front of the controller in Figure 1.10 and determines where to send requests that come in from the browser. (I’ve omitted the router from Figure 1.10 for simplicity, but we’ll discuss it in more detail starting in Section 2.2.2.) In particular, we want to change the default page, the root route, which determines the page that is served on the root URL. Because it’s the URL for an address like http://www.example.com/ (where nothing comes after the final forward slash), the root URL is often referred to as / (“slash”) for short.

As seen in Listing 1.9, the Rails routes file (config/routes.rb) includes a comment directing us to the Rails Guide on Routing, which includes instructions on how to define the root route. The syntax looks like this:

Click here to view code image

root 'controller_name#action_name'

In the present case, the controller name is application and the action name is hello, which results in the code shown in Listing 1.10.

Listing 1.9: The default routing file (formatted to fit).
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  # For details on the DSL available within this file,
  # see http://guides.rubyonrails.org/routing.html
end

Listing 1.10: Setting the root route.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'application#hello'
end

With the code from Listing 1.8 and Listing 1.10, the root route returns “hello, world!” as required (Figure 1.11).¹⁵ Hello, world!

15. The base URL for the Rails Tutorial Cloud9 shared URLs has changed from rails-tutorial-c9-mhartl.c9.io to rails-tutorial-mhartl.c9users.io, but in many cases the screenshots are identical, so the browser address bar will show old-style URLs in some figures (such as Figure 1.11). This is the sort of minor discrepancy you can resolve using your technical sophistication (Box 1.1).

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Change the content of the hello action in Listing 1.8 to read “hola, mundo!” instead of “hello, world!”.

2. Show that Rails supports non-ASCII characters by including an inverted exclamation point, as in “¡Hola, mundo!” (Figure 1.12).¹⁶ To get a ¡ character on a Mac, you can use Option-1; otherwise, you can always copy-and-paste the character into your editor.

16. Your editor may display a message like “invalid multibyte character”, but this is not a cause for concern. You can Google the error message if you want to learn how to make it go away.

3. By following the example of the hello action in Listing 1.8, add a second action called goodbye that renders the text “goodbye, world!”. Edit the routes file from Listing 1.10 so that the root route goes to goodbye instead of to hello (Figure 1.13).

Figure 1.11: Viewing “hello, world!” in the browser.

1.4 Version Control with Git

Now that we have a working “hello, world” application, we’ll take a moment for a step that, while technically optional, would be viewed by experienced software developers as practically essential: placing our application source code under version control. Version control systems allow us to track changes to our project’s code, collaborate more easily, and roll back any inadvertent errors (such as accidentally deleting files). Knowing how to use a version control system is a required skill for every professional-grade software developer.

Figure 1.12: Changing the root route to return “¡Hola, mundo!”.

There are many options for version control, but the Rails community has largely standardized on Git, a distributed version control system originally developed by Linus Torvalds to host the Linux kernel. Git is a large subject, and we’ll only be scratching the surface in this book; for a more thorough introduction, see Learn Enough Git to Be Dangerous.¹⁷

17. learnenough.com/git

Putting your source code under version control with Git is strongly recommended, not only because it’s nearly a universal practice in the Rails world, but also because it will allow you to back up and share your code more easily (Section 1.4.3) and deploy your application right here in the first chapter (Section 1.5).

Figure 1.13: Changing the root route to return “goodbye, world!”.

1.4.1 Installation and Setup

The cloud IDE recommended in Section 1.2.1 includes Git by default, so no installation is necessary in this case. Otherwise, Learn Enough Git to Be Dangerous includes instructions for installing Git on your system.

First-Time System Setup

Before using Git, you should perform a couple of one-time setup steps. These are system setups, meaning you only have to do them once per computer:

Click here to view code image

$ git config --global user.name "Your Name"
$ git config --global user.email your.email@example.com

Note that the name and email address you use in your Git configuration will be available in any repositories you make public.

First-Time Repository Setup

Now we come to some steps that are necessary each time you create a new repository (sometimes called a repo for short). The first step is to navigate to the root directory of the first app and initialize a new repository:

Click here to view code image

$ git init
Initialized empty Git repository in /home/ubuntu/workspace/hello_app/.git/

The next step is to add all the project files to the repository using git add -A:

$ git add -A

This command adds all the files in the current directory apart from those that match the patterns in a special file called .gitignore. The rails new command automatically generates a .gitignore file appropriate to a Rails project, but you can add additional patterns as well.¹⁸

18. Although we’ll never need to edit it in the main tutorial, an example of adding a rule to the .gitignore file appears in Section 3.6.2, which is part of the optional advanced testing setup in Section 3.6.

The added files are initially placed in a staging area, which contains pending changes to our project. We can see which files are in the staging area using the status command:

Click here to view code image

$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

  new file:   .gitignore
  new file:   Gemfile
  new file:   Gemfile.lock
  new file:   README.md
  new file:   Rakefile
  .
  .
  .

To tell Git we want to keep the changes, we use the commit command:

Click here to view code image

$ git commit -m "Initialize repository"
[master (root-commit) df0a62f] Initialize repository
.
.
.

The -m flag lets us add a message for the commit; if we omit -m, Git will open the system’s default editor and have us enter the message there. (All the examples in this book will use the -m flag.)

It is important to note that Git commits are local, recorded only on the machine on which the commits occur. We’ll see how to push the changes up to a remote repository (using git push) in Section 1.4.4.

By the way, we can see a list of the commit messages using the log command:

Click here to view code image

commit af72946fbebc15903b2770f92fae9081243dd1a1
Author: Michael Hartl <michael@michaelhartl.com>
Date:   Thu May 12 19:25:07 2016 +0000

    Initialize repository

Depending on the length of the repository’s log history, you may have to type q to quit. (As explained in Learn Enough Git to Be Dangerous, git log uses the less interface covered in Learn Enough Command Line to Be Dangerous.)

1.4.2 What Good Does Git Do You?

If you’ve never used version control before, it may not be entirely clear at this point what good it does you, so let me give just one example. Suppose you’ve made some accidental changes, such as (D’oh!) deleting the critical app/controllers/ directory.

Click here to view code image

$ ls app/controllers/
application_controller.rb concerns/
$ rm -rf app/controllers/
$ ls app/controllers/
ls: app/controllers/: No such file or directory

Here we’re using the Unix ls command to list the contents of the app/controllers/ directory and the rm command to remove it (Table 1.1). As noted in Learn Enough Command Line to Be Dangerous, the -rf flag means “recursive force”, which recursively removes all files, directories, subdirectories, and so on, without asking for explicit confirmation of each deletion.

Let’s check the status to see what changed:

Click here to view code image

$ git status
On branch master
Changed but not updated:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

      deleted:    app/controllers/application_controller.rb

no changes added to commit (use "git add" and/or "git commit -a")

We see here that a file has been deleted, but the changes are only on the “working tree”; they haven’t been committed yet. This means we can still undo the changes using the checkout command with the -f flag to force overwriting the current changes:

Click here to view code image

$ git checkout -f
$ git status
# On branch master
nothing to commit (working directory clean)
$ ls app/controllers/
application_controller.rb concerns/

The missing files and directories are back. That’s a relief!

1.4.3 Bitbucket

Now that we’ve put our project under version control with Git, it’s time to push our code up to Bitbucket, a site optimized for hosting and sharing Git repositories. (Learn Enough Git to Be Dangerous uses GitHub, but see Box 1.4 to learn the reasons why this tutorial uses Bitbucket instead.) Putting a copy of your Git repository at Bitbucket serves two purposes: it’s a full backup of your code (including the full history of commits), and it makes any future collaboration much easier.

Box 1.4: GitHub and Bitbucket

By far the two most popular sites for hosting Git repositories are GitHub and Bitbucket. The two services share many similarities: both sites allow for Git repository hosting and collaboration, as well as offering convenient ways to browse and search repositories. The important differences (from the perspective of this tutorial) are that GitHub offers unlimited free repositories (with collaboration) for open-source repositories while charging for private repos, whereas Bitbucket allows unlimited free private repos while charging for more than a certain number of collaborators. Which service you use for a particular repo thus depends on your specific needs.

Learn Enough Git to Be Dangerous (and some previous editions of this tutorial) use GitHub because of its emphasis on supporting open-source code, but growing concerns about security have led me to recommend that all web application repositories be private by default. The issue is that such repositories might contain potentially sensitive information such as cryptographic keys or passwords, which could be used to compromise the security of a site running the code. It is possible, of course, to arrange for this information to be handled securely (by having Git ignore it, for example), but this is error-prone and requires significant expertise.

As it happens, the sample application created in this tutorial is safe for exposure on the web, but it is dangerous to rely on this fact in general. Thus, to be as secure as possible, we will err on the side of caution and use private repositories by default. Since GitHub charges for private repositories while Bitbucket offers an unlimited number for free, for our present purposes Bitbucket is a better fit than GitHub.

(By the way, recently a third major Git hosting company has emerged, called GitLab. Originally designed principally as an open-source Git tool you hosted yourself, GitLab now offers a hosted version as well, and in fact allows for unlimited public and private repositories. This makes GitLab an excellent alternative to GitHub or Bitbucket for future projects.)

Getting started with Bitbucket is straightforward, though it may take a little technical sophistication (Box 1.1) to get everything to work just right:

1. Sign up for a Bitbucket account if you don’t already have one.

2. Copy your public key to your clipboard. As indicated in Listing 1.11, users of the cloud IDE can view their public key using the cat command, which can then be selected and copied. If you’re using your own system and see no output when running the command in Listing 1.11, follow the instructions on how to install a public key on your Bitbucket account.

3. Add your public key to Bitbucket by clicking on the avatar image in the upper right and selecting “Manage account” and then “SSH keys” (Figure 1.14).

Listing 1.11: Printing the public key using cat.

$ cat ˜/.ssh/id_rsa.pub

Once you’ve added your public key, click on “Create” to create a new repository, as shown in Figure 1.15. When filling in the information for the project, take care to leave the box next to “This is a private repository.” checked. After clicking “Create repository”, follow the instructions under “Command line > I have an existing project”, which should look something like Listing 1.12. (If it doesn’t look like Listing 1.12, it might be because the public key didn’t get added correctly, in which case I suggest trying that step again.) When pushing up the repository, answer yes if you see the question “Are you sure you want to continue connecting (yes/no)?”

Figure 1.14: Adding the SSH public key.

Figure 1.15: Creating the first app repository at Bitbucket.

Listing 1.12: Adding Bitbucket and pushing up the repository.

Click here to view code image

$ git remote add origin git@bitbucket.org:<username>/hello_app.git
$ git push -u origin --all

The commands in Listing 1.12 first tell Git that you want to add Bitbucket as the origin for your repository and then push your repository up to the remote origin. (Don’t worry about what the -u flag does; if you’re curious, do a web search for “git set upstream.”) Of course, you should replace <username> with your actual username. For example, the command I ran was

Click here to view code image

$ git remote add origin git@bitbucket.org:railstutorial/hello_app.git

The result is a page at Bitbucket for the hello_app repository, with file browsing, full commit history, and lots of other goodies (Figure 1.16).¹⁹

19. Because of how my public keys are set up on Cloud9, I created the repository as railstutorial and then added my main account, mhartl, as a collaborator. As a result, I can make commits under either account name.

1.4.4 Branch, Edit, Commit, Merge

If you’ve followed the steps in Section 1.4.3, you might notice that Bitbucket automatically rendered the repository’s README file, as shown in Figure 1.16. This file, called README.md, was generated automatically by the command in Listing 1.3. As indicated by the filename extension .md, it is written in Markdown,²⁰ a human-readable markup language designed to be easy to convert to HTML—which is exactly what Bitbucket has done.

20. See Learn Enough Text Editor to Be Dangerous and Learn Enough Git to Be Dangerous for more information about Markdown.

Figure 1.16: A Bitbucket repository page.

Figure 1.17: Bitbucket’s rendering of the default Rails README.

This automatic rendering of the README is convenient, but of course it would be better if we tailored the contents of the file to the project at hand. In this section, we’ll customize the README by adding some Rails Tutorial–specific content. In the process, we’ll see a first example of the branch, edit, commit, merge workflow that I recommend using with Git.²¹

21. For a convenient way to visualize Git repositories, take a look at Atlassian’s SourceTree app.

Branch

Git is incredibly good at making branches, which are effectively copies of a repository where we can make (possibly experimental) changes without modifying the parent files. In most cases, the parent repository is the master branch, and we can create a new topic branch by using checkout with the -b flag:

Click here to view code image

$ git checkout -b modify-README
Switched to a new branch 'modify-README'
$ git branch
  master
* modify-README

Here the second command, git branch, just lists all the local branches, and the asterisk * identifies which branch we’re currently on. Note that git checkout -b modify-README both creates a new branch and switches to it, as indicated by the asterisk in front of the modify-README branch.

The full value of branching only becomes clear when working on a project with multiple developers,²² but branches are helpful even for a single-developer tutorial such as this one. In particular, because the master branch is insulated from any changes we make to the topic branch, even if we really mess things up we can always abandon the changes by checking out the master branch and deleting the topic branch. We’ll see how to do this at the end of the section.

22. See, for example, the section on Collaborating in Learn Enough Git to Be Dangerous.

By the way, for a change as small as this one I wouldn’t normally bother with a new branch (opting instead to work directly on the master branch), but in the present context it’s a prime opportunity to start practicing good habits.

Edit

After creating the topic branch, we’ll edit the README to add custom content, as shown in Listing 1.13.

Listing 1.13: The new README file.
README.md

Click here to view code image

# Ruby on Rails Tutorial

## "hello, world!"

This is the first application for the
[*Ruby on Rails Tutorial*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/). Hello, world!

Commit

With the changes made, we can take a look at the status of our branch:

Click here to view code image

$ git status
On branch modify-README
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

At this point, we could use git add -A as in Section 1.4.1, but git commit provides the -a flag as a shortcut for the (very common) case of committing all modifications to existing files:

Click here to view code image

$ git commit -a -m "Improve the README file"
[modify-README 9dc4f64] Improve the README file
1 file changed, 5 insertions(+), 22 deletions(-)

Be careful about using the -a flag improperly; if you have added any new files to the project since the last commit, you still have to tell Git about them using git add -A first.

Note that we write the commit message in the present tense (and, technically speaking, the imperative mood). Git models commit as a series of patches, and in this context it makes sense to describe what each commit does, rather than what it did. Moreover, this usage matches up with the commit messages generated by Git commands themselves. See Committing to Git from Learn Enough Git to Be Dangerous for more information.

Merge

Now that we’ve finished making our changes, we’re ready to merge the results back into our master branch:

Click here to view code image

$ git checkout master
Switched to branch 'master'
$ git merge modify-README
Updating af72946..9dc4f64
Fast-forward
README.md | 27 +++++----------------------
1 file changed, 5 insertions(+), 22 deletions(-)

Note that the Git output frequently includes things like 34f06b7, which are related to Git’s internal representation of repositories. Your exact results will differ in these details, but otherwise should essentially match the output shown above.

After you’ve merged in the changes, you can tidy up your branches by deleting the topic branch using git branch -d if you’re done with it:

Click here to view code image

$ git branch -d modify-README
Deleted branch modify-README (was 9dc4f64).

This step is optional, and in fact it’s quite common to leave the topic branch intact. This way you can switch back and forth between the topic and master branches, merging in changes every time you reach a natural stopping point.

As mentioned above, it’s also possible to abandon your topic branch changes, in this case with git branch -D:

Click here to view code image

# For illustration only; don't do this unless you mess up a branch
$ git checkout -b topic-branch
$ <really mess up the branch>
$ git add -A
$ git commit -a -m "Make major mistake"
$ git checkout master
$ git branch -D topic-branch

Unlike the -d flag, the -D flag will delete the branch even though we haven’t merged in the changes.

Push

Now that we’ve updated the README, we can push the changes up to Bitbucket to see the result. Since we have already done one push (Section 1.4.3), on most systems we can omit origin master, and simply run git push:

$ git push

As with the default README, Bitbucket nicely converts the Markdown in our updated README to HTML (Figure 1.18).

Figure 1.18: The improved README file at Bitbucket.

1.5 Deploying

Even though this is only the first chapter, we’re already going to deploy our (nearly empty) Rails application to production. This step is optional, but deploying early and often allows us to catch any deployment problems early in our development cycle. The alternative—deploying only after laborious effort sealed away in a development environment—often leads to terrible integration headaches when launch time comes.²³

23. Though it shouldn’t matter for the example applications in the Rails Tutorial, if you’re worried about accidentally making your app public too soon, there are several options; see Section 1.5.4 for one.

Deploying Rails applications used to be a pain, but the Rails deployment ecosystem has matured rapidly in the past few years, and now there are several great options. These include shared hosts or virtual private servers running Phusion Passenger (a module for the Apache and Nginx²⁴ webservers), full-service deployment companies such as Engine Yard and Rails Machine, and cloud deployment services such as Engine Yard Cloud and Heroku.

24. Pronounced “Engine X.”

My favorite Rails deployment option is Heroku, which is a hosted platform built specifically for deploying Rails and other web applications. Heroku makes deploying Rails applications ridiculously easy—as long as your source code is under version control with Git. (This is yet another reason to follow the Git setup steps in Section 1.4 if you haven’t already.) In addition, for many purposes, including for this tutorial, Heroku’s free tier is more than sufficient.

The rest of this section is dedicated to deploying our first application to Heroku. Some of the ideas are fairly advanced, so don’t worry about understanding all the details; what’s important is that by the end of the process we’ll have deployed our application to the live web.

1.5.1 Heroku Setup

Heroku uses the PostgreSQL database (pronounced “post-gres-cue-ell” and often called “Postgres” for short), which means that we need to add the pg gem in the production environment to allow Rails to talk to Postgres:²⁵

25. Generally speaking, it’s a good idea for the development and production environments to match as closely as possible, which includes using the same database, but for the purposes of this tutorial we’ll always use SQLite locally and PostgreSQL in production. See Section 3.1 for more information.

Click here to view code image

group :production do
  gem 'pg', '0.18.4'
end

Also be sure to incorporate the changes made in Listing 1.5 preventing the sqlite3 gem from being included in a production environment, since SQLite isn’t supported at Heroku:

Click here to view code image

group :development, :test do
gem 'sqlite3', '1.3.11'
  gem 'byebug', '9.0.0', platform: :mri
end

The resulting Gemfile appears as in Listing 1.14.

Listing 1.14: A Gemfile with added and rearranged gems.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',        '5.0.0'
gem 'puma',         '3.4.0'
gem 'sass-rails',   '5.0.5'
gem 'uglifier',     '3.0.0'
gem 'coffee-rails', '4.2.1'
gem 'jquery-rails', '4.1.1'
gem 'turbolinks',   '5.0.0'
gem 'jbuilder',     '2.4.1'

group :development, :test do
gem 'sqlite3', '1.3.11'
  gem 'byebug', '9.0.0', platform: :mri
end

group :development do
  gem 'web-console',           '3.1.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '1.7.2'
  gem 'spring-watcher-listen', '2.0.0'
end

group :production do
  gem 'pg', '0.18.4'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

To prepare the system for deployment to production, we run bundle install with a special flag to prevent the local installation of any production gems (which in this case consists of the pg gem):

Click here to view code image

$ bundle install --without production

Because the only gem added in Listing 1.14 is restricted to a production environment, right now this command doesn’t actually install any additional local gems, but it’s needed to update Gemfile.lock with the pg gem. We can commit the resulting change as follows:

Click here to view code image

$ git commit -a -m "Update Gemfile for Heroku"

Next we have to create and configure a new Heroku account. The first step is to sign up for Heroku. Then check to see if your system already has the Heroku command-line client installed:

$ heroku version

Those using the cloud IDE should see the Heroku version number, indicating that the heroku CLI is available, but on other systems it may be necessary to install it using the Heroku Toolbelt.²⁶

26. toolbelt.heroku.com

Once you’ve verified that the Heroku command-line interface is installed, use the heroku command to log in and add your SSH key:

Click here to view code image

$ heroku login $ heroku keys:add

Finally, use the heroku create command to create a place on the Heroku servers for the sample app to live (Listing 1.15).

Listing 1.15: Creating a new application at Heroku.

Click here to view code image

$ heroku create
Creating app... done, fathomless-beyond-39164
https://damp-fortress-5769.herokuapp.com/ |
https://git.heroku.com/damp-fortress-5769.git

The heroku command creates a new subdomain just for our application, available for immediate viewing. There’s nothing there yet, though, so let’s get busy deploying.

1.5.2 Heroku Deployment, Step One

To deploy the application, the first step is to use Git to push the master branch up to Heroku:

$ git push heroku master

(You may see some warning messages, which you should ignore for now. We’ll discuss them further in Section 7.5.)

1.5.3 Heroku Deployment, Step Two

There is no step two! We’re already done. To see your newly deployed application, visit the address that you saw when you ran heroku create (i.e., Listing 1.15). (If you’re working on your local machine instead of the cloud IDE, you can also use heroku open.) The result appears in Figure 1.19. The page is identical to Figure 1.11, but now it’s running in a production environment on the live web.

Figure 1.19: The first Rails Tutorial application running on Heroku.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By making the same change as in Section 1.3.4, arrange for your production app to display “hola, mundo!”.

2. As in Section 1.3.4, arrange for the root route to display the result of the goodbye action. When deploying, confirm that you can omit master in the Git push, as in git push heroku.

1.5.4 Heroku Commands

There are many Heroku commands, and we’ll barely scratch the surface in this book. Let’s take a minute to show just one of them by renaming the application as follows:

Click here to view code image

$ heroku rename rails-tutorial-hello

Don’t use this name yourself; it’s already taken by me! In fact, you probably shouldn’t bother with this step right now; using the default address supplied by Heroku is fine. But if you do want to rename your application, you can arrange for it to be reasonably secure by using a random or obscure subdomain, such as the following:

hwpcbmze.herokuapp.com
seyjhflo.herokuapp.com
jhyicevg.herokuapp.com

With a random subdomain like this, someone could visit your site only if you gave them the address. (By the way, as a preview of Ruby’s compact awesomeness, here’s the code I used to generate the random subdomains:

Click here to view code image

('a'..'z').to_a.shuffle[0..7].join

Pretty sweet.)

In addition to supporting subdomains, Heroku also supports custom domains. (In fact, the Ruby on Rails Tutorial site lives at Heroku; if you’re reading this book online, you’re looking at a Heroku-hosted site right now!) See the Heroku documentation for more information about custom domains and other Heroku topics.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Run heroku help to see a list of Heroku commands. What is the command to display logs for an app?

2. Use the command identified in the previous exercise to inspect the activity on your application. What was the most recent event? (This command is often useful when debugging production apps.)

1.6 Conclusion

We’ve come a long way in this chapter: installation, development environment setup, version control, and deployment. In the next chapter, we’ll build on the foundation from Chapter 1 to make a database-backed toy app, which will give us our first real taste of what Rails can do.

If you’d like to share your progress at this point, feel free to send a tweet or Facebook status update with something like this:

I’m learning Ruby on Rails with the @railstutorial!
http://www.railstutorial.org/

I also recommend signing up for the Rails Tutorial email list,²⁷ which will ensure that you receive priority updates (and exclusive coupon codes) regarding the Ruby on Rails™ Tutorial.

27. railstutorial.org/email

1.6.1 What We Learned in This Chapter

• Ruby on Rails is a web development framework written in the Ruby programming language.

• Installing Rails, generating an application, and editing the resulting files is easy using a pre-configured cloud environment.

• Rails comes with a command-line command called rails that can generate new applications (rails new) and run local servers (rails server).

• We added a controller action and modified the root route to create a “hello, world” application.

• We protected against data loss while enabling collaboration by placing our application source code under version control with Git and pushing the resulting code to a private repository at Bitbucket.

• We deployed our application to a production environment using Heroku.

Chapter 2. A Toy App

In this chapter, we’ll develop a toy demo application to show off some of the power of Rails. The purpose is to get a high-level overview of Ruby on Rails programming (and web development in general) by rapidly generating an application using scaffold generators, which create a large amount of functionality automatically. As discussed in Box 1.2, the rest of the book will take the opposite approach, developing a full sample application incrementally and explaining each new concept as it arises, but for a quick overview (and some instant gratification) there is no substitute for scaffolding. The resulting toy app will allow us to interact with it through its URLs, giving us insight into the structure of a Rails application, including a first example of the REST architecture favored by Rails.

As with the forthcoming sample application, the toy app will consist of users and their associated microposts (thus constituting a minimalist Twitter-style app). The functionality will be utterly under-developed, and many of the steps will seem like magic, but worry not: the full sample app will develop a similar application from the ground up starting in Chapter 3, and I will provide plentiful forward-references to later material. In the mean time, have patience and a little faith—the whole point of this tutorial is to take you beyond this superficial, scaffold-driven approach to achieve a deeper understanding of Rails.

2.1 Planning the Application

In this section, we’ll outline our plans for the toy application. As in Section 1.3, we’ll start by generating the application skeleton using the rails new command with a specific Rails version number:

Click here to view code image

$ cd ˜/workspace
$ rails _5.0.0_ new toy_app
$ cd toy_app/

If you’re using the cloud IDE as recommended in Section 1.2.1, note that this second app can be created in the same workspace as the first. It is not necessary to create a new workspace. In order to get the files to appear, you may need to click the gear icon in the file navigator area and select “Refresh File Tree”.

Next, we’ll use a text editor to update the Gemfile needed by Bundler with the contents of Listing 2.1. Important note: For all the Gemfiles in this book, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed below (although they should be identical if you are reading this online).

Listing 2.1: A Gemfile for the toy app.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',        '5.0.0'
gem 'puma',         '3.4.0'
gem 'sass-rails',   '5.0.5'
gem 'uglifier',     '3.0.0'
gem 'coffee-rails', '4.2.1'
gem 'jquery-rails', '4.1.1'
gem 'turbolinks',   '5.0.0'
gem 'jbuilder',     '2.4.1'

group :development, :test do
  gem 'sqlite3', '1.3.11'
  gem 'byebug',  '9.0.0', platform: :mri
end

group :development do
  gem 'web-console',           '3.1.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '1.7.2'
  gem 'spring-watcher-listen', '2.0.0'
end

group :production do
  gem 'pg', '0.18.4'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Note that Listing 2.1 is identical to Listing 1.14.

As in Section 1.5.1, we’ll install the local gems while suppressing the installation of production gems using the --without production option:

Click here to view code image

$ bundle install --without production

As noted in Section 1.3.1, you may need to run bundle update as well (Box 1.1).

Finally, we’ll put the toy app under version control with Git:

Click here to view code image

$ git init
$ git add -A
$ git commit -m "Initialize repository"

You should also create a new repository by clicking on the “Create” button at Bitbucket (Figure 2.1), and then push up to the remote repository:

Click here to view code image

$ git remote add origin git@bitbucket.org:<username>/toy_app.git
$ git push -u origin --all

Finally, it’s never too early to deploy, which I suggest doing by following the same “hello, world!” steps from Section 1.3.4, as shown in Listing 2.2 and Listing 2.3.

Listing 2.2: Adding a hello action to the Application controller.
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
     render html: "hello, world!"
  end
end

Figure 2.1: Creating the toy app repository at Bitbucket.

Listing 2.3: Setting the root route.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'application#hello'
end

Then commit the changes and push up to Heroku:

Click here to view code image

$ git commit -am "Add hello"
$ heroku create
$ git push heroku master

(As in Section 1.5, you may see some warning messages, which you should ignore for now. We’ll deal with them in Section 7.5.) Apart from the URL of the Heroku app, the result should be the same as in Figure 1.19.

Now we’re ready to start making the app itself. The typical first step when making a web application is to create a data model, which is a representation of the structures needed by our application. In our case, the toy app will be a Twitter-style microblog, with only users and short (micro)posts. Thus, we’ll begin with a model for users of the app (Section 2.1.1), and then we’ll add a model for microposts (Section 2.1.2).

2.1.1 A Toy Model for Users

There are as many choices for a user data model as there are different registration forms on the web; for simplicity, we’ll go with a distinctly minimalist approach. Users of our toy app will have a unique identifier called id (of type integer), a publicly viewable name (of type string), and an email address (also of type string) that will double as a unique username. A summary of the data model for users appears in Figure 2.2.

As we’ll see starting in Section 6.1.1, the label users in Figure 2.2 corresponds to a table in a database, and the id, name, and email attributes are columns in that table.

2.1.2 A Toy Model for Microposts

The core of the micropost data model is even simpler than the one for users: a micropost has only an id and a content field for the micropost’s text (of type text).¹ There’s an additional complication, though: we want to associate each micropost with a particular user. We’ll accomplish this by recording the user_id of the owner of the post. The results are shown in Figure 2.3.

Figure 2.2: The data model for users.

1. Because microposts are short by design, the string type is actually big enough to contain them, but using text better expresses our intent, while also giving us greater flexibility should we ever wish to relax the length constraint.

Figure 2.3: The data model for microposts.

We’ll see in Section 2.3.3 (and more fully in Chapter 13) how this user_id attribute allows us to succinctly express the notion that a user potentially has many associated microposts.

2.2 The Users Resource

In this section, we’ll implement the users data model in Section 2.1.1, along with a web interface to that model. The combination will constitute a Users resource, which will allow us to think of users as objects that can be created, read, updated, and deleted through the web via the HTTP protocol. As promised in the introduction, our Users resource will be created by a scaffold generator program, which comes standard with each Rails project. I urge you not to look too closely at the generated code; at this stage, it will only serve to confuse you.

Rails scaffolding is generated by passing the scaffold command to the rails generate script. The argument of the scaffold command is the singular version of the resource name (in this case, User), together with optional parameters for the data model’s attributes:²

2. The name of the scaffold follows the convention of models, which are singular, rather than resources and controllers, which are plural. Thus, we have User instead of Users.

Click here to view code image

$ rails generate scaffold User name:string email:string
      invoke  active_record
      create    db/migrate/20160515001017_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.coffee
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

By including name:string and email:string, we have arranged for the User model to have the form shown in Figure 2.2. (Note that there is no need to include a parameter for id; it is created automatically by Rails for use as the primary key in the database.)

To proceed with the toy application, we first need to migrate the database using rails db:migrate, as shown in Listing 2.4.

Listing 2.4: Migrating the database.

Click here to view code image

$ rails db:migrate
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0017s
==  CreateUsers: migrated (0.0018s) ===========================================

The effect of Listing 2.4 is to update the database with our new users data model. (We’ll learn more about database migrations starting in Section 6.1.1.)

By the way, it is important to note that in every version of Rails before Rails 5, the db:migrate command used rake in place of rails, so for the sake of legacy applications it’s important to know how to use Rake (Box 2.1)

Box 2.1: Rake

In the Unix tradition, the Make utility has played an important role in building executable programs from source code. Rake is Ruby make, a Make-like language written in Ruby.

Before Rails 5, Ruby on Rails used Rake extensively, so for the sake of legacy Rails applications it’s important to know how to use it. Probably the two most common Rake commands in a Rails context are rake db:migrate (to update the database with a data model) and rake test (to run the automated test suite). In these and other uses of rake, it’s important to ensure that the command uses the version of Rake corresponding to the Rails application’s Gemfile, which is accomplished using the Bundler command bundler exec. Thus, the migration command

$ rails db:migrate

would be written as

$ bundle exec rake db:migrate

Having run the migration in Listing 2.4, we can run the local webserver in a separate tab (Figure 1.7) as follows:

Click here to view code image

$ rails server -b $IP -p $PORT    # Use 'rails server' if running locally.

Now the toy application should be available on the local server as described in Section 1.3.2. (If you’re using the cloud IDE, be sure to open the resulting development server in a new browser tab, not inside the IDE itself.)

2.2.1 A User Tour

If we visit the root URL at / (read “slash”, as noted in Section 1.3.4), we get the same “hello, world!” page shown in Figure 1.11, but in generating the Users resource scaffolding we have also created a large number of pages for manipulating users. For example, the page for listing all users is at /users, and the page for making a new user is at /users/new. The rest of this section is dedicated to taking a whirlwind tour through these user pages. As we proceed, it may help to refer to Table 2.1, which shows the correspondence between pages and URLs.

We start with the page to show all the users in our application, called index and located at /users. As you might expect, initially there are no users at all (Figure 2.4).

Table 2.1: The correspondence between pages and URLs for the Users resource.

Figure 2.4: The initial index page for the Users resource (/users).

To make a new user, we visit the new page at /users/new, as shown in Figure 2.5. In Chapter 7, this will become the user signup page. We can create a user by entering name and email values in the text fields and then clicking the Create User button. The result is the user show page at /users/1, as seen in Figure 2.6. (The green welcome message is accomplished using the flash, which we’ll learn about in Section 7.4.2.) Note that the URL is /users/1; as you might suspect, the number 1 is simply the user’s id attribute from Figure 2.2. In Section 7.1, this page will become the user’s profile page.

To change a user’s information, we visit the edit page at /users/1/edit (Figure 2.7). By modifying the user information and clicking the Update User button, we arrange to change the information for the user in the toy application (Figure 2.8). (As we’ll see in detail starting in Chapter 6, this user data is stored in a database back-end.) We’ll add user edit/update functionality to the sample application in Section 10.1.

Figure 2.5: The new user page (/users/new).

Figure 2.6: The page to show a user (/users/1).

Figure 2.7: The user edit page (/users/1/edit).

Figure 2.8: A user with updated information.

Now we’ll create a second user by revisiting the new page at /users/new and submitting a second set of user information. The resulting user index is shown in Figure 2.9. Section 7.1 will develop the user index into a more polished page for showing all users.

Having shown how to create, show, and edit users, we come finally to destroying them (Figure 2.10). You should verify that clicking on the link in Figure 2.10 destroys the second user, yielding an index page with only one user. (If it doesn’t work, be sure that JavaScript is enabled in your browser; Rails uses JavaScript to issue the request needed to destroy a user.) Section 10.4 adds user deletion to the sample app, taking care to restrict its use to a special class of administrative users.

Figure 2.9: The user index page (/users) with a second user.

Figure 2.10: Destroying a user.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. (For readers who know CSS) Create a new user, then use your browser’s HTML inspector to determine the CSS id for the text “User was successfully created.” What happens when you refresh your browser?

2. What happens if you try to create a user with a name but no email address?

3. What happens if you try create a user with an invalid email address, like “@example.com”?

4. Destroy each of the users created in the previous exercises. Does Rails display a message by default when a user is destroyed?

2.2.2 MVC in Action

Now that we’ve completed a quick overview of the Users resource, let’s examine one particular part of it in the context of the model-view-controller (MVC) pattern introduced in Section 1.3.3. Our strategy will be to describe the results of a typical browser hit—a visit to the user index page at /users—in terms of MVC (Figure 2.11).

Here is a summary of the steps shown in Figure 2.11:

1. The browser issues a request for the /users URL.

2. Rails routes /users to the index action in the Users controller.

3. The index action asks the User model to retrieve all users (User.all).

4. The User model pulls all the users from the database.

5. The User model returns the list of users to the controller.

6. The controller captures the users in the @users variable, which is passed to the index view.

Figure 2.11: A detailed diagram of MVC in Rails.

7. The view uses embedded Ruby to render the page as HTML.

8. The controller passes the HTML back to the browser.³

3. Some references indicate that the view returns the HTML directly to the browser (via a webserver such as Apache or Nginx). Regardless of the implementation details, I find it helpful to think of the controller as a central hub through which all the application’s information flows.

Now let’s take a look at the above steps in more detail. We start with a request issued from the browser—i.e., the result of typing a URL in the address bar or clicking on a link (Step 1 in Figure 2.11). This request hits the Rails router (Step 2), which dispatches the request to the proper controller action based on the URL (and, as we’ll see in Box 3.2, the type of request). The code to create the mapping of user URLs to controller actions for the Users resource appears in Listing 2.5. This code effectively sets up the table of URL/action pairs seen in Table 2.1. (The strange notation :users is a symbol, which we’ll learn about in Section 4.3.3.)

Listing 2.5: The Rails routes, with a rule for the Users resource.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  resources :users
  root 'application#hello'
end

While we’re looking at the routes file, let’s take a moment to associate the root route with the users index, so that “slash” goes to /users. Recall from Listing 2.3 that we added the root route

root 'application#hello'

so that the root route went to the hello action in the Application controller. In the present case, we want to use the index action in the Users controller, which we can arrange using the code shown in Listing 2.6.

Listing 2.6: Adding a root route for users.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  resources :users
  root 'users#index'
end

A controller contains a collection of related actions, and the pages from the tour in Section 2.2.1 correspond to actions in the Users controller. The controller generated by the scaffolding is shown schematically in Listing 2.7. Note the code class UsersController < ApplicationController, which is an example of a Ruby class with inheritance. (We’ll discuss inheritance briefly in Section 2.3.4 and cover both subjects in more detail in Section 4.4.)

Listing 2.7: The Users controller in schematic form.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end

You may notice that there are more actions than there are pages; the index, show, new, and edit actions all correspond to pages from Section 2.2.1, but there are additional create, update, and destroy actions as well. These actions don’t typically render pages (although they can); instead, their main purpose is to modify information about users in the database. This full suite of controller actions, summarized in Table 2.2, represents the implementation of the REST architecture in Rails (Box 2.2), which is based on the ideas of representational state transfer identified and named by computer scientist Roy Fielding.⁴ Note from Table 2.2 that there is some overlap in the URLs; for example, both the user show action and the update action correspond to the URL /users/1. The difference between them is the HTTP request method they respond to. We’ll learn more about HTTP request methods starting in Section 3.3.

4. Fielding, Roy Thomas. Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation, University of California, Irvine, 2000.

Table 2.2: RESTful routes provided by the Users resource in Listing 2.5.

Box 2.2: REpresentational State Transfer (REST)

If you read much about Ruby on Rails web development, you’ll see a lot of references to “REST”, which is an acronym for REpresentational State Transfer. REST is an architectural style for developing distributed, networked systems and software applications such as the World Wide Web and web applications. Although REST theory is rather abstract, in the context of Rails applications REST means that most application components (such as users and microposts) are modeled as resources that can be created, read, updated, and deleted—operations that correspond both to the CRUD operations of relational databases and to the four fundamental HTTP request methods: POST, GET, PATCH, and DELETE. (We’ll learn more about HTTP requests in Section 3.3 and especially Box 3.2.)

As a Rails application developer, the RESTful style of development helps you make choices about which controllers and actions to write: you simply structure the application using resources that get created, read, updated, and deleted. In the case of users and microposts, this process is straightforward, since they are naturally resources in their own right. In Chapter 14, we’ll see an example where REST principles allow us to model a subtler problem, “following users”, in a natural and convenient way.

To examine the relationship between the Users controller and the User model, let’s focus on a simplified version of the index action, shown in Listing 2.8. (Learning how to read code even when you don’t fully understand it is an important aspect of technical sophistication (Box 1.1).)

Listing 2.8: The simplified user index action for the toy application.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

This index action has the line @users = User.all (Step 3 in Figure 2.11), which asks the User model to retrieve a list of all the users from the database (Step 4), and then places them in the variable @users (pronounced “at-users”) (Step 5). The User model itself appears in Listing 2.9; although it is rather plain, it comes equipped with a large amount of functionality because of inheritance (Section 2.3.4 and Section 4.4). In particular, by using the Rails library called Active Record, the code in Listing 2.9 arranges for User.all to return all the users in the database.

Listing 2.9: The User model for the toy application.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
end

Once the @users variable is defined, the controller calls the view (Step 6), shown in Listing 2.10. Variables that start with the @ sign, called instance variables, are automatically available in the views; in this case, the index.html.erb view in Listing 2.10 iterates through the @users list and outputs a line of HTML for each one. (Remember, you aren’t supposed to understand this code right now. It is shown only for purposes of illustration.)

Listing 2.10: The view for the user index.
app/views/users/index.html.erb

Click here to view code image

<h1>Listing users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th colspan="3"></th>
    </tr>
  </thead>

<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, method: :delete,
                                     data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

<br>

<%= link_to 'New User', new_user_path %>

The view converts its contents to HTML (Step 7), which is then returned by the controller to the browser for display (Step 8).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By referring to Figure 2.11, write out the analogous steps for visiting the URL /users/1/edit.

2. Find the line in the scaffolding code that retrieves the user from the database in the previous exercise.

3. What is the name of the view file for the user edit page?

2.2.3 Weaknesses of this Users Resource

Though good for getting a general overview of Rails, the scaffold Users resource suffers from a number of severe weaknesses.

• No data validations. Our User model accepts data such as blank names and invalid email addresses without complaint.

• No authentication. We have no notion of logging in or out, and no way to prevent any user from performing any operation.

• No tests. This isn’t technically true—the scaffolding includes rudimentary tests—but the generated tests don’t test for data validation, authentication, or any other custom requirements.

• No style or layout. There is no consistent site styling or navigation.

• No real understanding. If you understand the scaffold code, you probably shouldn’t be reading this book.

2.3 The Microposts Resource

Having generated and explored the Users resource, we turn now to the associated Microposts resource. Throughout this section, I recommend comparing the elements of the Microposts resource with the analogous user elements from Section 2.2; you should see that the two resources parallel each other in many ways. The RESTful structure of Rails applications is best absorbed by this sort of repetition of form—indeed, seeing the parallel structure of Users and Microposts even at this early stage is one of the prime motivations for this chapter.

2.3.1 A Micropost Microtour

As with the Users resource, we’ll generate scaffold code for the Microposts resource using rails generate scaffold, in this case implementing the data model from Figure 2.3:⁵

5. As with the User scaffold, the scaffold generator for microposts follows the singular convention of Rails models; thus, we have generate Micropost.

Click here to view code image

$ rails generate scaffold Micropost content:text user_id:integer
      invoke  active_record
      create    db/migrate/20160515211229_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml
      invoke  resource_route
       route    resources :microposts
      invoke  scaffold_controller
      create    app/controllers/microposts_controller.rb
      invoke    erb
      create      app/views/microposts
      create      app/views/microposts/index.html.erb
      create      app/views/microposts/edit.html.erb
      create      app/views/microposts/show.html.erb
      create      app/views/microposts/new.html.erb
      create      app/views/microposts/_form.html.erb
      invoke    test_unit
      create      test/controllers/microposts_controller_test.rb
      invoke    helper
      create      app/helpers/microposts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/microposts/index.json.jbuilder
      create      app/views/microposts/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss

(If you get an error related to Spring, just run the command again.) To update our database with the new data model, we need to run a migration as in Section 2.2:

Click here to view code image

$ bundle exec rake db:migrate
==  CreateMicroposts: migrating ===============================================
-- create_table(:microposts)
   -> 0.0023s
==  CreateMicroposts: migrated (0.0026s) ======================================

Now we are in a position to create microposts in the same way we created users in Section 2.2.1. As you might guess, the scaffold generator has updated the Rails routes file with a rule for Microposts resource, as seen in Listing 2.11.⁶ As with users, the resources :microposts routing rule maps micropost URLs to actions in the Microposts controller, as seen in Table 2.3.

6. The scaffold code may have extra blank lines compared to Listing 2.11. This is not a cause for concern, as Ruby ignores such extra space.

Table 2.3: RESTful routes provided by the Microposts resource in Listing 2.11.

Listing 2.11: The Rails routes, with a new rule for Microposts resources.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  resources :microposts
  resources :users
  root 'users#index'
end

The Microposts controller itself appears in schematic form in Listing 2.12. Note that, apart from having MicropostsController in place of UsersController, Listing 2.12 is identical to the code in Listing 2.7. This is a reflection of the REST architecture common to both resources.

Listing 2.12: The Microposts controller in schematic form.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  .
  .
  .
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end

To make some actual microposts, we enter information at the new microposts page, /microposts/new, as seen in Figure 2.12.

Figure 2.12: The new micropost page (/microposts/new).

At this point, go ahead and create a micropost or two, taking care to make sure that at least one has a user_id of 1 to match the id of the first user created in Section 2.2.1. The result should look something like Figure 2.13.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. (For readers who know CSS) Create a new micropost, then use your browser’s HTML inspector to determine the CSS id for the text “Micropost was successfully created.” What happens when you refresh your browser?

2. Try to create a micropost with empty content and no user id.

Figure 2.13: The micropost index page (/microposts).

3. Try to create a micropost with over 140 characters of content (say, the first paragraph from the Wikipedia article on Ruby).

4. Destroy the microposts from the previous exercises.

2.3.2 Putting the Micro in Microposts

Any micropost worthy of the name should have some means of enforcing the length of the post. Implementing this constraint in Rails is easy with validations; to accept microposts with at most 140 characters (à la Twitter), we use a length validation. At this point, you should open the file app/models/micropost.rb in your text editor or IDE and fill it with the contents of Listing 2.13.

Listing 2.13: Constraining microposts to be at most 140 characters.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140 }
end

The code in Listing 2.13 may look rather mysterious—we’ll cover validations more thoroughly starting in Section 6.2—but its effects are readily apparent if we go to the new micropost page and enter more than 140 characters for the content of the post. As seen in Figure 2.14, Rails renders error messages indicating that the micropost’s content is too long. (We’ll learn more about error messages in Section 7.3.3.)

Figure 2.14: Error messages for a failed micropost creation.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Try to create a micropost with the same long content used in a previous exercise (Section 2.3.1). How has the behavior changed?

2. (For readers who know CSS) User your browser’s HTML inspector to determine the CSS id of the error message produced by the previous exercise.

2.3.3 A User has_many Microposts

One of the most powerful features of Rails is the ability to form associations between different data models. In the case of our User model, each user potentially has many microposts. We can express this in code by updating the User and Micropost models as in Listing 2.14 and Listing 2.15.

Listing 2.14: A user has many microposts.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts
end

Listing 2.15: A micropost belongs to a user.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }
end

We can visualize the result of this association in Figure 2.15. Because of the user_id column in the microposts table, Rails (using Active Record) can infer the microposts associated with each user.

In Chapter 13 and Chapter 14, we will use the association of users and microposts both to display all of a user’s microposts and to construct a Twitter-like micropost feed. For now, we can examine the implications of the user-micropost association by using the console, which is a useful tool for interacting with Rails applications. We first invoke the console with rails console at the command line, and then retrieve the first user from the database using User.first (putting the results in the variable first_user):⁷

7. Your console prompt might be something like 2.1.1 :001 >, but the examples use >> since Ruby versions will vary.

Figure 2.15: The association between microposts and users.

Click here to view code image

$ rails console
>> first_user = User.first
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> first_user.microposts
=> [#<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">, #<Micropost id: 2,
content: "Second micropost", user_id: 1, created_at: "2016-05-15 02:38:54",
updated_at: "2016-05-15 02:38:54">]
>> micropost = first_user.microposts.first # Micropost.first would also work.
=> #<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">
>> micropost.user
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> exit

(I include exit in the last line just to demonstrate how to exit the console. On most systems, you can also use Ctrl-D for the same purpose.)⁸ Here we have accessed the user’s microposts using the code first_user.microposts. With this code, Active Record automatically returns all the microposts with user_id equal to the id of first_user (in this case, 1). We’ll learn much more about the association facilities in Active Record in Chapter 13 and Chapter 14.

8. As with “Ctrl-C”, the capital “D” refers to the key on the keyboard, not the capital letter, so you don’t have to hold down the Shift key along with the Ctrl key.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Edit the user show page to display the content of the user’s first micropost. (Use your technical sophistication [Box 1.1] to guess the syntax based on the other content in the file.) Confirm by visiting /users/1 that it worked.

2. The code in Listing 2.16 shows how to add a validation for the presence of micropost content in order to ensure that microposts can’t be blank. Verify that you get the behavior shown in Figure 2.16.

3. Update Listing 2.17 by replacing FILL_IN with the appropriate code to validate the presence of name and email attributes in the User model (Figure 2.17).

Figure 2.16: The effect of a micropost presence validation.

Figure 2.17: The effect of presence validations on the User model.

Listing 2.16: Code to validate the presence of micropost content.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 },
                      presence: true
end

Listing 2.17: Adding presence validations to the User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts
   validates FILL_IN, presence: true    # Replace FILL_IN with the right code.
   validates FILL_IN, presence: true    # Replace FILL_IN with the right code.
end

2.3.4 Inheritance Hierarchies

We end our discussion of the toy application with a brief description of the controller and model class hierarchies in Rails. This discussion will only make much sense if you have some experience with object-oriented programming (OOP), particularly classes. Don’t worry if it’s confusing for now; we’ll discuss these ideas more thoroughly in Section 4.4.

We start with the inheritance structure for models. Comparing Listing 2.18 and Listing 2.19, we see that both the User model and the Micropost model inherit (via the left angle bracket <) from ApplicationRecord, which in turn inherits from ActiveRecord::Base, which is the base class for models provided by Active Record; a diagram summarizing this relationship appears in Figure 2.18. It is by inheriting from ActiveRecord::Base that our model objects gain the ability to communicate with the database, treat the database columns as Ruby attributes, and so on.

Listing 2.18: The User class, highlighting inheritance.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
end

Listing 2.19: The Micropost class, highlighting inheritance.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  .
  .
  .
end

The inheritance structure for controllers is essentially the same as that for models. Comparing Listing 2.20 and Listing 2.21, we see that both the Users controller and the Microposts controller inherit from the Application controller. Examining Listing 2.22, we see that ApplicationController itself inherits from ActionController::Base, which is the base class for controllers provided by the Rails library Action Pack. The relationships between these classes is illustrated in Figure 2.19.

Figure 2.18: The inheritance hierarchy for the User and Micropost models.

Listing 2.20: The UsersController class, highlighting inheritance.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
end

Listing 2.21: The MicropostsController class, highlighting inheritance.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  .
  .
  .
end

Listing 2.22: The ApplicationController class, highlighting inheritance.
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  .
  .
  .
end

Figure 2.19: The inheritance hierarchy for the Users and Microposts controllers.

As with model inheritance, both the Users and Microposts controllers gain a large amount of functionality by inheriting from a base class (in this case, ActionController::Base), including the ability to manipulate model objects, filter inbound HTTP requests, and render views as HTML. Since all Rails controllers inherit from ApplicationController, rules defined in the Application controller automatically apply to every action in the application. For example, in Section 9.1 we’ll see how to include helpers for logging in and logging out of all of the sample application’s controllers.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By examining the contents of the Application controller file, find the line that causes ApplicationController to inherit from ActionController::Base.

2. Is there an analogous file containing a line where ApplicationRecord inherits from ActiveRecord::Base? Hint: It would probably be a file called something like application_record.rb in the app/models directory.

2.3.5 Deploying the Toy App

With the completion of the Microposts resource, now is a good time to push the repository up to Bitbucket:

Click here to view code image

$ git status
$ git add -A
$ git commit -m "Finish toy app"
$ git push

Ordinarily, you should make smaller, more frequent commits, but for the purposes of this chapter a single big commit at the end is fine.

At this point, you can also deploy the toy app to Heroku as in Section 1.5:

$ git push heroku

(This assumes you created the Heroku app in Section 2.1. Otherwise, you should run heroku create and then git push heroku master.)

To get the application’s database to work, you’ll also have to migrate the production database, which involves running the migration command from Listing 2.4 prefixed with heroku run:

$ heroku run rails db:migrate

This updates the database at Heroku with the necessary user and micropost data models. After running the migration, you should be able to use the toy app in production, with a real PostgreSQL database back-end (Figure 2.20).

Finally, if you completed the exercises in Section 2.3.3, you will have to remove the code to display the first user’s micropost in order to get the app to load properly. In this case, simply delete the offending code, make another commit, and push again to Heroku.

Figure 2.20: Running the toy app in production.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Create a few users on the production app.

2. Create a few production microposts for the first user.

3. By trying to create a micropost with content over 140 characters, confirm that the validation from Listing 2.13 works on the production app.

2.4 Conclusion

We’ve come now to the end of the high-level overview of a Rails application. The toy app developed in this chapter has several strengths and a host of weaknesses.

Strengths

• High-level overview of Rails

• Introduction to MVC

• First taste of the REST architecture

• Beginning data modeling

• A live, database-backed web application in production

Weaknesses

• No custom layout or styling

• No static pages (such as “Home” or “About”)

• No user passwords

• No user images

• No logging in

• No security

• No automatic user/micropost association

• No notion of “following” or “followed”

• No micropost feed

• No meaningful tests

• No real understanding

The rest of this tutorial is dedicated to building on the strengths and eliminating the weaknesses.

2.4.1 What We Learned in This Chapter

• Scaffolding automatically creates code to model data and interact with it through the web.

• Scaffolding is good for getting started quickly but is bad for understanding.

• Rails uses the model-view-controller (MVC) pattern for structuring web applications.

• As interpreted by Rails, the REST architecture includes a standard set of URLs and controller actions for interacting with data models.

• Rails supports data validations to place constraints on the values of data model attributes.

• Rails comes with built-in functions for defining associations between different data models.

• We can interact with Rails applications at the command line using the Rails console.

Chapter 3. Mostly Static Pages

In this chapter, we will begin developing the professional-grade sample application that will serve as our example throughout the rest of this tutorial. Although the sample app will eventually have users, microposts, and a full login and authentication framework, we will begin with a seemingly limited topic: the creation of static pages. Despite its apparent simplicity, making static pages is a highly instructive exercise, rich in implications—a perfect start for our nascent application.

Although Rails is designed for making database-backed dynamic websites, it also excels at making the kind of static pages we might create using raw HTML files. In fact, using Rails even for static pages yields a distinct advantage: we can easily add just a small amount of dynamic content. In this chapter we’ll learn how. Along the way, we’ll get our first taste of automated testing, which will help us be more confident that our code is correct. Moreover, having a good test suite will allow us to refactor our code with confidence, changing its form without changing its function.

3.1 Sample App Setup

As in Chapter 2, before getting started we need to create a new Rails project, this time called sample_app, as shown in Listing 3.1.¹

1. If you’re using the cloud IDE, it’s often useful to use the “Goto Anything” command, which makes it easy to navigate the filesystem by typing in partial filenames. In this context, having the hello, toy, and sample apps present in the same project can be inconvenient due to the many common filenames. For example, when searching for a file called “Gemfile”, six possibilities will show up, because each project has matching files called Gemfile and Gemfile.lock. Thus, you may want to consider removing the first two apps before proceeding, which you can do by navigating to the workspace directory and running rm -rf hello_app/ toy_app/ (Table 1.1). (As long as you pushed the corresponding repositories up to Bitbucket, you can always recover them later.)

Listing 3.1: Generating a new sample app.

Click here to view code image

$ cd ˜/workspace
$ rails _5.0.0_ new sample_app
$ cd sample_app/

(As in Section 2.1, note that users of the cloud IDE can create this project in the same workspace as the applications from the previous two chapters. It is not necessary to create a new workspace.)

Note: For convenience, a reference implementation of the completed Rails Tutorial sample app is available at Bitbucket.²

2. bitbucket.org/railstutorial/sample_app_4th_ed

As in Section 2.1, our next step is to use a text editor to update the Gemfile with the gems needed by our application. Listing 3.2 is identical to Listing 1.5 and Listing 2.1 apart from the gems in the test group, which are needed for the optional advanced testing setup (Section 3.6) and integration testing starting in Section 5.3.4. Note: If you would like to install all the gems needed for the sample application, you should use the code in Listing 13.72 at this time. Important note: For all the Gemfiles in this book, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed below (although they should be identical if you are reading this online).

Listing 3.2: A Gemfile for the sample app.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',        '5.0.0'
gem 'puma',         '3.4.0'
gem 'sass-rails',   '5.0.5'
gem 'uglifier',     '3.0.0'
gem 'coffee-rails', '4.2.1'
gem 'jquery-rails', '4.1.1'
gem 'turbolinks',   '5.0.0'
gem 'jbuilder',     '2.4.1'

group :development, :test do
  gem 'sqlite3', '1.3.11'
  gem 'byebug',  '9.0.0', platform: :mri
end

group :development do
  gem 'web-console',           '3.1.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '1.7.2'
  gem 'spring-watcher-listen', '2.0.0'
end

group :test do
  gem 'rails-controller-testing', '0.1.1'
  gem 'minitest-reporters',       '1.1.9'
  gem 'guard',                    '2.13.0'
  gem 'guard-minitest',           '2.4.4'
end

group :production do
  gem 'pg', '0.18.4'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

As in the previous two chapters, we run bundle install to install and include the gems specified in the Gemfile, while skipping the installation of production gems using the option --without production:³

3. It’s worth noting that --without production is a “remembered option”, which means it will be included automatically the next time we run bundle install.

Click here to view code image

$ bundle install --without production

This arranges to skip the pg gem for PostgreSQL in development and use SQLite for development and testing. Heroku recommends against using different databases in development and production, but for the sample application it won’t make any difference, and SQLite is much easier than PostgreSQL to install and configure locally.⁴ In case you’ve previously installed a version of a gem (such as Rails itself) other than the one specified by the Gemfile, it’s a good idea to update the gems with bundle update to make sure the versions match:

4. I recommend eventually learning how to install and configure PostgreSQL in development, but the chances are now is not that time. When the time comes, Google “install configure postgresql <your system>” and “rails postgresql setup”, and prepare for a challenge. (On the cloud IDE, <your system> is Ubuntu.)

$ bundle update

With that, all we have left is to initialize the Git repository:

Click here to view code image

$ git init
$ git add -A
$ git commit -m "Initialize repository"

As with the first application, I suggest updating the README file to be more helpful and descriptive by replacing the default contents of README.md with the Markdown shown in Listing 3.3. Note that the README includes instructions for getting started with the application. (We won’t actually need to run rails db:migrate until Chapter 6, but it does no harm to include it now.)

Listing 3.3: An improved README file for the sample app.
README.md

Click here to view code image

# Ruby on Rails Tutorial sample application

This is the sample application for
[*Ruby on Rails Tutorial:
Learn Web Development with Rails*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/).

## License

All source code in the [Ruby on Rails Tutorial](http://railstutorial.org/)
is available jointly under the MIT License and the Beerware License. See
[LICENSE.md](LICENSE.md) for details.

## Getting started

To get started with the app, clone the repo and then install the needed gems:

```
$ bundle install --without production
```

Next, migrate the database:

```
$ rails db:migrate
```

Finally, run the test suite to verify that everything is working correctly:

```
$ rails test
```

If the test suite passes, you'll be ready to run the app in a local server:

```
$ rails server
```

For more information, see the
[*Ruby on Rails Tutorial* book](http://www.railstutorial.org/book).

Then commit the changes as follows:

Click here to view code image

$ git commit -am "Improve the README"

You may recall from Section 1.4.4 that we used the Git command git commit -a -m "Message", with flags for “all changes” (-a) and a message (-m). As shown in the second command above, Git also lets us roll the two flags into one using git commit -am "Message".

Since we’ll be using this sample app throughout the rest of the book, it’s a good idea to create a new repository at Bitbucket and push it up:

Click here to view code image

$ git remote add origin git@bitbucket.org:<username>/sample_app.git
$ git push -u origin --all # pushes up the repo and its refs for the first time

To avoid integration headaches later on, it’s also a good idea to deploy the app to Heroku even at this early stage. As in Chapter 1 and Chapter 2, I suggest following the “hello, world!” steps in Listing 3.4 and Listing 3.5. (The main reason for this is that the default Rails page typically breaks at Heroku, which makes it hard to tell if the deployment was successful or not.)

Listing 3.4: Adding a hello action to the Application controller.
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
    render text: "hello, world!"
  end
end

Listing 3.5: Setting the root route.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'application#hello'
end

Then commit the changes and push up to Bitbucket and Heroku:

Click here to view code image

$ git commit -am "Add hello"
$ git push
$ heroku create
$ git push heroku master

As in Section 1.5, you may see some warning messages, which you should ignore for now. We’ll deal with them in Section 7.5. Apart from the address of the Heroku app, the result should be the same as in Figure 1.19.

As you proceed through the rest of the book, I recommend pushing and deploying the application regularly, which automatically makes remote backups and lets you catch any production errors as soon as possible. If you run into problems at Heroku, make sure to take a look at the production logs to try to diagnose the problem:

$ heroku logs

Note: If you do end up using Heroku for a real-life application, be sure to follow the production webserver configuration in Section 7.5.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm that Bitbucket renders the Markdown for the README in Listing 3.3 as HTML.

2. By visiting the root route on the production server, verify that the deployment to Heroku succeeded.

3.2 Static Pages

With all the preparation from Section 3.1 finished, we’re ready to get started developing the sample application. In this section, we’ll take a first step toward making dynamic pages by creating a set of Rails actions and views containing only static HTML.⁵ Rails actions come bundled together inside controllers (the C in MVC from Section 1.3.3), which contain sets of actions related by a common purpose. We got a glimpse of controllers in Chapter 2, and will come to a deeper understanding once we explore the REST architecture more fully (starting in Chapter 6). In order to get our bearings, it’s helpful to recall the Rails directory structure from Section 1.3 (Figure 1.4). In this section, we’ll be working mainly in the app/controllers and app/views directories.

5. Our method for making static pages is probably the simplest, but it’s not the only way. The optimal method really depends on your needs; if you expect a large number of static pages, using a Static Pages controller can get quite cumbersome, but in our sample app we’ll only need a few. If you do need a lot of static pages, take a look at the high_voltage gem.

Recall from Section 1.4.4 that, when using Git, it’s a good practice to do our work on a separate topic branch rather than the master branch. If you’re using Git for version control, you should run the following command to checkout a topic branch for static pages:

$ git checkout -b static-pages

3.2.1 Generated Static Pages

To get started with static pages, we’ll first generate a controller using the same Rails generate script we used in Chapter 2 to generate scaffolding. Since we’ll be making a controller to handle static pages, we’ll call it the Static Pages controller, designated by the CamelCase name StaticPages. We’ll also plan to make actions for a Home page, a Help page, and an About page, designated by the lowercase action names home, help, and about. The generate script takes an optional list of actions, so we’ll include actions for the Home and Help pages directly on the command line, while intentionally leaving off the action for the About page so that we can see how to add it (Section 3.3). The resulting command to generate the Static Pages controller appears in Listing 3.6.

Listing 3.6: Generating a Static Pages controller.

Click here to view code image

$ rails generate controller StaticPages home help
      create  app/controllers/static_pages_controller.rb
       route  get 'static_pages/help'
       route  get 'static_pages/home'
      invoke  erb
      create    app/views/static_pages
      create    app/views/static_pages/home.html.erb
      create    app/views/static_pages/help.html.erb
      invoke  test_unit
      create    test/controllers/static_pages_controller_test.rb
      invoke  helper
      create    app/helpers/static_pages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/static_pages.coffee
      invoke    scss
      create      app/assets/stylesheets/static_pages.scss

Incidentally, it’s worth noting that rails g is a shortcut for rails generate, which is only one of several shortcuts supported by Rails (Table 3.1). For clarity, this tutorial always uses the full command, but in real life most Rails developers use one or more of the shortcuts shown in Table 3.1.⁶

6. In fact, many Rails developers also add an alias (as described in Learn Enough Text Editor to Be Dangerous) for the rails command, typically shortening it to just r. This allows us to run, e.g., a Rails server using the compact command r s.

Table 3.1: Some Rails shortcuts.

Before moving on, if you’re using Git it’s a good idea to add the files for the Static Pages controller to the remote repository:

Click here to view code image

$ git add -A
$ git commit -m "Add a Static Pages controller"
$ git push -u origin static-pages

The final command here arranges to push the static-pages topic branch up to Bitbucket. Subsequent pushes can omit the other arguments and write simply

$ git push

The commit and push sequence above represents the kind of pattern I would ordinarily follow in real-life development, but for simplicity I’ll typically omit such intermediate commits from now on.

In Listing 3.6, note that we have passed the controller name as CamelCase (so called because it resembles the humps of a Bactrian camel), which leads to the creation of a controller file written in snake case, so that a controller called StaticPages yields a file called static_pages_controller.rb. This is merely a convention, and in fact using snake case at the command line also works: the command

Click here to view code image

$ rails generate controller static_pages ...

also generates a controller called static_pages_controller.rb. Because Ruby uses CamelCase for class names (Section 4.4), my preference is to refer to controllers using their CamelCase names, but this is a matter of taste. (Since Ruby filenames typically use snake case, the Rails generator converts CamelCase to snake case using the underscore method.)

By the way, if you ever make a mistake when generating code, it’s useful to know how to reverse the process. See Box 3.1 for some techniques on how to undo things in Rails.

Box 3.1: Undoing Things

Even when you’re very careful, things can sometimes go wrong when developing Rails applications. Happily, Rails has some facilities to help you recover.

One common scenario is wanting to undo code generation—for example, when you change your mind on the name of a controller and want to eliminate the generated files. Because Rails creates a substantial number of auxiliary files along with the controller (as seen in Listing 3.6), this isn’t as easy as removing the controller file itself; undoing the generation means removing not only the principal generated file, but all the ancillary files as well. (In fact, as we saw in Section 2.2 and Section 2.3, rails generate can make automatic edits to the routes.rb file, which we also want to undo automatically.) In Rails, this can be accomplished with rails destroy followed by the name of the generated element. In particular, these two commands cancel each other out:

$ rails generate controller StaticPages home help
$ rails destroy controller StaticPages home help

Similarly, in Chapter 6 we’ll generate a model as follows:

$ rails generate model User name:string email:string

This can be undone using

$ rails destroy model User

(In this case, it turns out we can omit the other command-line arguments. When you get to Chapter 6, see if you can figure out why.)

Another technique related to models involves undoing migrations, which we saw briefly in Chapter 2 and will see much more of starting in Chapter 6. Migrations change the state of the database using the command

$ rails db:migrate

We can undo a single migration step using

$ rails db:rollback

To go all the way back to the beginning, we can use

$ rails db:migrate VERSION=0

As you might guess, substituting any other number for 0 migrates to that version number, where the version numbers come from listing the migrations sequentially.

With these techniques in hand, we are well-equipped to recover from the inevitable development snafus.

The Static Pages controller generation in Listing 3.6 automatically updates the routes file (config/routes.rb), which we saw briefly in Section 1.3.4. The routes file is responsible for implementing the router (seen in Figure 2.11) that defines the correspondence between URLs and web pages. The routes file is located in the config directory, where Rails collects files needed for the application configuration (Figure 3.1).

Figure 3.1: Contents of the sample app’s config directory.

Since we included the home and help actions in Listing 3.6, the routes file already has a rule for each one, as seen in Listing 3.7.

Listing 3.7: The routes for the home and help actions in the Static Pages controller.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  get 'static_pages/home'
  get 'static_pages/help'
  root 'application#hello'
end

Here the rule

get 'static_pages/home'

maps requests for the URL /static_pages/home to the home action in the Static Pages controller. Moreover, by using get we arrange for the route to respond to a GET request, which is one of the fundamental HTTP verbs supported by the hypertext transfer protocol (Box 3.2). In our case, this means that when we generate a home action inside the Static Pages controller we automatically get a page at the address /static_pages/home. To see the result, start a Rails development server as described in Section 1.3.2:

Click here to view code image

$ rails server -b $IP -p $PORT    # Use `rails server` if running locally.

Then navigate to /static_pages/home (Figure 3.2).

Box 3.2: GET, et cet.

The hypertext transfer protocol (HTTP) defines the basic operations GET, POST, PATCH, and DELETE. These refer to operations between a client computer (typically running a web browser such as Chrome, Firefox, or Safari) and a server (typically running a webserver such as Apache or Nginx). (It’s important to understand that, when developing Rails applications on a local computer, the client and server are the same physical machine, but in general they are different.) An emphasis on HTTP verbs is typical of web frameworks (including Rails) influenced by the REST architecture, which we saw briefly in Chapter 2 and will start learning more about in Chapter 7. GET is the most common HTTP operation, used for reading data on the web; it just means “get a page”, and every time you visit a site like http://www.google.com/ or http://www.wikipedia.org/ your browser is submitting a GET request. POST is the next most common operation; it is the request sent by your browser when you submit a form. In Rails applications, POST requests are typically used for creating things (although HTTP also allows POST to perform updates). For example, the POST request sent when you submit a registration form creates a new user on the remote site. The other two verbs, PATCH and DELETE, are designed for updating and destroying things on the remote server. These requests are less common than GET and POST since browsers are incapable of sending them natively, but some web frameworks (including Ruby on Rails) have clever ways of making it seem like browsers are issuing such requests. As a result, Rails supports all four of the request types: GET, POST, PATCH, and DELETE.

Figure 3.2: The raw home view (/static_pages/home).

To understand where this page comes from, let’s start by taking a look at the Static Pages controller in a text editor, which should look something like Listing 3.8. You may note that, unlike the demo Users and Microposts controllers from Chapter 2, the Static Pages controller does not use the standard REST actions. This is normal for a collection of static pages: the REST architecture isn’t the best solution to every problem.

Listing 3.8: The Static Pages controller made by Listing 3.6.
app/controllers/static_pages_controller.rb

Click here to view code image

class StaticPagesController < ApplicationController
  def home
  end

  def help
  end
end

We see from the class keyword in Listing 3.8 that static_pages_controller.rb defines a class, in this case called StaticPagesController. Classes are simply a convenient way to organize functions (also called methods) like the home and help actions, which are defined using the def keyword. As discussed in Section 2.3.4, the angle bracket < indicates that StaticPagesController inherits from the Rails class ApplicationController; as we’ll see in a moment, this means that our pages come equipped with a large amount of Rails-specific functionality. (We’ll learn more about both classes and inheritance in Section 4.4.)

In the case of the Static Pages controller, both of its methods are initially empty:

Click here to view code image

def home
end

def help
end

In plain Ruby, these methods would simply do nothing. In Rails, the situation is different—StaticPagesController is a Ruby class, but because it inherits from ApplicationController the behavior of its methods is specific to Rails. Thus when visiting the URL /static_pages/home, Rails looks in the Static Pages controller and executes the code in the home action, and then renders the view (the V in MVC from Section 1.3.3) corresponding to the action. In the present case, the home action is empty, so all visiting /static_pages/home does is render the view. So, what does a view look like, and how do we find it?

If you take another look at the output in Listing 3.6, you might be able to guess the correspondence between actions and views: an action like home has a corresponding view called home.html.erb. We’ll learn in Section 3.4 what the .erb part means; from the .html part you probably won’t be surprised that it basically looks like HTML (Listing 3.9).

Listing 3.9: The generated view for the Home page.
app/views/static_pages/home.html.erb

Click here to view code image

<h1>StaticPages#home</h1>
<p>Find me in app/views/static_pages/home.html.erb</p>

The view for the help action is analogous (Listing 3.10).

Listing 3.10: The generated view for the Help page.
app/views/static_pages/help.html.erb

Click here to view code image

<h1>StaticPages#help</h1>
<p>Find me in app/views/static_pages/help.html.erb</p>

Both of these views are just placeholders: they have a top-level heading (inside the h1 tag) and a paragraph (p tag) with the full path to the corresponding file.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Generate a controller called Foo with actions bar and baz.

2. By applying the techniques described in Box 3.1, destroy the Foo controller and its associated actions.

3.2.2 Custom Static Pages

We’ll add some (very slightly) dynamic content starting in Section 3.4, but as they stand, the files shown in Listing 3.9 and Listing 3.10 underscore an important point: Rails views can simply contain static HTML. This means we can begin customizing the Home and Help pages even with no knowledge of Rails, as shown in Listing 3.11 and Listing 3.12.

Listing 3.11: Custom HTML for the Home page.
app/views/static_pages/home.html.erb

Click here to view code image

<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

Listing 3.12: Custom HTML for the Help page.
app/views/static_pages/help.html.erb

Click here to view code image

<h1>Help</h1>
<p>
  Get help on the Ruby on Rails Tutorial at the
  <a href="http://www.railstutorial.org/help">Rails Tutorial help page</a>.
  To get help on this sample app, see the
  <a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
  book</a>.
</p>

The results of Listing 3.11 and Listing 3.12 are shown in Figure 3.3 and Figure 3.4.

Figure 3.3: A custom Home page.

Figure 3.4: A custom Help page.

3.3 Getting Started with Testing

Having created and filled in the Home and Help pages for our sample app (Section 3.2.2), now we’re going to add an About page as well. When making a change of this nature, it’s a good practice to write an automated test to verify that the feature is implemented correctly. Developed over the course of building an application, the resulting test suite serves as a safety net and as executable documentation of the application source code. When done right, writing tests also allows us to develop faster despite requiring extra code, because we’ll end up wasting less time trying to track down bugs. This is true only once we get good at writing tests, though, which is one reason it’s important to start practicing as early as possible.

Although virtually all Rails developers agree that testing is a good idea, there is a diversity of opinion on the details. There is an especially lively debate over the use of test-driven development (TDD),⁷ a testing technique in which the programmer writes failing tests first, and then writes the application code to get the tests to pass. The Ruby on Rails™ Tutorial takes a lightweight, intuitive approach to testing, employing TDD when convenient without being dogmatic about it (Box 3.3).

7. See, e.g., “TDD is dead. Long live testing.” by Rails creator David Heinemeier Hansson.

Box 3.3: When to Test

When deciding when and how to test, it’s helpful to understand why to test. In my view, writing automated tests has three main benefits:

1. Tests protect against regressions, where a functioning feature stops working for some reason.

2. Tests allow code to be refactored (i.e., changing its form without changing its function) with greater confidence.

3. Tests act as a client for the application code, thereby helping determine its design and its interface with other parts of the system.

Although none of the above benefits require that tests be written first, there are many circumstances where test-driven development (TDD) is a valuable tool to have in your kit. Deciding when and how to test depends in part on how comfortable you are writing tests; many developers find that as they get better at writing tests, they are more inclined to write them first. It also depends on how difficult the test is relative to the application code, how precisely the desired features are known, and how likely the feature is to break in the future.

In this context, it’s helpful to have a set of guidelines on when we should test first (or test at all). Here are some suggestions based on my own experience:

• When a test is especially short or simple compared to the application code it tests, lean toward writing the test first.

• When the desired behavior isn’t yet crystal clear, lean toward writing the application code first; then write a test to codify the result.

• Because security is a top priority, err on the side of writing tests of the security model first.

• Whenever a bug is found, write a test to reproduce it and protect against regressions, then write the application code to fix it.

• Lean against writing tests for code (such as detailed HTML structure) likely to change in the future.

• Write tests before refactoring code, focusing on testing error-prone code that’s especially likely to break.

In practice, the guidelines above mean that we’ll usually write controller and model tests first and integration tests (which test functionality across models, views, and controllers) second. And when we’re writing application code that isn’t particularly brittle or error-prone, or is likely to change (as is often the case with views), we’ll often skip testing altogether.

Our main testing tools will be controller tests (starting in this section), model tests (starting in Chapter 6), and integration tests (starting in Chapter 7). Integration tests are especially powerful, as they allow us to simulate the actions of a user interacting with our application using a web browser. Integration tests will eventually be our primary testing technique, but controller tests give us an easier place to start.

3.3.1 Our First Test

Now it’s time to add an About page to our application. As we’ll see, the test is short and simple, so we’ll follow the guidelines from Box 3.3 and write the test first. We’ll then use the failing test to drive the writing of the application code.

Getting started with testing can be challenging, requiring extensive knowledge of both Rails and Ruby. At this early stage, writing tests might thus seem hopelessly intimidating. Luckily, Rails has already done the hardest part for us, because rails generate controller (Listing 3.6) automatically generated a test file to get us started:

Click here to view code image

$ ls test/controllers/
static_pages_controller_test.rb

Let’s take a look at it (Listing 3.13).

Listing 3.13: The default tests for the StaticPages controller. GREEN
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end
end

It’s not important at this point to understand the syntax in Listing 3.13 in detail, but we can see that there are two tests, one for each controller action we included on the command line in Listing 3.6. Each test simply gets a URL and verifies (via an assertion) that the result is a success. Here the use of get indicates that our tests expect the Home and Help pages to be ordinary web pages, accessed using a GET request (Box 3.2). The response :success is an abstract representation of the underlying HTTP status code (in this case, 200 OK). In other words, a test like

Click here to view code image

test "should get home" do
  get static_pages_home_url
  assert_response :success
end

says “Let’s test the Home page by issuing a GET request to the Static Pages home URL and then making sure we receive a ‘success’ status code in response.”

To begin our testing cycle, we need to run our test suite to verify that the tests currently pass. We can do this with the rails command as follows:

Listing 3.14: GREEN

Click here to view code image

$ rails test
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

As required, initially our test suite is passing (GREEN). (You won’t actually see the color green unless you add the minitest reporters from the optional Section 3.6.1, but the terminology is common even when literal colors aren’t involved.) On some systems, the tests can take a relatively long time to start, which is due to two factors: (1) starting the Spring server to preload parts of the Rails environment, which only happens the first time; and (2) overhead associated with Ruby startup time. (The second factor is ameliorated when using Guard as suggested in Section 3.6.2.)

3.3.2 Red

As noted in Box 3.3, test-driven development involves writing a failing test first, writing the application code needed to get it to pass, and then refactoring the code if necessary. Because many testing tools represent failing tests with the color red and passing tests with the color green, this sequence is sometimes known as the “Red, Green, Refactor” cycle. In this section, we’ll complete the first step in this cycle, getting to RED by writing a failing test. Then we’ll get to GREEN in Section 3.3.3, and refactor in Section 3.4.3.⁸

8. By default, rails test shows red when the tests fail, but doesn’t show green when the tests pass. To arrange for a true Red–Green cycle, see Section 3.6.1.

Our first step is to write a failing test for the About page. Following the models from Listing 3.13, you can probably guess the right test, which is shown in Listing 3.15.

Listing 3.15: A test for the About page. RED
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
  end
end

We see from the highlighted lines in Listing 3.15 that the test for the About page is the same as the Home and Help tests with the word “about” in place of “home” or “help”.

As required, the test initially fails:

Listing 3.16: RED

Click here to view code image

$ rails test
3 tests, 2 assertions, 0 failures, 1 errors, 0 skips

3.3.3 Green

Now that we have a failing test (RED), we’ll use the failing test’s error messages to guide us to a passing test (GREEN), thereby implementing a working About page.

We can get started by examining the error message output by the failing test:

Listing 3.17: RED

Click here to view code image

$ rails test
NameError: undefined local variable or method `static_pages_about_url'

The error message here says that the Rails code for the About page URL is undefined, which is a hint that we need to add a line to the routes file. We can accomplish this by following the pattern in Listing 3.7, as shown in Listing 3.18.

Listing 3.18: Adding the about route. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  get  'static_pages/home'
  get  'static_pages/help'
  get 'static_pages/about'
  root 'application#hello'
end

The highlighted line in Listing 3.18 tells Rails to route a GET request for the URL /static_pages/about to the about action in the Static Pages controller. This automatically creates a helper called

static_pages_about_url

Running our test suite again, we see that it is still RED, but now the error message has changed:

Listing 3.19: RED

Click here to view code image

$ rails test
AbstractController::ActionNotFound:
The action 'about' could not be found for StaticPagesController

The error message now indicates a missing about action in the Static Pages controller, which we can add by following the model provided by home and help in Listing 3.8, as shown in Listing 3.20.

Listing 3.20: The Static Pages controller with added about action. RED
app/controllers/static_pages_controller.rb

Click here to view code image

class StaticPagesController < ApplicationController

  def home
  end

  def help
  end

  def about
  end
end

As before, our test suite is still RED, but the error message has changed again:

Click here to view code image

$ rails test
ActionController::UnknownFormat: StaticPagesController#about is missing
a template for this request format and variant.

This indicates a missing template, which in the context of Rails is essentially the same thing as a view. As described in Section 3.2.1, an action called home is associated with a view called home.html.erb located in the app/views/static_pages directory, which means that we need to create a new file called about.html.erb in the same directory.

The way to create a file varies by system setup, but most text editors will let you control-click inside the directory where you want to create the file to bring up a menu with a “New File” menu item. Alternately, you can use the File menu to create a new file and then pick the proper directory when saving it. Finally, you can use my favorite trick by applying the Unix touch command as follows:

Click here to view code image

$ touch app/views/static_pages/about.html.erb

As mentioned in Learn Enough Command Line to Be Dangerous, touch is designed to update the modification timestamp of a file or directory without otherwise affecting it, but as a side-effect it creates a new (blank) file if one doesn’t already exist. (If using the cloud IDE, you may have to refresh the file tree as described in Section 1.3.1. This is a good example of technical sophistication [Box 1.1].)⁹

9. You can also use the c9 command via c9 open <filename> to create and open a file at the same time.

Once you’ve created the about.html.erb file in the right directory, you should fill it with the contents shown in Listing 3.21.

Listing 3.21: Code for the About page. GREEN
app/views/static_pages/about.html.erb

Click here to view code image

<h1>About</h1>
<p>
  The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
  Tutorial</em></a> is a
  <a href="http://www.railstutorial.org/book">book</a> and
  <a href="http://screencasts.railstutorial.org/">screencast series</a>
  to teach web development with
  <a href="http://rubyonrails.org/">Ruby on Rails</a>.
  This is the sample application for the tutorial.
</p>

At this point, running rails test should get us back to GREEN:

Listing 3.22: GREEN

Click here to view code image

$ rails test
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

Of course, it’s never a bad idea to take a look at the page in a browser to make sure our tests aren’t completely crazy (Figure 3.5).

3.3.4 Refactor

Now that we’ve gotten to GREEN, we are free to refactor our code with confidence. When developing an application, often code will start to “smell”, meaning that it gets ugly, bloated, or filled with repetition. The computer doesn’t care what the code looks like, of course, but humans do, so it is important to keep the code base clean by refactoring frequently. Although our sample app is a little too small to refactor right now, code smell seeps in at every crack, and we’ll get started refactoring in Section 3.4.3.

Figure 3.5: The new About page (/static_pages/about).

3.4 Slightly Dynamic Pages

Now that we’ve created the actions and views for some static pages, we’ll make them slightly dynamic by adding some content that changes on a per-page basis: we’ll have the title of each page change to reflect its content. Whether a changing title represents truly dynamic content is debatable, but in any case it lays the necessary foundation for unambiguously dynamic content in Chapter 7.

Our plan is to edit the Home, Help, and About pages to make page titles that change on each page. This will involve using the <title> tag in our page views. Most browsers display the contents of the title tag at the top of the browser window, and it is also important for search-engine optimization. We’ll be using the full “Red, Green, Refactor” cycle: first by adding simple tests for our page titles (RED), then by adding titles to each of our three pages (GREEN), and finally using a layout file to eliminate duplication

Table 3.2: The (mostly) static pages for the sample app.

(Refactor). By the end of this section, all three of our static pages will have titles of the form “<page name> | Ruby on Rails Tutorial Sample App”, where the first part of the title will vary depending on the page (Table 3.2).

The rails new command (Listing 3.1) creates a layout file by default, but it’s instructive to ignore it initially, which we can do by changing its name:

Click here to view code image

$ mv app/views/layouts/application.html.erb layout_file    # temporary change

You wouldn’t normally do this in a real application, but it’s easier to understand the purpose of the layout file if we start by disabling it.

3.4.1 Testing Titles (Red)

To add page titles, we need to learn (or review) the structure of a typical web page, which takes the form shown in Listing 3.23. (This is covered in much more depth in Learn Enough HTML to Be Dangerous.)

Listing 3.23: The HTML structure of a typical web page.

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title>Greeting</title>
  </head>
  <body>
    <p>Hello, world!</p>
  </body>
</html>

The structure in Listing 3.23 includes a document type, or doctype, declaration at the top to tell browsers which version of HTML we’re using (in this case, HTML5);¹⁰ a head section, in this case with “Greeting” inside a title tag; and a body section, in this case with “Hello, world!” inside a p (paragraph) tag. (The indentation is optional— HTML is not sensitive to whitespace, and ignores both tabs and spaces—but it makes the document’s structure easier to see.)

10. HTML changes with time; by explicitly making a doctype declaration we make it likelier that browsers will render our pages properly in the future. The simple doctype <!DOCTYPE html> is characteristic of the latest HTML standard, HTML5.

We’ll write simple tests for each of the titles in Table 3.2 by combining the tests in Listing 3.15 with the assert_select method, which lets us test for the presence of a particular HTML tag (sometimes called a “selector”, hence the name):¹¹

11. For a list of common minitest assertions, see the table of available assertions in the Rails Guides testing article.

Click here to view code image

assert_select "title", "Home | Ruby on Rails Tutorial Sample App"

In particular, the code above checks for the presence of a <title> tag containing the string “Home | Ruby on Rails Tutorial Sample App”. Applying this idea to all three static pages gives the tests shown in Listing 3.24.

Listing 3.24: The Static Pages controller test with title tests. RED
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

With the tests from Listing 3.24 in place, you should verify that the test suite is currently RED:

Listing 3.25: RED

Click here to view code image

$ rails test
3 tests, 6 assertions, 3 failures, 0 errors, 0 skips

3.4.2 Adding Page Titles (Green)

Now we’ll add a title to each page, getting the tests from Section 3.4.1 to pass in the process. Applying the basic HTML structure from Listing 3.23 to the custom Home page from Listing 3.11 yields Listing 3.26.

Listing 3.26: The view for the Home page with full HTML structure. RED
app/views/static_pages/home.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title>Home | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

Figure 3.6: The Home page with a title.

The corresponding web page appears in Figure 3.6.¹²

12. Most of the screenshots in this book use Google Chrome, but Figure 3.6 uses Safari because Chrome doesn’t display the full page title. (As of this writing, Safari displays the page title only if you include an additional tab, which explains the second tab shown in Figure 3.6.) Also note that the URL in Figure 3.6 is actually /static_pages/home, but Safari displays only the base URL, which in this case is my development server at Cloud9.

Following this model for the Help page (Listing 3.12) and the About page (Listing 3.21) yields the code in Listing 3.27 and Listing 3.28.

Listing 3.27: The view for the Help page with full HTML structure. RED
app/views/static_pages/help.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title>Help | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Help</h1>
    <p>
      Get help on the Ruby on Rails Tutorial at the
      <a href="http://www.railstutorial.org/help">Rails Tutorial help
      page</a>.
      To get help on this sample app, see the
      <a href="http://www.railstutorial.org/book"><em>Ruby on Rails
      Tutorial</em> book</a>.
    </p>
  </body>
</html>

Listing 3.28: The view for the About page with full HTML structure. GREEN
app/views/static_pages/about.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title>About | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>About</h1>
    <p>
      The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
      Tutorial</em></a> is a
      <a href="http://www.railstutorial.org/book">book</a> and
      <a href="http://screencasts.railstutorial.org/">screencast series</a>
      to teach web development with
      <a href="http://rubyonrails.org/">Ruby on Rails</a>.
      This is the sample application for the tutorial.
    </p>
  </body>
</html>

At this point, the test suite should be back to GREEN:

Listing 3.29: GREEN

Click here to view code image

$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

Beginning in this section, we’ll start making modifications to the applications in the exercises that won’t generally be reflected in future code listings. The reason is so that the text makes sense to readers who don’t complete the exercises, but as a result your code will diverge from the main text if you do solve them. Learning to resolve small discrepancies like this is an excellent example of technical sophistication (Box 1.1).

1. You may have noticed some repetition in the Static Pages controller test (Listing 3.24). In particular, the base title, “Ruby on Rails Tutorial Sample App”, is the same for every title test. Using the special function setup, which is automatically run before every test, verify that the tests in Listing 3.30 are still GREEN. (Listing 3.30 uses an instance variable, seen briefly in Section 2.2.2 and covered further in Section 4.4.5, combined with string interpolation, which is covered further in Section 4.2.2.)

Listing 3.30: The Static Pages controller test with a base title. GREEN
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success

    assert_select "title", "Home | #{@base_title}"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | #{@base_title}"
  end
end

3.4.3 Layouts and Embedded Ruby (Refactor)

We’ve achieved a lot already in this section, generating three valid pages using Rails controllers and actions, but they are purely static HTML and hence don’t show off the power of Rails. Moreover, they suffer from terrible duplication:

• The page titles are almost (but not quite) exactly the same.

• “Ruby on Rails Tutorial Sample App” is common to all three titles.

• The entire HTML skeleton structure is repeated on each page.

This repeated code is a violation of the important “Don’t Repeat Yourself” (DRY) principle; in this section we’ll “DRY out our code” by removing the repetition. At the end, we’ll re-run the tests from Section 3.4.2 to verify that the titles are still correct.

Paradoxically, we’ll take the first step toward eliminating duplication by first adding some more: we’ll make the titles of the pages, which are currently quite similar, match exactly. This will make it much simpler to remove all the repetition at a stroke.

The technique involves using embedded Ruby in our views. Since the Home, Help, and About page titles have a variable component, we’ll use a special Rails function called provide to set a different title on each page. We can see how this works by replacing the literal title “Home” in the home.html.erb view with the code in Listing 3.31.

Listing 3.31: The view for the Home page with an embedded Ruby title. GREEN
app/views/static_pages/home.html.erb

Click here to view code image

<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

Listing 3.31 is our first example of embedded Ruby, also called ERb. (Now you know why HTML views have the file extension .html.erb.) ERb is the primary template system for including dynamic content in web pages.¹³ The code

13. There is a second popular template system called Haml (note: not “HAML”), which I personally love, but it’s not quite standard enough for use in an introductory tutorial.

<% provide(:title, "Home") %>

indicates using <% ... %> that Rails should call the provide function and associate the string "Home" with the label :title.¹⁴ Then, in the title, we use the closely related notation <%= ... %> to insert the title into the template using Ruby’s yield function:¹⁵

14. Experienced Rails developers might have expected the use of content_for at this point, but it doesn’t work well with the asset pipeline. The provide function is its replacement.

15. If you’ve studied Ruby before, you might suspect that Rails is yielding the contents to a block, and your suspicion would be correct. But you don’t need to know this to develop applications with Rails.

Click here to view code image

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

(The distinction between the two types of embedded Ruby is that <% ... %> executes the code inside, while <%= ... %> executes it and inserts the result into the template.) The resulting page is exactly the same as before, only now the variable part of the title is generated dynamically by ERb.

We can verify that all this works by running the tests from Section 3.4.2 and see that they are still GREEN:

Listing 3.32: GREEN

Click here to view code image

$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

Then we can make the corresponding replacements for the Help and About pages (Listing 3.33 and Listing 3.34).

Listing 3.33: The view for the Help page with an embedded Ruby title. GREEN
app/views/static_pages/help.html.erb

Click here to view code image

<% provide(:title, "Help") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Help</h1>
    <p>
      Get help on the Ruby on Rails Tutorial at the
      <a href="http://www.railstutorial.org/help">Rails Tutorial help
      section</a>.
      To get help on this sample app, see the
      <a href="http://www.railstutorial.org/book"><em>Ruby on Rails
      Tutorial</em> book</a>.
    </p>
  </body>
</html>

Listing 3.34: The view for the About page with an embedded Ruby title. GREEN
app/views/static_pages/about.html.erb

Click here to view code image

<% provide(:title, "About") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>About</h1>
    <p>
      The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
      Tutorial</em></a> is a
      <a href="http://www.railstutorial.org/book">book</a> and
      <a href="http://screencasts.railstutorial.org/">screencast series</a>
      to teach web development with
      <a href="http://rubyonrails.org/">Ruby on Rails</a>.
      This is the sample application for the tutorial.
    </p>
  </body>
</html>

Now that we’ve replaced the variable part of the page titles with ERb, each of our pages looks something like this:

Click here to view code image

<% provide(:title, "The Title") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    Contents
  </body>
</html>

In other words, all the pages are identical in structure, including the contents of the title tag, with the sole exception of the material inside the body tag.

In order to factor out this common structure, Rails comes with a special layout file called application.html.erb, which we renamed in the beginning of this section (Section 3.4) and which we’ll now restore:

Click here to view code image

$ mv layout_file app/views/layouts/application.html.erb

To get the layout to work, we have to replace the default title with the embedded Ruby from the examples above:

Click here to view code image

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

The resulting layout appears in Listing 3.35.

Listing 3.35: The sample application site layout. GREEN
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                              'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Note here the special line

<%= yield %>

This code is responsible for inserting the contents of each page into the layout. It’s not important to know exactly how this works; what matters is that using this layout ensures that, for example, visiting the page /static_pages/home converts the contents of home.html.erb to HTML and then inserts it in place of <%= yield %>.

It’s also worth noting that the default Rails layout includes several additional lines:

Click here to view code image

<%= csrf_meta_tags %>
<%= stylesheet_link_tag ... %>
<%= javascript_include_tag "application", ... %>

This code arranges to include the application stylesheet and JavaScript, which are part of the asset pipeline (Section 5.2.1), together with the Rails method csrf_meta_tags, which prevents cross-site request forgery (CSRF), a type of malicious web attack.

Of course, the views in Listing 3.31, Listing 3.33, and Listing 3.34 are still filled with all the HTML structure included in the layout, so we have to remove it, leaving only the interior contents. The resulting cleaned-up views appear in Listing 3.36, Listing 3.37, and Listing 3.38.

Listing 3.36: The Home page with HTML structure removed. GREEN
app/views/static_pages/home.html.erb

Click here to view code image

<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

Listing 3.37: The Help page with HTML structure removed. GREEN
app/views/static_pages/help.html.erb

Click here to view code image

<% provide(:title, "Help") %>
<h1>Help</h1>
<p>
  Get help on the Ruby on Rails Tutorial at the
  <a href="http://www.railstutorial.org/help">Rails Tutorial help section</a>.
  To get help on this sample app, see the
  <a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
  book</a>.
</p>

Listing 3.38: The About page with HTML structure removed. GREEN
app/views/static_pages/about.html.erb

Click here to view code image

<% provide(:title, "About") %>
<h1>About</h1>
<p>
  The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
  Tutorial</em></a> is a
  <a href="http://www.railstutorial.org/book">book</a> and
  <a href="http://screencasts.railstutorial.org/">screencast series</a>
  to teach web development with
  <a href="http://rubyonrails.org/">Ruby on Rails</a>.
  This is the sample application for the tutorial.
</p>

With these views defined, the Home, Help, and About pages are the same as before, but they have much less duplication.

Experience shows that even fairly simple refactoring is error-prone and can easily go awry. This is one reason why having a good test suite is so valuable. Rather than doublechecking every page for correctness—a procedure that isn’t too hard early on but rapidly becomes unwieldy as an application grows—we can simply verify that the test suite is still GREEN:

Listing 3.39: GREEN

Click here to view code image

$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

This isn’t a proof that our code is still correct, but it greatly increases the probability, thereby providing a safety net to protect us against future bugs.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Make a Contact page for the sample app.¹⁶ Following the model in Listing 3.15, first write a test for the existence of a page at the URL /static_pages/contact by testing for the title “Contact | Ruby on Rails Tutorial Sample App”. Get your test to pass by following the same steps as when making the About page in Section 3.3.3, including filling the Contact page with the content from Listing 3.40.

16. This exercise is solved in Section 5.3.1.

Listing 3.40: Code for a proposed Contact page.
app/views/static_pages/contact.html.erb

Click here to view code image

<% provide(:title, "Contact") %>
<h1>Contact</h1>
<p>
  Contact the Ruby on Rails Tutorial about the sample app at the
  <a href="http://www.railstutorial.org/contact">contact page</a>.
</p>

3.4.4 Setting the Root Route

Now that we’ve customized our site’s pages and gotten a good start on the test suite, let’s set the application’s root route before moving on. As in Section 1.3.4 and Section 2.2.2, this involves editing the routes.rb file to connect / to a page of our choice, which in this case will be the Home page. (At this point, I also recommend removing the hello action from the Application controller if you added it in Section 3.1.) As shown in Listing 3.41, this means changing the root route from

root 'application#hello'

to

root 'static_pages#home'

This arranges for requests for / to be routed to the home action in the Static Pages controller. The resulting routes file is shown in Figure 3.7.

Listing 3.41: Setting the root route to the Home page.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
end

Figure 3.7: The Home page at the root route.

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Adding the root route in Listing 3.41 leads to the creation of a Rails helper called root_url (in analogy with helpers like static_pages_home_url). By filling in the code marked FILL_IN in Listing 3.42, write a test for the root route.

2. Due to the code in Listing 3.41, the test in the previous exercise is already GREEN. In such a case, it’s harder to be confident that we’re actually testing what we think we’re testing, so modify the code in Listing 3.41 by commenting out the root route to get to RED (Listing 3.43). (We’ll talk more about Ruby comments in Section 4.2.1.) Then uncomment it (thereby restoring the original Listing 3.41) and verify that you get back to GREEN.

Listing 3.42: A test for the root route. GREEN
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get about" do
    get FILL_IN
    assert_response FILL_IN
  end
  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
  end
end

Listing 3.43: Commenting out the root route to get a failing test. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
#   root 'static_pages#home'
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
end

3.5 Conclusion

Seen from the outside, this chapter hardly accomplished anything: we started with static pages, and ended with… mostly static pages. But appearances are deceiving: by developing in terms of Rails controllers, actions, and views, we are now in a position to add arbitrary amounts of dynamic content to our site. Seeing exactly how this plays out is the task for the rest of this tutorial.

Before moving on, let’s take a minute to commit the changes on our topic branch and merge them into the master branch. Back in Section 3.2 we created a Git branch for the development of static pages. If you haven’t been making commits as we’ve been moving along, first make a commit indicating that we’ve reached a stopping point:

Click here to view code image

$ git add -A
$ git commit -m "Finish static pages"

Then merge the changes back into the master branch using the same technique as in Section 1.4.4:¹⁷

17. If you get an error message saying that the Spring process id (pid) file would be overwritten by the merge, just remove the file using rm -f *.pid at the command line.

Click here to view code image

$ git checkout master
$ git merge static-pages

Once you reach a stopping point like this, it’s usually a good idea to push your code up to a remote repository (which, if you followed the steps in Section 1.4.3, will be Bitbucket):

$ git push

I also recommend deploying the application to Heroku:

Click here to view code image

$ rails test
$ git push heroku

Here we’ve taken care to run the test suite before deploying, which is a good habit to develop.

3.5.1 What We Learned in This Chapter

• For a third time, we went through the full procedure of creating a new Rails application from scratch, installing the necessary gems, pushing it up to a remote repository, and deploying it to production.

• The rails script generates a new controller with rails generate controller ControllerName <optional action names>.

• New routes are defined in the file config/routes.rb.

• Rails views can contain static HTML or embedded Ruby (ERb).

• Automated testing allows us to write test suites that drive the development of new features, allow for confident refactoring, and catch regressions.

• Test-driven development uses a “Red, Green, Refactor” cycle.

• Rails layouts allow the use of a common template for pages in our application, thereby eliminating duplication.

3.6 Advanced Testing Setup

This optional section describes the testing setup used in the Ruby on Rails Tutorial screencast series. There are two main elements: an enhanced pass/fail reporter (Section 3.6.1), and an automated test runner that detects file changes and automatically runs the corresponding tests (Section 3.6.2). The code in this section is advanced and is presented for convenience only; you are not expected to understand it at this time.

The changes in this section should be made on the master branch:

$ git checkout master

3.6.1 Minitest Reporters

To get the default Rails tests to show RED and GREEN at the appropriate times, I recommend adding the code in Listing 3.44 to your test helper file,¹⁸ thereby making use of the minitest-reporters gem included in Listing 3.2.

18. The code in Listing 3.44 mixes single- and double-quoted strings. This is because rails new generates single-quoted strings, whereas the minitest reporters documentation uses double-quoted strings. This mixing of the two string types is common in Ruby; see Section 4.2.2 for more information.

Figure 3.8: Going from RED to GREEN in the cloud IDE.

Listing 3.44: Configuring the tests to show RED and GREEN.
test/test_helper.rb

Click here to view code image

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__ )
require 'rails/test_help'
require "minitest/reporters"
Minitest::Reporters.use!

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

The resulting transition from RED to GREEN in the cloud IDE appears as in Figure 3.8.¹⁹

19. The screenshot was taken using Rails 4.2, which as noted in Box 2.1 uses rake test in place of rails test. Resolving such minor discrepancies is a good exercise in technical sophistication (Box 1.1).

3.6.2 Automated Tests with Guard

One annoyance associated with using the rails test command is having to switch to the command line and run the tests by hand. To avoid this inconvenience, we can use Guard to automate the running of the tests. Guard monitors changes in the filesystem so that, for example, when we change the static_pages_controller_test.rb file, only those tests get run. Even better, we can configure Guard so that when, say, the home.html.erb file is modified, the static_pages_controller_test.rb automatically runs.

The Gemfile in Listing 3.2 has already included the guard gem in our application, so to get started we just need to initialize it:

Click here to view code image

$ bundle exec guard init
Writing new Guardfile to /home/ubuntu/workspace/sample_app/Guardfile
00:51:32 INFO minitest guard added to Guardfile, feel free to edit it

We then edit the resulting Guardfile so that Guard will run the right tests when the integration tests and views are updated (Listing 3.45). (Given its length and advanced nature, I recommend just copy-and-pasting the contents of Listing 3.45.)

Note: As of this writing, there is an interaction bug between Guard and Spring that causes all the tests to run every time. I’m keeping an eye on the issue and will update this section when it’s resolved.

Listing 3.45: A custom Guardfile.

Click here to view code image

# Defines the matching rules for Guard.
guard :minitest, spring: "bin/rails test", all_on_start: false do
  watch(%r{^test/(.*)/?(.*)_test\.rb$})
  watch('test/test_helper.rb') { 'test' }
  watch('config/routes.rb')    { integration_tests }
  watch(%r{^app/models/(.*?)\.rb$}) do |matches|
    "test/models/#{matches[1]}_test.rb"
  end
  watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
    resource_tests(matches[1])
  end
  watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
    ["test/controllers/#{matches[1]}_controller_test.rb"] +
    integration_tests(matches[1])
  end
    watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
      integration_tests(matches[1])
  end
  watch('app/views/layouts/application.html.erb') do
    'test/integration/site_layout_test.rb'
  end
  watch('app/helpers/sessions_helper.rb') do
    integration_tests << 'test/helpers/sessions_helper_test.rb'
  end
  watch('app/controllers/sessions_controller.rb') do
    ['test/controllers/sessions_controller_test.rb',
     'test/integration/users_login_test.rb']
  end
  watch('app/controllers/account_activations_controller.rb') do
    'test/integration/users_signup_test.rb'
  end
  watch(%r{app/views/users/*}) do
    resource_tests('users') +
    ['test/integration/microposts_interface_test.rb']
  end
end

# Returns the integration tests corresponding to the given resource.
def integration_tests(resource = :all)
  if resource == :all
    Dir["test/integration/*"]
  else
    Dir["test/integration/#{resource}_*.rb"]
  end
end

# Returns the controller tests corresponding to the given resource.
def controller_test(resource)
  "test/controllers/#{resource}_controller_test.rb"
end

# Returns all tests for the given resource.
def resource_tests(resource)
  integration_tests(resource) << controller_test(resource)
end

Here the line

Click here to view code image

guard :minitest, spring: "bin/rails test", all_on_start: false do

causes Guard to use the Spring server supplied by Rails to speed up loading times, while also preventing Guard from running the full test suite upon starting.

To prevent conflicts between Spring and Git when using Guard, you should add the spring/ directory to the .gitignore file used by Git to determine what to ignore when adding files or directories to the repository. The way to do this using the cloud IDE is as follows:

1. Click on the gear icon in the upper right part of the file navigation pane (Figure 3.9).

2. Select “Show hidden files” to show the .gitignore file in the application’s root directory (Figure 3.10).

3. Double-click on the .gitignore file (Figure 3.11) to open it, and then fill it with the contents of Listing 3.46.

Figure 3.9: The (rather subtle) gear icon in the file navigator pane.

Listing 3.46: Adding Spring to the .gitignore file.

Click here to view code image

# See https://help.github.com/articles/ignoring-files for more about
# ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
#   git config --global core.excludesfile '˜/.gitignore_global'

# Ignore bundler config.
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal

# Ignore all logfiles and tempfiles.
/log/*

/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore Byebug command history file.
.byebug_history

# Ignore Spring files.
/spring/*.pid

Figure 3.10: Showing hidden files in the file navigator.

The Spring server is still a little quirky as of this writing, and sometimes Spring processes will accumulate and slow performance of your tests. If your tests seem to be getting unusually sluggish, it’s thus a good idea to inspect the system processes and kill them if necessary (Box 3.4).

Figure 3.11: The normally hidden .gitignore file made visible.

Box 3.4: Unix Processes

On Unix-like systems such as Linux and OS X, user and system tasks each take place within a well-defined container called a process. To see all the processes on your system, you can use the ps command with the aux options:

$ ps aux

To filter the processes by type, you can run the results of ps through the grep pattern-matcher using a Unix pipe |:

$ ps aux | grep spring
ubuntu 12241 0.3 0.5 589960 178416 ? Ssl Sep20 1:46
spring app | sample_app | started 7 hours ago

The result shown gives some details about the process, but the most important thing is the first number, which is the process id, or pid. To eliminate an unwanted process, use the kill command to issue the Unix termination signal (which happens to be 15) to the pid:

$ kill -15 12241

This is the technique I recommend for killing individual processes, such as a rogue Rails server (with the pid found via ps aux | grep server), but sometimes it’s convenient to kill all the processes matching a particular process name, such as when you want to kill all the spring processes gunking up your system. In this particular case, you should first try stopping the processes with the spring command itself:

$ spring stop

Sometimes this doesn’t work, though, and you can kill all the processes with name spring using the pkill command as follows:

$ pkill -15 -f spring

Any time something isn’t behaving as expected or a process appears to be frozen, it’s a good idea to run ps aux to see what’s going on, and then run kill -15 <pid> or pkill -15 -f <name> to clear things up.

Once Guard is configured, you should open a new terminal and (as with the Rails server in Section 1.3.2) run it at the command line as follows:

$ bundle exec guard

The rules in Listing 3.45 are optimized for this tutorial, automatically running (for example) the integration tests when a controller is changed. To run all the tests, hit return at the guard> prompt. (This may sometimes give an error indicating a failure to connect to the Spring server. To fix the problem, just hit return again.)

To exit Guard, press Ctrl-D. To add additional matchers to Guard, refer to the examples in Listing 3.45, the Guard README, and the Guard wiki.

Before proceeding, you should add your changes and make a commit:

Click here to view code image

$ git add -A
$ git commit -m "Complete advanced setup"

Chapter 4. Rails-flavored ruby

Grounded in examples from Chapter 3, this chapter explores some elements of the Ruby programming language that are important for Rails. Ruby is a big language, but fortunately the subset needed to be productive as a Rails developer is relatively small. It also differs somewhat from the usual material covered in an introduction to Ruby. This chapter is designed to give you a solid foundation in Rails-flavored Ruby, whether or not you have prior experience in the language. It covers a lot of material, and it’s OK not to get it all on the first pass. I’ll refer back to it frequently in future chapters.

4.1 Motivation

As we saw in the last chapter, it’s possible to develop the skeleton of a Rails application, and even start testing it, with essentially no knowledge of the underlying Ruby language. We did this by relying on the test code provided by the tutorial and addressing each error message until the test suite was passing. This situation can’t last forever, though, and we’ll open this chapter with an addition to the site that brings us face-to-face with our Ruby limitations.

As in Section 3.2, we’ll use a separate topic branch to keep our changes selfcontained:

Click here to view code image

$ git checkout -b rails-flavored-ruby

We’ll merge our changes into master in Section 4.5.

4.1.1 Built-In Helpers

When we last saw our new application, we had just updated our mostly static pages to use Rails layouts to eliminate duplication in our views, as shown in Listing 4.1 (which is the same as Listing 3.35).

Listing 4.1: The sample application site layout.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                            'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                                            'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Let’s focus on one particular line in Listing 4.1:

Click here to view code image

<%= stylesheet_link_tag 'application', media: 'all',
                                       'data-turbolinks-track': 'reload' %>

This uses the built-in Rails function stylesheet_link_tag (which you can read more about at the Rails API) to include application.css for all media types (including computer screens and printers). To an experienced Rails developer, this line looks simple, but there are at least four potentially confusing Ruby ideas: built-in Rails methods, method invocation with missing parentheses, symbols, and hashes. We’ll cover all of these ideas in this chapter.

4.1.2 Custom Helpers

In addition to coming equipped with a large number of built-in functions for use in the views, Rails also allows the creation of new ones. Such functions are called helpers; to see how to make a custom helper, let’s start by examining the title line from Listing 4.1:

Click here to view code image

<%= yield(:title) %> | Ruby on Rails Tutorial Sample App

This relies on the definition of a page title (using provide) in each view, as in

Click here to view code image

<% provide(:title, "Home") %>
<h1>Sample App</h1> <p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

But what if we don’t provide a title? It’s a good convention to have a base title we use on every page, with an optional page title if we want to be more specific. We’ve almost achieved that with our current layout, with one wrinkle: as you can see, if you delete the provide call in one of the views, in the absence of a page-specific title, the full title appears as follows:

Click here to view code image

| Ruby on Rails Tutorial Sample App

In other words, there’s a suitable base title, but there’s also a leading vertical bar character | at the beginning.

To solve the problem of a missing page title, we’ll define a custom helper called full_title. The full_title helper returns a base title, “Ruby on Rails Tutorial Sample App”, if no page title is defined, and adds a vertical bar preceded by the page title if one is defined (Listing 4.2).¹

1. If a helper is specific to a particular controller, you should put it in the corresponding helper file; for example, helpers for the Static Pages controller generally go in app/helpers/static_pages_helper.rb. In our case, we expect the full_title helper to be used on all the site’s pages, and Rails has a special helper file for this case: app/helpers/application_helper.rb.

Listing 4.2: Defining a full_title helper.
app/helpers/application_helper.rb

Click here to view code image

module ApplicationHelper

  # Returns the full title on a per-page basis.
  def full_title(page_title = '')
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

Now that we have a helper, we can use it to simplify our layout by replacing

Click here to view code image

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

with

Click here to view code image

<title><%= full_title(yield(:title)) %></title>

as seen in Listing 4.3.

Listing 4.3: The site layout with the full_title helper. GREEN
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%=  full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag     'application', media: 'all',
                                             'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag  'application',
                                             'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

To put our helper to work, we can eliminate the unnecessary word “Home” from the Home page, allowing it to revert to the base title. We do this by first updating our test with the code in Listing 4.4, which updates the previous title test and adds one to test for the absence of the custom "Home" string in the title.

Listing 4.4: An updated test for the Home page’s title. RED
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

Let’s run the test suite to verify that one test fails:

Listing 4.5: RED

Click here to view code image

$ rails test
3 tests, 6 assertions, 1 failures, 0 errors, 0 skips

To get the test suite to pass, we’ll remove the provide line from the Home page’s view, as seen in Listing 4.6.

Listing 4.6: The Home page with no custom page title. GREEN
app/views/static_pages/home.html.erb

Click here to view code image

<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>

At this point the tests should pass:

Listing 4.7: GREEN

$ rails test

(Previous examples have included partial output of running rails test, including the number of passing and failing tests, but for brevity these will usually be omitted from here on.)

As with the line to include the application stylesheet in Section 4.1.1, the code in Listing 4.2 may look simple to the eyes of an experienced Rails developer, but it’s full of important Ruby ideas: modules, method definition, optional method arguments, comments, local variable assignment, booleans, control flow, string concatenation, and return values. This chapter will cover all of these ideas as well.

4.2 Strings and Methods

Our principal tool for learning Ruby will be the Rails console, a command-line program for interacting with Rails applications first seen in Section 2.3.3. The console itself is built on top of interactive Ruby (irb), and thus has access to the full power of the Ruby language. (As we’ll see in Section 4.4.4, the console also has access to the Rails environment.)

If you’re using the cloud IDE, there are a couple of irb configuration parameters I recommend including. Using the simple nano text editor, open a file called .irbrc in the home directory:²

2. The nano editor is easier for beginners, but for this sort of short edit I would almost always use the Vim editor instead. To learn Minimum Viable Vim, see Learn Enough Text Editor to Be Dangerous.

$ nano ˜ /.irbrc

Then fill it with the contents of Listing 4.8, which arranges to simplify the irb prompt and suppress some annoying auto-indent behavior.

Listing 4.8: Adding some irb configuration.
˜/.irbrc

Click here to view code image

IRB.conf[:PROMPT_MODE] = :SIMPLE
IRB.conf[:AUTO_INDENT_MODE] = false

Finally, exit nano with Ctrl-X and save ˜/.irbrc by typing y to confirm.

We can now start the console at the command line as follows:

Click here to view code image

$ rails console
Loading development environment
>>

By default, the console starts in a development environment, which is one of three separate environments defined by Rails (the others are test and production). This distinction won’t be important in this chapter, but it will in the future, and we’ll learn more about environments in Section 7.1.1.

The console is a great learning tool, and you should feel free to explore. Don’t worry—you (probably) won’t break anything. When using the console, type Ctrl-C if you get stuck, or type Ctrl-D to exit the console altogether. As with a regular terminal shell, you can also use up-arrow to retrieve previous commands, which can be a significant time-saver.

Throughout the rest of this chapter, you might find it helpful to consult the Ruby API. It’s packed (perhaps even too packed) with information; for example, to learn more about Ruby strings you can look at the Ruby API entry for the String class.

4.2.1 Comments

Ruby comments start with the pound sign # (also called the “hash mark” or, more poetically, the “octothorpe”) and extend to the end of the line. Ruby ignores comments, but they are useful for human readers (including, often, the original author!). In the code

Click here to view code image

# Returns the full title on a per-page basis.
def full_title(page_title = '')
  .
  .
  .
end

the first line is a comment indicating the purpose of the subsequent function definition.

You don’t ordinarily include comments in console sessions, but for instructional purposes I’ll include some comments in what follows, like this:

Click here to view code image

$ rails console
>> 17 + 42   # Integer addition
=> 59

If you follow along in this section by typing or copying-and-pasting commands into your own console, you can of course omit the comments if you like; the console will ignore them in any case.

4.2.2 Strings

Strings are probably the most important data structure for web applications, since web pages ultimately consist of strings of characters sent from the server to the browser. Let’s start exploring strings with the console:

Click here to view code image

$ rails console
>> ""         # An empty string
=> ""
>> "foo"      # A nonempty string
=> "foo"

These are string literals (also called literal strings), created using the double quote character ". The console prints the result of evaluating each line, which in the case of a string literal is just the string itself.

We can also concatenate strings with the + operator:

Click here to view code image

>> "foo" + "bar"    # String concatenation
=> "foobar"

Here the result of evaluating "foo" plus "bar" is the string "foobar".³

3. For more on the origins of “foo” and “bar”—and, in particular, the possible non-relation of “foobar” to “FUBAR”—see the Jargon File entry on “foo”.

Another way to build up strings is via interpolation using the special syntax #{}:⁴

4. Programmers familiar with Perl or PHP should compare this to the automatic interpolation of dollar sign variables in expressions like "foo $bar".

Click here to view code image

>> first_name = "Michael"    # Variable assignment
=> "Michael"
>> "#{first_name} Hartl"     # String interpolation
=> "Michael Hartl"

Here we’ve assigned the value "Michael" to the variable first_name and then interpolated it into the string "#{first_name} Hartl". We could also assign both strings a variable name:

Click here to view code image

>> first_name = "Michael"
=> "Michael"
>> last_name = "Hartl"
=> "Hartl"
>> first_name + " " + last_name     # Concatenation, with a space in between
=> "Michael Hartl"
>> "#{first_name} #{last_name}"     # The equivalent interpolation
=> "Michael Hartl"

Note that the final two expressions are equivalent, but I prefer the interpolated version; having to add the single space " " seems a bit awkward.

Printing

To print a string to the screen, the most commonly used Ruby function is puts (pronounced “put ess”, for “put string”, though some people do pronounce it like the word “puts” instead):

Click here to view code image

>>puts "foo"    # put string
foo
=> nil

The puts method operates as a side-effect: the expression puts "foo" prints the string to the screen and then returns literally nothing: nil is a special Ruby value for “nothing at all”. (In what follows, I’ll sometimes suppress the => nil part for simplicity.)

As seen in the examples above, using puts automatically includes a new line after the string gets printed (the same as the behavior of the echo command covered in Learn Enough Command Line to Be Dangerous). The closely related print command prints the raw string without the extra line:

Click here to view code image

>> print "foo"    # print string without extra line
foo=> nil

You can see here that the output foo bumps right up against the prompt in the second line.

The technical name for an extra line of blank space is a newline, typically represented by “backslash n” \n We can arrange for print to replicate the behavior of puts by including an explicit newline character in the string:

Click here to view code image

>> print "foo\n"  # Same as puts "foo"
foo
=> nil

Single-Quoted Strings

All the examples so far have used double-quoted strings, but Ruby also supports singlequoted strings. For many uses, the two types of strings are effectively identical:

Click here to view code image

>> 'foo'            # A single-quoted string
=> "foo"
>> 'foo' + 'bar'
=> "foobar"

There’s an important difference, though; Ruby won’t interpolate into singlequoted strings:

Click here to view code image

>> '#{foo} bar'     # Single-quoted strings don't allow interpolation
=> "\#{foo} bar"

Note how the console returns values using double-quoted strings, which requires a backslash to escape special character combinations such as #{.

If double-quoted strings can do everything that single-quoted strings can do, and interpolate to boot, what’s the point of single-quoted strings? They are often useful because they are truly literal, containing exactly the characters you type. For example, the “backslash” character is special on most systems, as in the literal newline \n. If you want a variable to contain a literal backslash, single quotes make it easier:

Click here to view code image

>> '\n'    # A literal 'backslash n' combination
=> "\\n"

As with the #{ combination in our previous example, Ruby needs to escape the backslash with an additional backslash; inside double-quoted strings, a literal backslash is represented with two backslashes. For a small example like this, there’s not much savings, but if there are lots of things to escape, it can be a real help:

Click here to view code image

>> 'Newlines (\n) and tabs (\t) both use the backslash character \.'
=> "Newlines (\\n) and tabs (\\t) both use the backslash character \\."

Finally, it’s worth noting that, in the common case that single and double quotes are essentially interchangeable, you’ll often find that the source code switches between the two without any apparent pattern. There’s really nothing to be done about this, except to say, “Welcome to Ruby!”

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Assign variables city and state to your current city and state of residence. (If residing outside the United States, substitute the analogous quantities.)

2. Using interpolation, print (using puts) a string consisting of the city and state separated by a comma and as space, as in “Los Angeles, CA”.

3. Repeat the previous exercise but with the city and state separated by a tab character.

4. What is the result if you replace double quotes with single quotes in the previous exercise?

4.2.3 Objects and Message Passing

Everything in Ruby, including strings and even nil, is an object. We’ll see the technical meaning of this in Section 4.4.2, but I don’t think anyone ever understood objects by reading the definition in a book; you have to build up your intuition for objects by seeing lots of examples.

It’s easier to describe what objects do, which is respond to messages. An object like a string, for example, can respond to the message length, which returns the number of characters in the string:

Click here to view code image

>> "foobar".length         # Passing the "length" message to a string
=> 6

Typically, the messages that get passed to objects are methods, which are functions defined on those objects.⁵ Strings also respond to the empty? method:

5. Apologies in advance for switching haphazardly between function and method throughout this chapter; in Ruby, they’re the same thing: all methods are functions, and all functions are methods, because everything is an object.

Click here to view code image

>> "foobar".empty?
=> false
>> "".empty?
=> true

Note the question mark at the end of the empty? method. This is a Ruby convention indicating that the return value is boolean: true or false. Booleans are especially useful for control flow:

Click here to view code image

>> s = "foobar"
>> if s.empty?
>>   "The string is empty"
>> else
>>   "The string is nonempty"
>> end
=> "The string is nonempty"

To include more than one clause, we can use elsif (else + if):

Click here to view code image

>> if s.nil?
>>   "The variable is nil"
>> elsif s.empty?
>>   "The string is empty"
>> elsif s.include?("foo")
>>   "The string includes 'foo'"
>> end
=> "The string includes 'foo'"

Booleans can also be combined using the && (“and”), || (“or”), and ! (“not”) operators:

Click here to view code image

>> x = "foo"
=> "foo"
>> y = ""
=> ""
>> puts "Both strings are empty" if x.empty? && y.empty?
=> nil
>> puts "One of the strings is empty" if x.empty? || y.empty?
"One of the strings is empty"
=> nil
>> puts "x is not empty" if !x.empty?
"x is not empty"
=> nil

Since everything in Ruby is an object, it follows that nil is an object, so it too can respond to methods. One example is the to_s method that can convert virtually any object to a string:

Click here to view code image

>> nil.to_s
=> ""

This certainly appears to be an empty string, as we can verify by chaining the messages we pass to nil:

Click here to view code image

>> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
>> nil.to_s.empty?      # Message chaining
=> true

We see here that the nil object doesn’t itself respond to the empty? method, but nil.to_s does.

There’s a special method for testing for nil-ness, which you might be able to guess:

Click here to view code image

>> "foo".nil?
=> false
>> "".nil?
=> false
>> nil.nil?
=> true

The code

Click here to view code image

puts "x is not empty" if !x.empty?

also shows an alternate use of the if keyword: Ruby allows you to write a statement that is evaluated only if the statement following if is true. There’s a complementary unless keyword that works the same way:

Click here to view code image

>> string = "foobar"
>> puts "The string '#{string}' is nonempty." unless string.empty?
The string 'foobar' is nonempty.
=> nil

It’s worth noting that the nil object is special, in that it is the only Ruby object that is false in a boolean context, apart from false itself. We can see this using !! (read “bang bang”), which negates an object twice, thereby coercing it to its boolean value:

Click here to view code image

>> !!nil
=> false

In particular, all other Ruby objects are true, even 0:

Click here to view code image

>> !!0
=> true

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. What is the length of the string “racecar”?

2. Confirm using the reverse method that the string in the previous exercise is the same when its letters are reversed.

3. Assign the string “racecar” to the variable s. Confirm using the comparison operator == that s and s.reverse are equal.

4. What is the result of running the code shown in Listing 4.9? How does it change if you reassign the variable s to the string “onomatopoeia”? Hint: Use the up-arrow to retrieve and edit previous commands

Listing 4.9: A simple palindrome test.

Click here to view code image

>> puts "It's a palindrome!" if s == s.reverse

4.2.4 Method Definitions

The console allows us to define methods the same way we did with the home action from Listing 3.8 or the full_title helper from Listing 4.2. (Defining methods in the console is a bit cumbersome, and ordinarily you would use a file, but it’s convenient for demonstration purposes.) For example, let’s define a function string_message that takes a single argument and returns a message based on whether the argument is empty or not:

Click here to view code image

>> def string_message(str = '')
>>   if str.empty?
>>     "It's an empty string!"
>>   else
>>     "The string is nonempty."
>>   end
>> end
=> :string_message
>> puts string_message("foobar")
The string is nonempty.
>> puts string_message("")
It's an empty string!
>> puts string_message
It's an empty string!

As seen in the final example, it’s possible to leave out the argument entirely (in which case we can also omit the parentheses). This is because the code

Click here to view code image

def string_message(str = '')

contains a default argument, which in this case is the empty string. This makes the str argument optional, and if we leave it off, it automatically takes the given default value.

Note that Ruby functions have an implicit return, meaning they return the last statement evaluated—in this case, one of the two message strings, depending on whether the method’s argument str is empty or not. Ruby also has an explicit return option; the following function is equivalent to the one above:

Click here to view code image

>> def string_message(str = '')
>>   return "It's an empty string!" if str.empty?
>>   return "The string is nonempty."
>> end

(The alert reader might notice at this point that the second return here is actually unnecessary—being the last expression in the function, the string "The string is nonempty." will be returned regardless of the return keyword, but using return in both places has a pleasing symmetry to it.)

It’s also important to understand that the name of the function argument is irrelevant as far as the caller is concerned. In other words, the first example above could replace str with any other valid variable name, such as the_function_argument, and it would work just the same:

Click here to view code image

>> def string_message(the_function_argument = '')
>>   if the_function_argument.empty?
>>     "It's an empty string!"
>>   else
>>     "The string is nonempty."
>>   end
>> end
=> nil
>> puts string_message("")
It's an empty string!
>> puts string_message("foobar")
The string is nonempty.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By replacing FILL_IN with the appropriate comparison test shown in Listing 4.10, define a method for testing palindromes. Hint: Use the comparison shown in Listing 4.9.

2. By running your palindrome tester on “racecar” and “onomatopoeia”, confirm that the first is a palindrome and the second isn’t.

3. By calling the nil? method on palindrome_tester("racecar"), confirm that its return value is nil (i.e., calling nil? on the result of the method should return true). This is because the code in Listing 4.10 prints its responses instead of returning them.

Listing 4.10: A simple tester for palindromes.

Click here to view code image

>> def palindrome_tester(s)
>>   if FILL_IN
>>     puts "It's a palindrome!"
>>   else
>>     puts "It's not a palindrome."
>>   end
>> end

4.2.5 Back to the Title Helper

We are now in a position to understand the full_title helper from Listing 4.2,⁶ which appears with commented annotations in Listing 4.11.

6. Well, there will still be one thing left that we don’t understand, which is how Rails ties this all together: mapping URLs to actions, making the full_title helper available in views, etc. This is an interesting subject, and I encourage you to investigate it further, but knowing exactly how Rails works is not necessary when using Rails.

Listing 4.11: An annotated title_helper.
app/helpers/application_helper.rb

Click here to view code image

module ApplicationHelper

  # Returns the full title on a per-page basis.       # Documentation comment
  def full_title(page_title = '')                     # Method def, optional arg
    base_title = "Ruby on Rails Tutorial Sample App"  # Variable assignment
    if page_title.empty?                              # Boolean test
      base_title                                      # Implicit return
    else
      page_title + " | " + base_title                 # String concatenation
    end
  end
end

These elements—function definition (with an optional argument), variable assignment, boolean tests, control flow, and string concatenation⁷—come together to make a compact helper method for use in our site layout. The final element is module ApplicationHelper: modules give us a way to package together related methods, which can then be mixed in to Ruby classes using include. When writing ordinary Ruby, you often write modules and include them explicitly yourself, but in the case of a helper module Rails handles the inclusion for us. The result is that the full_title method is automagically available in all our views.

7. It’s tempting to use string interpolation instead—indeed, this was the technique used in all previous versions of the tutorial—but in fact the call to provide converts the string into a so-called SafeBuffer object instead of an ordinary string. Interpolating and inserting into a view template then over-escapes any inserted HTML, so a title such as “Help’s on the way” would be converted to “Help&amp;#39;s on the way”. (Thanks to reader Jeremy Fleischman for pointing out this subtle issue.)

4.3 Other Data Structures

Although web apps are ultimately about strings, actually making those strings requires using other data structures as well. In this section, we’ll learn about some Ruby data structures important for writing Rails applications.

4.3.1 Arrays and Ranges

An array is just a list of elements in a particular order. We haven’t discussed arrays yet in the Rails Tutorial, but understanding them gives a good foundation for understanding hashes (Section 4.3.3) and for aspects of Rails data modeling (such as the has_many association seen in Section 2.3.3 and covered more in Section 13.1.3).

So far we’ve spent a lot of time understanding strings, and there’s a natural way to get from strings to arrays using the split method:

Click here to view code image

>>  "foo bar     baz".split    # Split a string into a three-element array.
=> ["foo", "bar", "baz"]

The result of this operation is an array of three strings. By default, split divides a string into an array by splitting on whitespace, but you can split on nearly anything else as well:

Click here to view code image

>> "fooxbarxbazx".split('x')
=> ["foo", "bar", "baz"]

As is conventional in most computer languages, Ruby arrays are zero-offset, which means that the first element in the array has index 0, the second has index 1, and so on:

Click here to view code image

>> a = [42, 8, 17]
=> [42, 8, 17]
>> a[0]              # Ruby uses square brackets for array access.
=> 42
>> a[1]
=> 8
>> a[2]
=> 17
>> a[-1]             # Indices can even be negative!
=> 17

We see here that Ruby uses square brackets to access array elements. In addition to this bracket notation, Ruby offers synonyms for some commonly accessed elements:⁸

8. The second method used here isn’t currently part of Ruby itself, but rather is added by Rails. It works in this case because the Rails console automatically includes the Rails extensions to Ruby.

Click here to view code image

>> a                  # Just a reminder of what 'a' is
=> [42, 8, 17]
>> a.first
=> 42
>> a.second
=> 8
>> a.last
=> 17
>> a.last == a[-1]    # Comparison using ==
=> true

This last line introduces the equality comparison operator ==, which Ruby shares with many other languages, along with the associated != (“not equal”), etc.:

Click here to view code image

>> x = a.length      # Like strings, arrays respond to the 'length' method.
=> 3
>> x == 3
=> true
>> x == 1
=> false
>> x != 1
=> true
>> x >= 1
=> true
>> x < 1
=> false

In addition to length (seen in the first line above), arrays respond to a wealth of other methods:

Click here to view code image

>> a
=> [42, 8, 17]
>> a.empty?
=> false
>> a.include?(42)
=> true
>> a.sort
=> [8, 17, 42]
>> a.reverse
=> [17, 8, 42]
>> a.shuffle
=> [17, 42, 8]
>> a
=> [42, 8, 17]

Note that none of the methods above changes a itself. To mutate the array, use the corresponding “bang” methods (so-called because the exclamation point is usually pronounced “bang” in this context):

Click here to view code image

>> a
=> [42, 8, 17]
>> a.sort!
=> [8, 17, 42]
>> a
=> [8, 17, 42]

You can also add to arrays with the push method or its equivalent operator, <<:

Click here to view code image

>> a.push(6)                  # Pushing 6 onto an array
=> [42, 8, 17, 6]
>> a << 7                     # Pushing 7 onto an array
=> [42, 8, 17, 6, 7]
>> a << "foo" << "bar"        # Chaining array pushes
=> [42, 8, 17, 6, 7, "foo", "bar"]

This last example shows that you can chain pushes together, and also that, unlike arrays in many other languages, Ruby arrays can contain a mixture of different types (in this case, integers and strings).

Before we saw split convert a string to an array. We can also go the other way with the join method:

Click here to view code image

>> a
=> [42, 8, 17, 6, 7, "foo", "bar"]
>> a.join                       # Join on nothing.
=> "4281767foobar"
>> a.join(', ')                 # Join on comma-space.
=> "42, 8, 17, 6, 7, foo, bar"

Closely related to arrays are ranges, which can probably most easily be understood by converting them to arrays using the to_a method:

Click here to view code image

>> 0..9
=> 0..9
>> 0..9.to_a              # Oops, call to_a on 9.
NoMethodError: undefined method `to_a' for 9:Fixnum
>> (0..9).to_a            # Use parentheses to call to_a on the range.
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Though 0..9 is a valid range, the second expression above shows that we need to add parentheses to call a method on it.

Ranges are useful for pulling out array elements:

Click here to view code image

>> a = %w[foo bar baz quux]        # Use %w to make a string array.
=> ["foo", "bar", "baz", "quux"]
>> a[0..2]
=> ["foo", "bar", "baz"]

A particularly useful trick is to use the index -1 at the end of the range to select every element from the starting point to the end of the array without explicitly having to use the array’s length:

Click here to view code image

>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..(a.length-1)]               # Explicitly use the array's length.
=> [2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..-1]                         # Use the index -1 trick.
=> [2, 3, 4, 5, 6, 7, 8, 9]

Ranges also work with characters:

Click here to view code image

>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Assign a to be to the result of splitting the string “A man, a plan, a canal, Panama” on comma-space.

2. Assign s to the string resulting from joining a on nothing.

3. Split s on whitespace and rejoin on nothing. Use the palindrome test from Listing 4.10 to confirm that the resulting string s is not a palindrome by the current definition. Using the downcase method, show that s.downcase is a palindrome.

4. What is the result of selecting element 7 from the range of letters a through z? What about the same range reversed? Hint: In both cases you will have to convert the range to an array.

4.3.2 Blocks

Both arrays and ranges respond to a host of methods that accept blocks, which are simultaneously one of Ruby’s most powerful and most confusing features:

Click here to view code image

>> (1..5).each { |i| puts 2 * i }
2
4
6
8
10
=> 1..5

This code calls the each method on the range (1..5) and passes it the block { |i| puts 2 * i }. The vertical bars around the variable name in |i| are Ruby syntax for a block variable, and it’s up to the method to know what to do with the block. In this case, the range’s each method can handle a block with a single local variable, which we’ve called i, and it just executes the block for each value in the range.

Curly braces are one way to indicate a block, but there is a second way as well:

Click here to view code image

>> (1..5).each do |i|
?>   puts 2 * i
>> end
2
4
6
8
10
=> 1..5

Blocks can be more than one line, and often are. In the Rails Tutorial we’ll follow the common convention of using curly braces only for short one-line blocks and the do..end syntax for longer one-liners and for multi-line blocks:

Click here to view code image

>> (1..5).each do |number|
?>   puts 2 * number
>>   puts '--'
>> end
2
--
4
--
6
--
8
--
10
--
=> 1..5

Here I’ve used number in place of i just to emphasize that any variable name will do.

Unless you already have a substantial programming background, there is no shortcut to understanding blocks; you just have to see them a lot, and eventually you’ll get used to them.⁹ Luckily, humans are quite good at making generalizations from concrete examples; here are a few more, including a couple using the map method:

9. Programming experts, on the other hand, might benefit from knowing that blocks are closures, which are one-shot anonymous functions with data attached.

Click here to view code image

>> 3.times { puts "Betelgeuse!" }   # 3.times takes a block with no variables.
"Betelgeuse!"
"Betelgeuse!"
"Betelgeuse!"
=> 3
>> (1..5).map { |i| i**2 }          # The ** notation is for 'power'.
=> [1, 4, 9, 16, 25]
>> %w[a b c]                        # Recall that %w makes string arrays.
=> ["a", "b", "c"]
>> %w[a b c].map { |char| char.upcase }
=> ["A", "B", "C"]
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]

As you can see, the map method returns the result of applying the given block to each element in the array or range. In the final two examples, the block inside map involves calling a particular method on the block variable, and in this case there’s a commonly used shorthand called “symbol-to-proc”:

Click here to view code image

>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
>> %w[A B C].map(&:downcase)
=> ["a", "b", "c"]

(This strange-looking but compact code uses a symbol, which we’ll discuss in Section 4.3.3.) One interesting thing about this construction is that it was originally added to Ruby on Rails, and people liked it so much that it has now been incorporated into core Ruby.

As one final example of blocks, we can take a look at an individual test from the file in Listing 4.4:

Click here to view code image

test "should get home" do
  get static_pages_home_url
  assert_response :success
  assert_select "title", "Ruby on Rails Tutorial Sample App"
end

It’s not important to understand the details (and in fact I don’t know the details offhand), but we can infer from the presence of the do keyword that the body of the test is a block. The test method takes in a string argument (the description) and a block, and then executes the body of the block as part of running the test suite.

By the way, we’re now in a position to understand the line of Ruby I threw into Section 1.5.4 to generate random subdomains:

Click here to view code image

('a'..'z').to_a.shuffle[0..7].join

Let’s build it up step-by-step:

Click here to view code image

>> ('a'..'z').to_a                     # An alphabet array
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q","r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle             # Shuffle it.
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7]       # Pull out the first eight elements.
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join  # Join them together to make one string.
=> "mznpybuj"

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the range 0..16, print out the first 17 powers of 2.

2. Define a method called yeller that takes in an array of characters and returns a string with an ALLCAPS version of the input. Verify that yeller (['o', 'l', 'd']) returns "OLD". Hint: Combine map, upcase, and join.

3. Define a method called random_subdomain that returns a randomly generated string of eight letters.

4. By replacing the question marks in Listing 4.12 with the appropriate methods, combine split, shuffle, and join to write a function that shuffles the letters in a given string.

Listing 4.12: Skeleton for a string shuffle function.

Click here to view code image

>> def string_shuffle(s)
>>   s.?('').?.?
>> end
>> string_shuffle("foobar")
=> "oobfra"

4.3.3 Hashes and Symbols

Hashes are essentially arrays that aren’t limited to integer indices. (In fact, some languages, especially Perl, sometimes call hashes associative arrays for this reason.) Instead, hash indices, or keys, can be almost any object. For example, we can use strings as keys:

Click here to view code image

>> user = {}                          # {} is an empty hash.
=> {}
>> user["first_name"] = "Michael"     # Key "first_name", value "Michael"
=> "Michael"
>> user["last_name"] = "Hartl"        # Key "last_name", value "Hartl"
=> "Hartl"
>> user["first_name"]                 # Element access is like arrays.
=> "Michael"
>> user                               # A literal representation of the hash
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}

Hashes are indicated with curly braces containing key-value pairs; a pair of braces with no key-value pairs—i.e., {}—is an empty hash. It’s important to note that the curly braces for hashes have nothing to do with the curly braces for blocks. (Yes, this can be confusing.) Although hashes resemble arrays, one important difference is that hashes don’t generally guarantee keeping their elements in a particular order.¹⁰ If order matters, use an array.

10. Ruby versions 1.9 and later actually guarantee that hashes keep their elements in the same order entered, but it would be unwise ever to count on a particular ordering.

Instead of defining hashes one item at a time using square brackets, it’s easy to use a literal representation with keys and values separated by =>, called a “hashrocket”:

Click here to view code image

>> user = { "first_name" => "Michael", "last_name" => "Hartl" }
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}

Here I’ve used the usual Ruby convention of putting an extra space at the two ends of the hash—a convention ignored by the console output. (Don’t ask me why the spaces are conventional; probably some early influential Ruby programmer liked the look of the extra spaces, and the convention stuck.)

So far we’ve used strings as hash keys, but in Rails it is much more common to use symbols instead. Symbols look kind of like strings but are prefixed with a colon instead of surrounded by quotes. For example, :name is a symbol. You can think of symbols as basically strings without all the extra baggage:¹¹

11. As a result of having less baggage, symbols are easier to compare to each other; strings need to be compared character by character, while symbols can be compared all in one go. This makes them ideal for use as hash keys.

Click here to view code image

>> "name".split('')
=> ["n", "a", "m", "e"]
>> :name.split('')
NoMethodError: undefined method 'split' for :name:Symbol
>> "foobar".reverse
=> "raboof"
>> :foobar.reverse
NoMethodError: undefined method 'reverse' for :foobar:Symbol

Symbols are a special Ruby data type shared with very few other languages, so they may seem weird at first, but Rails uses them a lot, so you’ll get used to them fast. Unlike strings, not all characters are valid:

Click here to view code image

>> :foo-bar
NameError: undefined local variable or method 'bar' for main:Object
>> :2foo
SyntaxError

As long as you start your symbols with a letter and stick to normal word characters, you should be fine.

In terms of symbols as hash keys, we can define a user hash as follows:

Click here to view code image

>> user = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> user[:name]              # Access the value corresponding to :name.
=> "Michael Hartl"
>> user[:password]          # Access the value of an undefined key.
=> nil

We see here from the last example that the hash value for an undefined key is simply nil.

Because it’s so common for hashes to use symbols as keys, as of version 1.9 Ruby supports a new syntax just for this special case:

Click here to view code image

>> h1 = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h2 = { name: "Michael Hartl", email: "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h1 == h2
=> true

The second syntax replaces the symbol/hashrocket combination with the name of the key followed by a colon and a value:

Click here to view code image

{ name: "Michael Hartl", email: "michael@example.com" }

This construction more closely follows the hash notation in other languages (such as JavaScript) and enjoys growing popularity in the Rails community. Because both hash syntaxes are still in common use, it’s essential to be able to recognize both of them. Unfortunately, this can be confusing, especially since :name is valid on its own (as a stand-alone symbol) but name: has no meaning by itself. The bottom line is that :name => and name: are effectively the same only inside literal hashes, so that

{ :name => "Michael Hartl" }

and

{ name: "Michael Hartl" }

are equivalent, but otherwise you need to use :name (with the colon coming first) to denote a symbol

Hash values can be virtually anything, even other hashes, as seen in Listing 4.13.

Listing 4.13: Nested hashes.

Click here to view code image

>> params = {}        # Define a hash called 'params' (short for 'parameters').
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>>  params[:user][:email]
=> "mhartl@example.com"

These sorts of hashes-of-hashes, or nested hashes, are heavily used by Rails, as we’ll see starting in Section 7.3.

As with arrays and ranges, hashes respond to the each method. For example, consider a hash named flash with keys for two conditions, :success and :danger:

Click here to view code image

>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?>   puts "Key #{key.inspect} has value #{value.inspect}"
>> end
Key :success has value "It worked!"
Key :danger has value "It failed."

Note that, while the each method for arrays takes a block with only one variable, each for hashes takes two, a key and a value. Thus, the each method for a hash iterates through the hash one key-value pair at a time.

The last example uses the useful inspect method, which returns a string with a literal representation of the object it’s called on:

Click here to view code image

>> puts (1..5).to_a            # Put an array as a string.
1
2
3
4
5
>> puts (1..5).to_a.inspect    # Put a literal array.
[1, 2, 3, 4, 5]
>> puts :name, :name.inspect
name
:name
>> puts "It worked!", "It worked!".inspect
It worked!
"It worked!"

By the way, using inspect to print an object is common enough that there’s a shortcut for it, the p function:¹²

12. There’s actually a subtle difference, which is that p returns the object being printed while puts always returns nil. (Thanks to reader Katarzyna Siwek for pointing this out.)

Click here to view code image

>> p :name        # Same output as 'puts :name.inspect'
:name

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Define a hash with the keys 'one', 'two', and 'three', and the values 'uno', 'dos', and 'tres'. Iterate over the hash, and for each key/value pair print out "'#{key}' in Spanish is '#{value}'".

2. Create three hashes called person1, person2, and person3, with first and last names under the keys :first and :last. Then create a params hash so that params[:father] is person1, params[:mother] is person2, and params[:child] is person3. Verify that, for example, params[:father] [:first] has the right value.

3. Define a hash with symbol keys corresponding to name, email, and a “password digest”, and values equal to your name, your email address, and a random string of 16 lowercase letters.

4. Find an online version of the Ruby API and read about the Hash method merge. What is the value of the following expression?

Click here to view code image

{ "a" => 100, "b" => 200 }.merge({ "b" => 300 })

4.3.4 CSS Revisited

It’s time now to revisit the line from Listing 4.1 used in the layout to include the cascading style sheets:

Click here to view code image

<%= stylesheet_link_tag 'application', media: 'all',
                                       'data-turbolinks-track': 'reload' %>

We are now nearly in a position to understand this. As mentioned briefly in Section 4.1, Rails defines a special function to include stylesheets, and

Click here to view code image

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

is a call to this function. But there are several mysteries. First, where are the parentheses? In Ruby, they are optional, so these two are equivalent:

Click here to view code image

# Parentheses on function calls are optional.
stylesheet_link_tag('application', media: 'all',
                                   'data-turbolinks-track': 'reload')
stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

Second, the media argument sure looks like a hash, but where are the curly braces? When hashes are the last argument in a function call, the curly braces are optional, so these two are equivalent:

Click here to view code image

# Curly braces on final hash arguments are optional.
stylesheet_link_tag 'application', { media: 'all',
                                     'data-turbolinks-track': 'reload' }
stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

Finally, why does Ruby correctly interpret the lines

Click here to view code image

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

even with a line break between the final elements? The answer is that Ruby doesn’t distinguish between newlines and other whitespace in this context.¹³ The reason I chose to break the code into pieces is that I prefer to keep lines of source code under 80 characters for legibility.¹⁴

13. A newline is what comes at the end of a line, thereby starting a new line. As noted in Section 4.2.2, it is typically represented by the character \n.

14. Actually counting columns could drive you crazy, which is why many text editors have a visual aid to help you. For example, if you take a look back at Figure 1.5, you’ll see a small vertical line on the right to help keep code under 80 characters. The cloud IDE (Section 1.2.1) includes such a line by default. If you use TextMate, you can find this feature under View > Wrap Column > 78. In Sublime Text, you can use View > Ruler > 78 or View > Ruler > 80.

So, we see now that the line

Click here to view code image

stylesheet_link_tag 'application', media: 'all',
                                   'data-turbolinks-track': 'reload'

calls the stylesheet_link_tag function with two arguments: a string, indicating the path to the stylesheet, and a hash with two elements, indicating the media type and telling Rails to use the turbolinks feature added in Rails 4.0. Because of the <%= %> brackets, the results are inserted into the template by ERb, and if you view the source of the page in your browser, you should see the HTML needed to include a stylesheet (Listing 4.14). (You may see some extra things, like ?body=1, after the CSS filenames. These are inserted by Rails to ensure that browsers reload the CSS when it changes on the server.)

Listing 4.14: The HTML source produced by the CSS includes.

Click here to view code image

<link data-turbolinks-track="true" href="/assets/application.css" media="all"
rel="stylesheet" />

4.4 Ruby Classes

We’ve said before that everything in Ruby is an object, and in this section we’ll finally get to define some of our own. Ruby, like many object-oriented languages, uses classes to organize methods; these classes are then instantiated to create objects. If you’re new to object-oriented programming, this may sound like gibberish, so let’s look at some concrete examples.

4.4.1 Constructors

We’ve seen lots of examples of using classes to instantiate objects, but we have yet to do so explicitly. For example, we instantiated a string using the double quote characters, which is a literal constructor for strings:

Click here to view code image

>> s = "foobar"    # A literal constructor for strings using double quotes
=> "foobar"
>> s.class
=> String

We see here that strings respond to the method class, and simply return the class they belong to.

Instead of using a literal constructor, we can use the equivalent named constructor, which involves calling the new method on the class name:¹⁵

15. These results will vary based on the version of Ruby you are using. This example assumes you are using Ruby 1.9.3 or later.

Click here to view code image

>> s = String.new("foobar")    # A named constructor for a string
=> "foobar"
>> s.class
=> String
>> s == "foobar"
=> true

This is equivalent to the literal constructor, but it’s more explicit about what we’re doing.

Arrays work the same way as strings:

Click here to view code image

>> a = Array.new([1, 3, 2])
=> [1, 3, 2]

Hashes, in contrast, are different. While the array constructor Array.new takes an initial value for the array, Hash.new takes a default value for the hash, which is the value of the hash for a nonexistent key:

Click here to view code image

>> h = Hash.new
=> {}
>> h[:foo]            # Try to access the value for the nonexistent key :foo.
=> nil
>> h = Hash.new(0)    # Arrange for nonexistent keys to return 0 instead of nil.
=> {}
>> h[:foo]
=> 0

When a method gets called on the class itself, as in the case of new, it’s called a class method. The result of calling new on a class is an object of that class, also called an instance of the class. A method called on an instance, such as length, is called an instance method.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. What is the literal constructor for the range of integers from 1 to 10?

2. What is the constructor using the Range class and the new method? Hint: new takes two arguments in this context.

3. Confirm using the == operator that the literal and named constructors from the previous two exercises are identical.

4.4.2 Class Inheritance

When learning about classes, it’s useful to find out the class hierarchy using the superclass method:

Click here to view code image

>> s = String.new("foobar")
=> "foobar"
>> s.class                        # Find the class of s.
=> String
>> s.class.superclass             # Find the superclass of String.
=> Object
>> s.class.superclass.superclass  # Ruby 1.9 uses a new BasicObject base class
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil

A diagram of this inheritance hierarchy appears in Figure 4.1. We see here that the superclass of String is Object and the superclass of Object is BasicObject, but BasicObject has no superclass. This pattern is true of every Ruby object: trace back the class hierarchy far enough and every class in Ruby ultimately inherits from BasicObject, which has no superclass itself. This is the technical meaning of “everything in Ruby is an object”.

Figure 4.1: The inheritance hierarchy for the String class.

To understand classes a little more deeply, there’s no substitute for making one of our own. Let’s make a Word class with a palindrome? method that returns true if the word is the same spelled forward and backward:

Click here to view code image

>> class Word
>>   def palindrome?(string)
>>     string == string.reverse
>>   end
>> end
=> :palindrome?

We can use it as follows:

Click here to view code image

>> w = Word.new              # Make a new Word object.
=> #<Word:0x22d0b20>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true

If this example strikes you as a bit contrived, good—this is by design. It’s odd to create a new class just to create a method that takes a string as an argument. Since a word is a string, it’s more natural to have our Word class inherit from String, as seen in Listing 4.15. (You should exit the console and re-enter it to clear out the old definition of Word.)

Listing 4.15: Defining a Word class in the console.

Click here to view code image

>> class Word < String             # Word inherits from String.
>>   # Returns true if the string is its own reverse.
>>   def palindrome?
>>     self == self.reverse        # self is the string itself.
>>   end
>> end
=> nil

Here Word < String is the Ruby syntax for inheritance (discussed briefly in Section 3.2), which ensures that, in addition to the new palindrome? method, words also have all the same methods as strings:

Click here to view code image

>> s = Word.new("level")     # Make a new Word, initialized with "level".
=> "level"
>> s.palindrome?             # Words have the palindrome? method.
=> true
>> s.length                  # Words also inherit all the normal string methods.
=> 5

Since the Word class inherits from String, we can use the console to see the class hierarchy explicitly:

Click here to view code image

>> s.class
=> Word
>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object

This hierarchy is illustrated in Figure 4.2.

Figure 4.2: The inheritance hierarchy for the (non-built-in) Word class from Listing 4.15.

In Listing 4.15, note that checking that the word is its own reverse involves accessing the word inside the Word class. Ruby allows us to do this using the self keyword: inside the Word class, self is the object itself, which means we can use

self == self.reverse

to check if the word is a palindrome.¹⁶ In fact, inside the String class the use of self. is optional on a method or attribute (unless we’re making an assignment), so

self == reverse

would work as well.

16. For more on Ruby classes and the self keyword, see the RailsTips post “Class and Instance Variables in Ruby”.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. What is the class hierarchy for a range? For a hash? For a symbol?

2. Confirm that the method shown in Listing 4.15 works even if we replace self.reverse with just reverse.

4.4.3 Modifying Built-in Classes

While inheritance is a powerful idea, in the case of palindromes it might be even more natural to add the palindrome? method to the String class itself, so that (among other things) we can call palindrome? on a string literal, which we currently can’t do:

Click here to view code image

>> "level".palindrome?
NoMethodError: undefined method `palindrome?' for "level":String

Amazingly, Ruby lets you do just this; Ruby classes can be opened and modified, allowing ordinary mortals such as ourselves to add methods to them:

Click here to view code image

>> class String
>>   # Returns true if the string is its own reverse.
>>   def palindrome?
>>     self == self.reverse
>>   end
>> end
=> nil
>> "deified".palindrome?
=> true

(I don’t know which is cooler: that Ruby lets you add methods to built-in classes, or that "deified" is a palindrome.)

Modifying built-in classes is a powerful technique, but with great power comes great responsibility, and it’s considered bad form to add methods to built-in classes without having a really good reason for doing so. Rails does have some good reasons; for example, in web applications we often want to prevent variables from being blank—e.g., a user’s name should be something other than spaces and other whitespace—so Rails adds a blank? method to Ruby. Since the Rails console automatically includes the Rails extensions, we can see an example here (this won’t work in plain irb):

Click here to view code image

>> "".blank?
=> true
>> "      ".empty?
=> false
>> "      ".blank?
=> true
>> nil.blank?
=> true

We see that a string of spaces is not empty, but it is blank. Note also that nil is blank; since nil isn’t a string, this is a hint that Rails actually adds blank? to String’s base class, which (as we saw at the beginning of this section) is Object itself. We’ll see some other examples of Rails additions to Ruby classes in Section 9.1.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that “racecar” is a palindrome and “onomatopoeia” is not. What about the name of the South Indian language “Malayalam”? Hint: Downcase it first.

2. Using Listing 4.16 as a guide, add a shuffle method to the String class. Hint: Refer to Listing 4.12.

3. Verify that Listing 4.16 works even if you remove self..

Listing 4.16: Skeleton for a shuffle method attached to the String class.

Click here to view code image

>> class String
>>   def shuffle
>>     self.?('').?.?
>>   end
>> end
>> "foobar".shuffle
=> "borafo"

4.4.4 A Controller Class

All this talk about classes and inheritance may have triggered a flash of recognition, because we have seen both before, in the Static Pages controller (Listing 3.20):

Click here to view code image

class StaticPagesController < ApplicationController

  def home
  end

  def help
  end

  def about
  end
end

You’re now in a position to appreciate, at least vaguely, what this code means: StaticPagesController is a class that inherits from ApplicationController, and comes equipped with home, help, and about methods. Since each Rails console session loads the local Rails environment, we can even create a controller explicitly and examine its class hierarchy:¹⁷

17. You don’t have to know what each class in this hierarchy does. I don’t know what they all do, and I’ve been programming in Ruby on Rails since 2005. This means either that (a) I’m grossly incompetent or (b) you can be a skilled Rails developer without knowing all its innards. I hope for both our sakes that it’s the latter.

Click here to view code image

>> controller = StaticPagesController.new
=> #<StaticPagesController:0x22855d0>
>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object

A diagram of this hierarchy appears in Figure 4.3.

Figure 4.3: The inheritance hierarchy for the Static Pages.

We can even call the controller actions inside the console, which are just methods:

Click here to view code image

>> controller.home
=> nil

Here the return value is nil because the home action is blank.

But wait—actions don’t have return values, at least not ones that matter. The point of the home action, as we saw in Chapter 3, is to render a web page, not to return a value. And I sure don’t remember ever calling StaticPagesController.new anywhere. What’s going on?

What’s going on is that Rails is written in Ruby, but Rails isn’t Ruby. Some Rails classes are used like ordinary Ruby objects, but some are just grist for Rails’ magic mill. Rails is sui generis, and should be studied and understood separately from Ruby.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By running the Rails console in the toy app’s directory from Chapter 2, confirm that you can create a user object using User.new.

2. Determine the class hierarchy of the user object.

4.4.5 A User Class

We end our tour of Ruby with a complete class of our own, a User class that anticipates the User model coming up in Chapter 6.

So far we’ve entered class definitions at the console, but this quickly becomes tiresome; instead, create the file example_user.rb in your application root directory and fill it with the contents of Listing 4.17.

Listing 4.17: Code for an example user.
example_user.rb

Click here to view code image

class User
  attr_accessor :name, :email

  def initialize(attributes = {})
    @name  = attributes[:name]
    @email = attributes[:email]
  end

  def formatted_email
    "#{@name} <#{@email}>"
  end
end

There’s quite a bit going on here, so let’s take it step by step. The first line,

attr_accessor :name, :email

creates attribute accessors corresponding to a user’s name and email address. This creates “getter” and “setter” methods that allow us to retrieve (get) and assign (set) @name and @email instance variables, which were mentioned briefly in Section 2.2.2 and Section 3.4.2. In Rails, the principal importance of instance variables is that they are automatically available in the views, but in general they are used for variables that need to be available throughout a Ruby class. (We’ll have more to say about this in a moment.) Instance variables always begin with an @ sign, and are nil when undefined.
The first method, initialize, is special in Ruby: it’s the method called when we execute User.new. This particular initialize takes one argument, attributes:

Click here to view code image

  def initialize(attributes = {})
    @name  = attributes[:name]
    @email = attributes[:email]
  end

Here the attributes variable has a default value equal to the empty hash, so that we can define a user with no name or email address. (Recall from Section 4.3.3 that hashes return nil for nonexistent keys, so attributes[:name] will be nil if there is no :name key, and similarly for attributes[:email].)

Finally, our class defines a method called formatted_email that uses the values of the assigned @name and @email variables to build up a nicely formatted version of the user’s email address using string interpolation (Section 4.2.2):

Click here to view code image

  def formatted_email
    "#{@name} <#{@email}>"
  end

Because @name and @email are both instance variables (as indicated with the @ sign), they are automatically available in the formatted_email method.

Let’s fire up the console, require the example user code, and take our User class out for a spin:

Click here to view code image

>> require './example_user'     # This is how you load the example_user code.
=> true
>> example = User.new
=> #<User:0x224ceec @email=nil, @name=nil>
>> example.name                 # nil since attributes[:name] is nil
=> nil
>> example.name = "Example User"           # Assign a non-nil name
=> "Example User"
>> example.email = "user@example.com"      # and a non-nil email address
=> "user@example.com"
>> example.formatted_email
=> "Example User <user@example.com>"

Here the '.' is Unix for “current directory”, and './example_user' tells Ruby to look for an example user file relative to that location. The subsequent code creates an empty example user and then fills in the name and email address by assigning directly to the corresponding attributes (assignments made possible by the attr_accessor line in Listing 4.17). When we write

example.name = "Example User"

Ruby is setting the @name variable to "Example User" (and similarly for the email attribute), which we then use in the formatted_email method.

Recalling from Section 4.3.4 we can omit the curly braces for final hash arguments, we can create another user by passing a hash to the initialize method to create a user with pre-defined attributes:

Click here to view code image

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User:0x225167c @email="mhartl@example.com", @name="Michael Hartl">
>> user.formatted_email
=> "Michael Hartl <mhartl@example.com>"

We will see starting in Chapter 7 that initializing objects using a hash argument, a technique known as mass assignment, is common in Rails applications.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In the example User class, change from name to separate full and last name attributes, and then add a method to the User class called full_name that returns the first and last names separated by a space. User it to replace the use of name in the formatted email method.

2. Add a method called alphabetical_name that returns the last name and first name separated by comma-space.

3. Verify that full_name.split is the same as alphabetical_name.split(', ').reverse.

4.5 Conclusion

This concludes our overview of the Ruby language. In Chapter 5, we’ll start putting it to good use in developing the sample application.

We won’t be using the example_user.rb file from Section 4.4.5, so I suggest removing it:

$ rm example_user.rb

Then commit the other changes to the main source code repository and merge into the master branch, push up to Bitbucket, and deploy to Heroku:

Click here to view code image

$ git commit -am "Add a full_title helper"
$ git checkout master
$ git merge rails-flavored-ruby

As a sanity check, it’s a good practice to run the test suite before pushing or deploying:

$ rails test

Then push up to Bitbucket:

$ git push

Finally, deploy to Heroku:

$ git push heroku

4.5.1 What We Learned in This Chapter

• Ruby has a large number of methods for manipulating strings of characters.

• Everything in Ruby is an object.

• Ruby supports method definition via the def keyword.

• Ruby supports class definition via the class keyword.

• Rails views can contain static HTML or embedded Ruby (ERb).

• Built-in Ruby data structures include arrays, ranges, and hashes.

• Ruby blocks are a flexible construct that (among other things) allow natural iteration over enumerable data structures.

• Symbols are labels, like strings without any additional structure.

• Ruby supports object inheritance.

• It is possible to open up and modify built-in Ruby classes.

• The word “deified” is a palindrome.

Chapter 5. Filling in the Layout

In the process of taking a brief tour of Ruby in Chapter 4, we learned about including the application stylesheet into the sample application (Section 4.1), but (as noted in Section 4.3.4) the stylesheet doesn’t yet contain any CSS. In this chapter, we’ll start filling in the custom stylesheet by incorporating a CSS framework into our application, and then we’ll add some custom styles of our own.¹ We’ll also start filling in the layout with links to the pages (such as Home and About) that we’ve created so far (Section 5.1). Along the way, we’ll learn about partials, Rails routes, and the asset pipeline, including an introduction to Sass (Section 5.2). We’ll end by taking a first important step toward letting users sign up to our site (Section 5.4).

1. Thanks to reader Colm Tuite for his excellent work in helping to convert the sample application over to the Bootstrap CSS framework.

Most of the changes in this chapter involve adding and editing markup in the sample application’s site layout, which (based on the guidelines in Box 3.3) is exactly the kind of work that we wouldn’t ordinarily test-drive, or even test at all. As a result, we’ll spend most of our time in our text editor and browser, using TDD only to add a Contact page (Section 5.3.1). We will add an important new test, though, writing our first integration test to check that the links on the final layout are correct (Section 5.3.4).

5.1 Adding Some Structure

The Ruby on Rails™ Tutorial is a book on web development, not web design, but it would be depressing to work on an application that looks like complete crap, so in this section we’ll add some structure to the layout and give it some minimal styling with CSS. In addition to using some custom CSS rules, we’ll make use of Bootstrap, an opensource web design framework from Twitter. We’ll also give our code some styling, so to speak, using partials to tidy up the layout once it gets a little cluttered.

When building web applications, it is often useful to get a high-level overview of the user interface as early as possible. Throughout the rest of this book, I will thus often include mockups (in a web context often called wireframes), which are rough sketches of what the eventual application will look like.² In this chapter, we will principally be developing the static pages introduced in Section 3.2, including a site logo, a navigation header, and a site footer. A mockup for the most important of these pages, the Home page, appears in Figure 5.1. You can see the final result in Figure 5.9. You’ll note that it differs in some details—for example, we’ll end up adding a Rails logo on the page—but that’s fine, since a mockup need not be exact.

2. The mockups in the Ruby on Rails™ Tutorial are made with an excellent online mockup application called Mockingbird.

Figure 5.1: A mockup of the sample application’s Home page.

As usual, if you’re using Git for version control, now would be a good time to make a new branch:

Click here to view code image

$ git checkout -b filling-in-layout

5.1.1 Site Navigation

As a first step toward adding links and styles to the sample application, we’ll update the site layout file application.html.erb (last seen in Listing 4.3) with additional HTML structure. This includes some additional divisions, some CSS classes, and the start of our site navigation. The full file is in Listing 5.1; explanations for the various pieces follow immediately thereafter. If you’d rather not delay gratification, you can see the results in Figure 5.2. (Note: It’s not very gratifying yet.)

Listing 5.1: The site layout with added structure.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                              'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <!--[if lt IE 9]>
      <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
      </script>
    <![endif]-->
  </head>
  <body>
    <header class="navbar navbar-fixed-top navbar-inverse">
      <div class="container">
        <%= link_to "sample app", '#', id: "logo" %>
        <nav>
          <ul class="nav navbar-nav navbar-right">
            <li><%= link_to "Home",   '#' %></li>
            <li><%= link_to "Help",   '#' %></li>
            <li><%= link_to "Log in", '#' %></li>
          </ul>
        </nav>
      </div>
    </header>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

Let’s look at the new elements in Listing 5.1 from top to bottom. As alluded to briefly in Section 3.4.1, Rails uses HTML5 by default (as indicated by the doctype <!DOCTYPE html>); since the HTML5 standard is relatively new, some browsers (especially older versions of Internet Explorer) don’t fully support it, so we include some JavaScript code (known as an “HTML5 shim (or shiv)”)³ to work around the issue:

3. The words shim and shiv are used interchangably in this context; the former is the proper term, based on the English word whose meaning is “a washer or thin strip of material used to align parts, make them fit, or reduce wear”, while the latter (meaning “a knife or razor used as a weapon”) is apparently a play on the name of the shim’s original author, Sjoerd Visscher.

Click here to view code image

<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->

The somewhat odd syntax

<!--[if lt IE 9]>

includes the enclosed line only if the version of Microsoft Internet Explorer (IE) is less than 9 (if lt IE 9). The weird [if lt IE 9] syntax is not part of Rails; it’s actually a conditional comment supported by Internet Explorer browsers for just this sort of situation. It’s a good thing, too, because it means we can include the HTML5 shim only for IE browsers less than version 9, leaving other browsers such as Firefox, Chrome, and Safari unaffected.

The next section includes a header for the site’s (plain-text) logo, a couple of divisions (using the div tag), and a list of elements with navigation links:

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", '#', id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home",   '#' %></li>
        <li><%= link_to "Help",   '#' %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>

Here the header tag indicates elements that should go at the top of the page. We’ve given the header tag three CSS classes,⁴ called navbar, navbar-fixed-top, and navbar-inverse, separated by spaces:

4. These are completely unrelated to Ruby classes.

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">

All HTML elements can be assigned both classes and ids; these are merely labels and are useful for styling with CSS (Section 5.1.2). The main difference between classes and ids is that classes can be used multiple times on a page, but ids can be used only once. In the present case, all the navbar classes have special meaning to the Bootstrap framework, which we’ll install and use in Section 5.1.2.

Inside the header tag, we see a div tag:

<div class="container">

The div tag is a generic division; it doesn’t do anything apart from divide the document into distinct parts. In older-style HTML, div tags are used for nearly all site divisions, but HTML5 adds the header, nav, and section elements for divisions common to many applications. In this case, the div has a CSS class as well (container). As with the header tag’s classes, this class has special meaning to Bootstrap.

After the div, we encounter some embedded Ruby:

Click here to view code image

<%= link_to "sample app", '#', id: "logo" %>
<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><%= link_to "Home",   '#' %></li>
    <li><%= link_to "Help",   '#' %></li>
    <li><%= link_to "Log in", '#' %></li>
  </ul>
</nav>

This uses the Rails helper link_to to create links (which we created directly with the anchor tag a in Section 3.2.2); the first argument to link_to is the link text, while the second is the URL. We’ll fill in the URLs with named routes in Section 5.3.3, but for now we use the stub URL '#' commonly used in web design (i.e., '#' is just a “stub”, or placeholder, for the real URL). The third argument is an options hash, in this case adding the CSS id logo to the sample app link. (The other three links have no options hash, which is fine since it’s optional.) Rails helpers often take options hashes in this way, giving us the flexibility to add arbitrary HTML options without ever leaving Rails.

The second element inside the divs is a list of navigation links, made using the unordered list tag ul, together with the list item tag li:

Click here to view code image

<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><%= link_to "Home",   '#' %></li>
    <li><%= link_to "Help",   '#' %></li>
    <li><%= link_to "Log in", '#' %></li>
  </ul>
</nav>

The <nav> tag, though formally unnecessary here, is used to more clearly communicate the purpose of the navigation links. Meanwhile, the nav, navbar-nav, and navbar-right classes on the ul tag have special meaning to Bootstrap and will be styled automatically when we include the Bootstrap CSS in Section 5.1.2. As you can verify by inspecting the navigation in your browser, once Rails has processed the layout and evaluated the embedded Ruby, the list looks like this:⁵

5. The spacing might look slightly different, which is fine because (as noted in Section 3.4.1) HTML is insensitive to whitespace.

Click here to view code image

<nav>
  <ul class="nav navbar-nav navbar-right">
    <li><a href="#">Home</a></li>
    <li><a href="#">Help</a></li>
    <li><a href="#">Log in</a></li>
  </ul>
</nav>

This is the text that will be returned to the browser.

The final part of the layout is a div for the main content:

Click here to view code image

<div class="container">
  <%= yield %>
</div>

As before, the container class has special meaning to Bootstrap. As we learned in Section 3.4.3, the yield method inserts the contents of each page into the site layout.

Apart from the site footer, which we’ll add in Section 5.1.3, our layout is now complete, and we can look at the results by visiting the Home page. To take advantage of the upcoming style elements, we’ll add some extra elements to the home.html.erb view (Listing 5.2).

Listing 5.2: The Home page with a link to the signup page.
app/views/static_pages/home.html.erb

Click here to view code image

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>

In preparation for adding users to our site in Chapter 7, the first link_to creates a stub link of the form

Click here to view code image

<a href="#" class="btn btn-lg btn-primary">Sign up now!</a>

In the div tag, the jumbotron CSS class has a special meaning to Bootstrap, as do the btn, btn-lg, and btn-primary classes in the signup button.

The second link_to shows off the image_tag helper, which takes as arguments the path to an image and an optional options hash, in this case setting the alt attribute of the image tag using symbols. For this to work, there needs to be an image called rails.png, which you should download from the Rails Tutorial website at http://railstutorial.org/rails.png and place in the app/assets/images/ directory.⁶ If you’re using the cloud IDE or another Unix-like system, you can accomplish this with the curl utility, as shown in Listing 5.3. (See Learn Enough Command Line to Be Dangerous for more information about curl.)

6. This image is actually the old Rails logo, but the new one is a scalable vector graphics (SVG) image and is less suitable for our present purposes.

Listing 5.3: Downloading an image.

Click here to view code image

$ curl -o app/assets/images/rails.png -OL railstutorial.org/rails.png

Because we used the image_tag helper in Listing 5.2, Rails will automatically find any images in the app/assets/images/ directory using the asset pipeline (Section 5.2).

Now we’re finally ready to see the fruits of our labors. You may have to restart the Rails server to see the changes (Box 1.1), and the results should appear as shown in Figure 5.2.

Figure 5.2: The Home page with no custom CSS.

To make the effects of image_tag clearer, let’s look at the HTML it produces by inspecting the image in our browser:⁷

7. You might notice that the img tag, rather than looking like <img >... </img >, instead looks like <img ... / >. Tags that follow this form are known as self-closing tags.

Click here to view code image

<img alt="Rails logo"
src="/assets/rails-9308b8f92fea4c19a3a0d8385b494526.png" />

Here the string 9308b8f92fea4c19a3a0d8385b494526 (which will differ on your system) is added by Rails to ensure that the filename is unique, which causes browsers to load images properly when they have been updated (instead of retrieving them from the browser cache). Note that the src attribute doesn’t include images, instead using an assets directory common to all assets (images, JavaScript, CSS, etc.). On the server, Rails associates images in the assets directory with the proper app/assets/images directory, but as far as the browser is concerned, all the assets look like they are in the same directory, which allows them to be served faster. Meanwhile, the alt attribute is what will be displayed if the page is accessed by a program that can’t display images (such as screen readers for the visually impaired).

As for the result shown in Figure 5.2, it might look a little underwhelming. Happily, though, we’ve done a good job of giving our HTML elements sensible classes, which puts us in a great position to add style to the site with CSS.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. It’s well-known that no web page is complete without a cat image. Using the command in Listing 5.4, arrange to download the kitten pic shown in Figure 5.3.⁸

8. Image retrieved from https://www.flickr.com/photos/deborah_s_perspective/14144861329 on 2016-01-09. Copyright © 2009 by Deborah and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

2. Using the mv command, move kitten.jpg to the correct asset directory for images (Section 5.2.1).

3. Using image_tag, add kitten.jpg to the Home page, as shown in Figure 5.4.

Listing 5.4: Downloading a cat picture from the Internet.

Click here to view code image

$ curl -OL cdn.learnenough.com/kitten.jpg

5.1.2 Bootstrap and Custom CSS

In Section 5.1.1, we associated many of the HTML elements with CSS classes, which gives us considerable flexibility in constructing a layout based on CSS. As noted in Section 5.1.1, many of these classes are specific to Bootstrap, a framework from Twitter that makes it easy to add nice web design and user interface elements to an HTML5 application. In this section, we’ll combine Bootstrap with some custom CSS rules to start adding some style to the sample application. It’s worth noting that using Bootstrap automatically makes our application’s design responsive, ensuring that it looks sensible across a wide range of devices.

Figure 5.3: An obligatory kitten pic.

Figure 5.4: The result of adding a kitten image to the Home page.

Our first step is to add Bootstrap, which in Rails applications can be accomplished with the bootstrap-sass gem, as shown in Listing 5.5.⁹ The Bootstrap framework natively uses the Less CSS language for making dynamic stylesheets, but the Rails asset pipeline supports the (very similar) Sass language by default (Section 5.2), so bootstrap-sass converts Less to Sass and makes all the necessary Bootstrap files available to the current application.¹⁰

9. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

10. It is also possible to use Less with the asset pipeline; see the less-rails-bootstrap gem for details.

Listing 5.5: Adding the bootstrap-sass gem to the Gemfile.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',          '5.0.0'
gem 'bootstrap-sass', '3.3.6'
.
.
.

To install Bootstrap, we run bundle install as usual:

$ bundle install

Although rails generate automatically creates a separate CSS file for each controller, it’s surprisingly hard to include them all properly and in the right order, so for simplicity we’ll put all of the CSS needed for this tutorial in a single file. The first step toward getting custom CSS to work is to create such a custom CSS file:

Click here to view code image

$ touch app/assets/stylesheets/custom.scss

(This uses the touch trick from Section 3.3.3 en route, but you can create the file however you like.) Here both the directory name and filename extension are important. The directory

app/assets/stylesheets/

is part of the asset pipeline (Section 5.2), and any stylesheets in this directory will automatically be included as part of the application.css file included in the site layout. Furthermore, the filename custom.scss includes the .scss extension, which indicates a “Sassy CSS” file and arranges for the asset pipeline to process the file using Sass. (We won’t be using Sass until Section 5.2.2, but it’s needed now for the bootstrap-sass gem to work its magic.)

Inside the file for the custom CSS, we can use the @import function to include Bootstrap (together with the associated Sprockets utility), as shown in Listing 5.6.¹¹

11. If these steps seem mysterious, take heart: I’m just following the instructions from the bootstrap-sass README file.

Listing 5.6: Adding Bootstrap CSS.
app/assets/stylesheets/custom.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";

The two lines in Listing 5.6 include the entire Bootstrap CSS framework. After restarting the webserver to incorporate the changes into the development application (by pressing Ctrl-C and then running rails server as in Section 1.3.2), the results appear as in Figure 5.5. The placement of the text isn’t good and the logo doesn’t have any style, but the colors and signup button look promising.

Next we’ll add some CSS that will be used site-wide for styling the layout and each individual page, as shown in Listing 5.7. The result is shown in Figure 5.6. (There are quite a few rules in Listing 5.7; to get a sense of what a CSS rule does, it’s often helpful to comment it out using CSS comments, i.e., by putting it inside /* ... */, and seeing what changes.)

Figure 5.5: The sample application with Bootstrap CSS.

Figure 5.6: Adding some spacing and other universal styling.

Listing 5.7: Adding CSS for some universal styling applying to all pages.
app/assets/stylesheets/custom.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";

/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center{
  text-align: center;
}

.center h1 {
  margin-bottom: 10px;
}

Note that the CSS in Listing 5.7 has a consistent form. In general, CSS rules refer either to a class, an id, an HTML tag, or some combination thereof, followed by a list of styling commands. For example,

body {
  padding-top: 60px;
}

puts 60 pixels of padding at the top of the page. Because of the navbarfixed-top class in the header tag, Bootstrap fixes the navigation bar to the top of the page, so the padding serves to separate the main text from the navigation. (Because the default navbar color changed after Bootstrap 2.0, we need the navbar-inverse class to make it dark instead of light.) Meanwhile, the CSS in the rule

.center{
  text-align: center;
}

associates the center class with the text-align: center property. In other words, the dot . in .center indicates that the rule styles a class. (As we’ll see in Listing 5.9, the pound sign # identifies a rule to style a CSS id.) This means that elements inside any tag (such as a div) with class center will be centered on the page. (We saw an example of this class in Listing 5.2.)

Although Bootstrap comes with CSS rules for nice typography, we’ll also add some custom rules for the appearance of the text on our site, as shown in Listing 5.8. (Not all of these rules apply to the Home page, but each rule here will be used at some point in the sample application.) The result of Listing 5.8 is shown in Figure 5.7.

Figure 5.7: Adding some typographic styling.

Listing 5.8: Adding CSS for nice typography.
app/assets/stylesheets/custom.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";
.
.
.
/* typography */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
}

h1 {
  font-size: 3em;
  letter-spacing: -2px;
  margin-bottom: 30px;
  text-align: center;
}

h2 {
  font-size: 1.2em;
  letter-spacing: -1px;
  margin-bottom: 30px;
  text-align: center;
  font-weight: normal;
  color: #777;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}

Finally, we’ll add some rules to style the site’s logo, which simply consists of the text “sample app”. The CSS in Listing 5.9 converts the text to uppercase and modifies its size, color, and placement. (We’ve used a CSS id because we expect the site logo to appear on the page only once, but you could use a class instead.)

Listing 5.9: Adding CSS for the site logo.
app/assets/stylesheets/custom.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";
.
.
.

/* header */

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
}

#logo:hover {
  color: #fff;
  text-decoration: none;
}

Here color: #fff changes the color of the logo to white. HTML colors can be coded with three pairs of base-16 (hexadecimal) numbers, one each for the primary colors red, green, and blue (in that order). The code #ffffff maxes out all three colors, yielding pure white, and #fff is a shorthand for the full #ffffff. The CSS standard also defines a large number of synonyms for common HTML colors, including white for #fff. The result of the CSS in Listing 5.9 is shown in Figure 5.8.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using code like that shown in Listing 5.10, comment out the cat image from Section 5.1.1. Verify using a web inspector that the HTML for the image no longer appears in the page source.

2. By adding the CSS in Listing 5.11 to custom.scss, hide all images in the application—currently just the Rails logo on the Home page). Verify with a web inspector that although the image doesn’t appear, the HTML source is still present.

Figure 5.8: The sample app with nicely styled logo.

Listing 5.10: Code to comment out embedded Ruby.

Click here to view code image

<%#= image_tag("kitten.jpg", alt: "Kitten") %>

Listing 5.11: CSS to hide all images.

img {
  display: none;
}

5.1.3 Partials

Although the layout in Listing 5.1 serves its purpose, it’s getting a little cluttered. The HTML shim takes up three lines and uses weird IE-specific syntax, so it would be nice to tuck it away somewhere on its own. In addition, the header HTML forms a logical unit, so it should all be packaged up in one place. The way to achieve this in Rails is to use a facility called partials. Let’s first take a look at what the layout looks like after the partials are defined (Listing 5.12).

Listing 5.12: The site layout with partials for the stylesheets and header.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                          'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                                          'data-turbolinks-track': 'reload' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

In Listing 5.12, we’ve replaced the HTML shim stylesheet lines with a single call to a Rails helper called render:

<%= render 'layouts/shim' %>

The effect of this line is to look for a file called app/views/layouts/_shim.html.erb, evaluate its contents, and insert the results into the view.¹² (Recall that <%= ... %> is the embedded Ruby syntax needed to evaluate a Ruby expression, and then insert the results into the template.) Note the leading underscore on the filename _shim.html.erb; this underscore is the universal convention for naming partials, and among other things makes it possible to identify all the partials in a directory at a glance.

12. Many Rails developers use a shared directory for partials shared across different views. I prefer to use the shared folder for utility partials that are useful on multiple views, while putting partials that are literally on every page (as part of the site layout) in the layouts directory. (We’ll create the shared directory starting in Chapter 7.) That seems to me a logical division, but putting them all in the shared folder certainly works fine, too.

Of course, to get the partial to work, we have to create the corresponding file and fill it with some content. In the case of the shim partial, this is just the three lines of shim code from Listing 5.1. The result appears in Listing 5.13.

Listing 5.13: A partial for the HTML shim.
app/views/layouts/_shim.html.erb

Click here to view code image

<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->

Similarly, we can move the header material into the partial shown in Listing 5.14 and insert it into the layout with another call to render. (As usual with partials, you will have to create the file by hand using your text editor.)

Listing 5.14: A partial for the site header.
app/views/layouts/_header.html.erb

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", '#', id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home",   '#' %></li>
        <li><%= link_to "Help",   '#' %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>

Now that we know how to make partials, let’s add a site footer to go along with the header. By now you can probably guess that we’ll call it _footer.html.erb and put it in the layouts directory (Listing 5.15).¹³

13. You may wonder why we use both the footer tag and .footer class. The answer is that the tag has a clear meaning to human readers and the class is used by Bootstrap. Using a div tag in place of footer would work as well.

Listing 5.15: A partial for the site footer.
app/views/layouts/_footer.html.erb

Click here to view code image

<footer class="footer">
  <small>
    The <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
    by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About", '#' %></li>
      <li><%= link_to "Contact", '#' %></li>
      <li><a href="http://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>

As with the header, in the footer we’ve used link_to for the internal links to the About and Contact pages and stubbed out the URLs with '#' for now. (As with header, the footer tag is new in HTML5.)

We can render the footer partial in the layout by following the same pattern as the stylesheets and header partials (Listing 5.16).

Listing 5.16: The site layout with a footer partial.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                          'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                                          'data-turbolinks-track': 'reload' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>

Of course, the footer will be ugly without some styling (Listing 5.17). The results appear in Figure 5.9.

Figure 5.9: The Home page with an added footer.

Listing 5.17: Adding the CSS for the site footer.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
}

footer a {
  color: #555;
}

footer a:hover {
  color: #222;
}

footer small {
  float: left;
}

footer ul {
  float: right;
  list-style: none;
}

footer ul li {
  float: left;
  margin-left: 15px;
}

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Replace the default Rails head with the call to render shown in Listing 5.18. Hint: For convenience, cut the default header rather than just deleting it.

2. Because we haven’t yet created the partial needed by Listing 5.18, the tests should be RED. Confirm that this is the case.

3. Create the necessary partial in the layouts directory, paste in the contents, and verify that the tests are now GREEN again.

Listing 5.18: Replacing the default Rails head with a call to render.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= render 'layouts/rails_default' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>

5.2 Sass and the Asset Pipeline

One of the most useful features of Rails is the asset pipeline, which significantly simplifies the production and management of static assets such as CSS, JavaScript, and images. This section first gives a high-level overview of the asset pipeline and then shows how to use Sass, a powerful tool for writing CSS.

5.2.1 The Asset Pipeline

From the perspective of a typical Rails developer, there are three main features to understand about the asset pipeline: asset directories, manifest files, and preprocessor engines.¹⁴ Let’s consider each in turn.

14. The structure of this section is based on the excellent blog post “The Rails 3 Asset Pipeline in (about) 5 Minutes” by Michael Erasmus. For more details, see the Rails Guide on the Asset Pipeline.

Asset Directories

The Rails asset pipeline uses three standard directories for static assets, each with its own purpose:

• app/assets: assets specific to the present application

• lib/assets: assets for libraries written by your dev team

• vendor/assets: assets from third-party vendors

Each of these directories has a subdirectory for each asset class: images, JavaScript, and cascading style sheets:

Click here to view code image

$ ls app/assets/
images/  javascripts/  stylesheets/

At this point, we’re in a position to understand the motivation behind the location of the custom CSS file in Section 5.1.2: custom.scss is specific to the sample application, so it goes in app/assets/stylesheets.

Manifest Files

Once you’ve placed your assets in their logical locations, you can use manifest files to tell Rails (via the Sprockets gem) how to combine them to form single files. (This applies to CSS and JavaScript but not to images.) As an example, let’s take a look at the default manifest file for app stylesheets (Listing 5.19).

Listing 5.19: The manifest file for app-specific CSS.
app/assets/stylesheets/application.css

Click here to view code image

/*
* This is a manifest file that'll be compiled into application.css, which
* will include all the files listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets,
* vendor/assets/stylesheets, or vendor/assets/stylesheets of plugins, if any,
* can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear
* at the bottom of the compiled file so the styles you add here take
* precedence over styles defined in any styles defined in the other CSS/SCSS
* files in this directory. It is generally better to create a new file per
* style scope.
*
*= require_tree .
*= require_self
*/

The key lines here are actually CSS comments, but they are used by Sprockets to include the proper files:

Click here to view code image

/*
.
.
.
*= require_tree .
*= require_self
*/

Here

*= require_tree .

ensures that all CSS files in the app/assets/stylesheets directory (including the tree subdirectories) are included into the application CSS. The line

*= require_self

specifies where in the loading sequence the CSS in application.css itself gets included.

Rails comes with sensible default manifest files, and in the Rails Tutorial we won’t need to make any changes, but the Rails Guides entry on the asset pipeline has more detail if you need it.

Preprocessor Engines

After you’ve assembled your assets, Rails prepares them for the site template by running them through several preprocessing engines and using the manifest files to combine them for delivery to the browser. We tell Rails which processor to use using filename extensions; the three most common cases are .scss for Sass, .coffee for CoffeeScript, and .erb for embedded Ruby (ERb). We first covered ERb in Section 3.4.3, and cover Sass in Section 5.2.2. We won’t be needing CoffeeScript in this tutorial, but it’s an elegant language that gets converted to JavaScript for execution in the browser.

The preprocessor engines can be chained, so that foobar.js.coffee gets run through the CoffeeScript processor, and foobar.js.erb.coffee gets run through both CoffeeScript and ERb (with the code running from right to left, i.e., CoffeeScript first).

Efficiency in Production

One of the best things about the asset pipeline is that it automatically results in assets that are optimized to be efficient in a production application. Traditional methods for organizing CSS and JavaScript involve splitting functionality into separate files and using nice formatting (with lots of indentation). While convenient for the programmer, this is inefficient in production. In particular, including multiple full-sized files can significantly slow page-load times, which is one of the most important factors affecting the quality of the user experience. With the asset pipeline, we don’t have to choose between speed and convenience: we can work with multiple nicely formatted files in development, and then use the asset pipeline to make efficient files in production. In particular, the asset pipeline combines all the application stylesheets into one CSS file (application.css), combines all the application JavaScript into one JavaScript file (application.js), and then minifies them to remove the unnecessary spacing and indentation that bloats file size. The result is the best of both worlds: convenience in development and efficiency in production.

5.2.2 Syntactically Awesome Stylesheets

Sass is a language for writing stylesheets that improves on CSS in many ways. In this section, we cover two of the most important improvements, nesting and variables. (A third technique, mixins, is introduced in Section 7.1.1.)

As noted briefly in Section 5.1.2, Sass supports a format called SCSS (indicated with a .scss filename extension), which is a strict superset of CSS itself; that is, SCSS only adds features to CSS, rather than defining an entirely new syntax.¹⁵ This means that every valid CSS file is also a valid SCSS file, which is convenient for projects with existing style rules. In our case, we used SCSS from the start in order to take advantage of Bootstrap. Since the Rails asset pipeline automatically uses Sass to process files with the .scss extension, the custom.scss file will be run through the Sass preprocessor before being packaged up for delivery to the browser.

15. Sass also supports an alternate syntax that does define a new language, which is less verbose (and has fewer curly braces) but is less convenient for existing projects and is harder to learn for those already familiar with CSS.

Nesting

A common pattern in stylesheets is having rules that apply to nested elements. For example, in Listing 5.7 we have rules both for .center and for .center h1:

Click here to view code image

.center{
  text-align: center;
}

.center h1 {
  margin-bottom: 10px;
}

We can replace this in Sass with

Click here to view code image

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

Here the nested h1 rule automatically inherits the .center context.

There’s a second candidate for nesting that requires a slightly different syntax. In Listing 5.9, we have the code

Click here to view code image

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
}

#logo:hover {
  color: #fff;
  text-decoration: none;
}

Here the logo id #logo appears twice, once by itself and once with the hover attribute (which controls its appearance when the mouse pointer hovers over the element in question). In order to nest the second rule, we need to reference the parent element #logo; in SCSS, this is accomplished with the ampersand character & as follows:

Click here to view code image

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: #fff;
    text-decoration: none;
  }
}

Sass changes &:hover into #logo:hover as part of converting from SCSS to CSS.

Both of these nesting techniques apply to the footer CSS in Listing 5.17, which can be transformed into the following:

Click here to view code image

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
  a {
    color: #555;
    &:hover {
      color: #222;
    }
  }
  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}

Converting Listing 5.17 by hand is a good exercise (Section 5.2.2), and you should verify that the CSS still works properly after the conversion.

Variables

Sass allows us to define variables to eliminate duplication and write more expressive code. For example, looking at Listing 5.8 and Listing 5.17, we see that there are repeated references to the same color:

Click here to view code image

h2 {
  .
  .
  .
  color: #777;
}
.
.
.
footer {
  .
  .
  .
  color: #777;
}

In this case, #777 is a light gray, and we can give it a name by defining a variable as follows:

$light-gray: #777;

This allows us to rewrite our SCSS like this:

Click here to view code image

$light-gray: #777;
.
.
.
h2 {
  .
  .
  .
  color: $light-gray;
}
.
.
.
footer {
  .
  .
  .
  color: $light-gray;
}

Because variable names such as $light-gray are more descriptive than #777, it’s often useful to define variables even for values that aren’t repeated. Indeed, the Bootstrap framework defines a large number of variables for colors, available online on the Bootstrap page of Less variables. That page defines variables using Less, not Sass, but the bootstrap-sass gem provides the Sass equivalents. It is not difficult to guess the correspondence; where Less uses an “at” sign @, Sass uses a dollar sign $. Looking at the Bootstrap variable page, we see that there is a variable for light gray:

@gray-light: #777;

This means that, via the bootstrap-sass gem, there should be a corresponding SCSS variable $gray-light. We can use this to replace our custom variable, $light-gray, which gives

Click here to view code image

h2 {
  .
  .
  .
  color: $gray-light;
}
.
.
.
footer {
  .
  .
  .
  color: $gray-light;
}

Applying the Sass nesting and variable definition features to the full SCSS file gives the file in Listing 5.20. This uses both Sass variables (as inferred from the Bootstrap Less variable page) and built-in named colors (i.e., white for #fff). Note in particular the dramatic improvement in the rules for the footer tag.

Listing 5.20: The initial SCSS file converted to use nesting and variables.
app/assets/stylesheets/custom.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

/* typography */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
}

h1 {
  font-size: 3em;
  letter-spacing: -2px;
  margin-bottom: 30px;
  text-align: center;
}

h2 {
  font-size: 1.2em;
  letter-spacing: -1px;
  margin-bottom: 30px;
  text-align: center;
  font-weight: normal;
  color: $gray-light;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}

/* header */

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: white;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: white;
    text-decoration: none;
  }
}

/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid $gray-medium-light;
  color: $gray-light;
  a {
    color: $gray;
    &:hover {
      color: $gray-darker;
    }
  }

  small {
    float: left;
  }
  ul {
    float: right;
    list-style: none;
    li {
      float: left;
      margin-left: 15px;
    }
  }
}

Sass gives us even more ways to simplify our stylesheets, but the code in Listing 5.20 uses the most important features and gives us a great start. See the Sass website for more details.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As suggested in Section 5.2.2, go through the steps to convert the footer CSS from Listing 5.17 to Listing 5.20 to SCSS by hand.

5.3 Layout Links

Now that we’ve finished a site layout with decent styling, it’s time to start filling in the links we’ve stubbed out with '#'. Of course, we could hard-code links like

Click here to view code image

<a href="/static_pages/about">About</a>

but that isn’t the Rails Way™. For one, it would be nice if the URL for the about page were /about rather than /static_pages/about. Moreover, Rails conventionally uses named routes, which involves code like

Click here to view code image

<%= link_to "About", about_path %>

Table 5.1: Route and URL mapping for site links.

This way the code has a more transparent meaning, and it’s also more flexible since we can change the definition of about_path and have the URL change everywhere about_path is used.

The full list of our planned links appears in Table 5.1, along with their mapping to URLs and routes. We took care of the first route in Section 3.4.4, and we’ll have implemented all but the last one by the end of this chapter. (We’ll make the last one in Chapter 8.)

5.3.1 Contact Page

For completeness, we’ll add the Contact page, which was left as an exercise in Chapter 3. The test appears as in Listing 5.21, which simply follows the model last seen in Listing 3.24.

Listing 5.21: A test for the Contact page. RED
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end

  test "should get contact" do
    get static_pages_contact_url
    assert_response :success
    assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
  end
end

At this point, the tests in Listing 5.21 should be RED:

Listing 5.22: RED

$ rails test

The application code parallels the addition of the About page in Section 3.3: first we update the routes (Listing 5.23), then we add a contact action to the Static Pages controller (Listing 5.24), and finally we create a Contact view (Listing 5.25).

Listing 5.23: Adding a route for the Contact page. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
  get  'static_pages/contact'
end

Listing 5.24: Adding an action for the Contact page. RED
app/controllers/static_pages_controller.rb

Click here to view code image

class StaticPagesController < ApplicationController
  .
  .
  .
  def contact
  end
end

Listing 5.25: The view for the Contact page. GREEN
app/views/static_pages/contact.html.erb

Click here to view code image

<% provide(:title, 'Contact') %>
<h1>Contact</h1>
<p>
  Contact the Ruby on Rails Tutorial about the sample app at the
  <a href="http://www.railstutorial.org/contact">contact page</a>.
</p>

Now make sure that the tests are GREEN:

Listing 5.26: GREEN

$ rails test

5.3.2 Rails Routes

To add the named routes for the sample app’s static pages, we’ll edit the routes file, config/routes.rb, that Rails uses to define URL mappings. We’ll begin by reviewing the route for the Home page (defined in Section 3.4.4), which is a special case, and then define a set of routes for the remaining static pages.

So far, we’ve seen three examples of how to define a root route, starting with the code

root 'application#hello'

in the hello app (Listing 1.10), the code

root 'users#index'

in the toy app (Listing 2.6), and the code

root 'static_pages#home'

in the sample app (Listing 3.41). In each case, the root method arranges for the root path / to be routed to a controller and action of our choice. Defining the root route in this way has a second important effect, which is to create named routes that allow us to refer to routes by a name rather than by the raw URL. In this case, these routes are root_path and root_url, with the only difference being that the latter includes the full URL:

Click here to view code image

root_path -> '/'
root_url  -> 'http://www.example.com/'

In the Rails Tutorial, we’ll follow the common convention of using the _path form except when doing redirects, where we’ll use the _url form. (This is because the HTTP standard technically requires a full URL after redirects, though in most browsers it will work either way.)

Because the default routes used in, for example, Listing 5.21 are rather verbose, we’ll also take this opportunity to define shorter named routes for the Help, About, and Contact pages. To do this, we need to make changes to the get rules from Listing 5.23, transforming lines like

get 'static_pages/help'

to

Click here to view code image

get '/help', to: 'static_pages#help'

This new pattern routes a GET request for the URL /help to the help action in the Static Pages controller. As with the rule for the root route, this creates two named routes, help_path and help_url:

Click here to view code image

help_path -> '/help'
help_url  -> 'http://www.example.com/help'

Applying this rule change to the remaining static page routes from Listing 5.23 gives Listing 5.27.

Listing 5.27: Routes for static pages. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
end

Note that Listing 5.27 also removes the route for 'static_pages/home', as we’ll always use root_path or root_url instead.

Because the tests in Listing 5.21 used the old routes, they are now RED. To get them GREEN again, we need to update the routes as shown in Listing 5.28. Note that we’ve taken this opportunity to update to the (optional) convention of using the *_path form of each named route.

Listing 5.28: The static pages tests with the new named routes. GREEN
test/controllers/static_pages_controller_test.rb

Click here to view code image

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get root_path
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get help_path
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get about_path
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end

  test "should get contact" do
    get contact_path
    assert_response :success
    assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
  end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. It’s possible to use a named route other than the default using the as: option. Drawing inspiration from this famous Far Side comic strip, change the route for the Help page to use helf.

2. Confirm that the tests are now RED. Get them to GREEN by updating the route in Listing 5.28.

3. Revert the changes from these exercises using Undo.

Listing 5.29: Changing ‘help’ to ‘helf’.

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help', as: 'helf'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
end

5.3.3 Using Named Routes

With the routes defined in Listing 5.27, we’re now in a position to use the resulting named routes in the site layout. This simply involves filling in the second arguments of the link_to functions with the proper named routes. For example, we’ll convert

<%= link_to "About", '#' %>

to

Click here to view code image

<%= link_to "About", about_path %>

and so on.

We’ll start in the header partial, _header.html.erb (Listing 5.30), which has links to the Home and Help pages. While we’re at it, we’ll follow a common web convention and link the logo to the Home page as well.

Listing 5.30: Header partial with links.
app/views/layouts/_header.html.erb

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <li><%= link_to "Log in", '#' %></li>
      </ul>
    </nav>
  </div>
</header>

We won’t have a named route for the “Log in” link until Chapter 8, so we’ve left it as '#' for now.

The other place with links is the footer partial, _footer.html.erb, which has links for the About and Contact pages (Listing 5.31).

Listing 5.31: Footer partial with links.
app/views/layouts/_footer.html.erb

Click here to view code image

<footer class="footer">
  <small>
    The <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
    by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
  </small>
  <nav>
    <ul>
      <li><%= link_to "About",   about_path %></li>
      <li><%= link_to "Contact", contact_path %></li>
      <li><a href="http://news.railstutorial.org/">News</a></li>
    </ul>
  </nav>
</footer>

With that, our layout has links to all the static pages created in Chapter 3, so that, for example, /about goes to the About page (Figure 5.10).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Update the layout links to use the helf route from Listing 5.29.

2. Revert the changes using Undo.

Figure 5.10: The About page at /about.

5.3.4 Layout Link Tests

Now that we’ve filled in several of the layout links, it’s a good idea to test them to make sure they’re working correctly. We could do this by hand with a browser, first visiting the root path and then checking the links by hand, but this quickly becomes cumbersome. Instead, we’ll simulate the same series of steps using an integration test, which allows us to write an end-to-end test of our application’s behavior. We can get started by generating a template test, which we’ll call site_layout:

Click here to view code image

$ rails generate integration_test site_layout
      invoke  test_unit
      create    test/integration/site_layout_test.rb

Note that the Rails generator automatically appends _test to the name of the test file.

Our plan for testing the layout links involves checking the HTML structure of our site:

1. Get the root path (Home page).

2. Verify that the right page template is rendered.

3. Check for the correct links to the Home, Help, About, and Contact pages.

Listing 5.32 shows how we can use Rails integration tests to translate these steps into code, beginning with the assert_template method to verify that the Home page is rendered using the correct view.¹⁶

16. Some developers insist that a single test shouldn’t contain multiple assertions. I find this practice to be unnecessarily complicated, while also incurring an extra overhead if there are common setup tasks needed before each test. In addition, a well-written test tells a coherent story, and breaking it up into individual pieces disrupts the narrative. I thus have a strong preference for including multiple assertions in a test, relying on Ruby (via minitest) to tell me the exact lines of any failed assertions.

Listing 5.32: A test for the links on the layout. GREEN
test/integration/site_layout_test.rb

Click here to view code image

require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end

Listing 5.32 uses some of the more advanced options of the assert_select method, seen before in Listing 3.24 and Listing 5.21. In this case, we use a syntax that allows us to test for the presence of a particular link–URL combination by specifying the tag name a and attribute href, as in

Click here to view code image

assert_select "a[href=?]", about_path

Here Rails automatically inserts the value of about_path in place of the question mark (escaping any special characters if necessary), thereby checking for an HTML tag of the form

<a href="/about">...</a>

Note that the assertion for the root path verifies that there are two such links (one each for the logo and navigation menu element):

Click here to view code image

assert_select "a[href=?]", root_path, count: 2

This ensures that both links to the Home page defined in Listing 5.30 are present.

Some more uses of assert_select appear in Table 5.2. While assert_select is flexible and powerful (having many more options than the ones shown here), experience shows that it’s wise to take a lightweight approach by testing only HTML elements (such as site layout links) that are unlikely to change much over time.

To check that the new test in Listing 5.32 passes, we can run just the integration tests using the following Rake task:

Listing 5.33: GREEN

$ rails test:integration

Table 5.2: Some uses of assert_select.

If all went well, you should run the full test suite to verify that all the tests are GREEN:

Listing 5.34: GREEN

$ rails test

With the added integration test for layout links, we are now in a good position to catch regressions quickly using our test suite.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In the footer partial, change about_path to contact_path and verify that the tests catch the error.

2. It’s convenient to use the full_title helper in the tests by including the Application helper into the test helper, as shown in Listing 5.35. We can then test for the right title using code like Listing 5.36. This is brittle, though, because now any typo in the base title (such as “Ruby on Rails Tutoial”) won’t be caught by the test suite. Fix this problem by writing a direct test of the full_title helper, which involves creating a file to test the application helper and then filling in the code indicated with FILL_IN in Listing 5.37. (Listing 5.37 uses assert_equal <expected>, <actual>, which verifies that the expected result matches the actual value when compared with the == operator.)

Listing 5.35: Including the Application helper in tests.
test/test_helper.rb

Click here to view code image

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all
  include ApplicationHelper
  .
  .
  .
end

Listing 5.36: Using the full_title helper in a test. GREEN
test/integration/site_layout_test.rb

Click here to view code image

require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    get contact_path
    assert_select "title", full_title("Contact")
  end
end

Listing 5.37: A direct test of the full_title helper.
test/helpers/application_helper_test.rb

Click here to view code image

require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal full_title,         FILL_IN
    assert_equal full_title("Help"), FILL_IN
  end
end

5.4 User Signup: A First Step

As a capstone to our work on the layout and routing, in this section we’ll make a route for the signup page, which will mean creating a second controller along the way. This is a first important step toward allowing users to register for our site; we’ll take the next step, modeling users, in Chapter 6, and we’ll finish the job in Chapter 7.

5.4.1 Users Controller

We created our first controller, the Static Pages controller, in Section 3.2. It’s time to create a second one, the Users controller. As before, we’ll use generate to make the simplest controller that meets our present needs, namely, one with a stub signup page for new users. Following the conventional REST architecture favored by Rails, we’ll call the action for new users new, which we can arrange to create automatically by passing new as an argument to generate. The result is shown in Listing 5.38.

Listing 5.38: Generating a Users controller (with a new action).

Click here to view code image

$ rails generate controller Users new
      create  app/controllers/users_controller.rb
       route  get 'users/new'
      Invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.js.coffee
      invoke    scss
      create      app/assets/stylesheets/users.css.scss

As required, Listing 5.38 creates a Users controller with a new action (Listing 5.39) and a stub user view (Listing 5.40). It also creates a minimal test for the new user page (Listing 5.41).

Listing 5.39: The initial Users controller, with a new action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def new
  end
end

Listing 5.40: The initial new view for Users.
app/views/users/new.html.erb

Click here to view code image

<h1>Users#new</h1>
<p>Find me in app/views/users/new.html.erb</p>

Listing 5.41: The generated test for the new user page. GREEN
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get users_new_url
    assert_response :success
  end
end

At this point, the tests should be GREEN:

Listing 5.42: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Per Table 5.1, change the route in Listing 5.41 to use signup_path instead of users_new_url.

2. The route in the previous exercise doesn’t yet exist, so confirm that the tests are now RED. (This is intended help us get comfortable with the RED/GREEN flow of Test-Driven Development [TDD, Box 3.3]; we’ll get the tests back to GREEN in Section 5.4.2.)

5.4.2 Signup URL

With the code from Section 5.4.1, we already have a working page for new users at /users/new, but recall from Table 5.1 that we want the URL to be /signup instead. We’ll follow the examples from Listing 5.27 and add a get '/signup' rule for the signup URL, as shown in Listing 5.43.

Listing 5.43: A route for the signup page. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
end

With the routes in Listing 5.43, we also need to update the test generated in Listing 5.38 with the new signup route, as shown in Listing 5.44.

Listing 5.44: Updating the Users controller test to use the signup route. GREEN
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
  get signup_path
    assert_response :success
  end
end

Next, we’ll use the newly defined named route to add the proper link to the button on the Home page. As with the other routes, get 'signup' automatically gives us the named route signup_path, which we put to use in Listing 5.45. Adding a test for the signup page is left as an exercise (Section 5.3.2.)

Listing 5.45: Linking the button to the signup page.
app/views/static_pages/home.html.erb

Click here to view code image

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>

Finally, we’ll add a custom stub view for the signup page (Listing 5.46).

Listing 5.46: The initial (stub) signup page.
app/views/users/new.html.erb

Click here to view code image

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<p>This will be a signup page for new users.</p>

With that, we’re done with the links and named routes, at least until we add a route for logging in (Chapter 8). The resulting new user page (at the URL /signup) appears in Figure 5.11.

Figure 5.11: The new signup page at /signup.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. If you didn’t solve the exercise in Section 5.4.1, change the test in Listing 5.41 to use the named route signup_path. Because of the route defined in Listing 5.43, this test should initially be GREEN.

2. In order to verify the correctness of the test in the previous exercise, comment out the signup route to get to RED, then uncomment to get to GREEN.

3. In the integration test from Listing 5.32, add code to visit the signup page using the get method and verify that the resulting page title is correct. Hint: Use the full_title helper as in Listing 5.36.

5.5 Conclusion

In this chapter, we’ve hammered our application layout into shape and polished up the routes. The rest of the book is dedicated to fleshing out the sample application: first, by adding users who can sign up, log in, and log out; next, by adding user microposts; and, finally, by adding the ability to follow other users.

At this point, if you are using Git, you should merge your changes back into the master branch:

Click here to view code image

$ git add -A
$ git commit -m "Finish layout and routes"
$ git checkout master
$ git merge filling-in-layout

Figure 5.12: The sample application in production.

Then push up to Bitbucket (running the test suite first for safety):

$ rails test
$ git push

Finally, deploy to Heroku:

$ git push heroku

The result of the deployment should be a working sample application on the production server (Figure 5.12).

5.5.1 What We Learned in This Chapter

• Using HTML5, we can define a site layout with logo, header, footer, and main body content.

• Rails partials are used to place markup in a separate file for convenience.

• CSS allows us to style the site layout based on CSS classes and ids.

• The Bootstrap framework makes it easy to make a nicely designed site quickly.

• Sass and the asset pipeline allow us to eliminate duplication in our CSS while packaging up the results efficiently for production.

• Rails allows us to define custom routing rules, thereby providing named routes.

• Integration tests effectively simulate a browser clicking from page to page.

Chapter 6. Modeling Users

In Chapter 5, we ended with a stub page for creating new users (Section 5.4). Over the course of the next six chapters, we’ll fulfill the promise implicit in this incipient signup page. In this chapter, we’ll take the first critical step by creating a data model for users of our site, together with a way to store that data. In Chapter 7, we’ll give users the ability to sign up for our site and create a user profile page. Once users can sign up, we’ll let them log in and log out as well (Chapter 8 and Chapter 9), and in Chapter 10 (Section 10.2.1) we’ll learn how to protect pages from improper access. Finally, in Chapter 11 and Chapter 12 we’ll add account activations (thereby confirming a valid email address) and password resets. Taken together, the material in Chapter 6 through Chapter 12 develops a full Rails login and authentication system. As you may know, there are various pre-built authentication solutions for Rails; Box 6.1 explains why, at least initially, it’s probably a better idea to roll your own.

Box 6.1: Rolling Your Own Authentication System

Virtually all web applications require a login and authentication system of some sort. As a result, most web frameworks have a plethora of options for implementing such systems, and Rails is no exception. Examples of authentication and authorization systems include Clearance, Authlogic, Devise, and CanCan (as well as non- Rails-specific solutions built on top of OpenID or OAuth). It’s reasonable to ask why we should reinvent the wheel. Why not just use an off-the-shelf solution instead of rolling our own?

For one, practical experience shows that authentication on most sites requires extensive customization, and modifying a third-party product is often more work than writing the system from scratch. In addition, off-the-shelf systems can be “black boxes”, with potentially mysterious innards; when you write your own system, you are far more likely to understand it. Moreover, recent additions to Rails (Section 6.3) make it easy to write a custom authentication system. Finally, if you do end up using a third-party system later on, you’ll be in a much better position to understand and modify it if you’ve first built one yourself.

6.1 User Model

Although the ultimate goal of the next three chapters is to make a signup page for our site (as mocked up in Figure 6.1), it would do little good now to accept information for new users: we don’t currently have any place to put it. Thus, the first step in signing up users is to make a data structure to capture and store their information.

In Rails, the default data structure for a data model is called, naturally enough, a model (the M in MVC from Section 1.3.3). The default Rails solution to the problem of persistence is to use a database for long-term data storage, and the default library for interacting with the database is called Active Record.¹ Active Record comes with a host of methods for creating, saving, and finding data objects, all without having to use the structured query language (SQL)² used by relational databases. Moreover, Rails has a feature called migrations to allow data definitions to be written in pure Ruby, without having to learn an SQL data definition language (DDL). The effect is that Rails insulates you almost entirely from the details of the database. In this book, by using SQLite for development and PostgreSQL (via Heroku) for deployment (Section 1.5), we have developed this theme even further, to the point where we barely ever have to think about how Rails stores data, even for production applications.

1. The name comes from the “active record pattern”, identified and named in Patterns of Enterprise Application Architecture by Martin Fowler.

2. Officially pronounced “ess-cue-ell”, though the alternate pronunciation “sequel” is also common. You can differentiate an individual author’s preference by the choice of indefinite article: those who write “a SQL database” prefer “sequel”, whereas those who write “an SQL database” prefer “ess-cue-ell”. As you’ll soon see, I prefer the latter.

Figure 6.1: A mockup of the user signup page.

As usual, if you’re following along using Git for version control, now would be a good time to make a topic branch for modeling users:

Click here to view code image

$ git checkout -b modeling-users

6.1.1 Database Migrations

You may recall from Section 4.4.5 that we have already encountered, via a custom-built User class, user objects with name and email attributes. That class served as a useful example, but it lacked the critical property of persistence: when we created a User object at the Rails console, it disappeared as soon as we exited. Our goal in this section is to create a model for users that won’t disappear quite so easily.

As with the User class in Section 4.4.5, we’ll start by modeling a user with two attributes, a name and an email address, the latter of which we’ll use as a unique username.³ (We’ll add an attribute for passwords in Section 6.3.) In Listing 4.17, we did this with Ruby’s attr_accessor method:

3. By using an email address as the username, we open the possibility of communicating with our users at a future date (Chapter 11 and Chapter 12).

Click here to view code image

class User
  attr_accessor :name, :email
  .
  .
  .
end

In contrast, when using Rails to model users we don’t need to identify the attributes explicitly. As noted briefly above, to store data Rails uses a relational database by default, which consists of tables composed of data rows, where each row has columns of data attributes. For example, to store users with names and email addresses, we’ll create a users table with name and email columns (with each row corresponding to one user). An example of such a table appears in Figure 6.2, corresponding to the data model shown in Figure 6.3. (Figure 6.3 is just a sketch; the full data model appears in Figure 6.4.) By naming the columns name and email, we’ll let Active Record figure out the User object attributes for us.

You may recall from Listing 5.38 that we created a Users controller (along with a new action) using the command

Click here to view code image

$ rails generate controller Users new

Figure 6.2: A diagram of sample data in a users table.

Figure 6.3: A sketch of the User data model.

The analogous command for making a model is generate model, which we can use to generate a User model with name and email attributes, as shown in Listing 6.1.

Listing 6.1: Generating a User model.

Click here to view code image

$ rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20160523010738_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

(Note that, in contrast to the plural convention for controller names, model names are singular: a Users controller, but a User model.) By passing the optional parameters name:string and email:string, we tell Rails about the two attributes we want, along with which types those attributes should be (in this case, string). Compare this with including the action names in Listing 3.6 and Listing 5.38.

One of the results of the generate command in Listing 6.1 is a new file called a migration. Migrations provide a way to alter the structure of the database incrementally, so that our data model can adapt to changing requirements. In the case of the User model, the migration is created automatically by the model generation script; it creates a users table with two columns, name and email, as shown in Listing 6.2. (We’ll see starting in Section 6.2.5 how to make a migration from scratch.)

Listing 6.2: Migration for the User model (to create a users table).
db/migrate/[timestamp]_create_users.rb

Click here to view code image

class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

Note that the name of the migration file is prefixed by a timestamp based on when the migration was generated. In the early days of migrations, the filenames were prefixed with incrementing integers, which caused conflicts for collaborating teams if multiple programmers had migrations with the same number. Barring the improbable scenario of migrations generated the same second, using timestamps conveniently avoids such collisions.

The migration itself consists of a change method that determines the change to be made to the database. In the case of Listing 6.2, change uses a Rails method called create_table to create a table in the database for storing users. The create_table method accepts a block (Section 4.3.2) with one block variable, in this case called t (for “table”). Inside the block, the create_table method uses the t object to create name and email columns in the database, both of type string.⁴ Here the table name is plural (users) even though the model name is singular (User), which reflects a linguistic convention followed by Rails: a model represents a single user, whereas a database table consists of many users. The final line in the block, t.timestamps, is a special command that creates two magic columns called created_at and updated_at, which are timestamps that automatically record when a given user is created and updated. (We’ll see concrete examples of the magic columns starting in Section 6.1.3.) The full data model represented by the migration in Listing 6.2 is shown in Figure 6.4. (Note the addition of the magic columns, which weren’t present in the sketch shown in Figure 6.3.)

4. Don’t worry about exactly how the t object manages to do this; the beauty of abstraction layers is that we don’t have to know. We can just trust the t object to do its job.

We can run the migration, known as “migrating up”, using the db:migrate command as follows:

$ rails db:migrate

Figure 6.4: The User data model produced by Listing 6.2.

(You may recall that we ran this command in a similar context in Section 2.2.) The first time db:migrate is run, it creates a file called db/development.sqlite3, which is an SQLite⁵ database. We can see the structure of the database by opening development.sqlite3 with DB Browser for SQLite. (If you’re using the cloud IDE, you should first download the database file to the local disk, as shown in Figure 6.5.) The result appears in Figure 6.6; compare this with the diagram in Figure 6.4. You might note that there’s one column in Figure 6.6 not accounted for in the migration: the id column. As noted briefly in Section 2.2, this column is created automatically and is used by Rails to identify each row uniquely.

5. Officially pronounced “ess-cue-ell-ite”, although the (mis)pronunciation “sequel-ite” is also common.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Rails uses a file called schema.db in the db/ directory to keep track of the structure of the database (called the schema, hence the filename). Examine your local copy of db/schema.rb and compare its contents to the migration code in Listing 6.2.

2. Most migrations (including all the ones in this tutorial) are reversible, which means we can “migrate down” and undo them with a single command, called db:rollback:

$ rails db:rollback

After running this command, examine db/schema.rb to confirm that the rollback was successful. (See Box 3.1 for another technique useful for reversing migrations.) Under the hood, this command executes the drop_table command to remove the users table from the database. The reason this works is that the change method knows that drop_table is the inverse of create_table, which means that the rollback migration can be easily inferred. In the case of an irreversible migration, such as one to remove a database column, it is necessary to define separate up and down methods in place of the single change method. Read about migrations in the Rails Guides for more information.

3. Re-run the migration by executing rails db:migrate again. Confirm that the contents of db/schema.rb have been restored.

Figure 6.5: Downloading a file from the cloud IDE.

Figure 6.6: The DB Browser for SQLite with our new users table.

6.1.2 The Model File

We’ve seen how the User model generation in Listing 6.1 generated a migration file (Listing 6.2), and we saw in Figure 6.6 the results of running this migration: it updated a file called development.sqlite3 by creating a table users with columns id, name, email, created_at, and updated_at. Listing 6.1 also created the model itself. The rest of this section is dedicated to understanding it.

We begin by looking at the code for the User model, which lives in the file user.rb inside the app/models/ directory. It is, to put it mildly, very compact (Listing 6.3).

Listing 6.3: The brand new User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
end

Recall from Section 4.4.2 that the syntax class User < ApplicationRecord means that the User class inherits from the ApplicationRecord class, which in turn inherits from ActiveRecord::Base (Figure 2.18), so that the User model automatically has all the functionality of the ActiveRecord::Base class. Of course, this knowledge doesn’t do us any good unless we know what ActiveRecord::Base contains, so let’s get started with some concrete examples.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In a Rails console, use the technique from Section 4.4.4 to confirm that User.new is of class User and inherits from ApplicationRecord.

2. Confirm that ApplicationRecord inherits from ActiveRecord::Base.

6.1.3 Creating User Objects

As in Chapter 4, our tool of choice for exploring data models is the Rails console. Since we don’t (yet) want to make any changes to our database, we’ll start the console in a sandbox:

Click here to view code image

$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>

As indicated by the helpful message “Any modifications you make will be rolled back on exit”, when started in a sandbox the console will “roll back” (i.e., undo) any database changes introduced during the session.

In the console session in Section 4.4.5, we created a new user object with User.new, which we had access to only after requiring the example user file in Listing 4.17. With models, the situation is different; as you may recall from Section 4.4.4, the Rails console automatically loads the Rails environment, which includes the models. This means that we can make a new user object without any further work:

Click here to view code image

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

We see here the default console representation of a user object.

When called with no arguments, User.new returns an object with all nil attributes. In Section 4.4.5, we designed the example User class to take an initialization hash to set the object attributes; that design choice was motivated by Active Record, which allows objects to be initialized in the same way:

Click here to view code image

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com",
created_at: nil, updated_at: nil>

Here we see that the name and email attributes have been set as expected.

The notion of validity is important for understanding Active Record model objects. We’ll explore this subject in more depth in Section 6.2, but for now it’s worth noting that our initial user object is valid, which we can verify by calling the boolean valid? method on it:

>> user.valid?
true

So far, we haven’t touched the database: User.new only creates an object in memory, while user.valid? merely checks to see if the object is valid. In order to save the User object to the database, we need to call the save method on the user variable:

Click here to view code image

>> user.save
    (0.1ms) SAVEPOINT active_record_1
  SQL (0.8ms) INSERT INTO "users" ("name", "email", "created_at",
  "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"],
  ["email", "mhartl@example.com"], ["created_at", 2016-05-23 19:05:58 UTC],
  ["updated_at", 2016-05-23 19:05:58 UTC]]
   (0.1ms) RELEASE SAVEPOINT active_record_1
=> true

The save method returns true if it succeeds and false otherwise. (Currently, all saves should succeed because there are as yet no validations; we’ll see cases in Section 6.2 when some will fail.) For reference, the Rails console also shows the SQL command corresponding to user.save (namely, INSERT INTO "users"…). We’ll hardly ever need raw SQL in this book,⁶ and I’ll omit discussion of the SQL commands from now on, but you can learn a lot by reading the SQL corresponding to Active Record commands.

6. The only exception is in Section 14.3.3

You may have noticed that the new user object had nil values for the id and the magic columns created_at and updated_at attributes. Let’s see if our save changed anything:

Click here to view code image

>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">

We see that the id has been assigned a value of 1, while the magic columns have been assigned the current time and date.⁷ Currently, the created and updated timestamps are identical; we’ll see them differ in Section 6.1.5.

7. The timestamps are recorded in Coordinated Universal Time (UTC), which for most practical purposes is the same as Greenwich Mean Time. From the NIST Time and Frequency FAQ: Q: Why is UTC used as the acronym for Coordinated Universal Time instead of CUT? A: In 1970 the Coordinated Universal Time system was devised by an international advisory group of technical experts within the International Telecommunication Union (ITU). The ITU felt it was best to designate a single abbreviation for use in all languages in order to minimize confusion. Since unanimous agreement could not be achieved on using either the English word order, CUT, or the French word order, TUC, the acronym UTC was chosen as a compromise.

As with the User class in Section 4.4.5, instances of the User model allow access to their attributes using a dot notation:

Click here to view code image

>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Mon, 23 May 2016 19:05:58 UTC +00:00

As we’ll see in Chapter 7, it’s often convenient to make and save a model in two steps as we have above, but Active Record also lets you combine them into one step with User.create:

Click here to view code image

>> User.create (name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">

Note that User.create, rather than returning true or false, returns the User object itself, which we can optionally assign to a variable (such as foo in the second command above).

The inverse of create is destroy:

Click here to view code image

>> foo.destroy
   (0.1ms) SAVEPOINT active_record_1
  SQL (0.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
   (0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">

Like create, destroy returns the object in question, though I can’t recall ever having used the return value of destroy. In addition, the destroyed object still exists in memory:

Click here to view code image

>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">

So how do we know if we really destroyed an object? And for saved and non-destroyed objects, how can we retrieve users from the database? To answer these questions, we need to learn how to use Active Record to find user objects.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm that user.name and user.email are of class String.

2. Of what class are the created_at and updated_at attributes?

6.1.4 Finding User Objects

Active Record provides several options for finding objects. Let’s use them to find the first user we created while verifying that the third user (foo) has been destroyed. We’ll start with the existing user:

Click here to view code image

>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">

Here we’ve passed the id of the user to User.find; Active Record returns the user with that id.

Let’s see if the user with an id of 3 still exists in the database:

Click here to view code image

>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3

Since we destroyed our third user in Section 6.1.3, Active Record can’t find it in the database. Instead, find raises an exception, which is a way of indicating an exceptional event in the execution of a program—in this case, a nonexistent Active Record id, which causes find to raise an ActiveRecord::RecordNotFound exception.⁸

8. Exceptions and exception handling are somewhat advanced Ruby subjects, and we won’t need them much in this book. They are important, though, and I suggest learning about them using one of the Ruby books recommended in Section 14.4.1

In addition to the generic find, Active Record also allows us to find users by specific attributes:

Click here to view code image

>> User.find_by(email: "mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">

Since we will be using email addresses as usernames, this sort of find will be useful when we learn how to let users log in to our site (Chapter 7). If you’re worried that find_by will be inefficient if there are a large number of users, you’re ahead of the game; we’ll cover this issue, and its solution via database indices, in Section 6.2.5.

We’ll end with a couple of more general ways of finding users. First, there’s first:

Click here to view code image

>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">

Naturally, first just returns the first user in the database. There’s also all:

Click here to view code image

>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2016-05-23 19:05:58",
updated_at: "2016-05-23 19:05:58">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>

As you can see from the console output, User.all returns all the users in the database as an object of class ActiveRecord::Relation, which is effectively an array (Section 4.3.1).

Exercises

Solutions to exercises are available for free at railstutorial.org/aw-solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Find the user by name. Confirm that find_by_name works as well. (You will often encounter this older style of find_by in legacy Rails applications.)

2. For most practical purposes, User.all acts like an array, but confirm that in fact it’s of class User::ActiveRecord_Relation.

3. Confirm that you can find the length of User.all by passing it the length method (Section 4.2.3). Ruby’s ability to manipulate objects based on how they act rather than on their formal class type is called duck typing, based on the aphorism that “If it looks like a duck, and it quacks like a duck, it’s probably a duck.”

6.1.5 Updating User Objects

Once we’ve created objects, we often want to update them. There are two basic ways to do this. First, we can assign attributes individually, as we did in Section 4.4.5:

Click here to view code image

>> user           # Just a reminder about our user's attributes
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true

Note that the final step is necessary to write the changes to the database. We can see what happens without a save by using reload, which reloads the object based on the database information:

Click here to view code image

>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"

Now that we’ve updated the user by running user.save, the magic columns differ, as promised in Section 6.1.3:

Click here to view code image

>> user.created_at
=> "2016-05-23 19:05:58"
>> user.updated_at
=> "2016-05-23 19:08:23"

The second main way to update multiple attributes is to use update_attributes:⁹

9. The update_attributes method is an alias for the shorter update method, but I prefer the longer version because of its similarity to the singular version of the method, update_attribute.

Click here to view code image

>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"

The update_attributes method accepts a hash of attributes, and on success performs both the update and the save in one step (returning true to indicate that the save went through). Note that if any of the validations fail, such as when a password is required to save a record (as implemented in Section 6.3), the call to update_attributes will fail. If we need to update only a single attribute, using the singular update_attribute bypasses this restriction:

Click here to view code image

>> user.update_attribute(:name, "El Duderino")
=> true
>> user.name
=> "El Duderino"

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Update the user’s name using assignment and a call to save.

2. Update the user’s email address using a call to update_attributes.

3. Confirm that you can change the magic columns directly by updating the created_at column using assignment and a save. Use the value 1.year.ago, which is a Rails way to create a timestamp one year before the present time.

6.2 User Validations

The User model we created in Section 6.1 now has working name and email attributes, but they are completely generic: any string (including an empty one) is currently valid in either case. And yet, names and email addresses are more specific than this. For example, name should be non-blank, and email should match the specific format characteristic of email addresses. Moreover, since we’ll be using email addresses as unique usernames when users log in, we shouldn’t allow email duplicates in the database.

In short, we shouldn’t allow name and email to be just any strings; we should enforce certain constraints on their values. Active Record allows us to impose such constraints using validations (seen briefly before in Section 2.3.2). In this section, we’ll cover several of the most common cases, validating presence, length, format, and uniqueness. In Section 6.3.2 we’ll add a final common validation, confirmation. And we’ll see in Section 7.3 how validations give us convenient error messages when users make submissions that violate them.

6.2.1 A Validity Test

As noted in Box 3.3, test-driven development isn’t always the right tool for the job, but model validations are exactly the kind of features for which TDD is a perfect fit. It’s difficult to be confident that a given validation is doing exactly what we expect it to without writing a failing test and then getting it to pass.

Our method will be to start with a valid model object, set one of its attributes to something we want to be invalid, and then test that it in fact is invalid. As a safety net, we’ll first write a test to make sure the initial model object is valid. This way, when the validation tests fail we’ll know it’s for the right reason (and not because the initial object was invalid in the first place).

To get us started, the command in Listing 6.1 produced an initial test for testing users, though in this case it’s practically blank (Listing 6.4).

Listing 6.4: The practically blank default User test.
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

To write a test for a valid object, we’ll create an initially valid User model object @user using the special setup method (discussed briefly in the Chapter 3 exercises), which automatically gets run before each test. Because @user is an instance variable, it’s automatically available in all the tests, and we can test its validity using the valid? method (Section 6.1.3). The result appears in Listing 6.5.

Listing 6.5: A test for an initially valid user. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end

Listing 6.5 uses the plain assert method, which in this case succeeds if @user.valid? returns true and fails if it returns false.

Because our User model doesn’t currently have any validations, the initial test should pass:

Listing 6.6: GREEN

$ rails test:models

Here we’ve used rails test:models to run just the model tests (compare to rails test:integration from Section 5.3.4).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In the console, confirm that a new user is currently valid.

2. Confirm that the user created in Section 6.1.3 is also valid.

6.2.2 Validating Presence

Perhaps the most elementary validation is presence, which simply verifies that a given attribute is present. For example, in this section we’ll ensure that both the name and email fields are present before a user gets saved to the database. In Section 7.3.3, we’ll see how to propagate this requirement up to the signup form for creating new users.

We’ll start with a test for the presence of a name attribute by building on the test in Listing 6.5. As seen in Listing 6.7, all we need to do is set the @user variable’s name attribute to a blank string (in this case, a string of spaces) and then check (using the assert_not method) that the resulting User object is not valid.

Listing 6.7: A test for validation of the name attribute. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid?
  end
end

At this point, the model tests should be RED:

Listing 6.8: RED

$ rails test:models

As we saw briefly before in the Chapter 2 exercises, the way to validate the presence of the name attribute is to use the validates method with argument presence: true, as shown in Listing 6.9. The presence: true argument is a one-element options hash; recall from Section 4.3.4 that curly braces are optional when passing hashes as the final argument in a method. (As noted in Section 5.1.1, the use of options hashes is a recurring theme in Rails.)

Listing 6.9: Validating the presence of a name attribute. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
     validates :name, presence: true
end

Listing 6.9 may look like magic, but validates is just a method. An equivalent formulation of Listing 6.9 using parentheses is as follows:

Click here to view code image

class User < ApplicationRecord
  validates(:name, presence: true)
end

Let’s drop into the console to see the effects of adding a validation to our User model:¹⁰

10. I’ll omit the output of console commands when they are not particularly instructive—for example, the results of User.new.

Click here to view code image

$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false

Here we check the validity of the user variable using the valid? method, which returns false when the object fails one or more validations, and true when all validations pass. In this case, we only have one validation, so we know which one failed, but it can still be helpful to check using the errors object generated on failure:

Click here to view code image

>> user.errors.full_messages
=> ["Name can't be blank"]

(The error message is a hint that Rails validates the presence of an attribute using the blank? method, which we saw at the end of Section 4.4.3.)

Because the user isn’t valid, an attempt to save the user to the database automatically fails:

>> user.save
=> false

As a result, the test in Listing 6.7 should now be GREEN:

Listing 6.10: GREEN

$ rails test:models

Following the model in Listing 6.7, writing a test for email attribute presence is easy (Listing 6.11), as is the application code to get it to pass (Listing 6.12).

Listing 6.11: A test for validation of the email attribute. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = ""
    assert_not @user.valid?
  end

  test "email should be present" do
    @user.email = "     "
    assert_not @user.valid?
  end
end

Listing 6.12: Validating the presence of an email attribute. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name,  presence: true
  validates :email, presence: true
end

At this point, the presence validations are complete, and the test suite should be GREEN:

Listing 6.13: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Make a new user called u and confirm that it’s initially invalid. What are the full error messages?

2. Confirm that u.errors.messages is a hash of errors. How would you access just the email errors?

6.2.3 Length Validation

We’ve constrained our User model to require a name for each user, but we should go further: the users’ names will be displayed on the sample site, so we should enforce some limit on their length. With all the work we did in Section 6.2.2, this step is easy.

There’s no science to picking a maximum length; we’ll just pull 50 out of thin air as a reasonable upper bound, which means verifying that names of 51 characters are too long. In addition, although it’s unlikely ever to be a problem, there’s a chance that a user’s email address could overrun the maximum length of strings, which for many databases is 255. Because the format validation in Section 6.2.4 won’t enforce such a constraint, we’ll add one in this section for completeness. Listing 6.14 shows the resulting tests.

Listing 6.14: A test for name length validation. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "name should not be too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end

For convenience, we’ve used “string multiplication” in Listing 6.14 to make a string 51 characters long. We can see how this works using the console:

Click here to view code image

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51

The email length validation arranges to make a valid email address that’s one character too long:

Click here to view code image

>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaa@example.com"
>> ("a" * 244 + "@example.com").length
=> 256

At this point, the tests in Listing 6.14 should be RED:

Listing 6.15: RED

$ rails test

To get them to pass, we need to use the validation argument to constrain length, which is just length, along with the maximum parameter to enforce the upper bound (Listing 6.16).

Listing 6.16: Adding a length validation for the name attribute. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email,  presence: true, length: { maximum: 255 }
end

Now the tests should be GREEN:

Listing 6.17:   GREEN

$ rails test

With our test suite passing again, we can move on to a more challenging validation: email format.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Make a new user with too-long name and email and confirm that it’s not valid.

2. What are the error messages generated by the length validation?

6.2.4 Format Validation

Our validations for the name attribute enforce only minimal constraints—any non-blank name under 51 characters will do—but of course the email attribute must satisfy the more stringent requirement of being a valid email address. So far we’ve only rejected blank email addresses; in this section, we’ll require email addresses to conform to the familiar pattern user@example.com.

Neither the tests nor the validation will be exhaustive, just good enough to accept most valid email addresses and reject most invalid ones. We’ll start with a couple of tests involving collections of valid and invalid addresses. To make these collections, it’s worth knowing about the useful %w[] technique for making arrays of strings, as seen in this console session:

Click here to view code image

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?>   puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp

Here we’ve iterated over the elements of the addresses array using the each method (Section 4.3.2). With this technique in hand, we’re ready to write some basic email format validation tests.

Because email format validation is tricky and error-prone, we’ll start with some passing tests for valid email addresses to catch any errors in the validation. In other words, we want to make sure not just that invalid email addresses like user@example,com are rejected, but also that valid addresses like user@example.com are accepted, even after we impose the validation constraint. (Right now, of course, they’ll be accepted because all non-blank email addresses are currently valid.) The result for a representative sample of valid email addresses appears in Listing 6.18

Listing 6.18: Tests for valid email formats. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end

Note that we’ve included an optional second argument to the assertion with a custom error message, which in this case identifies the address causing the test to fail:

Click here to view code image

assert @user.valid?, "#{valid_address.inspect} should be valid"

(This uses the interpolated inspect method mentioned in Section 4.3.3.) Including the specific address that causes any failure is especially useful in a test with an each loop like Listing 6.18; otherwise, any failure would merely identify the line number, which is the same for all the email addresses, and which wouldn’t be sufficient to identify the source of the problem.

Next we’ll add tests for the invalidity of a variety of invalid email addresses, such as user@example,com (comma in place of dot) and user_at_foo.org (missing the @ sign). As in Listing 6.18, Listing 6.19 includes a custom error message to identify the exact address causing any failure.

Listing 6.19: Tests for email format validation. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
end

At this point, the tests should be RED:

Listing 6.20: RED

$ rails test

The application code for email format validation uses the format validation, which works like this:

Click here to view code image

validates :email, format: { with: /<regular expression>/ }

This validates the attribute with the given regular expression (or regex), which is a powerful (and often cryptic) language for matching patterns in strings. This means we need to construct a regular expression to match valid email addresses while not matching invalid ones.

There actually exists a full regex for matching email addresses according to the official email standard, but it’s enormous, obscure, and quite possibly counterproductive.¹¹ In this tutorial, we’ll adopt a more pragmatic regex that has proven to be robust in practice. Here’s what it looks like:

11. For example, did you know that "Michael Hartl"@example.com, with quotation marks and a space in the middle, is a valid email address according to the standard? Incredibly, it is—but it’s absurd.

Click here to view code image

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

To help understand where this comes from, Table 6.1 breaks it into bite-sized pieces.¹²

12. Note that, in Table 6.1, “letter” really means “lowercase letter”, but the i at the end of the regex enforces case-insensitive matching.

Table 6.1: Breaking down the valid email regex.

Although you can learn a lot by studying Table 6.1, to really understand regular expressions I consider using an interactive regular expression matcher like Rubular to be essential (Figure 6.7).¹³ The Rubular website has a beautiful interactive interface for making regular expressions, along with a handy regex quick reference. I encourage you to study Table 6.1 with a browser window open to Rubular—no amount of reading about regular expressions can replace playing with them interactively. (Note: If you use the regex from Table 6.1 in Rubular, I recommend leaving off the \A and \z characters so that you can match more than one email address at a time in the given test string. Also note that the regex consists of the characters inside the slashes /.../, so you should omit those when using Rubular.)

13. If you find it as useful as I do, I encourage you to donate to Rubular to reward developer Michael Lovitt for his wonderful work.

Applying the regular expression from Table 6.1 to the email format validation yields the code in Listing 6.21.

Listing 6.21: Validating the email format with a regular expression. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }
end

Here the regex VALID_EMAIL_REGEX is a constant, indicated in Ruby by a name starting with a capital letter. The code

Click here to view code image

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
                  format: { with: VALID_EMAIL_REGEX }

ensures that only email addresses that match the pattern will be considered valid. (The expression above has one minor weakness: it allows invalid addresses that contain consecutive dots, such as foo@bar..com. Updating the regex in Listing 6.21 to fix this blemish is left as an exercise [Section 6.2.4].)

Figure 6.7: The awesome Rubular regular expression editor.

At this point, the tests should be GREEN:

Listing 6.22:  GREEN

$ rails test:models

This means that there’s only one constraint left: enforcing email uniqueness.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By pasting in the valid addresses from Listing 6.18 and invalid addresses from Listing 6.19 into the test string area at Rubular, confirm that the regex from Listing 6.21 matches all of the valid addresses and none of the invalid ones.

2. As noted above, the email regex in Listing 6.21 allows invalid email addresses with consecutive dots in the domain name, i.e., addresses of the form foo@bar..com. Add this address to the list of invalid addresses in Listing 6.19 to get a failing test, and then use the more complicated regex shown in Listing 6.23 to get the test to pass.

3. Add foo@bar..com to the list of addresses at Rubular, and confirm that the regex shown in Listing 6.23 matches all the valid addresses and none of the invalid ones.

Listing 6.23: Disallowing double dots in email domain names. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence:   true, length: { maximum: 255 },
                    format:     { with: VALID_EMAIL_REGEX }
end

6.2.5 Uniqueness Validation

To enforce uniqueness of email addresses (so that we can use them as usernames), we’ll be using the :unique option to the validates method. But be warned: there’s a major caveat, so don’t just skim this section—read it carefully.

We’ll start with some short tests. In our previous model tests, we’ve mainly used User.new, which just creates a Ruby object in memory, but for uniqueness tests we actually need to put a record into the database.¹⁴ The initial duplicate email test appears in Listing 6.24

14. As noted briefly in the introduction to this section, there is a dedicated test database, db/test.sqlite3, for this purpose.

Listing 6.24: A test for the rejection of duplicate email addresses. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end

The method here is to make a user with the same email address as @user using @user.dup, which creates a duplicate user with the same attributes. Since we then save @user, the duplicate user has an email address that already exists in the database, and hence should not be valid.

We can get the new test in Listing 6.24 to pass by adding uniqueness: true to the email validation, as shown in Listing 6.25.

Listing 6.25: Validating the uniqueness of email addresses. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

We’re not quite done, though. Email addresses are typically processed as if they were case-insensitive—i.e., foo@bar.com is treated the same as FOO@BAR.COM or FoO@BAr.coM—so our validation should incorporate this as well.¹⁵ It’s thus important to test for case-insensitivity, which we do with the code in Listing 6.26.

15. Technically, only the domain part of the email address is case-insensitive: foo@bar.com is actually different from Foo@bar.com. In practice, though, it is a bad idea to rely on this fact; as noted at about.com, “Since the case sensitivity of email addresses can create a lot of confusion, interoperability problems and widespread headaches, it would be foolish to require email addresses to be typed with the correct case. Hardly any email service or ISP does enforce case sensitive email addresses, returning messages whose recipient’s email address was not typed correctly (in all upper case, for example).” Thanks to reader Riley Moses for pointing this out.

Listing 6.26: Testing case-insensitive email uniqueness. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end

Here we are using the upcase method on strings (seen briefly in Section 4.3.2). This test does the same thing as the initial duplicate email test, but with an upper-case email address instead. If this test feels a little abstract, go ahead and fire up the console:

Click here to view code image

$ rails console --sandbox
>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true

Of course, duplicate_user.valid? is currently true because the uniqueness validation is case-sensitive, but we want it to be false. Fortunately, :uniqueness accepts an option, :case_sensitive, for just this purpose (Listing 6.27).

Listing 6.27: Validating the uniqueness of email addresses, ignoring case. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness:  case_sensitive: false
end

Note that we have simply replaced true in Listing 6.25 with case_sensitive: false

in Listing 6.27 (Rails infers that uniqueness should be true as well.)

At this point, our application—with an important caveat—enforces email uniqueness, and our test suite should pass:

Listing 6.28:  GREEN

$ rails test

There’s just one small problem, which is that the Active Record uniqueness validation does not guarantee uniqueness at the database level. Here’s a scenario that explains why:

1. Alice signs up for the sample app, with address alice@wonderland.com.

2. Alice accidentally clicks on “Submit” twice, sending two requests in quick succession.

3. The following sequence occurs: request 1 creates a user in memory that passes validation, request 2 does the same, request 1’s user gets saved, request 2’s user gets saved.

4. This results in two user records with the exact same email address, despite the uniqueness validation.

If the above sequence seems implausible, believe me, it isn’t: it can happen on any Rails website with significant traffic (which I once learned the hard way). Luckily, the solution is straightforward to implement: we just need to enforce uniqueness at the database level as well as at the model level. Our method is to create a database index on the email column (Box 6.2) and then require that the index is unique.

Box 6.2: Database Indices

When creating a column in a database, it is important to consider whether we will need to find records by that column. Consider, for example, the email attribute created by the migration in Listing 6.2. When we allow users to log in to the sample app starting in Chapter 7, we will need to find the user record corresponding to the submitted email address. Unfortunately, based on the naïve data model, the only way to find a user by email address is to look through each user row in the database and compare its email attribute to the given email—which means we might have to examine every row (since the user could be the last one in the database). This is known in the database business as a full-table scan, and for a real site with thousands of users it is a Bad Thing.

Putting an index on the email column fixes the problem. To understand a database index, it’s helpful to consider the analogy of a book index. In a book, to find all the occurrences of a given string, say “foobar”, you would have to scan each page for “foobar”—the paper version of a full-table scan. With a book index, on the other hand, you can just look up “foobar” in the index to see all the pages containing “foobar”. A database index works essentially the same way.

The email index represents an update to our data modeling requirements, which (as discussed in Section 6.1.1) is handled in Rails using migrations. We saw in Section 6.1.1 that generating the User model automatically created a new migration (Listing 6.2); in the present case, we are adding structure to an existing model, so we need to create a migration directly using the migration generator:

Click here to view code image

$ rails generate migration add_index_to_users_email

Unlike the migration for users, the email uniqueness migration is not pre-defined, so we need to fill in its contents with Listing 6.29.¹⁶

16. Of course, we could just edit the migration file for the users table in Listing 6.2 but that would require rolling back and then migrating back up. The Rails way is to use migrations every time we discover that our data model needs to change.

Listing 6.29: The migration for enforcing email uniqueness.
db/migrate/[timestamp]_add_index_to_users_email.rb

Click here to view code image

class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
  def change
    add_index :users, :email, unique: true
  end
end

This uses a Rails method called add_index to add an index on the email column of the users table. The index by itself doesn’t enforce uniqueness, but the option unique: true does.

The final step is to migrate the database:

$ rails db:migrate

(If this fails, try exiting any running sandbox console sessions, which can lock the database and prevent migrations.)

At this point, the test suite should be RED due to a violation of the uniqueness constraint in the fixtures, which contain sample data for the test database. User fixtures were generated automatically in Listing 6.1, and as shown in Listing 6.30 the email addresses are not unique. (They’re not valid either, but fixture data doesn’t get run through the validations.)

Listing 6.30: The default user fixtures. RED
test/fixtures/users.yml

Click here to view code image

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

Because we won’t need fixtures until Chapter 8, for now we’ll just remove them, leaving an empty fixtures file (Listing 6.31).

Listing 6.31: An empty fixtures file. GREEN
test/fixtures/users.yml

# empty

Having addressed the uniqueness caveat, there’s one more change we need to make to be assured of email uniqueness. Some database adapters use case-sensitive indices, considering the strings “Foo@ExAMPle.CoM” and “foo@example.com” to be distinct, but our application treats those addresses as the same. To avoid this incompatibility, we’ll standardize on all lowercase addresses, converting “Foo@ExAMPle.CoM” to “foo@example.com” before saving it to the database. The way to do this is with a callback, which is a method that gets invoked at a particular point in the lifecycle of an Active Record object. In the present case, that point is before the object is saved, so we’ll use a before_save callback to downcase the email attribute before saving the user.¹⁷ The result appears in Listing 6.32. (This is just a first implementation; we’ll discuss this subject again in Section 11.1, where we’ll use the preferred method reference convention for defining callbacks.)

17. See the Rails API entry on callbacks for more information on which callbacks Rails supports.

Listing 6.32: Ensuring email uniqueness by downcasing the email attribute. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

The code in Listing 6.32 passes a block to the before_save callback and sets the user’s email address to a lowercase version of its current value using the downcase string method. (Writing a test for email downcasing is left as an exercise [Section 6.2.5].)

In Listing 6.32, we could have written the assignment as

Click here to view code image

self.email = self.email.downcase

(where self refers to the current user), but inside the User model the self keyword is optional on the right-hand side:

self.email = email.downcase

We encountered this idea briefly in the context of reverse in the palindrome method (Section 4.4.2), which also noted that self is not optional in an assignment, so

email = email.downcase

wouldn’t work. (We’ll discuss this subject in more depth in Section 9.1.)

At this point, the Alice scenario above will work fine: the database will save a user record based on the first request, and it will reject the second save because the duplicate email address violates the uniqueness constraint. (An error will appear in the Rails log, but that doesn’t do any harm.) Moreover, adding this index on the email attribute accomplishes a second goal, alluded to briefly in Section 6.1.4: as noted in Box 6.2, the index on the email attribute fixes a potential efficiency problem by preventing a full-table scan when finding users by email address.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Add a test for the email downcasing from Listing 6.32, as shown in Listing 6.33. This test uses the reload method for reloading a value from the database and the assert_equal method for testing equality. To verify that Listing 6.33 tests the right thing, comment out the before_save line to get to RED; then uncomment it to get to GREEN.

2. By running the test suite, verify that the before_save callback can be written using the “bang” method email.downcase! to modify the email attribute directly, as shown in Listing 6.34.

Listing 6.33: A test for the email downcasing from Listing 6.32.
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end

  test "email addresses should be saved as lowercase" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
  end
end

Listing 6.34: An alternate callback implementation. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { email.downcase! }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

6.3 Adding a Secure Password

Now that we’ve defined validations for the name and email fields, we’re ready to add the last of the basic User attributes: a secure password. The method is to require each user to have a password (with a password confirmation) and then store a hashed version of the password in the database. (There is some potential for confusion here. In the present context, a hash refers not to the Ruby data structure from Section 4.3.3 but rather to the result of applying an irreversible hash function to input data.) We’ll also add a way to authenticate a user based on a given password, a method we’ll use in Chapter 8 to allow users to log in to the site.

The method for authenticating users will be to take a submitted password, hash it, and compare the result to the hashed value stored in the database. If the two match, then the submitted password is correct and the user is authenticated. By comparing hashed values instead of raw passwords, we will be able to authenticate users without storing the passwords themselves. This means that even if our database is compromised, our users’ passwords will still be secure.

6.3.1 A Hashed Password

Most of the secure password machinery will be implemented using a single Rails method called has_secure_password, which we’ll include in the User model as follows:

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

When included in a model as above, this one method adds the following functionality:

• The ability to save a securely hashed password_digest attribute to the database

• A pair of virtual attributes¹⁸ (password and password_confirmation), including presence validations upon object creation and a validation requiring that they match

18. In this context, virtual means that the attributes exist on the model object but do not correspond to columns in the database.

• An authenticate method that returns the user when the password is correct (and false otherwise)

The only requirement for has_secure_password to work its magic is for the corresponding model to have an attribute called password_digest. (The name digest comes from the terminology of cryptographic hash functions. In this context, hashed password and password digest are synonyms.)¹⁹ In the case of the User model, this leads to the data model shown in Figure 6.8.

19. Hashed password digests are often erroneously referred to as encrypted passwords. For example, the source code of has_secure_password makes this mistake, as did the first two editions of this tutorial. This terminology is wrong because by design encryption is reversible—the ability to encrypt implies the ability to decrypt as well. In contrast, the whole point of calculating a password’s hash digest is to be irreversible, so that it is computationally intractable to infer the original password from the digest. (Thanks to reader Andy Philips for pointing out this issue and for encouraging me to fix the broken terminology.)

To implement the data model in Figure 6.8 , we first generate an appropriate migration for the password_digest column. We can choose any migration name we want, but it’s convenient to end the name with to_users, since in this case Rails automatically constructs a migration to add columns to the users table. The result, with migration name add_password_digest_to_users, appears as follows:

Click here to view code image

$ rails generate migration add_password_digest_to_users password_digest:string

Here we’ve also supplied the argument password_digest:string with the name and type of attribute we want to create. (Compare this to the original generation of the users table in Listing 6.1, which included the arguments name:string and email:string.) By including password_digest:string, we’ve given Rails enough information to construct the entire migration for us, as seen in Listing 6.35.

Figure 6.8: The User data model with an added password_digest attribute.

Listing 6.35: The migration to add a password_digest column.
db/migrate/[timestamp]_add_password_digest_to_users.rb

Click here to view code image

class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password_digest, :string
  end
end

Listing 6.35 uses the add_column method to add a password_digest column to the users table. To apply it, we just migrate the database:

$ rails db:migrate

To make the password digest, has_secure_password uses a state-of-the-art hash function called bcrypt. By hashing the password with bcrypt, we ensure that an attacker won’t be able to log in to the site even if they manage to obtain a copy of the database. To use bcrypt in the sample application, we need to add the bcrypt gem to our Gemfile (Listing 6.36).²⁰

20. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

Listing 6.36: Adding bcrypt to the Gemfile.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',          '5.0.0'
gem 'bcrypt',         '3.1.11'
.
.
.

Then run bundle install as usual:

$ bundle install

6.3.2 User Has Secure Password

Now that we’ve supplied the User model with the required password_digest attribute and installed bcrypt, we’re ready to add has_secure_password to the User model, as shown in Listing 6.37.

Listing 6.37: Adding has_secure_password to the User model. RED
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }

  has_secure_password
end

As indicated by the RED indicator in Listing 6.37, the tests are now failing, as you can confirm at the command line:

Listing 6.38: RED

$ rails test

The reason is that, as noted in Section 6.3.1, has_secure_password enforces validations on the virtual password and password_confirmation attributes, but the tests in Listing 6.26 create an @user variable without these attributes:

Click here to view code image

def setup
  @user = User.new(name: "Example User", email: "user@example.com")
end

So, to get the test suite passing again, we just need to add a password and its confirmation, as shown in Listing 6.39.

Listing 6.39: Adding a password and its confirmation. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
end

Note that the first line inside the setup method includes an additional comma at the end, as required by Ruby’s hash syntax (Section 4.3.3). Leaving this comma off will produce a syntax error, and you should use your technical sophistication (Box 1.1) to identify and resolve such errors if (or, more realistically, when) they occur.

At this point the tests should be GREEN:

Listing 6.40: GREEN

$ rails test

We’ll see in just a moment the benefits of adding has_secure_password to the User model (Section 6.3.4), but first we’ll add a minimal requirement on password security.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm that a user with valid name and email still isn’t valid overall.

2. What are the error messages for a user with no password?

6.3.3 Minimum Password Standards

It’s good practice in general to enforce some minimum standards on passwords to make them harder to guess. There are many options for enforcing password strength in Rails, but for simplicity we’ll just enforce a minimum length and the requirement that the password not be blank. Picking a length of 6 as a reasonable minimum leads to the validation test shown in Listing 6.41.

Listing 6.41: Testing for a minimum password length. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end

Note the use of the compact multiple assignment

Click here to view code image

@user.password = @user.password_confirmation = "a" * 5

in Listing 6.41. This arranges to assign a particular value to the password and its confirmation at the same time (in this case, a string of length 5, constructed using string multiplication as in Listing 6.14).

You may be able to guess the code for enforcing a minimum length constraint by referring to the corresponding maximum validation for the user’s name (Listing 6.16):

Click here to view code image

validates :password, length: { minimum: 6 }

Combining this with a presence validation (Section 6.2.2) to ensure nonblank passwords, this leads to the User model shown in Listing 6.42. (It turns out the has_secure_password method includes a presence validation, but unfortunately it only applies to records with empty passwords, which allows users to create invalid passwords like ' ' [six spaces].)

Listing 6.42: The complete implementation for secure passwords. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
true, length: { minimum: 6 }
end

At this point, the tests should be GREEN:

Listing 6.43: GREEN

$ rails test:models

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm that a user with valid name and email but a too-short password isn’t valid.

2. What are the associated error messages?

6.3.4 Creating and Authenticating a User

Now that the basic User model is complete, we’ll create a user in the database as preparation for making a page to show the user’s information in Section 7.1. We’ll also take a more concrete look at the effects of adding has_secure_password to the User model, including an examination of the important authenticate method.

Since users can’t yet sign up for the sample application through the web—that’s the goal of Chapter 7—we’ll use the Rails console to create a new user by hand. For convenience, we’ll use the create method discussed in Section 6.1.3, but in the present case we’ll take care not to start in a sandbox so that the resulting user will be saved to the database. This means starting an ordinary rails console session and then creating a user with a valid name and email address together with a valid password and matching confirmation:

Click here to view code image

$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

To check that this worked, let’s look at the resulting users table in the development database using DB Browser for SQLite, as shown in Figure 6.9.²¹ (If you’re using the cloud IDE, you should download the database file as in Figure 6.5.) Note that the columns correspond to the attributes of the data model defined in Figure 6.8.

21. If for any reason something went wrong, you can always reset the database as follows:

1. Quit the console.

2. Run $ rm -f development.sqlite3 at the command line to remove the database. (We’ll learn a more elegant method for doing this in Chapter 7.)

3. Re-run the migrations using $ rails db:migrate.

4. Restart the console.

Returning to the console, we can see the effect of has_secure_password from Listing 6.42 by looking at the password_digest attribute:

Click here to view code image

>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"

This is the hashed version of the password ("foobar") used to initialize the user object. Because it’s constructed using bcrypt, it is computationally impractical to use the digest to discover the original password.²²

22. By design, the bcrypt algorithm produces a salted hash, which protects against two important classes of attacks (dictionary attacks and rainbow table attacks).

As noted in Section 6.3.1, has_secure_password automatically adds an authenticate method to the corresponding model objects. This method determines if a given password is valid for a particular user by computing its digest and comparing the result to password_digest in the database. In the case of the user we just created, we can try a couple of invalid passwords as follows:

Click here to view code image

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false

Figure 6.9: A user row in the SQLite database db/development.sqlite3.

Here user.authenticate returns false for invalid password. If we instead authenticate with the correct password, authenticate returns the user itself:

Click here to view code image

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

In Chapter 8, we’ll use the authenticate method to sign registered users into our site. In fact, it will turn out not to be important to us that authenticate returns the user itself; all that will matter is that it returns a value that is true in a boolean context. Recalling from Section 4.2.3 that !! converts an object to its corresponding boolean value, we can see that user.authenticate does the job nicely:

Click here to view code image

>> !!user.authenticate("foobar")
=> true

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Quit and restart the console, and then find the user created in this section.

2. Try changing the name by assigning a new name and calling save. Why didn’t it work?

3. Update user’s name to use your name. Hint: The necessary technique is covered in Section 6.1.5.

6.4 Conclusion

Starting from scratch, in this chapter we created a working User model with name, email, and password attributes, together with validations enforcing several important constraints on their values. In addition, we have the ability to securely authenticate users using a given password. This is a remarkable amount of functionality for only twelve lines of code.

In the next chapter, Chapter 7, we’ll make a working signup form to create new users, together with a page to display each user’s information. In Chapter 8, we’ll then use the authentication machinery from Section 6.3 to let users log into the site.

If you’re using Git, now would be a good time to commit if you haven’t done so in a while:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"

Then merge back into the master branch and push to the remote repository:

Click here to view code image

$ git checkout master
$ git merge modeling-users
$ git push

To get the User model working in production, we need to run the migrations at Heroku, which we can do with heroku run:

Click here to view code image

$ rails test
$ git push heroku
$ heroku run rails db:migrate

We can verify that this worked by running a console in production:

Click here to view code image

$ heroku run console --sandbox
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2016-05-23 20:54:41", updated_at: "2016-05-23 20:54:41",
password_digest: "$2a$10$74xFguZRoTZBXTUqs1FjpOf3OoLhrvgxC2wlohtTEcH...">

6.4.1 What We Learned in This Chapter

• Migrations allow us to modify our application’s data model.

• Active Record comes with a large number of methods for creating and manipulating data models.

• Active Record validations allow us to place constraints on the data in our models.

• Common validations include presence, length, and format.

• Regular expressions are cryptic but powerful.

• Defining a database index improves lookup efficiency while allowing enforcement of uniqueness at the database level.

• We can add a secure password to a model using the built-in has_secure_password method.

Chapter 7. Sign Up

Now that we have a working User model, it’s time to add an ability few websites can live without: letting users sign up. We’ll use an HTML form to submit user signup information to our application (Section 7.2), which will then be used to create a new user and save its attributes to the database (Section 7.4). At the end of the signup process, it’s important to render a profile page with the newly created user’s information, so we’ll begin by making a page for showing users, which will serve as the first step toward implementing the REST architecture for users (Section 2.2.2). Along the way, we’ll build on our work in Section 5.3.4 to write succinct and expressive integration tests.

In this chapter, we’ll rely on the User model validations from Chapter 6 to increase the odds of new users having valid email addresses. In Chapter 11, we’ll make sure of email validity by adding a separate account activation step to user signup.

Although this tutorial is designed to be as simple as possible while still being professional-grade, web development is a complicated subject, and Chapter 7 necessarily marks a significant increase in the difficulty of the exposition. I recommend taking your time with the material and reviewing it as necessary. (Some readers have reported simply doing the chapter twice is a helpful exercise.) You might also consider joining the Learn Enough Society to gain additional assistance, both with this tutorial and with its relevant prerequisites (especially Learn Enough Ruby to Be Dangerous, Learn Enough Sinatra to Be Dangerous, and Learn Enough Rails to Be Dangerous).

7.1 Showing Users

In this section, we’ll take the first steps toward the final profile by making a page to display a user’s name and profile photo, as indicated by the mockup in Figure 7.1.¹ Our eventual goal for the user profile pages is to show the user’s profile image, basic user data, and a list of microposts, as mocked up in Figure 7.2.² (Figure 7.2 has our first example of lorem ipsum text, which has a fascinating story that you should definitely read about sometime.) We’ll complete this task, and with it the sample application, in Chapter 14.

1. Mockingbird doesn’t support custom images like the profile photo in Figure 7.1; I put that in by hand using GIMP.

2. Image retrieved from http://www.flickr.com/photos/43803060@N00/24308857/ on 2014-06-16. Copyright © 2002 by Shaun Wallin and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

Figure 7.1: A mockup of the user profile made in this section.

Figure 7.2: A mockup of our best guess at the final profile page.

If you’re following along with version control, make a topic branch as usual:

$ git checkout -b sign-up

7.1.1 Debug and Rails Environments

The profiles in this section will be the first truly dynamic pages in our application. Although the view will exist as a single page of code, each profile will be customized using information retrieved from the application’s database. As preparation for adding dynamic pages to our sample application, now is a good time to add some debug information to our site layout (Listing 7.1). This displays some useful information about each page using the built-in debug method and params variable (which we’ll learn more about in Section 7.1.2).

Listing 7.1: Adding some debug information to the site layout.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
</html>

Since we don’t want to display debug information to users of a deployed application, Listing 7.1 uses

if Rails.env.development?

to restrict the debug information to the development environment, which is one of three environments defined by default in Rails (Box 7.1).³ In particular, Rails.env.development? is true only in a development environment, so the embedded Ruby

3. You can define your own custom environments as well; see the RailsCast on adding an environment for details.

Click here to view code image

<%= debug(params) if Rails.env.development? %>

won’t be inserted into production applications or tests. (Inserting the debug information into tests probably wouldn’t do any harm, but it probably wouldn’t do any good, either, so it’s best to restrict the debug display to development only.)

Box 7.1: Rails Environments

Rails comes equipped with three environments: test, development, and production. The default environment for the Rails console is development:

Click here to view code image

$ rails console
Loading development environment
>> Rails.env
=> "development"
>> Rails.env.development?
=> true
>> Rails.env.test?
=> false

As you can see, Rails provides a Rails object with an env attribute and associated environment boolean methods so that, for example, Rails.env.test? returns true in a test environment and false otherwise.

If you ever need to run a console in a different environment (to debug a test, for example), you can pass the environment as a parameter to the console script:

$ rails console test
Loading test environment
>> Rails.env
=> "test"
>> Rails.env.test?
=> true

As with the console, development is the default environment for the Rails server, but you can also run it in a different environment:

Click here to view code image

$ rails server --environment production

If you view your app running in production, it won’t work without a production database, which we can create by running rails db:migrate in production:

$$$$$$$$$

Click here to view code image

$ rails db:migrate RAILS_ENV=production

(I find it confusing that the idiomatic commands to run the console, server, and migrate commands in non-default environments use different syntax, which is why I bothered showing all three. It’s worth noting, though, that preceding any of them with RAILS_ENV=<env> will also work, as in RAILS_ENV=production rails server.)

By the way, if you have deployed your sample app to Heroku, you can see its environment using heroku run console:

$ heroku run console
>> Rails.env
=> "production"
>> Rails.env.production?
=> true

Naturally, since Heroku is a platform for production sites, it runs each application in a production environment.

To make the debug output look nice, we’ll add some rules to the custom stylesheet created in Chapter 5, as shown in Listing 7.2.

Listing 7.2: Adding code for a pretty debug box, including a Sass mixin.
app/assets/stylesheets/custom.css.scss

Click here to view code image

@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

@mixin box_sizing {
   -moz-box-sizing:      border-box;
   -webkit-box-sizing:   border-box;
   box-sizing:           border-box;
}
.
.
.
/* miscellaneous */

.debug_dump {
   clear: both;
   float: left;
   width: 100%;
   margin-top: 45px;
   @include box_sizing;
}

This introduces the Sass mixin facility, in this case called box_sizing. A mixin allows a group of CSS rules to be packaged up and used for multiple elements, converting

.debug_dump {
   .
   .
   .
   @include box_sizing;
}

to

Click here to view code image

.debug_dump {
   .
   .
   .
   -moz-box-sizing:    border-box;
   -webkit-box-sizing: border-box;
   box-sizing:         border-box;
}

We’ll put this mixin to use again in Section 7.2.1. The result in the case of the debug box is shown in Figure 7.3.⁴

4. The exact appearance of the Rails debug information is slightly version-dependent. For example, as of Rails 5 the debug information shows the permitted status of the information, a subject we’ll cover in Section 7.3.2. Use your technical sophistication (Box 1.1) to resolve such minor discrepancies.

The debug output in Figure 7.3 gives potentially useful information about the page being rendered:

Click here to view code image

---
controller: static_pages
action: home

This is a YAML⁵ representation of params, which is basically a hash, and in this case identifies the controller and action for the page. We’ll see another example in Section 7.1.2.

5. The Rails debug information is shown as YAML (a recursive acronym standing for “YAML Ain’t Markup Language”), which is a friendly data format designed to be both machine- and human-readable.

Figure 7.3: The sample application Home page with debug information.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Visit /about in your browser and use the debug information to determine the controller and action of the params hash.

2. In the Rails console, pull the first user out of the database and assign it to the variable user. What is the output of puts user.attributes.to_yaml? Compare this to using the y method via y user.attributes.

7.1.2 A Users Resource

In order to make a user profile page, we need to have a user in the database, which introduces a chicken-and-egg problem: how can the site have a user before there is a working signup page? Happily, this problem has already been solved: in Section 6.3.4, we created a User record by hand using the Rails console, so there should be one user in the database:

Click here to view code image

$ rails console
>> User.count
=> 1
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

(If you don’t currently have a user in your database, you should visit Section 6.3.4 now and complete it before proceeding.) We see from the console output above that the user has id 1, and our goal now is to make a page to display this user’s information. We’ll follow the conventions of the REST architecture favored in Rails applications (Box 2.2), which means representing data as resources that can be created, shown, updated, or destroyed—four actions corresponding to the four fundamental operations POST, GET, PATCH, and DELETE defined by the HTTP standard (Box 3.2).

When following REST principles, resources are typically referenced using the resource name and a unique identifier. What this means in the context of users—which we’re now thinking of as a Users resource—is that we should view the user with id 1 by issuing a GET request to the URL /users/1. Here the show action is implicit in the type of request—when Rails’ REST features are activated, GET requests are automatically handled by the show action.

We saw in Section 2.2.1 that the page for a user with id 1 has URL /users/1. Unfortunately, visiting that URL right now just gives an error, as seen in the server log (Figure 7.4).

We can get the routing for /users/1 to work by adding a single line to our routes file (config/routes.rb):

resources :users

Figure 7.4: The server log error for /users/1.

The result appears in Listing 7.3.

Listing 7.3: Adding a Users resource to the routes file.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get '/help',    to: 'static_pages#help'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
  get '/signup', to: 'users#new'
  resources :users
end

Although our immediate motivation is making a page to show users, the single line resources :users doesn’t just add a working /users/1 URL; it endows our sample application with all the actions needed for a RESTful Users resource,⁶ along with a large number of named routes (Section 5.3.3) for generating user URLs. The resulting correspondence of URLs, actions, and named routes is shown in Table 7.1. (Compare to Table 2.2.) Over the course of the next three chapters, we’ll cover all of the other entries in Table 7.1 as we fill in all the actions necessary to make Users a fully RESTful resource.

6. This means that the routing works, but the corresponding pages don’t necessarily work at this point. For example, /users/1/edit gets routed properly to the edit action of the Users controller, but since the edit action doesn’t exist yet, actually hitting that URL will return an error.

With the code in Listing 7.3, the routing works, but there’s still no page there (Figure 7.5). To fix this, we’ll begin with a minimalist version of the profile page, which we’ll flesh out in Section 7.1.4.

Table 7.1: RESTful routes provided by the Users resource in Listing 7.3.

Figure 7.5: The URL /users/1 with routing but no page.

We’ll use the standard Rails location for showing a user, which is app/views/users/show.html.erb. Unlike the new.html.erb view, which we created with the generator in Listing 5.38, the show.html.erb file doesn’t currently exist, so you’ll have to create it by hand⁷ and then fill it with the content shown in Listing 7.4.

7. Using, e.g., touch app/views/users/show.html.erb.

Listing 7.4: A stub view for showing user information.
app/views/users/show.html.erb

Click here to view code image

<%= @user.name %>, <%= @user.email %>

This view uses embedded Ruby to display the user’s name and email address, assuming the existence of an instance variable called @user. Of course, eventually the real user show page will look very different (and won’t display the email address publicly).

In order to get the user show view to work, we need to define an @user variable in the corresponding show action in the Users controller. As you might expect, we use the find method on the User model (Section 6.1.4) to retrieve the user from the database, as shown in Listing 7.5.

Listing 7.5: The Users controller with a show action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
  end
end

Here we’ve used params to retrieve the user id. When we make the appropriate request to the Users controller, params[:id] will be the user id 1, so the effect is the same as the find method User.find(1) we saw in Section 6.1.4. (Technically, params[:id] is the string "1", but find is smart enough to convert this to an integer.)

With the user view and action defined, the URL /users/1 works perfectly, as seen in Figure 7.6. (If you haven’t restarted the Rails server since adding bcrypt, you may have to do so at this time. This sort of thing is a good application of technical sophistication [Box 1.1].) Note that the debug information in Figure 7.6 confirms the value of params[:id]:

Click here to view code image

---
action: show
controller: users
id: '1'

This is why the code

User.find(params[:id])

in Listing 7.5 finds the user with id 1.

Figure 7.6: The user show page after adding a Users resource.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using embedded Ruby, add the created_at and updated_at “magic column” attributes to the user show page from Listing 7.4.

2. Using embedded Ruby, add Time.now to the user show page. What happens when you refresh the browser?

7.1.3 Debugger

We saw in Section 7.1.2 how the information in the debug could help us understand what’s going on in our application, but there’s also a more direct way to get debugging information using the byebug gem (Listing 3.2). To see how it works, we just need to add a line consisting of debugger to our application, as shown in Listing 7.6.

Listing 7.6: The Users controller with a debugger.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
     @user = User.find(params[:id])
     debugger
  end

  def new
  end
end

Now, when we visit /users/1, the Rails server shows a byebug prompt:

(byebug)

We can treat this like a Rails console, issuing commands to figure out the state of the application:

Click here to view code image

(byebug) @user.name
"Example User"
(byebug) @user.email
"example@railstutorial.org"
(byebug) params[:id]
"1"

To release the prompt and continue execution of the application, press Ctrl-D; then remove the debugger line from the show action (Listing 7.7).

Listing 7.7: The Users controller with the debugger line removed.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
     @user = User.find(params[:id])
  end

  def new
  end
end

Whenever you’re confused about something in a Rails application, it’s a good practice to put debugger close to the code you think might be causing the trouble. Inspecting the state of the system using byebug is a powerful method for tracking down application errors and interactively debugging your application.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. With the debugger in the show action as in Listing 7.6, hit /users/1. Use puts to display the value of the YAML form of the params hash. Hint: Refer to the relevant exercise in Section 7.1.1. How does it compare to the debug information shown by the debug method in the site template?

2. Put the debugger in the User new action and hit /users/new. What is the value of @user?

7.1.4 A Gravatar Image and a Sidebar

Having defined a basic user page in the previous section, we’ll now flesh it out a little with a profile image for each user and the first cut of the user sidebar. We’ll start by adding a “globally recognized avatar”, or Gravatar, to the user profile.⁸ Gravatar is a free service that allows users to upload images and associate them with email addresses they control. As a result, Gravatars are a convenient way to include user profile images without going through the trouble of managing image upload, cropping, and storage; all we need to do is construct the proper Gravatar image URL using the user’s email address, and the corresponding Gravatar image will automatically appear. (We’ll learn how to handle custom image upload in Section 13.4.)

8. In Hinduism, an avatar is the manifestation of a deity in human or animal form. By extension, the term avatar is commonly used to mean some kind of personal representation, especially in a virtual environment. (In the context of Twitter and other social media, the term avi has gained currency, which is likely a mutated form of avatar.)

Our plan is to define a gravatar_for helper function to return a Gravatar image for a given user, as shown in Listing 7.8.

Listing 7.8: The user show view with name and Gravatar.
app/views/users/show.html.erb

Click here to view code image

<% provide(:title, @user.name) %>
<h1>
   <%= gravatar_for @user %>
   <%= @user.name %>
</h1>

By default, methods defined in any helper file are automatically available in any view, but for convenience we’ll put the gravatar_for method in the file for helpers associated with the Users controller. As noted in the Gravatar documentation, Gravatar URLs are based on an MD5 hash of the user’s email address. In Ruby, the MD5 hashing algorithm is implemented using the hexdigest method, which is part of the Digest library:

Click here to view code image

>> email = "MHARTL@example.COM"
>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968"

Since email addresses are case-insensitive (Section 6.2.4) but MD5 hashes are not, we’ve used the downcase method to ensure that the argument to hexdigest is all lowercase. (Because of the email downcasing callback in Listing 6.32, this will never make a difference in this tutorial, but it’s a good practice in case the gravatar_for ever gets used on email addresses from other sources.) The resulting gravatar_for helper appears in Listing 7.9.

Listing 7.9: Defining a gravatar_for helper method.
app/helpers/users_helper.rb

Click here to view code image

module UsersHelper

  # Returns the Gravatar for the given user.
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

The code in Listing 7.9 returns an image tag for the Gravatar with a gravatar CSS class and alt text equal to the user’s name (which is especially convenient for sight-impaired users using a screen reader).

The profile page appears as in Figure 7.7, which shows the default Gravatar image that appears because user@example.com isn’t a real email address. (In fact, as you can see by visiting it, the example.com domain is reserved for examples like this one.)

To get our application to display a custom Gravatar, we’ll use update_attributes (Section 6.1.5) to change the user’s email to something I control:

Click here to view code image

$ rails console
>> user = User.first
>> user.update_attributes(name: "Example User",
?>                        email: "example@railstutorial.org",
?>                        password: "foobar",
?>                        password_confirmation: "foobar")
=> true

Here we’ve assigned the user the email address example@railstutorial.org, which I’ve associated with the Rails Tutorial logo, as seen in Figure 7.8.

Figure 7.7: The user profile page with the default Gravatar.

The last element needed to complete the mockup from Figure 7.1 is the initial version of the user sidebar. We’ll implement it using the aside tag, which is used for content (such as sidebars) that complements the rest of the page but can also stand alone. We include row and col-md-4 classes, which are both part of Bootstrap. The code for the modified user show page appears in Listing 7.10.

Listing 7.10: Adding a sidebar to the user show view.
app/views/users/show.html.erb

Click here to view code image

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
     <section class="user_info">
       <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
  </aside>
</div>

With the HTML elements and CSS classes in place, we can style the profile page (including the sidebar and the Gravatar) with the SCSS shown in Listing 7.11.⁹ (Note the nesting of the table CSS rules, which works only because of the Sass engine used by the asset pipeline.) The resulting page is shown in Figure 7.9.

9. Listing 7.11 includes the .gravatar_edit class, which we’ll put to work in Chapter 10.

Figure 7.8: The user show page with a custom Gravatar.

Figure 7.9: The user show page with a sidebar and CSS.

Listing 7.11: SCSS for styling the user show page, including the sidebar.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* sidebar */

aside {
  section.user_info {
    margin-top: 20px;
  }
  section {
    padding: 10px 0;
    margin-top: 20px;
    &:first-child {
      border: 0;
      padding-top: 0;
    }
    span {
      display: block;
      margin-bottom: 3px;
      line-height: 1;
    }
    h1 {
      font-size: 1.4em;
      text-align: left;
      letter-spacing: -1px;
      margin-bottom: 3px;
      margin-top: 0px;
    }
  }
}

.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Associate a Gravatar with your primary email address if you haven’t already. What is the MD5 hash associated with the image?

2. Verify that the code in Listing 7.12 allows the gravatar_for helper defined in Section 7.1.4 to take an optional size parameter, allowing code like gravatar_for user, size: 50 in the view. (We’ll put this improved helper to use in Section 10.3.1.)

3. The options hash used in the previous exercise is still commonly used, but as of Ruby 2.0 we can use keyword arguments instead. Confirm that the code in Listing 7.13 can be used in place of Listing 7.12. What are the diffs between the two?

Listing 7.12: Adding an options hash in the gravatar_for helper.
app/helpers/users_helper.rb

Click here to view code image

module UsersHelper

  # Returns the Gravatar for the given user.
  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

Listing 7.13: Using keyword arguments in the gravatar_for helper.
app/helpers/users_helper.rb

Click here to view code image

module UsersHelper

  # Returns the Gravatar for the given user.
  def gravatar_for(user, size: 80)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

7.2 Signup Form

Now that we have a working (though not yet complete) user profile page, we’re ready to make a signup form for our site. We saw in Figure 5.11 (shown again in Figure 7.10) that the signup page is currently blank: useless for signing up new users. The goal of this section is to start changing this sad state of affairs by producing the signup form mocked up in Figure 7.11.

Figure 7.10: The current state of the signup page /signup.

Figure 7.11: A mockup of the user signup page.

7.2.1 Using form_for

The heart of the signup page is a form for submitting the relevant signup information (name, email, password, confirmation). We can accomplish this in Rails with the form_for helper method, which takes in an Active Record object and builds a form using the object’s attributes.

Recalling that the signup page /signup is routed to the new action in the Users controller (Listing 5.43), our first step is to create the User object required as an argument to form_for. The resulting @user variable definition appears in Listing 7.14.

Listing 7.14: Adding an @user variable to the new action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end
end

The form itself appears as in Listing 7.15. We’ll discuss it in detail in Section 7.2.2, but first let’s style it a little with the SCSS in Listing 7.16. (Note the reuse of the box_sizing mixin from Listing 7.2.) Once these CSS rules have been applied, the signup page appears as in Figure 7.12.

Listing 7.15: A form to sign up new users.
app/views/users/new.html.erb

Click here to view code image

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

Listing 7.16: CSS for the signup form.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* forms */

input, textarea, select, .uneditable-input {
  border: 1px solid #bbb;
  width: 100%;
  margin-bottom: 15px;
  @include box_sizing;
}

input {
  height: auto !important;
}

Figure 7.12: The user signup form.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In Listing 7.15, replace :name with :nome. What error message do you get as a result?

2. Confirm by replacing all occurrences of f with foobar that the name of the block variable is irrelevant as far as the result is concerned. Why might foobar nevertheless be a bad choice?

7.2.2 Signup form HTML

To understand the form defined in Listing 7.15, it’s helpful to break it into smaller pieces. We’ll first look at the outer structure, which consists of embedded Ruby opening with a call to form_for and closing with end:

Click here to view code image

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

The presence of the do keyword indicates that form_for takes a block with one variable, which we’ve called f (for “form”).

As is usually the case with Rails helpers, we don’t need to know any details about the implementation, but what we do need to know is what the f object does: when called with a method corresponding to an HTML form element—such as a text field, radio button, or password field—f returns code for that element specifically designed to set an attribute of the @user object. In other words,

<%= f.label :name %>
<%= f.text_field :name %>

creates the HTML needed to make a labeled text field element appropriate for setting the name attribute of a User model.

If you look at the HTML for the generated form by Ctrl-clicking and using the “inspect element” function of your browser, the page’s source should look something like Listing 7.17. Let’s take a moment to discuss its structure.

Listing 7.17: The HTML for the form in Figure 7.12.

Click here to view code image

<form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">
  <input name="utf8" type="hidden" value="✓" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="user_name">Name</label>
  <input id="user_name" name="user[name]" type="text" />
  <label for="user_email">Email</label>
  <input id="user_email" name="user[email]" type="email" />

  <label for="user_password">Password</label>
  <input id="user_password" name="user[password]"
         type="password" />

  <label for="user_password_confirmation">Confirmation</label>
  <input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

  <input class="btn btn-primary" name="commit" type="submit"
         value="Create my account" />
</form>

We’ll start with the internal structure of the document. Comparing Listing 7.15 with Listing 7.17, we see that the embedded Ruby

<%= f.label :name %>
<%= f.text_field :name %>

produces the HTML

Click here to view code image

<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

while

<%= f.label :email %>
<%= f.email_field :email %>

produces the HTML

Click here to view code image

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

and

Click here to view code image

<%= f.label :password %>
<%= f.password_field :password %>

produces the HTML

Click here to view code image

<label for="user_password">Password</label>
<input id="user_password" name="user[password]" type="password" />

As seen in Figure 7.13, text and email fields (type="text" and type="email") simply display their contents, whereas password fields (type="password") obscure the input for security purposes, as seen in Figure 7.13. (The benefit of using an email field is that some systems treat it differently from a text field; for example, the code type="email" will cause some mobile devices to display a special keyboard optimized for entering email addresses.)

Figure 7.13: A filled-in form with text and password fields.

As we’ll see in Section 7.4, the key to creating a user is the special name attribute in each input:

Click here to view code image

<input id="user_name" name="user[name]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />

These name values allow Rails to construct an initialization hash (via the params variable) for creating users using the values entered by the user, as we’ll see in Section 7.3.

The second important element is the form tag itself. Rails creates the form tag using the @user object: because every Ruby object knows its own class (Section 4.4.1), Rails figures out that @user is of class User; moreover, since @user is a new user, Rails knows to construct a form with the post method, which is the proper verb for creating a new object (Box 3.2):

Click here to view code image

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

Here the class and id attributes are largely irrelevant; what’s important is action="/users" and method="post". Together, these constitute instructions to issue an HTTP POST request to the /users URL. We’ll see in the next two sections what effects this has.

You may also have noticed the code that appears just inside the form tag:

Click here to view code image

<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden"
       value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />

This code, which isn’t displayed in the browser, is used internally by Rails, so it’s not important for us to understand what it does. Briefly, it uses the Unicode character ✓ (a checkmark ✓) to force browsers to submit data using the right character encoding, and then it includes an authenticity token, which Rails uses to thwart an attack called a cross-site request forgery (CSRF). Knowing when it’s OK to ignore details like this is a good mark of technical sophistication (Box 1.1).¹⁰

10. See the Stack Overflow entry on the Rails authenticity token if you’re interested in the details of how this works.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Learn Enough HTML to Be Dangerous, in which all HTML is written by hand, doesn’t cover the form tag. Why not?

7.3 Unsuccessful Signups

Although we’ve briefly examined the HTML for the form in Figure 7.12 (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.14.

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:

Click here to view code image

<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 if-else 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.

Figure 7.14: A mockup of the signup failure page.

Listing 7.18: A create action that can handle signup failure.
app/controllers/users_controller.rb

Click here to view code image

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'
    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.

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.15, and the full debug information appears in Figure 7.16.

Figure 7.15: Signup failure.

Figure 7.16: 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.16):

Click here to view code image

"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.15. For example, the value of

Click here to view code image

<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

Click here to view code image

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

is mostly equivalent to

Click here to view code image

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

In previous versions of Rails, using

Click here to view code image

@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.15 and Figure 7.16 above), 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

Click here to view code image

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

Click here to view code image

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

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      # Handle a successful save.
    else
      render 'new'
    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.17, 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.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

Figure 7.17: The signup form submitted with invalid information.

1. By hitting the URL /users/new?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:

Click here to view code image

$ 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

Click here to view code image

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@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 (Table 1.1):

$ mkdir app/views/shared

We then need to create the _error_messages.html.erb partial file using our 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

Click here to view code image

<% 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.3 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:

Click here to view code image

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

Click here to view code image

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

As a result of its use of pluralize, the code

Click here to view code image

<%= 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 apps and on the web).

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

Click here to view code image

.
.
.
/* 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.18. 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 duplicates, but luckily this issue will be fixed automatically by the addition of allow_nil: true in Section 10.1.4.)

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

Figure 7.18: Failed signup with error messages.

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.12) compare to the URL for a submitted signup form (Figure 7.18)? 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):

Click here to view code image

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

Click here to view code image

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.29). (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:

Click here to view code image

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 Ruby.

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.

Putting the above ideas together leads to the test in Listing 7.23. We’ve also included a call to assert_template to check that a failed submission re-renders the new action. 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

Click here to view code image

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_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

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

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.

2. The URLs for an unsubmitted signup form and for a submitted signup form are /signup and /users, respectively, which don’t match. This is due to our use of a custom named route in the former case (added in Listing 5.43) and a default RESTful route in the latter case (Listing 7.3). Resolve this discrepancy by adding the code shown in Listing 7.26 and Listing 7.27. Submit the new form to confirm that both cases now use the same /signup URL. Are the tests still GREEN? Why?

3. Update the post in Listing 7.25 to use the new URL from the previous exercise. Confirm that the tests are still GREEN.

4. Confirm by reverting Listing 7.27 to its previous form (Listing 7.20) that the tests are still GREEN. This is a problem, because the URL being posted to isn’t right. Add an assert_select to the test in Listing 7.25 to catch this bug and get to RED; then change the form back to Listing 7.27 to get the tests GREEN again. Hint: Test for the presence of 'form[action="/signup"]' before posting to the form in the test.

Listing 7.25: A template for tests of the error messages.
test/integration/users_signup_test.rb

Click here to view code image

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_template 'users/new'
    assert_select 'div#<CSS id for error explanation>'
    assert_select 'div.<CSS class for field with error>'
  end
  .
  .
  .
end

Listing 7.26: Adding a signup route responding to POST requests.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
  post '/signup',  to: 'users#create'
  resources :users
end

Listing 7.27: Posting to the /signup URL.
app/views/users/new.html.erb

Click here to view code image

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user, url: signup_path) 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>

7.4 Successful Signups

Having handled invalid form submissions, now it’s time to complete the signup form by actually saving a new user (if valid) to the database. First, we try to save the user; if the save succeeds, the user’s information gets written to the database automatically, and we then redirect the browser to show the user’s profile (together with a friendly greeting), as mocked up in Figure 7.19. If it fails, we simply fall back on the behavior developed in Section 7.3.

Figure 7.19: A mockup of successful signup.

7.4.1 The Finished Signup Form

To complete a working signup form, we need to fill in the commented-out section in Listing 7.19 with the appropriate behavior. Currently, the form simply freezes on valid submission, as indicated by the color change in the submission button (Figure 7.20), although this behavior may be system-dependent. This is because the default behavior for a Rails action is to render the corresponding view, and there isn’t a view template corresponding to the create action (Figure 7.21).

Figure 7.20: The frozen page on valid signup submission.

Figure 7.21: The create template error in the server log.

Although it’s possible to render a template for the create action, the usual practice is to redirect to a different page instead when the creation is successful. In particular, we’ll follow the common convention of redirecting to the newly created user’s profile, although the root path would also work. The application code, which introduces the redirect_to method, appears in Listing 7.28.

Listing 7.28: The user create action with a save and a redirect.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

Note that we’ve written

redirect_to @user

where we could have used the equivalent

redirect_to user_url(@user)

This is because Rails automatically infers from redirect_to @user that we want to redirect to user_url(@user).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the Rails console, verify that a user is in fact created when submitting valid information.

2. Confirm by updating Listing 7.28 and submitting a valid user that redirect_to user_url(@user) has the same effect as redirect_to @user.

7.4.2 The Flash

With the code in Listing 7.28, our signup form is actually working, but before submitting a valid registration in a browser, we’re going to add a bit of polish common in web applications: a message that appears on the subsequent page (in this case, welcoming our new user to the application) and then disappears upon visiting a second page or on page reload.

The Rails way to display a temporary message is to use a special method called the flash, which we can treat like a hash. Rails adopts the convention of a :success key for a message indicating a successful result (Listing 7.29).

Listing 7.29: Adding a flash message to user signup.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

By assigning a message to the flash, we are now in a position to display the message on the first page after the redirect. Our method is to iterate through the flash and insert all relevant messages into the site layout. You may recall the console example in Section 4.3.3, where we saw how to iterate through a hash using the strategically named flash variable (Listing 7.30).

Listing 7.30: Iterating through a flash hash in the console.

Click here to view code image

$ rails console
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", danger: "It failed."}
>> flash.each do |key, value|
?> puts "#{key}"
?> puts "#{value}"
>> end
success
It worked!
danger
It failed.

By following this pattern, we can arrange to display the contents of the flash site-wide using code like this:

Click here to view code image

<% flash.each do |message_type, message| %>
   <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

(This code is a particularly ugly and difficult-to-read combination of HTML and ERb; making it prettier is left as an exercise [Section 7.4.4].) Here the embedded Ruby

alert-<%= message_type %>

makes a CSS class corresponding to the type of message, so that for a :success message the class is

alert-success

(The key :success is a symbol, but embedded Ruby automatically converts it to the string "success" before inserting it into the template.) Using a different class for each key allows us to apply different styles to different kinds of messages. For example, in Section 8.1.4 we’ll use flash[:danger] to indicate a failed login attempt.¹¹ (In fact, we’ve already used alert-danger once, to style the error message div in Listing 7.21.) Bootstrap CSS supports styling for four such flash classes for increasingly urgent message types (success, info, warning, and danger), and we’ll find occasion to use all of them in the course of developing the sample application (info in Section 11.2, warning in Section 11.3, and danger for the first time in Section 8.1.4).

11. Actually, we’ll use the closely related flash.now, but we’ll defer that subtlety until we need it.

Because the message is also inserted into the template, the full HTML result for

Click here to view code image

flash[:success] = "Welcome to the Sample App!"

appears as follows:

Click here to view code image

<div class="alert alert-success">Welcome to the Sample App!</div>

Putting the embedded Ruby discussed above into the site layout leads to the code in Listing 7.31.

Listing 7.31: Adding the contents of the flash variable to the site layout.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type %>"><%= message %></div>
      <% end %>
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
    .
    .
    .
  </body>
</html>

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In the console, confirm that you can use interpolation (Section 4.2.2) to interpolate a raw symbol. For example, what is the return value of "#{:success}"?

2. How does the previous exercise relate to the flash iteration shown in Listing 7.30?

7.4.3 The First Signup

We can see the result of all this work by signing up the first user for the sample app. Even though previous submissions didn’t work properly (as shown in Figure 7.20), the user.save line in the Users controller still works, so users might still have been created. To clear them out, we’ll reset the database as follows:

Figure 7.22: Filling in the information for the first signup.

$ rails db:migrate:reset

On some systems you might have to restart the webserver (using Ctrl-C) for the changes to take effect (Box 1.1).

We’ll create the first user with the name “Rails Tutorial” and email address “example@railstutorial.org”, as shown in Figure 7.22). The resulting page (Figure 7.23) shows a friendly flash message upon successful signup, including nice green styling for the success class, which comes included with the Bootstrap CSS framework from Section 5.1.2. Then, upon reloading the user show page, the flash message disappears as promised (Figure 7.24).

Figure 7.23: The results of a successful user signup, with flash message.

Figure 7.24: The flash-less profile page after a browser reload.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the Rails console, find by the email address to double-check that the new user was actually created. The result should look something like Listing 7.32.

2. Create a new user with your primary email address. Verify that the Gravatar correctly appears.

Listing 7.32: Finding the newly created user in the database.

Click here to view code image

$ rails console
>> User.find_by(email: "example@railstutorial.org")
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.
org", created_at: "2016-05-31 17:17:33", updated_at: "2016-05-31 17:17:33",
password_digest: "$2a$10$8MaeHdnOhZvMk3GmFdmpPOeG6a7u7/k2Z9TMjOanC9G...">

7.4.4 A Test for Valid Submission

Before moving on, we’ll write a test for valid submission to verify our application’s behavior and catch regressions. As with the test for invalid submission in Section 7.3.4, our main purpose is to verify the contents of the database. In this case, we want to submit valid information and then confirm that a user was created. In analogy with Listing 7.23, which used

Click here to view code image

assert_no_difference 'User.count' do
   post users_path, ...
end

here we’ll use the corresponding assert_difference method:

Click here to view code image

assert_difference 'User.count', 1 do
   post users_path, ...
end

As with assert_no_difference, the first argument is the string 'User.count', which arranges for a comparison between User.count before and after the contents of the assert_difference block. The second (optional) argument specifies the size of the difference (in this case, 1).

Incorporating assert_difference into the file from Listing 7.23 yields the test shown in Listing 7.33. Note that we’ve used the follow_redirect! method after posting to the users path. This simply arranges to follow the redirect after submission, resulting in a rendering of the 'users/show' template. (It’s probably a good idea to write a test for the flash as well, which is left as an exercise [Section 7.4.4].)

Listing 7.33: A test for a valid signup. Green
test/integration/users_signup_test.rb

Click here to view code image

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
  end
end

Note that Listing 7.33 also verifies that the user show template renders following successful signup. For this test to work, it’s necessary for the Users routes (Listing 7.3), the Users show action (Listing 7.5), and the show.html.erb view (Listing 7.8) to work correctly. As a result, the one line

assert_template 'users/show'

is a sensitive test for almost everything related to a user’s profile page. This sort of endto-end coverage of important application features illustrates one reason why integration tests are so useful.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Write a test for the flash implemented in Section 7.4.2. How detailed you want to make your tests is up to you; a suggested ultra-minimalist template appears in Listing 7.34, which you should complete by replacing FILL_IN with the appropriate code. (Even testing for the right key, much less the text, is likely to be brittle, so I prefer only to test that the flash isn’t empty.)

2. As noted above, the flash HTML in Listing 7.31 is ugly. Verify by running the test suite that the cleaner code in Listing 7.35, which uses the Rails content_tag helper, also works.

3. Verify that the test fails if you comment out the redirect line in Listing 7.28.

4. Suppose we changed @user.save to false in Listing 7.28. How does this change verify that the assert_difference block is testing the right thing?

Listing 7.34: A template for a test of the flash.
test/integration/users_signup_test.rb

Click here to view code image

require 'test_helper'
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert_not flash.FILL_IN
  end
end

Listing 7.35: The flash ERb in the site layout using content_tag.
app/views/layouts/application.html.erb

Click here to view code image

<!DOCTYPE html>
<html>
      .
      .
      .
      <% flash.each do |message_type, message| %>
        <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
      <% end %>
      .
      .
      .
</html>

7.5 Professional-Grade Deployment

Now that we have a working signup page, it’s time to deploy our application and get it working in production. Although we started deploying our application in Chapter 3, this is the first time it will actually do something, so we’ll take this opportunity to make the deployment professional-grade. In particular, we’ll add an important feature to the production application to make signup secure, and we’ll replace the default webserver with one suitable for real-world use.

As preparation for the deployment, you should merge your changes into the master branch at this point:

Click here to view code image

$ git add -A
$ git commit -m "Finish user signup"
$ git checkout master
$ git merge sign-up

7.5.1 SSL in Production

When submitting the signup form developed in this chapter, the name, email address, and password get sent over the network, and hence are vulnerable to being intercepted by malicious users. This is a potentially serious security flaw in our application, and the way to fix it is to use Secure Sockets Layer (SSL)¹² to encrypt all relevant information before it leaves the local browser. Although we could use SSL on just the signup page, it’s actually easier to implement it site-wide, which has the additional benefits of securing user login (Chapter 8) and making our application immune to the critical session hijacking vulnerability discussed in Section 9.1.

12. Technically, SSL is now TLS, for Transport Layer Security, but everyone I know still says “SSL”.

Enabling SSL is as easy as uncommenting a single line in production.rb, the configuration file for production applications. As shown in Listing 7.36, all we need to do is set the config variable to force the use of SSL in production.

Listing 7.36: Configuring the application to use SSL in production.
config/environments/production.rb

Click here to view code image

Rails.application.configure do
  .
  .
  .
  # Force all access to the app over SSL, use Strict-Transport-Security,
  # and use secure cookies.
  config.force_ssl = true
  .
  .
  .
end

At this stage, we need to set up SSL on the remote server. Setting up a production site to use SSL involves purchasing and configuring an SSL certificate for your domain. That’s a lot of work, though, and luckily we won’t need it here: for an application running on a Heroku domain (such as the sample application), we can piggyback on Heroku’s SSL certificate. As a result, when we deploy the application in Section 7.5.2, SSL will automatically be enabled. (If you want to run SSL on a custom domain, such as www.example.com, refer to Heroku’s documentation on SSL.)

7.5.2 Production Webserver

Having added SSL, we now need to configure our application to use a webserver suitable for production applications. By default, Heroku uses a pure-Ruby webserver called WEBrick, which is easy to set up and run but isn’t good at handling significant traffic. As a result, WEBrick isn’t suitable for production use, so we’ll replace WEBrick with Puma, an HTTP server that is capable of handling a large number of incoming requests.

To add the new webserver, we simply follow the Heroku Puma documentation. The first step is to include the puma gem in our Gemfile, but as of Rails 5 Puma is included by default (Listing 3.2). This means we can skip right to the second step, which is to replace the default contents of the file config/puma.rb with the configuration shown in Listing 7.37. The code in Listing 7.37 comes straight from the Heroku documentation,¹³ and there is no need to understand it (Box 1.1).

13. Listing 7.37 changes the formatting slightly so that the code fits in the standard 80 columns.

Listing 7.37: The configuration file for the production webserver.
config/puma.rb

Click here to view code image

workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/
  # deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end

We also need to make a so-called Procfile to tell Heroku to run a Puma process in production, as shown in Listing 7.38. The Procfile should be created in your application’s root directory (i.e., in the same location as the Gemfile).

Listing 7.38: Defining a Procfile for Puma.
./Procfile

Click here to view code image

web: bundle exec puma -C config/puma.rb

7.5.3 Production Deployment

With the production webserver configuration completed, we’re ready to commit and deploy:¹⁴

14. We haven’t changed the data model in this chapter, so running the migration at Heroku shouldn’t be necessary, but only if you followed the steps in Section 6.4. Because several readers reported having trouble, I’ve added heroku run rails db:migrate as a final step just to be safe.

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push
$ git push heroku
$ heroku run rails db:migrate

The signup form is now live, and the result of a successful signup is shown in Figure 7.25. Note the presence of https:// and a lock icon in the address bar of Figure 7.25, which indicate that SSL is working.

Ruby Version Number

When deploying to Heroku, you may get a warning message like this one:

Click here to view code image

###### WARNING:
       You have not declared a Ruby version in your Gemfile.
       To set your Ruby version add this line to your Gemfile:
       ruby '2.1.5'

Experience shows that at the level of this tutorial, the costs associated with including such an explicit Ruby version number outweigh the (negligible) benefits, so you should ignore this warning for now. The main issue is that keeping your sample app and system in sync with the latest Ruby version can be a huge inconvenience,¹⁵ and yet it almost never makes a difference which exact Ruby version number you use. Nevertheless, you should bear in mind that, should you ever end up running a mission-critical app on Heroku, specifying an exact Ruby version in the Gemfile is recommended to ensure maximum compatibility between development and production environments.

15. For example, at one point Heroku required Ruby 2.1.4, so I spent several hours trying unsuccessfully to install Ruby 2.1.4 on my local machine—only to discover that Ruby 2.1.5 had been released the previous day. Attempts to install Ruby 2.1.5 then also failed.

Figure 7.25: Signing up on the live web.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm on your browser that the SSL lock and https appear.

2. Create a user on the production site using your primary email address. Does your Gravatar appear correctly?

7.6 Conclusion

Being able to sign up users is a major milestone for our application. Although the sample app has yet to accomplish anything useful, we have laid an essential foundation for all future development. In Chapter 8 and Chapter 9, we will complete our authentication machinery by allowing users to log in and out of the application (with optional “remember me” functionality). In Chapter 10, we will allow all users to update their account information, and we will allow site administrators to delete users, thereby completing the full suite of Users resource REST actions from Table 7.1.

7.6.1 What We Learned in This Chapter

• Rails displays useful debug information via the debug method.

• Sass mixins allow a group of CSS rules to be bundled and reused in multiple places.

• Rails comes with three standard environments: development, test, and production.

• We can interact with users as a resource through a standard set of REST URLs.

• Gravatars provide a convenient way of displaying images to represent users.

• The form_for helper is used to generate forms for interacting with Active Record objects.

• Signup failure renders the new user page and displays error messages automatically determined by Active Record.

• Rails provides the flash as a standard way to display temporary messages.

• Signup success creates a user in the database and redirects to the user show page, and displays a welcome message.

• We can use integration tests to verify form submission behavior and catch regressions.

• We can configure our production application to use SSL for secure communications and Puma for high performance.

Chapter 8. Basic Login

Now that new users can sign up for our site (Chapter 7), it’s time to give them the ability to log in and log out. In this chapter, we’ll implement a basic but still fully functional login system: the application will maintain the logged-in state until the browser is closed by the user. The resulting authentication system will allow us to customize the site and implement an authorization model based on login status and identity of the current user. For example, we’ll be able to update the site header with login/logout links and a profile link.

In Chapter 10, we’ll impose a security model in which only logged-in users can visit the user index page, only the correct user can access the page for editing their information, and only administrative users can delete other users from the database. Finally, in Chapter 13, we’ll use the identity of a logged-in user to create microposts associated with that user, and in Chapter 14 we’ll allow the current user to follow other users of the application (thereby receiving a feed of their microposts).

The authentication system from this chapter will also serve a foundation for the more advanced login system developed in Chapter 9. Instead of “forgetting” users on browser close, Chapter 9 will start by automatically remembering users and will then optionally remember users based on the value of a “remember me” checkbox. As a result, taken together, Chapter 8 and Chapter 9 develop all three of the most common types of login systems on the web.

8.1 Sessions

HTTP is a stateless protocol, treating each request as an independent transaction that is unable to use information from any previous requests. This means there is no way within the hypertext transfer protocol to remember a user’s identity from page to page; instead, web applications requiring user login must use a session, which is a semi-permanent connection between two computers (such as a client computer running a web browser and a server running Rails).

The most common techniques for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a user id) that can be used by the application to retrieve the logged-in user from the database. In this section and in Section 8.2, we’ll use the Rails method called session to make temporary sessions that expire automatically on browser close.¹ In Chapter 9, we’ll learn how to make longer-lived sessions using the closely related cookies method.

1. Some browsers offer an option to restore such sessions via a “continue where you left off” feature, but of course Rails has no control over this behavior.

It’s convenient to model sessions as a RESTful resource: visiting the login page will render a form for new sessions, logging in will create a session, and logging out will destroy it. Unlike the Users resource, which uses a database back-end (via the User model) to persist data, the Sessions resource will use cookies, and much of the work involved in login comes from building this cookie-based authentication machinery. In this section and the next, we’ll prepare for this work by constructing a Sessions controller, a login form, and the relevant controller actions. We’ll then complete user login in Section 8.2 by adding the necessary session-manipulation code.

As in previous chapters, we’ll do our work on a topic branch and merge in the changes at the end:

$ git checkout -b basic-login

8.1.1 Sessions Controller

The elements of logging in and out correspond to particular REST actions of the Sessions controller: the login form is handled by the new action (covered in this section), actually logging in is handled by sending a POST request to the create action (Section 8.2), and logging out is handled by sending a DELETE request to the destroy action (Section 8.3). (Recall the association of HTTP verbs with REST actions from Table 7.1.)

To get started, we’ll generate a Sessions controller with a new action (Listing 8.1).

Listing 8.1: Generating the Sessions controller.

Click here to view code image

$ rails generate controller Sessions new

(Including new actually generates views as well, which is why we don’t include actions like create and destroy that don’t correspond to views.) Following the model from Section 7.2 for the signup page, our plan is to create a login form for creating new sessions, as mocked up in Figure 8.1.

Figure 8.1: A mockup of the login form.

Unlike the Users resource, which used the special resources method to obtain a full suite of RESTful routes automatically (Listing 7.3), the Sessions resource will use only named routes, handling GET and POST requests with the login route and DELETE request with the logout route. The result appears in Listing 8.2 (which also deletes the unneeded routes generated by rails generate controller).

Listing 8.2: Adding a resource to get the standard RESTful actions for sessions. RED
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to:  'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup', to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
  resources :users
end

With the routes in Listing 8.2, we also need to update the test generated in Listing 8.1 with the new login route, as shown in Listing 8.3.

Listing 8.3: Updating the Sessions controller test to use the signup route. GREEN
test/controllers/sessions_controller_test.rb

Click here to view code image

require 'test_helper'

class SessionsControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get login_path
    assert_response :success
  end
end

The routes defined in Listing 8.2 correspond to URLs and actions similar to those for users (Table 7.1), as shown in Table 8.1.

Table 8.1: Routes provided by the sessions rules in Listing 8.2.

Since we’ve now added several custom named routes, it’s useful to look at the complete list of the routes for our application, which we can generate using rails routes:

Click here to view code image

$ rails routes
   Prefix Verb   URI Pattern               Controller#Action
     root GET    /                         static_pages#home
     help GET    /help(.:format)           static_pages#help
    about GET    /about(.:format)          static_pages#about
  contact GET    /contact(.:format)        static_pages#contact
   signup GET    /signup(.:format)         users#new
    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create
   logout DELETE /logout(.:format)         sessions#destroy
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PATCH  /users/:id(.:format)      users#update
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy

It’s not necessary to understand the results in detail, but viewing the routes in this manner gives us a high-level overview of the actions supported by our application.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. What is the difference between GET login_path and POST login_path?

2. By piping the results of rails routes to grep, list all the routes associated with the Users resource. Do the same for Sessions. How many routes does each resource have? Hint: Refer to the section on grep in Learn Enough Command Line to Be Dangerous.

8.1.2 Login Form

Having defined the relevant controller and route, now we’ll fill in the view for new sessions, i.e., the login form. Comparing Figure 8.1 with Figure 7.11, we see that the login form is similar in appearance to the signup form, except with two fields (email and password) in place of four.

As seen in Figure 8.2, when the login information is invalid we want to re-render the login page and display an error message. In Section 7.3.3, we used an error-messages partial to display error messages, but we saw in that section that those messages are provided automatically by Active Record. This won’t work for session creation errors because the session isn’t an Active Record object, so we’ll render the error as a flash message instead.

Figure 8.2: A mockup of login failure.

Recall from Listing 7.15 that the signup form uses the form_for helper, taking as an argument the user instance variable @user:

Click here to view code image

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

The main difference between the session form and the signup form is that we have no Session model, and hence no analogue for the @user variable. This means that, in constructing the new session form, we have to give form_for slightly more information; in particular, whereas

form_for(@user)

allows Rails to infer that the action of the form should be to POST to the URL /users, in the case of sessions we need to indicate the name of the resource and the corresponding URL:²

2. A second option is to use form_tag in place of form_for, which might be even more idiomatically correct Rails, but it has less in common with the signup form, and at this stage I want to emphasize the parallel structure.

Click here to view code image

form_for(:session, url: login_path)

With the proper form_for in hand, it’s easy to make a login form to match the mockup in Figure 8.1 using the signup form (Listing 7.15) as a model, as shown in Listing 8.4.

Listing 8.4: Code for the login form.
app/views/sessions/new.html.erb

Click here to view code image

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

Note that we’ve added a link to the signup page for convenience. With the code in Listing 8.4, the login form appears as in Figure 8.3. (Because the “Log in” navigation link hasn’t yet been filled in, you’ll have to type the /login URL directly into your address bar. We’ll fix this blemish in Section 8.2.3.)

Figure 8.3: The login form.

The generated form HTML appears in Listing 8.5.

Listing 8.5: HTML for the login form produced by Listing 8.4.

Click here to view code image

<form accept-charset="UTF-8" action="/login" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="text" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>

Comparing Listing 8.5 with Listing 7.17, you might be able to guess that submitting this form will result in a params hash where params[:session][:email] and params[:session][:password] correspond to the email and password fields.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Submissions from the form defined in Listing 8.4 will be routed to the Session controller’s create action. How does Rails know to do this? Hint: Refer to Table 8.1 and the first line of Listing 8.5.

8.1.3 Finding and Authenticating a User

As in the case of creating users (signup), the first step in creating sessions (login) is to handle invalid input. We’ll start by reviewing what happens when a form gets submitted, and then arrange for helpful error messages to appear in the case of login failure (as mocked up in Figure 8.2). Then we’ll lay the foundation for successful login (Section 8.2) by evaluating each login submission based on the validity of its email/password combination.

Let’s start by defining a minimalist create action for the Sessions controller, along with empty new and destroy actions (Listing 8.6). The create action in Listing 8.6 does nothing but render the new view, but it’s enough to get us started. Submitting the /sessions/new form then yields the result shown in Figure 8.4.

Listing 8.6: A preliminary version of the Sessions create action.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end

Carefully inspecting the debug information in Figure 8.4 shows that, as hinted at the end of Section 8.1.2, the submission results in a params hash containing the email and password under the key session, which (omitting some irrelevant details used internally by Rails) appears as follows:

Click here to view code image

---
session:
  email: 'user@example.com'
  password: 'foobar'
commit: Log in
action: create
controller: sessions

As with the case of user signup (Figure 7.15), these parameters form a nested hash like the one we saw in Listing 4.13. In particular, params contains a nested hash of the form

Click here to view code image

{ session: { password: "foobar", email: "user@example.com" } }

Figure 8.4: The initial failed login, with create as in Listing 8.6.

This means that

params[:session]

is itself a hash:

Click here to view code image

{ password: "foobar", email: "user@example.com" }

As a result,

params[:session][:email]

is the submitted email address and

params[:session][:password]

is the submitted password.

In other words, inside the create action the params hash has all the information needed to authenticate users by email and password. Not coincidentally, we already have exactly the methods we need: the User.find_by method provided by Active Record (Section 6.1.4) and the authenticate method provided by has_secure_ password (Section 6.3.4). Recalling that authenticate returns false for an invalid authentication (Section 6.3.4), our strategy for user login can be summarized as shown in Listing 8.7.

Listing 8.7: Finding and authenticating a user.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      # Create an error message.
      render 'new'
    end
  end

  def destroy
  end
end

The first highlighted line in Listing 8.7 pulls the user out of the database using the submitted email address. (Recall from Section 6.2.5 that email addresses are saved as all lowercase, so here we use the downcase method to ensure a match when the submitted address is valid.) The next line can be a bit confusing but is fairly common in idiomatic Rails programming:

Click here to view code image

user && user.authenticate(params[:session][:password])

This uses && (logical and) to determine if the resulting user is valid. Taking into account that any object other than nil and false itself is true in a boolean context (Section 4.2.3), the possibilities appear as in Table 8.2. We see from Table 8.2 that the if statement is true only if a user with the given email both exists in the database and has the given password, exactly as required.

Table 8.2: Possible results of user && user.authenticate(...).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the Rails console, confirm each of the values in Table 8.2. Start with user = nil, and then use user = User.first. Hint: To coerce the result to a boolean value, use the bang-bang trick from Section 4.2.3, as in !! (user && user.authenticate('foobar')).

8.1.4 Rendering with a Flash Message

Recall from Section 7.3.3 that we displayed signup errors using the User model error messages. These errors are associated with a particular Active Record object, but this strategy won’t work here because the session isn’t an Active Record model. Instead, we’ll put a message in the flash to be displayed upon failed login. A first, slightly incorrect, attempt appears in Listing 8.8.

Listing 8.8: An (unsuccessful) attempt at handling failed login.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      flash[:danger] = 'Invalid email/password combination' # Not quite right!
      render 'new'
    end
  end

  def destroy
  end
end

Because of the flash message display in the site layout (Listing 7.31), the flash[:danger] message automatically gets displayed; because of the Bootstrap CSS, it automatically gets nice styling (Figure 8.5).

Figure 8.5: The flash message for a failed login.

Unfortunately, as noted in the text and in the comment in Listing 8.8, this code isn’t quite right. The page looks fine, though, so what’s the problem? The issue is that the contents of the flash persist for one request, but—unlike a redirect, which we used in Listing 7.29—re-rendering a template with render doesn’t count as a request. The result is that the flash message persists one request longer than we want. For example, if we submit invalid login information and then click on the Home page, the flash gets displayed a second time (Figure 8.6). Fixing this blemish is the task of Section 8.1.5.

8.1.5 A Flash Test

The incorrect flash behavior is a minor bug in our application. According to the testing guidelines from Box 3.3, this is exactly the sort of situation where we should write a test to catch the error so that it doesn’t recur. We’ll thus write a short integration test for the login form submission before proceeding. In addition to documenting the bug and preventing a regression, this will also give us a good foundation for further integration tests of login and logout.

Figure 8.6: An example of flash persistence.

We start by generating an integration test for our application’s login behavior:

Click here to view code image

$ rails generate integration_test users_login
      invoke test_unit
      create   test/integration/users_login_test.rb

Next, we need a test to capture the sequence shown in Figure 8.5 and Figure 8.6. The basic steps appear as follows:

1. Visit the login path.

2. Verify that the new sessions form renders properly.

3. Post to the sessions path with an invalid params hash.

4. Verify that the new sessions form gets re-rendered and that a flash message appears.

5. Visit another page (such as the Home page).

6. Verify that the flash message doesn’t appear on the new page.

A test implementing the above steps appears in Listing 8.9.

Listing 8.9: A test to catch unwanted flash persistence. RED
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end

After adding the test in Listing 8.9, the login test should be RED:

Listing 8.10: RED

Click here to view code image

$ rails test test/integration/users_login_test.rb

This shows how to run one (and only one) test file using rails test and the full path to the file.

The way to get the failing test in Listing 8.9 to pass is to replace flash with the special variant flash.now, which is specifically designed for displaying flash messages on rendered pages. Unlike the contents of flash, the contents of flash.now disappear as soon as there is an additional request, which is exactly the behavior we’ve tested in Listing 8.9. With this substitution, the corrected application code appears as in Listing 8.11.

Listing 8.11: Correct code for failed login. GREEN
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

We can then verify that both the login integration test and the full test suite are GREEN:

Listing 8.12: GREEN

Click here to view code image

$ rails test test/integration/users_login_test.rb
$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify in your browser that the sequence from Section 8.1.4 works correctly, i.e., that the flash message disappears when you click on a second page.

8.2 Logging In

Now that our login form can handle invalid submissions, the next step is to handle valid submissions correctly by actually logging a user in. In this section, we’ll log the user in with a temporary session cookie that expires automatically upon browser close. In Section 9.1, we’ll add sessions that persist even after closing the browser.

Implementing sessions will involve defining a large number of related functions for use across multiple controllers and views. You may recall from Section 4.2.5 that Ruby provides a module facility for packaging such functions in one place. Conveniently, a Sessions helper module was generated automatically when generating the Sessions controller (Section 8.1.1). Moreover, such helpers are automatically included in Rails views; by including the module into the base class of all controllers (the Application controller), we arrange to make them available in our controllers as well (Listing 8.13).³

3. I like this technique because it connects to the pure Ruby way of including modules, but Rails 4 introduced a technique called concerns that can also be used for this purpose. To learn how to use concerns, run a search for “how to use concerns in Rails”.

Listing 8.13: Including the Sessions helper module into the Application controller.
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

With this configuration complete, we’re now ready to write the code to log users in.

8.2.1 The log_in Method

Logging a user in is simple with the help of the session method defined by Rails. (This method is separate and distinct from the Sessions controller generated in Section 8.1.1.) We can treat session as if it were a hash and assign to it as follows:

session[:user_id] = user.id

This places a temporary cookie on the user’s browser containing an encrypted version of the user’s id, which allows us to retrieve the id on subsequent pages using session[:user_id]. In contrast to the persistent cookie created by the cookies method (Section 9.1), the temporary cookie created by the session method expires immediately when the browser is closed.

Because we’ll want to use the same login technique in a couple of different places, we’ll define a method called log_in in the Sessions helper, as shown in Listing 8.14.

Listing 8.14: The log_in function.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
end

Because temporary cookies created using the session method are automatically encrypted, the code in Listing 8.14 is secure, and there is no way for an attacker to use the session information to log in as the user. This applies only to temporary sessions initiated with the session method, though, and is not the case for persistent sessions created using the cookies method. Permanent cookies are vulnerable to a session hijacking attack, so in Chapter 9 we’ll have to be much more careful about the information we place on the user’s browser.

With the log_in method defined in Listing 8.14, we’re now ready to complete the session create action by logging the user in and redirecting to the user’s profile page. The result appears in Listing 8.15.⁴

4. The log_in method is available in the Sessions controller because of the module inclusion in Listing 8.13.

Listing 8.15: Logging in a user.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

Note the compact redirect

redirect_to user

which we saw before in Section 7.4.1. Rails automatically converts this to the route for the user’s profile page:

user_url(user)

With the create action defined in Listing 8.15, the login form defined in Listing 8.4 should now be working. It doesn’t have any effects on the application display, though, so short of inspecting the browser session directly, there’s no way to tell that you’re logged in. As a first step toward enabling more visible changes, in Section 8.2.2 we’ll retrieve the current user from the database using the id in the session. In Section 8.2.3, we’ll change the links on the application layout, including a URL to the current user’s profile.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Log in with a valid user and inspect your browser’s cookies. What is the value of the session content? Hint: If you don’t know how to view your browser’s cookies, Google for it (Box 1.1).

2. What is the value of the Expires attribute from the previous exercise?

8.2.2 Current User

Having placed the user’s id securely in the temporary session, we are now in a position to retrieve it on subsequent pages, which we’ll do by defining a current_user method to find the user in the database corresponding to the session id. The purpose of current_user is to allow constructions such as

<%= current_user.name %>

and

redirect_to current_user

To find the current user, one possibility is to use the find method, as on the user profile page (Listing 7.5):

Click here to view code image

User.find(session[:user_id])

But recall from Section 6.1.4 that find raises an exception if the user id doesn’t exist. This behavior is appropriate on the user profile page because it will only happen if the id is invalid, but in the present case session[:user_id] will often be nil (i.e., for non-logged-in users). To handle this possibility, we’ll use the same find_by method used to find by email address in the create method, with id in place of email:

User.find_by(id: session[:user_id])

Rather than raising an exception, this method returns nil (indicating no such user) if the id is invalid.

We could now define the current_user method as follows:

Click here to view code image

def current_user
  User.find_by(id: session[:user_id])
end

This would work fine, but it would hit the database multiple times if, e.g., current_user appeared multiple times on a page. Instead, we’ll follow a common Ruby convention by storing the result of User.find_by in an instance variable, which hits the database the first time but returns the instance variable immediately on subsequent invocations:⁵

5. This practice of remembering variable assignments from one method invocation to the next is known as memoization. (Note that this is a technical term; in particular, it’s not a misspelling of “memorization”—a subtlety lost on the hapless copyeditor of a previous edition of this book.)

Click here to view code image

if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
end

Recalling the or operator || seen in Section 4.2.3, we can rewrite this as follows:

Click here to view code image

@current_user = @current_user || User.find_by(id: session[:user_id])

Because a User object is true in a boolean context, the call to find_by only gets executed if @current_user hasn’t yet been assigned.

Although the preceding code would work, it’s not idiomatically correct Ruby; instead, the proper way to write the assignment to @current_user is like this:

Click here to view code image

@current_user ||= User.find_by(id: session[:user_id])

This uses the potentially confusing but frequently used ||= (“or equals”) operator (Box 8.1).

Box 8.1: What the *$@! is ||= ?

The ||= (“or equals”) assignment operator is a common Ruby idiom and is thus important for aspiring Rails developers to recognize. Although at first it may seem mysterious, or equals is easy to understand by analogy.

We start by noting the common pattern of incrementing a variable:

x = x + 1

Many languages provide a syntactic shortcut for this operation; in Ruby (and in C, C++, Perl, Python, Java, etc.), it can also appear as follows:

x += 1

Analogous constructs exist for other operators as well:

$ rails console
>> x = 1
=> 1
>> x += 1
=> 2
>> x *= 3
=> 6
>> x -= 8
=> -2
>> x /= 2
=> -1

In each case, the pattern is that x = x O y and x O= y are equivalent for any operator O.

Another common Ruby pattern is assigning to a variable if it’s nil but otherwise leaving it alone. Recalling the or operator || seen in Section 4.2.3, we can write this as follows:

>> @foo
=> nil
>> @foo = @foo || "bar"
=> "bar"
>> @foo = @foo || "baz"
=> "bar"

Since nil is false in a boolean context, the first assignment to @foo is nil || "bar", which evaluates to "bar". Similarly, the second assignment is @foo || "baz", i.e., "bar" || "baz", which also evaluates to "bar". This is because anything other than nil or false is true in a boolean context, and the series of || expressions terminates after the first true expression is evaluated. (This practice of evaluating || expressions from left to right and stopping on the first true value is known as short-circuit evaluation. The same principle applies to && statements, except in this case evaluation stops on the first false value.)

Comparing the console sessions for the various operators, we see that @foo = @foo || "bar" follows the x = x O y pattern with || in the place of O:

x    =   x   +   1     ->    x    +=    1
x    =   x   *   3     ->    x    *=    3
x    =   x   -   8     ->    x    -=    8
x    =   x   /   2     ->    x    /=    2
@foo = @foo || "bar"   ->    @foo ||= "bar"

Thus we see that @foo = @foo || "bar" and @foo ||= "bar" are equivalent. In the context of the current user, this suggests the following construction:

@current_user ||= User.find_by(id: session[:user_id])

Voilà!

(Technically, Ruby evaluates the expression @foo || @foo = "bar", which avoids an unnecessary assignment when @foo is nil or false. But this expression doesn’t explain the ||= notation as well, so the above discussion uses the nearly equivalent @foo = @foo || "bar".)

Applying the results of the above discussion yields the succinct current_ user method shown in Listing 8.16.

Listing 8.16: Finding the current user in the session.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Returns the current logged-in user (if any).
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

With the working current_user method in Listing 8.16, we’re now in a position to make changes to our application based on user login status.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm at the console that User.find_by(id: ...) returns nil when the corresponding user doesn’t exist.

2. In a Rails console, create a session hash with key :user_id. By following the steps in Listing 8.17, confirm that the ||= operator works as required.

Listing 8.17: Simulating session in the console.

Click here to view code image

>> session = {}
>> session[:user_id] = nil
>> @current_user ||= User.find_by(id: session[:user_id])
<What happens here?>
>> session[:user_id]= User.first.id
>> @current_user ||= User.find_by(id: session[:user_id])
<What happens here?>
>> @current_user ||= User.find_by(id: session[:user_id])
<What happens here?>

8.2.3 Changing the Layout Links

The first practical application of logging in involves changing the layout links based on login status. In particular, as seen in the Figure 8.7 mockup,⁶ we’ll add links for logging out, for user settings, for listing all users, and for the current user’s profile page. Note in Figure 8.7 that the logout and profile links appear in a dropdown “Account” menu; we’ll see in Listing 8.19 how to make such a menu with Bootstrap.

6. Image retrieved from https://www.flickr.com/photos/elevy/14730820387 on 2016-06-03. Copyright © 2014 by Elias Levy and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

At this point, in real life I would consider writing an integration test to capture the behavior described above. As noted in Box 3.3, as you become more familiar with the testing tools in Rails you may find yourself more inclined to write tests first. In this case, though, such a test involves several new ideas, so for now it’s best deferred to its own section (Section 8.2.4).

The way to change the links in the site layout involves using an if-else statement inside embedded Ruby to show one set of links if the user is logged in and another set of links otherwise:

Click here to view code image

<% if logged_in? %>
  # Links for logged-in users
<% else %>
  # Links for non-logged-in-users
<% end %>

This kind of code requires the existence of a logged_in? boolean method, which we’ll now define.

A user is logged in if there is a current user in the session, i.e., if current_ user is not nil. Checking for this requires the use of the “not” operator (Section 4.2.3), written using an exclamation point ! and usually read as “bang”. The resulting logged_in? method appears in Listing 8.18.

Figure 8.7: A mockup of the user profile after a successful login.

Listing 8.18: The logged_in? helper method.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Returns the current logged-in user (if any).
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  # Returns true if the user is logged in, false otherwise.
  def logged_in?
    !current_user.nil?
  end
end

With the addition in Listing 8.18, we’re now ready to change the layout links if a user is logged in. There are four new links, two of which are stubbed out (to be completed in Chapter 10):

Click here to view code image

<%= link_to "Users",    '#' %>
<%= link_to "Settings", '#' %>

The logout link, meanwhile, uses the logout path defined in Listing 8.2:

Click here to view code image

<%= link_to "Log out", logout_path, method: :delete %>

Notice that the logout link passes a hash argument indicating that it should submit with an HTTP DELETE request.⁷ We’ll also add a profile link as follows:

7. Web browsers can’t actually issue DELETE requests; Rails fakes it with JavaScript.

Click here to view code image

<%= link_to "Profile", current_user %>

Here we could write

Click here to view code image

<%= link_to "Profile", user_path(current_user) %>

but as usual Rails allows us to link directly to the user by automatically converting current_user into user_path(current_user) in this context. Finally, when users aren’t logged in, we’ll use the login path defined in Listing 8.2 to make a link to the login form:

Click here to view code image

<%= link_to "Log in", login_path %>

Putting everything together gives the updated header partial shown in Listing 8.19.

Listing 8.19: Changing the layout links for logged-in users.
app/views/layouts/_header.html.erb

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", "#" %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", "#" %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: "delete" %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

As part of including the new links into the layout, Listing 8.19 takes advantage of Bootstrap’s ability to make dropdown menus.⁸ Note in particular the inclusion of the special Bootstrap CSS classes such as dropdown, dropdown-menu, etc. To activate the dropdown menu, we need to include Bootstrap’s custom JavaScript library in the Rails asset pipeline’s application.js file, as shown in Listing 8.20.

8. See the Bootstrap components page for more information.

Listing 8.20: Adding the Bootstrap JavaScript library to application.js.
app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .

At this point, you should visit the login path and log in as a valid user (username example@railstutorial.org, password foobar), which effectively tests the code in the previous three sections.⁹ With the code in Listing 8.19 and Listing 8.20, you should see the dropdown menu and links for logged-in users, as shown in Figure 8.8.

9. You may have to restart the webserver to get this to work (Box 1.1).

If you quit your browser completely, you should also be able to verify that the application forgets your login status, requiring you to log in again to see the changes described above.¹⁰

10. If you’re using the cloud IDE, I recommend using a different browser to test the login behavior so that you don’t have to close down the browser running the IDE.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the cookie inspector in your browser (Section 8.2.1), remove the session cookie and confirm that the layout links revert to the non-logged-in state.

2. Log in again, confirming that the layout links change correctly. Then quit your browser and start it again to confirm that the layout links revert to the non-logged-in state. (If your browser has a “remember where I left off ” feature that automatically restores the session, be sure to disable it in this step [Box 1.1].)

Figure 8.8: A logged-in user with new links and a dropdown menu.

8.2.4 Testing Layout Changes

Having verified by hand that the application is behaving properly upon successful login, before moving on we’ll write an integration test to capture that behavior and catch regressions. We’ll build on the test from Listing 8.9 and write a series of steps to verify the following sequence of actions:

1. Visit the login path.

2. Post valid information to the sessions path.

3. Verify that the login link disappears.

4. Verify that a logout link appears

5. Verify that a profile link appears.

In order to see these changes, our test needs to log in as a previously registered user, which means that such a user must already exist in the database. The default Rails way to do this is to use fixtures, which are a way of organizing data to be loaded into the test database. We discovered in Section 6.2.5 that we needed to delete the default fixtures so that our email uniqueness tests would pass (Listing 6.31). Now we’re ready to start filling in that empty file with custom fixtures of our own.

In the present case, we need only one user, whose information should consist of a valid name and email address. Because we’ll need to log the user in, we also have to include a valid password to compare with the password submitted to the Sessions controller’s create action. Referring to the data model in Figure 6.8, we see that this means creating a password_digest attribute for the user fixture, which we’ll accomplish by defining a digest method of our own.

As discussed in Section 6.3.1, the password digest is created using bcrypt (via has_secure_password), so we’ll need to create the fixture password using the same method. By inspecting the secure password source code, we find that this method is

Click here to view code image

BCrypt::Password.create(string, cost: cost)

where string is the string to be hashed and cost is the cost parameter that determines the computational cost to calculate the hash. Using a high cost makes it computationally intractable to use the hash to determine the original password, which is an important security precaution in a production environment, but in tests we want the digest method to be as fast as possible. The secure password source code has a line for this as well:

Click here to view code image

cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost

This rather obscure code, which you don’t need to understand in detail, arranges for precisely the behavior described above: it uses the minimum cost parameter in tests and a normal (high) cost parameter in production. (We’ll learn more about the strange ?-: notation in Section 9.2.)

There are several places we could put the resulting digest method, but we’ll have an opportunity in Section 9.1.1 to reuse digest in the User model. This suggests placing the method in user.rb. Because we won’t necessarily have access to a user object when calculating the digest (as will be the case in the fixtures file), we’ll attach the digest method to the User class itself, which (as we saw briefly in Section 4.4.1) makes it a class method. The result appears in Listing 8.21.

Listing 8.21: Adding a digest method for use in fixtures.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end

With the digest method from Listing 8.21, we are now ready to create a user fixture for a valid user, as shown in Listing 8.22.¹¹

11. It’s worth noting that indentation in fixture files must take the form of spaces, not tabs, so take care when copying code like that shown in Listing 8.22.

Listing 8.22: A fixture for testing user login.
test/fixtures/users.yml

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

Note in particular that fixtures support embedded Ruby, which allows us to use

Click here to view code image

<%= User.digest('password') %>

to create the valid password digest for the test user.

Although we’ve defined the password_digest attribute required by has_ secure_password, sometimes it’s convenient to refer to the plain (virtual) password as well. Unfortunately, this is impossible to arrange with fixtures, and adding a password attribute to Listing 8.22 causes Rails to complain that there is no such column in the database (which is true). We’ll make do by adopting the convention that all fixture users have the same password ('password').

Having created a fixture with a valid user, we can retrieve it inside a test as follows:

user = users(:michael)

Here users corresponds to the fixture filename users.yml, while the symbol :michael references user with the key shown in Listing 8.22.

With the fixture user as above, we can now write a test for the layout links by converting the sequence enumerated at the beginning of this section into code, as shown in Listing 8.23.

Listing 8.23: A test for user logging in with valid information. GREEN
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with valid information" do
    get login_path
    post login_path, params: { session: { email: @user.email,
                                          password: 'password' } }
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
end

Here we’ve used

assert_redirected_to @user

to check the right redirect target and

follow_redirect!

to actually visit the target page. Listing 8.23 also verifies that the login link disappears by verifying that there are zero login path links on the page:

Click here to view code image

assert_select "a[href=?]", login_path, count: 0

By including the extra count: 0 option, we tell assert_select that we expect there to be zero links matching the given pattern. (Compare this to count: 2 in Listing 5.32, which checks for exactly two matching links.)

Because the application code was already working, this test should be GREEN:

Listing 8.24: GREEN

Click here to view code image

$ rails test test/integration/users_login_test.rb \
>            --name test_login_with_valid_information

This shows how to run a specific test within a test file by passing the option

Click here to view code image

--name test_login_with_valid_information

containing the name of the test. (A test’s name is just the word “test” and the words in the test description joined using underscores.) Note that the > on the second line is a “line continuation” character inserted automatically by the shell and should not be typed literally.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Delete the bang ! in the Session helper’s logged_in? method and confirm that the test in Listing 8.23 is RED.

2. Restore the ! to get back to GREEN.

8.2.5 Login Upon Signup

Although our authentication system is now working, newly registered users might be confused, as they are not logged in by default. Because it would be strange to force users to log in immediately after signing up, we’ll log in new users automatically as part of the signup process. To arrange this behavior, all we need to do is add a call to log_in in the Users controller create action, as shown in Listing 8.25.¹²

12. As with the Sessions controller, the log_in method is available in the Users controller because of the module inclusion in Listing 8.13.

Listing 8.25: Logging in the user upon signup.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

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

To test the behavior from Listing 8.25, we can add a line to the test from Listing 7.33 to check that the user is logged in. It’s helpful in this context to define an is_logged_in? helper method to parallel the logged_in? helper defined in Listing 8.18, which returns true if there’s a user id in the (test) session and false otherwise (Listing 8.26). (Because helper methods aren’t available in tests, we can’t use the current_user as in Listing 8.18, but the session method is available, so we use that instead.) Here we use is_logged_in? instead of logged_in? so that the test helper and Sessions helper methods have different names, which prevents them from being mistaken for each other.¹³

13. For example, I once had a test suite that was GREEN even after accidentally deleting the main log_in method in the Sessions helper. The reason is that the tests were happily using a test helper with the same name, thereby passing even though the application was completely broken. As with is_logged_in?, we’ll avoid this issue by defining the test helper log_in_as in Listing 9.24.

Listing 8.26: A boolean method for login status inside tests.
test/test_helper.rb

Click here to view code image

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end
end

With the code in Listing 8.26, we can assert that the user is logged in after signup using the line shown in Listing 8.27.

Listing 8.27: A test of login after signup. GREEN
test/integration/users_signup_test.rb

Click here to view code image

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name: "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end

At this point, the test suite should still be GREEN:

Listing 8.28: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Is the test suite RED or GREEN if you comment out the log_in line in Listing 8.25?

2. By using your text editor’s ability to comment out code, toggle back and forth between commenting out code in Listing 8.25 and confirm that the test suite toggles between RED and GREEN. (You will need to save the file between toggles.)

8.3 Logging Out

As discussed in Section 8.1, our authentication model is to keep users logged in until they log out explicitly. In this section, we’ll add this necessary logout capability. Because the “Log out” link has already been defined (Listing 8.19), all we need is to write a valid controller action to destroy user sessions.

So far, the Sessions controller actions have followed the RESTful convention of using new for a login page and create to complete the login. We’ll continue this theme by using a destroy action to delete sessions, i.e., to log out. Unlike the login functionality, which we use in both Listing 8.15 and Listing 8.25, we’ll only be logging out in one place, so we’ll put the relevant code directly in the destroy action. As we’ll see in Section 9.3, this design (with a little refactoring) will also make the authentication machinery easier to test.

Logging out involves undoing the effects of the log_in method from Listing 8.14, which involves deleting the user id from the session.¹⁴ To do this, we use the delete method as follows:

14. Some browsers offer a “remember where I left off” feature, which restores the session automatically, so be sure to disable any such feature before trying to log out.

session.delete(:user_id)

We’ll also set the current user to nil, although in the present case this won’t matter because of an immediate redirect to the root URL.¹⁵ As with log_in and associated methods, we’ll put the resulting log_out method in the Sessions helper module, as shown in Listing 8.29.

15. Setting @current_user to nil would only matter if @current_user were created before the destroy action (which it isn’t) and if we didn’t issue an immediate redirect (which we do). This is an unlikely combination of events, and with the application as presently constructed it isn’t necessary, but because it’s security-related I include it for completeness.

Listing 8.29: The log_out method.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

We can put the log_out method to use in the Sessions controller’s destroy action, as shown in Listing 8.30.

Listing 8.30: Destroying a session (user logout).
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

To test the logout machinery, we can add some steps to the user login test from Listing 8.23. After logging in, we use delete to issue a DELETE request to the logout path (Table 8.1) and verify that the user is logged out and redirected to the root URL. We also check that the login link reappears and that the logout and profile links disappear. The new steps appear in Listing 8.31.

Listing 8.31: A test for user logout. GREEN
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

(Now that we have is_logged_in? available in tests, we’ve also thrown in a bonus assert is_logged_in? immediately after posting valid information to the sessions path.)

With the session destroy action thus defined and tested, the initial signup/login/ logout triumvirate is complete, and the test suite should be GREEN:

Listing 8.32: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm in a browser that the “Log out” link causes the correct changes in the site layout. What is the correspondence between these changes and the final three steps in Listing 8.31?

2. By checking the site cookies, confirm that the session is correctly removed after logging out.

8.4 Conclusion

With the material in this chapter, our sample application has a fully functional login and authentication system. In the next chapter, we’ll take our app to the next level by adding the ability to remember users for longer than a single browser session.

Before moving on, merge your changes back into the master branch:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Implement basic login"
$ git checkout master
$ git merge basic-login

Then push up to the remote repository:

$ rails test
$ git push

Finally, deploy to Heroku as usual:

$ git push heroku

8.4.1 What We Learned in This Chapter

• Rails can maintain state from one page to the next using temporary cookies via the session method.

• The login form is designed to create a new session to log a user in.

• The flash.now method is used for flash messages on rendered pages.

• Test-driven development is useful when debugging by reproducing the bug in a test.

• Using the session method, we can securely place a user id on the browser to create a temporary session.

• We can change features such as links on the layouts based on login status.

• Integration tests can verify correct routes, database updates, and proper changes to the layout.

Chapter 9. Advanced Login

The basic login system developed in Chapter 8 is fully functional, but most modern websites include the ability to “remember” users when they visit the site again even if they’ve closed their browsers in the interim. In this chapter, we use permanent cookies to implement this behavior. We’ll start by automatically remembering users when they log in (Section 9.1), a common model used by sites such as Bitbucket and GitHub. We’ll then add the ability to optionally remember users using a “remember me” checkbox, a model used by sites such as Twitter and Facebook.

Because the Chapter 8 login system is complete by itself, the core of the sample application will work fine without it, and if desired you can skip right to Chapter 10 (and from there to Chapter 13). On the other hand, learning how to implement the “remember me” feature is both highly instructive by itself and lays an essential foundation for account activation (Chapter 11) and password reset (Chapter 12). Moreover, the result is an outstanding example of computer magic: You’ve seen a billion of these “remember me” login forms on the web, and now’s your chance to learn how to make one.

9.1 Remember Me

In this section, we’ll add the ability to remember our users’ login state even after they close and reopen their browsers. This “remember me” behavior will happen automatically, and users will automatically stay logged in until they explicitly log out. As we’ll see, the resulting machinery will make it easy to add an optional “remember me” checkbox as well (Section 9.2).

As usual, I suggest switching to a topic branch before proceeding:

Click here to view code image

$ git checkout -b advanced-login

9.1.1 Remember Token and Digest

In Section 8.2, we used the Rails session method to store the user’s id, but this information disappears when the user closes their browser. In this section, we’ll take the first step toward persistent sessions by generating a remember token appropriate for creating permanent cookies using the cookies method, together with a secure remember digest for authenticating those tokens.

As noted in Section 8.2.1, information stored using session is automatically secure, but this is not the case with information stored using cookies. In particular, persistent cookies are vulnerable to session hijacking, in which an attacker uses a stolen remember token to log in as a particular user. There are four main ways to steal cookies: (1) using a packet sniffer to detect cookies being passed over insecure networks,¹ (2) compromising a database containing remember tokens, (3) using cross-site scripting (XSS), and (4) gaining physical access to a machine with a logged-in user. We prevented the first problem in Section 7.5 by using Secure Sockets Layer (SSL) site-wide, which protects network data from packet sniffers. We’ll prevent the second problem by storing a hash digest of the remember tokens instead of the token itself, in much the same way that we stored password digests instead of raw passwords in Section 6.3.² Rails automatically prevents the third problem by escaping any content inserted into view templates. Finally, although there’s no iron-clad way to stop attackers who have physical access to a logged-in computer, we’ll minimize the fourth problem by changing tokens every time a user logs out and by taking care to cryptographically sign any potentially sensitive information we place on the browser.

1. Session hijacking was widely publicized by the Firesheep application, which showed that remember tokens at many high-profile sites were visible when connected to public Wi-Fi networks.

2. Rails 5 introduced a has_secure_token method that automatically generates random tokens, but it stores the unhashed values in the database, and hence is unsuitable for our present purposes.

With these design and security considerations in mind, our plan for creating persistent sessions appears as follows:

1. Create a random string of digits for use as a remember token.

2. Place the token in the browser cookies with an expiration date far in the future.

3. Save the hash digest of the token to the database.

4. Place an encrypted version of the user’s id in the browser cookies.

5. When presented with a cookie containing a persistent user id, find the user in the database using the given id and verify that the remember token cookie matches the associated hash digest from the database.

Note how similar the final step is to logging a user in, where we retrieve the user by email address and then verify (using the authenticate method) that the submitted password matches the password digest (Listing 8.7). As a result, our implementation will parallel aspects of has_secure_password.

We’ll start by adding the required remember_digest attribute to the User model, as shown in Figure 9.1.

To add the data model from Figure 9.1 to our application, we’ll generate a migration:

Click here to view code image

$ rails generate migration add_remember_digest_to_users remember_digest:string

(Compare to the password digest migration in Section 6.3.1.) As in previous migrations, we’ve used a migration name that ends in _to_users to tell Rails that the migration is designed to alter the users table in the database. Because we also included the attribute (remember_digest) and type (string), Rails generates a default migration for us, as shown in Listing 9.1.

Figure 9.1: The User model with an added remember_digest attribute.

Listing 9.1: The generated migration for the remember digest.
db/migrate/[timestamp]_add_remember_digest_to_users.rb

Click here to view code image

class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

Because we don’t expect to retrieve users by remember digest, there’s no need to put an index on the remember_digest column, and we can use the default migration as generated above:

$ rails db:migrate

Now we have to decide what to use as a remember token. There are many mostly equivalent possibilities—essentially, any long random string will do. The urlsafe_base64 method from the SecureRandom module in the Ruby standard library fits the bill:³ it returns a random string of length 22 composed of the characters A–Z, a–z, 0–9, “-”, and “_” (for a total of 64 possibilities, thus “base64”). A typical base64 string appears as follows:

3. This choice is based on the RailsCast on remember me.

Click here to view code image

$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"

Just as it’s perfectly fine if two users have the same password,⁴ there’s no need for remember tokens to be unique, but it’s more secure if they are.⁵ In the case of the base64 string above, each of the 22 characters has 64 possibilities, so the probability of two remember tokens colliding is a negligibly small 1/64²² = 2⁻¹³² ≈ 10⁻⁴⁰.⁶ As a bonus, by using base64 strings specifically designed to be safe in URLs (as indicated by the name urlsafe_base64), we’ll be able to use the same token generator to make account activation and password reset links in Chapter 12.

4. In any case, with bcrypt’s salted hashes there’s no way for us to tell if two users’ passwords match.

5. With unique remember tokens, an attacker always needs both the user id and the remember token cookies to hijack the session.

6. This hasn’t stopped some developers from adding a check to verify that no collision has occurred, but such efforts result from failing to grasp just how small 10⁻⁴⁰ is. For example, if we generated a billion tokens a second for the entire age of the Universe, the expected number of collisions would still be on the order of 10⁻²³.

Remembering users involves creating a remember token and saving the digest of the token to the database. We’ve already defined a digest method for use in the test fixtures (Listing 8.21), and we can use the results of the discussion above to create a new_token method to create a new token. As with digest, the new token method doesn’t need a user object, so we’ll make it a class method.⁷ The result is the User model shown in Listing 9.2.

7. As a general rule, if a method doesn’t need an instance of an object, it should be a class method. Indeed, this decision will prove to be wise in Section 11.2.

Listing 9.2: Adding a method for generating tokens.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
  SecureRandom.urlsafe_base64
  end
end

Our plan for the implementation is to make a user.remember method that associates a remember token with the user and saves the corresponding remember digest to the database. Because of the migration in Listing 9.1, the User model already has a remember_digest attribute, but it doesn’t yet have a remember_token attribute. We need a way to make a token available via user.remember_token (for storage in the cookies) without storing it in the database. We solved a similar issue with secure passwords in Section 6.3, which paired a virtual password attribute with a secure password_digest attribute in the database. In that case, the virtual password attribute was created automatically by has_secure_password, but we’ll have to write the code for a remember_token ourselves. The way to do this is to use attr_accessor to create an accessible attribute, which we saw before in Section 4.4.5:

Click here to view code image

class User < ApplicationRecord
   attr_accessor :remember_token
  .
  .
  .
  def remember
    self.remember_token = ...
    update_attribute(:remember_digest, ...)

   end
end

Note the form of the assignment in the first line of the remember method. Because of the way Ruby handles assignments inside objects, without self the assignment would create a local variable called remember_token, which isn’t what we want. Using self ensures that assignment sets the user’s remember_token attribute. (Now you know why the before_save callback from Listing 6.32 uses self.email instead of just email.) Meanwhile, the second line of remember uses the update_attribute method to update the remember digest. (As noted in Section 6.1.5, this method bypasses the validations, which is necessary in this case because we don’t have access to the user’s password or confirmation.)

With these considerations in mind, we can create a valid token and associated digest by first making a new remember token using User.new_token, and then updating the remember digest with the result of applying User.digest. This procedure gives the remember method shown in Listing 9.3.

Listing 9.3: Adding a remember method to the User model. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
    attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember

    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In the console, assign user to the first user in the database, and verify by calling it directly that the remember method works. How do remember_token and remember_digest compare?

2. In Listing 9.3, we defined the new token and digest class methods by explicitly prefixing them with User. This works fine and, because they are actually called using User.new_token and User.digest, it is probably the clearest way to define them. But there are two perhaps more idiomatically correct ways to define class methods, one slightly confusing and one extremely confusing. By running the test suite, verify that the implementations in Listing 9.4 (slightly confusing) and Listing 9.5 (extremely confusing) are correct. (Note that, in the context of Listing 9.4 and Listing 9.5, self is the User class, whereas the other uses of self in the User model refer to a user object instance. This is part of what makes them confusing.)

Listing 9.4: Defining the new token and digest methods using self. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns the hash digest of the given string.
  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def self.new_token
    SecureRandom.urlsafe_base64
  end
  .
  .
  .
end

Listing 9.5: Defining the new token and digest methods using class << self. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  class << self
    # Returns the hash digest of the given string.
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end
  # Returns a random token.
  def new_token
    SecureRandom.urlsafe_base64
  end
end
.
.
.

9.1.2 Login with Remembering

Having created a working user.remember method, we’re now in a position to create a persistent session by storing a user’s (encrypted) id and remember token as permanent cookies on the browser. The way to do this is with the cookies method, which (as with session) we can treat as a hash. A cookie consists of two pieces of information, a value and an optional expires date. For example, we could make a persistent session by creating a cookie with value equal to the remember token that expires 20 years from now:

Click here to view code image

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

(This uses one of the convenient Rails time helpers, as discussed in Box 9.1.) This pattern of setting a cookie that expires 20 years in the future is so common that Rails has a special permanent method to implement it, so that we can simply write

Click here to view code image

cookies.permanent[:remember_token] = remember_token

This causes Rails to set the expiration to 20.years.from_now automatically.

Box 9.1: Cookies Expire 20.years.from_now

You may recall from Section 4.4.2 that Ruby lets you add methods to any class, even built-in ones. In that section, we added a palindrome? method to the String class (and discovered as a result that "deified" is a palindrome), and we also saw how Rails adds a blank? method to class Object (so that "".blank?, " ".blank?, and nil.blank? are all true). The cookies.permanent method, which creates “permanent” cookies with an expiration 20.years.from_now, gives yet another example of this practice through one of Rails’ time helpers, which are methods added to Fixnum (the base class for integers):

$ rails console
>> 1.year.from_now
=> Wed, 21 Jun 2017 19:36:29 UTC +00:00
>> 10.weeks.ago
=> Tue, 12 Apr 2016 19:36:44 UTC +00:00

Rails adds other helpers, too:

>> 1.kilobyte
=> 1024
>> 5.megabytes
=> 5242880

These are useful for upload validations, making it easy to restrict, say, image uploads to 5.megabytes.

Although it should be used with caution, the flexibility to add methods to built-in classes allows for extraordinarily natural additions to plain Ruby. Indeed, much of the elegance of Rails ultimately derives from the malleability of the underlying Ruby language.

To store the user’s id in the cookies, we could follow the pattern used with the session method (Listing 8.14) using something like

cookies[:user_id] = user.id

Because it places the id as plain text, this method exposes the form of the application’s cookies and makes it easier for an attacker to compromise user accounts. To avoid this problem, we’ll use a signed cookie, which securely encrypts the cookie before placing it on the browser⁸:

8. Signing and encrypting are different operations in general, but as of Rails 4 the signed method does both by default.

Click here to view code image

cookies.signed[:user_id] = user.id

Because we want the user id to be paired with the permanent remember token, we should make it permanent as well, which we can do by chaining the signed and permanent methods:

Click here to view code image

cookies.permanent.signed[:user_id] = user.id

After the cookies are set, on subsequent page views we can retrieve the user with code like

Click here to view code image

User.find_by(id: cookies.signed[:user_id])

where cookies.signed[:user_id] automatically decrypts the user id cookie. We can then use bcrypt to verify that cookies [:remember_token] matches the remember_ digest generated in Listing 9.3. (In case you’re wondering why we don’t just use the signed user id, without the remember token, this would allow an attacker with possession of the encrypted id to log in as the user in perpetuity. In the present design, an attacker with both cookies can log in as the user only until the user logs out.)

The final piece of the puzzle is to verify that a given remember token matches the user’s remember digest, and in this context there are a couple of equivalent ways to use bcrypt to verify a match. If you look at the secure password source code, you’ll find a comparison like this:⁹

9. As noted in Section 6.3.1, “unencrypted password” is a misnomer, as the secure password is hashed, not encrypted.

Click here to view code image

BCrypt::Password.new(password_digest) == unencrypted_password

In our case, the analogous code would look like this:

Click here to view code image

BCrypt::Password.new(remember_digest) == remember_token

If you think about it, this code is really strange: it appears to be comparing a bcrypt password digest directly with a token, which would imply decrypting the digest in order to compare using ==. But the whole point of using bcrypt is for hashing to be irreversible, so this can’t be right. Indeed, digging into the source code of the bcrypt gem verifies that the comparison operator == is being redefined, and under the hood the comparison above is equivalent to the following:

Click here to view code image

BCrypt::Password.new(remember_digest).is_password?(remember_token)

Instead of ==, this uses the boolean method is_password? to perform the comparison. Because its meaning is a little clearer, we’ll prefer this second comparison form in the application code.

The above discussion suggests putting the digest–token comparison into an authenticated? method in the User model, which plays a role similar to that of the authenticate method provided by has_secure_password for authenticating a user (Listing 8.15). The implementation appears in Listing 9.6. (Although the authenticated? method in Listing 9.6 is tied specifically to the remember digest, it will turn out to be useful in other contexts as well, and we’ll generalize it in Chapter 11.)

Listing 9.6: Adding an authenticated? method to the User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

Note that the remember_token argument in the authenticated? method defined in Listing 9.6 is not the same as the accessor that we defined in Listing 9.3 using attr_accessor :remember_token; instead, it is a variable local to the method. (Because the argument refers to the remember token, it is not uncommon to use a method argument that has the same name.) Also note the use of the remember_digest attribute, which is the same as self.remember_digest and, like name and email in Chapter 6, is created automatically by Active Record based on the name of the corresponding database column (Listing 9.1).

We’re now in a position to remember a logged-in user, which we’ll do by adding a remember helper to go along with log_in, as shown in Listing 9.7.

Listing 9.7: Logging in and remembering a user.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
  def destroy
    log_out
    redirect_to root_url
  end
end

As with log_in, Listing 9.7 defers the real work to the Sessions helper, where we define a remember method that calls user.remember, thereby generating a remember token and saving its digest to the database. It then uses cookies to create permanent cookies for the user id and remember token as described above. The result appears in Listing 9.8.

Listing 9.8: Remembering the user.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Remembers a user in a persistent session.
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # Returns the current logged-in user (if any).
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  # Returns true if the user is logged in, false otherwise.
  def logged_in?
    !current_user.nil?
  end

  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

With the code in Listing 9.8, a user logging in will be remembered in the sense that their browser will get a valid remember token, but it doesn’t yet do us any good because the current_user method defined in Listing 8.16 knows only about the temporary session:

Click here to view code image

@current_user ||= User.find_by(id: session[:user_id])

In the case of persistent sessions, we want to retrieve the user from the temporary session if session[:user_id] exists, but otherwise we should look for cookies [:user_id] to retrieve (and log in) the user corresponding to the persistent session. We can accomplish this as follows:

Click here to view code image

if session[:user_id]
  @current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
  user = User.find_by(id: cookies.signed[:user_id])
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end

(This follows the same user && user.authenticated pattern we saw in Listing 8.7.) The code above will work, but note the repeated use of both session and cookies. We can eliminate this duplication as follows:

Click here to view code image

if (user_id = session[:user_id])
   @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
   user = User.find_by(id: user_id)
   if user && user.authenticated?(cookies[:remember_token])
     log_in user
     @current_user = user
   end
end

This uses the common but potentially confusing construction

Click here to view code image

if (user_id = session[:user_id])

Despite appearances, this is not a comparison (which would use double-equals ==) but rather is an assignment. If you were to read it in words, you wouldn’t say, “If user id equals session of user id…”, but rather something like, “If session of user id exists (while setting user id to session of user id)…”.¹⁰

10. I generally use the convention of putting such assignments in parentheses, which is a visual reminder that it’s not a comparison.

Defining the current_user helper as discussed above leads to the implementation shown in Listing 9.9.

Listing 9.9: Updating current_user for persistent sessions. RED
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Remembers a user in a persistent session.
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  # Returns true if the user is logged in, false otherwise.
  def logged_in?
    !current_user.nil?
  end

  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

With the code as in Listing 9.9, newly logged in users are correctly remembered, as you can verify by logging in, closing the browser, and checking that you’re still logged in when you restart the sample application and revisit the sample application. If you want, you can even inspect the browser cookies to see the result directly (Figure 9.2).¹¹

11. Google “<your browser name> inspect cookies” to learn how to inspect the cookies on your system.

There’s only one problem with our application as it stands: short of clearing their browser cookies (or waiting 20 years), there’s no way for users to log out. This is exactly the sort of thing our test suite should catch, and indeed it should currently be RED:

Listing 9.10: RED

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

Figure 9.2: The remember token cookie in the local browser.

1. By finding the cookie in your local browser, verify that a remember token and encrypted user id are present after logging in.

2. At the console, verify directly that the authenticated? method defined in Listing 9.6 works correctly.

9.1.3 Forgetting Users

To allow users to log out, we’ll define methods to forget users in analogy with the ones to remember them. The resulting user.forget method just undoes user.remember by updating the remember digest with nil, as shown in Listing 9.11.

Listing 9.11: Adding a forget method to the User model. RED
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end
end

With the code in Listing 9.11, we’re now ready to forget a permanent session by adding a forget helper and calling it from the log_out helper (Listing 9.12). As seen in Listing 9.12, the forget helper calls user.forget and then deletes the user_id and remember_token cookies.

Listing 9.12: Logging out from a persistent session. GREEN
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # Forgets a persistent session.
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # Logs out the current user.
  def log_out
      forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

At this point, the tests suite should be GREEN:

Listing 9.13: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. After logging out, verify that the corresponding cookies have been removed from your browser.

9.1.4 Two Subtle Bugs

There are two closely related subtleties left to address. The first subtlety is that even though the “Log out” link appears only when logged in, a user could potentially have multiple browser windows open to the site. If the user logged out in one window, thereby setting current_user to nil, clicking the “Log out” link in a second window would result in an error because of forget (current_user) in the log_out method (Listing 9.12).¹² We can avoid this by logging out only if the user is logged in.

12. Thanks to reader Paulo Célio Júnior for pointing this out.

The second subtlety is that a user could be logged in (and remembered) in multiple browsers, such as Chrome and Firefox, which causes a problem if the user logs out in the first browser but not the second, and then closes and re-opens the second one.¹³ For example, suppose that the user logs out in Firefox, thereby setting the remember digest to nil (via user.forget in Listing 9.11). The application will still work in Firefox; because the log_out method in Listing 9.12 deletes the user’s id, both highlighted conditionals are false:

13. Thanks to reader Niels de Ron for pointing this out.

Click here to view code image

# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

As a result, evaluation falls off the end of the current_user method, thereby returning nil as required.

In contrast, if we close Chrome, we set session [:user_id] to nil (because all session variables expire automatically on browser close), but the user_id cookie will still be present. This means that the corresponding user will still be pulled out of the database when Chrome is re-launched:

Click here to view code image

# Returns the user corresponding to the remember token cookie.
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])

       log_in user
       @current_user = user
     end
   end
end

Consequently, the inner if conditional will be evaluated:

Click here to view code image

user && user.authenticated?(cookies[:remember_token])

In particular, because user isn’t nil, the second expression will be evaluated, which raises an error. This is because the user’s remember digest was deleted as part of logging out (Listing 9.11) in Firefox, so when we access the application in Chrome we end up calling

Click here to view code image

BCrypt::Password.new(remember_digest).is_password?(remember_token)

with a nil remember digest, thereby raising an exception inside the bcrypt library. To fix this, we want authenticated? to return false instead.

These are exactly the sorts of subtleties that benefit from test-driven development, so we’ll write tests to catch the two errors before correcting them. We first get the integration test from Listing 8.31 to RED, as shown in Listing 9.14.

Listing 9.14: A test for user logout. RED
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    # Simulate a user clicking logout in a second window.
    delete logout_path
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

The second call to delete logout_path in Listing 9.14 should raise an error due to the missing current_user, leading to a RED test suite:

Listing 9.15: RED

$ rails test

The application code simply involves calling log_out only if logged_in? is true, as shown in Listing 9.16.

Listing 9.16: Only logging out if logged in. GREEN
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

The second case, involving a scenario with two different browsers, is harder to simulate with an integration test, but it’s easy to check in the User model test directly. All we need is to start with a user that has no remember digest (which is true for the @user variable defined in the setup method) and then call authenticated?, as shown in Listing 9.17. (Note that we’ve just left the remember token blank; it doesn’t matter what its value is, because the error occurs before it ever gets used.)

Listing 9.17: A test of authenticated? with a nonexistent digest. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

Because BCrypt::Password.new(nil) raises an error, the test suite should now be RED:

Listing 9.18: RED

$ rails test

To fix the error and get to GREEN, all we need to do is return false if the remember digest is nil, as shown in Listing 9.19.

Listing 9.19: Updating authenticated? to handle a nonexistent digest. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end
end

This uses the return keyword to return immediately if the remember digest is nil, which is a common way to emphasize that the rest of the method gets ignored in that case. The equivalent code

Click here to view code image

if remember_digest.nil?
  false
else
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

would also work fine, but I prefer the explicitness of the version in Listing 9.19 (which also happens to be slightly shorter).

With the code in Listing 9.19, our full test suite should be GREEN, and both subtleties should now be addressed:

Listing 9.20: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Comment out the fix in Listing 9.16 and then verify that the first subtle bug is present by opening two logged-in tabs, logging out in one, and then clicking “Log out” link in the other.

2. Comment out the fix in Listing 9.19 and verify that the second subtle bug is present by logging out in one browser and closing and opening the second browser.

3. Uncomment the fixes and confirm that the test suite goes from RED to GREEN.

9.2 “Remember Me” Checkbox

With the code in Section 9.1.3, our application has a complete, professional-grade authentication system. As a final step, we’ll see how to make staying logged in optional using a “remember me” checkbox. A mockup of the login form with such a checkbox appears in Figure 9.3.

To write the implementation, we start by adding a checkbox to the login form from Listing 8.4. As with labels, text fields, password fields, and submit buttons, checkboxes can be created with a Rails helper method. In order to get the styling right, though, we have to nest the checkbox inside the label, as follows:

Click here to view code image

<%= f.label :remember_me, class: "checkbox inline" do %>
  <%= f.check_box :remember_me %>
  <span>Remember me on this computer</span>
<% end %>

Figure 9.3: A mockup of a “remember me” checkbox.

Putting this into the login form gives the code shown in Listing 9.21.

Listing 9.21: Adding a “remember me” checkbox to the login form.
app/views/sessions/new.html.erb

Click here to view code image

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

In Listing 9.21, we’ve included the CSS classes checkbox and inline, which Bootstrap uses to put the checkbox and the text (“Remember me on this computer”) in the same line. In order to complete the styling, we need just a few more CSS rules, as shown in Listing 9.22. The resulting login form appears in Figure 9.4.

Listing 9.22: CSS for the “remember me” checkbox.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* forms */
.
.
.
.checkbox {
  margin-top: -10px;
  margin-bottom: 10px;
  span {
    margin-left: 20px;
    font-weight: normal;
  }
}

#session_remember_me {
  width: auto;
  margin-left: 0;
}

Figure 9.4: The login form with an added “remember me” checkbox.

Having edited the login form, we’re now ready to remember users if they check the checkbox and forget them otherwise. Incredibly, because of all our work in the previous sections, the implementation can be reduced to one line. We start by noting that the params hash for submitted login forms now includes a value based on the checkbox (as you can verify by submitting the form in Listing 9.21 with invalid information and inspecting the values in the debug section of the page). In particular, the value of

Click here to view code image

params[:session][:remember_me]

is '1' if the box is checked and '0' if it isn’t.

By testing the relevant value of the params hash, we can now remember or forget the user based on the value of the submission:¹⁴

14. Note that this means unchecking the box will log out the user on all browsers on all computers. The alternate design of remembering user login sessions on each browser independently is potentially more convenient for users, but it’s less secure and is also more complicated to implement. Ambitious readers are invited to try their hand at implementing it.

Click here to view code image

if params[:session][:remember_me] == '1'
  remember(user)
else
  forget(user)
end

As explained in Box 9.2, this sort of if-then branching structure can be converted to one line using the ternary operator as follows:¹⁵

15. Before we wrote remember user without parentheses, but when used with the ternary operator omitting them results in a syntax error.

Click here to view code image

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

Adding this to the Sessions controller’s create method leads to the amazingly compact code shown in Listing 9.23. (Now you’re in a position to understand the code in Listing 8.21, which uses the ternary operator to define the bcrypt cost variable.)

Listing 9.23: Handling the submission of the “remember me” checkbox.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

With the implementation in Listing 9.23, our login system is complete, as you can verify by checking or unchecking the box in your browser.

Box 9.2: 10 Types of People

There’s an old joke that there are 10 kinds of people in the world: those who understand binary and those who don’t (10, of course, being 2 in binary). In this spirit, we can say that there are 10 kinds of people in the world: those who like the ternary operator, those who don’t, and those who don’t yet know about it. (If you happen to be in the third category, soon you won’t be any longer.)

When you do a lot of programming, you quickly learn that one of the most common bits of control flow goes something like this:

if boolean?
  do_one_thing
else
  do_something_else
end

Ruby, like many other languages (including C/C++, Perl, PHP, and Java), allows you to replace this with a much more compact expression using the ternary operator (so called because it consists of three parts):

boolean? ? do_one_thing : do_something_else

You can also use the ternary operator to replace assignment, so that

if boolean?
  var = foo
else
  var = bar
end

becomes

var = boolean? ? foo : bar

Finally, it’s often convenient to use the ternary operator in a function’s return value:

def foo
  do_stuff
  boolean? ? "bar" : "baz"
end

Since Ruby implicitly returns the value of the last expression in a function, here the foo method returns "bar" or "baz" depending on whether boolean? is true or false.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By inspecting your browser’s cookies directly, verify that the “remember me” checkbox is having its intended effect.

2. At the console, invent examples showing both possible behaviors of the ternary operator (Box 9.2).

9.3 Remember Tests

Although our “remember me” functionality is now working, it’s important to write some tests to verify its behavior. One reason is to catch implementation errors, as discussed in a moment. Even more important, though, is that the core user persistence code is in fact completely untested at present. Fixing these issues will require some trickery, but the result will be a far more powerful test suite.

9.3.1 Testing the “Remember Me” Box

When I originally implemented the checkbox handling in Listing 9.23, instead of the correct

Click here to view code image

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

I actually used

Click here to view code image

params[:session][:remember_me] ? remember(user) : forget(user)

In this context, params[:session][:remember_me] is either '0' or '1', both of which are true in a boolean context, so the resulting expression is always true, and the application acts as if the checkbox is always checked. This is exactly the kind of error a test can catch.

Because remembering users requires that they be logged in, our first step is to define a helper to log users in inside tests. In Listing 8.23, we logged a user in using the post method and a valid session hash, but it’s cumbersome to do this every time. To avoid needless repetition, we’ll write a helper method called log_in_as to log in for us.

Our method for logging a user in depends on the type of test. Inside controller tests, we can manipulate the session method directly, assigning user.id to the :user_id key (as first seen in Listing 8.14):

Click here to view code image

def log_in_as(user)
  session[:user_id] = user.id
end

We call the method log_in_as to avoid any confusion with the application code’s log_in method as defined in Listing 8.14. Its location is in the ActiveSupport:: TestCase class inside the test_helper file, the same location as the is_logged_in? helper from Listing 8.26:

Click here to view code image

class ActiveSupport::TestCase
  fixtures :all

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end
  # Log in as a particular user.
  def log_in_as(user)
    session[:user_id] = user.id
  end
end

We won’t actually need this version of the method in this chapter, but we’ll put it to use in Chapter 10.

Inside integration tests, we can’t manipulate session directly, but we can post to the sessions path as in Listing 8.23, which leads to the log_in_as method shown here:

Click here to view code image

class ActionDispatch::IntegrationTest

  # Log in as a particular user.
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end

Because it’s located inside the ActionDispatch::IntegrationTest class, this is the version of log_in_as that will be called inside integration tests. We use the same method name in both cases because it lets us do things like use code from a controller test in an integration without making any changes to the login method.

Putting these two methods together yields the parallel log_in_as helpers shown in Listing 9.24.

Listing 9.24: Adding a log_in_as helper.
test/test_helper.rb

Click here to view code image

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end

# Log in as a particular user.
def log_in_as(user)
    session[:user_id] = user.id
  end
end

class ActionDispatch::      IntegrationTest

  # Log in as a particular user.
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end

Note that, for maximum flexibility, the second log_in_as method in Listing 9.24 accepts keyword arguments (as in Listing 7.13), with default values for the password and for the “remember me” checkbox set to 'password' and '1', respectively.

To verify the behavior of the “remember me” checkbox, we’ll write two tests, one each for submitting with and without the checkbox checked. This is easy using the login helper defined in Listing 9.24, with the two cases appearing as

Click here to view code image

log_in_as(@user, remember_me: '1')

and

Click here to view code image

log_in_as(@user, remember_me: '0')

(Because '1' is the default value of remember_me, we could omit the corresponding option in the first case above, but I’ve included it to make the parallel structure more apparent.)

After logging in, we can check if the user has been remembered by looking for the remember_token key in the cookies. Ideally, we would check that the cookie’s value is equal to the user’s remember token, but as currently designed, there’s no way for the test to get access to it: the user variable in the controller has a remember token attribute, but (because remember_token is virtual) the @user variable in the test doesn’t. Fixing this minor blemish is left as an exercise (Section 9.3.1), but for now we can just test to see if the relevant cookie is nil or not.

There’s one more subtlety, which is that for some reason inside tests the cookies

method doesn’t work with symbols as keys, so that

cookies[:remember_token]

is always nil. Luckily, cookies does work with string keys, so that

cookies['remember_token']

has the value we need. The resulting tests appear in Listing 9.25. (Recall from Listing 8.23 that users (:michael) references the fixture user from Listing 8.22.)

Listing 9.25: A test of the “remember me” checkbox. GREEN
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_nil cookies['remember_token']
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '0')
    assert_nil cookies['remember_token']
  end
end

Assuming you didn’t make the same implementation mistake I did, the tests should be GREEN:

Listing 9.26: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As mentioned above, the application currently doesn’t have any way to access the virtual remember_token attribute in the integration test in Listing 9.25. It is possible, though, using a special test method called assigns. Inside a test, you can access instance variables defined in the controller by using assigns with the corresponding symbol. For example, if the create action defines an @user variable, we can access it in the test using assigns (:user). Right now, the Sessions controller create action defines a normal (non-instance) variable called user, but if we change it to an instance variable we can test that cookies correctly contains the user’s remember token. By filling in the missing elements in Listing 9.27 and Listing 9.28 (indicated with question marks ? and FILL_IN), complete this improved test of the “remember me” checkbox.

Listing 9.27: A template for using an instance variable in the create action.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    ?user = User.find_by(email: params[:session][:email].downcase)
    if ?user && ?user.authenticate(params[:session][:password])
      log_in ?user
      params[:session][:remember_me] == '1' ? remember(?user) : forget(?user)
      redirect_to ?user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

Listing 9.28: A template for an improved “remember me” test. GREEN
test/integration/users_login_test.rb

Click here to view code image

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal FILL_IN, assigns(:user).FILL_IN
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '0')
    assert_nil cookies['remember_token']
  end
  .
  .
  .
end

9.3.2 Testing the Remember Branch

In Section 9.1.2, we verified by hand that the persistent session implemented in the preceding sections is working, but in fact the relevant branch in the current_user method is currently completely untested. My favorite way to handle this kind of situation is to raise an exception in the suspected untested block of code: if the code isn’t covered, the tests will still pass; if it is covered, the resulting error will identify the relevant test. The result in the present case appears in Listing 9.29.

Listing 9.29: Raising an exception in an untested branch. GREEN
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper
  .
  .
  .
  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      raise       # The tests still pass, so this branch is currently untested.
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

At this point, the tests are GREEN:

Listing 9.30: GREEN

$ rails test

This is a problem, of course, because the code in Listing 9.29 is broken. Moreover, persistent sessions are cumbersome to check by hand, so if we ever want to refactor the current_user method (as we will in Chapter 11), it’s important to test it.

Because the log_in_as helper method defined in Listing 9.24 automatically sets session[:user_id], testing the “remember” branch of the current_user method is difficult in an integration test. Luckily, we can bypass this restriction by testing the current_user method directly in a Sessions helper test, whose file we have to create:

Click here to view code image

$ touch test/helpers/sessions_helper_test.rb

The test sequence is simple:

1. Define a user variable using the fixtures.

2. Call the remember method to remember the given user.

3. Verify that current_user is equal to the given user.

Because the remember method doesn’t set session[:user_id], this procedure will test the desired “remember” branch. The result appears in Listing 9.31.

Listing 9.31: A test for persistent sessions.
test/helpers/sessions_helper_test.rb

Click here to view code image

require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

Note that we’ve added a second test, which checks that the current user is nil if the user’s remember digest doesn’t correspond correctly to the remember token, thereby testing the authenticated? expression in the nested if statement:

Click here to view code image

if user && user.authenticated?(cookies[:remember_token])

Incidentally, in Listing 9.31 we could write

Click here to view code image

assert_equal current_user, @user

instead, and it would work just the same, but (as mentioned briefly in Section 5.3.4) the conventional order for the arguments to assert_equal is expected, actual :

Click here to view code image

assert_equal <expected>, <actual>

which in the case of Listing 9.31 gives

Click here to view code image

assert_equal @user, current_user

With the code as in Listing 9.31, the test is RED as required:

Listing 9.32: RED

Click here to view code image

$ rails test test/helpers/sessions_helper_test.rb

We can get the tests in Listing 9.31 to pass by removing the raise and restoring the original current_user method, as shown in Listing 9.33.

Listing 9.33: Removing the raised exception. GREEN
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper
  .
  .
  .
  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

At this point, the test suite should be GREEN:

Listing 9.34: GREEN

$ rails test

Now that the “remember” branch of current_user is tested, we can be confident of catching regressions without having to check by hand.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify by removing the authenticated? expression in Listing 9.33 that the second test in Listing 9.31 fails, thereby confirming that it tests the right thing.

9.4 Conclusion

We’ve covered a lot of ground in the last three chapters, transforming our promising but unformed application into a site capable of the full suite of signup and login behaviors. All that is needed to complete the authentication functionality is to restrict access to pages based on login status and user identity. We’ll accomplish this task en route to giving users the ability to edit their information, which is the main goal of Chapter 10.

Before moving on, merge your changes back into the master branch:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Implement advanced login"
$ git checkout master
$ git merge advanced-login

Before deploying to Heroku, it’s worth noting that the application will briefly be in an invalid state after pushing but before the migration is finished. On a production site with significant traffic, it’s a good idea to turn maintenance mode on before making the changes:

Click here to view code image

$ heroku maintenance:on
$ git push heroku
$ heroku run rake db:migrate
$ heroku maintenance:off

This arranges to show a standard error page during the deployment and migration. (We won’t bother with this step again, but it’s good to see it at least once.) For more information, see the Heroku documentation on error pages.

9.4.1 What We Learned in This Chapter

• Rails can maintain state from one page to the next using persistent cookies via the cookies method.

• We associate to each user a remember token and a corresponding remember digest for use in persistent sessions.

• Using the cookies method, we create a persistent session by placing a permanent remember token cookie on the browser.

• Login status is determined by the presence of a current user based on the temporary session’s user id or the permanent session’s unique remember token.

• The application signs users out by deleting the session’s user id and removing the permanent cookie from the browser.

• The ternary operator is a compact way to write simple if-then statements.

Chapter 10. Updating, Showing, and Deleting Users

In this chapter, we will complete the REST actions for the Users resource (Table 7.1) by adding edit, update, index, and destroy actions. We’ll start by giving users the ability to update their profiles, which will also provide a natural opportunity to enforce an authorization model (made possible by the authentication code in Chapter 8). Then we’ll make a listing of all users (also requiring authentication), which will motivate the introduction of sample data and pagination. Finally, we’ll add the ability to destroy users, wiping them clear from the database. Since we can’t allow just any user to have such dangerous powers, we’ll take care to create a privileged class of administrative users authorized to delete other users.

10.1 Updating Users

The pattern for editing user information closely parallels that for creating new users (Chapter 7). Instead of a new action rendering a view for new users, we have an edit action rendering a view to edit users; instead of create responding to a POST request, we have an update action responding to a PATCH request (Box 3.2). The biggest difference is that, while anyone can sign up, only the current user should be able to update their information. The authentication machinery from Chapter 8 will allow us to use a before filter to ensure that this is the case.

To get started, let’s start work on an updating-users topic branch:

Click here to view code image

$ git checkout -b updating-users

10.1.1 Edit Form

We start with the edit form, whose mockup appears in Figure 10.1.¹ To turn the mockup in Figure 10.1 into a working page, we need to fill in both the Users controller edit action and the user edit view. We start with the edit action, which requires pulling the relevant user out of the database. Note from Table 7.1 that the proper URL for a user’s edit page is /users/1/edit (assuming the user’s id is 1). Recall that the id of the user is available in the params [:id] variable, which means that we can find the user with the code in Listing 10.1.

1. Image retrieved from http://www.flickr.com/photos/sashawolff/4598355045/ on 2014-08-25. Copyright © 2010 by Sasha Wolff and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

Figure 10.1: A mockup of the user edit page.

Listing 10.1: The user edit action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  private

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

The corresponding user edit view (which you will have to create by hand) is shown in Listing 10.2. Note how closely this resembles the new user view from Listing 7.15; the large overlap suggests factoring the repeated code into a partial, which is left as an exercise (Section 10.1.1).

Listing 10.2: The user edit view.
app/views/users/edit.html.erb

Click here to view code image

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@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 "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

Here we have reused the shared error_messages partial introduced in Section 7.3.3. By the way, the use of target="_blank" in the Gravatar link is a neat trick to get the browser to open the page in a new window or tab, which is sometimes convenient behavior when linking to third-party sites. (There’s a minor security issue associated with target="_blank"; dealing with this detail is left as an exercise [Section 10.1.1].)

With the @user instance variable from Listing 10.1, the edit page should render properly, as shown in Figure 10.2. The “Name” and “Email” fields in Figure 10.2 also show how Rails automatically pre-fills the Name and Email fields using the attributes of the existing @user variable.

Figure 10.2: The initial user edit page with pre-filled name and email.

Looking at the HTML source for Figure 10.2, we see a form tag as expected, as in Listing 10.3 (slight details may differ).

Listing 10.3: HTML for the edit form defined in Listing 10.2 and shown in Figure 10.2.

Click here to view code image

<form accept-charset="UTF-8" action="/users/1" class="edit_user"
      id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
  .
  .
  .
</form>

Note here the hidden input field:

Click here to view code image

<input name="_method" type="hidden" value="patch" />

Since web browsers can’t natively send PATCH requests (as required by the REST conventions from Table 7.1), Rails fakes it with a POST request and a hidden input field.²

2. Don’t worry about how this works; the details are of interest to developers of the Rails framework itself and by design are not important for Rails application developers.

There’s another subtlety to address here: the code form_for(@user) in Listing 10.2 is exactly the same as the code in Listing 7.15—so how does Rails know to use a POST request for new users and a PATCH for editing users? The answer is that it is possible to tell whether a user is new or already exists in the database via Active Record’s new_record? boolean method:

Click here to view code image

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

When constructing a form using form_for(@user), Rails uses POST if @user.new_ record? is true and PATCH if it is false.

As a final touch, we’ll fill in the URL of the settings link in the site navigation. This is easy using the named route edit_user_path from Table 7.1, together with the handy current_user helper method defined in Listing 9.9:

Click here to view code image

<%= link_to "Settings", edit_user_path(current_user) %>

The full application code appears in Listing 10.4).

Listing 10.4: Adding a URL to the “Settings” link in the site layout.
app/views/layouts/_header.html.erb

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", edit_user_path(current_user) %></li>
              <li class="divider"></li>
              </li>
                <%= link_to "Log out", logout_path, method: "delete" %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As noted above, there’s a minor security issue associated with using target="_blank" to open URLs, which is that the target site gains control of what’s known as the “window object” associated with the HTML document. The result is that the target site could potentially introduce malicious content, such as a phishing page. This is extremely unlikely to happen when linking to a reputable site like Gravatar, but it turns out that we can eliminate the risk entirely by setting the rel attribute (“relationship”) to "noopener" in the origin link. Add this attribute to the Gravatar edit link in Listing 10.2.

2. Remove the duplicated form code by refactoring the new.html.erb and edit.html.erb views to use the partial in Listing 10.5, as shown in Listing 10.6 and Listing 10.7. Note the use of the provide method, which we used before in Section 3.4.3 to eliminate duplication in the layout.³ (If you solved the exercise corresponding to Listing 7.27, this exercise won’t work exactly as written and you’ll have to apply your technical sophistication to resolve the discrepancy. My suggestion is to use the variable-passing technique shown in Listing 10.5 to pass the needed URL from Listing 10.6 and Listing 10.7 to the form in Listing 10.5.)

3. Thanks to Jose Carlos Montero Gómez for a suggestion that further reduced duplication in the new and edit partials.

Listing 10.5: A partial for the new and edit form.
app/views/users/_form.html.erb

Click here to view code image

<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

  <%= 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 %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>

Listing 10.6: The signup view with partial.
app/views/users/new.html.erb

Click here to view code image

<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>

Listing 10.7: The edit view with partial.
app/views/users/edit.html.erb

Click here to view code image

<% provide(:title, 'Edit user') %>
<% provide(:button_text, 'Save changes') %>
<h1>Update your profile</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">Change</a>
    </div>
  </div>
</div>

10.1.2 Unsuccessful Edits

In this section we’ll handle unsuccessful edits, following similar ideas to unsuccessful signups (Section 7.3). We start by creating an update action, which uses update_attributes (Section 6.1.5) to update the user based on the submitted params hash, as shown in Listing 10.8. With invalid information, the update attempt returns false, so the else branch renders the edit page. We’ve seen this pattern before; the structure closely parallels the first version of the create action (Listing 7.18).

Listing 10.8: The initial user update action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end
  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Handle a successful update.
    else
      render 'edit'
    end
  end

  private

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

Note the use of user_params in the call to update_attributes, which uses strong parameters to prevent mass assignment vulnerability (as described in Section 7.3.2).

Because of the existing User model validations and the error-messages partial in Listing 10.2, submission of invalid information results in helpful error messages (Figure 10.3).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm by submitting various invalid combinations of username, email, and password that the edit form won’t accept invalid submissions.

10.1.3 Testing Unsuccessful Edits

We left Section 10.1.2 with a working edit form. Following the testing guidelines from Box 3.3, we’ll now write an integration test to catch any regressions. Our first step is to generate an integration test as usual:

Figure 10.3: Error messages from submitting the update form.

Click here to view code image

$ rails generate integration_test users_edit
      invoke test_unit
      create   test/integration/users_edit_test.rb

Then we’ll write a simple test of an unsuccessful edit, as shown in Listing 10.9. The test in Listing 10.9 checks for the correct behavior by verifying that the edit template is rendered after getting the edit page and re-rendered upon submission of invalid information. Note the use of the patch method to issue a PATCH request, which follows the same pattern as get, post, and delete.

Listing 10.9: A test for an unsuccessful edit. GREEN
test/integration/users_edit_test.rb

Click here to view code image

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }
    assert_template 'users/edit'
  end
end

At this point, the test suite should still be GREEN:

Listing 10.10: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Add a line in Listing 10.9 to test for the correct number of error messages. Hint: Use an assert_select (Table 5.2) that tests for a div with class alert containing the text “The form contains 4 errors.”

10.1.4 Successful Edits (with TDD)

Now it’s time to get the edit form to work. Editing the profile images is already functional since we’ve outsourced image upload to the Gravatar website; we can edit Gravatars by clicking on the “change” link from Figure 10.2, as shown in Figure 10.4. Let’s get the rest of the user edit functionality working as well.

Figure 10.4: The Gravatar image-cropping interface, with a picture of some dude.

As you get more comfortable with testing, you might find that it’s useful to write integration tests before writing the application code instead of after. In this context, such tests are sometimes known as acceptance tests, since they determine when a particular feature should be accepted as complete. To see how this works, we’ll complete the user edit feature using test-driven development.

We’ll test for the correct behavior of updating users by writing a test similar to the one shown in Listing 10.9, only this time we’ll submit valid information. Then we’ll check for a nonempty flash message and a successful redirect to the profile page, while also verifying that the user’s information correctly changed in the database. The result appears in Listing 10.11. Note that the password and confirmation in Listing 10.11 are blank, which is convenient for users who don’t want to update their passwords every time they update their names or email addresses. Note also the use of @user.reload (first seen in Section 6.1.5) to reload the user’s values from the database and confirm that they were successfully updated. (This is the kind of detail you could easily forget initially, which is why acceptance testing [and TDD generally] require a certain level of experience to be effective.)

Listing 10.11: A test of a successful edit. RED
test/integration/users_edit_test.rb

Click here to view code image

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name: name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end
end

The update action needed to get the tests in Listing 10.11 to pass is similar to the final form of the create action (Listing 8.25), as seen in Listing 10.12.

Listing 10.12: The user update action. RED
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
end

As indicated in the caption to Listing 10.12, the test suite is still RED, which is the result of the password length validation (Listing 6.42) failing due to the empty password and confirmation in Listing 10.11. To get the tests to GREEN, we need to make an exception to the password validation if the password is empty. We can do this by passing the allow_nil: true option to validates, as seen in Listing 10.13.

Listing 10.13: Allowing empty passwords on update. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_blank: true
  .
  .
  .
end

In case you’re worried that Listing 10.13 might allow new users to sign up with empty passwords, recall from Section 6.3.3 that has_secure_password includes a separate presence validation that specifically catches nil passwords. (Because nil passwords now bypass the main presence validation but are still caught by has_secure_password, this also fixes the duplicate error message mentioned in Section 7.3.3.)

Figure 10.5: The result of a successful edit.

With the code in this section, the user edit page should be working (Figure 10.5), as you can double-check by re-running the test suite, which should now be GREEN:

Listing 10.14: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Double-check that you can now make edits by making a few changes on the development version of the application.

2. What happens when you change the email address to one without an associated Gravatar?

10.2 Authorization

In the context of web applications, authentication allows us to identify users of our site, while authorization lets us control what they can do. One nice effect of building the authentication machinery in Chapter 8 is that we are now in a position to implement authorization as well.

Although the edit and update actions from Section 10.1 are functionally complete, they suffer from a ridiculous security flaw: they allow anyone (even non-logged-in users) to access either action, and any logged-in user can update the information for any other user. In this section, we’ll implement a security model that requires users to be logged in and prevents them from updating any information other than their own.

In Section 10.2.1, we’ll handle the case of non-logged-in users who try to access a protected page to which they might normally have access. Because this could easily happen in the normal course of using the application, such users will be forwarded to the login page with a helpful message, as mocked up in Figure 10.6. On the other hand, users who try to access a page for which they would never be authorized (such as a logged-in user trying to access a different user’s edit page) will be redirected to the root URL (Section 10.2.2).

10.2.1 Requiring Logged-in Users

To implement the forwarding behavior shown in Figure 10.6, we’ll use a before filter in the Users controller. Before filters use the before_action command to arrange for a particular method to be called before the given actions.⁴ To require users to be logged in, we define a logged_in_user method and invoke it using before_action :logged_in_user, as shown in Listing 10.15.

4. The command for before filters used to be called before_filter, but the Rails core team decided to rename it to emphasize that the filter takes place before particular controller actions.

Figure 10.6: A mockup of the result of visiting a protected page

Listing 10.15: Adding a logged_in_user before filter. RED
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
    # Before filters

    # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

By default, before filters apply to every action in a controller, so here we restrict the filter to act only on the :edit and :update actions by passing the appropriate only: options hash.

We can see the result of the before filter in Listing 10.15 by logging out and attempting to access the user edit page /users/1/edit, as seen in Figure 10.7.

As indicated in the caption of Listing 10.15, our test suite is currently RED:

Listing 10.16: RED

$ rails test

The reason is that the edit and update actions now require a logged-in user, but no user is logged in inside the corresponding tests.

We’ll fix our test suite by logging the user in before hitting the edit or update actions. This is easy using the log_in_as helper developed in Section 9.3 (Listing 9.24), as shown in Listing 10.17.

Listing 10.17: Logging in a test user. GREEN
test/integration/users_edit_test.rb

Click here to view code image

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end
end

Figure 10.7: The login form after trying to access a protected page.

(We could eliminate some duplication by putting the test login in the setup method of Listing 10.17, but in Section 10.2.3 we’ll change one of the tests to visit the edit page before logging in, which isn’t possible if the login step happens during the test setup.)

At this point, our test suite should be GREEN:

Listing 10.18: GREEN

$ rails test

Even though our test suite is now passing, we’re not finished with the before filter, because the suite is still GREEN even if we remove our security model, as you can verify by commenting it out (Listing 10.19). This is a Bad Thing—of all the regressions we’d like our test suite to catch, a massive security hole is probably #1, so the code in Listing 10.19 should definitely be RED. Let’s write tests to arrange that.

Listing 10.19: Commenting out the before filter to test our security model. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  # before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
end

Because the before filter operates on a per-action basis, we’ll put the corresponding tests in the Users controller test. The plan is to hit the edit and update actions with the right kinds of requests and verify that the flash is set and that the user is redirected to the login path. From Table 7.1, we see that the proper requests are GET and PATCH, respectively, which means using the get and patch methods inside the tests. The results appear in Listing 10.20.

Listing 10.20: Testing that edit and update are protected. RED
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

Note that the second test shown in Listing 10.20 involves using the patch method to send a PATCH request to user_path(@user). According to Table 7.1, such a request gets routed to the update action in the Users controller, as required.

The test suite should now be RED, as required. To get it to GREEN, just uncomment the before filter (Listing 10.21).

Listing 10.21: Uncommenting the before filter. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
end

With that, our test suite should be GREEN:

Listing 10.22: GREEN

$ rails test

Any accidental exposure of the edit methods to unauthorized users will now be caught immediately by our test suite.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As noted above, by default before filters apply to every action in a controller, which in our cases is an error (requiring, e.g., that users log in to hit the signup page, which is absurd). By commenting out the only: hash in Listing 10.15, confirm that the test suite catches this error.

10.2.2 Requiring the Right User

Of course, requiring users to log in isn’t quite enough; users should only be allowed to edit their own information. As we saw in Section 10.2.1, it’s easy to have a test suite that misses an essential security flaw, so we’ll proceed using test-driven development to be sure our code implements the security model correctly. To do this, we’ll add tests to the Users controller test to complement the ones shown in Listing 10.20.

In order to make sure users can’t edit other users’ information, we need to be able to log in as a second user. This means adding a second user to our users fixture file, as shown in Listing 10.23.

Listing 10.23: Adding a second user to the fixture file.
test/fixtures/users.yml

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

By using the log_in_as method defined in Listing 9.24, we can test the edit and update actions as in Listing 10.24. Note that we expect to redirect users to the root path instead of the login path because a user trying to edit a different user would already be logged in.

Listing 10.24: Tests for trying to edit as the wrong user. RED
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end
end

To redirect users trying to edit another user’s profile, we’ll add a second method called correct_user, together with a before filter to call it (Listing 10.25). Note that the correct_user before filter defines the @user variable, so Listing 10.25 also shows that we can eliminate the @user assignments in the edit and update actions.

Listing 10.25: A before filter to protect the edit/update pages. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

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

    # Before filters

    # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # Confirms the correct user.
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user == current_user
    end
end

At this point, our test suite should be GREEN:

Listing 10.26: GREEN

$ rails test

As a final refactoring, we’ll adopt a common convention and define a current_user? boolean method for use in the correct_user before filter, which we define in the Sessions helper (Listing 10.27). We’ll use this method to replace code like

unless @user == current_user

with the (slightly) more expressive

unless current_user?(@user)

Listing 10.27: The current_user? method.
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Remembers a user in a persistent session.
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # Returns true if the given user is the current user.
  def current_user?(user)
    user == current_user
  end

  # Returns the user corresponding to the remember token cookie.
  def current_user
    .
    .
    .
  end
  .
  .
  .
end

Replacing the direct comparison with the boolean method gives the code shown in Listing 10.28.

Listing 10.28: The final correct_user before filter. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user, only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

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

    # Before filters

    # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # Confirms the correct user.
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Why is it important to protect both the edit and update actions?

2. Which action could you more easily test in a browser?

10.2.3 Friendly Forwarding

Our site authorization is complete as written, but there is one minor blemish: when users try to access a protected page, they are currently redirected to their profile pages regardless of where they were trying to go. In other words, if a non-logged-in user tries to visit the edit page, after logging in the user will be redirected to /users/1 instead of

/users/1/edit. It would be much friendlier to redirect them to their intended destination instead.

The application code will turn out to be relatively complicated, but we can write a ridiculously simple test for friendly forwarding just by reversing the order of logging in and visiting the edit page in Listing 10.17. As seen in Listing 10.29, the resulting test tries to visit the edit page, then logs in, and then checks that the user is redirected to the edit page instead of the default profile page. (Listing 10.29 also removes the test for rendering the edit template since that’s no longer the expected behavior.)

Listing 10.29: A test for friendly forwarding. RED
test/integration/users_edit_test.rb

Click here to view code image

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_path(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name: name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

Now that we have a failing test, we’re ready to implement friendly forwarding.⁵ In order to forward users to their intended destination, we need to store the location of the requested page somewhere and then redirect to that location instead of to the default. We accomplish this with a pair of methods, store_location and redirect_back_or, both defined in the Sessions helper (Listing 10.30).

5. The code in this section is adapted from the Clearance gem by thoughtbot.

Listing 10.30: Code to implement friendly forwarding. RED
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper
  .
  .
  .
  # Redirects to stored location (or to the default).
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  # Stores the URL trying to be accessed.
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

Here the storage mechanism for the forwarding URL is the same session facility we used in Section 8.2.1 to log the user in. Listing 10.30 also uses the request object (via request.original_url) to get the URL of the requested page.

The store_location method in Listing 10.30 puts the requested URL in the session variable under the key :forwarding_url, but only for a GET request. This prevents storing the forwarding URL if a user, say, submits a form when not logged in (which is an edge case but could happen if, e.g., a user deleted the session cookies by hand before submitting the form). In such a case, the resulting redirect would issue a GET request to a URL expecting POST, PATCH, or DELETE, thereby causing an error. Including if request.get? prevents this from happening.⁶

6. Thanks to reader Yoel Adler for pointing out this subtle issue, and for discovering the solution.

To make use of store_location, we need to add it to the logged_in_user before filter, as shown in Listing 10.31.

Listing 10.31: Adding store_location to the logged-in user before filter. RED
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

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

    # Before filters

    # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # Confirms the correct user.
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

To implement the forwarding itself, we use the redirect_back_or method to redirect to the requested URL if it exists, or some default URL otherwise, which we add to the Sessions controller create action to redirect after successful login (Listing 10.32). The redirect_back_or method uses the or operator || through

Click here to view code image

session[:forwarding_url] || default

This evaluates to session[:forwarding_url] unless it’s nil, in which case it evaluates to the given default URL. Note that Listing 10.30 is careful to remove the forwarding URL (via session.delete(:forwarding_url)); otherwise, subsequent login attempts would forward to the protected page until the user closed their browser.

(Testing for this is left as an exercise [Section 10.2.3].) Also note that the session deletion occurs even though the line with the redirect appears first; redirects don’t happen until an explicit return or the end of the method, so any code appearing after the redirect is still executed.

Listing 10.32: The Sessions create action with friendly forwarding. GREEN
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

With that, the friendly forwarding integration test in Listing 10.29 should pass, and the basic user authentication and page protection implementation is complete. As usual, it’s a good idea to verify that the test suite is GREEN before proceeding:

Listing 10.33: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Write a test to make sure that friendly forwarding only forwards to the given URL the first time. On subsequent login attempts, the forwarding URL should revert to the default (i.e., the profile page). Hint: Add to the test in Listing 10.29 by checking for the right value of session[:forwarding_url].

2. Put a debugger (Section 7.1.3) in the Sessions controller’s new action, then log out and try to visit /users/1/edit. Confirm in the debugger that the value of session[:forwarding_url] is correct. What is the value of request.get? for the new action? (Sometimes the terminal can freeze up or act strangely when you’re using the debugger; use your technical sophistication [Box 1.1] to resolve any issues.)

10.3 Showing All Users

In this section, we’ll add the penultimate user action, the index action, which is designed to display all the users instead of just one. Along the way, we’ll learn how to seed the database with sample users and how to paginate the user output so that the index page can scale up to display a potentially large number of users. A mockup of the result—users, pagination links, and a “Users” navigation link—appears in Figure 10.8.⁷ In Section 10.4, we’ll add an administrative interface to the users index so that users can also be destroyed.

7. Image retriefed from http://www.flickr.com/photos/glasgows/338937124/ on 2014-08-25. Copyright © 2008 by M&R Glasgow and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

10.3.1 Users Index

To get started with the users index, we’ll first implement a security model. Although we’ll keep individual user show pages visible to all site visitors, the user index will be restricted to logged-in users so that there’s a limit to how much unregistered users can see by default.⁸

8. This is the same authorization model used by Twitter.

To protect the index page from unauthorized access, we’ll first add a short test to verify that the index action is redirected properly (Listing 10.34).

Figure 10.8: A mockup of the users index page.

Listing 10.34: Testing the index action redirect. RED
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

Then we just need to add an index action and include it in the list of actions protected by the logged_in_user before filter (Listing 10.35).

Listing 10.35: Requiring a logged-in user for the index action. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user, only: [:edit, :update]
  def index
  end
  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

To display the users themselves, we need to make a variable containing all the site’s users and then render each one by iterating through them in the index view. As you may recall from the corresponding action in the toy app (Listing 2.8), we can use User.all to pull all the users out of the database, assigning them to an @users instance variable for use in the view, as seen in Listing 10.36. (If displaying all the users at once seems like a bad idea, you’re right, and we’ll remove this blemish in Section 10.3.3.)

Listing 10.36: The user index action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

To make the actual index page, we’ll make a view (whose file you’ll have to create) that iterates through the users and wraps each one in an li tag. We do this with the each method, displaying each user’s Gravatar and name, while wrapping the whole thing in a ul tag (Listing 10.37).

Listing 10.37: The users index view.
app/views/users/index.html.erb

Click here to view code image

<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

The code in Listing 10.37 uses the result of Listing 10.38 from Section 7.1.4, which allows us to pass an option to the Gravatar helper specifying a size other than the default. If you didn’t do that exercise, update your Users helper file with the contents of Listing 10.38 before proceeding. (You are also welcome to use the Ruby 2.0–style version from Listing 7.13 instead.)

Listing 10.38: Adding an options hash in the gravatar_for helper.
app/helpers/users_helper.rb

Click here to view code image

module UsersHelper

  # Returns the Gravatar for the given user.
  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

Let’s also add a little CSS (or, rather, SCSS) for style (Listing 10.39).

Listing 10.39: CSS for the users index.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

Finally, we’ll add the URL to the users link in the site’s navigation header using users_path, thereby using the last of the unused named routes in Table 7.1. The result appears in Listing 10.40.

Listing 10.40: Adding the URL to the users link.
app/views/layouts/_header.html.erb

Click here to view code image

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", users_path %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", edit_user_path(current_user) %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: "delete" %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

With that, the users index is fully functional, with all tests GREEN:

Listing 10.41: GREEN

$ rails test

On the other hand, as seen in Figure 10.9, it is a bit… lonely. Let’s remedy this sad situation.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. We’ve now filled in all the links in the site layout. Write an integration test for all the layout links, including the proper behavior for logged-in and non-logged-in users. Hint: Use the log_in_as helper and add to the steps shown in Listing 5.32.

Figure 10.9: The users index page with only one user.

10.3.2 Sample Users

In this section, we’ll give our lonely sample user some company. Of course, to create enough users to make a decent users index, we could use our web browser to visit the signup page and make the new users one by one, but a far better solution is to use Ruby to make the users for us.

First, we’ll add the Faker gem to the Gemfile, which will allow us to make sample users with semi-realistic names and email addresses (Listing 10.42).⁹ (Ordinarily, you’d probably want to restrict the faker gem to a development environment, but in the case of the sample app we’ll be using it on our production site as well [Section 10.5].)

9. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

Listing 10.42: Adding the Faker gem to the Gemfile.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',          '5.0.0'
gem 'bcrypt',         '3.1.11'
gem 'faker',          '1.6.3'
.
.
.

Then install as usual:

$ bundle install

Next, we’ll add a Ruby program to seed the database with sample users, for which Rails uses the standard file db/seeds.rb. The result appears in Listing 10.43. (The code in Listing 10.43 is a bit advanced, so don’t worry too much about the details.)

Listing 10.43: A program for seeding the database with sample users.
db/seeds.rb

Click here to view code image

User.create!(name: "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name: name,
               email: email,
               password:              password,
               password_confirmation: password)
end

The code in Listing 10.43 creates an example user with name and email address replicating our previous one, and then makes 99 more. The create! method is just like the create method, except it raises an exception (Section 6.1.4) for an invalid user rather than returning false. This behavior makes debugging easier by avoiding silent errors.

With the code as in Listing 10.43, we can reset the database and then invoke the Rake task using db:seed:¹⁰

10. In principle, these two tasks can be combined in rails db:reset, but as of this writing this command doesn’t work with the latest version of Rails.

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

Seeding the database can be slow and on some systems could take up to a few minutes. Also, some readers have reported that they are unable to run the reset command if the Rails server is running, so you may have to stop the server first before proceeding (Box 1.1).

After running the db:seed Rake task, our application should have 100 sample users. As seen in Figure 10.10, I’ve taken the liberty of associating the first few sample addresses with Gravatars so that they’re not all the default Gravatar image.

Figure 10.10: The users index page with 100 sample users.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that trying to visit the edit page of another user results in a redirect as required by Section 10.2.2.

10.3.3 Pagination

Our original user doesn’t suffer from loneliness any more, but now we have the opposite problem: our user has too many companions, and they all appear on the same page. Right now there are a hundred, which is already a reasonably large number, and on a real site it could be thousands. The solution is to paginate the users, so that (for example) only 30 show up on a page at any one time.

There are several pagination methods available in Rails; we’ll use one of the simplest and most robust, called will_paginate. To use it, we need to include both the will_paginate gem and bootstrap-will_paginate, which configures will_paginate to use Bootstrap’s pagination styles. The updated Gemfile appears in Listing 10.44.¹¹

11. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

Listing 10.44: Including will_paginate in the Gemfile.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',                   '5.0.0'
gem 'bcrypt',                  '3.1.11'
gem 'faker',                   '1.6.3'
gem 'will_paginate',           '3.0.0'
gem 'bootstrap-will_paginate', '0.0.10'
.
.
.

Then run bundle install:

$ bundle install

You should also restart the webserver to ensure that the new gems are loaded properly.

To get pagination working, we need to add some code to the index view telling Rails to paginate the users, and we need to replace User.all in the index action with an object that knows about pagination. We’ll start by adding the special will_paginate method in the view (Listing 10.45); we’ll see in a moment why the code appears both above and below the user list.

Listing 10.45: The users index with pagination.
app/views/users/index.html.erb

Click here to view code image

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>
<%= will_paginate %>

The will_paginate method is a little magical; inside a users view, it automatically looks for an @users object and then displays pagination links to access other pages. The view in Listing 10.45 doesn’t work yet, though, because currently @users contains the results of User.all (Listing 10.36), whereas will_paginate requires that we paginate the results explicitly using the paginate method:

Click here to view code image

$ rails console
>> User.paginate(page: 1)
  User Load (1.5ms) SELECT "users".* FROM "users" LIMIT 30 OFFSET 0
   (1.7ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...

Note that paginate takes a hash argument with key :page and value equal to the page requested. User.paginate pulls the users out of the database one chunk at a time (30 by default), based on the :page parameter. So, for example, page 1 is users 1–30, page 2 is users 31–60, etc. If page is nil, paginate simply returns the first page.

Using the paginate method, we can paginate the users in the sample application by using paginate in place of all in the index action (Listing 10.46). Here the page parameter comes from params [:page], which is generated automatically by will_paginate.

Listing 10.46: Paginating the users in the index action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

The users index page should now be working, appearing as in Figure 10.11. (On some systems, you may have to restart the Rails server at this point.) Because we included will_paginate both above and below the user list, the pagination links appear in both places.

If you now click on either the 2 link or Next link, you’ll get the second page of results, as shown in Figure 10.12.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Confirm at the console that setting the page to nil pulls out the first page of users.

2. What is the Ruby class of the pagination object? How does it compare to the class of User.all?

Figure 10.11: The users index page with pagination.

Figure 10.12: Page 2 of the users index.

10.3.4 Users Index Test

Now that our users index page is working, we’ll write a lightweight test for it, including a minimal test for the pagination from Section 10.3.3. The idea is to log in, visit the index path, verify the first page of users is present, and then confirm that pagination is present on the page. For these last two steps to work, we need to have enough users in the test database to invoke pagination, i.e., more than 30.

We created a second user in the fixtures in Listing 10.23, but 30 or so more users is a lot to create by hand. Luckily, as we’ve seen with the user fixture’s password_digest attribute, fixture files support embedded Ruby, which means we can create 30 additional users as shown in Listing 10.47. (Listing 10.47 also creates a couple of other named users for future reference.)

Listing 10.47: Adding 30 extra users to the fixture.
test/fixtures/users.yml

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name: <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

With the fixtures defined in Listing 10.47, we’re ready to write a test of the users index. First we generate the relevant test:

Click here to view code image

$ rails generate integration_test users_index
      invoke  test_unit
      create    test/integration/users_index_test.rb

The test itself involves checking for a div with the required pagination class and verifying that the first page of users is present. The result appears in Listing 10.48.

Listing 10.48: A test of the users index, including pagination. GREEN
test/integration/users_index_test.rb

Click here to view code image

require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user) get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

The result should be a GREEN test suite:

Listing 10.49: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By commenting out the pagination links in Listing 10.45, confirm that the test in Listing 10.48 goes RED.

2. Confirm that commenting out only one of the calls to will_paginate leaves the tests GREEN. How would you test for the presence of both sets of will_paginate links? Hint: Use a count from Table 5.2.

10.3.5 Partial Refactoring

The paginated users index is now complete, but there’s one improvement I can’t resist including: Rails has some incredibly slick tools for making compact views, and in this section we’ll refactor the index page to use them. Because our code is well-tested, we can refactor with confidence, assured that we are unlikely to break our site’s functionality.

The first step in our refactoring is to replace the user li from Listing 10.45 with a render call (Listing 10.50).

Listing 10.50: The first refactoring attempt in the index view.
app/views/users/index.html.erb

Click here to view code image

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

Here we call render not on a string with the name of a partial, but rather on a user variable of class User;¹² in this context, Rails automatically looks for a partial called _user.html.erb, which we must create (Listing 10.51).

12. The name user is immaterial—we could have written @users.each do |foobar| and then used render foobar. The key is the class of the object—in this case, User.

Listing 10.51: A partial to render a single user.
app/views/users/_user.html.erb

Click here to view code image

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

This is a definite improvement, but we can do even better: we can call render directly on the @users variable (Listing 10.52).

Listing 10.52: The fully refactored users index. GREEN
app/views/users/index.html.erb

Click here to view code image

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">

  <%= render @users %>
  </ul>

<%= will_paginate %>

Here Rails infers that @users is a list of User objects; moreover, when called with a collection of users, Rails automatically iterates through them and renders each one with the _user.html.erb partial (inferring the name of the partial from the name of the class). The result is the impressively compact code in Listing 10.52.

As with any refactoring, you should verify that the test suite is still GREEN after changing the application code:

Listing 10.53: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Comment out the render line in Listing 10.52 and confirm that the resulting tests are RED.

10.4 Deleting Users

Now that the users index is complete, there’s only one canonical REST action left: destroy. In this section, we’ll add links to delete users, as mocked up in Figure 10.13, and define the destroy action necessary to accomplish the deletion. But first, we’ll create the class of administrative users, or admins, authorized to do so.

10.4.1 Administrative Users

We will identify privileged administrative users with a boolean admin attribute in the User model, which will lead automatically to an admin? boolean method to test for admin status. The resulting data model appears in Figure 10.14.

As usual, we add the admin attribute with a migration, indicating the boolean type on the command line:

Click here to view code image

$ rails generate migration add_admin_to_users admin:boolean

Figure 10.13: A mockup of the users index with delete links.

Figure 10.14: The User model with an added admin boolean attribute.

The migration adds the admin column to the users table, as shown in Listing 10.54. Note that we’ve added the argument default: false to add_column in Listing 10.54, which means that users will not be administrators by default. (Without the default: false argument, admin will be nil by default, which is still false, so this step is not strictly necessary. It is more explicit, though, and communicates our intentions more clearly both to Rails and to readers of our code.)

Listing 10.54: The migration to add a boolean admin attribute to users.
db/migrate/[timestamp]_add_admin_to_users.rb

Click here to view code image

class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

Next, we migrate as usual:

$ rails db:migrate

As expected, Rails figures out the boolean nature of the admin attribute and automatically adds the question-mark method admin?:

Click here to view code image

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

Here we’ve used the toggle! method to flip the admin attribute from false to true.

As a final step, let’s update our seed data to make the first user an admin by default (Listing 10.55).

Listing 10.55: The seed data code with an admin user.
db/seeds.rb

Click here to view code image

User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
            admin: true)
99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

Then reset the database:

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

Revisiting strong parameters

You might have noticed that Listing 10.55 makes the user an admin by including admin: true in the initialization hash. This underscores the danger of exposing our objects to the wild web: if we simply passed an initialization hash in from an arbitrary web request, a malicious user could send a PATCH request as follows:¹³

13. Command-line tools such as curl can issue PATCH requests of this form.

patch /users/17?admin=1

This request would make user 17 an admin, which would be a potentially serious security breach.

Because of this danger, it is essential that we only update attributes that are safe to edit through the web. As noted in Section 7.3.2, this is accomplished using strong parameters by calling require and permit on the params hash:

Click here to view code image

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

Note in particular that admin is not in the list of permitted attributes. This is what prevents arbitrary users from granting themselves administrative access to our application. Because of its importance, it’s a good idea to write a test for any attribute that isn’t editable, and writing such a test for the admin attribute is left as an exercise (Section 10.4.1).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By issuing a PATCH request directly to the user path as shown in Listing 10.56, verify that the admin attribute isn’t editable through the web. To be sure your test is covering the right thing, your first step should be to add admin to the list of permitted parameters in user_params so that the initial test is RED.

Listing 10.56: Testing that the admin attribute is forbidden.
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: {
                                      user: { password:              FILL_IN,
                                              password_confirmation: FILL_IN,
                                              admin: FILL_IN } }
    assert_not @other_user.FILL_IN.admin?
  end
  .
  .
  .
end

10.4.2 The destroy Action

The final step needed to complete the Users resource is to add delete links and a destroy action. We’ll start by adding a delete link for each user on the users index page, restricting access to administrative users. The resulting "delete" links will be displayed only if the current user is an admin (Listing 10.57).

Listing 10.57: User delete links (viewable only by admins).
app/views/users/_user.html.erb

Click here to view code image

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

Note the method: :delete argument, which arranges for the link to issue the necessary DELETE request. We’ve also wrapped each link inside an if statement so that only admins can see them. The result for our admin user appears in Figure 10.15.

Figure 10.15: The users index with delete links.

Web browsers can’t send DELETE requests natively, so Rails fakes them with JavaScript. This means that the delete links won’t work if the user has JavaScript disabled. If you must support non-JavaScript-enabled browsers you can fake a DELETE request using a form and a POST request, which works even without JavaScript.¹⁴

14. See the RailsCast on “Destroy Without JavaScript” for details.

To get the delete links to work, we need to add a destroy action (Table 7.1), which finds the corresponding user and destroys it with the Active Record destroy method, finally redirecting to the users index, as seen in Listing 10.58. Because users have to be logged in to delete users, Listing 10.58 also adds :destroy to the logged_in_user before filter.

Listing 10.58: Adding a working destroy action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

  private
  .
  .
  .
end

Note that the destroy action uses method chaining to combine the find and destroy into one line:

Click here to view code image

User.find(params[:id]).destroy

As constructed, only admins can destroy users through the web since only they can see the delete links, but there’s still a terrible security hole: any sufficiently sophisticated attacker could simply issue a DELETE request directly from the command line to delete any user on the site. To secure the site properly, we also need access control on the destroy action, so that only admins can delete users.

As in Section 10.2.1 and Section 10.2.2, we’ll enforce access control using a before filter, this time to restrict access to the destroy action to admins. The resulting admin_user before filter appears in Listing 10.59.

Listing 10.59: A before filter restricting the destroy action to admins.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # Confirms an admin user.
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As the admin user, destroy a few sample users through the web interface. What are the corresponding entries in the server log?

10.4.3 User Destroy Tests

With something as dangerous as destroying users, it’s important to have good tests for the expected behavior. We start by arranging for one of our fixture users to be an admin, as shown in Listing 10.60.

Listing 10.60: Making one of the fixture users an admin.
test/fixtures/users.yml

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name: <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

Following the practice from Section 10.2.1, we’ll put action-level tests of access control in the Users controller test file. As with the logout test in Listing 8.31, we’ll use delete to issue a DELETE request directly to the destroy action. We need to check two cases: first, users who aren’t logged in should be redirected to the login page; second, users who are logged in but who aren’t admins should be redirected to the Home page. The result appears in Listing 10.61.

Listing 10.61: Action-level tests for admin access control. GREEN
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end
  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end
end

Note that Listing 10.61 also makes sure that the user count doesn’t change using the assert_no_difference method (seen before in Listing 7.23).

The tests in Listing 10.61 verify the behavior in the case of an unauthorized (non-admin) user, but we also want to check that an admin can use a delete link to successfully destroy a user. Since the delete links appear on the users index, we’ll add these tests to the users index test from Listing 10.48. The only really tricky part is verifying that a user gets deleted when an admin clicks on a delete link, which we’ll accomplish as follows:

Click here to view code image

assert_difference 'User.count', -1 do
  delete user_path(@other_user)
end

This uses the assert_difference method first seen in Listing 7.33 when creating a user, this time verifying that a user is destroyed by checking that User.count changes by −1 when issuing a delete request to the corresponding user path.

Putting everything together gives the pagination and delete test in Listing 10.62, which includes tests for both admins and non-admins.

Listing 10.62: An integration test for delete links and destroying users. GREEN
test/integration/users_index_test.rb

Click here to view code image

require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end

Note that Listing 10.62 checks for the right delete links, including skipping the test if the user happens to be the admin (which lacks a delete link due to Listing 10.57).

At this point, our deletion code is well-tested, and the test suite should be GREEN:

Listing 10.63: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By commenting out the admin user before filter in Listing 10.59, confirm that the tests go RED.

10.5 Conclusion

We’ve come a long way since introducing the Users controller way back in Section 5.4. Those users couldn’t even sign up; now users can sign up, log in, log out, view their profiles, edit their settings, and see an index of all users—and some can even destroy other users.

As it presently stands, the sample application forms a solid foundation for any website requiring users with authentication and authorization. In Chapter 11 and Chapter 12, we’ll add two additional refinements: an account activation link for newly registered users (verifying a valid email address in the process) and password resets to help users who forget their passwords.

Before moving on, be sure to merge all the changes into the master branch:

Click here to view code image

$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push

You can also deploy the application and even populate the production database with sample users (using the pg:reset task to reset the production database):

Click here to view code image

$ rails test
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
$ heroku restart

Of course, on a real site you probably wouldn’t want to seed it with sample data, but I include it here for purposes of illustration (Figure 10.16). Incidentally, the order of the sample users in Figure 10.16 may vary, and on my system doesn’t match the local version from Figure 10.11; this is because we haven’t specified a default ordering for users when retrieved from the database, so the current order is database-dependent. This doesn’t matter much for users, but it will for microposts, and we’ll address this issue further in Section 13.1.4.

Figure 10.16: The sample users in production.

10.5.1 What We Learned in This Chapter

• Users can be updated using an edit form, which sends a PATCH request to the update action.

• Safe updating through the web is enforced using strong parameters.

• Before filters give a standard way to run methods before particular controller actions.

• We implement an authorization using before filters.

• Authorization tests use both low-level commands to submit particular HTTP requests directly to controller actions and high-level integration tests.

• Friendly forwarding redirects users where they wanted to go after logging in.

• The users index page shows all users, one page at a time.

• Rails uses the standard file db/seeds.rb to seed the database with sample data using rails db:seed.

• Running render @users automatically calls the _user.html.erb partial on each user in the collection.

• A boolean attribute called admin on the User model automatically creates an admin? boolean method on user objects.

• Admins can delete users through the web by clicking on delete links that issue DELETE requests to the Users controller destroy action.

• We can create a large number of test users using embedded Ruby inside fixtures.

Chapter 11. Account Activation

At present, newly registered users immediately have full access to their accounts (Chapter 7); in this chapter, we’ll implement an account activation step to verify that the user controls the email address they used to sign up.¹ This will involve associating an activation token and digest with a user, sending the user an email with a link including the token, and activating the user upon clicking the link. In Chapter 12, we’ll apply similar ideas to allow users to reset their passwords if they forget them. Each of these two features will involve creating a new resource, thereby giving us a chance to see further examples of controllers, routing, and database migrations. In the process, we’ll also have a chance to learn how to send email in Rails, both in development and in production.

1. This chapter is independent of the others, apart from the mailer generation in Listing 11.6, which is used in Chapter 12. Readers can skip to Chapter 12 or to Chapter 13 with minimal discontinuity, although the former will be substantially more challenging due to substantial overlap with this chapter.

Our strategy for handling account activation parallels user login (Section 8.2) and especially remembering users (Section 9.1). The basic sequence appears as follows:

1. Start users in an “unactivated” state.

2. When a user signs up, generate an activation token and corresponding activation digest.

3. Save the activation digest to the database, and then send an email to the user with a link containing the activation token and user’s email address.²

2. We could use the user’s id instead, since it’s already exposed in the URLs of our application, but using email addresses is more future-proof in case we want to obfuscate user ids for any reason (such as to prevent competitors from knowing how many users our application has, for example).

4. When the user clicks the link, find the user by email address, and then authenticate the token by comparing with the activation digest.

5. If the user is authenticated, change the status from “unactivated” to “activated”.

Table 11.1: The analogy between login, remembering, account activation, and password reset.

Because of the similarity with passwords and remember tokens, we will be able to reuse many of the same ideas for account activation (as well as password reset), including the User.digest and User.new_token methods and a modified version of the user.authenticated? method. Table 11.1 illustrates the analogy (including the password reset from Chapter 12).

In Section 11.1, we’ll make a resource and data model for account activations (Section 11.1), and in Section 11.2 we’ll add a mailer for sending account activation emails (Section 11.2). We’ll implement the actual account activation, including a generalized version of the authenticated? method from Table 11.1, in Section 11.3.

11.1 Account Activations Resource

As with sessions (Section 8.1), we’ll model account activations as a resource even though they won’t be associated with an Active Record model. Instead, we’ll include the relevant data (including the activation token and activation status) in the User model itself.

Because we’ll be treating account activations as a resource, we’ll interact with them via a standard REST URL. The activation link will be modifying the user’s activation status, and for such modifications the standard REST practice is to issue a PATCH request to the update action (Table 7.1). The activation link needs to be sent in an email, though, and hence will involve a regular browser click, which issues a GET request instead of PATCH. This design constraint means that we can’t use the update action, but we’ll do the next-best thing and use the edit action instead, which does respond to GET requests.

As usual, we’ll make a topic branch for the new feature:

Click here to view code image

$ git checkout -b account-activation

11.1.1 Account Activations Controller

As with Users and Sessions, the actions (or, in this case, the sole action) for the Account Activations resource will live inside an Account Activations controller, which we can generate as follows:³

3. Because we’ll be using an edit action, we could include edit on the command line, but this would also generate both an edit view and a test, neither of which we’ll turn out to need.

Click here to view code image

$ rails generate controller AccountActivations

As we’ll see in Section 11.2.1, the activation email will involve a URL of the form

Click here to view code image

edit_account_activation_url(activation_token, ...)

which means we’ll need a named route for the edit action. We can arrange for this with the resources line shown in Listing 11.1, which gives the RESTful route shown in Table 11.2.

Table 11.2: RESTful route provided by the Account Activations resource in Listing 11.1.

Listing 11.1: Adding a route for the Account Activations edit action.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
end

We’ll define the edit action itself in Section 11.3.2, after we’ve finished the Account Activations data model and mailers.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that the test suite is still GREEN.

2. Why does Table 11.2 list the _url form of the named route instead of the _path form? Hint: We’re going to use it in an email.

11.1.2 Account Activation Data Model

As discussed in the introduction, we need a unique activation token for use in the activation email. One possibility would be to use a string that’s stored both in the database and included in the activation URL, but this raises security concerns if our database is compromised. For example, an attacker with access to the database could immediately activate newly created accounts (thereby logging in as the user), and could then change the password to gain control.⁴

4. It’s mainly for this reason that we won’t be using the (perhaps slightly misnamed) has_secure_token facility added in Rails 5, which stores the corresponding token in the database as unhashed cleartext.

To prevent such scenarios, we’ll follow the example of passwords (Chapter 6) and remember tokens (Chapter 9) by pairing a publicly exposed virtual attribute with a secure hash digest saved to the database. This way we can access the activation token using

user.activation_token

and authenticate the user with code like

Click here to view code image

user.authenticated?(:activation, token)

(This will require a modification of the authenticated? method defined in Listing 9.6.)

We’ll also add a boolean attribute called activated to the User model, which will allow us to test if a user is activated using the same kind of auto-generated boolean method we saw in Section 10.4.1:

if user.activated? ...

Finally, although we won’t use it in this tutorial, we’ll record the time and date of the activation in case we want it for future reference. The full data model appears in Figure 11.1.

The migration to add the data model from Figure 11.1 adds all three attributes at the command line:

Click here to view code image

$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime

(As noted in Section 8.2.4, the > on the second line is a “line continuation” character inserted automatically by the shell, and should not be typed literally.) As with the admin attribute (Listing 10.54), we’ll add a default boolean value of false to the activated attribute, as shown in Listing 11.2.

Listing 11.2: A migration for account activation (with added index).
db/migrate/[timestamp]_add_activation_to_users.rb

Click here to view code image

class AddActivationToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :activation_digest, :string
           add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end

We then apply the migration as usual:

$ rails db:migrate

Activation Token Callback

Because every newly signed-up user will require activation, we should assign an activation token and digest to each user object before it’s created. We saw a similar idea in Section 6.2.5, where we needed to convert an email address to lowercase before saving a user to the database. In that case, we used a before_save callback combined with the downcase method (Listing 6.32). A before_save callback is automatically called before the object is saved, which includes both object creation and updates, but in the case of the activation digest we only want the callback to fire when the user is created. This requires a before_create callback, which we’ll define as follows:

Figure 11.1: The User model with added account activation attributes.

Click here to view code image

before_create :create_activation_digest

This code, called a method reference, arranges for Rails to look for a method called create_activation_digest and run it before creating the user. (In Listing 6.32, we passed before_save an explicit block, but the method reference technique is generally preferred.) Because the create_activation_digest method itself is only used internally by the User model, there’s no need to expose it to outside users; as we saw in Section 7.3.2, the Ruby way to accomplish this is to use the private keyword:

Click here to view code image

private

  def create_activation_digest
    # Create the token and digest.
  end

All methods defined in a class after private are automatically hidden, as seen in this console session:

Click here to view code image

$ rails console
>> User.first.create_activation_digest
NoMethodError: private method 'create_activation_digest' called for #<User>

The purpose of the before_create callback is to assign the token and corresponding digest, which we can accomplish as follows:

Click here to view code image

self.activation_token  = User.new_token
self.activation_digest = User.digest(activation_token)

This code simply reuses the token and digest methods used for the remember token, as we can see by comparing it with the remember method from Listing 9.3:

Click here to view code image

# Remembers a user in the database for use in persistent sessions.
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

The main difference is the use of update_attribute in the latter case. The reason for the difference is that remember tokens and digests are created for users that already exist in the database, whereas the before_create callback happens before the user has been created, so there’s not yet any attribute to update. As a result of the callback, when a new user is defined with User.new (as in user signup, Listing 7.19), it will automatically get both activation_token and activation_digest attributes; because the latter is associated with a column in the database (Figure 11.1), it will be written to the database automatically when the user is saved.

Putting together the discussion above yields the User model shown in Listing 11.3. As required by the virtual nature of the activation token, we’ve added a second attr_accessor to our model. Note that we’ve taken the opportunity to replace the email downcasing callback from Listing 6.32 with a method reference.

Listing 11.3: Adding account activation code to the User model. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save :downcase_email
  before_create :create_activation_digest
  validates :name, presence: true, length: { maximum: 50 }
  .
  .
  .
  private

    # Converts email to all lower-case.
    def downcase_email
      self.email = email.downcase
    end

    # Creates and assigns the activation token and digest.
    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

Seed and Fixture Users

Before moving on, we should also update our seed data and fixtures so that our sample and test users are initially activated, as shown in Listing 11.4 and Listing 11.5. (The Time.zone.now method is a built-in Rails helper that returns the current timestamp, taking into account the time zone on the server.)

Listing 11.4: Activating seed users by default.
db/seeds.rb

Click here to view code image

User.create!(name: "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true,
             activated: true,
             activated_at: Time.zone.now)
99.times do |n|
   name = Faker::Name.name
   email = "example-#{n+1}@railstutorial.org"
   password = "password"
   User.create!(name: name,
               email: email,
               password: password,
               password_confirmation: password,
               activated: true,
               activated_at: Time.zone.now)
end

Listing 11.5: Activating fixture users.
test/fixtures/users.yml

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true
  activated: true
  activated_at: <%= Time.zone.now %>
archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>
lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>
malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>
<% 30.times do |n| %>
user_<%= n %>:
  name: <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>
<% end %>

To apply the changes in Listing 11.4, reset the database to reseed the data as usual:

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that the test suite is still GREEN after the changes made in this section.

2. By instantiating a User object in the console, confirm that calling the create_activation_digest method raises a NoMethodError due to its being a private method. What is the value of the user’s activation digest?

3. In Listing 6.34, we saw that email downcasing can be written more simply as email.downcase! (without any assignment). Make this change to the downcase_email method in Listing 11.3 and verify by running the test suite that it works.

11.2 Account Activation Emails

With the data modeling complete, we’re now ready to add the code needed to send an account activation email. The method is to add a User mailer using the Action Mailer library, which we’ll use in the Users controller create action to send an email with an activation link. Mailers are structured much like controller actions, with email templates defined as views. These templates will include links with the activation token and email address associated with the account to be activated.

11.2.1 Mailer Templates

As with models and controllers, we can generate a mailer using rails generate, as shown in Listing 11.6.

Listing 11.6: Generating the User mailer.

Click here to view code image

$ rails generate mailer UserMailer account_activation password_reset

In addition to the necessary account_activation method, Listing 11.6 generates the password_reset method we’ll need in Chapter 12.

The command in Listing 11.6 also generates two view templates for each mailer, one for plain-text email and one for HTML email. For the account activation mailer method, they appear as in Listing 11.7 and Listing 11.8. (We’ll take care of the corresponding password reset templates in Chapter 12.)

Listing 11.7: The generated account activation text view.
app/views/user_mailer/account_activation.text.erb

Click here to view code image

UserMailer#account_activation

<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb

Listing 11.8: The generated account activation HTML view.
app/views/user_mailer/account_activation.html.erb

Click here to view code image

<h1>UserMailer#account_activation</h1>

<p>
  <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>

Let’s take a look at the generated mailers to get a sense of how they work (Listing 11.9 and Listing 11.10). We see in Listing 11.9 that there is a default from address common to all mailers in the application, and each method in Listing 11.10 has a recipient’s address as well. (Listing 11.9 also uses a mailer layout corresponding to the email format; although it won’t ever matter in this tutorial, the resulting HTML and plain-text mailer layouts can be found in app/views/layouts.) The generated code also includes an instance variable (@greeting), which is available in the mailer views in much the same way that instance variables in controllers are available in ordinary views.

Listing 11.9: The generated application mailer.
app/mailers/application_mailer.rb

Click here to view code image

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout 'mailer'
end

Listing 11.10: The generated User mailer.
app/mailers/user_mailer.rb

Click here to view code image

class UserMailer < ApplicationMailer
  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.account_activation.subject
  #
  def account_activation
    @greeting = "Hi"

    mail to: "to@example.org"
  end

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.password_reset.subject
  #
  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

To make a working activation email, we’ll first customize the generated template as shown in Listing 11.11. Next, we’ll create an instance variable containing the user (for use in the view), and then mail the result to user.email (Listing 11.12). As seen in Listing 11.12, the mail method also takes a subject key, whose value is used as the email’s subject line.

Listing 11.11: The application mailer with a new default from address.
app/mailers/application_mailer.rb

Click here to view code image

class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end

Listing 11.12: Mailing the account activation link.
app/mailers/user_mailer.rb

Click here to view code image

class UserMailer < ApplicationMailer
  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

As with ordinary views, we can use embedded Ruby to customize the template views—in this case greeting the user by name and including a link to a custom activation link. Our plan is to find the user by email address and then authenticate the activation token, so the link needs to include both the email and the token. Because we’re modeling activations using an Account Activations resource, the token itself can appear as the argument of the named route defined in Listing 11.1:

Click here to view code image

edit_account_activation_url(@user.activation_token, ...)

Recalling that

edit_user_url(user)

produces a URL of the form

Click here to view code image

http://www.example.com/users/1/edit

the corresponding account activation link’s base URL will look like this:

Click here to view code image

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

Here q5lt38hQDc_959PVoo6b7A is a URL-safe base64 string generated by the new_token method (Listing 9.2), and it plays the same role as the user id in /users/1/edit. In particular, in the Activations controller edit action, the token will be available in the params hash as params[:id].

In order to include the email as well, we need to use a query parameter, which in a URL appears as a key-value pair located after a question mark:⁵

5. URLs can contain multiple query parameters, consisting of multiple key-value pairs separated by the ampersand character &, as in /edit?name=Foo%20Bar&email=foo%40example.com.

Click here to view code image

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

Notice that the @ in the email address appears as %40, i.e., it’s “escaped out” to guarantee a valid URL. The way to set a query parameter in Rails is to include a hash in the named route:

Click here to view code image

edit_account_activation_url(@user.activation_token, email: @user.email)

When using named routes in this way to define query parameters, Rails automatically escapes out any special characters. The resulting email address will also be unescaped automatically in the controller and will be available via params[:email].

With the @user instance variable as defined in Listing 11.12, we can create the necessary links using the named edit route and embedded Ruby, as shown in Listing 11.13 and Listing 11.14. Note that the HTML template in Listing 11.14 uses the link_to method to construct a valid link.

Listing 11.13: The account activation text view.
app/views/user_mailer/account_activation.text.erb

Click here to view code image

Hi <%= @user.name %>,

Welcome to the Sample App! Click on the link below to activate your account:

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>

Listing 11.14: The account activation HTML view.
app/views/user_mailer/account_activation.html.erb

Click here to view code image

<h1>Sample App</h1>

<p>Hi <%= @user.name %>,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. At the console, verify that the escape method in the CGI module escapes out the email address as shown in Listing 11.15. What is the escaped value of the string "Don't panic!"?

Listing 11.15: Escaping an email with CGI.escape.

Click here to view code image

>> CGI.escape('foo@example.com')
=> "foo%40example.com"

11.2.2 Email Previews

To see the results of the templates defined in Listing 11.13 and Listing 11.14, we can use email previews, which are special URLs exposed by Rails to let us see what our email messages look like. First, we need to add some configuration to our application’s development environment, as shown in Listing 11.16.

Listing 11.16: Email settings in development.
config/environments/development.rb

Click here to view code image

Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'example.com' # Don't use this literally; use your local dev host
                       # instead
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  .
  .
  .
end

Listing 11.16 uses a host name of 'example.com', but as indicated in the comment you should use the actual host of your development environment. For example, on my system either of the following works (depending on whether I’m using the cloud IDE or the local server):

Click here to view code image

host = 'rails-tutorial-mhartl.c9users.io'    # Cloud IDE

or

Click here to view code image

host = 'localhost:3000'                    # Local server

After restarting the development server to activate the configuration in Listing 11.16, we next need to update the User mailer preview file, which was automatically generated in Section 11.2, as shown in Listing 11.17.

Listing 11.17: The generated User mailer previews.
test/mailers/previews/user_mailer_preview.rb

Click here to view code image

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    UserMailer.account_activation
  end

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end

end

Because the account_activation method defined in Listing 11.12 requires a valid user object as an argument, the code in Listing 11.17 won’t work as written. To fix it, we define a user variable equal to the first user in the development database, and then pass it as an argument to UserMailer.account_activation (Listing 11.18). Note that Listing 11.18 also assigns a value to user.activation_token, which is necessary because the account activation templates in Listing 11.13 and Listing 11.14 need an account activation token. (Because activation_token is a virtual attribute [Section 11.1], the user from the database doesn’t have one.)

Listing 11.18: A working preview method for account activation.
test/mailers/previews/user_mailer_preview.rb

Click here to view code image

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end
end

With the preview code as in Listing 11.18, we can visit the suggested URLs to preview the account activation emails. (If you are using the cloud IDE, you should replace localhost:3000 with the corresponding base URL.) The resulting HTML and text emails appear as in Figure 11.2 and Figure 11.3.

Figure 11.2: A preview of the HTML version of the account activation email.

Figure 11.3: A preview of the text version the account activation email.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Preview the email templates in your browser. What do the Date fields read for your previews?

11.2.3 Email Tests

As a final step, we’ll write a couple of tests to double-check the results shown in the email previews. This isn’t as hard as it sounds, because Rails has generated useful example tests for us (Listing 11.19).

Listing 11.19: The User mailer test generated by Rails.
test/mailers/user_mailer_test.rb

Click here to view code image

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase

  test "account_activation" do
    mail = UserMailer.account_activation
    assert_equal "Account activation", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end

  test "password_reset" do
    mail = UserMailer.password_reset
    assert_equal "Password reset", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end
end

The tests in Listing 11.19 use the powerful assert_match method, which can be used either with a string or a regular expression:

Click here to view code image

assert_match 'foo', 'foobar'     # true
assert_match 'baz', 'foobar'     # false
assert_match /\w+/, 'foobar'     # true
assert_match /\w+/, '$#!*+@'     # false

The test in Listing 11.20 uses assert_match to check that the name, activation token, and escaped email appear in the email’s body. For the last of these, note the use of

CGI.escape(user.email)

to escape the test user’s email, which we met briefly in Section 11.2.1.⁶

6. When I originally wrote this chapter, I couldn’t recall offhand how to escape URLs in Rails, and figuring it out was pure technical sophistication (Box 1.1). What I did was Google “ruby rails escape url”, which led me to find two main possibilities, URI.encode(str) and CGI.escape(str). Trying them both revealed that the latter works. (It turns out there’s a third possibility: the ERB::Util library supplies a url_encode method that has the same effect.)

Listing 11.20: A test of the current email implementation. RED
test/mailers/user_mailer_test.rb

Click here to view code image

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase

  test "account_activation" do
    user = users(:michael) user.activation_token = User.new_token
    mail = UserMailer.account_activation(user)
    assert_equal "Account activation", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.name,               mail.body.encoded
    assert_match user.activation_token,   mail.body.encoded
    assert_match CGI.escape(user.email),  mail.body.encoded
  end
end

Note that Listing 11.20 takes care to add an activation token to the fixture user, which would otherwise be blank. (Listing 11.20 also removes the generated password reset test, which we’ll add back [in modified form] in Section 12.2.2.)

To get the test in Listing 11.20 to pass, we have to configure our test file with the proper domain host, as shown in Listing 11.21.

Listing 11.21: Setting the test domain host.
config/environments/test.rb

Click here to view code image

Rails.application.configure do
  .
  .
  .
  config.action_mailer.delivery_method = :test
  config.action_mailer.default_url_options = { host: 'example.com' }
  .
  .
  .
end

With the code as above, the mailer test should be GREEN:

Listing 11.22: GREEN

$ rails test:mailers

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that the full test suite is still GREEN.

2. Confirm that the test goes RED if you remove the call to CGI.escape in Listing 11.20.

11.2.4 Updating the Users create Action

To use the mailer in our application, we just need to add a couple of lines to the create action used to sign users up, as shown in Listing 11.23. Note that Listing 11.23 has changed the redirect behavior upon signing up. Before, we redirected to the user’s profile page (Section 7.4), but that doesn’t make sense now that we’re requiring account activation. Instead, we now redirect to the root URL.

Listing 11.23: Adding account activation to user signup. RED
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      UserMailer.account_activation(@user).deliver_now
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end

Because Listing 11.23 redirects to the root URL instead of to the profile page and doesn’t log the user in as before, the test suite is currently RED, even though the application is working as designed. We’ll fix this by temporarily commenting out the failing lines, as shown in Listing 11.24. We’ll uncomment these lines and write passing tests for account activation in Section 11.3.3.

Listing 11.24: Temporarily commenting out failing tests. GREEN
test/integration/users_signup_test.rb

Click here to view code image

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_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name: "Example User",
                                         email: "user@example.com",
                                         password: "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    # assert_template 'users/show'
    # assert is_logged_in?
  end
end

If you now try signing up as a new user, you should be redirected as shown in Figure 11.4, and an email like the one shown in Listing 11.25 should be generated. Note that you will not receive an actual email in a development environment, but it will show up in your server logs. (You may have to scroll up a bit to see it.) Section 11.4 discusses how to send email for real in a production environment.

Figure 11.4: The Home page with an activation message after signup.

Listing 11.25: A sample account activation email from the server log.

Click here to view code image

UserMailer#account_activation: processed outbound mail in 292.4ms
Sent mail to michael@michaelhartl.com (47.3ms)
Date: Mon, 06 Jun 2016 20:17:41 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <5755da6518cb4_f2c9222494c7178e@mhartl-rails-tutorial-3045526.mail> Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5755da6513e89_f2c9222494c71639";
charset=UTF-8
Content-Transfer-Encoding: 7bit

----==_mimepart_5755da6513e89_f2c9222494c71639
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit

Hi Michael Hartl,

Welcome to the Sample App! Click on the link below to activate your account:

https://rails-tutorial-mhartl.c9users.io/account_activations/
-L9kBsbIjmrqpJGB0TUKcA/edit?email=michael%40michaelhartl.com

----==_mimepart_5755da6513e89_f2c9222494c71639
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      /* Email styles need to be inline */
    </style>
</head>

<body>
   <h1>Sample App </h1>

<p>Hi Michael Hartl,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<a href="https://rails-tutorial-mhartl.c9users.io/account_activations/
-L9kBsbIjmrqpJGB0TUKcA/edit?email=michael%40michaelhartl.com">Activate</a>
  </body>
</html>

----==_mimepart_5755da6513e89_f2c9222494c71639--

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Sign up as a new user and verify that you’re properly redirected. What is the content of the generated email in the server log? What is the value of the activation token?

2. Verify at the console that the new user has been created but that it is not yet activated.

11.3 Activating the Account

Now that we have a correctly generated email as in Listing 11.25, we need to write the edit action in the Account Activations controller that actually activates the user. As usual, we’ll write a test for this action, and once the code is tested we’ll refactor it to move some functionality out of the Account Activations controller and into the User model.

11.3.1 Generalizing the authenticated? Method

Recall from the discussion in Section 11.2.1 that the activation token and email are available as params[:id] and params[:email], respectively. Following the model of passwords (Listing 8.7) and remember tokens (Listing 9.9), we plan to find and authenticate the user with code something like this:

Click here to view code image

user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])

(As we’ll see in a moment, there will be one extra boolean in the expression above. See if you can guess what it will be.)

The above code uses the authenticated? method to test if the account activation digest matches the given token, but at present this won’t work because that method is specialized to the remember token (Listing 9.6):

Click here to view code image

# Returns true if the given token matches the digest.
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

Here remember_digest is an attribute on the User model, and inside the model we can rewrite it as follows:

self.remember_digest

Somehow, we want to be able to make this variable, so we can call

self.activation_token

instead by passing in the appropriate parameter to the authenticated? method.

The solution involves our first example of metaprogramming, which is essentially a program that writes a program. (Metaprogramming is one of Ruby’s strongest suits, and many of the “magic” features of Rails are due to its use of Ruby metaprogramming.) The key in this case is the powerful send method, which lets us call a method with a name of our choice by “sending a message” to a given object. For example, in this console session we use send on a native Ruby object to find the length of an array:

Click here to view code image

$ rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

Here we see that passing the symbol :length or string "length" to send is equivalent to calling the length method on the given object. As a second example, we’ll access the activation_digest attribute of the first user in the database:

Click here to view code image

>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"

Note in the last example that we’ve defined an attribute variable equal to the symbol :activation and used string interpolation to build up the proper argument to send. This would work also with the string 'activation', but using a symbol is more conventional, and in either case

"#{attribute}_digest"

becomes

"activation_digest"

once the string is interpolated. (We saw how symbols are interpolated as strings in Section 7.4.2.)

Based on this discussion of send, we can rewrite the current authenticated? method as follows:

Click here to view code image

def authenticated?(remember_token)
  digest = self.send("remember_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(remember_token)
end

With this template in place, we can generalize the method by adding a function argument with the name of the digest, and then use string interpolation as above:

Click here to view code image

def authenticated?(attribute, token)
  digest = self.send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

(Here we have renamed the second argument token to emphasize that it’s now generic.) Because we’re inside the user model, we can also omit self, yielding the most idiomatically correct version:

Click here to view code image

def authenticated?(attribute, token)
  digest = send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

We can now reproduce the previous behavior of authenticated? by invoking it like this:

Click here to view code image

user.authenticated?(:remember, remember_token)

Applying this discussion to the User model yields the generalized authenticated? method shown in Listing 11.26.

Listing 11.26: A generalized authenticated? method. RED
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns true if the given token matches the digest.
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

The caption to Listing 11.26 indicates a RED test suite:

Listing 11.27:RED

$ rails test

The reason for the failure is that the current_user method (Listing 9.9) and the test for nil digests (Listing 9.17) both use the old version of authenticated?, which expects one argument instead of two. To fix this, we simply update the two cases to use the generalized method, as shown in Listing 11.28 and Listing 11.29.

Listing 11.28: Using the generalized authenticated? method in current_user. RED
app/helpers/sessions_helper.rb

Click here to view code image

module SessionsHelper
  .
  .
  .
  # Returns the current logged-in user (if any).
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

Listing 11.29: Using the generalized authenticated? method in the User test. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?(:remember, '')
  end
end

At this point, the tests should be GREEN:

Listing 11.30: GREEN

$ rails test

Refactoring the code as above is incredibly more error-prone without a solid test suite, which is why we went to such trouble to write good tests in Section 9.1.2 and Section 9.3.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Create and remember a new user at the console. What are the user’s remember and activation tokens? What are the corresponding digests?

2. Using the generalized authenticated? method from Listing 11.26, verify that the user is authenticated according to both the remember token and the activation token.

11.3.2 Activation edit Action

With the authenticated? method as in Listing 11.26, we’re now ready to write an edit action that authenticates the user corresponding to the email address in the params hash. Our test for validity will look like this:

Click here to view code image

if user && !user.activated? && user.authenticated?(:activation, params[:id])

Note the presence of !user.activated?, which is the extra boolean alluded to above. This prevents our code from activating users who have already been activated, which is important because we’ll be logging in users upon confirmation and we don’t want to allow attackers who manage to obtain the activation link to log in as the user.

If the user is authenticated according to the booleans above, we need to activate the user and update the activated_at timestamp:⁷

7. Here we use two calls to update_attribute rather than a single call to updated_attributes because (per Section 6.1.5) the latter would run the validations. Lacking in this case the user password, these validations would fail.

Click here to view code image

user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)

This leads to the edit action shown in Listing 11.31. Note also that Listing 11.31 handles the case of an invalid activation token; this should rarely happen, but it’s easy enough to redirect in this case to the root URL.

Listing 11.31: An edit action to activate accounts.
app/controllers/account_activations_controller.rb

Click here to view code image

class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated, true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

With the code in Listing 11.31, you should now be able to paste in the URL from Listing 11.25 to activate the relevant user. For example, on my system I visited the URL

Click here to view code image

http://rails-tutorial-mhartl.c9users.io/account_activations/ fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com

and got the result shown in Figure 11.5.

Of course, currently user activation doesn’t actually do anything, because we haven’t changed how users log in. In order to have account activation mean something, we need to allow users to log in only if they are activated. As shown in Listing 11.32, the way to do this is to log the user in as usual if user.activated? is true; otherwise, we redirect to the root URL with a warning message (Figure 11.6).

Figure 11.5: The profile page after a successful activation.

Figure 11.6: The warning message for a not-yet-activated user.

Listing 11.32: Preventing unactivated users from logging in.
app/controllers/sessions_controller.rb

Click here to view code image

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      if user.activated?
        log_in user
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        redirect_back_or user
      else
        message = "Account not activated. "
        message += "Check your email for the activation link."
        flash[:warning] = message
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

With that, apart from one refinement, the basic functionality of user activation is done. (That refinement is preventing unactivated users from being displayed, which is left as an exercise [Section 11.3.3].) In Section 11.3.3, we’ll complete the process by adding some tests and then doing a little refactoring.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Paste in the URL from the email generated in Section 11.2.4. What is the activation token?

2. Verify at the console that the User is authenticated according to the activation token in the URL from the previous exercise. Is the user now activated?

11.3.3 Activation Test and Refactoring

In this section, we’ll add an integration test for account activation. Because we already have a test for signing up with valid information, we’ll add the steps to the test developed in Section 7.4.4 (Listing 7.33). There are quite a few steps, but they are mostly straight-forward; see if you can follow along in Listing 11.33. (The highlights in Listing 11.33 indicate lines that are especially important or easy to miss, but there are other new lines as well, so take care to add them all.)

Listing 11.33: Adding account activation to the user signup test. GREEN
test/integration/users_signup_test.rb

Click here to view code image

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end

  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_template 'users/new'
  assert_select 'div#error_explanation'
  assert_select 'div.field_with_errors'
end

test "valid signup information with account activation" do
  get signup_path
  assert_difference 'User.count', 1 do
    post users_path, params: { user: { name: "Example User",
                                       email: "user@example.com",
                                       password: "password",
                                       password_confirmation: "password" } }
  end
  assert_equal 1, ActionMailer::Base.deliveries.size
  user = assigns(:user)
  assert_not user.activated?
  # Try to log in before activation.
  log_in_as(user)
  assert_not is_logged_in?
  # Invalid activation token
  get edit_account_activation_path("invalid token", email: user.email)
  assert_not is_logged_in?
  # Valid token, wrong email
  get edit_account_activation_path(user.activation_token, email: 'wrong')
  assert_not is_logged_in?
  # Valid activation token
  get edit_account_activation_path(user.activation_token, email: user.email)
  assert user.reload.activated?
  follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end

There’s a lot of code in Listing 11.33, but the only completely novel code is in the line

Click here to view code image

assert_equal 1, ActionMailer::Base.deliveries.size

This code verifies that exactly 1 message was delivered. Because the deliveries array is global, we have to reset it in the setup method to prevent our code from breaking if any other tests deliver email (as will be the case in Chapter 12). Listing 11.33 also uses the assigns method for the first time in the main tutorial; as explained in a Chapter 9 exercise (Section 9.3.1), assigns lets us access instance variables in the corresponding action. For example, the Users controller’s create action defines an @user variable (Listing 11.23), so we can access it in the test using assigns(:user). Finally, note that Listing 11.33 restores the lines we commented out in Listing 11.24.

At this point, the test suite should be GREEN:

Listing 11.34: GREEN

$ rails test

With the test in Listing 11.33, we’re ready to refactor a little by moving some of the user manipulation out of the controller and into the model. In particular, we’ll make an activate method to update the user’s activation attributes and a send_activation_email to send the activation email. The extra methods appear in Listing 11.35, and the refactored application code appears in Listing 11.36 and Listing 11.37.

Listing 11.35: Adding user activation methods to the User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Activates an account.
  def activate
    update_attribute(:activated, true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # Sends activation email.
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private
    .
    .
    .
end

Listing 11.36: Sending email via the user model object.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end

Listing 11.37: Account activation via the user model object.
app/controllers/account_activations_controller.rb

Click here to view code image

class AccountActivationsController < ApplicationController
  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

Note that Listing 11.35 eliminates the use of user., which would break inside the User model because there is no such variable:

Click here to view code image

-user.update_attribute(:activated,    true)
-user.update_attribute(:activated_at, Time.zone.now)
+update_attribute(:activated,    true)
+update_attribute(:activated_at, Time.zone.now)

(We could have switched from user to self, but recall from Section 6.2.5 that self is optional inside the model.) It also changes @user to self in the call to the User mailer:

Click here to view code image

-UserMailer.account_activation(@user).deliver_now
+UserMailer.account_activation(self).deliver_now

These are exactly the kinds of details that are easy to miss during even a simple refactoring but will be caught by a good test suite. Speaking of which, the test suite should still be GREEN:

Listing 11.38: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In Listing 11.35, the activate method makes two calls to the update_attribute, each of which requires a separate database transaction. By filling in the template shown in Listing 11.39, replace the two update_attribute calls with a single call to update_columns, which hits the database only once. After making the changes, verify that the test suite is still GREEN.

2. Right now all users are displayed on the user index page at /users and are visible via the URL /users/:id, but it makes sense to show users only if they are activated. Arrange for this behavior by filling in the template shown in Listing 11.40.⁸ (This uses the Active Record where method, which we’ll learn more about in Section 13.3.3.)

8. Note that Listing 11.40 uses and in place of &&. The two are nearly identical, but the latter operator has a higher precedence, which binds too tightly to root_url in this case. We could fix the problem by putting root_url in parentheses, but the idiomatically correct way to do it is to use and instead.

3. To test the code in the previous exercise, write integration tests for both /users and /users/:id.

Listing 11.39: A template for using update_columns.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save :downcase_email
  before_create :create_activation_digest
  .
  .
  .
  # Activates an account.
  def activate
    update_columns(activated: FILL_IN, activated_at: FILL_IN)
  end

  # Sends activation email.
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private

    # Converts email to all lower-case.
    def downcase_email
      self.email = email.downcase
    end

    # Creates and assigns the activation token and digest.
    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

Listing 11.40: A template for code to show only active users.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.where(activated: FILL_IN).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless FILL_IN
  end
  .
  .
  .
end

11.4 Email in Production

Now that we’ve got account activations working in development, in this section we’ll configure our application so that it can actually send email in production. We’ll first get set up with a free service to send email, and then configure and deploy our application.

To send email in production, we’ll use SendGrid, which is available as an add-on at Heroku for verified accounts. (This requires adding credit card information to your Heroku account, but there is no charge when verifying an account.) For our purposes, the “starter” tier (which as of this writing is limited to 400 emails a day but costs nothing) is the best fit. We can add it to our app as follows:

Click here to view code image

$ heroku addons:create sendgrid:starter

(This might fail on systems with an older version of Heroku’s command-line interface. In this case, either upgrade to the latest Heroku toolbelt or try the older syntax heroku addons:add sendgrid:starter.)

To configure our application to use SendGrid, we need to fill out the SMTP settings for our production environment. As shown in Listing 11.41, you will also have to define a host variable with the address of your production website.

Listing 11.41: Configuring Rails to use SendGrid in production.
config/environments/production.rb

Click here to view code image

Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '<your heroku app>.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address         => 'smtp.sendgrid.net',
    :port            => '587',
    :authentication  => :plain,
    :user_name       => ENV['SENDGRID_USERNAME'],
    :password        => ENV['SENDGRID_PASSWORD'],
    :domain          => 'heroku.com',
    :enable_starttls_auto => true
  }
  .
  .
  .
end

The email configuration in Listing 11.41 includes the user_name and password of the SendGrid account, but note that they are accessed via the ENV environment variable instead of being hard-coded. This is a best practice for production applications, which for security reasons should never expose sensitive information such as raw passwords in source code. In the present case, these variables are configured automatically via the SendGrid add-on, but we’ll see an example in Section 13.4.4 where we’ll have to define them ourselves. In case you’re curious, you can view the environment variables used in Listing 11.41 as follows:

Click here to view code image

$ heroku config:get SENDGRID_USERNAME
$ heroku config:get SENDGRID_PASSWORD

At this point, you should merge the topic branch into master:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation

Then push up to the remote repository and deploy to Heroku:

Click here to view code image

$ rails test
$ git push
$ git push heroku
$ heroku run rails db:migrate

Once the Heroku deploy has finished, try signing up for the sample application in production using an email address you control. You should get an activation email as implemented in Section 11.2 and shown in Figure 11.7. Clicking on the link should activate the account as promised, as shown in Figure 11.8.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Sign up for a new account in production. Did you get the email?

2. Click on the link in the activation email to confirm that it works. What is the corresponding entry in the server log? Hint: Run heroku logs at the command line.

Figure 11.7: An account activation email sent in production.

Figure 11.8: Successful account activation in production.

11.5 Conclusion

With the added account activation, our sample application’s sign up, log in, and log out machinery is nearly complete. The only significant feature left is allowing users to reset their passwords if they forget them. As we’ll see in Chapter 12, password reset shares many features with account activation, which means that we’ll be able to put the knowledge we’ve gained in this chapter to good use.

11.5.1 What We Learned in This Chapter

• Like sessions, account activations can be modeled as a resource despite not being Active Record objects.

• Rails can generate Action Mailer actions and views to send email.

• Action Mailer supports both plain-text and HTML mail.

• As with ordinary actions and views, instance variables defined in mailer actions are available in mailer views.

• Account activations use a generated token to create a unique URL for activating users.

• Account activations use a hashed activation digest to securely identify valid activation requests.

• Both mailer tests and integration tests are useful for verifying the behavior of the User mailer.

• We can send email in production using SendGrid.

Chapter 12. Password Reset

Having completed account activation (and thereby verified the user’s email address) in Chapter 11, we’re now in a good position to implement password reset, and thereby handle the common case of users forgetting their passwords.¹ As we’ll see, many of the steps are similar to those for account activation, and we will have several opportunities to apply the lessons learned in Chapter 11. The beginning is different, though; unlike account activation, implementing password resets requires both a change to one of our views and two new forms (to handle email and new password submission).

1. This chapter is independent of the others apart from using the mailer generation in Listing 11.6, but it closely parallels Chapter 11, so it’s much easier if you’ve followed that chapter first.

Before writing any code, let’s mock up the expected sequence for resetting passwords. We’ll start by adding a “forgot password” link to the sample application’s login form (Figure 12.1). The “forgot password” link will go to a page with a form that takes in an email address and sends an email containing a password reset link (Figure 12.2). The reset link will go to a form for resetting the user’s password (with confirmation), as shown in Figure 12.3.

If you followed Chapter 11, you already have a mailer for password resets, which was generated in Section 11.2 (Listing 11.6). In this section, we’ll complete the necessary preliminaries by adding a resource and data model for password resets (Section 12.1) to go along with the mailer. We’ll implement the actual password reset in Section 12.3.

Figure 12.1: A mockup of a “forgot password” link.

Similar to account activations, our general plan is to make a Password Resets resource, with each password reset consisting of a reset token and corresponding reset digest. The primary sequence goes like this:

1. When a user requests a password reset, find the user by the submitted email address.

2. If the email address exists in the database, generate a reset token and corresponding reset digest.

3. Save the reset digest to the database, and then send an email to the user with a link containing the reset token and user’s email address.

4. When the user clicks the link, find the user by email address, and then authenticate the token by comparing it to the reset digest.

5. If authenticated, present the user with the form for changing the password.

Figure 12.2: A mockup of the “forgot password” form.

12.1 Password Resets Resource

As with sessions (Section 8.1) and account activations (Chapter 11), we’ll model password resets as a resource even though they won’t be associated with an Active Record model. Instead, we’ll include the relevant data (including the reset token) in the User model itself.

Figure 12.3: A mockup of the reset password form.

Because we’ll be treating password resets as a resource, we’ll interact with them via the standard REST URLs. Unlike the activation link, which required only an edit action, in this case we’ll be rendering both new and edit forms for manipulating password resets, as well as creating and updating them, so we’ll end up using four RESTful routes in total.

As usual, we’ll make a topic branch for the new feature:

Click here to view code image

$ git checkout -b password-reset

12.1.1 Password Resets Controller

Our first step is to generate a controller for the Password Resets resource, in this case making both new and edit actions per the discussion above:

Click here to view code image

$ rails generate controller PasswordResets new edit --no-test-framework

Note that we’ve included a flag to skip generating tests. This is because we don’t need the controller tests, preferring instead to build on the integration test from Section 11.3.3.

Because we’ll need forms both for creating new password resets (Figure 12.2) and for updating them by changing the password in the User model (Figure 12.3), we need routes for new, create, edit, and update. We can arrange this with the resources line shown in Listing 12.1.

Listing 12.1: Adding a resource for password resets.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
   root   'static_pages#home'
   get    '/help',    to: 'static_pages#help'
   get    '/about',   to: 'static_pages#about'
   get    '/contact', to: 'static_pages#contact'
   get    '/signup',  to: 'users#new'
   get    '/login',   to: 'sessions#new'
   post   '/login',   to: 'sessions#create'
   delete '/logout',  to: 'sessions#destroy'
   resources :users
   resources :account_activations, only: [:edit]
   resources :password_resets, only:[:new, :create, :edit, :update]
end

The code in Listing 12.1 arranges for the RESTful routes shown in Table 12.1. In particular, the first route in Table 12.1 gives a link to the “forgot password” form via

new_password_reset_path

as seen in Listing 12.2 and Figure 12.4.

Table 12.1: RESTful routes provided by the Password Resets resource in Listing 12.1.

Listing 12.2: Adding a link to password resets.
app/views/sessions/new.html.erb

Click here to view code image

<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
   <div class="col-md-6 col-md-offset-3">
      <%= form_for(:session, url: login_path) do |f| %>
         <%= f.label :email %>
         <%= f.email_field :email, class: 'form-control' %>
         <%= f.label :password %>
         <%= link_to "(forgot password)", new_password_reset_path %>
         <%= f.password_field :password, class: 'form-control' %>
         <%= f.label :remember_me, class: "checkbox inline" do %>
            <%= f.check_box :remember_me %>
            Remember me on this computer
         <% end %>
         <%= f.submit "Log in", class: "btn btn-primary" %>
      <% end %>
      <p>New user? <%= link_to "Sign up now!", signup_path %></p>
   </div>
</div>

Figure 12.4: The login page with a “forgot password” link.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that the test suite is still GREEN.

2. Why does Table 12.1 list the _url form of the edit named route instead of the _path form? Hint: The answer is the same as for the similar account activations exercise (Section 11.1.1).

12.1.2 New Password Resets

To create new password resets, we first need to define the data model, which is similar to the one used for account activation (Figure 11.1). Following the pattern set by remember tokens (Chapter 9) and account activation tokens (Chapter 11), password resets will pair a virtual reset token for use in the reset email with a corresponding reset digest for retrieving the user. If we instead stored an unhashed token, an attacker with access to the database could send a reset request to the user’s email address and then use the token and email to visit the corresponding password reset link, thereby gaining control of the account. Using a digest for password resets is thus essential. As an additional security precaution, we’ll also plan to expire the reset link after a couple of hours, which requires recording the time when the reset gets sent. The resulting reset_digest and reset_sent_at attributes appear in Figure 12.5.

The migration to add the attributes from Figure 12.5 appears as follows:

Click here to view code image

$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime

Figure 12.5: The User model with added password reset attributes.

(As before, the > on the second line is a “line continuation” character inserted automatically by the shell, and should not be typed literally.) We then migrate as usual:

$ rails db:migrate

To make the view for new password resets, we’ll work in analogy with the previous form for making a new non–Active Record resource, namely, the login form (Listing 8.4) for creating a new session, shown again in Listing 12.3 for reference.

Listing 12.3: Reviewing the code for the login form.
app/views/sessions/new.html.erb

Click here to view code image

<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
   <div class="col-md-6 col-md-offset-3">
      <%= form_for(:session, url: login_path) do |f| %>
         <%= f.label :email %>
         <%= f.email_field :email, class: 'form-control' %>
         <%= f.label :password %>
         <%= f.password_field :password, class: 'form-control' %>
         <%= f.label :remember_me, class: "checkbox inline" do %>
            <%= f.check_box :remember_me %>
            <span>Remember me on this computer</span>
         <% end %>
         <%= f.submit "Log in", class: "btn btn-primary" %>
      <% end %>
      <p>New user? <%= link_to "Sign up now!", signup_path %></p>
   </div>
</div>

The new password resets form has a lot in common with Listing 12.3; the most important differences are the use of a different resource and URL in the call to form_for and the omission of the password attribute. The result appears in Listing 12.4 and Figure 12.6.

Listing 12.4: A new password reset view.
app/views/password_resets/new.html.erb

Click here to view code image

<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
   <div class="col-md-6 col-md-offset-3">
      <%= form_for(:password_reset, url: password_resets_path) do |f| %>
         <%= f.label :email %>
         <%= f.email_field :email, class: 'form-control' %>
         <%= f.submit "Submit", class: "btn btn-primary" %>
      <% end %>
   </div>
</div>

Figure 12.6: The “forgot password” form.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Why does the form_for in Listing 12.4 use :password_reset instead of @password_reset?

12.1.3 Password Reset create Action

Upon submitting the form in Figure 12.6, we need to find the user by email address and update its attributes with the password reset token and sent-at timestamp. We then redirect to the root URL with an informative flash message. As with login (Listing 8.11), in the case of an invalid submission we re-render the new page with a flash.now message. The results appear in Listing 12.5.

Listing 12.5: A create action for password resets.
app/controllers/password_resets_controller.rb

Click here to view code image

class PasswordResetsController < ApplicationController
   def new
   end
   def create
      @user = User.find_by(email: params[:password_reset][:email].downcase)
      if @user
         @user.create_reset_digest
         @user.send_password_reset_email
         flash[:info] = "Email sent with password reset instructions"
         redirect_to root_url
      else
         flash.now[:danger] = "Email address not found"
         render 'new'
      end
   end
   def edit
   end
end

The code in the User model parallels the create_activation_digest method used in the before_create callback (Listing 11.3), as seen in Listing 12.6.

Listing 12.6: Adding password reset methods to the User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
   attr_accessor : remember_token, :activation_token, :reset_token
   before_save :downcase_email
   before_create :create_activation_digest
   .
   .
   .
   # Activates an account.
   def activate
      update_attribute(:activated, true)
      update_attribute(:activated_at, Time.zone.now)
   end
   # Sends activation email.
   def send_activation_email
      UserMailer.account_activation(self).deliver_now
   end
   # Sets the password reset attributes.
   def create_reset_digest
      self.reset_token = User.new_token
      update_attribute(:reset_digest, User.digest(reset_token))
      update_attribute(:reset_sent_at, Time.zone.now)
   end
   # Sends password reset email.
   def send_password_reset_email
      UserMailer.password_reset(self).deliver_now
   end
   private
      # Converts email to all lower-case.
      def downcase_email
         self.email = email.downcase
      end
      # Creates and assigns the activation token and digest.
      def create_activation_digest
         self.activation_token = User.new_token
         self.activation_digest = User.digest(activation_token)
      end
end

As shown in Figure 12.7, at this point the application’s behavior for invalid email addresses is already working. To get the application working upon submission of a valid email address as well, we need to define a password reset mailer method.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

Figure 12.7: The “forgot password” form for an invalid email address.

1. Submit a valid email address to the form shown in Figure 12.6. What error message do you get?

2. Confirm at the console that the user in the previous exercise has valid reset_digest and reset_sent_at attributes, despite the error. What are the attribute values?

12.2 Password Reset Emails

We left Section 12.1 with a nearly working create action in the Password Resets controller. The only thing missing is the method to deliver valid password reset emails.

If you followed Section 11.1, you already have a default password_reset method in app/mailers/user_mailer.rb as a result of the User mailer generation in Listing 11.6. If you skipped Chapter 11, you can just copy the code below (omitting the account_activation and related methods) and create the missing files as necessary.

12.2.1 Password Reset Mailer and Templates

In Listing 12.6, we applied the design pattern implemented as a refactoring in Section 11.3.3 by putting the User mailer directly in the model (Listing 12.6):

Click here to view code image

UserMailer.password_reset(self).deliver_now

The password reset mailer method needed to get this working is nearly identical to the mailer for account activation developed in Section 11.2. We first create a password_ reset method in the user mailer (Listing 12.7), and then define view templates for plain-text email (Listing 12.8) and HTML email (Listing 12.9).

Listing 12.7: Mailing the password reset link.
app/mailers/user_mailer.rb

Click here to view code image

class UserMailer < ApplicationMailer
   def account_activation(user)
      @user = user
      mail to: user.email, subject: "Account activation"
   end
   def password_reset(user)
      @user = user
      mail to: user.email, subject: "Password reset"
   end
end

Listing 12.8: The password reset plain-text email template.
app/views/user_mailer/password_reset.text.erb

Click here to view code image

To reset your password click the link below:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and your password will stay as it is.

Listing 12.9: The password reset HTML email template.
app/views/user_mailer/password_reset.html.erb

Click here to view code image

<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
                                                      email: @user.email) %>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and your password will stay as it is.
</p>

As with account activation emails (Section 11.2), we can preview password reset emails using the Rails email previewer. The code is exactly analogous to Listing 11.18, as shown in Listing 12.10.

Listing 12.10: A working preview method for password reset.
test/mailers/previews/user_mailer_preview.rb

Click here to view code image

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
   # Preview this email at
   # http://localhost:3000/rails/mailers/user_mailer/account_activation
   def account_activation
      user = User.first
      user.activation_token = User.new_token
      UserMailer.account_activation(user)
   end
   # Preview this email at
   # http://localhost:3000/rails/mailers/user_mailer/password_reset
   def password_reset
      user = User.first
      user.reset_token = User.new_token
      UserMailer.password_reset(user)
   end
end

With the code in Listing 12.10, the HTML and text email previews appear as in Figure 12.8 and Figure 12.9.

With the code in Listing 12.7, Listing 12.8, and Listing 12.9, submission of a valid email address appears as shown in Figure 12.10. The corresponding email appears in the server log and should look something like Listing 12.11.

Listing 12.11: A sample password reset email from the server log.

Click here to view code image

Sent mail to michael@michaelhartl.com (66.8ms)
Date: Mon, 06 Jun 2016 22:00:41 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <5407babbee139_8722b257d04576a@mhartl-rails-tutorial-953753.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
   boundary="--==_mimepart_5407babbe3505_8722b257d045617";
   charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/plain;
   charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
http://rails-tutorial-mhartl.c9users.io/password_resets/3BdBrXeQZSWqFIDRN8cxHA/ edit?email=michael%40michaelhartl.com
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and your password will stay as it is.
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/html;
   charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<a href="http://rails-tutorial-mhartl.c9users.io/ password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com">Reset password</a>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and your password will stay as it is.
</p>
----==_mimepart_5407babbe3505_8722b257d045617--

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Preview the email templates in your browser. What do the Date fields read for your previews?

2. Submit a valid email address to the new password reset form. What is the content of the generated email in the server log?

3. At the console, find the user object corresponding to the email address from the previous exercise and verify that it has valid reset_digest and reset_sent_at attributes.

Figure 12.8: A preview of the HTML version of the password reset email.

Figure 12.9: A preview of the text version of the password reset email.

Figure 12.10: The result of submitting a valid email address.

12.2.2 Email Tests

In analogy with the account activation mailer method test (Listing 11.20), we’ll write a test of the password reset mailer method, as shown in Listing 12.12.

Listing 12.12: Adding a test of the password reset mailer method. GREEN
test/mailers/user_mailer_test.rb

Click here to view code image

require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
   test "account_activation" do
      user = users(:michael)
      user.activation_token = User.new_token
      mail = UserMailer.account_activation(user)
      assert_equal "Account activation", mail.subject
      assert_equal [user.email], mail.to
      assert_equal ["noreply@example.com"], mail.from
      assert_match user.name,               mail.body.encoded
      assert_match user.activation_token,   mail.body.encoded
      assert_match CGI.escape(user.email),  mail.body.encoded
   end
   test "password_reset" do
      user = users(:michael)
      user.reset_token = User.new_token
      mail = UserMailer.password_reset(user)
      assert_equal "Password reset", mail.subject
      assert_equal [user.email], mail.to
      assert_equal ["noreply@example.com"], mail.from
      assert_match user.reset_token,        mail.body.encoded
      assert_match CGI.escape(user.email),  mail.body.encoded
   end
end

At this point, the test suite should be GREEN:

Listing 12.13: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Run just the mailer tests. Are they GREEN?

2. Confirm that the test goes RED if you remove the second call to CGI.escape in Listing 12.12.

12.3 Resetting the Password

Now that we have a correctly generated email as in Listing 12.11, we need to write the edit action in the Password Resets controller that actually resets the user’s password. As in Section 11.3.3, we’ll write a thorough integration test as well.

12.3.1 Reset edit Action

Password reset emails such as that shown in Listing 12.11 contain links of the following form:

Click here to view code image

http://example.com/password_resets/
       3BdBrXeQZSWqFIDRN8cxHA/edit?email=fu%40bar.com

To get these links to work, we need a form for resetting passwords. The task is similar to updating users via the user edit view (Listing 10.2), but in this case with only password and confirmation fields.

There’s an additional complication, though: we expect to find the user by email address, which means we need its value in both the edit and update actions. The email will automatically be available in the edit action because of its presence in the link above, but after we submit the form its value will be lost. The solution is to use a hidden field to place (but not display) the email on the page, and then submit it along with the rest of the form’s information. The result appears in Listing 12.14.

Listing 12.14: The form to reset a password.
app/views/password_resets/edit.html.erb

Click here to view code image

<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
   <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
         <%= render 'shared/error_messages' %>
         <%= hidden_field_tag :email, @user.email %>
         <%= 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 "Update password", class: "btn btn-primary" %>
      <% end %>
   </div>
</div>

Note that Listing 12.14 uses the form tag helper

Click here to view code image

hidden_field_tag :email, @user.email

instead of

Click here to view code image

f.hidden_field :email, @user.email

because the reset link puts the email in params [:email], whereas the latter would put it in params [:user] [:email].

To get the form to render, we need to define an @user variable in the Password Resets controller’s edit action. As with account activation (Listing 11.31), this involves finding the user corresponding to the email address in params[:email]. We then need to verify that the user is valid, i.e., that it exists, is activated, and is authenticated according to the reset token from params[:id] (using the generalized authenticated? method defined in Listing 11.26). Because the existence of a valid @user is needed in both the edit and update actions, we’ll put the code to find and validate it in a couple of before filters, as shown in Listing 12.15.

Listing 12.15: The edit action for password reset.
app/controllers/password_resets_controller.rb

Click here to view code image

class PasswordResetsController < ApplicationController
   before_action :get_user, only: [:edit, :update]
   before_action :valid_user, only: [:edit, :update]
   .
   .
   .
   def edit
   end
   private
      def get_user
         @user = User.find_by(email: params[:email])
      end
      # Confirms a valid user.
      def valid_user
         unless ( @user && @user.activated? &&
               @user.authenticated?( :reset, params[ :id]))
            redirect_to root_url
         end
      end
end

In Listing 12.15, compare the use of

Click here to view code image

authenticated?(:reset, params[:id])

to

Click here to view code image

authenticated?(:remember, cookies[:remember_token])

in Listing 11.28 and

Click here to view code image

authenticated?(:activation, params[:id])

in Listing 11.31. Together, these three uses complete the authentication methods shown in Table 11.1.

With the code as above, following the link from Listing 12.11 should render a password reset form. The result appears in Figure 12.11.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Follow the link in the email from the server log in Section 12.2.1. Does it properly render the form as shown in Figure 12.11?

2. What happens if you submit the form from the previous exercise?

Figure 12.11: The password reset form.

12.3.2 Updating the Reset

Unlike the Account Activations edit method, which simply toggles the user from “inactive” to “active”, the edit method for Password Resets is a form, which must therefore submit to a corresponding update action. To define this update action, we need to consider four cases:

1. An expired password reset

2. A failed update due to an invalid password

3. A failed update (which initially looks “successful”) due to a blank password and confirmation

4. A successful update

Cases (1), (2), and (4) are fairly straightforward, but Case (3) is non-obvious and is explained in more detail below.

Case (1) applies to both the edit and update actions, and so logically belongs in a before filter:

Click here to view code image

before_action :check_expiration, only: [:edit, :update] # Case (1)

This requires defining a private check_expiration method, and while we’re at it we also need to add to the valid_user filter the requirement that the user be activated:

Click here to view code image

# Confirms a valid user.
def valid_user
   unless (@user && @user.activated? &&
               @user.authenticated?(:reset, params[:id]))
   redirect_to root_url
end
end
# Checks expiration of reset token.
def check_expiration
   if @user.password_reset_expired?
      flash[:danger] = "Password reset has expired."
      redirect_to new_password_reset_url
   end
end

In the check_expiration method, we’ve deferred the expiration check to the instance method password_reset_expired?, which is a little tricky and will be defined in a moment.

Listing 12.16 shows the implementation of these filters, together with the update action that implements Cases (2)–(4). Case (2) gets handled by a failed update, with the error messages from the shared partial in Listing 12.14 displaying automatically when the edit form is re-rendered. Case (4) corresponds to a successful change, and the result is similar to a successful login (Listing 8.25).

The only failure case not handled by Case (2) is when the password is empty, which is currently allowed by our User model (Listing 10.13) and so needs to be caught and handled explicitly.² This is Case (3). Our method in this case is to add an error directly to the @user object’s error messages using errors.add:

2. We need only handle the case where the password is empty because if the confirmation is empty, the confirmation validation (which is skipped if the password is empty) will catch the problem and supply a relevant error message.

Click here to view code image

@user.errors.add(:password, "can't be empty")

The result of putting Cases (1)–(4) together is the update action shown in Listing 12.16.

Listing 12.16: The update action for password reset.
app/controllers/password_resets_controller.rb

Click here to view code image

class PasswordResetsController < ApplicationController
   before_action :get_user,         only: [:edit, :update]
   before_action :valid_user,       only: [:edit, :update]
   before_action :check_expiration, only: [:edit, :update] # Case (1)
   def new
   end
   def create
      @user = User.find_by(email: params[:password_reset][:email].downcase)
      if @user
         @user.create_reset_digest
         @user.send_password_reset_email
         flash[:info] = "Email sent with password reset instructions"
         redirect_to root_url
      else
         flash.now[:danger] = "Email address not found"
         render 'new'
      end
   end
   def edit
   end
   def update
      if params[ :user][ :password].empty?       # Case (3)
         @user.errors.add(:password, "can't be empty")
         render 'edit'
      elsif @user.update_attributes(user_params) # Case (4)
         log_in @user
         flash[:success] = "Password has been reset."
         redirect_to @user
      else
         render 'edit'                           # Case (2)
      end
   end
   private
      def user_params
         params.require( :user).permit( :password, :password_confirmation)
      end
      # Before filters
      def get_user
         @user = User.find_by(email: params[:email])
      end
      # Confirms a valid user.
      def valid_user
         unless (@user && @user.activated? &&
                          @user.authenticated?(:reset, params[:id]))
            redirect_to root_url
         end
      end
      # Checks expiration of reset token.
      def check_expiration
         if @user.password_reset_expired?
            flash[:danger] = "Password reset has expired."
            redirect_to new_password_reset_url
         end
      end
end

Note that we’ve added a user_params method permitting both the password and password confirmation attributes (Section 7.3.2).

As noted above, the implementation in Listing 12.16 delegates the boolean test for password reset expiration to the User model via the code

@user.password_reset_expired?

To get this to work, we need to define the password_reset_expired? method. As indicated in the email templates from Section 12.2.1, we’ll consider a password reset to be expired if it was sent more than two hours ago, which we can express in Ruby as follows:

reset_sent_at < 2.hours.ago

This can be confusing if you read < as “less than”, because then it sounds like “Password reset sent less than two hours ago”, which is the opposite of what we want. In this context, it’s better to read < as “earlier than”, which gives something like “Password

reset sent earlier than two hours ago.” That is what we want, and it leads to the password_reset_expired? method in Listing 12.17. (For a formal demonstration that the comparison is correct, see the proof in Section 12.6.)

Listing 12.17: Adding password reset methods to the User model.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
   .
   .
   .
   # Returns true if a password reset has expired.
   def password_reset_expired?
      reset_sent_at < 2.hours.ago
   end
   private
      .
      .
      .
end

With the code in Listing 12.17, the update action in Listing 12.16 should be working. The results for invalid and valid submissions are shown in Figure 12.12 and Figure 12.13, respectively. (Lacking the patience to wait two hours, we’ll cover the third branch in a test, which is left as an exercise [Section 12.3.2].)

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Follow the email link from Section 12.2.1 again and submit mismatched passwords to the form. What is the error message?

2. In the console, find the user belonging to the email link, and retrieve the value of the password_digest attribute. Now submit valid matching passwords to the form shown in Figure 12.12. Did the submission appear to work? How did it affect the value of password_digest? Hint: Use user.reload to retrieve the new value.

Figure 12.12: A failed password reset.

Figure 12.13: A successful password reset.

12.3.3 Password Reset Test

In this section, we’ll write an integration test covering two of the three branches in Listing 12.16, invalid and valid submission. (As noted above, testing the third branch is left as an exercise.) We’ll get started by generating a test file for password resets:

Click here to view code image

$ rails generate integration_test password_resets
         invoke test_unit
         create test/integration/password_resets_test.rb

The steps to test password resets broadly parallel the test for account activation from Listing 11.33, though there is a difference at the outset: we first visit the “forgot password” form and submit invalid and then valid email addresses, the latter of which creates a password reset token and sends the reset email. We then visit the link from the email and again submit invalid and valid information, verifying the correct behavior in each case. The resulting test, shown in Listing 12.18, is an excellent exercise in reading code.

Listing 12.18: An integration test for password resets.
test/integration/password_resets_test.rb

Click here to view code image

require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
   def setup
      ActionMailer::Base.deliveries.clear
      @user = users(:michael)
   end
   test "password resets" do
      get new_password_reset_path
      assert_template 'password_resets/new'
      # Invalid email
      post password_resets_path,params: { password_reset: { email: "" } }
      assert_not flash.empty?
      assert_template 'password_resets/new'
      # Valid email
      post password_resets_path,
               params: { password_reset: { email: @user.email } }
      assert_not_equal @user.reset_digest, @user.reload.reset_digest
      assert_equal 1, ActionMailer::Base.deliveries.size
      assert_not flash.empty?
      assert_redirected_to root_url
      # Password reset form
      user = assigns(:user)
      # Wrong email
      get edit_password_reset_path(user.reset_token, email: "")
      assert_redirected_to root_url
      # Inactive user
      user.toggle!(:activated)
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_redirected_to root_url
      user.toggle!(:activated)
      # Right email, wrong token
      get edit_password_reset_path('wrong token', email: user.email)
      assert_redirected_to root_url
      # Right email, right token
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_template 'password_resets/edit'
      assert_select "input[name=email][type=hidden][value=?]", user.email
      # Invalid password & confirmation
      patch password_reset_path(user.reset_token),
                params: { email: user.email,
                          user: { password: "foobaz",
                                  password_confirmation: "barquux" } }
      assert_select 'div#error_explanation'
      # Empty password
      patch password_reset_path(user.reset_token),
               params: { email: user.email,
                         user: { password: "",
                                 password_confirmation: "" } }
      assert_select 'div#error_explanation'
      # Valid password & confirmation
      patch password_reset_path(user.reset_token),
               params: { email: user.email,
                         user: { password: "foobaz",
                                 password_confirmation: "foobaz" } }
      assert is_logged_in?
      assert_not flash.empty?
      assert_redirected_to user
   end
end

Most of the ideas in Listing 12.18 have appeared previously in this tutorial; the only really novel element is the test of the input tag:

Click here to view code image

assert_select "input[name=email][type=hidden][value=?]", user.email

This makes sure that there is an input tag with the right name, (hidden) type, and email address:

Click here to view code image

<input id="email" name="email" type="hidden" value="michael@example.com" />

With the code as in Listing 12.18, our test suite should be GREEN:

Listing 12.19: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In Listing 12.6, the create_reset_digest method makes two calls to update_attribute, each of which requires a separate database operation. By filling in the template shown in Listing 12.20, replace the two update_attribute calls with a single call to update_columns, which hits the database only once. After making the changes, verify that the test suite is still GREEN. (For convenience, Listing 12.20 includes the results of solving the exercise in Listing 11.39.)

2. Write an integration test for the expired password reset branch in Listing 12.16 by filling in the template shown in Listing 12.21. (This code introduces response.body, which returns the full HTML body of the page.) There are many ways to test for the result of an expiration, but the method suggested by Listing 12.21 is to (case-insensitively) check that the response body includes the word “expired”.

3. Expiring password resets after a couple of hours is a nice security precaution, but there is an even more secure solution for cases where a public computer is used. The reason is that the password reset link remains active for 2 hours and can be used even if logged out. If a user reset their password from a public machine, anyone could press the back button and change the password (and get logged in to the site). To fix this, add the code shown in Listing 12.22 to clear the reset digest on successful password update.³

4. Add a line to Listing 12.18 to test for the clearing of the reset digest in the previous exercise. Hint: Combine assert_nil (first seen in Listing 9.25) with user.reload (Listing 11.33) to test the reset_digest attribute directly.

Listing 12.20: A template for using update_columns.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
   attr_accessor :remember_token, :activation_token, :reset_token
   before_save :downcase_email
   before_create :create_activation_digest
   .
   .
   .
   # Activates an account.
   def activate
      update_columns(activated: true, activated_at: Time.zone.now)
   end
   # Sends activation email.
   def send_activation_email
      UserMailer.account_activation(self).deliver_now
   end
   # Sets the password reset attributes.
   def create_reset_digest
      self.reset_token = User.new_token
      update_columns( reset_digest: FILL_IN, reset_sent_at: FILL_IN)
   end
   # Sends password reset email.
   def send_password_reset_email
      UserMailer.password_reset(self).deliver_now
   end
   private
      # Converts email to all lower-case.
      def downcase_email
         self.email = email.downcase
      end
      # Creates and assigns the activation token and digest.
      def create_activation_digest
         self.activation_token  = User.new_token
         self.activation_digest = User.digest(activation_token)
      end
end

3. Thanks to reader Tristan Ludowyk for suggesting this feature and for providing both a detailed description and a suggested implementation.

Listing 12.21: A test for an expired password reset. GREEN
test/integration/password_resets_test.rb

Click here to view code image

require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
   def setup
      ActionMailer::Base.deliveries.clear
      @user = users(:michael)
   end
   .
   .
   .
   test "expired token" do
      get new_password_reset_path post
      password_resets_path,
               params: { password_reset: { email: @user.email } }
      @user = assigns(:user)
      @user.update_attribute(:reset_sent_at, 3.hours.ago)
      patch password_reset_path(@user.reset_token),
               params: { email: @user.email,
                         user: { password: "foobar",
                                 password_confirmation: "foobaz" } }
      assert_response :redirect
      follow_redirect!
      assert_match /FILL_IN/i, response.body
   end
end

Listing 12.22: Clearing the reset digest on successful password reset.
app/controllers/password_resets_controller.rb

Click here to view code image

class PasswordResetsController < ApplicationController
   .
   .
   .
   def update
      if params[:user][:password].empty?
         @user.errors.add(:password, "can't be empty")
         render 'edit'
      elsif @user.update_attributes(user_params)
         log_in @user
         @user.update_attribute( :reset_digest, nil)
         flash[:success] = "Password has been reset."
         redirect_to @user
      else
         render 'edit'
      end
   end
   .
   .
   .
end

12.4 Email in Production (Take Two)

Now that we’ve got password resets working in development, in this section we’ll get them working in production as well. The steps are exactly the same as for account activations, so if you already followed Section 11.4 you can skip right to Listing 12.24.

To send email in production, we’ll use SendGrid, which is available as an add-on at Heroku for verified accounts. (This requires adding credit card information to your

Heroku account, but there is no charge when verifying an account.) For our purposes, the “starter” tier (which as of this writing is limited to 400 emails a day but costs nothing) is the best fit. We can add it to our app as follows:

Click here to view code image

$ heroku addons:create sendgrid:starter

(This might fail on systems with older version of Heroku’s command-line interface. In this case, either upgrade to the latest Heroku toolbelt or try the older syntax heroku addons:add sendgrid:starter.)

To configure our application to use SendGrid, we need to fill out the SMTP settings for our production environment. As shown in Listing 12.23, you will also have to define a host variable with the address of your production website.

Listing 12.23: Configuring Rails to use SendGrid in production.
config/environments/production.rb

Click here to view code image

Rails.application.configure do
   .
   .
   .
   config.action_mailer.raise_delivery_errors = true
   config.action_mailer.delivery_method = :smtp
   host = '<your heroku app>.herokuapp.com'
   config.action_mailer.default_url_options = { host: host }
   ActionMailer::Base.smtp_settings = {
      :address          => 'smtp.sendgrid.net',
      :port             => '587',
      :authentication   => :plain,
      :user_name        => ENV['SENDGRID_USERNAME'],
      :password         => ENV['SENDGRID_PASSWORD'],
      :domain           => 'heroku.com',
      :enable_starttls_auto => true
   }
   .
   .
   .
end

The email configuration in Listing 11.41 includes the user_name and password of the SendGrid account, but note that they are accessed via the ENV environment variable instead of being hard-coded. This is a best practice for production applications, which for security reasons should never expose sensitive information such as raw passwords in source code. In the present case, these variables are configured automatically via the SendGrid add-on, but we’ll see an example in Section 13.4.4 where we’ll have to define them ourselves.

At this point, you should merge the topic branch into master (Listing 12.24).

Listing 12.24: Merging the password-reset branch into master.

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Add password reset"
$ git checkout master
$ git merge password-reset

Then push up to the remote repository and deploy to Heroku:

Click here to view code image

$ rails test
$ git push
$ git push heroku
$ heroku run rails db:migrate

Once the Heroku deploy has finished, you can reset your password by clicking the “(forgot password)” link (Figure 12.4). The result should be a reset email as shown in Figure 12.14. Following the link and making invalid or valid submissions should work as it did in development (Figure 12.12 and Figure 12.13).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Sign up for a new account in production. Did you get the email?

2. Click on the link in the activation email to confirm that it works. What is the corresponding entry in the server log? Hint: Run heroku logs at the command line.

3. Are you able to successfully update your password?

Figure 12.14: A password reset email sent in production.

12.5 Conclusion

With the added password resets, our sample application’s sign up, log in, and log out machinery is complete and professional-grade. The rest of the Ruby on Rails™ Tutorial builds on this foundation to make a site with Twitter-like microposts (Chapter 13) and a status feed of posts from followed users (Chapter 14). In the process, we’ll learn about some of the most powerful features of Rails, including image upload, custom database queries, and advanced data modeling with has_many and has_many :through.

12.5.1 What We Learned in This Chapter

• Like sessions and account activations, password resets can be modeled as a resource despite not being Active Record objects.

• Rails can generate Action Mailer actions and views to send email.

• Action Mailer supports both plain-text and HTML mail.

• As with ordinary actions and views, instance variables defined in mailer actions are available in mailer views.

• Password resets use a generated token to create a unique URL for resetting passwords.

• Password resets use a hashed reset digest to securely identify valid reset requests.

• Both mailer tests and integration tests are useful for verifying the behavior of the User mailer.

• We can send email in production using SendGrid.

12.6 Proof of Expiration Comparison

We saw in Section 12.3 that the comparison test for determining when a password reset has expired is

reset_sent_at < 2.hours.ago

as seen in Listing 12.17. This looks likes it might be read as “reset sent less than two hours ago”, which is the opposite of what we want. In this section, we’ll prove that the above comparison is correct.⁴

4. This proof is the price you pay for reading a web development tutorial written by a physics Ph.D. Just be grateful I couldn’t find a way to work () ψ = E ψ or Gµν = 8π T µν (= 4τ T µν ) into the exposition.

We start by defining two time intervals. Let Δtr be the time interval since sending the password reset and Δte be the expiration time limit (e.g., two hours). A password reset has expired if the time interval since the reset was sent is greater than the expiration limit:

Δtr > Δte. (12.1)

If we write the time now as tN, the password reset sending time as tr, and the expiration time as te (e.g., two hours ago), then we have

Δtr = tN − tr (12.2)

and

Δte = tN − te. (12.3)

Plugging Eq. (12.2) and Eq. (12.3) into (12.1) then gives

Δtr  >  Δte

tN − tr  >  tN − te

−tr  >  −te

which upon multiplying through by −1 yields

tr < te. (12.4)

Converting (12.4) to code with the value te = 2 hours ago gives the password_ reset_expired? method shown in Listing 12.17:

Click here to view code image

def password_reset_expired?
   reset_sent_at < 2.hours.ago
end

As noted in Section 12.3, if we read < as “earlier than” instead of “less than”, this code makes sense as the English sentence “The password reset was sent earlier than two hours ago.”

Chapter 13. User Microposts

In the course of developing the core sample application, we’ve now encountered four resources—users, sessions, account activations, and password resets—but only the first of these is backed by an Active Record model with a table in the database. The time has finally come to add a second such resource: user microposts, which are short messages associated with a particular user.¹ We first saw microposts in larval form in Chapter 2, and in this chapter we will make a full-strength version of the sketch from Section 2.3 by constructing the Micropost data model, associating it with the User model using the has_many and belongs_to methods, and then making the forms and partials needed to manipulate and display the results (including, in Section 13.4, uploaded images). In Chapter 14, we’ll complete our tiny Twitter clone by adding the notion of following users in order to receive a feed of their microposts.

1. The name is motivated by the common description of Twitter as a microblog; since blogs have posts, microblogs should have microposts.

13.1 A Micropost Model

We begin the Microposts resource by creating a Micropost model, which captures the essential characteristics of microposts. What follows builds on the work from Section 2.3; as with the model in that section, our new Micropost model will include data validations and an association with the User model. Unlike that model, the present Micropost model will be fully tested, and will also have a default ordering and automatic destruction if its parent user is destroyed.

If you’re using Git for version control, I suggest making a topic branch at this time:

Click here to view code image

$ git checkout -b user-microposts

13.1.1 The Basic Model

The Micropost model needs only two attributes: a content attribute to hold the micropost’s content and a user_id to associate a micropost with a particular user. The result is a Micropost model with the structure shown in Figure 13.1.

It’s worth noting that the model in Figure 13.1 uses the text data type for micropost content (instead of string), which is capable of storing an arbitrary amount of text. Even though the content will be restricted to fewer than 140 characters (Section 13.1.2) and hence would fit inside the 255-character string type, using text better expresses the nature of microposts, which are more naturally thought of as blocks of text. Indeed, in Section 13.3.2 we’ll use a text area instead of a text field for submitting microposts. In addition, using text gives us greater flexibility should we wish to increase the length limit at a future date (as part of internationalization, for example). Finally, using the text type results in no performance difference in production,² so it costs us nothing to use it here.

2. www.postgresql.org/docs/9.1/static/datatype-character.html

Figure 13.1: The Micropost data model.

As with the case of the User model (Listing 6.1), we generate the Micropost model using generate model (Listing 13.1).

Listing 13.1: Generating the Micropost model.

Click here to view code image

$ rails generate model Micropost content:text user:references

This migration leads to the creation of the Micropost model shown in Listing 13.2. In addition to inheriting from ApplicationRecord as usual (Section 6.1.2), the generated model includes a line indicating that a micropost belongs_to a user, which is included as a result of the user:references argument in Listing 13.1. We’ll explore the implications of this line in Section 13.1.3.

Listing 13.2: The generated Micropost model.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
   belongs_to :user
end

The generate command in Listing 13.1 also produces a migration to create a microposts table in the database (Listing 13.3); compare it to the analogous migration for the users table from Listing 6.2. The biggest difference is the use of references, which automatically adds a user_id column (along with an index and a foreign key reference)³ for use in the user/micropost association. As with the User model, the Micropost model migration automatically includes the t.timestamps line, which (as mentioned in Section 6.1.1) adds the magic created_at and updated_at columns shown in Figure 13.1. (We’ll put the created_at column to work starting in Section 13.1.4.)

3. The foreign key reference is a database-level constraint indicating that the user id in the microposts table refers to the id column in the users table. This detail will never be important in this tutorial, and the foreign key constraint isn’t even supported by all databases. (It’s supported by PostgreSQL, which we use in production, but not by the development SQLite database adapter.) We’ll learn more about foreign keys in Section 14.1.2.

Listing 13.3: The Micropost migration with added index.
db/migrate/[timestamp]_create_microposts.rb

Click here to view code image

class CreateMicroposts < ActiveRecord::Migration[5.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

Because we expect to retrieve all the microposts associated with a given user id in reverse order of creation, Listing 13.3 adds an index (Box 6.2) on the user_id and created_at columns:

Click here to view code image

add_index :microposts, [:user_id, :created_at]

By including both the user_id and created_at columns as an array, we arrange for Rails to create a multiple key index, which means that Active Record uses both keys at the same time.

With the migration in Listing 13.3, we can update the database as usual:

$ rails db:migrate

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using Micropost.new in the console, instantiate a new Micropost object called micropost with content “Lorem ipsum” and user id equal to the id of the first user in the database. What are the values of the magic columns created_at and updated_at?

2. What is micropost.user for the micropost in the previous exercise? What about micropost.user.name?

3. Save the micropost to the database. What are the values of the magic columns now?

13.1.2 Micropost Validations

Now that we’ve created the basic model, we’ll add some validations to enforce the desired design constraints. One of the necessary aspects of the Micropost model is the presence of a user id to indicate which user made the micropost. The idiomatically correct way to do this is to use Active Record associations, which we’ll implement in Section 13.1.3, but for now we’ll work with the Micropost model directly.

The initial micropost tests parallel those for the User model (Listing 6.7). In the setup step, we create a new micropost while associating it with a valid user from the fixtures and then check that the result is valid. Because every micropost should have a user id, we’ll add a test for a user_id presence validation. Putting these elements together yields the test in Listing 13.4.

Listing 13.4: Tests for the validity of a new micropost. GREEN
test/models/micropost_test.rb

Click here to view code image

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

     def setup
    @user = users(:michael)
    # This code is not idiomatically correct.
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

     test "should be valid" do
    assert @micropost.valid?
  end

      test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end

As indicated by the comment in the setup method, the code to create the micropost is not idiomatically correct, which we’ll fix in Section 13.1.3.

As with the original User model test (Listing 6.5), the first test in Listing 13.4 is just a sanity check, but the second is a test of the presence of the user id, for which we’ll add the presence validation shown in Listing 13.5.

Listing 13.5: A validation for the micropost’s user_id. GREEN
app/models/micropost.rb

Click here to view code image

class Micropost < ActiveRecord::Base
   belongs_to :user
   validates :user_id, presence: true
end

By the way, as of Rails 5 the tests in Listing 13.4 actually pass without the validation in Listing 13.5, but only when using the idiomatically incorrect line highlighted in Listing 13.4. The user id presence validation is necessary after switching to the idiomatically correct code in Listing 13.12, so we include it here for convenience.

With the code in Listing 13.5 the tests should (still) be GREEN:

Listing 13.6: GREEN

$ rails test:models

Next, we’ll add validations for the micropost’s content attribute (following the example from Section 2.3.2). As with the user_id, the content attribute must be present, and it is further constrained to be no longer than 140 characters (which is what puts the micro in micropost).

As with the User model validations (Section 6.2), we’ll add the micropost content validations using test-driven development. The resulting tests generally follow the examples from the User model validation tests, as shown in Listing 13.7.

Listing 13.7: Tests for the Micropost model validations. RED
test/models/micropost_test.rb

Click here to view code image

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

      def setup
    @user = users(:michael)
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

      test "should be valid" do
    assert @micropost.valid?
  end

     test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end

  test "content should be present" do
    @micropost.content = " "
    assert_not @micropost.valid?
  end

  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end
end

As in Section 6.2, the code in Listing 13.7 uses string multiplication to test the micropost length validation:

Click here to view code image

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

The corresponding application code is virtually identical to the name validation for users (Listing 6.16), as shown in Listing 13.8.

Listing 13.8: The Micropost model validations. GREEN
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

At this point, the full test suite should be GREEN:

Listing 13.9: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. At the console, instantiate a micropost with no user id and blank content. Is it valid? What are the full error messages?

2. At the console, instantiate a second micropost with no user id and content that’s too long. Is it valid? What are the full error messages?

13.1.3 User/Micropost Associations

When constructing data models for web applications, it is essential to be able to make associations between individual models. In the present case, each micropost is associated with one user, and each user is associated with (potentially) many microposts—a relationship seen briefly in Section 2.3.3 and shown schematically in Figure 13.2 and Figure 13.3. As part of implementing these associations, we’ll write tests for the Micropost model and add a couple of tests to the User model.

Figure 13.2: The belongs_to relationship between a micropost and its associated user.

Figure 13.3: The has_many relationship between a user and its microposts.

Table 13.1: A summary of user/micropost association methods.

Using the belongs_to/has_many association defined in this section, Rails constructs the methods shown in Table 13.1. Note from Table 13.1 that instead of

Click here to view code image

Micropost.create
Micropost.create!
Micropost.new

we have

Click here to view code image

user.microposts.create
user.microposts.create!
user.microposts.build

These latter methods constitute the idiomatically correct way to make a micropost, namely, through its association with a user. When a new micropost is made in this way, its user_id is automatically set to the right value. In particular, we can replace the code

@user = users(:michael)
# This code is not idiomatically correct.
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)

from Listing 13.4 with this:

Click here to view code image

@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")

(As with new, build returns an object in memory but doesn’t modify the database.) Once we define the proper associations, the resulting @micropost variable will automatically have a user_id attribute equal to its associated user’s id.

To get code like @user.microposts.build to work, we need to update the User and Micropost models with code to associate them. The first of these was included automatically by the migration in Listing 13.3 via belongs_to :user, as shown in Listing 13.10. The second half of the association, has_many :microposts, needs to be added by hand, as shown in (Listing 13.11).

Listing 13.10: A micropost belongs_to a user. GREEN
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

Listing 13.11: A user has_many microposts. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end

With the association thus made, we can update the setup method in Listing 13.4 with the idiomatically correct way to build a new micropost, as shown in Listing 13.12.

Listing 13.12: Using idiomatically correct code to build a micropost. GREEN
test/models/micropost_test.rb

Click here to view code image

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    @micropost = @user.microposts.build(content: "Lorem ipsum")
  end

  test "should be valid" do
    assert @micropost.valid?
  end

  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
  .
  .
  .
end

Of course, after this minor refactoring the test suite should still be GREEN:

Listing 13.13: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Set user to the first user in the database. What happens when you execute the command micropost = user.microposts.create(content: "Lorem ipsum")?

2. The previous exercise should have created a micropost in the database. Confirm this by running user.microposts.find(micropost.id). What if you write micropost instead of micropost.id?

3. What is the value of user == micropost.user? How about user.micro posts.first == micropost?

13.1.4 Micropost Refinements

In this section, we’ll add a couple of refinements to the user/micropost association. In particular, we’ll arrange for a user’s microposts to be retrieved in a specific order, and we’ll also make microposts dependent on users so that they will be automatically destroyed if their associated user is destroyed.

Default Scope

By default, the user.microposts method makes no guarantees about the order of the posts, but (following the convention of blogs and Twitter) we want the microposts to come out in reverse order of when they were created so that the most recent post is first.⁴ We’ll arrange for this to happen using a default scope.

4. We briefly encountered a similar issue in Section 10.5 in the context of the users index.

This is exactly the sort of feature that could easily lead to a spurious passing test (i.e., a test that would pass even if the application code were wrong), so we’ll proceed using test-driven development to be sure we’re testing the right thing. In particular, let’s write a test to verify that the first micropost in the database is the same as a fixture micropost we’ll call most_recent, as shown in Listing 13.14.

Listing 13.14: Testing the micropost order. RED
test/models/micropost_test.rb

Click here to view code image

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase
  .
  .
  .
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
end

Listing 13.14 relies on having some micropost fixtures, which we define as shown in Listing 13.15.

Listing 13.15: Micropost fixtures.
test/fixtures/microposts.yml

Click here to view code image

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>

Note that we have explicitly set the created_at column using embedded Ruby. Because it’s a “magic” column automatically updated by Rails, setting it by hand isn’t ordinarily possible, but it is possible in fixtures. In practice this might not be necessary, and in fact on many systems the fixtures are created in order. In this case, the final fixture in the file is created last (and hence is most recent), but it would be foolish to rely on this behavior, which is brittle and probably system-dependent.

With the code in Listing 13.14 and Listing 13.15, the test suite should be RED:

Listing 13.16: RED

Click here to view code image

$ rails test test/models/micropost_test.rb \
>            --name test_order_should_be_most_recent_first

We’ll get the test to pass using a Rails method called default_scope, which among other things can be used to set the default order in which elements are retrieved from the database. To enforce a particular order, we’ll include the order argument in default_scope, which lets us order by the created_at column as follows:

order(:created_at)

Unfortunately, this orders the results in ascending order from smallest to biggest, which means that the oldest microposts come out first. To pull them out in reverse order, we can push down one level deeper and include a string with some raw SQL:

order('created_at DESC')

Here DESC is SQL for “descending”, i.e., in descending order from newest to oldest.⁵ In older versions of Rails, using this raw SQL used to be the only option to get the desired behavior, but as of Rails 4.0 we can use a more natural pure-Ruby syntax as well:

5. SQL is case-insensitive, but it is conventional to write SQL keywords (such as DESC) in all-caps.

order(created_at: :desc)

Adding this in a default scope for the Micropost model gives Listing 13.17.

Listing 13.17: Ordering the microposts with default_scope. GREEN
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

Listing 13.17 introduces the “stabby lambda” syntax for an object called a Proc (procedure) or lambda, which is an anonymous function (a function created without a name). The stabby lambda -> takes in a block (Section 4.3.2) and returns a Proc, which can then be evaluated with the call method. We can see how it works at the console:

Click here to view code image

>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil

(This is a somewhat advanced Ruby topic, so don’t worry if it doesn’t make sense right away.)

With the code in Listing 13.17, the tests should be GREEN:

Listing 13.18: GREEN

$ rails test

Dependent: Destroy

Apart from proper ordering, there is a second refinement we’d like to add to microposts. Recall from Section 10.4 that site administrators have the power to destroy users. It stands to reason that if a user is destroyed, the user’s microposts should be destroyed as well.

We can arrange for this behavior by passing an option to the has_many association method, as shown in Listing 13.19.

Listing 13.19: Ensuring that a user’s microposts are destroyed along with the user.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

Here the option dependent: :destroy arranges for the dependent microposts to be destroyed when the user itself is destroyed. This prevents userless microposts from being stranded in the database when admins choose to remove users from the system.

We can verify that Listing 13.19 is working with a test for the User model. All we need to do is save the user (so it gets an id) and create an associated micropost. Then we check that destroying the user reduces the micropost count by 1. The result appears in Listing 13.20. (Compare to the integration test for “delete” links in Listing 10.62.)

Listing 13.20: A test of dependent: :destroy. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end
end

If the code in Listing 13.19 is working correctly, the test suite should still be GREEN:

Listing 13.21: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. How does the value of Micropost.first.created_at compare to Micropost.last.created_at?

2. What are the SQL queries for Micropost.first and Micropost.last? Hint: They are printed out by the console.

3. Let user be the first user in the database. What is the id of its first micropost? Destroy the first user in the database using the destroy method, then confirm using Micropost.find that the user’s first micropost was also destroyed.

13.2 Showing Microposts

Although we don’t yet have a way to create microposts through the web—that comes in Section 13.3.2—this won’t stop us from displaying them (and testing that display). Following Twitter’s lead, we’ll plan to display a user’s microposts not on a separate microposts index page but rather directly on the user show page itself, as mocked up in Figure 13.4. We’ll start with fairly simple ERb templates for adding a micropost display to the user profile, and then we’ll add microposts to the seed data from Section 10.3.2 so that we have something to display.

13.2.1 Rendering Microposts

Our plan is to display the microposts for each user on their respective profile page (show.html.erb), together with a running count of how many microposts they’ve made. As we’ll see, many of the ideas are similar to our work in Section 10.3 on showing all users.

Figure 13.4: A mockup of a profile page with microposts.

In case you’ve added some microposts in the exercises, it’s a good idea to reset and reseed the database at this time:

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

Although we won’t need the Microposts controller until Section 13.3, we will need the views directory in just a moment, so let’s generate the controller now:

Click here to view code image

$ rails generate controller Microposts

Our primary purpose in this section is to render all the microposts for each user. We saw in Section 10.3.5 that the code

Click here to view code image

<ul class="users">
  <%= render @users %>
</ul>

automatically renders each of the users in the @users variable using the _user. html.erb partial. We’ll define an analogous _micropost.html.erb partial so that we can use the same technique on a collection of microposts as follows:

Click here to view code image

<ol class="microposts">
  <%= render @microposts %>
</ol>

Note that we’ve used the ordered list tag ol (as opposed to an unordered list ul) because microposts are listed in a particular order (reverse-chronological). The corresponding partial appears in Listing 13.22.

Listing 13.22: A partial for showing a single micropost.
app/views/microposts/_micropost.html.erb

Click here to view code image

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>

This uses the awesome time_ago_in_words helper method, whose meaning is probably clear and whose effect we will see in Section 13.2.2. Listing 13.22 also adds a CSS id for each micropost using

Click here to view code image

<li id="micropost-<%= micropost.id %>">

This is a generally good practice, as it opens up the possibility of manipulating individual microposts at a future date (using JavaScript, for example).

The next step is to address the difficulty of displaying a potentially large number of microposts. We’ll solve this problem the same way we solved it for users in Section 10.3.3, namely, using pagination. As before, we’ll use the will_paginate method:

Click here to view code image

<%= will_paginate @microposts %>

If you compare this with the analogous line on the user index page, Listing 10.45, you’ll see that before we had just

<%= will_paginate %>

This worked because, in the context of the Users controller, will_paginate assumes the existence of an instance variable called @users (which, as we saw in Section 10.3.3, should be of class ActiveRecord::Relation). In the present case, since we are still in the Users controller but want to paginate microposts instead, we’ll pass an explicit @microposts variable to will_paginate. Of course, this means that we will have to define such a variable in the user show action (Listing 13.23).

Listing 13.23: Adding an @microposts instance variable to the user show action.
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end
  .
  .
  .
end

Notice here how clever paginate is—it even works through the microposts association, reaching into the microposts table and pulling out the desired page of microposts.

Our final task is to display the number of microposts for each user, which we can do with the count method:

user.microposts.count

As with paginate, we can use the count method through the association. In particular, count does not pull all the microposts out of the database and then call length on the resulting array, as this would become inefficient as the number of microposts grew. Instead, it performs the calculation directly in the database, asking the database to count the microposts with the given user_id (an operation for which all databases are highly optimized). (In the unlikely event that finding the count is still a bottleneck in your application, you can make it even faster using a counter cache.)

Putting all the elements above together, we are now in a position to add microposts to the profile page, as shown in Listing 13.24. Note the use of if @user. microposts.any? (a construction we saw before in Listing 7.21), which makes sure that an empty list won’t be displayed when the user has no microposts.

Listing 13.24: Adding microposts to the user show page.
app/views/users/show.html.erb

Click here to view code image

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
     </section>
   </aside>
   <div class="col-md-8">
     <% if @user.microposts.any? %>
       <h3></a>Microposts (<%= @user.microposts.count %>)</h3>
       <ol class="microposts">
         <%= render @microposts %>
       </ol>
       <%= will_paginate @microposts %>
     <% end %>
   </div>
</div>

Figure 13.5: The user profile page with code for microposts—but no microposts.

At this point, we can get a look at our updated user profile page in Figure 13.5. It’s rather…disappointing. Of course, this is because there are not currently any microposts. It’s time to change that.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. As mentioned briefly in Section 7.3.3, helper methods like time_ago_in_words are available in the Rails console via the helper object. Using helper, apply time_ago_in_words to 3.weeks.ago and 6.months.ago.

2. What is the result of helper.time_ago_in_words(1.year.ago)?

3. What is the Ruby class for a page of microposts? Hint: Use the code in Listing 13.40 as your model, and call the class method on paginate with the argument page: nil.

13.2.2 Sample Microposts

With all the work making templates for user microposts in Section 13.2.1, the ending was rather anticlimactic. We can rectify this sad situation by adding microposts to the seed data from Section 10.3.2.

Adding sample microposts for all the users actually takes a rather long time, so first we’ll select just the first six users (i.e., the five users with custom Gravatars, and one with the default Gravatar) using the take method:

Click here to view code image

User.order(:created_at).take(6)

The call to order ensures that we find the first six users that were created.

For each of the selected users, we’ll make 50 microposts (plenty to overflow the pagination limit of 30). To generate sample content for each micropost, we’ll use the Faker gem’s handy Lorem.sentence method.⁶ The result is the new seed data method shown in Listing 13.25. (The reason for the order of the loops in Listing 13.25 is to intermix the microposts for use in the status feed [Section 14.3]. Looping over the users first gives feeds with big runs of microposts from the same user, which is visually unappealing.)

6. Faker::Lorem.sentence returns lorem ipsum text; as noted in Chapter 6, lorem ipsum has a fascinating back story.

Listing 13.25: Adding microposts to the sample data.
db/seeds.rb

Click here to view code image

.
.
.
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end

At this point, we can reseed the development database as usual:

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

You should also quit and restart the Rails development server.

With that, we are in a position to enjoy the fruits of our Section 13.2.1 labors by displaying information for each micropost.⁷ The preliminary results appear in Figure 13.6.

The page shown in Figure 13.6 has no micropost-specific styling, so let’s add some (Listing 13.26) and take a look at the resulting pages.⁸

7. By design, the Faker gem’s lorem ipsum text is randomized, so the contents of your sample microposts will differ.

8. For convenience, Listing 13.26 actually has all the CSS needed for this chapter.

Figure 13.6: The user profile with unstyled microposts.

Listing 13.26: The CSS for microposts (including all the CSS for this chapter).
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* microposts */

.microposts {
  list-style: none;
  padding: 0;
  li {
    padding: 10px 0;
    border-top: 1px solid #e8e8e8;
  }
  .user {
    margin-top: 5em;
    padding-top: 0;
  }
  .content {
    display: block;
    margin-left: 60px;
    img {
      display: block;
      padding: 5px 0;
    }
   }
   .timestamp {
     color: $gray-light;
     display: block;
     margin-left: 60px;
   }
   .gravatar {
     float: left;
     margin-right: 10px;
     margin-top: 5px;
   }
}

aside {
  textarea {
    height: 100px;
    margin-bottom: 5px;
  }
}

span.picture {
  margin-top: 10px;
   input {
     border: 0;
   }
}

Figure 13.7 shows the user profile page for the first user, while Figure 13.8 shows the profile for a second user. Finally, Figure 13.9 shows the second page of microposts for the first user, along with the pagination links at the bottom of the display. In all three cases, observe that each micropost display indicates the time since it was created (e.g., “Posted 1 minute ago.”); this is the work of the time_ago_in_words method from Listing 13.22. If you wait a couple of minutes and reload the pages, you’ll see how the text gets automatically updated based on the new time.

Figure 13.7: The user profile with microposts (/users/1).

Figure 13.8: The profile of a different user, also with microposts (/users/5).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. See if you can guess the result of running (1..10).to_a.take(6). Check at the console to see if your guess is right.

2. Is the to_a method in the previous exercise necessary?

3. Faker has a huge number of occasionally amusing applications. By consulting the Faker documentation, learn how to print out a fake university name, a fake phone number, a fake Hipster Ipsum sentence, and a fake Chuck Norris fact.

Figure 13.9: Micropost pagination links (/users/1?page=2).

13.2.3 Profile Micropost Tests

Because newly activated users get redirected to their profile pages, we already have a test that the profile page renders correctly (Listing 11.33). In this section, we’ll write a short integration test for some of the other elements on the profile page, including the work from this section. We’ll start by generating an integration test for the profiles of our site’s users:

Click here to view code image

$ rails generate integration_test users_profile
      invoke test_unit
      create test/integration/users_profile_test.rb

To test the micropost display on the profile, we need to associate the fixture microposts with a user. Rails includes a convenient way to build associations in fixtures, like this:

Click here to view code image

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

By identifying the user as michael, we tell Rails to associate this micropost with the corresponding user in the users fixture:

Click here to view code image

michael:
  name: Michael Example
  email: michael@example.com
  .
  .
  .

To test micropost pagination, we’ll also generate some additional micropost fixtures using the same embedded Ruby technique we used to make additional users in Listing 10.47:

Click here to view code image

<% 30.times do |n| %>
micropost_<%= n %>:
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>

Putting all this together gives the updated micropost fixtures in Listing 13.27.

Listing 13.27: Micropost fixtures with user associations.
test/fixtures/microposts.yml

Click here to view code image

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>
  user: michael

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>

  user: michael

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>

  user: michael

<% 30.times do |n| %>
micropost_<%= n %>:
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>

With the test data thus prepared, the test itself is fairly straightforward: we visit the user profile page and check for the page title and the user’s name, Gravatar, micropost count, and paginated microposts. The result appears in Listing 13.28. Note the use of the full_title helper from Listing 4.2 to test the page’s title, which we gain access to by including the Application Helper module into the test.⁹

9. If you’d like to refactor other tests to use full_title (such as those in Listing 3.30), you should include the Application Helper in test_helper.rb instead.

Listing 13.28: A test for the user profile. GREEN
test/integration/users_profile_test.rb

Click here to view code image

require 'test_helper'

class UsersProfileTest < ActionDispatch::IntegrationTest
  include ApplicationHelper

  def setup
    @user = users(:michael)
  end

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
end

The micropost count assertion in Listing 13.28 uses response.body, which we saw briefly in the Chapter 12 exercises (Section 12.3.3). Despite its name, response.body contains the full HTML source of the page (and not just the page’s body). This means that if all we care about is that the number of microposts appears somewhere on the page, we can look for a match as follows:

Click here to view code image

assert_match @user.microposts.count.to_s, response.body

This is a much less specific assertion than assert_select; in particular, unlike assert_select, using assert_match in this context doesn’t require us to indicate which HTML tag we’re looking for.

Listing 13.28 also introduces the nesting syntax for assert_select:

Click here to view code image

assert_select 'h1>img.gravatar'

This checks for an img tag with class gravatar inside a top-level heading tag (h1).

Because the application code was working, the test suite should be GREEN:

Listing 13.29: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Comment out the application code needed to change the two 'h1' lines in Listing 13.28 from GREEN to RED.

2. Update Listing 13.28 to test that will_paginate appears only once. Hint: Refer to Table 5.2.

13.3 Manipulating Microposts

Having finished both the data modeling and display templates for microposts, we now turn our attention to the interface for creating them through the web. In this section, we’ll also see the first hint of a status feed —a notion brought to full fruition in Chapter 14. Finally, as with users, we’ll make it possible to destroy microposts through the web.

There is one break with past convention worth noting: the interface to the Microposts resource will run principally through the Profile and Home pages, so we won’t need actions like new or edit in the Microposts controller; we’ll need only create and destroy. This leads to the routes for the Microposts resource shown in Listing 13.30. The code in Listing 13.30 leads in turn to the RESTful routes shown in Table 13.2, which is a small subset of the full set of routes seen in Table 2.3. Of course, this simplicity is a sign of being more advanced, not less—we’ve come a long way since our reliance on scaffolding in Chapter 2, and we no longer need most of its complexity.

Listing 13.30: Routes for the Microposts resource.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root    'static_pages#home'
  get     '/help',    to: 'static_pages#help'
  get     '/about',   to: 'static_pages#about'
  get     '/contact', to: 'static_pages#contact'
  get     '/signup', to: 'users#new'
  get     '/login',   to: 'sessions#new'
  post    '/login',   to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

Table 13.2: RESTful routes provided by the Microposts resource in Listing 13.30.

13.3.1 Micropost Access Control

We begin our development of the Microposts resource with some access control in the Microposts controller. In particular, because we access microposts through their associated users, both the create and destroy actions must require users to be logged in.

Tests to enforce logged-in status mirror those for the Users controller (Listing 10.20 and Listing 10.61). We simply issue the correct request to each action and confirm that the micropost count is unchanged and the result is redirected to the login URL, as seen in Listing 13.31.

Listing 13.31: Authorization tests for the Microposts controller. RED
test/controllers/microposts_controller_test.rb

Click here to view code image

require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end
end

Writing the application code needed to get the tests in Listing 13.31 to pass requires a little refactoring first. Recall from Section 10.2.1 that we enforced the login requirement using a before filter that called the logged_in_user method (Listing 10.15). At the time, we needed that method only in the Users controller, but now we find that we need it in the Microposts controller as well, so we’ll move it into the Application controller, which is the base class of all controllers (Section 4.4.4). The result appears in Listing 13.32.

Listing 13.32: Moving the logged_in_user method into the Application controller. RED
app/controllers/application_controller.rb

Click here to view code image

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  private

    # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

To avoid code repetition, you should also remove logged_in_user from the Users controller at this time (Listing 13.33).

Listing 13.33: The Users controller with the logged-in user filter removed. RED
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  .
  .
  .
  private

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

    # Before filters

    # Confirms the correct user.
    def correct_user
      @user = User.find(params[:id])
       redirect_to(root_url) unless current_user?(@user)
     end

     # Confirms an admin user.
     def admin_user
       redirect_to(root_url) unless current_user.admin?
     end
end

With the code in Listing 13.32, the logged_in_user method is now available in the Microposts controller, which means that we can add create and destroy actions and then restrict access to them using a before filter, as shown in Listing 13.34.

Listing 13.34: Adding authorization to the Microposts controller actions. GREEN
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
  end

  def destroy
  end
end

At this point, the tests should pass:

Listing 13.35: GREEN

\$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Why is it a bad idea to leave a copy of logged_in_user in the Users controller?

13.3.2 Creating Microposts

In Chapter 7, we implemented user signup by making an HTML form that issued an HTTP POST request to the create action in the Users controller. The implementation of micropost creation is similar; the main difference is that rather than using a separate page at /microposts/new, we will put the form on the Home page itself (i.e., the root path /), as mocked up in Figure 13.10.

When we last left the Home page, it appeared as in Figure 5.8—that is, it had a “Sign up now!” button in the middle. Since a micropost creation form makes sense only in the context of a particular logged-in user, one goal of this section will be to serve different versions of the Home page depending on a visitor’s login status. We’ll implement this in Listing 13.37 below.

Figure 13.10: A mockup of the Home page with a form for creating microposts.

We’ll start with the create action for microposts, which is similar to its user analogue (Listing 7.28); the principal difference lies in using the user/micropost association to build the new micropost, as seen in Listing 13.36. Note the use of strong parameters via micropost_params, which permits only the micropost’s content attribute to be modified through the web.

Listing 13.36: The Microposts controller create action.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

To build a form for creating microposts, we use the code in Listing 13.37, which serves up different HTML based on whether the site visitor is logged in or not.

Listing 13.37: Adding microposts creation to the Home page (/).
app/views/static_pages/home.html.erb

Click here to view code image

<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
      sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
   </div>

   <%= link_to image_tag("rails.png", alt: "Rails logo"),
               'http://rubyonrails.org/' %>
<% end %>

(Having so much code in each branch of the if-else conditional is a bit messy, and cleaning it up using partials is left as an exercise [Section 13.3.2].)

To get the page defined in Listing 13.37 working, we need to create and fill in a couple of partials. The first is the new Home page sidebar, as shown in Listing 13.38.

Listing 13.38: The partial for the user info sidebar.
app/views/shared/_user_info.html.erb

Click here to view code image

<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>

Note that, as in the profile sidebar (Listing 13.24), the user info in Listing 13.38 displays the total number of microposts for the user. There’s a slight difference in the display, though; in the profile sidebar, “Microposts” is a label, and showing “Microposts (1)” makes sense. In the present case, though, saying “1 microposts” is ungrammatical, so we arrange to display “1 micropost” and “2 microposts” using the pluralize method we saw in Section 7.3.3.

We next define the form for creating microposts (Listing 13.39), which is similar to the signup form in Listing 7.15.

Listing 13.39: The form partial for creating microposts.
app/views/shared/_micropost_form.html.erb

Click here to view code image

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

We need to make two changes before the form in Listing 13.39 will work. First, we need to define @micropost, which (as before) we do through the association:

Click here to view code image

@micropost = current_user.microposts.build

The result appears in Listing 13.40.

Listing 13.40: Adding a micropost instance variable to the home action.
app/controllers/static_pages_controller.rb

Click here to view code image

class StaticPagesController < ApplicationController

  def home
    @micropost = current_user.microposts.build if logged_in?
  end

  def help
  end

  def about
  end

  def contact
  end
end

Of course, current_user exists only if the user is logged in, so the @micropost variable should only be defined in this case.

The second change needed to get Listing 13.39 to work is to redefine the error- messages partial so the following code from Listing 13.39 works:

Click here to view code image

<%= render 'shared/error_messages', object: f.object %>

You may recall from Listing 7.20 that the error-messages partial references the @user variable explicitly, but in the present case we have an @micropost variable instead. To unify these cases, we can pass the form variable f to the partial and access the associated object through f.object, so that in

form_for(@user) do |f|

f.object is @user, and in

form_for(@micropost) do |f|

f.object is @micropost, etc.

To pass the object to the partial, we use a hash with value equal to the object and key equal to the desired name of the variable in the partial, which is what the second line in Listing 13.39 accomplishes. In other words, object: f.object creates a variable called object in the error_messages partial, and we can use it to construct a customized error message, as shown in Listing 13.41.

Listing 13.41: Error messages that work with other objects. RED
app/views/shared/_error_messages.html.erb

Click here to view code image

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

At this point, you should verify that the test suite is RED:

Listing 13.42: RED

$ rails test

This is a hint that we need to update the other occurrences of the error-messages partial, which we used when signing up users (Listing 7.20), resetting passwords (Listing 12.14), and editing users (Listing 10.2). The updated versions are shown in Listing 13.43, Listing 13.45, and Listing 13.44.

Listing 13.43: Updating the rendering of user signup errors.
app/views/users/new.html.erb

Click here to view code image

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
      <%= 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>

Listing 13.44: Updating the errors for editing users.
app/views/users/edit.html.erb

Click here to view code image

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <%= 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 "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails">change</a>
    </div>
  </div>
</div>

Listing 13.45: Updating the errors for password resets.
app/views/password_resets/edit.html.erb

Click here to view code image

<% provide(:title, 'Reset password') %>
<h1>Password reset</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <%= hidden_field_tag :email, @user.email %>
      <%= 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 "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

At this point, all the tests should be GREEN:

$ rails test

Additionally, all the HTML in this section should render properly, showing the form as in Figure 13.11 and a form with a submission error as in Figure 13.12.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Refactor the Home page to use separate partials for the two branches of the if-else statement.

Figure 13.11: The Home page with a new micropost form.

Figure 13.12: The Home page with a form error.

13.3.3 A Proto-Feed

Although the micropost form is actually now working, users can’t immediately see the results of a successful submission because the current Home page doesn’t display any microposts. If you like, you can verify that the form shown in Figure 13.11 is working by submitting a valid entry and then navigating to the profile page to see the post, but that’s rather cumbersome. It would be far better to have a feed of microposts that includes the user’s own posts, as mocked up in Figure 13.13. (In Chapter 14, we’ll generalize this feed to include the microposts of users being followed by the current user, à la Twitter.)

Since each user should have a feed, we are led naturally to a feed method in the User model, which will initially just select all the microposts belonging to the current user. We’ll accomplish this using the where method on the Micropost model (seen briefly before in Section 11.3.3), as shown in Listing 13.46.¹⁰

10. See the Rails Guide on the Active Record Query Interface for more on where and related methods.

Figure 13.13: A mockup of the Home page with a proto-feed.

Listing 13.46: A preliminary implementation for the micropost status feed.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Defines a proto-feed.
  # See "Following users" for the full implementation.
  def feed
    Micropost.where("user_id = ?", id)
  end

    private
    .
    .
    .
end

The question mark in

Click here to view code image

Micropost.where("user_id = ?", id)

ensures that id is properly escaped before being included in the underlying SQL query, thereby avoiding a serious security hole called SQL injection. The id attribute here is just an integer (i.e., self.id, the unique ID of the user), so there is no danger of SQL injection in this case, but it does no harm, and always escaping variables injected into SQL statements is a good habit to cultivate.

Alert readers might note at this point that the code in Listing 13.46 is essentially equivalent to writing

Click here to view code image

def feed
  microposts
end

We’ve used the code in Listing 13.46 instead because it generalizes much more naturally to the full status feed needed in Chapter 14.

To use the feed in the sample application, we add an @feed_items instance variable for the current user’s (paginated) feed, as in Listing 13.47, and then add a status feed partial (Listing 13.48) to the Home page (Listing 13.49). Note that, now that there are two lines that need to be run when the user is logged in, Listing 13.47 changes

Click here to view code image

@micropost = current_user.microposts.build if logged_in?

from Listing 13.40 to

Click here to view code image

if logged_in?
  @micropost = current_user.microposts.build
  @feed_items = current_user.feed.paginate(page: params[:page])
end

thereby moving the conditional from the end of the line to an if-end statement.

Listing 13.47: Adding a feed instance variable to the home action.
app/controllers/static_pages_controller.rb

Click here to view code image

class StaticPagesController < ApplicationController
  def home
    if logged_in?
      @micropost = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end

  def help
  end

  def about
  end

  def contact
  end
end

Listing 13.48: The status feed partial.
app/views/shared/_feed.html.erb

Click here to view code image

<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

The status feed partial defers the rendering to the micropost partial defined in Listing 13.22:

<%= render @feed_items %>

Here Rails knows to call the micropost partial because each element of @feed_items has class Micropost. This causes Rails to look for a partial with the corresponding name in the views directory of the given resource:

Click here to view code image

app/views/microposts/_micropost.html.erb

Figure 13.14: The Home page with a proto-feed.

We can add the feed to the Home page by rendering the feed partial as usual (Listing 13.49). The result is a display of the feed on the Home page, as required (Figure 13.14).

Listing 13.49: Adding a status feed to the Home page.
app/views/static_pages/home.html.erb

Click here to view code image

<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
     <div class="col-md-8">
       <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

At this point, creating a new micropost works as expected, as seen in Figure 13.15. There is one subtlety, though: on failed micropost submission, the Home page expects an @feed_items instance variable, so failed submissions currently break. The easiest solution is to suppress the feed entirely by assigning it an empty array, as shown in

Figure 13.15: The Home page after creating a new micropost.

Listing 13.50. (Unfortunately, returning a paginated feed doesn’t work in this case. Implement it and click on a pagination link to see why.)

Listing 13.50: Adding an (empty) @feed_items instance variable to the create action.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Use the newly created micropost UI to create the first real micropost. What are the contents of the INSERT command in the server log?

2. In the console, set user to the first user in the database. Confirm that the values of by Micropost.where("user_id = ?", user.id), user.microposts, and user.feed are all the same. Hint: It’s probably easiest to compare directly using ==.

Figure 13.16: A mockup of the proto-feed with micropost delete links.

13.3.4 Destroying Microposts

The last piece of functionality to add to the Microposts resource is the ability to destroy posts. As with user deletion (Section 10.4.2), we accomplish this with “delete” links, as mocked up in Figure 13.16. Unlike that case, which restricted user destruction to admin users, the delete links will work only for microposts created by the current user.

Our first step is to add a delete link to the micropost partial as in Listing 13.22. The result appears in Listing 13.51.

Listing 13.51: Adding a delete link to the micropost partial.
app/views/microposts/_micropost.html.erb

Click here to view code image

<li id="<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

The next step is to define a destroy action in the Microposts controller, which is analogous to the user case in Listing 10.59. The main difference is that rather than using an @user variable with an admin_user before filter, we’ll find the micropost through the association, which will automatically fail if a user tries to delete another user’s micropost. We’ll put the resulting find inside a correct_user before filter, which checks that the current user actually has a micropost with the given id. The result appears in Listing 13.52.

Listing 13.52: The Microposts controller destroy action.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user, only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

Note that the destroy method in Listing 13.52 redirects to the URL

request.referrer || root_url

This uses the request.referrer method,¹¹ which is related to the request. original_url variable used in friendly forwarding (Section 10.2.3) and is just the previous URL (in this case, the Home page).¹² This is convenient because microposts appear on both the Home page and on the user’s profile page, so by using request.referrer we arrange to redirect back to the page issuing the delete request in both cases. If the referring URL is nil (as is the case inside some tests), Listing 13.52 sets the root_url as the default using the || operator. (Compare to the default options defined in Listing 9.24.)

11. This corresponds to HTTP_REFERER, as defined by the specification for HTTP. Note that “referer” is not a typo—the word is actually misspelled in the spec. Rails corrects this error by writing “referrer” instead.

12. I didn’t remember offhand how to get this URL inside a Rails application, so I Googled “rails request previous url” and found a Stack Overflow thread with the answer.

With the code as above, the result of destroying the second-most recent post appears in Figure 13.17.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Create a new micropost and then delete it. What are the contents of the DELETE command in the server log?

2. Confirm directly in the browser that the line redirect_to request.referrer || root_url can be replaced with the line redirect_back(fallback_ location: root_url). (This method was added in Rails 5.)

13.3.5 Micropost Tests

With the code in Section 13.3.4, the Micropost model and interface are complete. All that’s left is writing a short Microposts controller test to check authorization and a micropost integration test to tie it all together.

Figure 13.17: The Home page after deleting the second-most-recent micropost.

We’ll start by adding a few microposts with different owners to the micropost fixtures, as shown in Listing 13.53. (We’ll be using only one for now, but we’ve put in the others for future reference.)

Listing 13.53: Adding a micropost with a different owner.
test/fixtures/microposts.yml

Click here to view code image

.
.
.
ants:
  content: "Oh, is that what you want? Because that's how you get ants!"
  created_at: <%= 2.years.ago %>
  user: archer

zone:
  content: "Danger zone!"
  created_at: <%= 3.days.ago %>
  user: archer

tone:
  content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
  created_at: <%= 10.minutes.ago %>
  user: lana

van:
  content: "Dude, this van's, like, rolling probable cause."
  created_at: <%= 4.hours.ago %>
  user: lana

We next write a short test to make sure one user can’t delete the microposts of a different user, and we also check for the proper redirect, as seen in Listing 13.54.

Listing 13.54: Testing micropost deletion with a user mismatch. GREEN
test/controllers/microposts_controller_test.rb

Click here to view code image

require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end
test "should redirect destroy for wrong micropost" do
    log_in_as(users(:michael))
    micropost = microposts(:ants)
    assert_no_difference 'Micropost.count' do
      delete micropost_path(micropost)
    end
    assert_redirected_to root_url
  end
end

Finally, we’ll write an integration test to log in, check the micropost pagination, make an invalid submission, make a valid submission, delete a post, and then visit a second user’s page to make sure there are no “delete” links. We start by generating a test as usual:

Click here to view code image

$ rails generate integration_test microposts_interface
      invoke test_unit
      create   test/integration/microposts_interface_test.rb

The test appears in Listing 13.55. See if you can connect the lines in Listing 13.12 to the steps mentioned above.

Listing 13.55: An integration test for the micropost interface. GREEN
test/integration/microposts_interface_test.rb

Click here to view code image

require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # Invalid submission
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # Valid submission
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # Delete post
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # Visit different user (no delete links)
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end
end

Because we wrote working application code first, the test suite should be GREEN:

Listing 13.56: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. For each of the four scenarios indicated by comments in Listing 13.55 (starting with “Invalid submission”), comment out application code to get the corresponding test to RED, then uncomment to get back to GREEN.

2. Add tests for the sidebar micropost count (including proper pluralization). Listing 13.57 will help get you started.

Listing 13.57: A template for the sidebar micropost count test.
test/integration/microposts_interface_test.rb

Click here to view code image

require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "micropost sidebar count" do
    log_in_as(@user) get root_path
    assert_match "#{FILL_IN} microposts", response.body
    # User with zero microposts
    other_user = users(:malory)
    log_in_as(other_user)
    get root_path
    assert_match "0 microposts", response.body
    other_user.microposts.create!(content: "A micropost")
    get root_path
    assert_match FILL_IN, response.body
  end
end

13.4 Micropost Images

Now that we’ve added support for all relevant micropost actions, in this section we’ll make it possible for microposts to include images as well as text. We’ll start with a basic version good enough for development use, and then add a series of enhancements to make image upload production-ready.

Adding image upload involves two main visible elements: a form field for uploading an image and the micropost images themselves. A mockup of the resulting “Upload image” button and micropost photo appears in Figure 13.18.¹³

13. Image retrieved from https://www.flickr.com/photos/grungepunk/14026922186 on 2014-09-19. Copyright © 2014 by Jussie D. Brito and used unaltered under the terms of the Creative Commons Attribution-ShareAlike 2.0 Generic license.

13.4.1 Basic Image Upload

To handle an uploaded image and associate it with the Micropost model, we’ll use the CarrierWave image uploader. To get started, we need to include the carrierwave gem in the Gemfile (Listing 13.58).¹⁴ For completeness, Listing 13.58 also includes the mini_magick and fog gems needed for image resizing (Section 13.4.3) and image upload in production (Section 13.4.4).

14. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

Figure 13.18: A mockup of micropost image upload (with an uploaded image).

Listing 13.58: Adding CarrierWave to the Gemfile.

Click here to view code image

source 'https://rubygems.org'
gem 'rails',                   '5.0.0'
gem 'bcrypt',                  '3.1.11'
gem 'faker',                   '1.6.3'
gem 'carrierwave',             '0.11.2'
gem 'mini_magick',             '4.5.1'
gem 'fog',                     '1.38.0'
gem 'will_paginate',           '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
.
.
.

Then we install as usual:

$ bundle install

CarrierWave adds a Rails generator for creating an image uploader, which we’ll use to make an uploader for an image called picture:¹⁵

15. Initially, I called the new attribute image, but that name was so generic it ended up being confusing.

Click here to view code image

$ rails generate uploader Picture

Images uploaded with CarrierWave should be associated with a corresponding attribute in an Active Record model, which simply contains the name of the image file in a string field. The resulting augmented data model for microposts appears in Figure 13.19.

Figure 13.19: The Micropost data model with a picture attribute.

To add the required picture attribute to the Micropost model, we generate a migration and migrate the development database:

Click here to view code image

$ rails generate migration add_picture_to_microposts picture:string
$ rails db:migrate

The way to tell CarrierWave to associate the image with a model is to use the mount_uploader method, which takes as arguments a symbol representing the attribute and the class name of the generated uploader:

Click here to view code image

mount_uploader :picture, PictureUploader

(Here PictureUploader is defined in the file picture_uploader.rb, which we’ll start editing in Section 13.4.2, but for now the generated default is fine.) Adding the uploader to the Micropost model gives the code shown in Listing 13.59.

Listing 13.59: Adding an image to the Micropost model.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

On some systems, you may need to restart the Rails server at this point to keep the test suite GREEN. (If you’re using Guard as described in Section 3.6.2, you may need to restart that as well, and it may even be necessary to exit the terminal shell and re-run Guard in a new one.)

To include the uploader on the Home page as in Figure 13.18, we need to include a file_field tag in the micropost form, as shown in Listing 13.60.

Listing 13.60: Adding image upload to the micropost create form.
app/views/shared/_micropost_form.html.erb

Click here to view code image

<%= form_for(@micropost, html: { multipart: true }) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture %>
  </span>
<% end %>

Note the inclusion of

html: { multipart: true }

in the arguments to form_for, which is necessary for file uploads.

Finally, we need to add picture to the list of attributes permitted to be modified through the web. This involves editing the micropost_params method, as shown in Listing 13.61.

Listing 13.61: Adding picture to the list of permitted attributes.
app/controllers/microposts_controller.rb

Click here to view code image

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user, only: :destroy
  .
  .
  .
  private

    def micropost_params
      params.require(:micropost).permit(:content, :picture)
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

Once the image has been uploaded, we can render it using the image_tag helper in the micropost partial, as shown in Listing 13.62. Notice the use of the picture? boolean method to prevent displaying an image tag when there isn’t an image. This method is created automatically by CarrierWave based on the name of the image attribute. The result of making a successful submission by hand appears in Figure 13.20. Writing an automated test for image upload is left as an exercise (Section 13.4.1).

Figure 13.20: The result of submitting a micropost with an image.

Listing 13.62: Adding image display to microposts.
app/views/microposts/_micropost.html.erb

Click here to view code image

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Upload a micropost with attached image. Does the result look too big? (If so, don’t worry; we’ll fix it in Section 13.4.3).

2. Following the template in Listing 13.63, write a test of the image uploader in Section 13.4. As preparation, you should add an image to the fixtures directory (using, e.g, cp app/assets/images/rails.png test/fixtures/). The additional assertions in Listing 13.63 check both for a file upload field on the Home page and for a valid image attribute on the micropost resulting from valid submission. Note the use of the special fixture_file_upload method for uploading files as fixtures inside tests.¹⁶ Hint: To check for a valid picture attribute, use the assigns method mentioned in Section 11.3.3 to access the micropost in the create action after valid submission.

16. Windows users should add a :binary parameter: fixture_file_upload(file, type, :binary).

Listing 13.63: A template for testing image upload.
test/integration/microposts_interface_test.rb

Click here to view code image

require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    assert_select 'input[type=FILL_IN]'
    # Invalid submission
    post microposts_path, params: { micropost: { content: "" } }
    assert_select 'div#error_explanation'
    # Valid submission
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, micropost: { content: content, picture: FILL_IN }
    end
    assert FILL_IN.picture?
    follow_redirect!
    assert_match content, response.body
    # Delete a post.
    assert_select 'a', 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # Visit a different user.
    get user_path(users(:archer))
    assert_select 'a', { text: 'delete', count: 0 }
  end
  .
  .
  .
end

13.4.2 Image Validation

The uploader in Section 13.4.1 is a good start, but it has significant limitations. In particular, it doesn’t enforce any constraints on the uploaded file, which can cause problems if users try to upload large files or invalid file types. To remedy this defect, we’ll add validations for the image size and format, both on the server and on the client (i.e., in the browser).

The first image validation, which restricts uploads to valid image types, appears in the CarrierWave uploader itself. The resulting code (which appears as a commented-out suggestion in the generated uploader) verifies that the image filename ends with a valid image extension (PNG, GIF, and both variants of JPEG), as shown in Listing 13.64.

Listing 13.64: The picture format validation.
app/uploaders/picture_uploader.rb

Click here to view code image

class PictureUploader < CarrierWave::Uploader::Base
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  def extension_white_list
    %w(jpg jpeg gif png)
  end
end

The second validation, which controls the size of the image, appears in the Micropost model itself. In contrast to previous model validations, file size validation doesn’t correspond to a built-in Rails validator. As a result, validating images requires defining a custom validation, which we’ll call picture_size and define as shown in Listing 13.65. Note the use of validate (as opposed to validates) to call a custom validation.

Listing 13.65: Adding validations to images.
app/models/micropost.rb

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
  validate :picture_size

  private

    # Validates the size of an uploaded picture.
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
   end
end

This custom validation arranges to call the method corresponding to the given symbol (:picture_size). In picture_size itself, we add a custom error message to the errors collection (seen before briefly in Section 6.2.2), in this case a limit of 5 megabytes (using a syntax seen before in Box 9.1).

To go along with the validations in Listing 13.64 and Listing 13.65, we’ll add two client-side checks on the uploaded image. We’ll first mirror the format validation by using the accept parameter in the file_field input tag:

Click here to view code image

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>

The valid formats consist of the MIME types accepted by the validation in Listing 13.64.

Next, we’ll include a little JavaScript (or, more specifically, jQuery) to issue an alert if a user tries to upload an image that’s too big (which prevents accidental time-consuming uploads and lightens the load on the server):

Click here to view code image

$('#micropost_picture').bind('change', function() {
  var size_in_megabytes = this.files[0].size/1024/1024;
  if (size_in_megabytes > 5) {
    alert('Maximum file size is 5MB. Please choose a smaller file.');
  }
});

Although jQuery isn’t the focus of this book, you might be able to figure out that the code above monitors the page element containing the CSS id micropost_picture (as indicated by the hash mark #), which is the id of the micropost form in Listing 13.60. (The way to figure this out is to Ctrl-click and use your browser’s web inspector.) When the element with that CSS id changes, the jQuery function fires and issues the alert method if the file is too big.¹⁷

17. To learn how to do things like this, you can do what I did: Google around for things like “javascript maximum file size” until you find something on Stack Overflow.

The result of adding these additional checks appears in Listing 13.66.¹⁸

18. More advanced users of jQuery would probably put the size check in its own JavaScript function, but since this isn’t a JavaScript tutorial the code in Listing 13.66 is fine for our purposes.

Listing 13.66: Checking the file size with jQuery.
app/views/shared/_micropost_form.html.erb

Click here to view code image

<%= form_for(@micropost, html: { multipart: true }) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
  </span>
<% end %>

<script type="text/javascript">
  $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
  });
</script>

As you can see by trying to upload a file that’s too big, the code in Listing 13.66 doesn’t actually clear the file input field, so a user can just dismiss the alert box and continue trying to upload the file. If this were a book on jQuery, we would probably polish this slight blemish by clearing the field, but it’s important to understand that front-end code like that shown in Listing 13.66 can’t prevent a user from trying to upload a file that’s too big. Even if our code cleared the file input field, there would be no way to prevent a user from editing the JavaScript with a web inspector or issue a direct POST request using, e.g., curl. To prevent users from uploading arbitrarily large files, it is thus essential to include a server-side validation, as in Listing 13.65.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. What happens if you try uploading an image bigger than 5 megabytes?

2. What happens if you try uploading a file with an invalid extension?

13.4.3 Image Resizing

The image size validations in Section 13.4.2 are a good start, but they still allow the uploading of images large enough to break our site’s layout, sometimes with frightening results (Figure 13.21). Thus, while it’s convenient to allow users to select fairly large images from their local disk, it’s also a good idea to resize the images before displaying them.¹⁹

19. It’s possible to constrain the display size with CSS, but this doesn’t change the image size. In particular, large images would still take a while to load. (You’ve probably visited websites where “small” images seemingly take forever to load. This is why.)

Figure 13.21: A frighteningly large uploaded image.

We’ll be resizing images using the image manipulation program ImageMagick, which we need to install on the development environment. (As we’ll see in Section 13.4.4, when using Heroku for deployment ImageMagick comes pre-installed in production.) On the cloud IDE, we can do this as follows:²⁰

20. I got this from the official Ubuntu documentation. If you’re not using the cloud IDE or an equivalent Linux system, do a Google search for “imagemagick <your platform>”. On OS X, brew install imagemagick should work if you have Homebrew installed.

Click here to view code image

$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing

(If you’re developing on your local machine, you may have to install ImageMagick a different way, such as using Homebrew on a Mac via brew install imagemagick. Use your technical sophistication [Box 1.1] if you get stuck.)

Next, we need to include CarrierWave’s MiniMagick interface for ImageMagick, together with a resizing command. For the resizing command, there are several possibilities listed in the MiniMagick documentation, but the one we want is resize_to_limit: [400, 400], which resizes large images so that they aren’t any bigger than 400px in either dimension, while simultaneously leaving smaller images alone. (The other main possibilities listed in the CarrierWave documentation on MiniMagick stretch images if they’re too small, which isn’t what we want in this case.) With the code as shown in Listing 13.67, large images are now resized nicely (Figure 13.22).

Figure 13.22: A nicely resized image.

Listing 13.67: Configuring the image uploader for image resizing.app/uploaders/picture_uploader.rb

Click here to view code image

class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]

  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  def extension_white_list
    %w(jpg jpeg gif png)
  end
end

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Upload a large image and confirm directly that the resizing is working. Does the resizing work even if the image isn’t square?

2. If you completed the image upload test in Listing 13.63, at this point your test suite may be giving you a confusing error message. Fix this issue by configuring CarrierWave to skip image resizing in tests using the initializer file shown in Listing 13.68.

Listing 13.68: An initializer to skip image resizing in tests.
config/initializers/skip_image_resizing.rb

Click here to view code image

if Rails.env.test?
  CarrierWave.configure do |config|
    config.enable_processing = false
  end
end

13.4.4 Image Upload In Production

The image uploader developed in Section 13.4.3 is good enough for development, but (as seen in the storage :file line in Listing 13.67) it uses the local filesystem for storing the images, which isn’t a good practice in production.²¹ Instead, we’ll use a cloud storage service to store images separately from our application.²²

21. Among other things, file storage on Heroku is temporary, so uploaded images will be deleted every time you deploy.

22. This is a challenging section and can be skipped without loss of continuity.

To configure our application to use cloud storage in production, we’ll use the fog gem, as shown in Listing 13.69.

Listing 13.69: Configuring the image uploader for production.
app/uploaders/picture_uploader.rb

Click here to view code image

class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]
  if Rails.env.production?
    storage :fog
  else
    storage :file
  end
  # Override the directory where uploaded files will be stored
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  def extension_white_list
    %w(jpg jpeg gif png)
  end
end

Listing 13.69 uses the production? boolean from Box 7.1 to switch storage method based on the environment:

Click here to view code image

if Rails.env.production?
  storage :fog
else
  storage :file
end

There are many choices for cloud storage, but we’ll use one of the most popular and well-supported, Amazon.com’s Simple Storage Service (S3).²³ Here are the essential steps to getting set up:

23. S3 is a paid service, but the storage needed to set up and test the Rails Tutorial sample application costs less than a cent per month.

1. Sign up for an Amazon Web Services account.

2. Create a user via AWS Identity and Access Management (IAM) and record the access key and secret key.

3. Create an S3 bucket (with a name of your choice) using the AWS Console, and then grant read and write permission to the user created in the previous step.

You will likely find setting up S3 to be a challenging exercise in technical sophistication (Box 1.1); for further details on the steps above, consult the S3 documentation²⁴ (and, if necessary, Google or Stack Overflow).

24. aws.amazon.com/documentation/s3/

Once you’ve created and configured your S3 account, you should create and fill the CarrierWave configuration file as shown in Listing 13.70.

Note: If your setup isn’t working, your region location may be the issue. Some users may have to add :region => ENV['S3_REGION'] to the fog credentials, followed by heroku config:set S3_REGION=<bucket region> at the command line, where the bucket region should be something like 'eu-central-1', depending on your location. To determine the correct region, consult the list of Regions and Endpoints at Amazon.

Listing 13.70: Configuring CarrierWave to use S3.
config/initializers/carrier_wave.rb

Click here to view code image

if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      # Configuration for Amazon S3
      :provider               => 'AWS',
      :aws_access_key_id      => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key  => ENV['S3_SECRET_KEY']
    }
    config.fog_directory       = ENV['S3_BUCKET']
  end
end

As with production email configuration (Listing 11.41), Listing 13.70 uses Heroku ENV variables to avoid hard-coding sensitive information. In Section 11.4 and Section 12.4, these variables were defined automatically via the SendGrid add-on, but in this case we need to define them explicitly, which we can accomplish using heroku config:set as follows:

Click here to view code image

$ heroku config:set S3_ACCESS_KEY=<access key>
$ heroku config:set S3_SECRET_KEY=<secret key>
$ heroku config:set S3_BUCKET=<bucket name>

With the configuration above, we are ready to commit our changes and deploy. I recommend updating your .gitignore file as shown in Listing 13.71 so that the image uploads directory is ignored.

Listing 13.71: Adding the uploads directory to the .gitignore file.

Click here to view code image

.
.
.
# Ignore uploaded test images.
/public/uploads

We’re now ready to commit the changes on our topic branch and merge back to master:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push

Figure 13.23: An uploaded image in production.

Then we deploy, reset the database, and reseed the sample data:

Click here to view code image

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

Because Heroku comes with an installation of ImageMagick, the result is successful image resizing and upload in production, as seen in Figure 13.23.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Upload a large image and confirm directly that the resizing is working in production. Does the resizing work even if the image isn’t square?

13.5 Conclusion

With the addition of the Microposts resource, we are nearly finished with our sample application. All that remains is to add a social layer by letting users follow each other. We’ll learn how to model such user relationships, and see the implications for the microposts feed, in Chapter 14.

If you skipped Section 13.4.4, be sure to commit and merge your changes:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push

Then deploy to production:

Click here to view code image

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

It’s worth noting that this chapter saw the last of the necessary gem installations. For reference, the final Gemfile is shown in Listing 13.72.²⁵

25. As always, you should use the version numbers listed at gemfiles-4th-ed.railstutorial.org instead of the ones listed here.

Listing 13.72: The final Gemfile for the sample application.

Click here to view code image

source 'https://rubygems.org'

gem 'rails',                    '5.0.0'
gem 'bcrypt',                   '3.1.11'
gem 'faker',                    '1.6.3'
gem 'carrierwave',              '0.11.2'
gem 'mini_magick',              '4.5.1'
gem 'fog',                      '1.38.0'
gem 'will_paginate',            '3.1.0'
gem 'bootstrap-will_paginate', '0.0.10'
gem 'bootstrap-sass',           '3.3.6'
gem 'puma',                     '3.4.0'
gem 'sass-rails',               '5.0.5'
gem 'uglifier',                 '3.0.0'
gem 'coffee-rails',             '4.2.1'
gem 'jquery-rails',             '4.1.1'
gem 'turbolinks',               '5.0.0'
gem 'jbuilder',                 '2.4.1'

group :development, :test do
  gem 'sqlite3', '1.3.11'
  gem 'byebug',   '9.0.0', platform: :mri
end

group :development do
  gem 'web-console',           '3.1.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '1.7.2'
  gem 'spring-watcher-listen', '2.0.0'
end

group :test do
  gem 'rails-controller-testing', '0.1.1'
  gem 'minitest-reporters',        '1.1.9'
  gem 'guard',                     '2.13.0'
  gem 'guard-minitest',            '2.4.4'
end

group :production do
  gem 'pg'       '0.18.4'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

13.5.1 What We Learned in This Chapter

• Microposts, like Users, are modeled as a resource backed by an Active Record model.

• Rails supports multiple-key indices.

• We can model a user having many microposts using the has_many and belongs_to methods in the User and Micropost models, respectively.

• The has_many/belongs_to combination gives rise to methods that work through the association.

• The code user.microposts.build(...) returns a new Micropost object automatically associated with the given user.

• Rails supports default ordering via default_scope.

• Scopes take anonymous functions as arguments.

• The dependent: :destroy option causes objects to be destroyed at the same time as associated objects.

• Pagination and object counts can both be performed through associations, leading to automatically efficient code.

• Fixtures support the creation of associations.

• It is possible to pass variables to Rails partials.

• The where method can be used to perform Active Record selections.

• We can enforce secure operations by always creating and destroying dependent objects through their association.

• We can upload and resize images using CarrierWave.

Chapter 14. Following Users

In this chapter, we will complete the Rails Tutorial sample application by adding a social layer that allows users to follow (and unfollow) other users, resulting in each user’s Home page displaying a status feed of the followed users’ microposts. We’ll start by learning how to model relationships between users in Section 14.1, and we’ll build the corresponding web interface in Section 14.2 (including an introduction to Ajax). We’ll end by developing a fully functional status feed in Section 14.3.

This final chapter contains some of the most challenging material in the tutorial, including some Ruby/SQL trickery to make the status feed. Through these examples, you will see how Rails can handle even rather intricate data models, which should serve you well as you go on to develop your own applications with their own specific requirements. To help with the transition from tutorial to independent development, Section 14.4 offers some pointers to more advanced resources.

Because the material in this chapter is particularly challenging, before writing any code we’ll pause for a moment and take a tour of the interface. As in previous chapters, at this early stage we’ll represent pages using mockups.¹ The full page flow runs as follows: a user (John Calvin) starts at his profile page (Figure 14.1) and navigates to the Users page (Figure 14.2) to select a user to follow. Calvin navigates to the profile of a second user, Thomas Hobbes (Figure 14.3, page 682), clicking on the “Follow” button to follow that user. This changes the “Follow” button to “Unfollow” and increments Hobbes’s “followers” count by one (Figure 14.4, page 683). Navigating to his home page, Calvin now sees an incremented “following” count and finds Hobbes’s microposts in his status feed (Figure 14.5, page 684). The rest of this chapter is dedicated to making this page flow actually work.

1. Image of child retrieved from http://www.flickr.com/photos/john_lustig/2518452221/ on 2013-12-16. Copyright © 2008 by John Lustig and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license. Image of tiger retrieved from https://www.flickr.com/photos/renemensen/9187111340 on 2014-08-15. Copyright © 2013 by Rene Mesen and used unaltered under the terms of the Creative Commons Attribution 2.0 Generic license.

Figure 14.1: A current user’s profile.

14.1 The Relationship Model

Our first step in implementing following users is to construct a data model, which is not as straightforward as it seems. Naïvely, it seems that a has_many relationship would do: a user has_many followed users and has_many followers. As we will see, there is a problem with this approach, and we’ll learn how to fix it using has_many :through.

Figure 14.2: Finding a user to follow.

As usual, Git users should create a new topic branch:

Click here to view code image

$ git checkout -b following-users

14.1.1 A Problem with the Data Model (and a Solution)

As a first step toward constructing a data model for following users, let’s examine a typical case. For instance, consider a user who follows a second user: we could say that, e.g., Calvin is following Hobbes, and Hobbes is followed by Calvin, so that Calvin is the follower and Hobbes is followed. Using Rails’ default pluralization convention, the set of all users following a given user is that user’s followers, and hobbes.followers is an array of those users. Unfortunately, the reverse doesn’t work: by default, the set of all followed users would be called the followeds, which is ungrammatical and clumsy. We’ll adopt Twitter’s convention and call them following (as in “50 following, 75 followers”), with a corresponding calvin.following array.

Figure 14.3: The profile of a user to follow, with a follow button.

This discussion suggests modeling the followed users as in Figure 14.6, with a following table and a has_many association. Since user.following should be a collection of users, each row of the following table would need to be a user, as identified by the followed_id, together with the follower_id to establish the association.² In addition, since each row is a user, we would need to include the user’s other attributes, including the name, email, password, etc.

2. For simplicity, Figure 14.6 omits the following table’s id column.

Figure 14.4: A profile with an unfollow button and incremented followers count.

The problem with the data model in Figure 14.6 is that it is terribly redundant: each row contains not only each followed user’s id, but all their other information as well—all of which is already in the users table. Even worse, to model user followers we would need a separate, similarly redundant followers table. Finally, this data model is a maintainability nightmare: each time a user changed (say) their name, we would need to update not just the user’s record in the users table but also every row containing that user in both the following and followers tables.

The problem here is that we are missing an underlying abstraction. One way to find the proper model is to consider how we might implement the act of following in a web application. Recall from Section 7.1.2 that the REST architecture involves resources that are created and destroyed. This leads us to ask two questions: When a user follows another user, what is being created? When a user unfollows another user, what is being destroyed? Upon reflection, we see that in these cases the application should either create or destroy a relationship between two users. A user then has many relationships and has many following (or followers) through these relationships.

Figure 14.5: The Home page with status feed and incremented following count.

Figure 14.6: A naïve implementation of user following.

There’s an additional detail we need to address regarding our application’s data model: unlike symmetric Facebook-style friendships, which are always reciprocal (at least at the data-model level), Twitter-style following relationships are potentially asymmetric—Calvin can follow Hobbes without Hobbes following Calvin. To distinguish between these two cases, we’ll adopt the terminology of active and passive relationships: if Calvin is following Hobbes but not vice versa, Calvin has an active relationship with Hobbes and Hobbes has a passive relationship with Calvin.³

3. Thanks to reader Paul Fioravanti for suggesting this terminology.

We’ll focus now on using active relationships to generate a list of followed users and consider the passive case in Section 14.1.5. Figure 14.6 suggests how to implement it: since each followed user is uniquely identified by followed_id, we could convert following to an active_relationships table, omit the user details, and use followed_id to retrieve the followed user from the users table. A diagram of the data model appears in Figure 14.7.

Because we’ll end up using the same database table for both active and passive relationships, we’ll use the generic term relationship for the table name, with a corresponding Relationship model. The result is the Relationship data model shown in Figure 14.8. We’ll see starting in Section 14.1.4 how to use the Relationship model to simulate both Active Relationship and Passive Relationship models.

Figure 14.7: A model of followed users through active relationships.

Figure 14.8: The Relationship data model.

To get started with the implementation, we first generate a migration corresponding to Figure 14.8:

Click here to view code image

$ rails generate model Relationship follower_id:integer followed_id:integer

Because we will be finding relationships by follower_id and by followed_id, we should add an index on each column for efficiency, as shown in Listing 14.1.

Listing 14.1: Adding indices for the relationships table.
db/migrate/[timestamp]_create_relationships.rb

Click here to view code image

class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

Listing 14.1 also includes a multiple-key index that enforces uniqueness on (follower_ id, followed_id) pairs, so that a user can’t follow another user more than once. (Compare to the email uniqueness index from Listing 6.29 and the multiple-key index in Listing 13.3.) As we’ll see starting in Section 14.1.4, our user interface won’t allow this to happen, but adding a unique index arranges to raise an error if a user tries to create duplicate relationships anyway (for example, by using a command-line tool such as curl).

To create the relationships table, we migrate the database as usual:

$ rails db:migrate

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. For the user with id equal to 1 from Figure 14.7, what would the value of user.following.map(&:id) be? (Recall the map(&:method_name) pattern from Section 4.3.2; user.following.map(&:id) just returns the array of ids.)

2. By referring again to Figure 14.7, determine the ids of user.following for the user with id equal to 2. What would the value of user.following.map(&:id) be for this user?

14.1.2 User/Relationship Associations

Before implementing user following and followers, we first need to establish the association between users and relationships. A user has_many relationships, and—since relationships involve two users—a relationship belongs_to both a follower and a followed user.

As with microposts in Section 13.1.3, we will create new relationships using the user association, with code such as

Click here to view code image

user.active_relationships.build(followed_id: ...)

At this point, you might expect application code as in Section 13.1.3, and it’s similar, but there are two key differences.

First, in the case of the user/micropost association we could write

Click here to view code image

class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end

This works because by convention Rails looks for a Micropost model corresponding to the :microposts symbol.⁴ In the present case, though, we want to write

4. Technically, Rails converts the argument of has_many to a class name using the classify method, which converts "foo_bars" to "FooBar".

Click here to view code image

has_many :active_relationships

even though the underlying model is called Relationship. We will thus have to tell Rails the model class name to look for.

Second, before we wrote

Click here to view code image

class Micropost < ApplicationRecord
  belongs_to :user
  .
  .
  .
end

in the Micropost model. This works because the microposts table has a user_id attribute to identify the user (Section 13.1.1). An id used in this manner to connect two database tables is known as a foreign key, and when the foreign key for a User model object is user_id, Rails infers the association automatically: by default, Rails expects a foreign key of the form <class>_id, where <class> is the lowercase version of the class name.⁵ In the present case, although we are still dealing with users, the user following another user is now identified with the foreign key follower_id, so we have to tell that to Rails.

5. Technically, Rails uses the underscore method to convert the class name to an id. For example, "FooBar".underscore is "foo_bar", so the foreign key for a FooBar object would be foo_bar_id.

The result of the above discussion is the user/relationship association shown in Listing 14.2 and Listing 14.3.

Listing 14.2: Implementing the active relationships has_many association.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: : destroy
  .
  .
  .
end

(Since destroying a user should also destroy that user’s relationships, we’ve added dependent: :destroy to the association.)

Listing 14.3: Adding the follower belongs_to association to the Relationship model.
app/models/relationship.rb

Click here to view code image

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

The followed association isn’t actually needed until Section 14.1.4, but the parallel follower/followed structure is clearer if we implement them both at the same time.

The relationships in Listing 14.2 and Listing 14.3 give rise to methods analogous to the ones we saw in Table 13.1, as shown in Table 14.1.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the create method from Table 14.1 in the console, create an active relationship for the first user in the database where the followed id is the second user.

2. Confirm that the values for active_relationship.followed and active_relationship.follower are correct.

Table 14.1: A summary of user/active relationship association methods.

14.1.3 Relationship Validations

Before moving on, we’ll add a couple of Relationship model validations for completeness. The tests (Listing 14.4) and application code (Listing 14.5) are straightforward. As with the generated user fixture from Listing 6.30, the generated relationship fixture also violates the uniqueness constraint imposed by the corresponding migration (Listing 14.1). The solution—removing the fixture contents as in Listing 6.31—is also the same, as seen in Listing 14.6.

Listing 14.4: Testing the Relationship model validations.
test/models/relationship_test.rb

Click here to view code image

require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase
  def setup
    @relationship = Relationship.new(follower_id: users(:michael).id,
                                     followed_id: users(:archer).id)

  end
  test "should be valid" do
    assert @relationship.valid?
  end

  test "should require a follower_id" do
    @relationship.follower_id = nil
    assert_not @relationship.valid?
  end
  test "should require a followed_id" do
    @relationship.followed_id = nil
    assert_not @relationship.valid?
  end
end

Listing 14.5: Adding the Relationship model validations.
app/models/relationship.rb

Click here to view code image

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

Listing 14.6: Removing the contents of the relationship fixture. GREEN
test/fixtures/relationships.yml

# empty

At this point, the tests should be GREEN:

Listing 14.7: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify by commenting out the validations in Listing 14.5 that the tests still pass. (This is a change as of Rails 5, and in previous versions of Rails the validations are required. We’ll plan to leave them in for completeness, but it’s worth bearing in mind that you may see these validations omitted in other people’s code.)

14.1.4 Followed Users

We come now to the heart of the Relationship associations: following and followers. Here we will use has_many :through for the first time: a user has many following through relationships, as illustrated in Figure 14.7. By default, in a has_many :through association Rails looks for a foreign key corresponding to the singular version of the association. In other words, with code like

Click here to view code image

has_many :followeds, through: :active_relationships

Rails would see “followeds” and use the singular “followed”, assembling a collection using the followed_id in the relationships table. But, as noted in Section 14.1.1, user.followeds is rather awkward, so we’ll write user.following instead. Naturally, Rails allows us to override the default, in this case using the source parameter (as shown in Listing 14.8), which explicitly tells Rails that the source of the following array is the set of followed ids.

Listing 14.8: Adding the User model following association.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy
  has_many :following, through: :active_relationships, source: :followed
  .
  .
  .
end

The association defined in Listing 14.8 leads to a powerful combination of Active Record and array-like behavior. For example, we can check if the followed users collection includes another user with the include? method (Section 4.3.1), or find objects through the association:

Click here to view code image

user.following.include?(other_user)
user.following.find(other_user)

Although in many contexts we can effectively treat following as an array, Rails is smart about how it handles things under the hood. For example, code like

Click here to view code image

following.include?(other_user)

looks like it might have to pull all the followed users out of the database to apply the include? method, but in fact for efficiency Rails arranges for the comparison to happen directly in the database. (Compare to the code in Section 13.2.1, where we saw that

user.microposts.count

performs the count directly in the database.)

To manipulate following relationships, we’ll introduce follow and unfollow utility methods so that we can write, e.g., user.follow(other_user). We’ll also add an associated following? boolean method to test if one user is following another.⁶

6. Once you have a lot of experience modeling a particular domain, you can often guess such utility methods in advance, and even when you can’t you’ll often find yourself writing them to make the tests cleaner. In this case, though, it’s OK if you wouldn’t have guessed them. Software development is usually an iterative process— you write code until it starts getting ugly, and then you refactor it—but for brevity the tutorial presentation is streamlined a bit.

This is exactly the kind of situation where I like to write some tests first. The reason is that we are quite far from writing a working web interface for following users, but it’s hard to proceed without some sort of client for the code we’re developing. In this case, it’s easy to write a short test for the User model, in which we use following? to make sure the user isn’t following the other user, use follow to follow another user, use following? to verify that the operation succeeded, and finally unfollow and verify that it worked. The result appears in Listing 14.9.

Listing 14.9: Tests for some “following” utility methods. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

By referring to the methods in Table 14.1, we can write the follow, unfollow, and following? methods using the association with following, as shown in Listing 14.10. (Note that we have omitted the user self variable whenever possible.)

Listing 14.10: Utility methods for following. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  def feed
    .
    .
    .
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # Unfollows a user.
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # Returns true if the current user is following the other user.
  def following?(other_user)
    following.include?(other_user)
  end

  private
  .
  .
  .
end

With the code in Listing 14.10, the tests should be GREEN:

Listing 14.11: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. At the console, replicate the steps shown in Listing 14.9.

2. What is the SQL for each of the commands in the previous exercise?

14.1.5 Followers

The final piece of the relationships puzzle is to add a user.followers method to go with user.following. You may have noticed from Figure 14.7 that all the information needed to extract an array of followers is already present in the relationships table (which we are treating as the active_relationships table via the code in Listing 14.2). Indeed, the technique is exactly the same as for followed users, with the roles of follower_id and followed_id reversed, and with passive_relationships in place of active_relationships. The data model then appears as in Figure 14.9.

Figure 14.9: A model for user followers through passive relationships.

The implementation of the data model in Figure 14.9 parallels Listing 14.8 exactly, as seen in Listing 14.12.

Listing 14.12: Implementing user.followers using passive relationships.
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
  .
  .
  .
end

It’s worth noting that we could actually omit the :source key for followers in Listing 14.12, using simply

Click here to view code image

has_many :followers, through: :passive_relationships

This is because, in the case of a :followers attribute, Rails will singularize “followers” and automatically look for the foreign key follower_id in this case. Listing 14.12 keeps the :source key to emphasize the parallel structure with the has_many :following association.

We can conveniently test the data model above using the followers.include? method, as shown in Listing 14.13. (Listing 14.13 might have used a followed_by? method to complement the following? method, but it turns out we won’t need it in our application.)

Listing 14.13: A test for followers. GREEN
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.followers.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

Listing 14.13 adds only one line to the test from Listing 14.9, but so many things have to go right to get it to pass that it’s a very sensitive test of the code in Listing 14.12.

At this point, the full test suite should be GREEN:

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. At the console, create several followers for the first user in the database (which you should call user). What is the value of user.followers.map(&:id)?

2. Confirm that user.followers.count matches the number of followers you created in the previous exercise.

3. What is the SQL used by user.followers.count? How is this different from user.followers.to_a.count? Hint: Suppose that the user had a million followers.

14.2 A Web Interface for Following Users

Section 14.1 placed some rather heavy demands on our data modeling skills, and it’s fine if it takes a while to soak in. In fact, one of the best ways to understand the associations is to use them in the web interface.

In the introduction to this chapter, we saw a preview of the page flow for user following. In this section, we will implement the basic interface and following/unfollowing functionality shown in those mockups. We will also make separate pages to show the user following and followers arrays. In Section 14.3, we’ll complete our sample application by adding the user’s status feed.

14.2.1 Sample Following Data

As in previous chapters, we will find it convenient to use rails db:seed to fill the database with sample relationships. This will allow us to design the look and feel of the web pages first, deferring the back-end functionality until later in this section.

Code to seed the following relationships appears in Listing 14.14. Here we somewhat arbitrarily arrange for the first user to follow users 3 through 51, and then have users 4 through 41 follow that user back. The resulting relationships will be sufficient for developing the application interface.

Listing 14.14: Adding following/follower relationships to the sample data.
db/seeds.rb

Click here to view code image

# Users
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin:    true,
             activated:  true,
             activated_at: Time.zone.now)

99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name: name,
               email: email,
               password:              password,
               password_confirmation: password,
               activated: true,
               activated_at: Time.zone.now)

end

# Microposts
users = User.order(:created_at).take(6)
50. times do
   content = Faker::Lorem.sentence(5)
   users.each { |user| user.microposts.create!(content: content) }
end

# Following relationships
users = User.all
user = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

To execute the code in Listing 14.14, we reset and reseed the database as usual:

Click here to view code image

$ rails db:migrate:reset
$ rails db:seed

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Using the console, confirm that User.first.followers.count matches the value expected from Listing 14.14.

2. Confirm that User.first.following.count is correct as well.

14.2.2 Stats and a Follow Form

Now that our sample users have both followed users and followers, we need to update the profile page and Home page to reflect this. We’ll start by making a partial to display the following and follower statistics on the profile and home pages. We’ll next add a follow/unfollow form, and then make dedicated pages for showing “following” (followed users) and “followers”.

As noted in Section 14.1.1, we’ll adopt Twitter’s convention of using “following” as a label for followed users, as in “50 following”. This usage is reflected in the mockup sequence starting in Figure 14.1 and shown in close-up in Figure 14.10.

The stats in Figure 14.10 consist of the number of users the current user is following and the number of followers, each of which should be a link to its respective dedicated display page. In Chapter 5, we stubbed out such links with the dummy text '#', but that was before we had much experience with routes. This time, although we’ll defer the actual pages to Section 14.2.3, we’ll make the routes now, as seen in Listing 14.15. This code uses the :member method inside a resources block, which we haven’t seen before, but see if you can guess what it does.

Listing 14.15: Adding following and followers actions to the Users controller.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup', to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

Figure 14.10: A mockup of the stats partial.

You might suspect that the URLs for following and followers will look like /users/1/following and /users/1/followers, and that is exactly what the code in Listing 14.15 arranges. Since both pages will be showing data, the proper HTTP verb is a GET request, so we use the get method to arrange for the URLs to respond appropriately. Meanwhile, the member method arranges for the routes to respond to URLs containing the user id. The other possibility, collection, works without the id, so that

Click here to view code image

resources :users do
  collection do
    get :tigers
  end
end

would respond to the URL /users/tigers (presumably to display all the tigers in our application).⁷

7. For more details on such routing options, see the Rails Guides article on “Rails Routing from the Outside In”.

A table of the routes generated by Listing 14.15 appears in Table 14.2. Note the named routes for the followed user and followers pages, which we’ll put to use shortly.

With the routes defined, we are now in a position to define the stats partial, which involves a couple of links inside a div, as shown in Listing 14.16.

Table 14.2: RESTful routes provided by the custom rules in resource in Listing 14.15.

Listing 14.16: A partial for displaying follower stats.
app/views/shared/_stats.html.erb

Click here to view code image

<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

Since we will be including the stats on both the user show pages and the home page, the first line of Listing 14.16 picks the right one using

<% @user ||= current_user %>

As discussed in Box 8.1, this does nothing when @user is not nil (as on a profile page), but when it is (as on the Home page) it sets @user to the current user. Note also that the following/follower counts are calculated through the associations using

@user.following.count

and

@user.followers.count

Compare these to the microposts count from Listing 13.24, where we wrote

@user.microposts.count

to count the microposts. As in that case, Rails calculates the count directly in the database for efficiency.

One final detail worth noting is the presence of CSS ids on some elements, as in

Click here to view code image

<strong id="following" class="stat">
...
</strong>

This is for the benefit of the Ajax implementation in Section 14.2.5, which accesses elements on the page using their unique ids.

With the partial in hand, including the stats on the Home page is easy, as shown in Listing 14.17.

Listing 14.17: Adding follower stats to the Home page.
app/views/static_pages/home.html.erb

Click here to view code image

<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

To style the stats, we’ll add some SCSS, as shown in Listing 14.18 (which contains all the stylesheet code needed in this chapter). The resulting Home page appears in Figure 14.11.

Figure 14.11: The Home page with follow stats.

Listing 14.18: SCSS for the Home page sidebar.
app/assets/stylesheets/custom.scss

Click here to view code image

.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}
.gravatar_edit {
  margin-top: 15px;
}
.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}
.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}
.users.follow {
  padding: 0;
}

/* forms */
.
.
.

We’ll render the stats partial on the profile page in a moment, but first let’s make a partial for the follow/unfollow button, as shown in Listing 14.19.

Listing 14.19: A partial for a follow/unfollow form.
app/views/users/_follow_form.html.erb

Click here to view code image

<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

This does nothing but defer the real work to follow and unfollow partials, which need new routes for the Relationships resource, which follows the Microposts resource example (Listing 13.30), as seen in Listing 14.20.

Listing 14.20: Adding the routes for user relationships.
config/routes.rb

Click here to view code image

Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup' => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout' => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end

The follow/unfollow partials themselves are shown in Listing 14.21 and Listing 14.22.

Listing 14.21: A form for following a user.
app/views/users/_follow.html.erb

Click here to view code image

<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

Listing 14.22: A form for unfollowing a user.
app/views/users/_unfollow.html.erb

Click here to view code image

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: method: :delete ) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

These two forms both use form_for to manipulate a Relationship model object; the main difference between the two is that Listing 14.21 builds a new relationship, whereas Listing 14.22 finds the existing relationship. Naturally, the former sends a POST request to the Relationships controller to create a relationship, while the latter sends a DELETE request to destroy a relationship. (We’ll write these actions in Section 14.2.4.) Finally, you’ll note that the follow form doesn’t have any content other than the button, but it still needs to send the followed_id to the controller. We accomplish this with the hidden_field_tag method in Listing 14.21, which produces HTML of the form

Click here to view code image

<input id="followed_id" name="followed_id" type="hidden" value="3" />

As we saw in Section 12.3 (Listing 12.14), the hidden input tag puts the relevant information on the page without displaying it in the browser.

We can now include the follow form and the following statistics on the user profile page simply by rendering the partials, as shown in Listing 14.23. Profiles with follow and unfollow buttons, respectively, appear in Figure 14.12 and Figure 14.13.

Figure 14.12: A user profile with a follow button (/users/2).

Listing 14.23: Adding the follow form and follower stats to the user profile page.
app/views/users/show.html.erb

Click here to view code image

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

We’ll get these buttons working soon enough—in fact, we’ll do it two ways, the standard way (Section 14.2.4) and using Ajax (Section 14.2.5)—but first we’ll finish the HTML interface by making the following and followers pages.

Figure 14.13: A user profile with an unfollow button (/users/5).

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify that /users/2 has a follow form and that /users/5 has an unfollow form. Is there a follow form on /users/1?

2. Confirm in the browser that the stats appear correctly on the Home page and on the profile page.

3. Write tests for the stats on the Home page. Hint: Add to the test in Listing 13.28. Why don’t we also have to test the stats on the profile page?

14.2.3 Following and Followers Pages

Pages to display followed users and followers will resemble a hybrid of the user profile page and the user index page (Section 10.3.1), with a sidebar of user information (including the following stats) and a list of users. In addition, we’ll include a raster of smaller user profile image links in the sidebar. Mockups matching these requirements appear in Figure 14.14 (following) and Figure 14.15 (followers).

Our first step is to get the following and followers links to work. We’ll follow Twitter’s lead and have both pages require user login. As with most previous examples of access control, we’ll write the tests first, as shown in Listing 14.24. Note that Listing 14.24 uses the named routes from Table 14.2.

Listing 14.24: Tests for the authorization of the following and followers pages. RED
test/controllers/users_controller_test.rb

Click here to view code image

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect following when not logged in" do
    get following_user_path(@user)
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get followers_user_path(@user)
    assert_redirected_to login_url
  end
end

The only tricky part of the implementation is realizing that we need to add two new actions to the Users controller. Based on the routes defined in Listing 14.15, we need to call them following and followers. Each action needs to set a title, find the user, retrieve either @user.following or @user.followers (in paginated form), and then render the page. The result appears in Listing 14.25.

Figure 14.14: A mockup of the user following page.

Figure 14.15: A mockup of the user followers page.

Listing 14.25: The following and followers actions. GREEN
app/controllers/users_controller.rb

Click here to view code image

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

As we’ve seen throughout this tutorial, the usual Rails convention is to implicitly render the template corresponding to an action, such as rendering show.html.erb at the end of the show action. In contrast, both actions in Listing 14.25 make an explicit call to render, in this case rendering a view called show_follow, which we must create. The reason for the common view is that the ERb is nearly identical for the two cases, and Listing 14.26 covers them both.

Listing 14.26: The show_follow view used to render following and followers. GREEN
app/views/users/show_follow.html.erb

Click here to view code image

<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

The actions in Listing 14.25 render the view from Listing 14.26 in two contexts, “following” and “followers”, with the results shown in Figure 14.16 and Figure 14.17. Note that nothing in the above code uses the current user, so the same links work for other users, as shown in Figure 14.18.

At this point, the tests in Listing 14.24 should be GREEN due to the before filter in Listing 14.25.

Figure 14.16: Showing the users the given user is following.

Figure 14.17: Showing the given user’s followers.

Listing 14.27: GREEN

$ rails test

To test the show_follow rendering, we’ll write a couple of short integration tests that verify the presence of working following and followers pages. They are designed to be a sanity check, not to be comprehensive. Indeed, as noted in Section 5.3.4, comprehensive tests of things like HTML structure are likely to be brittle and thus counterproductive. Our plan in the case of following/followers pages is to check the number is correctly displayed and that links with the right URLs appear on the page.

Figure 14.18: Showing a different user’s followers.

To get started, we’ll generate an integration test as usual:

Click here to view code image

$ rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

Next, we need to assemble some test data, which we can do by adding some relationships fixtures to create following/follower relationships. Recall from Section 13.2.3 that we can use code like

Click here to view code image

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

to associate a micropost with a given user. In particular, we can write

user: michael

instead of

user_id: 1

Applying this idea to the relationships fixtures gives the associations in Listing 14.28.

Listing 14.28: Relationships fixtures for use in following/follower tests.
test/fixtures/relationships.yml

Click here to view code image

one:
   follower: michael
   followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

The fixtures in Listing 14.28 first arrange for Michael to follow Lana and Malory, and then arrange for Michael to be followed by Lana and Archer. To test for the right count, we can use the same assert_match method we used in Listing 13.28 to test for the display of the number of microposts on the user profile page. Adding in assertions for the right links yields the tests shown in Listing 14.29.

Listing 14.29: Tests for following/follower pages. GREEN
test/integration/following_test.rb

Click here to view code image

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end

In Listing 14.29, note that we include the assertion

Click here to view code image

assert_not @user.following.empty?

which is included to make sure that

Click here to view code image

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

isn’t vacuously true (and similarly for followers). In other words, if @user. following.empty? were true, not a single assert_select would execute in the loop, leading the tests to pass and thereby give us a false sense of security.

The test suite should now be GREEN:

Listing 14.30: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Verify in a browser that /users/1/followers and /user/1/following work. Do the image links in the sidebar work as well?

2. Comment out the application code needed to turn the assert_select tests in Listing 14.29 RED to confirm they’re testing the right thing.

14.2.4 A Working Follow Button the Standard Way

Now that our views are in order, it’s time to get the follow/unfollow buttons working. Because following and unfollowing involve creating and destroying relationships, we need a Relationships controller, which we generate as usual

Click here to view code image

$ rails generate controller Relationships

As we’ll see in Listing 14.32, enforcing access control on the Relationships controller actions won’t much matter, but we’ll still follow our previous practice of enforcing the security model as early as possible. In particular, we’ll check that attempts to access actions in the Relationships controller require a logged-in user (and thus get redirected to the login page), while also not changing the Relationship count, as shown in Listing 14.31.

Listing 14.31: Basic access control tests for relationships. RED
test/controllers/relationships_controller_test.rb

Click here to view code image

require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

We can get the tests in Listing 14.31 to pass by adding the logged_in_user before filter (Listing 14.32).

Listing 14.32: Access control for relationships. GREEN
app/controllers/relationships_controller.rb

Click here to view code image

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

To get the follow and unfollow buttons to work, all we need to do is find the user associated with the followed_id in the corresponding form (i.e., Listing 14.21 or Listing 14.22), and then use the appropriate follow or unfollow method from Listing 14.10. The full implementation appears in Listing 14.33.

Listing 14.33: The Relationships controller.
app/controllers/relationships_controller.rb

Click here to view code image

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

We can see from Listing 14.33 why the security issue mentioned above is minor: if an unlogged-in user were to hit either action directly (e.g., using a command-line tool like curl), current_user would be nil, and in both cases the action’s second line would raise an exception, resulting in an error but no harm to the application or its data. It’s best not to rely on that, though, so we’ve taken the extra step and added an additional layer of security.

With that, the core follow/unfollow functionality is complete, and any user can follow or unfollow any other user, as you can verify by clicking the corresponding buttons in your browser. (We’ll write integration tests to verify this behavior in Section 14.2.6.) The result of following user #2 is shown in Figure 14.19 and Figure 14.20.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Follow and unfollow /users/2 through the web. Did it work?

2. According to the server log, which templates are rendered in each case?

Figure 14.19: An unfollowed user.

Figure 14.20: The result of following an unfollowed user.

14.2.5 A Working Follow Button with Ajax

Although our user following implementation is complete as it stands, we have one bit of polish left to add before starting work on the status feed. You may have noticed in Section 14.2.4 that both the create and destroy actions in the Relationships controller simply redirect back to the original profile. In other words, a user starts on another user’s profile page, follows the other user, and is immediately redirected back to the original page. It is reasonable to ask why the user needs to leave that page at all.

This is exactly the problem solved by Ajax, which allows web pages to send requests asynchronously to the server without leaving the page.⁸ Because adding Ajax to web forms is a common practice, Rails makes Ajax easy to implement. Indeed, updating the follow/unfollow form partials is trivial: just change

8. Because it is nominally an acronym for asynchronous JavaScript and XML, Ajax is sometimes misspelled “AJAX”, even though the original Ajax article spells it as “Ajax” throughout.

form_for

to

form_for ..., remote: true

and Rails automagically uses Ajax. The updated partials appear in Listing 14.34 and Listing 14.35.

Listing 14.34: A form for following a user using Ajax.
app/views/users/_follow.html.erb

Click here to view code image

<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

Listing 14.35: A form for unfollowing a user using Ajax.
app/views/users/_unfollow.html.erb

Click here to view code image

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: method: :delete ,
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

The actual HTML generated by this ERb isn’t particularly relevant, but you might be curious, so here’s a peek at a schematic view (details may differ):

Click here to view code image

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

This sets the variable data-remote="true" inside the form tag, which tells Rails to allow the form to be handled by JavaScript. By using a simple HTML property instead of inserting the full JavaScript code (as in previous versions of Rails), Rails follows the philosophy of unobtrusive JavaScript.

Having updated the form, we now need to arrange for the Relationships controller to respond to Ajax requests. We can do this using the respond_to method, responding appropriately depending on the type of request. The general pattern looks like this:

Click here to view code image

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

The syntax is potentially confusing, and it’s important to understand that in the code above only one of the lines gets executed. (In this sense, respond_to is more like an if-then-else statement than a series of sequential lines.) Adapting the Relationships controller to respond to Ajax involves adding respond_to as above to the create and destroy actions from Listing 14.33. The result appears as in Listing 14.36. Note the change from the local variable user to the instance variable @user; in Listing 14.33 there was no need for an instance variable, but now such a variable is necessary for use in Listing 14.34 and Listing 14.35.

Listing 14.36: Responding to Ajax requests in the Relationships controller.
app/controllers/relationships_controller.rb

Click here to view code image

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html redirect_to @user
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

The actions in Listing 14.36 degrade gracefully, which means that they work fine in browsers that have JavaScript disabled (although a small amount of configuration is necessary, as shown in Listing 14.37).

Listing 14.37: Configuration needed for graceful degradation of form submission.
config/application.rb

Click here to view code image

require File.expand_path('../boot', __FILE__ )
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # Include the authenticity token in remote forms.
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

On the other hand, we have yet to respond properly when JavaScript is enabled. In the case of an Ajax request, Rails automatically calls a JavaScript embedded Ruby (.js.erb) file with the same name as the action, i.e., create.js.erb or destroy.js.erb. As you might guess, such files allow us to mix JavaScript and embedded Ruby to perform actions on the current page. It is these files that we need to create and edit in order to update the user profile page upon being followed or unfollowed.

Inside a JS-ERb file, Rails automatically provides the jQuery JavaScript helpers to manipulate the page using the Document Object Model (DOM). The jQuery library (which we saw briefly in Section 13.4.2) provides a large number of methods for manipulating the DOM, but here we will need only two. First, we will need to know about the dollar-sign syntax to access a DOM element based on its unique CSS id. For example, to manipulate the follow_form element, we will use the syntax

$("#follow_form")

(Recall from Listing 14.19 that this is a div that wraps the form, not the form itself.) This syntax, inspired by CSS, uses the # symbol to indicate a CSS id. As you might guess, jQuery, like CSS, uses a dot . to manipulate CSS classes.

The second method we’ll need is html, which updates the HTML inside the relevant element with the contents of its argument. For example, to replace the entire follow form with the string "foobar", we would write

Click here to view code image

$("#follow_form").html("foobar")

Unlike plain JavaScript files, JS-ERb files also allow the use of embedded Ruby, which we apply in the create.js.erb file to update the follow form with the unfollow partial (which is what should show after a successful following) and update the follower count. The result is shown in Listing 14.38. This uses the escape_javascript method, which is needed to escape out the result when inserting HTML in a JavaScript file.

Listing 14.38: The JavaScript embedded Ruby to create a following relationship.
app/views/relationships/create.js.erb

Click here to view code image

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

Note the presence of line-ending semicolons, which are characteristic of languages with syntax descended from ALGOL.

The destroy.js.erb file is analogous (Listing 14.39).

Listing 14.39: The Ruby JavaScript (RJS) to destroy a following relationship.
app/views/relationships/destroy.js.erb

Click here to view code image

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

With that, you should navigate to a user profile page and verify that you can follow and unfollow without a page refresh.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Unfollow and refollow /users/2 through the web. Did it work?

2. According to the server log, which templates are rendered in each case?

14.2.6 Following Tests

Now that the follow buttons are working, we’ll write some simple tests to prevent regressions. To follow a user, we post to the relationships path and verify that the number of followed users increases by 1:

Click here to view code image

assert_difference '@user.following.count', 1 do
post relationships_path, params: followed_id: @other.id
end

This tests the standard implementation, but testing the Ajax version is almost exactly the same; the only difference is the addition of the option xhr: true:

Click here to view code image

assert_difference '@user.following.count', 1 do
post relationships_path, params: followed_id: @other.id , xhr: true
end

Here xhr stands for XmlHttpRequest; setting the xhr option to true issues an Ajax request in the test, which causes the respond_to block in Listing 14.36 to execute the proper JavaScript method.

The same parallel structure applies to deleting users, with delete instead of post. Here we check that the followed user count goes down by 1 and include the relationship and followed user’s id:

Click here to view code image

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship)
end

and

Click here to view code image

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship), xhr: true
end

Putting the two cases together gives the tests in Listing 14.40.

Listing 14.40: Tests for the follow and unfollow buttons. GREEN
test/integration/following_test.rb

Click here to view code image

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end
  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

At this point, the tests should be GREEN:

Listing 14.41: GREEN

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. By commenting and uncommenting each of the lines in the respond_to blocks (Listing 14.36), verify that the tests are testing the right things. Which test fails in each case?

2. What happens if you delete one of the occurrences of xhr: true in Listing 14.40? Explain why this is a problem, and why the procedure in the previous exercise would catch it.

14.3 The Status Feed

We come now to the pinnacle of our sample application: the status feed of microposts. Appropriately, this section contains some of the most advanced material in the entire tutorial. The full status feed builds on the proto-feed from Section 13.3.3 by assembling an array of the microposts from the users being followed by the current user, along with the current user’s own microposts. Throughout this section, we’ll proceed through a series of feed implementations of increasing sophistication. To accomplish this, we will need some fairly advanced Rails, Ruby, and even SQL programming techniques.

Because of the heavy lifting ahead, it’s especially important to review where we’re going. A recap of the final status feed, shown in Figure 14.5, appears again in Figure 14.21.

Figure 14.21: A mockup of a user’s Home page with a status feed.

Figure 14.22: The feed for a user (id 1) following users with ids 2, 7, 8, and 10.

14.3.1 Motivation and Strategy

The basic idea behind the feed is simple. Figure 14.22 shows a sample microposts database table and the resulting feed. The purpose of a feed is to pull out the microposts whose user ids correspond to the users being followed by the current user (and the current user itself), as indicated by the arrows in the diagram.

Although we don’t yet know how to implement the feed, the tests are relatively straightforward, so (following the guidelines in Box 3.3) we’ll write them first. The key is to check all three requirements for the feed: microposts for both followed users and the user itself should be included in the feed, but a post from an unfollowed user should not be included. Based on the fixtures in Listing 10.47 and Listing 13.53, this means that Michael should see Lana’s posts and his own posts, but not Archer’s posts. Converting these requirements to assertions and recalling that the feed is in the User model (Listing 13.46) gives the updated User model test shown in Listing 14.42.

Listing 14.42: A test for the status feed. RED
test/models/user_test.rb

Click here to view code image

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "feed should have the right posts" do
    michael = users(:michael)
    archer = users(:archer)
    lana    = users(:lana)
    # Posts from followed user
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # Posts from self
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # Posts from unfollowed user
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end
end

Of course, the current implementation is just a proto-feed, so the new test is initially RED:

Listing 14.43: RED

$ rails test

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Assuming the micropost’s ids are numbered sequentially, with larger numbers being more recent, what would user.feed.map(&:id) return for the feed shown in Figure 14.22? Hint: Recall the default scope from Section 13.1.4.

14.3.2 A First Feed Implementation

With the status feed design requirements captured in the test from Listing 14.42, we’re ready to start writing the feed. Since the final feed implementation is rather intricate, we’ll build up to it by introducing one piece at a time. The first step is to think of the kind of query we’ll need. We need to select all the microposts from the microposts table with ids corresponding to the users being followed by a given user (or the user itself). We might write this schematically as follows:

Click here to view code image

SELECT * FROM microposts
WHERE user_id IN (<list of ids >) OR user_id = <user id>

In writing this code, we’ve guessed that SQL supports an IN keyword that allows us to test for set inclusion. (Happily, it does.)

Recall from the proto-feed in Section 13.3.3 that Active Record uses the where method to accomplish the kind of select shown above, as illustrated in Listing 13.46. There, our select was very simple; we just picked out all the microposts with user id corresponding to the current user:

Click here to view code image

Micropost.where("user_id = ?", id)

Here, we expect it to be more complicated, something like this:

Click here to view code image

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

We see from these conditions that we’ll need an array of ids corresponding to the users being followed. One way to do this is to use Ruby’s map method, available on any “enumerable” object, i.e., any object (such as an Array or a Hash) that consists of a collection of elements.⁹ We saw an example of this method in Section 4.3.2; as another example, we’ll use map to convert an array of integers to an array of strings:

9. The main requirement is that enumerable objects must implement an each method to iterate through the collection.

Click here to view code image

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Situations like the one illustrated above, where the same method gets called on each element in the collection, are common enough that there’s a shorthand notation for it (seen briefly in Section 4.3.2) that uses an ampersand & and a symbol corresponding to the method:

Click here to view code image

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

Using the join method (Section 4.3.1), we can create a string composed of the ids by joining them on comma-space :

Click here to view code image

>> [1, 2, 3, 4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

We can use the above method to construct the necessary array of followed user ids by calling id on each element in user.following. For example, for the first user in the database this array appears as follows:

Click here to view code image

>> User.first.following.map(&:id)
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

In fact, because this sort of construction is so useful, Active Record provides it by default:

Click here to view code image

>> User.first.following_ids
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

Here the following_ids method is synthesized by Active Record based on the has_many :following association (Listing 14.8); the result is that we need only append _ids to the association name to get the ids corresponding to the user.following collection. A string of followed user ids then appears as follows:

Click here to view code image

>> User.first.following_ids.join(', ')
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

When inserting into an SQL string, though, you don’t need to do this; the ?

interpolation takes care of it for you (and in fact eliminates some database-dependent incompatibilities). This means we can use following_ids by itself. As a result, the initial guess of

Click here to view code image

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

actually works! The result appears in Listing 14.44.

Listing 14.44: The initial working feed. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns true if a password reset has expired.
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  # Returns a user's status feed.
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end
  .
  .
  .
end

The test suite should be GREEN:

Listing 14.45: GREEN

$ rails test

In some applications, this initial implementation might be good enough for most practical purposes, but Listing 14.44 isn’t the final implementation; see if you can make a guess about why not before moving on to the next section. (Hint: What if a user is following 5000 other users?)

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. In Listing 14.44, remove the part of the query that finds the user’s own posts. Which test in Listing 14.42 breaks?

2. In Listing 14.44, remove the part of the query that finds the followed users’ posts. Which test in Listing 14.42 breaks?

3. How could you change the query in Listing 14.44 to have the feed erroneously return microposts of unfollowed users, thereby breaking the third test in Listing 14.42? Hint: Returning all the microposts would do the trick.

14.3.3 Subselects

As hinted at in the last section, the feed implementation in Section 14.3.2 doesn’t scale well when the number of microposts in the feed is large, as would likely happen if a user were following, say, 5000 other users. In this section, we’ll reimplement the status feed in a way that scales better with the number of followed users.

The problem with the code in Section 14.3.2 is that following_ids pulls all the followed users’ ids into memory, and creates an array the full length of the followed users array. Since the condition in Listing 14.44 actually just checks inclusion in a set, there must be a more efficient way to do this, and indeed SQL is optimized for just such set operations. The solution involves pushing the finding of followed user ids into the database using a subselect.

We’ll start by refactoring the feed with the slightly modified code in Listing 14.46.

Listing 14.46: Using key-value pairs in the feed’s where method. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns a user's status feed.
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                       following_ids: following_ids, user_id: id)
  end
  .
  .
  .
end

As preparation for the next step, we have replaced

Click here to view code image

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

with the equivalent

Click here to view code image

Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
               following_ids: following_ids, user_id: id)

The question mark syntax is fine, but when we want the same variable inserted in more than one place, the second syntax is more convenient.

The above discussion implies that we will be adding a second occurrence of user_id in the SQL query. In particular, we can replace the Ruby code

following_ids

with the SQL snippet

Click here to view code image

following_ids = "SELECT followed_id FROM relationships
                 WHERE follower_id = :user_id"

This code contains an SQL subselect, and internally the entire select for user 1 would look something like this:

Click here to view code image

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE  follower_id = 1)
      OR user_id = 1

This subselect arranges for all the set logic to be pushed into the database, which is more efficient.

With this foundation, we are ready for a more efficient feed implementation, as seen in Listing 14.47. Note that because it is now raw SQL, the following_ids string is interpolated, not escaped.

Listing 14.47: The final implementation of the feed. GREEN
app/models/user.rb

Click here to view code image

class User < ApplicationRecord
  .
  .
  .
  # Returns a user's status feed.
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end
  .
  .
  .
end

This code has a formidable combination of Rails, Ruby, and SQL, but it does the job and does it well:

Listing 14.48: GREEN

$ rails test

Of course, even the subselect won’t scale forever. For bigger sites, you would probably need to generate the feed asynchronously using a background job, but such scaling subtleties are beyond the scope of this tutorial.

With the code in Listing 14.47, our status feed is now complete. Recall from Section 13.3.3 that the Home page already includes the feed. In Chapter 13, the result was only a proto-feed (Figure 13.14), but with the implementation in Listing 14.47 as seen in Figure 14.23 the Home page now shows the full feed.

Figure 14.23: The Home page with a working status feed.

At this point, we’re ready to merge our changes into the master branch:

Click here to view code image

$ rails test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

We can then push the code to the remote repository and deploy the application to production:

Click here to view code image

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

The result is a working status feed on the live web (Figure 14.24).

Figure 14.24: A working status feed on the live web.

Exercises

Solutions to exercises are available for free at railstutorial.org/solutions with any Rails Tutorial purchase. To see other people’s answers and to record your own, join the Learn Enough Society at learnenough.com/society.

1. Write an integration test to verify that the first page of the feed appears on the Home page as required. A template appears in Listing 14.49.

2. Note that Listing 14.49 escapes the expected HTML using CGI.escapeHTML (which is closely related to the CGI.escape method we used in Section 11.2.3 to escape URLs). Why is escaping the HTML necessary in this case? Hint: Try removing the escaping and carefully inspect the page source for the micropost content that doesn’t match. Using the search feature of your terminal shell (Cmd-F on Ctrl-F on most systems) to find the word “sorry” may prove particularly helpful.

Listing 14.49: Testing the feed HTML. GREEN
test/integration/following_test.rb

Click here to view code image

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end
  .
  .
  .
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(FILL_IN), FILL_IN
    end
  end
end

14.4 Conclusion

With the addition of the status feed, we’ve finished the sample application for the Ruby on Rails™ Tutorial. This application includes examples of all the major features of Rails, including models, views, controllers, templates, partials, filters, validations, callbacks, has_many/belongs_to and has_many :through associations, security, testing, and deployment.

Despite this impressive list, there is still much to learn about web development. As a first step in this process, this section contains some suggestions for further learning.

14.4.1 Guide to Further Resources

There is a wealth of Rails resources in stores and on the web—indeed, the supply is so rich that it can be overwhelming. The good news is that, having gotten this far, you’re ready for almost anything else out there. Here are some suggestions for further learning:

• The Learn Enough Society: Premium subscription service that includes a special enhanced version of the Ruby on Rails™ Tutorial book and 15+ hours of streaming screencast lessons filled with the kind of tips, tricks, and live demos that you can’t get from reading a book. Also includes text and videos for the other Learn Enough tutorials. Scholarship discounts are available.

• Launch School: Lots of in-person developer bootcamps have sprung up in recent years, and I recommend looking for one in your area, but Launch School is available online and so can be taken from anywhere. Launch School is an especially good choice if you want instructor feedback within the context of a structured curriculum.

• The Turing School of Software & Design: A full-time, 27-week Ruby/Rails/JavaScript training program in Denver, Colorado. Most of their students start with limited programming experience but have the determination and drive needed to pick it up quickly. Turing guarantees its students will find a job after graduating or they’ll refund the cost of tuition.

• Bloc: An online bootcamp with a structured curriculum, personalized mentorship, and a focus on learning through concrete projects. Use the coupon code BLOCLOVESHARTL to get $500 off the enrollment fee.

• Firehose Project: A mentor-driven, online coding bootcamp focused on real-world programming skills like test-driven development, algorithms, and building an advanced web application as part of an agile team. Two-week free intro course.

• Thinkful: An online class that pairs you with a professional engineer as you work through a project-based curriculum. Subjects include Ruby on Rails, front-end development, web design, and data science.

• Pragmatic Studio: Online Ruby and Rails courses from Mike and Nicole Clark.

• RailsApps: Instructive sample Rails apps

• Code School: A large variety of interactive programming courses

14.4.2 What We Learned in This Chapter

• Rails’ has_many :through allows the modeling of complicated data relationships.

• The has_many method takes several optional arguments, including the object class name and the foreign key.

• Using has_many and has_many :through with properly chosen class names and foreign keys, we can model both active (following) and passive (being followed) relationships.

• Rails routing supports nested routes.

• The where method is a flexible and powerful way to create database queries.

• Rails supports issuing lower-level SQL queries if needed.

• By putting together everything we’ve learned in this book, we’ve successfully implemented user following with a status feed of microposts from followed users.

Index

Note: Page numbers in italics indicate figures, those with t indicate tables, and those with n indicate footnotes.

Symbols

/ (slash)

root URL, 60

Unix directory separator, 8

== (equal signs)

assignment operator, 423-424, 438-439

equality operator, 164

\ (backslash)

as escape character, 155

as literal character, 155

" " (double quotes)

defining strings, 155

exchanging for single quotes, 155

mixing with single quotes, 136

: (colon), as a hashkey, 171-172

!! (exclamation points), coercing Boolean values, 158, 295

!= (exclamation point, equal sign), not equal operator, 164

' (single quotes), mixing with double quotes, 136

[ ] (square brackets), accessing elements, 164

@ (at sign), instance variables, 72, 224

/* ... */ (slash, asterisk), CSS comments, 205

&& (ampersands), and operator, 157, 376-377

+ (plus operator), concatenating strings, 153

<<(less-than signs), adding to arrays, 165

||= (vertical bars, equal sign), or equals

assignment operator, 387-388

! (exclamation point)

bang methods, 165

not operator, 157

? (question mark)

boolean indicator, 156

in query parameters, 528

( ) (parentheses)

in statement syntax, 424 n

and the ternary operator, 437 n14

{ } (curly braces)

block indicator, 167

hash indicator, 171

# (hash symbol)

comment indicator, 21, 152

styling CSS IDs, 726

styling CSS IDs (Listing 5.9), 208

URL stub, 198

# (pound sign). See # (hash symbol).

=> (equal, greater than), hashrocket, 171

||(vertical bars)

block variable indicator, 167

or operator, 157

$ (dollar sign)

command line prompt, 8

dollar-sign syntax, 726

Sass variables, 224

10 types of people, 438-439

A

-a flag, 43

About page

creating, 115-118, 118

mockup, 235

sample page, 118

testing, 114

Abstraction layers, 252 n

Acceptance tests, 463

Account activation

edit action (Listings 11.31 to 11.32), 545-549

generalized authenticated? method (Listings 11.26 to 1.29), 543-545

metaprogramming, 541

overview, 515-516, 540

preventing unactivated users from logging in (Listing 11.32), 545-549

versus remembering, login, and password reset, 516 t

sign-up page with warning message, 548

successfully activated profile page, 547

Account activation mailer method

account activation HTML view (Listing 11.14), 529

account activation text view (Listing 11.11), 529

Action Mailer library, 525

adding account activation to user sign-up (Listing 11.23), 536-537

assert_match method (Listings 11.19 to 11.20), 534-535

commenting out failing tests (Listing 11.24), 537

creating an activation email (Listings 11.6 to 11.14), 525-529

email, sent in production, 558

email previews (Listings 11.16 to 11.18), 530-533

escaping out special characters, 528

generating a mailer (Listings 11.6 to 11.14), 525-529

Home page with activation message, 538

mailing the account activation link (Listing 11.12), 527

query parameters, 528

sample activation email (Listing 11.25), 538-540

setting the test domain host (Listing 11.21), 535

User mailer test (Listings 11.19 to 11.21), 533-536

User mailer test, screen mockups, 532-533

Account activation resource

activating fixture users (Listing 11.5), 523-524

activating seed users (Listing 11.4), 523

activation tokens, 516, 520-522

adding a resource (Listing 11.1), 518

method reference technique, 521

migration for account activation (Listing 11.2), 519-520

overview, 516-524

User model with account activation attributes, 520

Account activation test

activation via user model object (Listing 11.37), 552-553

integration test (Listings 11.33 to 11.37), 549-553

overview, 549

sending email via user model object (Listing 11.36), 552

user activation methods in the User model (Listing 11.35), 551-552

user sign-up test (Listing 11.33), 550-551

Action Mailer library, 525

Actions, corresponding views (Listings 3.9, 3.10), 107

Activation tokens, 516, 520-522

Active Record. See also SQL (Structured Query Language); Validating user data.

definition, 248

finding User objects, 260-261

initializing objects, 257

making and saving a model, 257-259

uniqueness, at the database level (Listings 6.29 to 6.34), 281-286

uniqueness, validating (Listings 6.24 to 6.34), 278-286

validating users, 263-264. See also Validating user data.

validity, 257

Active Record::RecordNotFound exception, 260

Active relationships, 685, 685

add_column method (Listing 6.35), 288

add_index method, 282

Adler, Yoel, 480 n

admin attribute (Listings 10.54 to 10.56), 502-505

admin? boolean method (Listings 10.54 to 10.55), 502-503

Administrative users

admin attribute (Listings 10.54 to 10.56), 502-505

admin? boolean method (Listings 10.54 to 10.55), 502-503

deleting (Listings 10.54 to 10.55), 502-503, 501

identifying (Listings 10.54 to 10.55), 502-503

Ajax (Listings 14.34 to 14.39), 723-727

Ampersands (&&), and operator, 157, 376-377

Anonymous functions, 614

any? method, 338

app/assets directory, 218

ApplicationController class (Listing 2.22), 85-87, 86, 87

application.html.erb file, 128-129

application.html.erb file (Listings 4.3, 5.1), 148, 195-196

Applications

getting started. See Hello world program.

interacting with. See Console.

renaming, 50

samples. See Hello world program; Static pages; Toy application.

Architectural Styles and the Design of Network-based Software Architecture (Fielding), 70 n

Arrays. See also Ranges.

! (exclamation point), bang methods, 165

!= (exclamation point, equal sign), not equal operator, 164

[ ] (square brackets), accessing elements, 164

== (equal signs), equality operator, 164

adding to, 165

associative, 170

bang methods, 165

classes, 177

converting ranges to, 166

converting strings to, 163

converting to strings, 165

versus hashes, 170-172

indexing, 163

mutating, 165

pulling out elements, 166

selecting every element, 166

of strings, 272

zero-offset, 163

assert method (Listings 6.4 to 6.5), 265

assert_match method (Listings 11.19 to 11.20), 534-535

assert_select method

examples of (Listing 5.33), 238 t

sign-up failure, 342

testing for HTML tags (Listing 3.24), 120

Asset directories, 218

Asset pipeline

asset directories, 218

definition, 218

manifest files (Listing 5.19), 218-219

preprocessor engines, 219-220

production efficiency, 220

Associations between Micropost models (Listings 13.10 to 13.12), 608-611, 609 t

Associative arrays. See Hashes.

At sign (@), instance variables, 72, 224

attr_accessible method, 333

attr.accessor method (Listing 4.17), 250

Attribute accessors, creating, 188

authenticate method

authenticating passwords, 293-296

authenticating users (Listing 8.7), 376

examples (Listing 8.7), 377 t

authenticated? method (Listings 11.26 to 1.29), 543-545

Authenticating users (Listing 8.7), 376, 377 t

Authentication

versus authorization, 467

passwords, 293-296

Authentication systems, rolling your own, 247-248

Authenticity token, sign-up forms, 328

Authorization, friendly forwarding

create action (Listing 10.32), 482

implementation code (Listing 10.30), 480

logged_in_user before filter (Listing 10.31), 480-481

overview, 478-483

redirect_back_or method (Listing 10.32), 481-482

testing for (Listings 10.29 to 10.32), 478-483

Authorization, requiring logged-in users

adding a logged_in_user method (Listings 10.15), 468-469

before_action command (Listing 10.15), 467-469

before filters (Listing 10.15), 467-469

logged_in_user before filter (Listings 10.15), 468-469

logging in a test user (Listing 10.15), 468-471

login form mockup, a protected page, 468

login form mockup, access failure, 468

overview, 467-473

testing edits (Listings 10.20 to 10.21), 471-473

Authorization, requiring the right user

adding a user to the fixture file (Listings 10.23 to 10.28), 473-478

correct_user before filter (Listings 10.27 to 10.28), 476-478

current_user? method (Listings 10.27 to 10.28), 476-478

overview, 473-478

testing a correct user (Listings 10.27 to 10.28), 476-478

testing the wrong user (Listing 10.24), 474

Authorization versus authentication, 467

Automated testing

getting started, 112

Green results, 114-117

Guard program, 137-143

overview, 110-111

Red, Green, Refactor, 113-118

Red results, 113-114

refactoring, 117-118

timing tests, 110-111

aux options, 142-143

Avatar, definition, 314 n

B

Backing up code, 37

Backslash (\)

as escape character, 155

as literal character, 155

Bang bang (!!), coercing Boolean values, 158, 295

Bang methods, 165

bar, word origin, 153 n

Base titles, 147

Bash command-line interface, 14

bcrypt hash function (Listing 6.36), 288-289, 294 n22

Before filters (Listing 10.15), 467-473

before_action command (Listing 10.15), 467-473

before_filter command, 467 n

belongs_to associations (Listings 11.9, 13.10, 14.3), 608-611, 689

Bitbucket (Listing 1.12)

overview, 36-40, 39

pushing changes up to, 39

Blank? method, 184

Bloc, 6

Block variables, definition, 167

Blocks

as closures, 168 n

definition, 167

map method, 168

multiline, 168

overview, 167-170

Books and publications. See also Online resources.

Architectural Styles and the Design of Network-based Software Architecture (Fielding), 70 n

“Class and Instance Variables in Ruby,” 182 n

Learn Enough Command Line to be Dangerous, 1

Learn Enough Git to be Dangerous, 32

Learn Enough Text Editor to be Dangerous, 41 n

Patterns of Enterprise Application Architecture (Fowler), 248 n1

prerequisites for Rails, 5-6

Pro Git, 40 n

“The Rails 3 Asset Pipeline in (about) 5 minutes” (Erasmus), 217 n

“Rails Routing from the Outside In,” 701 n

Ruby on Rails Tutorial website, 2 n

Boolean values

combining, 157

in control flow, 156

identifying, 156

Bootstrap, drop-down menus (Listing 8.20), 393-394

Bootstrap and custom CSS. See also Site layout.

adding CSS (Listings 5.6 to 5.9), 205-210

adding the bootstrap.sass gem (Listing 5.5), 204

color (Listing 5.9), 210

Home page mockup, 206, 208, 211

installing Bootstrap, 204

overview, 202-204

site logo (Listing 5.9), 210

typography (Listing 5.8), 208, 208-210

bootstrap.sass gem (Listing 5.5), 204

bootstrap-will_paginate method (Listings 10.44 to 10.46), 492-494

box_sizing, 305-306

Branches. See also Git, branching.

current branch, identifying, 42

definition, 41

deleting, 44

editing, 43

listing, 42

merging, 43-44

pushing changes up to the Bitbucket, 39

status, displaying, 43

Built-in classes

adding methods to, 417-418

modifying, 183-184

Built-in functions, 146

bundle command, 23 n, 99

bundle exec, 60

Bundler, 18-23

Buttons, web page

follow buttons, 682, 708

follow buttons (Listing 14.40), 727-729

linking to (Listing 5.45), 242-244

unfollow buttons, 681-682, 709

unfollow buttons (Listing 14.40), 727-729

byebug gem (Listing 3.2), 312

C

Callbacks, databases, 283

CamelCase, 101

CarrierWave uploader

adding to the Gemfile (Listing 13.58), 657-659

configuring for production (Listings 13.69 to 13.71), 671-674

data model for microposts, 659

description, 657-663

MiniMagick interface (Listing 13.67), 669-670

uploading images (Listings 13.59 to 13.62), 659-663

uploading images in production (Listings 13.69 to 13.71), 671-674

validating images (Listings 13.64 to 13.66), 664-667

Cascading Style Sheets (CSS). See CSS (Cascading Style Sheets).

Case sensitivity

downcasing email addresses (Listing 6.32), 283

email addresses (Listings 6.26 to 6.27), 279-280

indices (Listings 6.32 to 6.34), 283-286

SQL, 614 n

cat command (Listing 1.11), 37-38

cd (change directory) command, 14-15 t

Célio, Paulo Júnior, 428 n

Change directory (cd) command, 14

change method (Listing 6.2), 252

Changes, overwriting, 36

checkout (co) command, 36

Clark, Mike, 7

Clark, Nicole, 7

“Class and Instance Variables in Ruby,” 182 n

Class methods, 179

Class names, converting to IDs, 688 n

Classes. See also CSS classes.

arrays, 178

attribute accessors, creating, 188

built-in, adding methods to, 417-418

built-in, modifying, 183-184

checking for palindromes, 180-183

class methods, 179

constructors, 178-179

defining (Listing 4.15), 181-183

definition, 106

description, 177

getter methods, creating, 188

hashes, 178

hierarchy, determining, 179-180

inheritance, 179-183, 182

instance method, 179

instance variables, creating, 188

instances of, 179

literal constructors, 178

named constructors, 178

new method, 178

self keyword, 182-183

setter methods, creating, 188

Static Pages controller, 185-187

superclass method, 179-180

User class (Listing 4.17), 187-190

Client computer, definition, 104

Closures, 168 n

Cloud integrated development environment. See Cloud9.

Cloud storage

for micropost images, 671-674

S3 (Simple Storage Service), 672-674

Cloud9. See also Installing Rails on Cloud9; Local development environment.

creating a new workspace, 11, 12

Find in Files global search, 10

getting started, 11-12

local servers, sharing, 26

Rails Tutorial Cloud9 workspace, 10

setting code indentation, 12, 12

signing up, 11

co (checkout) command, 36

Code generation, undoing, 102-103

Code School, 6

coffee file extension, 219-220

Collisions, checking for, 412 n6

Colon (:), as a hashkey, 171-172

Color (Listing 5.9), 210

Columns, in databases, 250

Commands. See also specific commands

Heroku, 50-51

Unix, 14-15 t

Commenting out failing tests (Listing 11.24), 537

Comments

/* ... */ (slash, asterisk), CSS comments, 205

# (hash symbol), comment indicator, 21

# (pound sign), comment indicator, 152

in the console, 152

commit command, 35, 43

Commit messages, displaying, 35

Committing changes, 35, 43

Concerns technique, 382 n

config directory, listing contents of, 103

Confirmation of passwords, 293-296

Confirmation of passwords (Listings 6.37 to 6.40), 289-291

Console

˜irb configuration (Listing 4.8), 151

comments, 152

definition, 150

environments, 151, 302-306

exiting, 151

message chaining, 157

message passing, 156-159

method definitions, 159-161

in Micropost resource, 82

objects, 156-159

prompts, 82 n

starting, 150-151

strings, 153-155

toy application, 82

console command, 82

Constructors, 178-179

Contact page

adding actions (Listing 5.24), 229-230

adding routes (Listing 5.23), 229

integration test for layout links (Listing 5.32), 236-238

named routes (Listings 5.27 to 5.28), 230-231

root routes (Listing 5.23), 228-230

testing (Listing 5.21), 228-230

view for (Listing 5.25), 230

content_for function, 126 n14

Controller class. See Static Pages controller.

Controller tests. See Testing, Static Pages controller.

Controllers, generating (Listing 3.6), 100-107. See also Sessions controller; Static Pages controller.

Conventions in this book, 8-9

Converting

arrays to strings, 165

ranges to arrays, 166

strings from objects, 157

strings to arrays, 163

Cookies

decrypting, 419

encrypting, 418

expiration, 417-425

inspecting, 425 n

permanent, 417-425

remembering user login, 410

session hijacking, 410, 412 n5

signed, 418

stealing, 410

storing userid in, 418-419

cookies method, 366

cookies.permanent method, 417-425

Coordinated Universal Time (UTC), 258 n7

correct_user before filter (Listings 10.27 to 10.28), 476-478

count method

showing microposts, 620

sign-up failure, 341

Counter cache, showing microposts, 620

cp (copy files) command, 14-15 t

create action

creating microposts (Listing 13.36), 635-636

friendly forwarding (Listing 10.32), 482

password reset (Listings 12.5 to 12.6), 571-573

sign-up failure (listing 7.18), 329-332

sign-up success (Listing 7.28), 346-349

strong parameters (Listing 7.19), 334

view template (Listing 7.28), 346-348, 347

create command, 48

Create directory (mkdir) command, 14

create method, creating users, 259, 293-296

create_table method (Listing 6.2), 252

Creating

About page, 115-118, 118

an activation email (Listings 11.6 to 11.14), 525-529

attribute accessors, 188

database indices, 281

fixtures (Listings 8.21 to 8.22), 397-398

following relationships (Listings 14.38), 727

getter methods, 188

Help pages, 100

instance variables, 188

microposts. See Microposts, creating.

persistent sessions, 410

remember me checkbox (Listings 9.21 to 9.23), 433-438

sample users (Listings 10.42 to 10.43), 489-491, 490-491, 491

Sessions controller, 366-369

Creating (continued)

setter methods, 188

Static Pages controller class (Listing 3.20), 185-187

temporary sessions (Listing 8.13), 382-383

User objects, 256-260

user profile pages, 307-311

users, 62-66, 62-63, 259, 293-296

Creating, microposts

adding to the Home page (Listing 13.37), 636-637

create action (Listing 13.36), 635-636

creating (Listings 13.36 to 13.45), 635-642

form for creating microposts (Listing 13.37), 635

redefining error messages (Listings 13.41 to 13.45), 639-642

user info sidebar (Listing 13.38), 637

CSRF (cross-site request forgery), 328

CSS (Cascading Style Sheets). See also Site layout.

{ } (curly braces), hash indicator, 176

including style sheets (Listing 4.1), 175-177

rules, bundling and reusing. See Sass mixins (Listing 7.2).

Sass mixins (Listing 7.2), 304-306

stylesheet_link_tag function, 146, 177

styling error messages with (Listing 7.22), 339-340

tool for. See Sass utility.

CSS classes, 197-202

.css file extension, 205

Ctrl-C keystroke

case sensitivity, 24

restarting the console, 151

shutting down a server, 24

Ctrl-D keystroke

case sensitivity, 82

exiting Guard, 143

exiting the console, 82

curl utility, 200-201

Curly braces ({})

block indicator, 168

hash indicator, 171, 176

Current user, finding (Listings 8.16 to 8.17), 389-390

current_user, updating (Listing 9.9), 424-425

current_user method (Listings 8.16 to 8.17), 389-390

current_user? method (Listings 10.27 to 10.28), 476-478

Custom environments, 302 n

D

-d flag, 44

-D flag, 44

Data models

associations (Listings 2.14, 2.15), 81-82, 82

toy application, 57-59, 57-59, 72

Data structures. See also CSS (Cascading Style Sheets).

arrays, 163-167

blocks, 167-170

hashes, 170-172

ranges, 163-167

symbols, 172-175

Data validation, toy application, 74. See also Validating user data.

Database indices, creating, 281

Database migration. See also Rake db:migrate command.

definition, 251

dropping tables, 254

magic columns, 252, 258

migrating down, 254

migrating the User model (Listing 6.2), 251-254

migrating up, 252

migration file name, 252

reversing. See Undoing, database migration.

rolling back. See Undoing, database migration.

in a sandbox, 256-257

timestamp, 252

undoing, 102-103, 254, 256, 294 n21

Databases. See also User model.

callbacks, 283

case sensitive indices (Listings 6.32 to 6.34), 283-286

columns, 250

full-table scan, 281

interacting with. See Active Record; SQL (Structured Query Language).

method reference convention, 283

resetting, 294 n21. See also Undoing, database migration.

reversing changes. See Undoing, database migration.

rolling back changes. See Undoing, database migration.

rows, 250

tables, 250

Date and time, User objects, 258

db/development.sqlite3 file, 253

db:rollback command, 254

db/seeds.rb file (Listing 10.43), 490

de Ron, Niels, 428 n

Debug, and Rails environments (Listing 7.1), 301-306

Debug box, sample code (Listing 7.21), 304

Debug information, sign-up failure (Listing 7.18), 330-333

debug method (Listing 7.1), 302

debugger

byebug gem (Listing 3.2), 312

use for, 312-313

with Users controller (Listing 7.6), 312

Decrypting cookies, 419-420

Default scope for Micropost models (Listings 13.14 to 13.17), 612-615

default_scope method (Listing 13.17), 614

DELETE method, 104-105

DELETE requests from web browsers, 392 n

Deleting. See also destroy action; Destroying.

branches, 44

files and directories, 36

recursively, 36

Deleting, users

administrative users (Listings 10.54 to 10.55), 502-503, 501

destroy action (Listings 10.57 to 10.59), 505-508

overview, 500

strong parameters (Listing 10.56), 504-505

testing destroyed users (Listings 10.60 to 10.62), 508-511

users index with delete links, mockup, 501

dependent: :destroy method (Listings 13.19 to 13.20), 615-616

Deployment

overview, 45-46

tools for. See Heroku.

toy application, 88-89, 89

Deployment, professional grade

overview, 358

SSL certificates, 359

SSL (Secure Sockets Layer) in production (Listing 7.36), 358-359

destroy action

deleting sessions (Listings 8.29 to 8.32), 403-405

deleting users (Listings 10.57 to 10.59), 505-508

destroying microposts, 650-652

destroy command, 102

Destroying. See also DELETE method; Deleting.

following relationships (Listing 14.39), 727

Micropost model users (Listings 13.19 to 13.20), 615-616

microposts, 650-652

sessions (Listings 8.29 to 8.32), 403-405

things on a remote server, 105

User objects, 259

users, toy application, 63-66, 65

Development environment

cloud. See Cloud9.

setting up. See Cloud9.

Development environments, 302 n

development.sqlite3 file, 253

Dictionary attacks, protection from, 294 n22

Directories

contents, listing, 36

deleting, 36

updating the modification timestamp, 116-117

Displaying. See also Listing; Showing.

branch status, 43

commit messages, 35

email addresses, 310

.gitignore file, 142

IP binding address, 23

port number, 23

sign-up error messages (Listings 7.20 to 7.22), 336-340

user names, 310

Displaying user data

custom environments, 302 n

debug, and Rails environments (Listing 7.1), 301-306

development environments, 303

lorem ipsum text, 301, 622 n–623 n

production environments, 303

Rails environments, 302-306

show page, mockups, 311

test environments, 303

user email addresses, 310

user names, 310

user profile page, mockup, 300

Users controller with a show action (Listing 7.5), 310

div tag, 197-198

Doctype declarations (Listing 3.23), 120

Document divisions, 197-198

Document type (Listing 3.23), 119

Dollar sign ($)

command line prompt, 8

dollar-sign syntax, 726

Sass variables, 224

Double quotes (" ")

defining strings, 155

exchanging for single quotes, 155

mixing with single quotes, 136

Downcasing email addresses (Listing 6.32), 283

Drop-down menus, mockup, 395

drop_table command, 254

DRY (Don’t Repeat Yourself) principle, 125

Duplication, eliminating. See Refactoring code.

Dynamic content, page titles

base titles, 147

on every page. See Base titles.

full_title helper function (Listings 4.2, 4.3, 4.11), 147-48, 162

HTML structure of a typical web page (Listing 3.23), 119

layout files (Listing 3.35), 128-129

list of sample pages, 119 t

missing page titles, 147

overview, 118-119

RED testing, 119-121

templates for. See Embedded Ruby.

yield function, 126

E

each method, 174, 272

edit action

account activation (Listings 11.31 to 11.32), 545-549

password reset (Listings 12.14 to 12.15), 581-583

updating users (Listings 10.1 to 10.4), 452-457

Edit form, updating users (Listing 10.3), 454

Edit form mockups

initial edit page, 455

unsuccessful edits, 461

updating users, 455

user edit page, 452

Edit view (Listing 10.2), 454

Editing

branches, 42

Git branches, 43

Rails router, 29-30

user information, toy application, 62, 62, 63-64

elseif statement, 157

Email. See also Account activation mailer method; Password reset mailer method.

account activation, sample (Listing 11.25), 538-540

email via user model object (Listing 11.36), 552

fixtures (Listing 6.30), 282

generating an account activation mailer (Listings 11.6 to 11.14), 525-529

mailing the account activation link (Listing 11.12), 527

User mailer test (Listings 11.19 to 11.22), 533-536

User mailer test, screen mockups, 532-533

Email, sending in production

configuring SendGrid (Listing 11.41, 12.23), 556-557, 595-597

overview, 556-557, 595-597

Email addresses

displaying, 310

user names as, 250 n

validating user data (Listings 6.18 to 6.23), 272-277

Email addresses, validating

case sensitivity (Listings 6.26 to 6.27), 279-280

downcasing (Listing 6.32), 283

format (Listings 6.18 to 6.21), 272-277

with regular expressions (Listing 6.21), 274-277, 275 t

uniqueness (Listings 6.23 to 6.34), 278-286

uniqueness at the database level (Listings 6.29 to 6.34), 281-286

Email previews (Listings 11.16 to 11.18), 530-533

Embedded Ruby (Listings 3.31 to 3.40), 126-132

empty? method, 156-157, 338

Enabling SSL, 358-359

Encrypting cookies, 418

Encryption versus hashing, 287 n19

Environments, in the console, 151. See also Development environment; Production environment; Test environment.

Equal, greater than (=>), hashrocket, 171

Equal signs (==)

assignment operator, 423-424, 438-439

equality operator, 164

Erasmus, Michael, 217 n

ERb. See Embedded Ruby.

.erb file extension, 219-220

Error messages

failed micropost creation, 80, 80, 80

Googling, 5

invalid multibyte character, 30 n

Rails Tutorial help, 9

redefining, microposts (Listings 13.41 to 13.45), 639-642

Spring process ID (pid) file will be overwritten, 135 n

toy application, 80

You have not declared a Ruby version, 361

error_explanation (Listing 7.22), 339-340

_error_messages.html.erb partial file (Listing 7.21), 337-338

Escaping out special characters, 528

Exceptions

definition, 260

raising (Listing 9.29), 445-446

removing (Listing 9.33), 448

Exclamation point (!)

bang methods, 165

not operator, 157

Exclamation point, equal sign (!=), not equal operator, 164

Exclamation points (!!), coercing Boolean values, 158, 295

Expiration of cookies, setting, 417-425

Expressions, short-circuit evaluation, 388. See also Regular expressions.

F

f flag, 36

Failed password reset, 589

Failure-page mockups, 330-331

Faker gem

adding to the Gemfile (Listing 10.42), 489-490

creating sample data, 490-491

lorem ipsum text, 301, 622 n–623 n

Feed. See Microposts, proto-feed.

@feed_items instance variable (Listings 13.47 to 13.50), 645-649

Fielding, Roy, 70

field_with_errors class (Listing 7.22), 339-340

File compression, uglifier gem, 21

File navigator, advanced testing, 140-141

Files

blank, creating, 116-117

creating with touch command, 116-117

deleting, 36

GIT staging area, 33

updating the modification timestamp, 116-117

Find in Files global search, 10

find method (Listing 7.5), 386-389

find_by method, finding users (Listings 8.7, 8.16), 376, 386-389

Finding

User objects, 260-261

users (Listings 8.16 to 8.17), 389-390

Fioravanti, Paul, 685 n

Firehose Project, 7

First application. See Hello world program.

first_user variable, 82

Fixtures

adding a digest method (Listings 8.21 to 8.22), 397-398

creating (Listings 8.21 to 8.22), 397-398

definition, 396

Flash, in sessions

code for failed login (Listings 8.9 to 8.12), 379-382

flash test (Listings 8.9 to 8.12), 379-382

flash.now method (Listing 8.11), 381

rendering with a flash message (Listing 8.8), 377-379, 377 t

Flash messages (Listings 7.29 to 7.31), 349-351

flash.now method (Listing 8.11), 381

Flow control, 438-439

Follow buttons

mockups, 682, 708

testing (Listing 14.40), 727-729

Followed users (Listings 14.8 to 14.10), 692-695

Followers

definition, 681-682

Relationship model (Listings 14.12 to 14.13), 695-697

testing for (Listing 14.13), 697

Followers page, following user mockups, 712, 716

followers.include? method (Listing 14.13), 697

Following an unfollowed user, 722

Following associations (Listings 14.8 to 14.10), 692-695

Following page

following user mockups, 711

users following mockups, 715

Following relationships

creating (Listing 14.38), 727

destroying (Listing 14.39), 727

testing (Listing 14.40), 727-729

Following users, definition, 681-682

Following users, Relationship model

active relationships, 685, 685

adding indices (Listing 14.1), 686-687

association methods, summary of, 690 t

belongs_to associations (Listings 11.9, 13.10, 14.3), 689

data model, 686

data model problem, 681-687

enforcing uniqueness, 687

followed users (Listings 14.8 to 14.10), 692-695

followers (Listings 14.12 to 14.13), 695-697

followers.include? method (Listing 14.13), 697

following association (Listings 14.8 to 14.10), 692-695

foreign keys, 688

has_many associations (Listing 14.2), 687-689

has_many :through association (Listings 14.8 to 14.10), 692-695

multiple-key indices, 687

passive relationships, 685

passive relationships (Listings 14.12 to 14.13), 695-697

passive relationships, model for, 696

Twitter-style relationships, 682

user.followers method (Listings 14.12 to 14.13), 695-697

user.following method (Listings 14.8 to 14.10), 692-695

validations (Listings 14.4 to 14.5), 690-691

Following users, status feed

initial status feed (Listing 14.44), 732-735

join method, 734

map method, 733

motivation and strategy (Listing 14.42), 731-732

sample, 730

subselects (Listings 14.45 to 14.47), 736-740

Following users, web interface

access control for relationships (Listings 14.31 to 14.33), 720-721

displaying statistics, 701, 704

displaying statistics (Listings 14.15 to 14.23), 700-710

follow buttons, creating (Listings 14.31 to 14.33), 720-721

followers action (Listing 14.25), 712-713

following action (Listing 14.25), 712-713

following/followers pages (Listings 14.24 to 14.29), 710-718

follow/unfollow forms (Listings 14.15 to 14.23), 700-710

Relationships controller (Listing 14.32), 720

resources block, 699

routes for user relationships (Listing 14.20), 706-707

sample following data (Listing 14.14), 698-699

show_follow view (Listing 14.26), 713-714

testing authorizations (Listing 14.24), 710-711

Following-user mockups

following an unfollowed user, 722

status feed on the live web, 740

unfollowed user, 722

user followers page, 712, 716

users following page, 711, 715

Following-user mockups, Home page

with follow statistics, 704

incremented followers count, 684

status feed, 684, 730, 739

Following-user mockups, user profiles

basic page, 680

finding a user to follow, 681

follow button, 681-682, 708

incremented followers count, 681-682

unfollow button, 681-682, 709

foo, word origin, 153 n

foobar, word origin, 153 n

.footer classes (Listing 5.15), 214 n

footer tag (Listing 5.15), 214 n

Footers

Home page mockup, 215

partials for (Listings 5.15 to 5.17), 214-216

site layout (Listings 5.15 to 5.17), 214-216

Foreign key reference, 603 n

Foreign keys, 688

forget method (Listing 9.11), 426-427

forgot password form. See also Password reset.

basic, 563

invalid email address, 573

modified, 570

valid email address, 579

forgot password link, 562

form tag, sign-up forms, 328

form_for helper, 371-372

form_for method, 320-324

form_tag helper, 371 n

Fowler, Martin, 248 n1

FUBAR, word origin, 153 n

Full-table database scan, 281

full_title helper function (Listings 4.2, 4.3, 4.11), 147-48, 162

Functions. See also Methods.

anonymous, 614

built-in, 146

definition, 106

implicit return, 160

versus methods, 156 n

nameless. See Anonymous functions.

Functions, helpers. See also specific helpers

definition, 147

specific to a particular controller, 147 n

G

gem command

installing Rails, 12-13

specifying a version range (Listings 1.1 to 1.5), 12-13

Gemfile

static pages (Listing 3.2), 94-95

updating (Listing 2.1), 54-55

generate command (Listing 3.6), 100

generate model command, 251

Generating code. See Scaffolding.

GET method, 104-105

Getter methods, creating, 188

Getting started. See also Books and publications; Online resources.

with Cloud9, 11-12

exercises, 7

first application. See Hello world program.

introduction, 4-5

overview, 1-4

prerequisites, 5-7

Git

adding the public key, 37-39, 38

backing up code, 37-40

benefits of, 35-36

Bitbucket (Listing 1.12), 36-40, 39

cat command (Listing 1.11), 37-38

checkout command, 36

deleting files and directories, 36

f flag, 36

GitHub, 37

hosting sites, 36-37

listing directory contents, 36

ls command, 36

overwriting current changes, 36

preventing conflicts Spring server, 139

q command, 35

quitting, 35

README file. See README.md file (Listing 1.13).

-rf flag, 36

rm command, 15 t, 36

Git, branching

branch status, displaying, 43

current branch, identifying, 42

deleting branches, 44

editing branches, 42

listing branches, 42

merging results, 43-44

pushing changes up to the Bitbucket, 39

Git, installation and setup

adding messages to the commit, 35

commit command, 35, 43

commit messages, displaying, 35

committing changes, 35, 43

first-time repository, 34-35

first-time system, 33-34

.gitignore file, 34

log command, 35

-m flag, 35

overview, 33-35

staging area for files, 33

status command, 34

Git, repositories

backing up, 37

copies of. See Git, branching.

pushing changes up to, 39

setting up, 34-35

staging areas, 33

GitHub, 37

.gitignore file

adding Spring server (Listing 3.43), 140-141

displaying, 142

generating, 34

GitLab, 37

Googling error messages, 5

Goto Anything command, 93 n

.gravatar_edit class (Listing 7.11), 317 n

gravatar_for helper function (Listing 7.9), 315

Gravatars (globally recognized avatars) (Listings 7.8 to 7.9), 314-318

GREEN flag, 9

GREEN (passing) tests (Listings 3.17 to 3.22), 114-117

Guard program, 137-143

H

Haml template system, 126 n13

Hansson, David Heinemeier, 3, 110 n

Hash symbol (#)

comment indicator, 21

styling CSS IDs, 725

styling CSS IDs (Listing 5.9), 208

URL stub, 198

Hashed passwords (Listings 6.35 to 6.36), 286-289

Hashes

: (colon), as a hashkey, 171-172

{ } (curly braces), hash indicator, 171, 176

=> (equal, greater than), hashrocket, 171

versus arrays, 170-172

classes, 178

computational cost, 396

definition, 286

each method, 174

indices, 171

inspect method, 174-175

keys, 171

as last argument, 176

nested, 174

p function, 175

printing, 175

symbols as hash keys, 171-172

Hashing

versus encryption, 287 n19

MD5 hashing algorithm, 314-315

options hash, 267

has_many associations

Relationship model (Listing 14.3), 689

toy application, 81-82, 82

has_many relationships (Listing 13.11), 608-611, 609

has_many :through association (Listings 14.8 to 14.10), 692-695

has_secure_password method, 286-289, 294

header tag, 197-198

Headers

partials for, 212-216

site layout, 197, 212-216

Hello world program

controller action (Listing 1.8), 28-31

creating a skeleton, 14

default page, 27

making a workspace directory (Listing 1.2), 14-18

MVC (Model-View-Controller) architecture, 26-27, 28

Rails router, editing, 29-30

root route (Listings 1.9 to 1.10), 29-30

root URL, 29-30

viewing in a browser, 31

Help, Rails Tutorial, 9

help action (Listings 3.6, 3.7, 3.10), 100, 104, 107

Help for Rules Tutorial, 7 n

Help page

custom HTML (Listing 3.12), 108

Help pages

cleaned up views (Listings 3.36 to 3.38), 130-131

creating, 100-101, 104, 107

custom, sample, 109

custom HTML (Listing 3.11), 108

generated view (Listing 3.10), 107

with HTML structure removed (Listings 3.37 to 3.38), 130-131

refactored views (Listings 3.36 to 3.38), 130-131

Helpers. See specific helpers

Heroku

commands, 50-51

creating and configuring an account (Listing 1.15), 48

deployment, 48-50

deployment to, 361

heroku command, 48

heroku create command, 48

heroku open command, 49

PostgreSQL database, 46

renaming applications, 50

setup (Listings 1.14 to 1.15), 46-48

SSH key, adding, 48

heroku command, 48

heroku create command, 48

heroku open command, 49

Heroku Toolbelt, 48

hexdigest method, 314

Hidden fields, password reset (Listing 12.14), 581

Hierarchy, examining Static Pages controller, 185-187, 186

Hierarchy of classes, determining, 179-180

Highlighted lines in code samples, 9

home action, creating Home pages (Listings 3.6, 3.7), 100-104

Home page mockups

Bootstrap and custom CSS, 202-210, 206, 208, 211

CSS styling, 206

custom page, 109

with debug information, 306

footer, 215

form error, 643

form for creating microposts, 635, 642

with proto-feed, 644, 647

site logo (Listing 5.9), 209, 210

site navigation, 201

spacing, 206

typography, 208

without custom CSS, 200-202, 201

Home page mockups, following users

with follow statistics, 704

incremented followers count, 684

status feed, 684, 730, 739

Home pages. See also Site layout.

with account activation message, 538

adding microposts (Listing 13.37), 636-637

boot route, setting, 133

boot route, setting (Listing 3.41), 132-133

creating (Listings 3.6, 3.7), 100-104

custom HTML (Listing 3.11), 108

embedded Ruby title (Listings 3.31 to 3.40), 125-132

full HTML structure (Listings 3.26 to 3.30), 121-125

generated view (Listing 3.9), 107

link to the sign-up page (Listing 5.2), 196 n

raw view, 105

with a title, 122

Home pages, follower statistics

adding (Listing 14.17), 703-704

styling (Listing 14.18), 704-705

HTML

doctype declarations (Listing 3.23), 120

document divisions, 197-198

document type (Listing 3.23), 120

header tag, 197-198

li: (list item) tag, 198

lists, 198

nav tag, 198-199

placing markup in a separate file. See Partials.

section tag, 198

selectors, 120

self-closing tags, 201 n

structure of a typical web page (Listing 3.23), 120

testing for specific tags, 120

ul: (unordered list) tag, 198-199

whitespace sensitivity, 120, 198

HTML shims (Listings 5.12 to 5.13), 196, 212-213

html.erb file extension, 126

HTTP (Hypertext Transfer Protocol) remembering a user’s identity. See Sessions. stateless protocol, 366

HTTP request methods, 104-105. See also specific methods

I

IDE (integrated development environment). See Cloud9.

IDs, CSS, 198

if-else statement, 390

if-then statements, writing. See Ternary operator.

Images

for microposts. See CarrierWave uploader; Microposts, images.

representing users. See Gravatars.

setting alternative display of, 201-202

image_tag helper function

rendering images (Listing 13.62), 661-663

setting alternate images (Listing 5.2), 196 n

Implicit return, functions, 160

Indentation, setting in Cloud9, 12, 12

index action

showing all users (Listings 10.34 to 10.40), 483-488

toy application (Listing 2.6), 68-69

index action redirect (Listing 10.34), 484-485

Index page, showing all users

adding the URL to the users link (Listing 10.40), 487

bootstrap-will_paginate method (Listings 10.44 to 10.46), 492-494

CSS for users index (Listing 10.39), 487

index action (Listings 10.34 to 10.40), 483-488

index action redirect (Listing 10.34), 484-485

index view (Listings 10.34 to 10.40), 485-486

overview, 483-488

paginate method (Listings 10.45 to 10.46), 493-494

pagination (Listings 10.45 to 10.46), 493-494, 495

partial refactoring (Listings 10.50 to 10.52), 498-499

requiring a logged_in_user before filter (Listing 10.35), 485

testing index action redirect (Listing 10.34), 484-485

testing pagination (Listings 10.47 to 10.48), 496-497, 495

will_paginate method (Listings 10.44 to 10.46), 492-494

Index page, toy application, 71-73, 79

Index page mockups, showing all users

basic page, 484

with one hundred users, 491

with one user, 489

with pagination, 495

Index pages, toy application, 61-65

Indexing arrays, 163

Indices, 171

Inheritance, 179-183, 182, 256

Inheritance hierarchies, toy application

ApplicationController class (Listing 2.22), 85-87, 86, 87

for controllers (Listings 2.20 to 2.21), 85-87, 86, 87

MicropostsController class (Listing 2.21), 86 for models (Listings 2.18 to 2.19), 85-87, 86, 87

overview, 85-87, 86, 87

schematic of, 86, 87

UsersController class (Listings 2.12, 2.20), 76-78, 86

Initialization hash, 257

Initializing objects, 257

inspect method, 174-175, 273

Inspecting cookies, 425 n

Installing

Bootstrap, 204

Gemfile (Listing 1.4), 19-23

Git. See Git, installation and setup.

Rails on a local system (Listing 1.6), 23-24

Installing Rails on Cloud9

bundle command, 23 n

Bundler, 18-23

command-line program, 23

default directory structure, 18 t

gem command, 12-13

installing Gemfile (Listing 1.4), 19-23

opening a new terminal tab, 25

overview, 12-13

rails new command (Listing 1.3), 14-18

rails server (Listings 1.6 to 1.7), 23-25

sample directory structure, 17

script, 23

shortcuts, 101 t

with a specific version number (Listings 1.1 to 1.5), 12-13

Instance methods, 179

Instance variables, creating, 188

Instances of classes, 179

Integrated development environment (IDE). See Cloud9.

Integration testing

account activation (Listing 11.33), 550-551

assertions (Listing 5.32), 236-238, 238 t

Contact page (Listing 5.21), 228-230

for invalid sign-up submissions (Listing 7.23), 341-343

for layout links (Listing 5.32), 236-238

multiple assertions per test, 236 n

for valid sign-up submissions (Listing 7.33), 355-356

“Invalid multibyte character” error message, 30 n

Invalid session input, handling, 373-377

IP binding address, specifying and displaying, 23

$IP environment variable, specifying, 23

irb configuration (Listing 4.8), 151

Iteration over innumerable data structures. See Blocks.

J

join method, 165, 734

.js.erb files, 726

JS-ERB files, 726

K

Keys, 171

kill command, 143

L

Labels. See Symbols.

Lambda, 614

Launch School, 7

Layout. See Site layout.

Layout files (Listing 3.35), 128-129

Learn Enough Command Line to be Dangerous, 1

Learn Enough Git to be Dangerous, 32

Learn Enough Society, 6

Learn Enough Text Editor to be Dangerous, 41 n

Length validation (Listing 2.13), 79-81

Less-than signs (<<), adding to arrays, 165

li: (list item) tag, 198-199

lib/assets directory, 218

Links. See also Site layout.

creating, 198

named routes, 227–228 t

stub URLs, 198

link_to helper function, 198

Listing. See also Displaying; Showing.

branches, 42

directory contents, 36

users, toy application, 61, 61, 61, 63, 66-67, 65

Lists, HTML, 198-199

Literal constructors, 178

Literal strings, 153

Local development environment, 10

Local servers

running. See rails server command.

sharing, 26

log command, 35

Logged in status, determining (Listings 8.18 to 8.20), 391-394

logged_in? method (Listing 8.18), 391-392

logged_in_user method

friendly forwarding (Listing 10.31), 480-481

Microposts controller (Listing 13.32), 632-633

requiring logged-in users (Listing 10.15), 467-473

Logging in. See also Remember me; Sessions.

changing layout links (Listings 8.18 to 8.20), 391-394

concerns technique, 382 n

current user, finding (Listings 8.16 to 8.17), 389-390

current_user method (Listings 8.16 to 8.17), 389-390

find method (Listing 7.5), 386-389

find_by method (Listing 8.16), 386-389

logged in status, determining (Listings 8.18 to 8.20), 391-394

logged_in? method (Listings 8.18 to 8.20), 391-394

log_in method (Listings 8.14 to 8.15), 383-385

overview (Listing 8.13), 382-383

and redirecting to the profile page (Listing 8.15), 384-385

session method (Listings 8.14 to 8.15), 383-385

successful login mockup, 391

upon sign-up (Listings 8.25 to 8.27), 400-402

user profile mockup, 391

validating user login (Listings 8.23 to 8.24), 398-399

Logging in, testing layout changes

mockup, drop-down menu, 395

mockup, logged-in user, 395

overview, 395-400

Logging out

only if logged in (Listings 9.11 to 9.19), 428-433

overview (Listings 8.29 to 8.32), 403-405

from persistent sessions (Listing 9.12), 427

a test for logout (Listing 9.12), 427

Login form, code for (Listing 12.3), 569

Login form mockups

access failure, 468

drop-down menus (Listings 8.21 to 8.24), 395

a protected page, 468

“remember me” checkbox, 434, 436

successful login, 391

log_in method (Listings 8.14 to 8.15), 383-385

Login page with forgot password link, 567

Login versus remembering, account activation, and password reset, 516 t

log_in_as method (Listing 9.23), 440-443

Logo. See Site logo.

log_out method (Listings 8.29 to 8.32), 403-405

lorem ipsum text, 301, 622 n–623 n

Lorem.sentence method (Listings 13.25 to 13.26), 622-626

Lovitt, Michael, 275 n

ls command, 36

Ludowyk, Tristan, 593 n

M

-m flag, 34

Magic columns, 252, 258

make utility. See Rake language.

Manifest files (Listing 5.194), 218-219

map method

blocks, 168

status feed, following-users page, 732-735

Mapping user URLs to

controller actions (Listing 2.5), toy application, 68, 65

correspondence between pages, 61 t

Markdown markup language, 41

Markup, placing in a separate file. See Partials.

Mass assignment, 333-336

.md file extension, 41

MD5 hashing algorithm, 314-315

Memoization, 386 n

Merging branches, 43-44

Message chaining, 157

Message passing, 156-159

Messages, adding to a commit, 35. See also Error messages; Sign-up error messages.

Metaprogramming, 541

Method definitions in the console, 159-161

Method reference convention, 284

Method reference technique, account activation, 521

Methods. See also Functions.

adding to built-in classes, 417-418

calling themselves. See Class methods.

versus functions, 156

remembering variable assignments. See Memoization.

Micropost controller (Listing 2.12), 76–78

Micropost model. See also Showing microposts.

associations between models (Listings 13.10 to 13.12), 608-611

associations between models, summary of, 609 t

basic model, 602-604

belongs_to relationships (Listings 11.9, 12.3, 13.10), 608-611

data model, 602

default scope (Listings 13.14 to 13.17), 612-615

default_scope method (Listing 13.17), 614

dependent: :destroy (Listings 13.19 to 13.20), 615-616

destroying users (Listings 13.19 to 13.20), 615-616

foreign key reference, 603 n

generating (Listings 13.1 to 13.3), 602-604

has_many relationships (Listing 13.11), 608-611, 609

migration with added index (Listing 13.3), 604

multiple key index, 604

ordering posts (Listings 13.17), 614

overview, 601-602

validation (Listings 13.4 to 13.8), 605-608

Micropost model, mockups

Home page with a form error, 643

Home page with form for creating microposts, 635, 642

Home page with proto-feed, 644, 647

user profile page with code for microposts, 621

user profile page with microposts, 617, 625-626

user profile page with pagination links, 627

user profile page with unstyled microposts, 623

Micropost model, testing user profiles

associating fixture microposts with users (Listing 13.27), 628-629

micropost pagination, 625-627

profile structure (Listings 13.27 to 13.28), 627-630

Micropost page, toy application, 79

Micropost resource. See Toy application, Micropost resource.

Microposts

access control (Listings 13.31 to 13.34), 632-634

authorization tests for Micropost controller (Listings 13.31 to 13.34), 632-634

definition, 601

displaying. See Showing microposts.

feed. See Microposts, proto-feed.

logged_in_user method (Listing 13.32), 632-633

routes for the Micropost resource (Listing 13.30), 628 t

samples. See Micropost model, mockups.

Microposts, creating

adding to the Home page (Listing 13.37), 636-637

create action (Listing 13.36), 635-636

creating (Listings 13.36 to 13.45), 635-642

form for creating microposts (Listing 13.37), 632

redefining error messages (Listings 13.41 to 13.45), 639-642

user info sidebar (Listing 13.38), 637

Microposts, destroying

adding a delete link (Listing 13.51), 650-652

destroy action, 650-652

HTTP_REFERER, 652 n

request.referrer method, 652, 652 n

Microposts, images. See also CarrierWave uploader.

adding to Micropost model (Listing 13.58), 658-659

basic uploading (Listings 13.59 to 13.62), 659-663

cloud storage, 672-674

display size, constraining, 668 n

format validation (Listings 13.64 to 13.66), 664-667

image_tag helper (Listing 13.62), 661-663

picture attribute, 659, 660

resizing, 668-669

resizing (Listings 13.67 to 13.68), 667-670

size validation (Listings 13.65 to 13.66), 665-667

Microposts, mockups

Home page after deleting a micropost, 653

Home page with a new micropost, 648

Home page with a proto-feed, 643, 647

proto-feed with delete links, 650

Microposts, proto-feed

failed submissions, 648

@feed_items instance variable (Listings 13.47 to 13.50), 645-649

Home page with a proto-feed, 644

overview, 643-649

SQL injection, security hole, 645

status feed (Listings 13.48 to 13.49), 645-649

where method (Listing 13.46), 634-635

Microposts, testing

adding a micropost with a different owner (Listing 13.53), 653-654

deletion with user mismatch (Listing 13.54), 654-655

integration test for the micropost interface (Listing 13.55), 655-656

@microposts instance variable (Listing 13.23), 619

MicropostsController class (Listing 2.21), 86

Migrating down, 254

Migrating up, 252

Migration file name, 252

Migrations. See Database migration; Rake db:migrate command.

MiniMagick, CarrierWave (Listing 13.67), 669-670

MiniTest assertions, list of, 120 n

MiniTest reporters, 136-137

minitest-reporters gem, 136

Missing page titles, 147

mkdir (create directory) command, 14-15 t

Mockingbird, 194 n, 300 n

Mockups, tool for, 194 n. See also specific mockups.

Model file, User model, 255-256

Modeling users. See User model.

Modification timestamp, updating, 116-117

Moses, Riley, 279 n

Multiline blocks, 168

Multiple-key indices, 604, 687

Mutating arrays, 165

mv (move files) command, 14-15 t

MVC (Model-View-Controller) architecture

controllers, 26-27

default Rails page, 27

flow diagram, 67

models, 26-27

requests, 26

schematic view, 28

sharing a local server, 26

toy application, 66-73

views, 26-27

N

Named constructors, 178

Named routes

in links, 227–228 t

in site layout (Listings 5.30 to 5.31), 233-235

Naming conventions

CamelCase, 100-101

scaffold command, 58 n

scaffolding, 58 n

snake case, 100-101

User model, 251

nav tag, 198-199

navbar class, 197

navbar-fixed-top class, 197

navbar-inverse class, 197

navbar-nav class, 198-199

Navigating

the filesystem, 93 n

site layout, 195-202

Nested hashes, 174

Nesting Sass style sheets (Listing 5.7), 221-223

new action, creating a Sessions controller, 366-369

new method, as named constructor, 178

newline

in CSS (Cascading Style Sheets), 176

definition, 154

Nginx, 46

nil value

in a boolean context, 158

definition, 154

and the empty? method, 157

message chaining, 157

responding to methods, 157

testing for, 157-158

“Nothing at all” value. See nil value.

O

Objects

in the console, 156-159

converting to strings, 157

definition, 156

message passing, 156-159

Octothorpe. See Hash symbol (#), comment indicator.

ol (ordered list) tag, 618

Online resources. See also Books and publications.

Bloc, 6

Code School, 6

Firehose Project, 7

help, Rails Tutorial, 9

help for Rules Tutorial, 7 n

Launch School, 7

Learn Enough Society, 6

Pragmatic Studio, 7

Rails Guides, 7

RailsApps, 7

RailsCasts, 7

Thinkful, 7

open command, 49

Opening new terminal tabs, 24–25

Options hash, 267

Ordered list (ol) tag, 618

Ordering micropost posts (Listings 13.17), 614

Overwriting current changes, 36

P

p function, 174-175

paginate method (Listing 10.46), 494

Pagination

microposts, 619-621, 625-627, 626

showing all users (Listings 10.44 to 10.46), 492-494, 495

will_paginate method, 619-621

Palindromes, checking for, 180-183

params hash, sign-up failure (Listing 7.15), 332-333

params variable (Listing 7.1), 301

Parentheses ( () )

in statement syntax, 424 n

and the ternary operator, 437 n14

Partial refactoring (Listings 10.50 to 10.52), 498-499

Partials

for headers, 212-216

for HTML shims (Listings 5.12 to 5.13), 212-213

overview, 211-212

render helper function, 212-213

sharing across views, 212 n

for site footer (Listings 5.15 to 5.17), 214-216

Passive relationships

model for, 696

Relationship model, 685-686

Relationship model (Listings 14.12 to 14.13), 695-697

Password reset. See also Forgot password form.

adding a link to (Listing 12.2), 566

adding password reset methods (Listings 12.5 to 12.6), 571-573

adding reset methods to the User model (Listing 12.52), 588

clearing the reset digest (Listing 12.22), 595

create action (Listings 12.5 to 12.6), 571-573

edit action (Listings 12.14 to 12.15), 581-583

expired resets, testing for (Listing 12.21), 594-595

form for, 584

form for (Listings 12.14 to 12.15), 581-583

generating a controller for (Listings 12.1 to 12.2), 565-566

hidden fields (Listing 12.14), 581

integration test (Listing 12.18), 590-591

login form, code for (Listing 12.3), 569

overview, 561-563

password reset view (Listing 12.4), 570

reset attributes, 568

resetting the password (Listings 12.14 to 12.15), 581-583

update action (Listings 12.16 to 12.17), 584-588

update_columns template (Listing 12.20), 593-594

User model, 568

Password reset, mockups

failed password reset, 589

forgot password form, 563, 570, 573, 578

forgot password link, 562

login page with forgot password link, 567

password reset form, 584

reset email, HTML version, 578

reset email, text version, 578

reset password form, 564

successful password reset, 589

Password reset mailer method

creating (Listing 12.7), 574-575

HTML email template (Listing 12.9), 575

mailing the password reset link (Listings 12.7 to 12.11), 574-577

plain-text email template (Listing 12.8), 575

sample reset email from the server log (Listing 12.11), 576-577

testing (Listing 12.12), 579-580

view templates, defining (Listings 12.8 to 12.9), 575

working preview method (Listing 12.10), 576

Password reset test

failed password reset, mockup, 589

integration test (Listing 12.18), 591-592

overview, 590-592

successful password reset, mockup, 589

Password reset view (Listing 12.4), 570

Password security

authentication, 293-296

bcrypt hash function (Listing 6.36), 288-289, 294 n22

confirmation (Listings 6.37 to 6.40), 289-291

hash, definition, 286

hashed passwords (Listings 6.35 to 6.36), 286-289

hashing versus encryption, 287 n19

minimum length (Listings 6.41 to 6.42), 291-293

overview, 286

password digest, 287. See also Hashed password.

salted hashed, 294 n22

unencrypted passwords, 419 n

User model, 288

virtual, definition, 287 n18

virtual attributes, 287

Password test versus account activation, remembering, and login, 516 t

password_confirmation attribute, 287

password_digest attribute (Listing 6.35), 287-288. See also Hashed passwords.

Password_reset method (Listings 12.7 to 12.11), 574-577

Passwords

allowing blank (Listing 10.13), 465

duplicates across users, 412

PATCH method, 104-105

Patterns of Enterprise Application Architecture (Fowler), 248 n1

Permanent cookies, 417-425

permanent method, 417-425

Persistence, 249

Persistent sessions

creating, 410

logging out (Listing 9.12), 427

testing (Listing 9.31), 447

updating current_user (Listing 9.9), 424-425

Philips, Andy, 287 n19

Phusion Passenger, 45-46

picture attribute, 659, 660

Pictures. See Images.

pid file will be overwritten, error message, 135 n

pluralize text helper, 338

Plus operator (+), concatenating strings, 153

$PORT environment variable, 23

Port number, specifying and displaying, 23

POST method, 104-105

PostgreSQL database, 46-48

Pound sign. See Hash symbol (#), comment indicator.

Pragmatic Studio, 7

Preprocessor engines, 219-220

Printing

hashes, 174-175

strings, 154

private keyword, 334

Pro Git, 40 n

Proc file, creating from a block, 614

Processes, displaying, 142-143

Production environments, 303

Profile page, successful account activation, 547

Programs that write other programs. See Metaprogramming.

Proto-feed

failed submissions, 648

@feed_items instance variable (Listings 13.47 to 13.50), 645-649

Home page with a proto-feed, 644

overview, 643-649

SQL injection, security hole, 645

status feed (Listings 13.48 to 13.49), 645-649

where method (Listing 13.46), 634-635

provide function, 126 n14

ps command, 142-143

push method, adding to arrays, 165

Pushing changes up to Git repositories, 39

puts function, 154

Q

q command, 35

Query parameters, account activation, 528

Question mark (?)

boolean indicator, 156

in query parameters, 528

Quitting Git, 35

R

Rails

creator of, 3

definition, 4

environments, 302-306

learning Ruby first, 6

open source, 4

prerequisites, 5-7

shortcuts, 101 t

“The Rails 3 Asset Pipeline in (about) 5 minutes” (Erasmus), 217 n

Rails console. See Console.

Rails Guides, 7

rails new command (Listing 1.3), 14-18

Rails router, editing, 29-30

Rails routes (Listings 5.27 to 5.28), 230-233. See also Named routes; Root routes.

“Rails Routing from the Outside In,” 701 n

rails server command (Listings 1.6 to 1.7). See also Installing Rails on Cloud9.

default RAILS page, 27

IP binding address, specifying and displaying, 23

$IP environment variable, 23

overview, 23-25

$PORT environment variable, 23

port number, specifying and displaying, 23

Rails Tutorial Cloud9 workspace, 10

Rails Tutorial help, 7 n

RailsApps, 7

RailsCasts, 7

rake db:migrate command, 60

Rake language, definition, 60

Ranges, 163-167. See also Arrays.

Reading data from the web. See GET method.

README.md file (Listing 1.13, 3.3)

branching, 41-42

creating, 40-41

definition, 40

editing, 42

.md file extension, 41

merging with the master branch, 43-44

sample code, 96-97

README.md file (Listings 1.13, 3.3)

committing changes to, 43

pushing changes up to Bitbucket, 44-45

RecordNotFound exception, 260

Recursive force, deleting files and directories, 36

Red, Green, Refactor cycle, 113

RED flag, 9

RED (failing) tests (Listing 3.15), 113-114

redirect_back_or method (Listing 10.32), 481-482

Refactoring code

definition, 110

removing duplication (Listings 3.31 to 3.40), 125-132

smelly code, 117-118

Regex. See Regular expressions.

Regressions, 110-111

Regular expressions

Rubular tool, 275-277, 276

validating email addresses (Listing 6.21), 274-277, 275 t

validating user data (Listing 6.21), 275-277, 275 t

Relationship model. See Following users, Relationship model.

Remember branch, testing (Listings 9.21 to 9.23), 445-449

Remember me. See also Logging in.

cookies, 410

duplicate user passwords, 412

forget method (Listing 9.11), 426-427

overview, 409-410

persistent sessions, 410

“remember me” checkbox, 434, 436

remember_digest attribute (Listings 9.1 to 9.3), 411-415, 411 t

remember_token method (Listing 9.3), 414-415

tokens (Listings 9.1 to 9.3), 410-415

Remember me, at login

authenticated? method (Listing 9.6), 420-421

authenticated? method (Listings 9.17 to 9.19), 431-433

overview, 417-425

persistent sessions (Listing 9.9), 424-425

remembering a logged-in user (Listings 9.7 to 9.9), 421-425

Remember me checkbox

creating (Listings 9.21 to 9.23), 433-438

CSS for (Listing 9.22), 435-436

login form mockups, 434, 436

testing (Listings 9.24 to 9.28), 440-445

remember me function, testing

collisions, checking for, 412 n6

log_in_as method (Listing 9.24), 440-441

persistent sessions (Listing 9.31), 447

raising exceptions (Listing 9.29), 445-446

remember branch (Listings 9.21 to 9.23), 445-449

remember me checkbox (Listings 9.24 to 9.28), 440-444

remembering cookies (Listing 9.25), 442-443

removing raised exceptions (Listing 9.33), 448

remember_digest attribute (Listings 9.1 to 9.3), 411-415, 411 t

Remembered options, 95 n

Remembering cookies (Listing 9.25), 442-443

Remembering versus login, account activation and password reset, 516 t

remember_token method (Listings 9.3), 414-415

Renaming applications, 50

render helper function, 212-213

Rendering images, in microposts (Listings 13.22 to 13.24, 13.62), 617-621, 661-663

Repositories. See Git, repositories.

request.referrer method, 652, 652 n

Reset attributes, password reset, 568

Reset password form, 564

Resetting

databases. See Undoing, database migrations.

passwords. See Password reset.

Resizing micropost images (Listings 13.67 to 13.69), 667-670, 668-669

REST (REpresentational State Transfer), 70

RESTful routes

for the Micropost resource (Listing 13.30), 628, 629 t

toy application, 71 t, 76 t

RESTful URLs, 308-309 t

-rf flag, 36

rm command, 15 t, 36

Root routes

adding (Listing 2.3), 56

defining (Listings 5.27 to 5.28), 230-231

definition (Listings 1.9 to 1.10), 29-30

setting (Listing 3.41), 132-133, 133

Root URL, 29-30

Routing. See Named routes; Rails routes; Root routes.

Rows, databases, 250

Rubular tool, 275-277, 276

Ruby language

classes. See Classes.

data structures. See Data structures.

make utility. See Rake language.

version numbers, displaying, 361-362

Ruby on Rails. See Rails.

Ruby on Rails Tutorial website, 2 n

Rules (CSS), bundling and reusing. See Sass mixins (Listing 7.2).

S

S3 (Simple Storage Service), 672-674

Salted hashed passwords, 294 n22

Sample applications. See Hello world program; Static pages; Toy application.

Sample data, creating

lorem ipsum text, 301, 622 n–623 n

Lorem.sentence method (Listings 13.25 to 13.26), 622-626

microposts (Listings 13.25 to 13.26), 622-626

users (Listing 10.43), 490-491

users (Listings 10.42 to 10.43), 491

Sandbox, for database migration, 256-257

Sass mixins (Listing 7.2), 304-306

Sass utility, alternate syntax, 220 n

Sass utility, asset pipeline

asset directories, 218

definition, 218

manifest files (Listing 5.19), 218-219

preprocessor engines, 219-220

production efficiency, 220

Sass utility, style sheets (Listing 5.7). See also CSS (Cascading Style Sheets).

nesting, 221-223

overview, 220-221

variable names, 224

variables (Listings 5.8, 5.17), 223-227

save method, 257

Saving User objects to the database, 257

scaffold command

naming conventions, 58 n

toy application, 58, 74-78

Scaffolding

description, 3-4

generate scaffold command, 3

Micropost resource, 74-78

naming conventions, 58 n

scaffold command, 58

toy application, 53

.scss file extension, 205, 219-220

section tag, 197

Secure Sockets Layer (SSL). See SSL (Secure Sockets Layer).

Security, preventing unactivated users from logging in (Listing 11.32), 548. See also Authorization; Named routes; Password security.

Selecting array elements, 166

Selectors, 120

self keyword, 182-183

Self-closing tags, 201 n

SendGrid, configuring (Listing 11.41, 12.23), 556-557, 595-597

Server computer, definition, 104-105

Server log errors, 308

Servers, sharing local, 26

Session hijacking, 358, 410, 412 n5

session method (Listings 8.14 to 8.15), 383-385

Sessions. See also Logging in.

cookies, 366

definition, 366

destroying (Listings 8.29 to 8.32), 403-405

Sessions, creating

permanent. See Persistent sessions.

temporary (Listings 8.14 to 8.15), 383-385

Sessions, finding and authenticating users

authenticate method (Listing 8.7), 376, 377 t

authenticating users (Listing 8.7), 376, 377 t

failed login, mockup, 375

find_by method (Listing 8.7), 376

finding users (Listing 8.7), 376

invalid input, handling, 373-377

overview, 373-377

Sessions, flash

code for failed login (Listings 8.9 to 8.12), 379-382

flash test (Listings 8.9 to 8.12), 379-382

flash.now method (Listing 8.11), 381

rendering with a flash message (Listing 8.8), 377-379, 377 t

Sessions, login form creation

code sample (Listing 8.4), 371

form_for helper, 371-372

form_tag helper, 371 n

HTML sample (Listing 8.5), 373

mockup of completed form, 372

overview, 370-373

Sessions, login form mockups

added links, 372

basic form, 367

completed form, 372

with flash message, 378

flash persistence (Listings 8.9 to 8.12), 379-382

login failure, 370, 375, 378

Sessions, persistent

creating, 410

logging out (Listing 9.12), 427

testing (Listing 9.31), 447

updating current_user (Listing 9.9), 424-425

Sessions controller

adding resources (Listing 8.2), 368

overview, 366-369

routes, custom, 369 t

routes, listing, 368-369

Setter methods, creating, 188

Shims (Listings 5.12 to 5.13), 196, 212-213

Shivs. See HTML shims.

Short-circuit evaluation, 388

Shortcuts, Rails commands, 101 t. See also Ctrl-C keystroke; Ctrl-D keystroke.

Show page, mockups, 311

Showing all users. See also Displaying; Listing.

db/seeds.rb file (Listing 10.43), 490

Faker gem, adding to the Gemfile (Listing 10.42), 488-492

overview, 483

sample users, creating (Listing 10.43), 490-491

sample users, creating (Listings 10.42 to 10.43), 491

Showing all users, index page

adding the URL to the users link (Listing 10.40), 487

bootstrap-will_paginate method (Listings 10.44 to 10.46), 492-494

CSS for users index (Listing 10.39), 487

index action (Listings 10.34 to 10.40), 483-488

index action redirect (Listing 10.34), 484-485

index view (Listings 10.34 to 10.40), 485-486

overview, 483-488

paginate method (Listings 10.44 to 10.46), 492-494

pagination (Listings 10.44 to 4.46), 492-494

pagination (Listings 10.44 to 10.46), 495

partial refactoring (Listings 10.50 to 10.52), 498-499

requiring a logged_in_user before filter (Listing 10.35), 485

testing index action redirect (Listing 10.34), 484-485

testing pagination (Listings 10.47 to 10.48), 496-497, 495

will_paginate method (Listings 10.44 to 10.46), 492-494

Showing all users, index page mockups

basic page, 484

with one hundred users, 491

with one user, 489

with pagination, 495

Showing microposts. See also Micropost model.

count method, 620

counter cache, 620

@microposts instance variable (Listing 13.23), 619

number of posts per user, 620

paginating posts, 619-621

profile page with microposts, mockup, 617

rendering microposts (Listings 13.22 to 13.24), 617-621

time_ago_in_words method (Listing 13.22), 618-619

will_paginate method (Listing 13.24), 620-621

Side effects of functions, 154

Sidebar (Listings 7.10 to 7.11), 316-319

Signed cookies, 418

Signing up on Cloud9, 11

Sign-up, logging in (Listings 8.25 to 8.27), 400-402

Sign-up error messages

_error_messages.html.erb partial file (Listing 7.21), 337-338

any? method, 338

displaying (Listings 7.20 to 7.22), 336-340

empty? method, 338

error_explanation (Listing 7.22), 339-340

field_with_errors class (Listing 7.22), 339-340

mockups, 340

overview, 335-340

pluralize text helper, 338-339

styling with CSS (Listing 7.22), 339-340

testing, template for, 344

Sign-up failure

assert_select method, 342

attr_accessible method, 333

count method, 341

create action (listing 7.18), 329-332

debug information (Listing 7.18), 330-333

failure-page mockups, 331

mass assignment, 333-336

params hash (Listing 7.15), 332-333

private keyword, 334

strong parameters (Listing 7.19), 333-336

testing for invalid submissions (Listing 7.23), 341-343

testing for valid submissions (Listing 7.33), 355-356

a working form (Listing 7.18), 329-333

Sign-up forms

authenticity token, 328

CSRF (cross-site request forgery), 328

form tag, 328

form_for method, 320-324

mockups, 320-324, 321-322, 321-324, 324, 340

overview, 320

Sign-up forms, HTML

overview, 325-329

sample HTML (Listing 7.15), 325-327

Sign-up page

with account activation warning message, 548

adding an @user variable (Listing 7.14), 320-321

assigning a route (Listing 5.43), 242-244

custom stub view (Listing 5.46), 243

first user sign-up, 352-355, 352

flash messages (Listings 7.29 to 7.31), 349-351

linking a button (Listing 5.45), 242-244

linking to the Home page (Listing 5.2), 196 n

Users controller, creating (Listings 5.38 to 5.39), 240-241

Sign-up page, mockups, 244, 249

filled-in form, 327

first user sign-up, 352

forms, 321-324

sign-up failure (Listing 7.18), 329-333, 330-331

Sign-up success

create action (Listing 7.28), 344-345

finished sign-up form, 346-348

finished sign-up form (Listing 7.22), 347

overview, 345-346

welcome message (Listings 7.29 to 7.31), 349-352

Sign-up test (Listing 11.33), 550-551

Simple Storage Service (S3), 672-674

Single quotes ('), mixing with double quotes, 136

Site layout. See also Bootstrap; Links.

alternate images, 200-201

document divisions, 198

footers (Listings 5.15 to 5.17), 214-216

headers, 197, 212-216

navigation, 195-202

site logo (Listing 5.9), 209

web page (Listings 3.31 to 3.40), 125-132

Site logo (Listing 5.9), 209

Site navigation

application.html.erb file (Listings 4.3, 5.1), 195-202

Home page mockup, 201

updating the site layout file (Listing 5.1), 195-202

Siwek, Katarzyna, 174-175

Size validation, micropost images (Listings 13.65 to 13.66), 665-667

Skeletons (code). See Scaffolding.

Slash (/)

root URL, 60

Unix directory separator, 8

Slash, asterisk (/* ... */), CSS comments, 205

Smelly code, 117-118

Snake case, 101

Spacing, Home page mockup, 206

split method, 163

spring command, 143

Spring process ID (pid) file will be overwritten, error message, 135 n

Spring server

adding to .gitignore file (Listing 3.46), 140

preventing conflicts with Git, 139

SQL (Structured Query Language)

alternatives to. See Active Record.

case sensitivity, 614 n

naming conventions, 614 n

pronouncing “SQL,” 248 n2

SQL injection, security hole, 645

SQLite Database Browser

pronouncing “SQLite,” 253

screen shots, 254, 255, 295

Square brackets ([]), accessing elements, 164

SSH key, adding to Heroku, 48

SSL (Secure Sockets Layer)

enabling, 358-359

in production deployment (Listing 7.36), 358-359

session hijacking, 358

SSL certificates, 359

Stabby lambda, 614

State, maintaining. See Cookies; Remember me.

Stateless protocol, 366

Static pages. See also About page; Contact page; Help page; Home page; Sign-up page.

boot route, setting, 133

boot route, setting (Listing 3.41), 132-133

custom pages (Listings 3.11), 108

embedded Ruby (Listings 3.31 to 3.40), 126-132

Gemfile (Listing 3.2), 94-95

generating (Listing 3.6), 100-107

generating a controller, 100-107. See also Static Pages controller.

generating a sample app (Listing 3.1), 94

help action (Listings 3.6, 3.7, 3.10), 100, 104, 107

layouts (Listings 3.31 to 3.40), 125-132

README file (Listing 3.3), 96-97

refactoring (Listings 3.31 to 3.40), 125-132

setup, 93-99

Static pages, dynamic titles

base titles, 147

on every page. See Base titles.

full_title helper function (Listings 4.2, 4.3, 4.11), 147-48, 162

HTML structure of a typical web page (Listing 3.23), 119

layout files (Listing 3.35), 128-129

list of sample pages, 119 t

missing page titles, 147

overview, 118-119

RED testing, 119-121

templates for. See Embedded Ruby.

yield function, 126

Static Pages controller

code sample (Listing 3.6), 100

contents of config directory, 103

generating (Listing 3.6), 100-107

help action (Listings 3.6, 3.7, 3.10), 100, 104, 106-107

hierarchy, examining, 185-187, 186, 186

home action (Listings 3.6, 3.7), 100-104

Home page, generated view (Listing 3.9), 107

Home page, raw view, 105

Rails shortcuts, 101 t

Static Pages controller, testing

default (Listing 3.13), 112-113

generating a test file (Listing 3.13), 112-113

GREEN (passing) tests (Listings 3.17 to 3.22), 114-117

Red, Green, Refactor cycle, 113

RED (failing) tests (Listing 3.15), 113-114

refactoring code, 117

status command, 34

Status feed

following-user mockups, 740

micropost (Listings 13.48 to 13.49), 645-649

Status feed, following-users page

initial status feed (Listing 14.44), 732-735

join method, 734

map method, 733

motivation and strategy (Listing 14.42), 731-732

sample, 730

subselects (Listings 14.45 to 14.47), 736-740

Stealing cookies, 410

String literals, 153

Strings

arrays of, 272

assigning values to, 153

assigning variable names to, 153

concatenating, 153

in the console, 153-155

converting from objects, 157

converting to arrays, 163

definition, 153

double-quoted, 153

interpolating, 153

printing, 154

single-quoted, 154-155

Strong parameters

create action (Listing 7.19), 334

deleting users (Listing 10.56), 504-505

sign-up failure (Listing 7.18), 330-333

Structured Query Language (SQL)

alternatives to. See Active Record.

pronouncing “SQL,” 248 n2

Stub URLs, linking to, 198

Stub view for user information (Listing 7.4), 310

Style sheets, CSS (Listing 4.1), 175-177

Style sheets, Sass utility

nesting (Listing 5.7), 221-223

overview, 220-221

variable names, 224

variables (Listings 5.8, 5.17), 223-227

stylesheet_link_tag function, 146, 177

Styling with CSS (Listing 7.22), 339-340

Subselects (Listings 14.45 to 14.47), 736-740

Successful password reset, 589

superclass method, 179-180

Symbols

as hash keys, 171-172

overview, 172-175

T

Table attacks, protection from, 294 n22

Tables

databases, 250

dropping from databases, 254

TDD (test-driven development), 110. See also Testing.

Technical sophistication, 1-2

Templates

testing error messages, 344

update_columns (Listing 12.20), 593-594

Templates for pages. See Embedded Ruby.

10 types of people, 438-439, 438-439

Ternary operator

in function return values, 439

and parentheses ( () ), 437 n14

replacing assignment operators, 438-439

Test environments, 303

Test suites, 110

Testing. See also Integration testing; TDD (test-driven development).

About page, 114

authorization tests for Micropost controller (Listings 13.31 to 13.34), 632-634

authorizations, following users (Listing 14.24), 710-711

automated. See Automated testing.

commenting out failed tests (Listing 11.24), 537

a correct user (Listings 10.27 to 10.28), 476-478

destroyed users (Listings 10.60 to 10.62), 508-511

edits (Listings 10.20 to 10.21), 471-473

error messages, template for, 344

for expired password resets, (Listing 12.21), 594-595

failing tests, writing. See TDD (test-driven development).

for followers (Listing 14.13), 697

following relationships (Listing 14.40), 727-729

for friendly forwarding (Listings 10.29 to 10.32), 478-483

index action redirect (Listing 10.34), 484-485

logging in a test user (Listing 10.15), 468-471

logout (Listings 8.29 to 8.32), 404-405

logout (Listings 9.12), 427

MiniTest assertions, list of, 120-121

model tests. See Validating user data.

multiple assertions per test, 236 n

for nil values, 157-158

pagination (Listings 10.44 to 10.46), 492-494, 495

password reset mailer method (Listing 12.12), 579-580

persistent sessions (Listing 9.31), 447

refactoring code, 110

regressions, 110-111

for specific HTML tags, 120

timing of, 110-111

unfollow buttons (Listing 14.40), 727-729

unsuccessful user updates (Listings 10.9), 461-462

the wrong user (Listing 10.24), 474

Testing, account activation

activation via user model object (Listing 11.37), 552-553

integration test (Listings 11.33 to 11.37), 550-551

overview, 549

sending email via user model object (Listing 11.36), 552

setting the test domain host (Listing 11.21), 535

user activation methods in the User model (Listing 11.35), 551-552

user sign-up test (Listing 11.33), 550-551

Testing, advanced

adding Spring to .gitignore file (Listing 3.4), 140

automated, 137-143

custom Guardfile (Listing 3.45), 138-139

file navigator, 140-141

Guard program, 137-143

MiniTest reporters, 136-137

Testing, Micropost user profiles

associating fixture microposts with users (Listing 13.27), 628-629

micropost pagination, 625-627

profile structure (Listings 13.27 to 13.28), 627-630

Testing, Static Pages controller

default (Listing 3.13), 112-113

generating a test file (Listing 3.13), 112-113

GREEN (passing) tests (Listings 3.17 to 3.22), 114-117

Red, Green, Refactor cycle, 113

RED (failing) tests (Listing 3.15), 113-114

refactoring code, 117

Text editors, aligning columns, 177 n

Thinkful, 7

Thomas, Dave, 7

Time helpers, 418

time_ago_in_words method (Listing 13.22), 618-619

Timestamp on files and directories, updating, 116-117

Timestamps

database migrations, 252

User objects, 258 n7

UTC (Coordinated Universal Time), 258 n7

Titles on static pages

adding, 121-125

base titles, 147

on every page. See Base titles.

full_title helper function (Listings 4.2, 4.3, 4.11), 147-48, 162

HTML structure of a typical web page (Listing 3.24), 120

layout files (Listing 3.35), 128-129

list of sample pages, 119 t

missing page titles, 147

overview, 118-119

templates for. See Embedded Ruby.

yield function, 126

Titles on static pages, testing

Home page title (Listing 4.4, 4.6), 148-149

page titles (Listings 3.23 to 3.25), 119-121

page titles (Listings 3.23, 3.24 to 3.25), 120-121

TLS (Transport Security Layer), 358 n. See also SSL (Secure Sockets Layer).

to_a method, 166

Tokens (Listings 9.1 to 9.3), 410-415

Torvalds, Linus, 32

to_s method, 157

touch command, 116-117

Toy application

deploying, 88-89, 89

micropost data model, 57-58, 58

planning, 54-57

scaffolding, 53

updating the Gemfile (Listing 2.1), 54-55

user data model (Listing 2.6), 57-58, 57-58, 72. See also Toy application, Users resource.

Toy application, inheritance hierarchies

ApplicationController class (Listing 2.22), 85-87, 86, 87

for controllers (Listings 2.20 to 2.21), 85-87, 86, 87

MicropostsController class (Listing 2.21), 86

for models (Listings 2.18 to 2.19), 85-87, 86, 87

overview, 85-87, 86, 87

UsersController class (Listings 2.12, 2.20), 76-78, 86

Toy application, Micropost resource

data model associations (Listings 2.14, 2.15), 81-82, 82

data validation, 74

error messages, 80

first_user variable, 82

has_many associations, 81-82, 82

index page, 79

length validation (Listing 2.13), 79-81

Micropost controller (Listing 2.12), 76–78

micropost page, 77

Rails console, 82

RESTful routes, 76 t

scaffolding, 74-78

Toy application, Users resource

bundle exec, 60

controller actions (Listing 2.7), 69-70

correspondence between pages and URLs, 61 t

creating users, 62-64, 62-63

destroying users, 64-66, 65

editing user information, 62, 63-64

index action (Listing 2.6), 68-69

index page, 60, 61, 71-73

listing users, 61, 61, 63, 66-67, 65

mapping user URLs to controller actions (Listing 2.5), 68, 65

migrating the database, 59-60

MVC (Model-View-Controller), 65-73

overview, 58-60

rake db:migrate command, 60

Rake language, 60

REST (REpresentational State Transfer), 70

RESTful routes, 71 t

root route, adding (Listing 2.3), 56

scaffold command, 58

user page, 63

user pages, 61-65, 61-65

Users controller (Listing 2.7), 69-70

weaknesses, 74

Transport Security Layer (TLS), 358 n. See also SSL (Secure Sockets Layer).

Tuite, Colm, 193 n

Twitter-style relationships, 685

Typography (Listing 5.8), 208, 208-210, 209

U

uglifier gem, 21

ul: (unordered list) tag, 198

underscore method, 688 n

Undoing

code generation, 102-103

database migrations, 102-103, 254, 256, 294 n21

Unencrypted passwords, 419 n

Unfollow buttons

mockups, 681-682, 709

testing (Listing 14.40), 727-729

Unfollowed user, mockup, 722

Uniform Resource Locator (URL), correspondence between pages, 61 t

Uniqueness, email addresses

at the database level (Listings 6.29 to 6.34), 281-286

validating (Listings 6.23 to 6.34), 278-286

Unix command line

/ (forward slash), directory separator, 7

? (dollar sign), command line prompt, 8

cd (change directory) command, 14-15 t

common commands, 15 t

cp (copy files) command, 14-15 t

mkdir (create directory) command, 14-15 t

mv (move files) command, 14-15 t

overview, 14

Unobtrusive JavaScript, 724

Unordered list (ul:) tag, 198

Unsuccessful sign-up. See Sign-up failure.

upcase method (Listing 6.26), 279

update action

password reset (Listings 12.16 to 12.17), 584-588

updating users (Listing 10.8), 459-460

updating users (Listings 10.11 to 10.13), 464-465

Updating

remote servers. See PATCH method.

User objects, 262-263

Updating a remote server. See PATCH method.

Updating users

adding a URL to the settings link (Listing 10.4), 456-457

edit action (Listings 10.1 to 10.4), 452-457

edit form (Listing 10.3), 455

edit form mockups, 455

edit view (Listing 10.2), 454

overview, 451

strong parameters (Listing 10.56), 504-505

update action (Listing 10.8), 459-460

Updating users, successful edits with TDD

acceptance tests, 463

allowing blank passwords (Listing 10.13), 465

overview, 462-466

successful mockup, 466

update action (Listings 10.11 to 10.13), 464-465

Updating users, unsuccessful edits

edit form mockup, 461

overview (Listing 10.8), 459-460

testing (Listing 10.9), 461-462

update action (Listing 10.8), 459-460

Uploading images

CarrierWave uploader (Listings 13.58 to 13.62), 657-663

CarrierWave uploader (Listings 13.69 to 13.71), 671-674

micropost (Listings 13.58 to 13.63), 657-663

URL (Uniform Resource Locator), correspondence between pages, 61 t

url_encode method, 535 n

User activation methods, testing (Listing 11.35), 551-552

User class (Listing 4.17), 187-190

User class inheritance, 256

User data, showing. See Displaying user data.

User email addresses, displaying, 310

User info sidebar (Listing 13.38), 637

User model. See also Databases.

with account activation attributes, 519

database migrations, 249-255

email addresses as user names, 250 n

generating (Listing 6.1), 251

model file, 255-256

naming conventions, 251

overview, 248-249

persistence, 249

sample, 250, 253

sample code (Listing 6.3), 256

sketch of, 251

viewing the structure of, 253

User model, User objects

creating, 256-260

current date and time, 258

destroying, 259

finding, 260-261

initialization hash, 257

saving to the database, 257

timestamps, 258 n7

updating, 262-263

validity. See Validating user data.

User names

displaying, 310

email addresses as, 250 n

User objects

creating, 256-260

current date and time, 258

destroying, 260-261

finding, 260-261

initialization hash, 257

saving to the database, 257

timestamps, 258 n7

User pages, toy application, 61-65, 61-65, 63

User profile page

adding a Users resource (Listing 7.3), 308

creating, 307-311

Gravatars (globally recognized avatars) (Listings 7.8 to 7.9), 314-318

logging in, mockup, 391

mockups, 300

RESTful URLs, 308-309 t

routing, 307-309 t

server log errors, 308

sidebar (Listings 7.10 to 7.11), 316-319

stub view for user information (Listing 7.4), 310

test for valid submission (Listing 7.33), 355-356

User profile page mockups

basic page, 301

code for microposts, 621

CSS, 322–324

CSS (Listing 7.11), 317-319, 318

custom Gravatar, 315–317

default Gravatar, 315–316

with microposts, 617, 625-626

with pagination links, 627

sidebar, 318

unfollow button, 709

with unstyled microposts, 623

User profile page mockups, following-users

basic page, 680

finding a user to follow, 681

follow button, 681-682, 708

incremented followers count, 681-682

unfollow button, 681-682, 709

User sign-up test (Listing 11.33), 550-551

user.followers method (Listings 14.12 to 14.13), 695-697

user.following method (Listings 14.8 to 14.10), 692-695

user.forget method (Listing 9.11), 426-427

Userid, storing in cookies, 418-419

Users

adding to the fixture file (Listings 10.23 to 10.28), 473-478

deleting. See Deleting, users.

displaying. See Showing all users.

editing. See Updating users.

updating. See Updating users.

validating. See Validating user data.

Users controller

creating a sign-up page (Listings 5.38 to 5.39), 240-241

with a show action (Listing 7.5), 310

toy application (Listing 2.7), 69-70

Users index with delete links, mockup, 501

Users resource, adding to a user profile page (Listing 7.3), 308. See also Toy application, Users resource.

UsersController class (Listings 2.12, 2.20), 76-78, 86

UTC (Coordinated Universal Time), 258 n7

V

valid? method, 257

Validating

images, CarrierWave uploader (Listings 13.64 to 13.66), 664-667

Micropost models (Listings 13.4 to 13.8), 605-608

Relationship model (Listings 14.4 to 14.5), 690-691

user login (Listings 8.23 to 8.24), 398-399

Validating user data

case sensitivity (Listings 6.26 to 6.27), 279-280

email addresses (Listings 6.18 to 6.23), 272-277

format (Listings 6.18 to 6.21), 272-277

length (Listings 6.14, 6.16), 269-271

options hash, 267

overview, 263-264

presence (Listings 6.7 to 6.12), 266-269

with regular expressions (Listing 6.21), 275-277, 275 t

validity testing (Listings 6.4 to 6.5), 264-265

Variable names, Sass style sheets, 224

Variables, Sass style sheets (Listings 5.8, 5.17), 223-227

vendor/assets directory, 218

Version control

application source code. See Git.

definition, 32

specifying a version range (Listings 1.1 to 1.5), 12-13

Vertical bars (||)

block variable indicator, 168

or operator, 157

Vertical bars, equal sign (||=), or equals

assignment operator, 387-388

Vertical dots in code listings, 9

Views

cleaned up (Listings 3.36 to 3.38), 130-131

corresponding actions (Listings 3.9, 3.10), 107

embedded Ruby (Listings 3.31 to 3.40), 125-132

full_title helper function (Listings 4.2, 4.3, 4.11), 147-48, 162

Virtual attributes, 287

Visscher, Sjoerd, 196 n

w[ ] technique, 272

WEBrick webserver, 359

where method (Listing 13.46), 634-635

Whitespace sensitivity, HTML, 120, 196 n

will_paginate method

showing all users (Listings 10.44 to 10.46), 492-494

showing microposts (Listing 13.24), 620-621

Wireframes. See Mockups.

—without production option, 95

Workspaces

creating on Cloud9, 11, 12

Rails Tutorial Cloud9, 10

Y

YAML (YAML Ain’t Markup Language), 305 n

yield method, 126, 198

You have not declared a Ruby version, error message, 361

Z

Zero-offset arrays, 163

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets

Code Snippets