Skip to content

Instantly share code, notes, and snippets.

@lilliealbert
Last active August 29, 2015 14:00
Show Gist options
  • Save lilliealbert/3b91a606d56c967151c1 to your computer and use it in GitHub Desktop.
Save lilliealbert/3b91a606d56c967151c1 to your computer and use it in GitHub Desktop.
New RailsBridge intermediate curriculum

Intermediate Curriculum

TODO

  • Figure out what kind of illustrations would be fun to add
  • Write the various explanations

Introduction

We're going to build a job board. We're gonna use Rails generators, which do a lot less than scaffolds, so we're gonna get a little less done today than we did when we built Suggestotron. But we're gonna build an app in little pieces, so you'll understand better how the pieces fit together.

Follow the Error

We're going to play FOLLOW THE ERROR, in which we try to do something that we wish worked, read the error message to see what is currently inadequate in our code to accomplish that thing we were trying to do (like load a particular page), and then implement whatever the error is complaining about.

Notable Things

As you might have noticed, we're assuming you've already been to a RailsBridge workshop before or have otherwise already explored a Rails app, and are ready for deeper knowledge.

We're also going to skip deploying to Heroku this time around, but you can definitely use the instructions from the Suggestotron curriculum to deploy your app to the internet!

OH HEY: THIS CURRICULUM IS WRITTEN FOR RAILS 4. THINGS WILL GET AWKWARD / BROKEN IF YOU'RE USING RAILS 3

Tips for everyone:

  • When adding code, it's awesome for students to walk through the code line by line and say outloud what is happening. (i.e., "The string is being stored in the instance variable" or "The method snorgle is being defined"). If you do it every time, you'll get really comfortable with the vocabularly of Rails!

Create a Rails app

Step One

First, let's get into a directory for RailsBridge projects (either finding & moving there or making a new folder).

mkdir railsbridge

or

cd railsbridge

Rails New!!!

Run the following command in the console:

rails new job_board -T

The -T in that command means that when you make new files using Rails generators, it doesn't automatically create test files using Test::Unit (the default Rails testing framework).

Watch all the files that are created! Wow!

Open the project in Sublime Text

Move into the directory for your new Rails app:

cd job_board

And open the project in Sublime:

  • Open Sublime
  • Under Project, choose "Add Folder to Project"

(You must have at least one window open, so if that option is greyed out, open a window with cmd+n (mac) or ctl+n (PC))

Fix Up Those Defaults

We're going to be looking at the Rails server output a lot, which includes a lot of noise by default. Find the file Gemfile, and add the following:

gem 'quiet-assets'

Delete the following lines:

gem 'turbolinks'

Save the file, and then in the command line, run the following command:

bundle

Look at your empty app

Start the Rails server

in the terminal, type:

rails server

Now, let's check out our default home page

In the broswer, visit http://localhost:3000

Yup, that's the default Rails home page!


Set up our jobs home page

Routes!!!

That Rails default page is boring. Let's add a page to list the open jobs at RailsBridgeCorp!

Let's use the errors to tell us what we need to do. First, let's visit the page that we want to put our jobs on: http://localhost:3000/jobs.

OH NO! IT'S AN ERROR!

Oh, wait, it's helpful! Let's stop and read the error and figure out what it's telling us.

"No route matches [GET] "/jobs""

So it's looking for a route, but can't find one. Let's add one!

Open up the routes file. It's in the config directory. If you're using Sublime Text 2, you can open it by hitting cmd+p (mac) / ctl+p (pc) and tying in route, then hitting enter.

We're going to need a resource route, which will create EIGHT different routes for us. Add this to line two:

    resources :jobs

Now, lets go look at what that made. In your browser, visit http://localhost:3000/rails/info

  • Helper: this is how you will refer to a given path in your application. Instead of hard-coding '/jobs', you'll use jobs_path.
  • HTTP Verb: this is which verb the HTTP request will use. We'll go over HTTP these later!
  • Path: The is what URL you'll use to trigger a given action
  • Controller#Action: the name of the controller (in our case, jobs for all of them) and the method that will be triggered (after the #).

