├── README.md ├── part_three.md └── part_two_comments_and_votes.md /README.md: -------------------------------------------------------------------------------- 1 | ## Reddit on Rails: A Hero's Journey 2 | 3 | In past exercises for [UT on Rails](http://schneems.com/ut-rails) I've given you a starter project to work on, and I've given very step by step instructions with the intent of teaching specific concepts and skills. By now you have the knowledge to accomplish much with Rails. We will use that knowledge to build a website from scratch similar in functionality to http://reddit.com. 4 | 5 | Instead of focusing on specific concepts and introducing ideas, in this exercise we will be looking at Rails from a high level. We will be doing things that you've already done, it is your job as a student to try to anticipate what we should be doing next before we do it, and then to execute the implementation with full understanding. If you have any doubts over why we are doing something, you should ask someone immediately, or write them down to investigate later. While you can get through the exercise by blindly following instructions, you will be more lost when attempting to implement things by yourself. If you want to get the full bennefit of this exercise, try to not copy any code. Type in code as it appears, and fix any mistakes that you make. This exercise is not about the final product but about the journey. 6 | 7 | As we go along you'll get a glimpse into my thought process for making websites. While yours might differ in time I find seeing someone else's work flow always strengthens mine. 8 | 9 | Sometimes I will be missing steps between instructions, such as "stop your server". Other times I will give high level instruction. This is not laziness on my part, but rather a chance for you to exercise what you've learned as a programmer. It is your job to verify the output of commands you type into the console. This should not be a passive exercise. 10 | 11 | As a side note, I usually have 3 or more console tabs open to the current directory I'm working in so I can run my server and console and any rake commands I want at the same time. 12 | 13 | ## Install 14 | 15 | You will need ruby 1.9 on your system. 16 | 17 | $ ruby -v 18 | 19 | You will also need rails and bundler. You can find install directions and materials in previous exercises http://schneems.com/ut-rails. This is not a beginner exercise, but rather an intermediate trial by fire, new developers are not expected to be able to finish. Good luck. 20 | 21 | ## Let's get Started 22 | 23 | First we will take a look at what we want to build, make a game plan on how to make it, and then iterate very slowly until our product becomes what we desire. 24 | 25 | If you're not familiar with http://reddit.com, go there right now. Take a look around, click some links. Reddit is a very popular social-ish network for sharing links. The basic idea is simple, a user submits a link with a title that they find interesting. If others like the link they vote it up. If they dislike the link, they vote it down. Top links should show up on the front page. 26 | 27 | Before I begin writing any code for a project I will craft user stories around the interactions. If we take those user stories and break out the nouns (people, places, things) this will give us a good idea of what our models need to be. An example of a user story for this Reddit clone might be something like: 28 | 29 | "A user visits the home page of the website, they then log in. They click a "submit link" 30 | button and enter a url and a title. They submit that link and title and then end up on a 31 | page that shows the title, link, votes for that link and any comments." 32 | 33 | 34 | As I develop a Rails site I will move in as small of steps as I possibly can and validate they are working each step along the way as I would expect. On a high level I will need MVCr (model, view, controller, and routes) to form a full resource that can be used on our website. Since we have to start somewhere I typically start with the models. I'll go back to my user story and pull out all of my nouns: 35 | 36 | 37 | "user", "home page", "web site", "submit link button", "link", "url", "title", "page", "votes", 38 | "comments" 39 | 40 | There are some duplicates and sum such as "web site" that aren't very helpful. When looking at these nouns I've got some top level nouns such as: 41 | 42 | "user", "link", "votes", "comments" 43 | 44 | And then I've got sum nested nouns, for example we mentioned "url" and "title", but these should likely be attributes of a link. You might be questioning why "votes" isn't an attribute of "link", while you could store a vote_count with each link, we need some way to associate a user with a vote, so each user can only vote once. I know this because i've used reddit a fair bit, but we could have come to this conclusion by writing a user story around voting on links. 45 | 46 | Now that we've got our top level nouns, we can turn them into proper models. Since we're going to go in small steps, we'll only work on one model at a time, fleshing out MVCr as we go along. 47 | 48 | We'll start out with the "user" model: 49 | 50 | ## User Model 51 | 52 | While you could call this "person" or "human" or something similar, "user" has become a pretty standard term in web applications. To have a user in our application we want some standard attributes. To help get these we could imagine what you would expect when you view your own profile. Likely you would want some way of logging in, so the system would have a username or email, or both, and it would have a password to keep strangers from pretending to be you. 53 | 54 | Even if we have a username, it makes sense to keep a user's email since they might forget their password and need a reset link sent to them. If you want to add more attributes or functionality later, you can. So don't worry about getting everything. So our user model now looks something like this: 55 | 56 | User Table 57 | username: string 58 | email: string 59 | password: string 60 | created_at: datetime 61 | updated_at: datetime 62 | 63 | There is one small problem with this model. Storing passwords in plain text is a horrible practice, insecure at best and negligent at worse. We could store our own password "fingerprint" using a hashing library like [bcrypt](http://en.wikipedia.org/wiki/Bcrypt) which is similar to MD5 but more secure. Then we could compare our stored "fingerprint" to a user submitted password that has been run through our hashing (fingerprinting) library. This is a commonly accepted practice, but we are lazy programmers. People smarter than us have already written this functionality in other libraries. So rather than store our password in plain text we will use a ruby library to give us this functionality. 64 | 65 | Currently [Devise](https://github.com/plataformatec/devise/) is the industry standard for user authentication, but it wasn't always that way. If you didn't know about it, you could find good library by googling for `rails user authentication library`. Another good resource is the [Ruby Toolbox](https://www.ruby-toolbox.com/) which will help compare libraries with similar functions. You can see their section on [Rails Authentication](https://www.ruby-toolbox.com/categories/rails_authentication) and you will see that at the time of this writing Devise is on top. 66 | 67 | 68 | ## Rails New 69 | 70 | So we have a good idea on how we want to start but haven't written any code at all, this is fine but let's at least make a new rails project. In your projects folder run the command to make a new rails project call it `reddit_on_rails`. If you don't know how to do that in your project directory try running: 71 | 72 | $ rails --help 73 | 74 | Once you've generated a new rails app go into that directory: 75 | 76 | $ cd reddit_on_rails/ 77 | 78 | You can verify that you are in a Rails app by checking the file system: 79 | 80 | $ ls 81 | Gemfile README.rdoc app config.ru doc log script tmp 82 | Gemfile.lock Rakefile config db lib public test vendor 83 | 84 | If you run `$ rails --help` now you will see a different output: 85 | 86 | $ rails --help 87 | Usage: rails COMMAND [ARGS] 88 | 89 | The most common rails commands are: 90 | generate Generate new code (short-cut alias: "g") 91 | console Start the Rails console (short-cut alias: "c") 92 | server Start the Rails server (short-cut alias: "s") 93 | dbconsole Start a console for the database specified in config/database.yml 94 | (short-cut alias: "db") 95 | new Create a new Rails application. "rails new my_app" creates a 96 | new application called MyApp in "./my_app" 97 | 98 | In addition to those, there are: 99 | application Generate the Rails application code 100 | destroy Undo code generated with "generate" (short-cut alias: "d") 101 | benchmarker See how fast a piece of code runs 102 | profiler Get profile information from a piece of code 103 | plugin Install a plugin 104 | runner Run a piece of code in the application environment (short-cut alias: "r") 105 | 106 | All commands can be run with -h (or --help) for more information. 107 | 108 | If you get stuck using a rails command you can always use this help, you can also drill down to get more help. If you didn't know what generate did you could run 109 | 110 | $ rails generate --help 111 | 112 | And deeper still 113 | 114 | $ rails generate model --help 115 | 116 | You're not expected to know all the syntax of all the Rails commands by heart, but you do need to know where to look for help. 117 | 118 | 119 | Now that we're in our project you might want to initialize a git repository and commit. This makes it easier to see what changes you do to your app. 120 | 121 | We already laid out our user model above so lets make it. We will need a model file in `app/models` and a migration in `/db/migrate`. We can manually make the model file but making migrations manually is frowned upon. Lets make both with a generator. Generate a model using: 122 | 123 | $ rails generate model user 124 | 125 | Here we could have specified the fields for our user like $ rails generate model user email:string ...`, but we can add them in manually. I prefer a hands on approach that gives me control and visibility into what is going on. 126 | 127 | We should now have a `app/model/user.rb` file and a `db/migrate/...._create_users.rb` file. The number in front is a time stamp and is used by rails to know what migrations we've already run. 128 | 129 | Open up the migration file, you'll see there is already some code in there but we need to add our `email` and `username` fields (created_at and updated_at) were added via the `t.timestamps` by rails. Rails will automatically add a primary key of `id` as well. 130 | 131 | If you don't know how to add fields to this file you can search for `rails migrations docs` on google and you will find the [API Docs](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) as well as the [Guide Docs](http://guides.rubyonrails.org/migrations.html) both are good resources. You might also choose to look at an old project of yours, this isn't cheating...it's using your resources effectively. 132 | 133 | 134 | Looking in the API docs we see we can add a string column by adding this line inside of our `create_table` block: 135 | 136 | t.string :email 137 | t.string :username 138 | 139 | Add those lines to your migration document now. We'll then verify they were added correctly by running: 140 | 141 | $ rake db:migrate 142 | 143 | This will create our users table and columns. To verify this we can start a new rails console and create a user: 144 | 145 | $ rails console 146 | > u = User.new 147 | > u.email = "foo@bar.com" 148 | > u.username = "schneems" 149 | > u.save 150 | 151 | If you get an error like this: 152 | 153 | > User.create(:email => "foo@bar.com", :username => "schneems") 154 | ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: email, username 155 | 156 | That's okay, copy the error into google and try to figure out why we got it, in this case we might find this [document on mass assignment security](http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity/ClassMethods.html) which will tell you that we have to whitelist attributes on our models. Open up your `app/models/user.rb` and add an `attr_accessor` to include :email, and :username. 157 | 158 | 159 | attr_accessible :email, :username 160 | 161 | You can restart your console or you can reload all of the classes by calling: 162 | 163 | > reload! 164 | 165 | Either way, you should now be able to use mass assignment to create a user: 166 | 167 | > User.create(:email => "foo@bar.com", :username => "schneems") 168 | (0.1ms) begin transaction 169 | SQL (0.7ms) INSERT INTO "users" ("created_at", "email", "updated_at", "username") VALUES (?, ?, ?, ?) [["created_at", Sun, 29 Jul 2012 19:08:02 UTC +00:00], ["email", "foo@bar.com"], ["updated_at", Sun, 29 Jul 2012 19:08:02 UTC +00:00], ["username", "schneems"]] 170 | (2.0ms) commit transaction 171 | => # 173 | 174 | 175 | By now you should be able to make new users as you please, if you cannot fix any errors you get. Save your code and commit to git. Congrats - you just made a User model. 176 | 177 | 178 | ## User Log In 179 | 180 | We mentioned before that we'll use Devise to handle the sign in and registration process of our app. We already created the model for user so we still need views, controllers, and routes to finish the login and signup process. Devise has all of these baked in. Open up the [Devise documentation](https://github.com/plataformatec/devise/) and we will follow along. Go to the "Getting Started" section. First we must add this line to our `Gemfile`: 181 | 182 | gem 'devise' 183 | 184 | Now run: 185 | 186 | bundle install 187 | 188 | Devise comes with built in generators that it adds to Rails. If you run help on rails generators now you'll see the new generators: 189 | 190 | $ rails generate --help 191 | ... 192 | Devise: 193 | devise 194 | devise:install 195 | devise:views 196 | 197 | Following along with the devise documentation we will first run: 198 | 199 | $ rails generate devise:install 200 | 201 | It will create some files, most importantly the `config/initializers/devise.rb` and it will give us some warnings: 202 | 203 | 204 | 1. Ensure you have defined default url options in your environments files. Here 205 | is an example of default_url_options appropriate for a development environment 206 | in config/environments/development.rb: 207 | 208 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 209 | 210 | In production, :host should be set to the actual host of your application. 211 | 212 | 2. Ensure you have defined root_url to *something* in your config/routes.rb. 213 | For example: 214 | 215 | root :to => "home#index" 216 | 217 | 3. Ensure you have flash messages in app/views/layouts/application.html.erb. 218 | For example: 219 | 220 |

<%= notice %>

221 |

<%= alert %>

222 | 223 | 4. If you are deploying Rails 3.1 on Heroku, you may want to set: 224 | 225 | config.assets.initialize_on_precompile = false 226 | 227 | On config/application.rb forcing your application to not access the DB 228 | or load models when precompiling your assets. 229 | 230 | It is important to read output of any new commands you are running, they often have important information. Let's follow these instructions quickly. 231 | 232 | Open `config/environments/development.rb` and add this line to it: 233 | 234 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 235 | 236 | We don't have a production url yet, but when we do we will need to set that in production.rb. 237 | 238 | Now it says we have to have `root_url` defined. But we've got a problem, we don't have any controllers yet. We can make a controller to store our homepage, the example they gave uses the `home_controller` but it could be anything. I like to have a controller called `pages_controller` in my projects, we can then use `pages#index` for our homepage, and then if we want to add additional views like about, privacy_policy, etc. we can use the `show` action to render those pages. While it makes sense to have `home#index` be the homepage, i think it makes less sense to have `home#show` render an about page. Create a pages controller now. You can either do this manually by adding a file `app/controllers/pages_controller.rb` or you can run a generator: 239 | 240 | $ rails generate controller pages 241 | 242 | For rails all, models are singular and controllers are plural. FYI - if you mess this up you can always reverse a generation by running `destroy` and trying again `$ rails destroy controller pages`, but don't run that - it will erase our controller. 243 | 244 | 245 | Now we've got a controller in `app/controllers/pages_controller.rb`, if you're making your own, it should look like this: 246 | 247 | class PagesController < ApplicationController 248 | end 249 | 250 | 251 | Super simple. Add an index action 252 | 253 | 254 | class PagesController < ApplicationController 255 | def index 256 | end 257 | end 258 | 259 | And then add the routes. In `config/routes.rb` add: 260 | 261 | resources :pages 262 | 263 | And: 264 | 265 | root :to => "pages#index" 266 | 267 | You can verify your routes by running: 268 | 269 | $ rake routes 270 | 271 | Now from MVCr on this we don't need a model, we've got our controller and our routes, so we need to add our views. Make a file `app/views/pages/index.html.erb` and put some text in it to let you know it rendered: 272 | 273 |

I AMA Homepage

274 | 275 | To verify this all worked we can spin up a rails server 276 | 277 | $ rails server 278 | 279 | And visit [http://localhost:3000](http://localhost:3000) and you should see: 280 | 281 | I AMA Homepage 282 | 283 | If you see the "Welcome Aboard" file, you can remove `index.html` from your public folder. Try refreshing, and you should now see your view instead. If you don't, go back over the steps and see what you did wrong. Use the logs and google if you run into errors. 284 | 285 | So by now we've got 2 of those suggested items in the output done. For the next open `app/views/layouts/application.html.erb` and add these two lines above the `<%= yield %>`: 286 | 287 | 288 |

<%= notice %>

289 |

<%= alert %>

290 | 291 | 292 | Finally add this line inside of your `config/application.rb` file. 293 | 294 | 295 | config.assets.initialize_on_precompile = false 296 | 297 | 298 | Now would be a good time to commit to git. We're not quite done with devise just yet. We need to tell devise which model we want to log in with to do this we will run the `rails generate devise` command we can get more information about it by running: 299 | 300 | $ rails generate devise --help 301 | 302 | Here we see that we can run `rails generate devise NAME [options]` where NAME is the name of our MODEL that we want to use devise on. In our case this is the user model so go ahead and run: 303 | 304 | $ rails generate devise user 305 | 306 | You should see some output from this like: 307 | 308 | invoke active_record 309 | create db/migrate/20120729200407_add_devise_to_users.rb 310 | insert app/models/user.rb 311 | route devise_for :users 312 | 313 | We can verify that we have a new migration, that `app/models/user.rb` has more code in it and that our `config/routes.rb` has new code in it. You can look at these files to see what code devise added. You should run 314 | 315 | 316 | $ rake db:migrate 317 | 318 | to add the new fields to your user model. You might get an error when you run this. Why? 319 | 320 | You should get this error: 321 | 322 | SQLite3::SQLException: duplicate column name: email: ALTER TABLE "users" ADD "email" varchar(255) DEFAULT '' NOT NULL 323 | 324 | Since we already have an email column on our user model, go into the last migration and comment out this line: 325 | 326 | t.string :email, :null => false, :default => "" 327 | 328 | Run the migration again. If you have users in your database with duplicate emails, you can destroy them by running 329 | 330 | $ rails console 331 | > User.destroy_all 332 | 333 | Once you've got the migrations running correctly we should be able to start our server (restart if it's already on). You can see the routes that devise added by running 334 | 335 | $ rake routes 336 | new_user_session GET /users/sign_in(.:format) devise/sessions#new 337 | user_session POST /users/sign_in(.:format) devise/sessions#create 338 | destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy 339 | user_password POST /users/password(.:format) devise/passwords#create 340 | new_user_password GET /users/password/new(.:format) devise/passwords#new 341 | edit_user_password GET /users/password/edit(.:format) devise/passwords#edit 342 | PUT /users/password(.:format) devise/passwords#update 343 | cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel 344 | user_registration POST /users(.:format) devise/registrations#create 345 | new_user_registration GET /users/sign_up(.:format) devise/registrations#new 346 | edit_user_registration GET /users/edit(.:format) devise/registrations#edit 347 | PUT /users(.:format) devise/registrations#update 348 | DELETE /users(.:format) devise/registrations#destroy 349 | 350 | 351 | Here we see that we can make a new user by visiting the [/users/sign_up](http://localhost:3000) path. To see where this view came from read the next optional section: 352 | 353 | ### FYI Rails Engines (this section is optional) 354 | 355 | You should see view containing a signup form. Devise is a Rails engine which means that it can behave much like a mini rails app within your app. These views and controllers are actually contained inside of the devise gem. 356 | 357 | To see where this code comes from you can run: 358 | 359 | $ bundle open devise 360 | 361 | You may have to specify your editor 362 | 363 | $ EDITOR=subl bundle open devise 364 | 365 | If you have sublime installed on your system, you can get the `subl` command installed by following these directions http://www.sublimetext.com/docs/2/osx_command_line.html and googling if you run into problems. If you can't get that working you can run: 366 | 367 | $ gem env 368 | 369 | To see where your INSTALLATION DIRECTORY directory is, mine is at `/Users/schneems/.rvm/gems/ruby-1.9.3-p194`. You can go to this directory (note it will be different for you). 370 | 371 | $ cd /Users/schneems/.rvm/gems/ruby-1.9.3-p194 372 | 373 | Then into the gems directory 374 | 375 | $ cd gems 376 | 377 | You can verify that devise is there by running: 378 | 379 | $ ls 380 | 381 | Once you know the version of devise you are using you can cd into that directory: 382 | 383 | $ cd devise-2.1.2 384 | 385 | Now you can run: 386 | 387 | $ pwd 388 | 389 | To get the current working directory for me it is `/Users/schneems/.rvm/gems/ruby-1.9.3-p194/gems` you can then use a text editor to open that directory. You may need to google to find out how to show hidden files and directories (like .rvm) to find the right files. You can also run 390 | 391 | $ open . 392 | 393 | in the console to open the current directory in a finder. 394 | 395 | 396 | As you can see getting the `bundle open` method to work is well worth the time, and will help quite a bit later. 397 | 398 | Now that you have the devise code open you can see where that view is stored in `devise/app/views/devise/registrations/new.html.erb`. The structure of an engine follows that of a Rails app. You can see the registration controller in `controllers/devise/registrations_controller.rb`. Pretty cool huh? 399 | 400 | You might be tempted to make modifications to these files directly, don't. They were installed as a rubygem and aren't a part of your project (those changes don't show up in your source control) therefore when you push to production you won't see those changes. Because of this, devise gives us a built in set of generators for modifying its views and controllers. We won't be doing that right now, but you can read more about it here: [Configuring Views](https://github.com/plataformatec/devise/#configuring-views) and [Configuring Controllers](https://github.com/plataformatec/devise/#configuring-controllers). 401 | 402 | This section is purely for your own info, it wasn't required...but hopefully gave you a better idea of how those views showed up on your screen. 403 | 404 | ## Back to the Required work 405 | 406 | If you skipped the Rails engine part, go back and read it some time. We'll continue building your app now. Try signing up as a user using your web interface. Once you do you should see your custom home page! 407 | 408 | You can check to make sure your user was created by looking in the console: 409 | 410 | $ rails console 411 | > User.last 412 | 413 | Sweet, by now we've got our User model, devise controllers, views and routes for registering and signing in and signing out! That's all the MVCr we need for our user for now. Let's go back to our user story and see how much of it we can do now. 414 | 415 | "A user visits the home page of the website, they then log in. They submit that link and title and then end up on a 416 | page that shows the title, link, votes for that link and any comments." 417 | 418 | Let's try this, first visit your home page [http://localhost:3000](http://localhost:3000). Now follow the next step, "log in". We haven't added any type of log in or log out functionality to our site. Maybe we should do that right now. 419 | 420 | Looking at the devise docs you'll see that we should have a `current_user` available if the user is logged in. We can use some logic to add links in our layout. Get the devise routes again by running: 421 | 422 | $ rake routes 423 | new_user_session GET /users/sign_in(.:format) devise/sessions#new 424 | user_session POST /users/sign_in(.:format) devise/sessions#create 425 | destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy 426 | user_password POST /users/password(.:format) devise/passwords#create 427 | new_user_password GET /users/password/new(.:format) devise/passwords#new 428 | edit_user_password GET /users/password/edit(.:format) devise/passwords#edit 429 | PUT /users/password(.:format) devise/passwords#update 430 | cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel 431 | user_registration POST /users(.:format) devise/registrations#create 432 | new_user_registration GET /users/sign_up(.:format) devise/registrations#new 433 | edit_user_registration GET /users/edit(.:format) devise/registrations#edit 434 | PUT /users(.:format) devise/registrations#update 435 | DELETE /users(.:format) devise/registrations#destroy 436 | pages GET /pages(.:format) pages#index 437 | POST /pages(.:format) pages#create 438 | new_page GET /pages/new(.:format) pages#new 439 | edit_page GET /pages/:id/edit(.:format) pages#edit 440 | page GET /pages/:id(.:format) pages#show 441 | PUT /pages/:id(.:format) pages#update 442 | DELETE /pages/:id(.:format) pages#destroy 443 | root / pages#index 444 | 445 | 446 | So the sign in path is accessed by the `new_user_session` helper and the register is `new_user_registration` and sign out is `destroy_user_session`. Open `app/views/layouts/application.html.erb` and add this logic to your view: 447 | 448 | 449 | <%# user is logged in, show log out link %> 450 | <% if current_user.present? %> 451 | <%= link_to 'Sign Out', destroy_user_session_path, :method => :delete %> 452 | <%# user is not logged in, show signup and login links %> 453 | <% else %> 454 | <%= link_to 'Sign In', new_user_session_path %> | 455 | <%= link_to 'Register Now!', new_user_registration_path %> 456 | <% end %> 457 | 458 | Refresh your page, you should see some of your links. If you are signed in, try signing out. Once signed out, try signing in. If you forgot your password make a new user account. 459 | 460 | Once you've verified this works, commit to git. 461 | 462 | Let's go back to our user story now, we can do our first sentence: 463 | 464 | "A user visits the home page of the website, they then log in." 465 | 466 | Great! Let's look at the next one: 467 | 468 | "They click a "submit link" button and enter a url and a title" 469 | 470 | Okay, we can't do any of that right now. Before we can, let's go back to our nouns (remember those?). We had: 471 | 472 | "user", "link", "votes", "comments" 473 | 474 | We already did MVCr for user noun (model). To do the next part we will need a "link" model of some kind. Let's make that model now. 475 | 476 | 477 | ## Link did all the work, why does Zelda get the legend? 478 | 479 | Reading though our User story we know that a link should have a url and a title. 480 | 481 | 482 | Link: 483 | url: string 484 | title: string 485 | 486 | We also get the feeling that there is a relationship between user and link. If you were going to describe your website to someone you could say correctly that a user can have many links but each link can only have one user. Because of this we need to store the relationship somehow between links and users. If you don't remember when we talked about associations you can always google or check docs. One of my favorites is the [rails association guide](http://guides.rubyonrails.org/association_basics.html). After a quick refresher, you'll see that we need a foreign key in the model that belongs_to the other model. So our link model now looks like this: 487 | 488 | Link: 489 | user_foreign_key: integer 490 | url: string 491 | title: string 492 | 493 | By default rails expects foreign keys to end with `_id` and begin with the model name it is referencing so for a foreign key that points at the user model we can name it `user_id` 494 | 495 | Link: 496 | user_id: integer 497 | url: string 498 | title: string 499 | 500 | 501 | This sounds like a pretty solid link model to me, let's generate our migration and model. Remember models are singular, and you can always run `destroy` if you misspell something. 502 | 503 | $ rails generate model link 504 | 505 | Let's open our migration and add in our fields. To simulate forgetting a field, let's leave out the title field: 506 | 507 | 508 | 509 | t.integer :user_id 510 | t.string :url 511 | 512 | Trust me, it's impossible to get your model 100% right the first time. You can always add and remove columns later as we'll see in just a second. 513 | 514 | Run: 515 | 516 | $ rake db:migrate 517 | 518 | Now we have an incomplete link model, we can verify in the console if you wish. How do we add our missing column? 519 | 520 | We'll need a new migration so run: 521 | 522 | $ rails generate migration add_title_to_link 523 | 524 | We'll get a new migration that looks like this: 525 | 526 | class AddTitleToLink < ActiveRecord::Migration 527 | def change 528 | end 529 | end 530 | 531 | Use the [Rails Active Record Docs](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) and the `add_column` method to add a string column to the links table. 532 | 533 | When you're done you should be able to run `$ rake db:migrate` and then see your column in the console. 534 | 535 | $ rails console 536 | > link = Link.new 537 | > link.title = "Schneems sets world record for teaching Rails" 538 | > link.save 539 | 540 | If you mess up and add things you didn't intend in your migration you can always see what rake tasks are available by running `$ rake -T` From there we would usually use 541 | 542 | $ rake db:rollback 543 | 544 | To go back one step or: 545 | 546 | $ rake db:rollback STEP=2 547 | 548 | to go back two steps. 549 | 550 | Once you've got a working migration it is much easier to add a new migration then to edit an old one. If you've committed a migration to source control you should not modify it unless there is a really good reason. New migrations are easy to make just run `$ rails generate migration ...` 551 | 552 | Once you've got the title attribute in your links table, add `attr_accessors` until this works in your rails console: 553 | 554 | $ rails console 555 | > reload! 556 | > Link.create(:url => "http://schneems.com", :title => "Awesome") 557 | 558 | 559 | We also want to verify that our association works. Add 560 | 561 | has_many :links 562 | 563 | and 564 | 565 | belongs_to :user 566 | 567 | In the appropriate files. You should then be able to run 568 | 569 | $ rails console 570 | > User.last.links 571 | > Link.last.user 572 | 573 | 574 | Once that works, save everything and commit to git. Let's review where we are. We've got our model but no controller, view or routes. We'll need a form for submitting new links, which means a new and create action, we also want to show that link after submission, so a show action. Let's add our routes and controller now. In `config/routes.rb` add: 575 | 576 | resources :links 577 | 578 | Then make a new controller `app/controllers/links_controller.rb`. We can start off by copying the contents of our pages controller: 579 | 580 | class PagesController < ApplicationController 581 | def index 582 | 583 | end 584 | end 585 | 586 | But then we need to change the class name and add some actions 587 | 588 | 589 | class LinksController < ApplicationController 590 | def show 591 | end 592 | 593 | def new 594 | end 595 | 596 | def create 597 | end 598 | end 599 | 600 | We will leave these blank for now, and also create 3 view files under `app/views/links` called `show.html.erb`, `new.html.erb` and `create.html.erb`. To verify they work, I like adding unique text to each and then visiting the routes manually. You can always run `$ rake routes` to find the available routes. You should see this in your output: 601 | 602 | 603 | links GET /links(.:format) links#index 604 | POST /links(.:format) links#create 605 | new_link GET /links/new(.:format) links#new 606 | edit_link GET /links/:id/edit(.:format) links#edit 607 | link GET /links/:id(.:format) links#show 608 | PUT /links/:id(.:format) links#update 609 | DELETE /links/:id(.:format) links#destroy 610 | 611 | Note: While I'm coding by myself, I usually don't make controller actions and views ahead of time like this. Typically I'll just code until I get an error that tells me I'm missing a template which I know means I need a view right there. Most of the error messages in Rails are decent at telling you what is missing. Rather than trying to avoid errors at all costs, we can let Rails remind us what we're missing. 612 | 613 | That means we can manually visit [/links/1](http://localhost:3000/links/1) and [/links/new](http://localhost:3000/links/new). Before we pre-emptively add code to our controller, let's go back to our user story: 614 | 615 | "They click a "submit link" button and enter a url and a title" 616 | 617 | So let's add a button (or link, sometimes our user stories might need updating) on the main page to a page that we can put a form on to make a new link. If you look at your existing routes and controller actions which might be good to hold a form? 618 | 619 | Hopefully you picked the `links#new` action. We can open up our `pages/index.html.erb` and add a link to the new view: 620 | 621 | <%= link_to "Add a link", new_link_path %> 622 | 623 | With your server on, refresh your home page. Click the link, and we've got half of our user story sentence complete. 624 | 625 | Now we'll need a form and some things in our `new` controller action to finish the sentence. 626 | 627 | I'll be honest with you - Rails forms were the hardest thing for me to get a handle on. My best recommendation to you is to become well acquainted with the docs, and keep in mind that at the end of the day, rails is building HTML forms. If you can't figure out how to do something, if you can find an example of it on another website you can reverse engineer the logic. I also like cheating off of old code of mine or out of rails generated scaffolding. 628 | 629 | All of coding is like an open book test, as long as it works it doesn't matter what resources you used to get there. Google, docs, old code, stack overflow, someone you met at a hackathon, or even your cat randomly hitting keys on your keyboard. So let's get our cheat on. I'm going to open up one of our old projects and copy the form out of `app/views/user/_form.html.erb`.This was generated from Rails scaffolding, so it can't be too wrong... 630 | 631 | 632 | If you don't have any other projects, you could always start a new junk project and run a scaffold generator `$ rails generate scaffold foo name:string`. You can also build your own looking at the [Action View Form Helper Docs](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html). 633 | 634 | If you chose to make your own, you can skip this next bit until the next section. If you copied out of an old project you should get something like this: 635 | 636 | <%= form_for(@user) do |f| %> 637 | <% if @user.errors.any? %> 638 |
639 |

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

640 | 641 | 646 |
647 | <% end %> 648 | 649 |
650 | <%= f.label :name %>
651 | <%= f.text_field :name %> 652 |
653 |
654 | <%= f.label :job_title %>
655 | <%= f.text_field :job_title %> 656 |
657 |
658 | <%= f.submit %> 659 |
660 | <% end %> 661 | 662 | Hopefully you shouldn't be too lost, but typing up all that code from scratch can be difficult and prone to error. Change "user" to "link" 663 | 664 | <%= form_for(@link) do |f| %> 665 | <% if @link.errors.any? %> 666 |
667 |

<%= pluralize(@link.errors.count, "error") %> prohibited this link from being saved:

668 | 669 | 674 |
675 | <% end %> 676 | 677 |
678 | <%= f.label :name %>
679 | <%= f.text_field :name %> 680 |
681 |
682 | <%= f.label :job_title %>
683 | <%= f.text_field :job_title %> 684 |
685 |
686 | <%= f.submit %> 687 |
688 | <% end %> 689 | 690 | Also change `:name` and `:job_title` to `url` and `title`: 691 | 692 | <%= form_for(@link) do |f| %> 693 | <% if @link.errors.any? %> 694 |
695 |

<%= pluralize(@link.errors.count, "error") %> prohibited this link from being saved:

696 | 697 | 702 |
703 | <% end %> 704 | 705 |
706 | <%= f.label :title %>
707 | <%= f.text_field :title %> 708 |
709 |
710 | <%= f.label :url %>
711 | <%= f.text_field :url %> 712 |
713 |
714 | <%= f.submit %> 715 |
716 | <% end %> 717 | 718 | Once you've got that saved, refresh the `links#new` page and you should get an error: 719 | 720 | undefined method `model_name' for NilClass:Class 721 | 722 | So this error isn't so great, but if you look at the line where it is pointing: 723 | 724 | <%= form_for(@link) do |f| %> 725 | 726 | We're passing in a `@link` variable we haven't initialized yet. Let's add that to our `links#new` action in the controller: 727 | 728 | @link = Link.new 729 | 730 | Refresh the page, you should now see a form. Fix any errors until you do. 731 | 732 | ## Create a Link 733 | 734 | By now we can render our view form, so we've got our model, routes, view, and most of our controller out of the way. Let's go back to our user story: 735 | 736 | 737 | "A user visits the home page of the website, they then log in. They click a "submit link" 738 | button and enter a url and a title. They submit that link and title and then end up on a 739 | page that shows the title, link, votes for that link and any comments." 740 | 741 | So now we can do the first and second sentence. What about the next one: 742 | 743 | 744 | "They submit that link and title and then end up on a page that shows 745 | the title, link, votes for that link and any comments." 746 | 747 | Let's enter a title and url and see what happens. You might end up on a blank create view, but that's not what we wanted, we actually want to populate our database. We'll have to add that to our controller. I want you to code this for yourself. You can use old code, scaffolding, or just write it for yourself. Don't forget to use logs to see the `params` available: 748 | 749 | 750 | Started POST "/links" for 127.0.0.1 at 2012-07-29 16:54:44 -0500 751 | Processing by LinksController#create as HTML 752 | Parameters: {"utf8"=>"✓", "authenticity_token"=>"b7ZKLfYSbuzzfa1/X+GU0p3sH5l0B7ekFgeXtndeLj0=", "link"=>{"title"=>"Heroku", "url"=>"http://herokou.com"}, "commit"=>"Create Link"} 753 | 754 | 755 | When you're done you should be able to enter in a title and url and find it in the console. 756 | 757 | $ rails console 758 | > Link.last.url 759 | => "http://heroku.com" 760 | > Link.last.title 761 | => "Heroku" 762 | 763 | When you can do that, save and commit to git. Looking at our user story, we're expected to be able to see the link once we've submitted. To do this, you'll want to redirect to the show action if a save is successful in the create action. 764 | 765 | Once a user lands on the show action after submitting their form, we're almost done with our user story. Save and commit to git. 766 | 767 | 768 | "They submit that link and title and then end up on a page that shows 769 | the title, link, votes for that link and any comments." 770 | 771 | This last part mentions links, votes, and comments. While we already have links, we don't have votes or comments, and this is the first time they've been mentioned. 772 | 773 | That indicates to me that we should have some more user stories before completing the rest of this one. Votes first: 774 | 775 | "A logged in user visits the home page, sees a user submitted link and votes on it. 776 | The user should end up on the home page and the vote count should change up or down 777 | according to how they voted" 778 | 779 | Now Comments 780 | 781 | "A logged in user visits the home page, clicks the comments link under a user submitted 782 | link, there they can add a comment message to that link. Other users can view that 783 | comment message by visiting the link's show page." 784 | 785 | 786 | This gives us a good starting point for next week. Make sure everything you've got is committed. 787 | 788 | ## Production 789 | 790 | Once everything works follow the appropriate directions here: https://devcenter.heroku.com/articles/rails3 to put your app on Heroku. You should be given a unique URL. You can also reference this guide: http://guides.railsgirls.com/heroku/. 791 | 792 | Email me a link to your app's url. Your final project needs to be capable of running on Heroku, now is a good time to get experience configuring an application for production. 793 | 794 | 795 | ## Fin 796 | 797 | Hopefully by now you're growing accustomed to the idea of building MVCr and then repeating. User stories can help us focus on what we need to move forward, and running into errors because we haven't implemented parts yet, can be helpful. Programming by nature is reductive. We take a problem and reduce it into smaller manageable pieces until that problem is solved. The more you do this the better you will get. You'll find some of my methods don't work well for you, or you'll find others do things slightly different. At the end of the day only two things matter, that the website behaves as you want, and that the website behaves as a user expects. As you grow, you'll likely get better at programming in general, never forget that while better code means it will be easier to maintain and change, if you neglect the user experience then your web app isn't any better. 798 | 799 | You've come a long way in your journey to becoming a Rails master. Many of you are picking up Rails as your first serious form of coding, which is quite impressive. In just a couple of short weeks we've gone from nothing to be able to make a pretty big dent in the functionality of a complex website like Reddit in just one day. 800 | 801 | 802 | Go celebrate by visiting http://www.reddit.com/r/funny/ for a bit (caution potentially strong language). -------------------------------------------------------------------------------- /part_three.md: -------------------------------------------------------------------------------- 1 | ## Reddit on Rails: A Hero's Journey Part 3 2 | ## Last Week 3 | 4 | Last week in our [UT on Rails Course](http://schneems.com/ut-rails), we prototyped and built two new models along with some complex VCr behind the scenes. While our site could certainly use some polish, it's beginning to have some of the base features we would expect from a service similar to [Reddit](http://reddit.com). Hopefully you're feeling a bit more confident in your ability to dissect a user story and pull out the needed elements to build a model. If you don't get it right the first time, you can always rollback or add columns. 5 | 6 | 7 | ## This week 8 | 9 | Since we've been focusing on getting our barebones features to work, this week we will work on some polish. We'll add some styling, do some testing to ensure we don't accidentally break our site, add a site search, and talk a little about how you could move forward with the site. 10 | 11 | 12 | ## Make it Pretty 13 | 14 | Looking at [Reddit](http://reddit.com) you might not think that looks matter too much, but users consistently will choose to use a more attractive service if everything else is equal. If you've ever gone to a hackathon, or other competitive coding event, you're likely used to the winners having a polish, flashy UI. If you're building a professional quality web app, I recommend hiring a designer to help with wire-frames, comps, and assets generation. 15 | 16 | While many sites might look like they're not designed (google, facebook, etc.) they put a great deal of time, money, and energy in maintaining that clean look. If you're interested in learning a little about design I recommend [the non designers design book](http://www.amazon.com/The-Non-Designers-Design-Book-Typographic/dp/1566091594) for an intro to design basics, for css I recommend anything by [Khoi Vinh](http://www.subtraction.com/2010/11/05/i-wrote-a-book), [Dan Cederholm](http://simplebits.com/#books), or [Zeldman](http://www.zeldman.com/). 17 | 18 | Design, like programming, isn't something you can just pick up over night, it takes practice and effort to get good at. If you're really interested in this stuff, find a local user group, buy books, subscribe to blogs, and work at it. 19 | 20 | Don't forget there is more to design than just looking pretty. Interaction and usability design is crucial for having a good application. You can go just as deep into usability as visual design. As a rails developer, you don't need to be the best at all of these things, but if you have a wide breadth of knowledge you will be more successful in coding and in life. Even if you never have to design a day in your life, understanding how to work with those that do will. 21 | 22 | 23 | 24 | ## Bootstrap 25 | 26 | We will be using a CSS framework called [bootstrap](http://twitter.github.com/bootstrap/), released and by [Twitter](http://twitter.com). It is a collection of components that make it easier for people to design websites. Like Rails, they've decided on a set of defaults that you can change. There are a number of components but my favorite feature of bootstrap is a grid system. [Grids have been used to layout webpages](http://en.wikipedia.org/wiki/Grid_(page_layout) and other media for some time, but html and css have never had a built in grid system. Bootstrap helps us out by giving us one using standard css classes and html tags. 27 | 28 | If you have bootstrap css loaded from http://twitter.github.com/bootstrap/assets/css/bootstrap.css then you could do something like this: 29 | 30 | 31 |
32 |
...
33 |
...
34 |
35 | 36 | And the result is that you have different elements of different column sizes (span4 and span8) inside of a row. You can go to town with this your grid system, for more info check out http://twitter.github.com/bootstrap/scaffolding.html#gridSystem. To do this next part you don't need to know bootstrap, but the more you know the easier it will be. 37 | 38 | 39 | ## Boilerplate Bootstrap Layout 40 | 41 | You can cheat a bit and replace the code in your `app/views/layouts/application.html` layout with this: 42 | 43 | 44 | 45 | 46 | 47 | 48 | RedditOnRails 49 | 50 | 51 | 52 | <%= csrf_meta_tags %> 53 | 54 | 55 | 56 | 57 | 58 | <%= stylesheet_link_tag "application", :media => "all" %> 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 106 | 107 |
108 |
109 |
110 | 111 | <%= yield %> 112 |
113 |
114 |
115 | <%= javascript_include_tag "application" %> 116 | 117 | 118 | 119 | 120 | 121 | Browse over the code and try to make sense of it. We're loading the styles at the top of the page before any body content, and javascript last so the page appears to load faster and our dom is fully available by the time the javascript gets loaded. We have some Heroku branding that you can take or leave, but I think it looks nice. Take a look at the classes specified in the layout, these are what you can use to control the look and flow of your website with bootstrap. 122 | 123 | 124 | 125 | 126 | ## Add the Flash 127 | 128 | It is important that you don't forget the flash message in your layout I add this code to show a flash messages right above my `<%= yield %>` in the `application.html.erb` layout: 129 | 130 | <% [:notice, :error, :alert].each do |level| %> 131 | <% unless flash[level].blank? %> 132 |
133 | × 134 | <%= content_tag :p, flash[level] %> 135 |
136 | <% end %> 137 | <% end %> 138 | 139 | 140 | 141 | ## Sidebar 142 | 143 | You might have noticed this curious line of code: 144 | 145 | 146 | <%= yield :sidebar %> 147 | 148 | 149 | We already have a yield on the page so what is up with this `yield :sidebar` ? Rails gives us the ability to render from a variety of locations using yield and `content_for`. You can put content in that area from any page. For example if you want the "add a link" link to go in the sidebar from the main page you could do something like this: 150 | 151 | <%= content_for :sidebar do %> 152 | <%= link_to "Add a link", new_link_path %> 153 | <% end %> 154 | 155 | Now when you refresh the link will show up in the sidebar. Using named yield like this is a great way to keep your views clean and to allow individual files to add content to layouts. If you didn't want to show that whole section when no sidebar content was available you can check to see if any content has been written. 156 | 157 | <% if content_for?(:sidebar) %> 158 |
159 | 160 |

Sidebar

161 | <%= yield :sidebar %> 162 |
163 | <% end %> 164 | 165 | Now the sidebar will only be shown if you are rendering content for it. Chances are we always want to show content in the sidebar so this might not be needed, but just remember that you can always check for presence of content from a `content_for`. 166 | 167 | In addition to sidebars I usually have a number of these yields in my layout including but not limited to 168 | 169 | <%= yield :head_stylesheets %> 170 | <%= yield :header %> 171 | <%= yield :footer %> 172 | <%= yield :js %> 173 | <%= yield :analytics %> 174 | 175 | You can also use it to set the `id` and `class` of your main body with something like this: 176 | 177 | 180 | 181 | This can be useful for styling your whole layout differently depending on the page the user is viewing and if they are logged in or not. I also like to add the controller action and name to the body id by default. By adding in this hook, you could change the styling of your page later in an individual view file without needing a separate layout. 182 | 183 | If you call content_for multiple times it will add multiple elements, this can be very convenient behavior. Try it out now by adding multiple links to the sidebar 184 | 185 | <%= content_for :sidebar do %> 186 | <%= link_to "Add a link", new_link_path %>
187 | <% end %> 188 | 189 | <%= content_for :sidebar do %> 190 | <%= link_to "Awesome Blog", 'http://schneems.com' %> 191 | <% end %> 192 | 193 | Of course you could have just put both of those links in one `content_for` but sometimes it is nice to add content from different places and know that it is just getting added and won't over-ride the previous values. This is one of my favorite Rails features, and I don't see it used very much. If you find your view files becoming very large and the need for multiple layout files, consider if you might be able to dry some code up using `yield` and `content_for`. 194 | 195 | Now that we've got our site looking like we want it to, let's add a piece of functionality that has come to be expected in just about every website these days. Search! 196 | 197 | 198 | ## Search it! 199 | 200 | Lucky for us we're working with the ever so awesome [Postgres](http://postgresguide.com/) database in production on Heroku which has built in full-search capability. While one option could be matching strings with a `like` query: 201 | 202 | $ rails console 203 | > Link.where("title like '%ruby programming%'") 204 | 205 | There is a better way. This is a very expensive, slow and not very accurate method for search. There are several alternatives out there to the built in postgres full-text search such as [web solr](https://addons.heroku.com/websolr) and [elastic search](https://addons.heroku.com/bonsai). If you have high load on your database it might be a good idea have your full text search on another service, though it would require another service on your machine. Heroku provides a free shared Postgres database in production so that makes using Postgres full text search an even better fit. They also offer many different tiers of [production postgres instances](https://postgres.heroku.com/pricing) when you're ready to grow. 206 | 207 | It's always a good idea to keep your production and development environments as close as possible, so you'll want to use Postgres locally. Make sure that you've got a version of postgres of 9.1 or above locally: 208 | 209 | $ postgres --version 210 | postgres (PostgreSQL) 9.1.2 211 | 212 | If you don't have postgres you will need to [install postgres locally](https://devcenter.heroku.com/articles/local-postgresql). 213 | 214 | You'll also need to have the `pg` gem in your `Gemfile` 215 | 216 | gem 'pg' 217 | 218 | And remove your sqlite gem. Then `$ bundle install`. Next specify the appropriate database adapter in your `config/database.yml`: 219 | 220 | development: 221 | adapter: postgresql 222 | encoding: utf8 223 | database: reddit_on_rails 224 | pool: 5 225 | host: localhost 226 | 227 | 228 | If you're just switching over to Postgres now, you will lose any data you had stored in sqlite. This shouldn't be a problem, it just means you'll have to sign up and add some more links afterwards. Create a database by running: 229 | 230 | $ rake db:create 231 | 232 | Now we need our schema in the database so you can run 233 | 234 | $ rake db:migrate 235 | 236 | You should be set now, you can double check by restarting your rails server or opening up your console and performing a query. 237 | 238 | While we could search all kinds of things, lets focus on searching for links right now, specifically the title of links. We could do this writing our own sql, but whenever you're doing something you feel might be fairly common it is always a good idea to research if there are any libraries you can use. To make using full text search with Postgres easier I decided to use the [Textacular Gem](https://github.com/textacular/textacular). 239 | 240 | To get started add it to your Gemfile: 241 | 242 | gem 'textacular', require: 'textacular/rails' 243 | 244 | Then run: 245 | 246 | $ bundle install 247 | 248 | Now you should be ready to go, fire up a console session and try it out. The gem puts a method `search` on all ActiveRecord objects. 249 | 250 | $ rails console 251 | > Link.create(:title => "UT on Rails", :url => "http://schneems.com/ut-rails") 252 | > Link.search(:title => "rails") 253 | => [# Link.search(:title => "rails").to_sql 259 | => SELECT "links".*, ts_rank(to_tsvector('english', "links"."title"::text), to_tsquery('english', 'rails'::text)) 260 | AS "rank0.19344968583449806" FROM "links" WHERE (to_tsvector('english', "links"."title"::text) @@ to_tsquery( 261 | 'english', 'rails'::text)) ORDER BY "rank0.19344968583449806" DESC 262 | 263 | As you can see from the query there is quite a bit going on here, but at the end of the day the `textacular` gem is using SQL that Postgres can read in and output a search. Don't expect this sql to work on another RDBMS like mysql, it is custom to Postgres. Back in the day people used to strive for cross database compliance on websites (the ability to switch between mysql and postgresql for example) but most people have realized that switching once a site has already been deployed to production is fairly un-common, and also these custom features are awesome. Postgres has some amazing custom features like the key-value column type [hstore](http://schneems.com/post/19298469372/you-got-nosql-in-my-postgres-using-hstore-in-rails). 264 | 265 | If you're not convinced that using full-text search with Postgres (through the textacular gem). It can do many advanced things, one of the most useful is text stemming. Let's say we've got a link: 266 | 267 | > Link.create(:title => "the home of richard schneeman", :url => "http://schneems.com") 268 | 269 | If you were to search for `schneemans`, which means snowmen in German, in google you would expect to find this link, but we can't with a `like` query: 270 | 271 | > Link.where("title like '%schneemans%'") 272 | ==> [] 273 | 274 | Now if you try the search method, postgres will stem `schneemans` and return a link that contains a common lexical root `schneeman` in a word: 275 | 276 | > Link.search(:title => "schneemans") 277 | => [# 'search#index' 315 | 316 | And add an index method to `app/controllers/search_controller.rb` 317 | 318 | def index 319 | end 320 | 321 | Finally make a new view file in `app/views/search/index.html.erb`. It might be tempting to re-use your home-page since it's already displaying a list of links, but I would rather start off writing duplicate view code and then optimize later by refactoring to just one view instead of trying to shoe-horn two different view features into one another. This doesn't mean it is a bad idea, just that it might not be until you're 5 nested if statements deep into your one template that you realize you really needed two templates. Most gross view code i've seen is the result of premature "optimization". While Rails incourages you to be DRY (don't repeat yourself) there is a limit to this motto, as a rubyist once said you want to stay DRY but just don't get chaffed. 322 | 323 | Now that we've got the basics we can add a search form. This is the first form in this entire course that hasn't been backed by a model. In the past we've used `form_for` and always passed in an object such as `@user` or `@link` so how do we build a form using rails without an object from our database? 324 | 325 | If you googled the question you'll find that there is another view helper called `form_tag` we can use. It accepts a path as its first argument, in this case we want it to go to our `search_path` (SearchController#index). Then since we're not using a `form_for` we will have to use tag helpers to generate our form fields. So instead of `f.text_field` we will use `text_field_tag`. The final product would look like this: 326 | 327 | <%= form_tag(search_path, :method => "get", :style => "margin: 5px 0 0 0") do %> 328 | <%= text_field_tag(:q, nil, :placeholder => "search") %> 329 | <%= submit_tag("Search") %> 330 | <% end %> 331 | 332 | 333 | It is very important that you include the `:method => "get"` in this form helper otherwise the default of forms it to submit POST requests. If you're confused by the difference between `form_for` and `form_tag`, don't worry I was confused for a really long time. I just try to remember that `form_for` is super exclusive and requires a database object while kids playing `tag` are happy to let anyone join in on the fun. It might not be the best way to remember, but when in doubt look at the docs, especially the [Rails form guides](http://guides.rubyonrails.org/form_helpers.html). 334 | 335 | Add the above code to your layout so a user can search from anywhere on your site. Refresh your page and put in a search term like "foo". Hit enter and take a look at your logs. You should see something like this: 336 | 337 | Started GET "/search?utf8=%E2%9C%93&q=foo&commit=Search" for 127.0.0.1 at 2012-10-03 21:54:45 -0400 338 | Processing by SearchController#index as HTML 339 | Parameters: {"utf8"=>"✓", "q"=>"foo", "commit"=>"Search"} 340 | Rendered search/index.html.erb within layouts/application (0.3ms) 341 | Completed 200 OK in 9ms (Views: 8.0ms | ActiveRecord: 0.0ms) 342 | 343 | Here you'll see that we hit the right controller and action and that our search term came through under the param "q". Why is that? If you'll remember in our search form we had a `text_field` using the `:q` symbol like so: 344 | 345 | <%= text_field_tag(:q) %> 346 | 347 | **Note:** The "nil" (refers to the default value) and ":placeholder => 'search'" is optional 348 | 349 | This maps to a top level param of `:q` being sent to the server. While you don't have to use `q` you could use "query", "search_term", or "anything_you_want"; though `q` is short and fairly common for legacy reasons in search actions on websites. So why break with tradition? 350 | 351 | Now that our form works, we'll want to finish writing our controller action and view. Since we know we're sending the query over in `params[:q]` it would be pretty easy to search our database like this: 352 | 353 | @links = Link.search(:title => params[:q]) 354 | 355 | Now that we've got an array of links let's add them to our view: 356 | 357 | Here are all the links that matched "<%= params[:q] %>" 358 | 381 | 382 | 383 | Save your controller and your index and search for something that should be in your database. You can add a fresh link if you like: 384 | 385 | $ rails c 386 | > Link.create(:title => "Homepage of the Internet", :url => "http://reddit.com") 387 | 388 | Then search for it with something like "internets homepage" you should see your link. You can play around with different variations and you'll see that Postgres isn't fool-proof. For example searching for "home page of the internet" got me nothing. Search is an inherently hard problem and while humans are great at picking out similar or "fuzzy" matches, machines need all the help they can get. You'll find for any generic searching storage solution there will be quite a bit of configuration, if your site depends largely on search, take some time researching your options and once you've picked a storage solution, spend some time researching how to optimize and configure it. For us the default configuration should work just fine, besides we can't make the search "too good" otherwise it wouldn't be a true reddit clone (reddit search is famously horrible). 389 | 390 | Now that search works you want to make sure that user's don't get overwhelmed with links and that your server isn't crushed by pulling too many entries from your database. You can do this by adding pagination. Since the `search` method returns and `ActiveRecord::Relation` we can just use methods from the `will_paginate` library that we used in the PagesController. 391 | 392 | 393 | @links = Link.search(:title => params[:q]).page(params[:page]).per_page(20) 394 | 395 | Then you just need to add the will_paginate view helper and you should be good to go! Once you've got that working and committed, let's make sure we don't accidentally break our site. Lest we end up having to show our users this: 396 | 397 | ![reddit is down](http://f.cl.ly/items/2F0Z1h3f2e2l2s1h0c1X/Screen%20Shot%202012-10-27%20at%2010.52.31%20AM.png) 398 | 399 | ## Testing 400 | 401 | The single best thing you can do to improve the quality of code you write, is to test it. Tests have some negative connotation in society. Being "put to the test" isn't exactly a good thing, who wants their patience tested, and no one celebrates when a teacher announces "big test tomorrow!". The testing we do in programming is different, failing a test is just as important as passing. What exactly is a test, and why to they help? Let's take a step back and consider our morning routines. 402 | 403 | When you wake up in the morning you likely have a routine that might involve brushing your teeth. You don't know it but while you're doing this task you're constantly running hundreds of small tests in your head to make sure things are going as planned. When you pick up the toothpaste, you might check the label for a certain slogan, or wait for a hint of minty freshness to waft through the air before you put it on a brush and stick it in your mouth. This behavior is natural, almost instinctual, and is certainly a good thing. If you didn't perform these seemingly pointless checks, you might one day find yourself with a mouth full of hair creme, or athlete's foot medication. As you drive, you check your speed, as you write you check your spelling. It is only natural that when you code you check your code. Unlike driving a car or brushing your teeth, coding is unique in that we can write automated tests that perform these checks for us faster and more consistently than we can. 404 | 405 | What might you want to test? Does your app rely on new users signing up? Perhaps you should test that. What about accepting a payment or completing a sale, you should definitely check that. Take a step back and consider the elements that are important to your application and then make sure you don't get the digital equivalent of a mouth full of foot creme by breaking signup and not realizing it for several days. 406 | 407 | Tests are great for verifying that the code we write is actually solving the problem we want it to solve. Even better it serves as a living documentation of the intent of the code. If you see some gnarly code someone else wrote and want to fix it, how do you know you got it right? If that code is tested, go ahead and make your changes, run the tests to see if they still pass and you're good to go. If you get a failure, not to worry, the test can help guide you on what is missing. Tests can be like living specification over what the code is _supposed_ to do. 408 | 409 | Enough with the talk, let's dive into writing a test. We can use our user stories to help us test, if you go back to the first one we made: 410 | 411 | "A user visits the home page of the website, they then log in. They click a "submit link" 412 | button and enter a url and a title. They submit that link and title and then end up on a 413 | page that shows the title, link, votes for that link and any comments." 414 | 415 | It's likely when you coded this feature you manually tested it to make sure it was working. It's always good to verify manually, but let's add an automated test so that no matter what we change, we can always verify this feature is still working like we expect. 416 | 417 | 418 | ## Integration Testing with Capybara 419 | 420 | To accomplish this type of test we will be using an "integration" testing tool called Capybara. The reason this testing is "integration" is we are testing all of our components (as they are integrated) together: MVC(r). Capybara can actually run a headless version of a browser and then simulate different user actions, such as filling out forms and clicking on links. 421 | 422 | Add capybara to your gemfile: 423 | 424 | group :test do 425 | gem 'database_cleaner' 426 | gem 'capybara', '~> 1.1.2' 427 | end 428 | 429 | We'll also need to use the `database_cleaner` gem, you can read the [Capybara docs](https://github.com/jnicklas/capybara) for more info. Run `$ bundle install` and then add this code to the end of your `test/test_helper.rb` file: 430 | 431 | 432 | require 'capybara/rails' 433 | 434 | # Transactional fixtures do not work with Selenium tests, because Capybara 435 | # uses a separate server thread, which the transactions would be hidden 436 | # from. We hence use DatabaseCleaner to truncate our test database. 437 | DatabaseCleaner.strategy = :truncation 438 | 439 | class ActionDispatch::IntegrationTest 440 | # Make the Capybara DSL available in all integration tests 441 | include Capybara::DSL 442 | 443 | # Stop ActiveRecord from wrapping tests in transactions 444 | self.use_transactional_fixtures = false 445 | 446 | teardown do 447 | DatabaseCleaner.clean # Truncate the database 448 | Capybara.reset_sessions! # Forget the (simulated) browser state 449 | Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver 450 | end 451 | end 452 | 453 | You can use Capybara with a number of different testing frameworks, we will use the built in framework that Rails provides. Even if you chose to use a different framework later, the concepts are very similar. 454 | 455 | Create a `test/integration` folder if it does not already exist. This is where our Capybara tests will live. They don't have to go in that folder, but it helps if we are all on the same page. We are going to test our user story now. 456 | 457 | 458 | "A user visits the home page of the website, they then log in. They click a "submit link" 459 | button and enter a url and a title. They submit that link and title and then end up on a 460 | page that shows the title, link, votes for that link and any comments." 461 | 462 | We have a few distinct steps, but over all we are going to test the creation of links. We want the file name of our test to reflect this so we can name it `link_create_test.rb` making sure that it ends in `_test.rb` this is how Rails (and `Test::Unit`) knows it is a test. Inside of that test add this skeleton code: 463 | 464 | 465 | require 'test_helper' 466 | 467 | class LinkCreateTest < ActionDispatch::IntegrationTest 468 | 469 | end 470 | 471 | 472 | Notice that we are inheriting from `ActionDispatch::IntegrationTest` which is the class we modified in our `test_helper.rb`. Since we inherit from this class we can define tests by adding a test block that looks like this: 473 | 474 | test "the world is round" do 475 | assert true 476 | end 477 | 478 | Without adding any more code, go ahead and run your tests. You can do this in a number of ways, you could run: 479 | 480 | 481 | $ rake test 482 | 483 | You may need to add `bundle exec` before `rake test`. You should get an output similar to this: 484 | 485 | # Running tests: 486 | 487 | Finished tests in 0.004353s, 0.0000 tests/s, 0.0000 assertions/s. 488 | 489 | 0 tests, 0 assertions, 0 failures, 0 errors, 0 skips 490 | Run options: 491 | 492 | # Running tests: 493 | 494 | Finished tests in 0.002594s, 0.0000 tests/s, 0.0000 assertions/s. 495 | 496 | 0 tests, 0 assertions, 0 failures, 0 errors, 0 skips 497 | Run options: 498 | 499 | # Running tests: 500 | 501 | . 502 | 503 | Finished tests in 0.037623s, 26.5795 tests/s, 26.5795 assertions/s. 504 | 505 | 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips 506 | 507 | 508 | You can see that we had one test, with one assertion and zero failures, errors, or skips. Since testing is so important it is the default rake task, you can get the same response by running: 509 | 510 | $ rake 511 | 512 | 513 | Congrats, you just wrote a passing test, but it doesn't tell us much, let's have it follow our user story. First we need to log in as a user. Make a new test: 514 | 515 | 516 | test "logged in user submits valid links" do 517 | end 518 | 519 | 520 | 521 | We need to simulate a user creating a link. Since we don't care about the signup process for this test we can just make a user directly in our database, and then walk through the sign in process. First we will create a user with a random unique email: 522 | 523 | user = User.create(:email => "#{Time.now.to_f}@example.com", :password => "password") 524 | 525 | **Note:** It needs to be a unique email otherwise it won't save to the database and your test will likely fail accidentally 526 | 527 | Try running your rake task to make sure you get no errors. Next we need to visit the user sign in page. We can use capybara's `visit` method and pass in a path using our rails routes: 528 | 529 | visit new_user_session_path 530 | 531 | You can manually specify a path if you want, like this: 532 | 533 | visit 'users/sign_in' 534 | 535 | But it is more brittle in the long run. Try running your rake task to make sure you get no errors. 536 | 537 | Next we can verify we are on the page we expect 538 | 539 | assert_equal '/users/sign_in', current_path 540 | 541 | 542 | or 543 | 544 | assert_equal new_user_session_path, current_path 545 | 546 | 547 | The code `assert_equal` takes to entries and compares them together if they are equal it will pass the test, otherwise it will fail the test. You may wonder why we didn't just use something like this: 548 | 549 | 550 | assert '/users/sign_in' == current_path 551 | 552 | The method `assert_equal` knows that you expect the current_path (the second argument) to equal `'/users/sign_in'`. If it does not it will fail and tell you the value of expected versus actual. Using `assert` will only tell you that they did not equal and cannot give you the two different values. Basically `assert_equal` gives you more information and a better test experience in this case. 553 | 554 | It is nice to verify and test each step of the way, so we don't get any unexpected results, your code should look like this: 555 | 556 | require 'test_helper' 557 | 558 | class LinkCreateTest < ActionDispatch::IntegrationTest 559 | 560 | test "logged in user submits valid links" do 561 | user = User.create(:email => "#{Time.now.to_f}@example.com", :password => "password") 562 | visit new_user_session_path 563 | assert_equal '/users/sign_in', current_path 564 | end 565 | 566 | end 567 | 568 | Save and run tests, they should be passing, if not fix your test or your code. At any time you want capybara to show you what it is doing you can add this line in to debug: 569 | 570 | save_and_open_page 571 | 572 | Go ahead and try it: 573 | 574 | 575 | require 'test_helper' 576 | 577 | class LinkCreateTest < ActionDispatch::IntegrationTest 578 | 579 | test "logged in user submits valid links" do 580 | user = User.create(:email => "#{Time.now.to_f}@example.com", :password => "password") 581 | visit new_user_session_path 582 | assert_equal '/users/sign_in', current_path 583 | save_and_open_page 584 | end 585 | 586 | end 587 | 588 | 589 | The sign in page should show up in your browser, this is a good way to make sure your test is going as planned. Now we are on the sign up page we need to fill in the email and password for our user. We will need to target the text boxes so we can tell capybara where to fill in text, view source on the page and grab the id's of the text input elements. Now we can capybara to fill in the email: 590 | 591 | fill_in 'user_email', :with => user.email 592 | 593 | 594 | This will target the element with an id of `user_email` and add in the email our user has in the database. Now we need to fill in the password: 595 | 596 | fill_in 'user_password', :with => 'password' 597 | 598 | and finally submit the form: 599 | 600 | click_button 'Sign in' 601 | 602 | Since a valid sign in will result in a different page and an invalid sign in will return the user to the same page we can add an assertion to make sure the sign in worked 603 | 604 | 605 | refute_equal new_user_session_path, current_path 606 | 607 | Where we are saying the `current_path` should not be equal to the user sign in page. Your code should now look something like this: 608 | 609 | require 'test_helper' 610 | 611 | class LinkCreateTest < ActionDispatch::IntegrationTest 612 | 613 | test "logged in user submits valid links" do 614 | user = User.create(:email => "#{Time.now.to_f}@example.com", :password => "password") 615 | visit new_user_session_path 616 | assert_equal '/users/sign_in', current_path 617 | 618 | fill_in 'user_email', :with => user.email 619 | fill_in 'user_password', :with => 'password' 620 | 621 | click_button 'Sign in' 622 | refute_equal '/users/sign_in', current_path 623 | 624 | save_and_open_page 625 | end 626 | 627 | end 628 | 629 | Save and run the tests. Your tests should pass, and you should now be on the homepage as a logged in user. You now need to visit `new_link_path`, enter a link and a url, and then verify they saved correctly. 630 | 631 | 632 | first 633 | 634 | visit new_link_path 635 | 636 | then 637 | 638 | title = "Random Title: #{Time.now.to_f}" 639 | fill_in 'link_title', :with => title 640 | 641 | 642 | finally 643 | 644 | fill_in 'link_url', :with => 'http://schneems.com' 645 | click_button 'Create Link' 646 | 647 | Now we want to verify the page and the content: 648 | 649 | 650 | link = Link.last 651 | assert_equal link_path(link), current_path 652 | 653 | assert has_content?(link.title) 654 | assert has_content?(link.url) 655 | 656 | 657 | Save and run your tests, fix them or your code until they pass. You might be tempted to add hundreds of assertions to one test, but don't go overboard just yet. Though this test seems straightforward it might be brittle in very subtle ways, for instance if we change the user sign in path all the assertions to `'/users/sign_in'` would fail. Ideally you want to make the minimum number of assertions required to verify behavior, but beyond that you're just making your tests more brittle. This is a hard balance to achieve, you will get better at it over time the more you write and run your tests. If your test keeps failing even though the feature still works, it is a good indication that it is too brittle. If your test does not fail, but a feature breaks, you likely need to add extra code. 658 | 659 | 660 | Another thing to consider when writing tests is speed and maintainability. Tests are code too and need to be maintained, duplicating testing logic, or over testing areas of your app will end up with a very large very slow test suite that is difficult to maintain. As you advance in your testing knowledge you will likely come to love testing all sorts of code and maybe even adopt TDD (test driven development). It is important to remember that tests are a means to an end. Tests are there to help you with refactoring, feature stability, and for ensuring interfaces (more on this later) are consistent. If a given test or suite of tests ever starts to impair your ability to run your code, you should refactor or remove those tests. 661 | 662 | ## Integration is a Good Thing 663 | 664 | Some people detest integration tests as they claim they are slow (they aren't) and they are brittle. Brittle means they break even when there is no user facing breakage. If you look over our test code we could introduce a breakage simply by changing our html, by specifying a different element `id` for instance. To this I say: use conventions. Designers should never style using html id's (that's a whole different talk) and if you're a developers should have no reason to change an element's id, so I say those are pretty safe. Other areas like targeting specific text "Sign in" can be brittle, so I often add an id to the to make testing easier `:id => "signUserInButton"`. Even with precautions your tests might suddenly break and give you a false positive. When this happens, just fix it and talk to your team to try to find out why it broke and to come up with some better conventions or training so that it rarely happens in the future. 665 | 666 | Integration tests take longer to run each test since it requires spinning up a headless webserver and actually interacting with forms. How on earth could it not be "slow" then? Since you're testing the full stack you can get away with fewer tests, and your tests will be much more "human" like and capable of testing real failures. I've worked on projects obsessed with unit tests (small tests for individual methods) that took 30 minutes or more to run, that did nothing to catch several crippling bugs deployed to production. My gem [oPRO](https://github.com/opro/opro) almost exclusively uses Integration tests, runs in under 2 minutes total and has over 90% code coverage (pretty decent). 667 | 668 | 669 | ## Breaking code 670 | 671 | 672 | Now that you've got a test, it is important that you run it frequently to check to make sure you didn't accidentally break something. At very least, I run my test suites before deploying to production on [Heroku](http://heroku.com). If i'm working with a team, I run my tests before I push my changes up to my shared repository (usually on Github). Let's experiment with changing our code to break our tests to see what happens. Open up `app/controllers/link_controller.rb` and comment out everything in the `create` action. Now run your tests again and see what happens. When I run this test I get an output like this: 673 | 674 | test_logged_in_user_submits_valid_links(LinkCreateTest) [/test/integration/link_create_test.rb:25]: 675 | <"/links/980190962"> expected but was 676 | <"/links">. 677 | 678 | 679 | This tells me exactly what line my test failed at which happens to be this one: 680 | 681 | 682 | assert_equal link_path(link), current_path 683 | 684 | It doesn't tell me exactly what is wrong, but it does give me a hint. Since we know everything up to this point worked, we can add a debugging `save_and_open_page` or we could tail our development logs while running the test again: 685 | 686 | 687 | $ tail log/test.log 688 | 689 | 690 | A test will tell you where there is a problem, but it won't always point you to the exact point, see if you can use debugging skills to determine that the problem lies in your `links_controller.rb`. 691 | 692 | 693 | ## Interfaces and Tests 694 | 695 | What we just tested here was the interface between your user and the application. What is an interface? It is the point of contact between a human and a technology. A car's interface is a steering wheel and gas pedal. A phone's would be the buttons (or touch screen) and speakers. Just like those physical technologies, our digital ones have interfaces. For our web app, the interface is a browser. More specifically we are using URLs and form elements within a browser to manipulate our application. While we are used to interfaces and technologies being fixed, if you've ever used a hammer before, it's likely you'll still use it the same way a year from now. But digital technologies are slightly different because the interface is just a representation of the underlying application. If we change our html, we can still have the same effective app, but our interface changed. Similarly we can change our app without changing the interface. 696 | 697 | So when we use Capybara to navigate to a web page, click on buttons and fill in elements, we aren't really testing our application code directly, but through this web interface. I like testing my web apps like this because it is how users will interact with an app, and if you do something to trivial that would impact a user like accidentally removing a link, integration tests can help you catch the mistake (sometimes). There are other types of tests, you can test controllers by themselves with functional tests where the interface is a controller method such as `create` a HTTP verb such as `POST` and params such as `{:title => "schneems"}`. You can test your routes, and you can also test your models directly using unit tests. After integration tests, unit tests tend to be my favorite, they don't test the interface a user will use, but rather they test coding interfaces. When you define a method name that takes arguments, you are defining an interface for calling the code in that method, if you change the interface (changing the method name for examlple) then you will break other code unless it changes too. As you get more advanced in coding, unit testing can greatly help you cleanly define these "interfaces". 698 | 699 | 700 | We only tested one of our user stories, but you could easily test all of them. Isolate the behavior you want to test, then move towards the goal step by step as a user would. 701 | 702 | It is hard to show just how important tests are in a walk-through like this since there aren't stakes on the line. If you deploy a bug, you don't have hundreds of users that you are letting down. You don't have any co-workers you can upset by breaking features. Just remember that we play like we practice. If you treat these examples like a production quality application, it means when you deploy your baby to the world (wide web) it will be in good, tested, hands. 703 | 704 | To put it one way: I know a lot of _bad_ programmers who _don't_ test their code much, but I don't know any programmers that test their code frequently and are still bad. Testing is immediate feedback in code form. If you're going to work on open source Ruby projects, tests are a requirement. 705 | 706 | 707 | ## Does Search Work? Are you sure search works? 708 | 709 | Since our site's search is much better, we'll assume that our user's will rely heavily on it and that they would be really bummed if something happened and it quit working. To make sure nothing happens to our precious feature let's write a test for it. Not only will this help us to catch bugs, but it can also help us experiment with different search back ends or configuration if we wanted. 710 | 711 | Make a new file `test/integration/search_test.rb`. I typically cheat when making new test files and just copy the contents of another test file if it exists, you could copy `link_create_test.rb` and then make a few changes: 712 | 713 | require 'test_helper' 714 | 715 | class SearchTest < ActionDispatch::IntegrationTest 716 | test "search for item in database works" do 717 | 718 | end 719 | end 720 | 721 | Now we need to go back to our user story 722 | 723 | "A user visits our site, enters a search for 'Internet homepage' and is directed to a page that 724 | includes a link to 'reddit.com'" 725 | 726 | 727 | Simple enough, let's start out by visiting the home page. Go ahead and write the code for that while I grab a drink, remember you can cheat off of your old test file. You can use `save_and_open_page` at the end of the test to verify that you're on the right page if you want. 728 | 729 | Now would be a good time to verify that you're using Postgres in your `test` environment: 730 | 731 | test: 732 | adapter: postgresql 733 | encoding: utf8 734 | database: reddit_on_rails_test 735 | pool: 5 736 | host: localhost 737 | 738 | Also verify that all migrations on the test environment have been run: 739 | 740 | $ bundle exec rake db:migrate RAILS_ENV=test 741 | 742 | You can run all the tests by running: 743 | 744 | $ bundle exec rake test 745 | 746 | Or just that specific file by running 747 | 748 | $ rake test TEST=test/integration/search_test.rb 749 | 750 | Once that works you want to make a link in your database with a given title (you can just use `Link.create` for simplicity). Then you want to target the search field with capybara and enter in a query that is similar but not identical to the title of the link you saved in the database. Then tell capybara to hit the submit button. 751 | 752 | Run your test and verify that you see a link on the search page. If you don't, debug using `puts` taking it one step at a time. Did your link save to the database? Are you typing in the right field? If you add that same database entry and search in a `$ rails console` session do you get a return? 753 | 754 | Once you're satisfied that your test works correctly you want to make sure that you've got some assert statements. I like to assert the url that I'm currently on. In this situation it also makes sense to look for text on the page that matches the text in our link. Make sure you're not asserting generic content that is always going to be there like `div` or content that might change. A general rule of thumb I use, is to only assert content if it's content I've defined in the specific test. So if i make a user with an email of "captain_rocket_pants29@schneems.com" I could reasonably expect that string to be on their account page with little fear that it would accidentally be there or accidentally be removed. 755 | 756 | Run your test and make sure it passes. Try commenting out your `SearchController#index` action and re-run just to make sure that the test fails if you were to accidentally break something. It might seem silly but it is really easy to write a test that isn't actually testing anything. For example if you picked a link titled "Sign In" it would find that text on any page for a logged out user. 757 | 758 | Remove your commented out code and make sure all your tests run 759 | 760 | $ bundle exec rake test 761 | 762 | Once you're happy with your tests, commit everything to git (if you haven't been doing so already) and we're ready to deploy your reddit competitor to the cloud 763 | 764 | ## Deploy 765 | 766 | Tests and deployment go together like peanut butter and jelly. Run your tests throughout development to help you build the features you want, then run the whole test suite before you deploy to make sure you're not introducing bugs into production. While you won't be able to fully replace manual testing ever, writing automated tests will save you time and help you sleep sound at night. When you do run into production bugs, write tests for them so they never happen again. You can consider practicing [5 whys](http://en.wikipedia.org/wiki/5_Whys) and asking yourself why a test didn't catch the failure previously. Tests might not be magic bullets, but they're certainly ammo to make better products and coders. 767 | 768 | Take this opportunity to deploy and try to introduce errors into your own product in production, when you do write a test that would fail if that bug continues to exist in your website. You won't always be able to catch every bug with tests, but you'll catch no bugs with tests if you don't write tests. 769 | 770 | 771 | ## The End of the Beginning 772 | 773 | Sad to say, we're done for this exercise. If you feel pretty amazing like you just completed a monumentous task that mere non-programming mortals can only dream of, it's because you should. Take a step back and look at what you've done. You've cloned a good portion of functionality from one of the top sites on the Internet. You mastered votes, conquered user sign-up, demolished front-end design, obliterated full-text search and wrapped it all up in a tested package. Not just anyone could make it this far, you may now call yourself a Rails programmer. As you're congratulating yourself take some time to think to the future and of the past. How did you get here? What pushed you to follow 10 weeks of videos, lectures, quizzes, and exercises? This "Reddit on Rails" exercise is nearly 20,000 words and you read them all. Surely you must have a good reason to put yourself through all of that. Do you have an app idea burning a hole in your pocket? Did you want to make more money, or get a better job? Do you want to change the world through creation and code? Whatever your reason, you're here and you should hold on to that and not stop. 774 | 775 | Learning is a life long journey my friends, and programming is no different. Now that you're done with this course try to plan your own next steps. Join a user group. Pick up a Ruby (with or without Rails) programming book like [Metaprogramming Ruby](http://pragprog.com/book/ppmetr/metaprogramming-ruby). Build the product of your dreams, or maybe go through some [other rails tutorials](http://ruby.railstutorial.org/). Don't forget to subscribe to [my blog](http://schneems.com) and follow me on twitter [@schneems](http://twitter.com/schneems). You might also want to subscribe to [reddit.com/r/ruby](http://reddit.com/r/ruby) and [ruby weekly](http://rubyweekly.com). Maybe you've had enough of Ruby and Rails and you want to [dive into front end development](http://www.abookapart.com/) or maybe you fancy yourself an [iPhone programmer](http://www.bignerdranch.com/book/ios_programming_the_big_nerd_ranch_guide_rd_edition_), or perhaps you want to become a ballet dancer. 776 | 777 | Whatever it is that moves you, hold onto it and go with it. 778 | -------------------------------------------------------------------------------- /part_two_comments_and_votes.md: -------------------------------------------------------------------------------- 1 | ## Reddit on Rails: A Hero's Journey Part 2 2 | 3 | ## Last Week 4 | 5 | Last week in our [UT on Rails Course](http://schneems.com/ut-rails), we used user stories to prototype the link posting ability in a site that replicates some of the functionality form [reddit](http://reddit.com). We broke problems into smaller problems until we were able to solve them. We built MVCr and had a pretty good start to our project. When you step away from a project, it can be difficult to remember where you left off. If possible, I always like to end with a reminder of where I stopped. For some people that means writing an unfinished test, or writing a physical note in a notebook. Since you can't exactly read my notebook right now, we ended with some unfinished user stories so before we try to continue, let's see exactly where we ended. After we implemented most of the link submitting functionality we wrote these two user stories: 6 | 7 | "A logged in user visits the home page, sees a user submitted link and votes on it. 8 | The user should end up on the home page and the vote count should change up or down 9 | according to how they voted" 10 | 11 | and 12 | 13 | "A logged in user visits the home page, clicks the comments link under a user submitted 14 | link, there they can add a comment message to that link. Other users can view that 15 | comment message by visiting the link's show page." 16 | 17 | 18 | Now we have a place to start. Looking at the first sentence in both of those stories, a user logs in (we already implemented this functionality) and then when they visits the home page they will see links and comments. Since we haven't built comments yet, lets populate our front page with some links. 19 | 20 | Like last week you'll be required to use critical thinking and everything you've learned to build out our app. You will run into exceptions and strange behavior. Use docs, logs, the console, previous projects, and anything else you can get your hands on. Make small changes and verify every step of the way. You excited? I am, let's do this thing! 21 | 22 | ## Links on the Homepage 23 | 24 | We've already built a good deal of functionality for our Link feature. We have a link model, a links controller, link views, and routes. We also have a home page we built last time in `pages#index`, so we need to pull links out of our database and put them on our front page. We probably want to start in the console to make sure that we've got some links. Fire up the `rails console` and verify you have links in your database: 25 | 26 | $ rails console 27 | > Link.count 28 | # => 3 29 | 30 | If you have one or fewer links make some right now using `Link.create` remember links have a `url` and a `title` if you ever need to see the schema for a given model you can just type it into the console. 31 | 32 | $ rails console 33 | > Link 34 | # => Link(id: integer, user_id: integer, url: string, created_at: datetime, updated_at: datetime, title: string) 35 | 36 | Now you should have a handful of links in your database for testing purposes. Start your server and visit the home page [http://localhost:3000](http://localhost:3000). You should see a sign in or sign out link depending on your status as well as a link labeled "add a link". So what do we need to do to get links displayed on our home page? We need to pull them out of the database and then display them some how. Write a SQL query that populates an instance variable `@links` in the `pages#index` controller. Then in your view display all of the links on your site. Eventually we will want to order these and paginate them, but for now displaying will be enough. You will want to display the title, and when someone clicks the title they are taken to the submitted url. If you're confused about the functionality visit [reddit](http://reddit.com) 37 | 38 | 39 | 44 | 45 | 46 | Once you've got that working a user should be able to refresh the page, click on a title and be redirected to the given url. Once you've got that working commit to git. 47 | 48 | 49 | Let's take a step back and think about what we just did. We pulled links from the database and displayed them on our home page. What we just did isn't exactly new. We've pulled users and products, and all sorts of things out of databases and displayed them in lists, but this is the first time we've done so in a custom controller. Remember we made the `pages#index` controller and action to satisfy an earlier user story when we were adding devise. While we arguably could have used the `links#index` controller and action we aren't bound to just using controllers that are directly associated with models in the database. In fact, in any well designed Rails app you'll find that you will have many more controllers than models. 50 | 51 | While we are on the home page lets break from our user stories for just a second and build in pagination and set the order of our links. 52 | 53 | ## Pagination 54 | 55 | In the last section we pulled all of our Links from the database and displayed them on our homepage. Now we're going to add some pagination (the "next" and "previous" links you see when you browse sites like Google or Amazon). To do this we will use the [will paginate gem](https://github.com/mislav/will_paginate) follow the instructions to add the gem to your gemfile then run `$ bundle install`. Now you should be able to go into your console and run this: 56 | 57 | $ rails console 58 | > Link.order('created_at DESC').page(1).per_page(1) 59 | 60 | You should get an array with one link, if you change that to the second page you should get the next link in the series 61 | 62 | $ rails console 63 | > Link.order('created_at DESC').page(2).per_page(1) 64 | 65 | Change your active record query in your controller to use `page`, `order`, and `per_page`. You can pull values from the params hash `params[:page]` and `params[:per_page]`. You will need to provide some default values for instance if you use 66 | 67 | params[:per_page] ||= 25 68 | params[:page] ||= 1 69 | 70 | Then the page number will be supplied by the user and if it is not it will be set to 1. Similarly if a per_page is not supplied it will default to 25 (which is how many links show up when you visit reddit). 71 | 72 | Add this functionality to your controller now. To finish up pagination we can use will paginate's view helper. Add this to your view: 73 | 74 | 75 | <%= will_paginate @links %> 76 | 77 | 78 | If you have more links in your will paginate collection than are currently showing you will get a convenient "next" and "previous" links where appropriate. You can test this out by visiting the home page and supplying `per_page` and `page` parameters [http://localhost:3000?per_page=1&page=1](http://localhost:3000?per_page=1&page=1). 79 | 80 | Save and commit to git. 81 | 82 | ## Comments 83 | 84 | Okay we got some of this user story finished 85 | 86 | "A logged in user visits the home page, sees a user submitted link..." 87 | 88 | We even added a bit more with pagination. We could of course write a user story around pagination if you wanted but it's such a common feature typically saying "paginate the links" would be enough. From here you could refine your pagination with different styles, but for now we're done with that feature. While the end of the sentence talks about votes, I would like to build a comments feature first. To help us we're going to switch user stories that we are working on. While it feels weird that we are leaving a story in mid sentence, remember that we are using them to gnererate small discrete tasks. If we were to break these into a to-do list we could have put listing all the links on the homepage under one to-do. Staying flexible and working on the most pressing/interesting problems/features will help you stay sharp and focused. Let's take a look at that other user story. 89 | 90 | 91 | "A logged in user visits the home page, clicks the comments link under a user submitted 92 | link, there they can add a comment message to that link. Other users can view that 93 | comment message by visiting the link's show page." 94 | 95 | 96 | Okay, so a logged in user can already visit the home page, but there is no comments link under a user submitted link. So let's add one. Before we do finish the rest of the story, we need to understand the full scope of what that link should do and where it should go. The last sentence gives us a clue, we should see comments for a given link by visiting that link's show page. So right now in your home page add this under each of your user submitted links. 97 | 98 | <%= link_to "comments", link %> 99 | 100 | Where link is an active record object. If you prefer you could also run `$ rake routes` and find that the link helper is called `link_path()` so you could alternatively use `link_path(link)` or `link_path(link.id)` or `link_path(:id => link.id)` which are all similar. Once you've added your link visit your [Home Page](http://localhost:3000) and refresh, you should see a "comments" link. Click one of them and you should be directed to `links#show`, you can confirm via the url or using your rails logs. 101 | Once you have this working save and commit to git. 102 | 103 | Taking a look back at our story we see that have some nouns without associated models. We have "comment" and that comment has a "message" and we need to know what link the comment is on. We will also want to store the author who left that comment. So right now we know that a comment will have exactly one and only link, that a link can have many comments, a link will belong to only one user and a user can have many comments. Let's see what a model might look like: 104 | 105 | Comment Table 106 | message: text 107 | user_id: integer 108 | link_id: integer 109 | created_at: datetime 110 | updated_at: datetime 111 | 112 | Here we are setting message to `:text` instead of a `:string`. By default a string will be limited to 255 characters, while `text` has no limitation on length. 113 | 114 | 115 | If you're confused, especially about why we want a `user_id` and a `link_id` remember week two of our [UT on Rails](http://schneems.com/ut-rails) course where we talked about [modeling relationships](http://schneems.com/post/25503708759/databases-rails-week-2-modeling-relationships-and). You can also view the first part of last week's exercise where we modeled our links. You might remember that in this type of association (has_many & belongs_to) we must store the relationship somehow in our database, and it is far easier to store it with the record that "belongs" to the other record. So since a comment belongs to both a "user" and a "link" we will add a foreign key of `user_id` and `link_id` to use with our association. 116 | 117 | Now would be a good time to discuss open source libraries. We used an open source library for user authentication and for pagination so why not for comments? If you browse [the ruby toolbox](https://www.ruby-toolbox.com/categories/rails_comments), you'll see there are more than a few comment gems out there. The top gem: [Acts as Commentable with Threading](https://github.com/elight/acts_as_commentable_with_threading) is one attractive option. When deciding if I desire to use a given open source library to solve a problem, I ask myself about the problem, and about the library. With authentication, do I really want to write code that could affect the security of my site? With pagination, the ordering and the offset is easy enough in plain SQL but I will_paginate gives me a nice syntax and some convenient helper methods, like the view helper that gave us the next and previous butttons. With comments I already understand the problem well enough to have an idea what the model needs to be, so I'm not sure if a library will actually help me with the implementation. So the next part would be looking at the library. Using the Ruby Toolbox or your own detective skills, you can decide if a library is active and people are using it based on the number of watchers, the last time it was updated, and the quality of documentation. Devise and will_paginate both have quite a bit of watchers and you can see download activity on [Rubygems.org](http://rubygems.org/gems/will_paginate). We aren't looking for raw numbers, we're looking for signs that a developer has tried the library and then liked it enough to come back and use it again. 118 | 119 | In this scenario [Acts as Commentable with Threading](https://github.com/elight/acts_as_commentable_with_threading) doesn't look like a bad option, but it would be better for us to have more control over our code for now. By writing custom commenting code, you will be better equipped in the future to decide if you should use a library or not. 120 | 121 | ## Comment Model Implementation 122 | 123 | Now that we've got an idea how we want to implement our comments, we'll need MVCr. We already know what our model should look like so we will need the ruby code in `app/models` and we will also need a migration. You can use a generator for both or build the ruby model manually and then generate the migration using a generator. Since last time we used `$ rails generate model`. We will build our model manually and then make a migration using a generator. 124 | 125 | First make a new file `app/model/comment.rb` remember that models are always singular. Inside of your model you can copy the code we have in our `link` model: 126 | 127 | class Link < ActiveRecord::Base 128 | attr_accessible :title, :url 129 | end 130 | 131 | But we will need to change a few things: 132 | 133 | class Comment < ActiveRecord::Base 134 | attr_accessible :message, :user_id, :link_id 135 | end 136 | 137 | Now we need some fields in our database run: 138 | 139 | $ rails generate migration create_comments 140 | 141 | You'll see something like this: 142 | 143 | class CreateComments < ActiveRecord::Migration 144 | def up 145 | end 146 | 147 | def down 148 | end 149 | end 150 | 151 | 152 | When we make a migration, we need to tell Rails how to undo the changes we've made (like last week when we rolled some of our migrations back). If we add a column when we are migrating up, we need to tell it to drop that column when we are rolling back or migrating down. If you look in the migration `add_devise_to_users.rb` you will see there is code in both an up and a down method. If you glance at some other migrations you'll see that instead of `up` and `down` we simply have a `change` migration. This is relying on Rails to be smart about our changes. If we tell it to add a column then it will assume that is what we want to do while going up, and we want to do the opposite while going down (remove that column). Not all changes can be reversed automatically so, thats why Rails gives us the option of using `up` and `down`. While I advocate knowing where to find the docs and being able to write a migration yourself from scratch, I personally find it quicker to look at older migrations when writing my own. Go into the `create_links.rb` migration and copy the change code: 153 | 154 | def change 155 | create_table :links do |t| 156 | t.integer :user_id 157 | t.string :url 158 | 159 | t.timestamps 160 | end 161 | end 162 | 163 | Replace `links` with `comments` and then add the columns we need (message, user_id, link_id). Don't forget that `message` is a `text` column and not a `string` column. When you are done run the migrations `$ rake -T` if you can't remember the command. Once you have migrated the data you should be able to run this in the console: 164 | 165 | $ rails console 166 | > comment = Comment.new 167 | > comment.user_id = 1 168 | > comment.link_id = 1 169 | > comment.message = "Hello world" 170 | > comment.save 171 | 172 | If you can't then try rolling back your migration and fixing any problems or error messages you encounter. Once you get it working we'll want to add the association to our comments. Add `has_many` and `belongs_to` in the appropriate models `app/models/comments.rb`, `app/models/link.rb` and `app/models/user.rb`. Once you have them specified correctly you should be able to run this in the console: 173 | 174 | $ rails console 175 | > Comment.last.link 176 | > Comment.last.user 177 | > User.last.comments 178 | > Link.last.comments 179 | 180 | Once you can run those commands in your console correctly then save and commit the results to git. 181 | 182 | Now we've got a pretty good model on our hands let's give our user's some views, controllers, and routes to allow them to add comments without the command line. 183 | 184 | 185 | ## Comments VCr: Controller 186 | 187 | Going back to our story: 188 | 189 | "A logged in user visits the home page, clicks the comments link under a user submitted 190 | link, there they can add a comment message to that link. Other users can view that 191 | comment message by visiting the link's show page." 192 | 193 | A user will need to add a comment on a link's show page, this is where we will add our view for the form and for showing all the comments. That means we will need a controller action to handle creation of a comment and an associated route. 194 | 195 | First start out by creating a file `app/controllers/comments_controller.rb` (while models are singular, controllers are plural). From there we can copy the structure in our links controller and replace the needed parts: 196 | 197 | class LinksController < ApplicationController 198 | def show 199 | # ... 200 | end 201 | 202 | def new 203 | @link = Link.new 204 | end 205 | 206 | def create 207 | # ... 208 | end 209 | 210 | end 211 | 212 | 213 | We need to change the name of our controller and we will only need the create action: 214 | 215 | class CommentsController < ApplicationController 216 | def create 217 | # ... 218 | end 219 | end 220 | 221 | When a comment is created, we can just redirect the user back to the same view so we can use this code: 222 | 223 | def create 224 | @comment = Comment.create(params[:comment]) 225 | redirect_to :back 226 | end 227 | 228 | ## Protecting our user_id (fyi) 229 | 230 | Based on this, we can construct a comment based on a user supplied `:user_id`, `:link_id` and `:message`. That means somewhere in our form we have to have a field (hidden or otherwise) for those fields. Do you see any problem having our controller accept a user_id from our user? When we build html in ruby and send to a user, our user could use a tool like chrome's inspector to edit that html and submit a form. So even if we have a hidden field like this: 231 | 232 | 233 | 234 | A malicious user could edit our html to change that value from 9 to an arbitrary `user_id`. This means that anyone could post a comment as any other user. To get around this we can use devise to make sure a user is logged in when hitting our controller action and then we can use our `current_user` variable to make sure only the currently logged in user is making comments that are tied to their own account. 235 | 236 | Looking at the the [Devise Docs](https://github.com/plataformatec/devise/) you will find that we can require a user to be logged in by adding this in our controller: 237 | 238 | before_filter :authenticate_user! 239 | 240 | So we can add this to our controller like this: 241 | 242 | 243 | class CommentsController < ApplicationController 244 | before_filter :authenticate_user! 245 | 246 | def create 247 | @comment = Comment.create(params[:comment]) 248 | redirect_to :back 249 | end 250 | end 251 | 252 | Rails give us the [before_filter](http://guides.rubyonrails.org/action_controller_overview.html#filters) method we can use in our controllers. What we are saying here is that devise has given us a method called `authenticate_user!` that will run before any of our methods. If `authenticate_user!` redirects, then the user will never hit the `create` action. Basically the code inside of `authenticate_user!` checks to see if a user has a valid session and if they are logged in it will let them continue, otherwise it will redirect them to the sign in path. 253 | 254 | Before filters can take options, for example if we only wanted this to run on the create action we could use this instead: 255 | 256 | before_filter :authenticate_user!, :only => [:create] 257 | 258 | You can make any action into a before filter, you don't have to rely on devise. For instance you could make one called `redirect_back_unless_logged_in`: 259 | 260 | 261 | class CommentsController < ApplicationController 262 | before_filter :redirect_back_unless_logged_in 263 | 264 | def create 265 | @comment = Comment.create(params[:comment]) 266 | redirect_to :back 267 | end 268 | 269 | def redirect_back_unless_logged_in 270 | redirect_to :back unless current_user.present? 271 | end 272 | end 273 | 274 | But since devise has already given us a very useful method we can just use that instead. So now that we have ensured a user is correctly logged in. We can make sure that only a `current_user` (which we get from devise) will be allowed to make new comments under their own account. Since we can call `User.last.comments` and `User.last.comments.create(:message => "foo")` we can use this syntax in our controller: 275 | 276 | 277 | class CommentsController < ApplicationController 278 | before_filter :authenticate_user! 279 | 280 | def create 281 | @comment = current_user.comments.create(params[:comment]) 282 | redirect_to :back 283 | end 284 | end 285 | 286 | By calling `current_user.comments` we are telling rails that the foreign key in comments will have to point at current_user's primary key. This also means now we don't have to include `user_id` in the form on our website. Sweet. 287 | 288 | You may be wondering about how we get associate our link in our form and how we can make sure that the user is only submitting a comment for the correct link. As we hinted at earlier we will use a hidden field in our form to supply the controller with a `link_id`. For the next part checking that the user didn't modify that id in the html...we won't worry about it. If a logged in user wants to add a comment on a different link that's fine, it's not going to affect anyone except for the currently logged in user. This is different from the `user_id` problem we had earlier where a user modifying their html would result in another user seeing strange behavior. 289 | 290 | If you're a bit lost, don't worry, if your website is small or if it is for an internal tool, then you'll likely never run into the problem of opening this type of security hole. If you decide to take up Rails development for a living, then you will need to understand this risk very well. 291 | 292 | ## Comments VCr: Routes 293 | 294 | By far the easiest part in the MVCr to implement if you're sticking to defaults add this to your routes: 295 | 296 | resources :comments 297 | 298 | If you wanted you could scope this to only produce a route for `create`: 299 | 300 | resources :comments, :only => [:create] 301 | 302 | Typically I don't bother with this as I'm likely to add other functionality later. 303 | 304 | 305 | ## Comments VCr: View 306 | 307 | So now we've got our model, controller, and routes we just need to add a view. We already settled that we would have our form in our `links#show` view. So let's copy an existing form from `links#new` and then modify it. Open up `app/views/links/show.html.erb` and paste in the copied form: 308 | 309 | 310 | <%= form_for(@link) do |f| %> 311 | <% if @link.errors.any? %> 312 |
313 |

<%= pluralize(@link.errors.count, "error") %> prohibited this link from being saved:

314 | 315 | 320 |
321 | <% end %> 322 | 323 |
324 | <%= f.label :title %>
325 | <%= f.text_field :title %> 326 |
327 |
328 | <%= f.label :url %>
329 | <%= f.text_field :url %> 330 |
331 |
332 | <%= f.submit %> 333 |
334 | <% end %> 335 | 336 | First we'll need to change `link` to `comment`: 337 | 338 | <%= form_for(@comment) do |f| %> 339 | <% if @comment.errors.any? %> 340 |
341 |

<%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

342 | 343 | 348 |
349 | <% end %> 350 | 351 |
352 | <%= f.label :title %>
353 | <%= f.text_field :title %> 354 |
355 |
356 | <%= f.label :url %>
357 | <%= f.text_field :url %> 358 |
359 |
360 | <%= f.submit %> 361 |
362 | <% end %> 363 | 364 | Next we'll change `:title` to `:message` and remove `:url`: 365 | 366 | <%= form_for(@comment) do |f| %> 367 | <% if @comment.errors.any? %> 368 |
369 |

<%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

370 | 371 | 376 |
377 | <% end %> 378 | 379 |
380 | <%= f.label :message %>
381 | <%= f.text_field :message %> 382 |
383 |
384 | <%= f.submit %> 385 |
386 | <% end %> 387 | 388 | Notice that our `:message` is a `text_field` this what we used for our `title` and `url` in the link model. While this makes sense for entering in strings, `:message` is stored in a `text` column. So it would be easier to add more text if we use a `text_area` instead: 389 | 390 | <%= f.text_area :message %> 391 | 392 | As a bonus, we can add some placeholder text: 393 | 394 | <%= f.text_area :message, :placeholder => "Add a comment" %> 395 | 396 | 397 | 398 | Now we'll just need to populate our `@comment` instance variable. Open up your links controller and add `@comment = Comment.new` into your `show` action: 399 | 400 | def show 401 | @link = Link.find(params[:id]) 402 | @comment = Comment.new 403 | end 404 | 405 | By now we should have enough to render our view. Visit a link's page like [links/1](localhost:3000/links/1) and a form for our comment should render. If you get errors, fix them. 406 | 407 | 408 | We also want to add a hidden field for our `link_id` 409 | 410 | <%= f.hidden_field :link_id, :value => @link.id %> 411 | 412 | Add this code, and refresh your page, you won't see anything change but if you view source or use a tool like chrome's inspector you should see something like this in the html: 413 | 414 | 415 | 416 | Now when we submit the form we will get `:link_id => 1` added to our `params[:comment]` hash. 417 | 418 | Log in if you aren't already, now enter in a comment, and click submit. You should be redirected back to the same link show page. You can check your logs to make sure that we hit the right action: 419 | 420 | 421 | Started POST "/comments" for 127.0.0.1 at 2012-08-05 16:00:59 -0500 422 | Processing by CommentsController#create as HTML 423 | Parameters: {"utf8"=>"✓", "authenticity_token"=>"b7ZKLfYSbuzzfa1/X+GU0p3sH5l0B7ekFgeXtndeLj0=", "comment"=>{"link_id"=>"1", "message"=>"hello world"}, "commit"=>"Create Comment"} 424 | User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 425 | (0.1ms) begin transaction 426 | SQL (11.4ms) INSERT INTO "comments" ("created_at", "link_id", "message", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) [["created_at", Sun, 05 Aug 2012 21:00:59 UTC +00:00], ["link_id", 1], ["message", "hello world"], ["updated_at", Sun, 05 Aug 2012 21:00:59 UTC +00:00], ["user_id", 1]] 427 | (0.9ms) commit transaction 428 | Redirected to http://localhost:3000/links/1 429 | Completed 302 Found in 109ms (ActiveRecord: 14.5ms) 430 | 431 | 432 | Here notice that we called the `CommentsController` and the `create` action (`CommentsController#create`) and that our parameters contain a `comment` hash: 433 | 434 | "comment"=>{"link_id"=>"1", "message"=>"hello world"} 435 | 436 | Also notice that this hash has both a `link_id` and a `message`. You can verify that the comment was correctly created in the console: 437 | 438 | $ rails console 439 | > Comment.last 440 | # => # 441 | 442 | Furthermore we can verify it has the proper link and user associations. 443 | 444 | $ rails console 445 | > Comment.last.user 446 | # => # Comment.last.link.url 448 | # => "http://schneems.com" 449 | 450 | So now that we are sure our comment was saved correctly we want to finish out our user story: 451 | 452 | "A logged in user visits the home page, clicks the comments link under a user submitted 453 | link, there they can add a comment message to that link. Other users can view that 454 | comment message by visiting the link's show page." 455 | 456 | We have to allow other users to view those comments. You can do that by adding some code similar to this in your link show view: 457 | 458 |

Comments:

459 |
460 | <% @link.comments.each do |comment| %> 461 |

<%= comment.message %>

462 | <% end %> 463 |
464 | 465 | Once you've done that then our comments user story is complete. Ensure that comments show up when you refresh the page and attempt to make new comments. Fix any errors you encounter. Save and commit to git. You should commit to git frequently in your own projects so if you get something working and then break it while working on a new feature you can inspect the most immediate changes using a tool like gitx for the github app. 466 | 467 | 468 | At this point and time it might be a good idea to write a new comment user story or or to add to the existing one. You can visit [reddit](http://reddit.com) for some inspiration. We could add votes to our comments, we could add comment threading, but at very least we can put the author's name and how long ago the comment was posted on the form. I'll leave the exercise of writing a user story up to you including everything else you want. We may do more with comments later but for now we need to finish our existing user stories. We still haven't finished voting. 469 | 470 | 471 | ## Voting 472 | 473 | Take a look at the remaining user story: 474 | 475 | 476 | "A logged in user visits the home page, sees a user submitted link and votes on it. 477 | The user should end up on the home page and the vote count should change up or down 478 | according to how they voted" 479 | 480 | This tells us we need to have a "vote" noun, which implies that we need a "vote" model. So it begins the great MVCr dance. We'll use the same methods we used with links and comments. Lets first start building the model. Reading the story we know that a vote should belong to a user and should be on a link, so we need some associations there. We also know that the vote should be either positive (up) or negative (down). First let's look at the associations, how would you describe them to someone. 481 | 482 | A user and a vote are associated somehow. Can a user have a single vote? Yes. Can a user only have a single vote (i.e. only vote once ever)? No, a user can vote on multiple links so they should have many votes. Can a vote have multiple users? No. Can a vote have one user? Yes. So based on these questions we now know that users has_many votes and that votes belongs_to user. Sometimes it can be confusing to which syntax we should use. I've seen people say incorrectly that a user has_many votes and that votes has_one user but this isn't quite right. Your foreign key goes on the model that belongs_to the other model, so without one of your models belonging to one of your other models there wouldn't be a foreign key!! Unlike in fight club, the things you own do not end up owning you. If a user has many products, one of those products does not "have" control over their user, but simply belongs to that user. 483 | 484 | Let's do a similar exercise with a vote and a link. Can a link have more that one vote? Yes. Is a link limited to one vote total? No. Can a single vote belong to a link? Yes. Can a single vote belong to multiple links? It could but that would be weird, so...no. 485 | 486 | Simple questions like these can help you define the proper associations to make in your Rails app. 487 | 488 | Putting that all together, a user has many votes, and a vote belongs to a user. A link has many votes and a vote belongs to a link. This is very similar to the relationships we just built for comments. 489 | 490 | Next we'll need to figure out how to store whether a vote is an "up" vote or a "down" vote. We could store this in any manner of ways. We could have a string column that we populate with "up" or "down" or we could have an integer column that is `1` for an up vote and `-1` for a down vote, or we could have a boolean column that is `true` when you have voted something up and `false` when you have voted something down. Any of those would work so we'll just chose one. I like booleans for this since you don't have to worry about misspelling anything and we're really only storing two different states anyway. If you wanted to allow people to rate something from 0 to 5 in the future instead of a simple boolean vote, picking integers might be of value, but for for now simpler is better so we'll use boolean values. 491 | 492 | Vote Table 493 | user_id: integer 494 | link_id: integer 495 | up: boolean 496 | 497 | Here we will store `up` as true if the user has voted the link up and false if the user has voted the link down. Now that we've know what we want our model to look like we need to make a migration and a `app/models/vote.rb` file. You can do this anyway you chose, using a generator or manually. Look back in the directions if you need help, by now you should be able to make your own migrations and model files. When you are done you should be able add a vote to the database: 498 | 499 | 500 | $ rails console 501 | > vote = Vote.new 502 | > vote.user_id = 1 503 | > vote.link_id = 1 504 | > vote.up = true 505 | > vote.save 506 | # => true 507 | 508 | You should also be able to query the vote from the database based on associated models: 509 | 510 | $ rails console 511 | > Vote.last.user 512 | > Vote.last.link 513 | > Link.last.votes 514 | > User.last.votes 515 | 516 | Note that user and link are singular while `votes` is plural. This is convention. Once you can do all of that with no errors and get back expected results save and commit the results to git. 517 | 518 | 519 | ## Votes VCr: 520 | 521 | Generate routes for votes, see previous instruction or use the routes files. 522 | 523 | Generate a controller for votes. For now it only needs a `create` action. It should belong in `app/controllers/votes_controller.rb` (controllers are plural). You can copy the comments controller and go from there, but instead of `current_user.comments` we will need `current_user.votes`. When a user submits a vote they should go back to the same page they started on. We can do that using `redirect_to :back`. 524 | 525 | Now we've got a model, a controller, routes but no view. Back to our user story: 526 | 527 | "A logged in user visits the home page, sees a user submitted link and votes on it. 528 | The user should end up on the home page and the vote count should change up or down 529 | according to how they voted" 530 | 531 | 532 | So we have links to add votes from the home page. Let's open up `pages#index`. We should have something that looks similar to this: 533 | 534 | <% @links.each do |link| %> 535 | <%= link_to link.title, link.url %> 536 | <%= link_to "comments", link %> 537 | <% end %> 538 | 539 | Where we are displaying all of our paginated links. We want to add an up-vote and a down vote button here. Let's do that now. If we run `$ rake routes` we will see that we can access the create action using this route: 540 | 541 | votes GET /votes(.:format) votes#index 542 | POST /votes(.:format) votes#create 543 | 544 | So we can use the `votes_path` helper and we must specify that we use the POST http request. Let's add an up vote to our view right now: 545 | 546 | <%= link_to "+", votes_path(:vote => {:link_id => link.id, :up => true}), :method => :post %> 547 | 548 | Before you refresh your [home page](http://localhost:3000) make sure that `link_id` and `up` are both accessible in your vote model. Now visit the [localhost:3000](http://localhost:3000) and you should see a "+" next to each link. Click on one of them, you should be redirected back to the same page. If you get any errors, fix them and try again. You get a successful post you should see something like this in your log: 549 | 550 | 551 | Started POST "/votes?link%5Blink_id%5D=1&link%5Bup%5D=true" for 127.0.0.1 at 2012-08-05 17:43:14 -0500 552 | Processing by VotesController#create as HTML 553 | Parameters: {"authenticity_token"=>"b7ZKLfYSbuzzfa1/X+GU0p3sH5l0B7ekFgeXtndeLj0=", "vote"=>{"link_id"=>"1", "up"=>"true"}} 554 | User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 555 | (0.1ms) begin transaction 556 | SQL (0.6ms) INSERT INTO "votes" ("created_at", "link_id", "up", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) [["created_at", Sun, 05 Aug 2012 22:43:14 UTC +00:00], ["link_id", nil], ["up", nil], ["updated_at", Sun, 05 Aug 2012 22:43:14 UTC +00:00], ["user_id", 1]] 557 | (2.1ms) commit transaction 558 | Redirected to http://localhost:3000/ 559 | Completed 302 Found in 7ms (ActiveRecord: 3.0ms) 560 | 561 | 562 | Verify the controller and action are correct, and verify that the parameters contain a "link" hash. 563 | 564 | "vote"=>{"link_id"=>"1", "up"=>"true"} 565 | 566 | Now we can confirm that a vote was created by checking the console 567 | 568 | $ rails console 569 | > Vote.last 570 | # => # 571 | 572 | If you don't see a `user_id` a `link_id` and an `up` value, then you need to figure out what went wrong and fix it. Now that we have our votes working we can add a vote count and make it possible to down vote a link: 573 | 574 | <%= link_to "-", votes_path(:vote => {:link_id => link.id, :up => false}), :method => :post %> 575 | 576 | Note that we changed the value of `:up` to false. We can also show the number of total up votes and down votes. 577 | 578 | Up Votes: <%= link.votes.where(:up => true).count %> 579 | Down Votes: <%= link.votes.where(:up => false).count %> 580 | 581 | Though likely we only want to show total vote count which would be up votes minus down votes: 582 | 583 | Votes: <%= link.votes.where(:up => true).count - link.votes.where(:up => false).count %> 584 | 585 | Now if we refresh the page we can see the vote count that a link has on it! Go ahead and try down voting and up voting links to make sure that the value changes. Once you get all that working save and commit to git. Lets check our story: 586 | 587 | 588 | "A logged in user visits the home page, sees a user submitted link and votes on it. 589 | The user should end up on the home page and the vote count should change up or down 590 | according to how they voted" 591 | 592 | It looks like everything is in good working order, but if you played around with our voting feature you may have noticed that you can vote a link up once and only once. Our app doesn't have the same restriction, so one user could rig the vote. Before we fix this lets take a second to meditate on how this will affect users by writing another user story. 593 | 594 | "A logged in user that votes for a link cannot vote for the same link in the same 595 | direction. A logged in user can see that which links they have voted for and in which 596 | direction up/down" 597 | 598 | ## Vote Validations 599 | 600 | The easiest thing we can do to prevent duplicate votes from saving is to add validations to our vote model. Open up `app/models/vote.rb` and add this validation: 601 | 602 | 603 | validates :user_id, :uniqueness => { :scope => :link_id } 604 | 605 | 606 | You can view the [rails validation docs](http://guides.rubyonrails.org/active_record_validations_callbacks.html) for more info. Essentially what we are telling our vote model is that it cannot save any new votes unless the `user_id` and `link_id` combo haven't been taken created before. If we were going to leave the scope part out then it would verify that all `user_id` rows in our vote table are unique which would prevent users from voting on anything more than once...ever. Since we don't want that we only want to prevent users from voting on one link over and over again. 607 | 608 | 609 | Now that we have new validation code in place we might already have some bad entries that have duplicate `user_id` and `link_id` saved to the database. You can get rid of these by iterating through all the votes in your system and deleting them if they are invalid. You can use the `find_each` method to get all votes (this uses batched find so you will not run out of memory even on a huge number of votes). This method takes a block that will be yielded to for each vote. [Find Each docs](http://apidock.com/rails/ActiveRecord/Batches/ClassMethods/find_each). This should do the trick 610 | 611 | Vote.find_each {|vote| vote.destroy unless vote.valid? } 612 | 613 | Once you've done this you'll remove all duplicate votes. Now, with the validation code in your model, refresh your home page and click the "+" button a few times. What happens? Nothing? Good. If the count on the vote changes then double check your validations. 614 | 615 | Now try to change your vote from an up vote, to a down vote. What happened? Nothing? Bad. A user should be able to switch their vote, why isn't this working? We're hitting the vote controller and the create action (double check with your log) so let's look at that code: 616 | 617 | def create 618 | @vote = current_user.votes.create(params[:vote]) 619 | redirect_to :back 620 | end 621 | 622 | 623 | So our down vote isn't saving but why? Let's debug with `puts` change your code to this: 624 | 625 | def create 626 | @vote = current_user.votes.create(params[:vote]) 627 | puts "======================" 628 | puts @vote.errors.inspect 629 | redirect_to :back 630 | end 631 | 632 | The first puts with the equals signs i call a tracer bullet, I add it just to make my second puts easier to find in the log file. The second puts will output any errors on our @vote object. If a validation prevents an object from saving to the database it will add `errors` to that model. Try voting something down again and let's see the output in the log. 633 | 634 | 635 | In the log we can find our tracer and the errors: 636 | 637 | ====================== 638 | #, 640 | @messages={:user_id=>["has already been taken"]}> 641 | 642 | So here we have the problem, our user_id has already been taken. This makes sense since we added a validation but we're adding a down vote, not an up vote...why are we getting this error? 643 | 644 | We can see that from the inspect that we were trying to save a user_id of `1` and link_id `1` (note your numbers might be different). Open up a console and look for a vote with those credentials: 645 | 646 | 647 | $ rails console 648 | > Vote.where(:link_id => 1, :user_id => 1).first 649 | # => # 650 | 651 | Okay so there is already a vote with that user id and link id in the database. How do we change the attribute? Rather than trying to create every time in the controller we could try to find a link before updating its attributes! We could do something like this: 652 | 653 | @vote = Vote.where(:link_id => params[:vote][:link_id], :user_id => current_user.id).first 654 | if @vote 655 | @vote.up = params[:vote][:up] 656 | @vote.save 657 | else 658 | @vote = current_user.votes.create(params[:vote]) 659 | end 660 | redirect_to :back 661 | 662 | 663 | Or we could get fancy with our boolean logic: 664 | 665 | @vote = current_user.votes.where(:link_id => params[:vote][:link_id]).first || current_user.votes.create(params[:vote]) 666 | @vote.update_attributes(:up => params[:vote][:up]) 667 | redirect_to :back 668 | 669 | Or written another way we could do 670 | 671 | @vote = current_user.votes.where(:link_id => params[:vote][:link_id]).first 672 | @vote ||= current_user.votes.create(params[:vote]) 673 | @vote.update_attributes(:up => params[:vote][:up]) 674 | redirect_to :back 675 | 676 | All of these are doing roughly the same thing. Change the code in your controller to the method you like best. You should be able to refresh the page and click either "+" or "-". Make sure you removed invalid votes from the database as above. If you get any errors fix them, then save and commit the results to git. 677 | 678 | So we're making some good progress but still haven't quite finished our story: 679 | 680 | "A logged in user visits the home page, sees a user submitted link and votes on it. 681 | The user should end up on the home page and the vote count should change up or down 682 | according to how they voted" 683 | 684 | We need to let a user know when they have voted for a link and whether they voted up or down. To do this reddit changes the up vote arrow color. Since we don't have votes yet we can change the "+" to a "*" and make it not click-able. You can do that using SQL like this: 685 | 686 | 687 | 688 | <% if current_user && current_user.votes.where(:link_id => link.id, :up => true).present? %> 689 | * 690 | <% else %> 691 | <%= link_to "+", votes_path(:vote => {:link_id => link.id, :up => true}), :method => :post %> 692 | <% end %> 693 | 694 | Go ahead and add that to your home page view. Make sure that behavior matches what you would expect. Repeat the process for the down vote link. Test the behavior and make sure everything works as you desire. Save and commit to git. 695 | 696 | 697 | Now are user story is complete! We'll need to write some more user stories and some more code next week, but for now sit back, look at what you've created and be proud. 698 | 699 | Take a second to deploy your changes to Heroku where anyone you want can get to your website. 700 | 701 | 702 | ## Fin 703 | 704 | We've done quite a bit today. We prototyped and built two new models along with some complex VCr behind the scenes. While our site could certainly use some polish, it's beginning to have some of the base features we would expect from a service similar to [Reddit](http://reddi.com). Hopefully you're feeling a bit more confident in your ability to dissect a user story and pull out the needed elements to build a model. If you don't get it right the first time, you can always rollback or add columns. Comments and votes are pretty important pieces in many web sites, you can find different variations of them on facebook, youtube, and of course Reddit. Understanding how to take an abstract concept like a "vote" or a "like" and translate that into a working web feature is much easier if we break it into pieces with the help of user stories. There will be one final exercise where we add some polish to our sites. The one difference between the process I've been walking you though and what I would actually do is user testing. As I'm building features i'm constantly asking my friends and family if my user stories make sense, and when I get working prototypes like what we have here, I ask them to use it with minimal direction to see if they find any new bugs or problems. Non-technical friends can be more helpful than technical friends when it comes to validating your designs and implementation. Hopefully you're working on your own project in addition to following these tutorials right now. If you are, let go of your ego and put it in front of someone with little or no explanation. You will learn more in 10 minutes of watching another user use your product than in hours of reading up on usability best practices. 705 | 706 | You've come along way in a short amount of time. You should be proud of your work you deserve a break, go check out [/r/pics](http://www.reddit.com/r/pics/) and kill some time (caution user submitted though typically awesome pictures). --------------------------------------------------------------------------------