(IF YOU ARE ON RAILS 3 THIS WILL FAIL AND YOU NEED TO START OVER AFTER UPGRADING TO RAILS 4, YO)

Okay, cool, looks like /jobs is now a thing. Let's go visit that page again!

NEW ERROR!!!!

"uninitialized constant JobsController"

Add a controller

Time for a shortcut!!! Unlike the scaffold that we make in Suggestotron that makes about a million different files, we're just going to use a Rails generator to make the files we need.

In the console, type:

rails generate controller jobs

this will make us a few files! review them in the console.

Now: refresh http://localhost:3000/jobs

NEW ERROR!!!! YAY!!!

"The action 'index' could not be found for JobsController"

Huh? What is telling Rails to look in JobsController? Well, the route file is!

Let's go back to http://localhost:3000/rails/info again and look at what /jobs is pointing to.

It's looking for a method called index on the jobs controller! But there isn't one, boooo. So let's add it.

Open up your jobs controller (again, try to use keyboard shortcuts - cmd+p (mac) / ctl+p (pc) - to find the file called jobs_controller.)

Add the index method to the controller:

    def index
    end

And refresh http://localhost:3000/jobs

NEW ERROR!!!!! WOO HOO!!!

This time, we're looking at this big ball of text:

Missing template jobs/index, application/index with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in: * "/Users/lillie/railsbridge/job_board/app/views"

What this means is that Rails expects a template to exist with the same name as the controller method. It wants jobs/index to exist, but it just doesn't! So sad.

Let's add it!

Within app/views/jobs, add a file called index.html.erb.

[EXPLAIN ERB HERE]

Save that file, and refresh http://localhost:3000/jobs

NO ERROR!?!?! How boring. Let's add some content so we can be more confident that this really a thing. Within index.html.erb, add this:

<h1>RailsBridgeCorp Open Jobs</h1>

(Yeah, we said it would be a job board, but RailsBridge has lots of unpaid jobs looking for humans, so this seems fun.)

DONE! Well, except that we don't have any jobs. And even though we could hand-code a table of job titles here, that would be awfully tedious and we'd probably get sick of adding job postings every time someone came up with a new volunteer position. Let's empower people to add their own postings!


Store jobs in the database

So in order to make it possible for users to create jobs, we need some place for them to go, and for Rails to know what a job is! We're going to use another Rails generator - this time to make our migration and model!

in the console, run this command:

rails g model job

(the g stands for generate, which has many more letters than is necessary to type)

That generated two files: a migration and models/job.rb.

Making that migration file useful

Open up your migration file (cmd+p / ctl+p and type createjobs and hit enter) you'll see the following:

class CreateJobs < ActiveRecord::Migration
  def change
    create_table :jobs do |t|
      
      t.timestamps
    end
  end
end

This is making a table in our database called jobs. Right now it just has the timestamps (created_at and updated_at). What else should a job have? Let's start with a title and description. That should be pretty good to start.

add the the title and description like so:

    create_table :jobs do |t|
      t.text :title
      t.text :description
      t.timestamps
    end

Now we need to execute this file, so that the database schema gets updated.

In the terminal, run this command:

rake db:migrate

This uses a utility called rake to run a task called db:migrate, which in turn looks through all of your migration files and runs any that haven't already been run at some point in the past.

Okay, so we've got some place to store our jobs. But how can we make any? THROUGH THE MAGIC OF FORMS!!!


Rails Forms: A Party Waiting To Happen

Web forms. They're like, the most basic thing about the internet, other than cat gifs, right? So they should be straightforward, right? WRONG! They are exciting and just complicated enough to bake your noodle a bit. But don't worry, Rails has strong opinions about forms, so we won't have to do too much typing.

First, we'll make the form. Then, we'll hook the form up so that is actually does something! (Like store its contents in the database.)

Setting up the page for the form

Let's take a look at our handy routes, which can help us figure out what we need to do. We're going to follow the same pattern that we did for the main /jobs page.

Visit http://localhost:3000/rails/info

Check it out: there's a route for /jobs/new. That's the one we want for our form. Let's visit that page and see what it says: http://localhost:3000/jobs/new.

An error!! And maybe this time, we know what we need to do! Let's add a method to our jobs controller called new:

  def new
  end

Refresh http://localhost:3000/jobs/new, and we see that familiar "Template is missing" error. So, let's add that template:

Under app/views/jobs, add a file called new.html.erb. This will be our form. Add some html to the topic to keep things moving along:

  <h1>Add a job</h1>

Refresh again: http://localhost:3000/jobs/new

And we're ready to add our form!

Add a form!

Since forms are such a big part of CRUD, Rails has handy helpers for them. We'll be using a form helper specifically made for use with a model.

###[EXPLAIN CRUD]

In the view file you were just editing (app/views/jobs/new), add the following code:

    <%= form_for @job do |f| %>
      <div>
        <%= f.label :title %>
        <%= f.text_field :title %>
      </div>
      <div>
        <%= f.label :description %>
        <%= f.text_area :description, size: '60x6' %>
      </div>
      <div>
        <%= f.submit %>
      </div>
    <% end %>
    

Save that file, and reload the page. You should get an error with a message like this: "First argument in form cannot contain nil or be empty"

In order for the method form_for to do its job, it has to know about the thing that it's building the form for. So we need to give it an object. We do that in the controller. Open up the jobs_controller, and update the new method:

    def new
        @jobs = Jobs.new
    end

Now we should see our mostly unstyled form!

[COMPARE HTML IN THE BROWSER TO THE FORM HELPER]

Make that form actually work

Checking out the rails server

Arrange your screens so that you can see your server logs (in the terminal window) at the same time as your browser. Now, refresh the form page, and look at what happens in the server. You should see output like Started GET "/jobs/new".

[DISCUSS LOGGING / OUTPUT OF THE SERVER]

Now, try to submit your form.

YAY! An Error!

The action 'create' could not be found for JobsController

So, let's add it:

(Don't forget to navigate to the jobs controller by using the search shortcut, cmd+p or ctl+p)

def create
end

Alright, new error -- we don't have a template called create.html.erb. BUT! This time we're not going to make one. We actually want to go somewhere useful, right? Let's go back to the main jobs page (where we will eventually be listing our jobs). We need to tell the create method where to go, instead of letting it decide based on method name.

def create
    render :index
end

Okay, now go to http://localhost:3000/jobs/new again.

Great! Okay, we submitted our form. But did it do anything?

Sadly, no. We just took our form contents and dropped them on the ground.

Saving form data!

Head back to http://localhost:3000/jobs/new, and get your Rails console and your browser next to eachother again. Submit the form again, this time looking for the section that looks something like:

Parameters: {"utf8"=>"✓", "authenticity_token"=>"f48rtxanK9/MHu7TPvd6QzygGnrwv0P2/bxLllozw5U=", "job"=>{"title"=>"Meta-organizer", "description"=>"We need an somene to organize the organizers."}, "commit"=>"Create Job"}

This is the precious data that our form is sending, and right now we're just throwing it away. Let's not do that! Since we're using Rails 4 and all its great conventions, we're going to use Strong Parameters to limit what kind of data our form can submit to our app.

Add this code to your jobs controller. (Notice that we're expanding the create method. Don't just copy and paste and end up with two create methods, folks.)

  def create
    Job.create(job_params)
    redirect_to jobs_path
  end

  private

  def job_params
    params.require(:job).permit(:title, :description)
  end

Walk through this code line by line with the class! (But don't get too hung up on how strong params works — suffice it to say that it limits the parameters that we'll allow in to the ones listed here.)

Rails console time! Open up another tab or window in your terminal, and enter rails console (or rails c for the lazy/efficient).

Now, type in Job.count. See what number you get.

Now, submit a new job. What's the count now?

If the count is going up, yay!!! You got it! If not, time to debug!

Tips for Effective Debugging

  • Take a deep breath. Most of your time programming will be looking for bugs.
  • Read the error message out loud. Even if you already think you know what it means.
  • Check every assumption you can think of. You think that something is getting stored in a variable? WHO KNOWS?
    • A good way to check your assumptions is to print out anything you can to the Rails server log. Add puts statements to your code!
    • For example: If the jobs count isn't changing when we make jobs, make the jobs controller look like this. Now, it'll will print to the console the line "In the create method!!!!" and whatever is being returned from Job.create(job_params)
 def create
  p "In the create method!!!!!!"
  job = Job.create(job_params)  
  p jobs
  redirect_to jobs_path
 end
  • Think about how you would explain the problem to someone else, or, actually explain the problem to another student or a TA!

Listing Our Jobs

Going back to the jobs index (http://localhost:3000/jobs) and not seeing our new jobs kinda sucks. Let's actually build the job board part of this job board now!

Get all the jobs out of the database

If we're going to show our jobs in view, first we have to get them out of the database and store them in an instance variable. Update the index method to look like this:

def index
    @jobs = Job.all
end

Before we show the jobs, let's actually look at what that is doing. Go back to your Rails console and run Job.all. What is it doing?

Show those jobs!

Add this to app/views/jobs/index.html.erb:

<% @jobs.each do |job| %>
  <h3><%= job.title %></h3>
  <p><%= job.description %></p>
<% end %>

What is this doing? Go through this line by line, having one person explain each line.


Add Navigation

Let's add a top nav so that every page can link back to the main page and the new page.

Open up the file app/views/layouts/application.html.erb

Layouts

None of the view pages that we've been working with so far have had the necessary structure for a valid HTML page. No or or JavaScript or CSS tags. This application layout file is where all of that stuff exists!

Copying and pasting a nav bar onto every. single. page. of our application would be the worst. So instead of putting it into our individual pages, we add the nav bar HTML to this file!

Add the following code above the line <%= yield %>

<header>
  <div class="left-nav">
    <ul>
        <li><%= link_to "Home", jobs_path %></li>
    </ul>
  </div>
  <div class="right-nav">
    <ul>
        <li><%= link_to "Add Job", new_job_path %></li>
    </ul>
  </div>
  <div class="clearfix"></div>
</header>

Make sure not to delete the line <%= yield %>, because that is how the individual pieces of view content gets included on the site!

Things to note:

  • We use Rails link helpers instead of typing in <a href="/jobs/new">Add Job</a> [SAY WHY]
  • The <header> tag is HTML5. Aren't we cool!?

So let's take a look at it. Refresh, and ... isn't that horrifying looking? Let's make it look like a nav, albeit a very ugly one.

Head on up to the assets directory, and you should have a file here: app/assets/stylesheets/jobs.css.scss. This is a Rails-default created stylesheet, and isn't the best. Smart CSS people have taught us that CSS should be organized into component, not based on where it is used. So let's delete that file and make a new one.

Actually, we're going to make two new ones. Under app/assets/stylesheets, add global.css.scss:

body {
  margin: 0;
}

.content {
  margin: 10px auto;
  width: 70%;
}

.clearfix {
  clear: both;
}

This is where we put styles that affect the whole app.

Now, under app/assets/stylesheets, add nav.css.scss:

header {
  background: grey;
  padding: 10px;
  ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
  }
  a {
    text-decoration: none;
    font-weight: bold;
    color: white;
  }
}

.left-nav {
  float: left;
}

.right-nav {
  float: right;
}

It's still pretty horrific, but we've got other things to worry about for now. Turns out sometimes job descriptions have typos, and need to be updated. Let's make that possible!


Update listings

[Review REST and Rails URL patterns]

Say we want to edit the first job posting. If we look at http://localhost:3000/rails/info, we see this line:

  edit_job_path	 GET   /jobs/:id/edit(.:format)   jobs#edit

So, it looks like if we want to edit the job description, we should visit this URL: http://localhost:3000/jobs/1/edit.

We've seen this before, right? Let's add the controller action:

  def edit
  end

Refresh, template is missing. Alright, let's add that edit view, under app/views/jobs/edit.html.erb

  <h1>Edit Posting</h1>

Okay, so that's awesome.

Time to extract a partial!!!

Definition: partial

A partial in Rails is a chunk of ERB that can be interpolated into a page. This allows a single piece of content to be included in multiple different places, and also allows views to be organized into discrete pieces.

Rails form helpers are designed beautifully for CRUD interfaces. So we're not gonna have to write very much code to make this form work.

First, a refactor: we're going to move the create form into a parial.

Make a new file under jobs like so: app/views/jobs/_form.html.erb, and move the following code OUT of app/views/jobs/new.html.erb and into the _form.html.erb file:

<%= form_for @job do |f| %>
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>
  <div>
    <%= f.label :description %>
    <%= f.text_area :description, size: '60x6' %>
  </div>
  <div>
    <%= f.submit %>
  </div>
<% end %>

Now, in app/views/jobs/new.html.erb, add the following line:

<%= render :form %>

Add a job posting, just to make sure that the form is working as expected.

Use the power of partials

Now that we have a form partial, we can reuse it! In app/views/jobs/edit.html.erb, we can add the same line under the header:

<%= render :form %>

Refresh, and ... ERROR!! First argument in form cannot contain nil or be empty. Okay, that seems reasonable. It looks like we don't have a job ... because we haven't gotten our job out of the database! Let's go to the jobs_controller and fix that.

  def edit
    @job = Job.find(params[:id])
  end

[Review Params]

Actually Update The Job

So now the form works. Let's try to update that job posting.

ERROR!!! The action 'update' could not be found for JobsController. That makes sense. Let's add that action to the jobs controller

def update
end

Try it again, and ... template missing error! Similarly to create, we don't have a template to render for update. So let's just send them back to the jobs listing.

def update
  redirect_to jobs_path
end

Try again, and ... no errors! But we're still not seeing our changes.

Who knows what we're missing?

Here's what the update method should actually look like:

def update
  @job = Job.find(params[:id])
  @job.update_attributes(job_params)
  redirect_to jobs_path
end

We need to save our changes to the database so they can actually persist!

Add a Link

Our users probably aren't going to know they can hit /jobs/:id/edit to visit the edit form, so let's add a link to it on the jobs index so we end up with this (we're just adding the line with the <h5> header in it ... don't copy and paste the whole thing!):

<% @jobs.each do |job| %>
  <h3><%= job.title %></h3>
  <p><%= job.description %></p>
  <h5><%= link_to "Edit Posting", edit_job_path(job)%></h5>
<% end %>

Delete Postings

Once a job is filled, we don't want a listing for it hanging out forever. Let's add a way to delete postings.

If we look at our handy Routes page http://localhost:3000/rails/info/routes, we see this

job_path   DELETE   /jobs/:id(.:format)   jobs#destroy

So we need to send the DELETE http verb to the server, so we can get to the destory method on the jobs controller (that we will make soon). It turns out rails link_to helpers accept specific verbs as an argument, so we can add this line below the edit posting link that we just added:

  <h5><%= link_to "Delete Posting", job, method: :delete %></h5>

Go to the index, and try to delete something. The action 'destroy' could not be found for JobsController? This is like my favorite error now! Let's a method to destroy the job posting to the controller:

  def destroy
    @job = Job.find(params[:id])
    @job.destroy
    redirect_to jobs_path
  end

Try it again, and ... kablammo! We're destroying jobs left and right!

Time to add more things!

The time where we tell you what to type in has finally ended. You should do one or all of the things below!

  • Add a show page for each listing and move the Edit and Delete links to there.
  • Add validations — postings without titles or descriptions shouldn't be allowed, right?
  • Add other useful fields to jobs
  • Display jobs index in a table; make sortable with data_tables
  • Add tags (time for a has_many, folks!!!)
  • Make it look better
  • Add a user model and auth with Devise
  • Test it! Write a controller spec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment