How to Build a Forum with Ruby on Rails

Image for post
Image for post

Howdy, hello, how are ya? Like the subtitle states… this tutorial is based on Week 10 of Mackenzie Child’s 12 Web Apps in 12 Weeks Series. Link to his video here.

I’ve made sure that this is the BEST tutorial I’ve ever written (yet). I included a bunch of helpful links and videos and screenshots and gifs and memes to help you immerse yourself in the content. Should get you out the gate pretty quick with your Ruby on Rails learning path.

If you are casually browsing and are interested in doing a Ruby on Rails tutorial, stop what you are doing RIGHT NOW and give this podcast a listen! It will give you the fuel you need to continue down the web developer path; it definitely fueled me.

This tutorial assumes you have Ruby on Rails installed and all other pre-requisites, like a github account and a text editor and such, and that you understand how to jump around in the command line / terminal.

If you don’t know how to do of these things, that’s certainly ok. Everybody starts somewhere. Here are a few helpful links to get started:

P.S. I’m on a Mac, so everything is going to be a bit easier for a Mac user to follow along with.

Keep this in mind: it seems to me that your NUMBER 1 job as a developer is to learn how to learn; read, listen, watch… just figure out how the hell to build something from nothing, using any resources you can get your hands on.

I’m going to run through this tutorial as I would do them. It’s not necessarily way to do it, just how I do it, and — loosely — how Mackenzie Child does it in his tutorial. Let’s dive in:

Image for post
Image for post

if you get some value out of this article, and you’d like to support the author, purchase your next Amazon shipment through my affiliate link here. Cheers!

Table of Contents

  1. Getting Started. Creating the App.
  2. Git init
  3. That Sublime Trick
  4. Create a Post Model (video)
  5. Create a Post Controller (video)
  6. Routes (video)
  7. Creating an Index View
  8. Gems!
  9. Revisiting our Index View with HAML
  10. Add New and Create Methods to Post Controller
  11. Create New Post Form (video)
  12. Create the Show Action and View (video)
  13. Add Ability to Edit and Destroy a Post (video)
  14. Looping Through Posts on the Index Page (video)
  15. Setting up Devise (video)
  16. Connect Users and Posts (video)
  17. Add Navigation and HAML Styling (video)
  18. Users can sign in and out (video)
  19. Users can comment (video)
  20. Style the Comments (video)

Step 1: Getting Started. Creating the App.

Open up terminal and…

cd Documents

then

cd Projects

then

rails new forum

I have just generated a brand new rails app called “forum” inside of my Projects directory. Hurray.

Image for post
Image for post

Here, watch the thing:

annnnnd then let’s jump into the new directory and make sure the brick is working properly!

cd forum

then

rails s

then check out local host! http://localhost:3000/

Image for post
Image for post

Sweeeeeet. We gold baby.

Then ctrl + c to kill the server!

Looks something like this:

Ok cool. Moving on.

Image for post
Image for post

Step 2: Git init

Ok, we’ve done a very satisfying thing here. We now have a Ruby on Rails app brick that’s ready to be turned into a magical brick with powers. We’re like the hogwarts that transforms the brick into a wizard, you know?

But first, let’s practice some proper git hygiene and go ahead and commit our project to a fresh repository.

Soooo, get on over to Github and create a new repo!

Image for post
Image for post

Image for post
Image for post

And then, I’ll follow these directions above and do…

git init

then check the status with…

git status

you’ll see some red files that are unstaged…

Image for post
Image for post

Add ALL those files with a…

git add .

and then run another quick git status to see some green…

git status
Image for post
Image for post

Now, go ahead and…

git commit -m "first commit"

then copy / paste that junk from your repo…

git remote add origin https://github.com/deallen7/forum.git
git push -u origin master

Hit “enter” and then you should be golden…

Refresh your git page to see your uploaded files…

Ok, we’re really getting somewhere. Moving forward.

Step 3: That Sublime Trick

Short step here. If you’re using Sublime, you should most definitely check this little trick out so that you can open the files directly from the command line.

Step 4: Create a Post Model (video)

Alright, so. Well create our Post Model next.

rails g model post title:string content:text

then jump into sublime text and look at our migration file…

Image for post
Image for post

then, as always, we’ll run a migration…

rake db:migrate

looks like:

Now our post model is g2g…

Step 5: Create a Post Controller (video)

The model stores the post data… The controller helps control the conversation between the backend (database) and the frontend (the view!).

So we need a controller. Controllers are plural. Posts instead of post.

rails g controller posts

Now let’s open that controller up!

Image for post
Image for post

And inside the controller…

class PostsController < ApplicationController
def index
end
end

The index controller basically controls the page that will display all of the posts from all of the users. Maybe think of it as the reddit frontpage. And now, we’re going to ask the application to consider the Posts Index page to be the homepage by updating the routes.rb file…

Step 6: Routes (video)

Now inside the routes file…

Image for post
Image for post
Rails.application.routes.draw do
resources :posts
root 'posts#index'

end

If we start up our server again, we’ll see something new!

AN ERROR PAGE!

Image for post
Image for post

Get used to seeing red, people. It’s your job to google that sh!t and figure it out. Welcome to life as a dev.

It’s pretty simple, though. We’re missing a template. We’re missing a view. The data can’t be displayed because there is no associated view for the index controller!

Make sense?

So let’s create a view!

Step 7: Creating an Index View

Create a new file called index.html.erb inside the app/views/post folder…

Image for post
Image for post

And then let’s restart the server!

Remember we just have to hit:

rails s

And then navigate to local host… http://localhost:3000/

Image for post
Image for post

Step 8: Gems! (video)

Whelp. Looks like I jumped the gun a bit. So let me get re-aligned with Mackenzie. He’s going to use a few gems, including HAML. Let’s follow along.

Nav to rubygems.org…

Search for HAML…

Copy / paste the gem into your Gemfile…

gem 'haml', '~> 5.0', '>= 5.0.4'

Then we’re adding the simple form gem…

gem 'simple_form', '~> 4.0', '>= 4.0.1'

And then the devise gem…

gem 'devise', '~> 4.4', '>= 4.4.3'

Great. Save.

You’ll now how these puppies inside your gemfile…

gem 'devise', '~> 4.4', '>= 4.4.3'
gem 'simple_form', '~> 4.0', '>= 4.0.1'
gem 'haml', '~> 5.0', '>= 5.0.4'

Next, inside the command line:

bundle install

Cool.

Step 9: Revisiting our Index View with HAML

We’re going to use HAML, so we need to re-name our index file from index.html.erb to index.html.haml…

And we’ll be using a slightly different syntax. Learning something new erday…

Image for post
Image for post

Start up the server and let’s see what we’re dealing with…

rails s
Image for post
Image for post

Cool.

Step 10: Add New and Create Methods to Post Controller (video)

Inside the post controller…

class PostsController < ApplicationController def index
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
end
private def post_params
params.require(:post).permit(:title, :content)
end
end

Great. SAVE!

Now, let’s start up the server and see what we’ve accomplished…

rails s 

Navigate to http://localhost:3000/posts/new

And…

Image for post
Image for post

Ouch. Error.

But we know how to deal with this. We’re just missing a View file. See? READ THE ERROR MESSAGE. Do not fear the red.

Kill the server. crtl + c.

Inside the view/posts file…

Image for post
Image for post

Now, save. Then restart the server.

rails s

And…

Image for post
Image for post

#winning

Image for post
Image for post

Step 11: Create New Post Form (video)

This form is going to be a partial. A partial is a cool thing. Google what it is please. “what is a ruby on rails partial?”

There. We learning how to be a dev already. Googling stuff.

Anyways… a partial. We’ll want to create a new file underneath view/posts called:

_form.html.haml
Image for post
Image for post

Inside the form partial, using simple form…

= simple_form_for @post do |f|
= f.input :title
= f.input :content
= f.submit

Save.

We’re going to render the partial inside of our new.html.haml file…

= render 'form'
Image for post
Image for post

Let’s SAVE and then restart the server…

Image for post
Image for post

Ugly but hey at least it works.

Now back inside the post controller…

def create
@post = Post.new(post_params)
if @post.save
redirect_to @post
else
render 'new'
end
end

If we’re able to save our new post, redirect to the post show page (we haven’t create yet), else render the form again…

Let’s try it out!

And we get: The action ‘show’ could not be found for PostsController

That was to be expected. We don’t have a show action OR a show view, do we?

Step 12: Create the Show Action and View (video)

Inside the controller…

def show
@post = Post.find(params[:id])
end

And of course we also need the view…

Create a new file called:

show.html.haml

Inside that file…

%h1= @post.title
%p= @post.content
Image for post
Image for post

Restart the server and see what we’ve done…

Image for post
Image for post

Great! Still #winning.

Step 13: Add Ability to Edit and Destroy a Post (video)

Back inside the post controller…

We’re going to create some new actions / methods / whateveryouwannacallthem…

def edit
end
def update
end
def destroy
end

More meat coming inside these sandwiches but for now let’s do a bit of refactoring with Mackenzie…

Create a new private method called “find_post”…

def find_post
@post = Post.find(params[:id])
end

And now… a before action

before_action :find_post, only: [:show, :edit, :update, :destroy]

Now this private find_post method will be run before all of these actions… before the show, edit, update and destroy action…

We can now update our show action to…

def show
end

Cool. DRY principles in action here. DON’T REPEAT YOURSELF.

Actions looks like:

def edit
end
def update
if @post.update
redirect_to @post
else
render 'edit'
end
end
def destroy
@post.destroy
redirect_to root_path
end

You probably see that we don’t have a ‘edit’ view yet…

And we also don’t have a destroy or delete button in our view…

We’ll add those things…

The links first!

%h1= @post.title
%p= @post.content
= link_to "Edit", edit_post_path(@post)
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }
= link_to "Home", root_path

Looks like:

Image for post
Image for post

Now, for the edit post view…

Create a new file under the view/posts called edit.html.haml

Image for post
Image for post

And let’s check it out in our server…

rails s
Image for post
Image for post
Image for post
Image for post

Cool. Things are working as they should!

Now, let’s try to update a post…

Image for post
Image for post

Big ol FAIL.

Back into the post controller…

We just need to add in (post_params) to our update action…

Like so:

def update
if @post.update(post_params)
redirect_to @post
else
render 'edit'
end
end

Ok, try again!

Image for post
Image for post

It works!

Great. Now to test the delete function. Go create a new post and then delete it.

Worked for me.

Now, let’s commit our work to Github before moving on…

git status

woah, lots of red.

git add .

and then

git commit -m "added post actions and views; new, create, update, destroy"

then

git push

We done. Moving forward.

Step 14: Looping Through Posts on the Index Page (video)

Inside the post controller, update the Index action:

def index
@posts = Post.all.order("created_at DESC")
end

Then inside the index view file…

%h1 Hi there! This is the Index Page.- @posts.each do |post|
%h2= post.title
%p
Published at
= time_ago_in_words(post.created_at)
= link_to "New Post", new_post_path

This should loop through all the Posts in our database…

Let’s try it out!

Image for post
Image for post

Great. Design is sh!t but it’s working. Moving forward.

Actually wait. Let’s update to:

%h1 Hi there! This is the Index Page.- @posts.each do |post|
%h2= link_to post.title, post
%p
Published at
= time_ago_in_words(post.created_at)
= link_to "New Post", new_post_path

We’ve made the titles links to the full post.

Ok, let’s commit to git.

git status
git add .
git commit -m "added loop to publish all posts to index page"
git push

Cool. Onward.

Step 15: Setting up Devise (video)

Check out the github page for devise. It’ll walk you through getting started. Or just follow along here :)

In our command line:

rails generate devise:install

then follow those directions…

In the config/environments/development.rb file…

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

we already have a root url…

then copy paste those notices into our application.html.erb file…

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

then skip the heroku piece…

skip the views piece…

run the model generator!

rails generate devise user 

Cool. Onward. My screen looks like this:

Image for post
Image for post

Now, migrate that database…

rake db:migrate 
Image for post
Image for post

Ok. Now let’s run the server and try to sign up!

rails s

Nav to http://localhost:3000/users/sign_up

And look! A sign up form. Thanks Devise!

Image for post
Image for post

Now to try signing myself up…

Image for post
Image for post

Great. See the message up top?

Cool. Onward. Git commit time.

git add .
git commit -m "set up devise"
git push

Step 16: Connect Users and Posts (video)

Back inside the controller…

def new
@post = current_user.posts.build
end

and…

def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post
else
render 'new'
end
end

save.

Now to create an association!

inside post.rb…

class Post < ApplicationRecord
belongs_to :user
end

And, inside user.rb…

class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :posts
end

Whelp, looks like ol Mackenzie hit a snag here so we need to work backwards and add a user column to our database…. onward.

Video is here.

rails g migration add_user_id_to_posts user_id:integer

then,

rake db:migrate

then back inside the rails console…

rails c

and

@posts = Post.first

Check it:

Image for post
Image for post

We have a user:id: nil

Great. Working.

Now let’s try creating a new post and see if the new post is tied to me, the user.

So I’m starting up the server again, I’m creating a new post, and then jumping back inside the console to see if the new post is tied to a user id…

Image for post
Image for post

IT IS! Success.

Image for post
Image for post

Great. Everything is working correctly.

Now, I’m going to work backwards and assign all the old posts to ME!

Looks something like this:

@post = Post.first
@post.user_id = 1
@post.save

Then, you can do something like this to see all the posts and see which have nil user_ids…

@post = Post.all

Op, there they are…

Select one that is unassigned like so:

@post = Post.find(3)
@post.user_id = 1
@post.save

Follow Mac here.

Cool. Repeat. Save. Onward.

Inside our index page, let’s display the user email alongside the posts…

%h1 Hi there! This is the Index Page.- @posts.each do |post|
%h2= link_to post.title, post
%p
Published at
= time_ago_in_words(post.created_at)
= post.user.email
= link_to "New Post", new_post_path

Start up your server and see if this worked. Worked for me.

Onwards. Git commit.

git add .
git commit -m "added association between post and user"
git push

Step 17: Add Navigation (video)

Ok, inside the application.html.erb file…

Converting to HAML!

Rename to application.html.haml

And then update the file to haml syntax…

!!!
%html
%head
%title Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
%body
%p.notice= notice
%p.alert= alert
= yield

try it out in the localhost to see if everything is ok…

Works. Great. Onwards.

Now to create a header. Fun. HAML is annoying to me but hey, let’s just learn something new why don’t we?

Onwards.

!!!
%html
%head
%title Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
%body
%nav
#logo
%p David's Rails Forum
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

then inside my stylesheet, following MC here:

Image for post
Image for post

Update that file name to applcation.css.scss

And add:

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}

Save. Refresh. See?

Ugh. HAML. Keep at it David. Just keep going.

Ripped the new post link out of the index page and stuck it inside the application.html.haml page…

Index page:

%h1 Hi there! This is the Index Page.- @posts.each do |post|
%h2= link_to post.title, post
%p
Published at
= time_ago_in_words(post.created_at)
= post.user.email

Application page:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header
#logo
%p David's Rails Forum
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

Ok geez. That was 37 minutes of following Mackenzie, and 2.5 hours of my time. Time to get up and get outside for a bit. Stretch, get some sun, that sort of thing. Back later this afternoon….

Ok! Back at it.

OK! So I’m here. And I just figured out how darn picky HAML is about your indentations and line breaks…

If your page doesn’t look like this, you might need to get really detailed about your spaces or tabs or indentations.

Image for post
Image for post
Image for post
Image for post

Little spaces / tabs joke there ^

Moving forward. More HAML. Just slogging through.

Updated fonts:

body {
font-family: 'Lato', sans-serif;
}
.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.main_header {
width: 80%;
max-width: 1140px;
margin: 0 auto;
}

And linked to the font resource:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header
#logo
%p David's Rails Forum
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

(the font came from Google fonts here)

Time to style the logo…

It’s actually pretty useful to see how MC uses Google Chrome to preview style changes. Check out the video here. If you aren’t interested, you’ll find the code below…

Application.css.scss file up to this point:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 80%;
max-width: 1140px;
margin: 0 auto;
#logo {
float: left;
p {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
}
}
#buttons {
float: right;
}
}

And the application.html.haml file:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header.clearfix
#logo
%p David's Rails Forum
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

Page looks like:

Image for post
Image for post

Now let’s add some colors yay.

Added a background color:

body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

More styling to follow…

Adding colors, updating width of the header, adding ‘normalize’, (again, you can follow along here, I’m not going to be super-detailed about documenting this styling stuff)

application.html.haml:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
%link{ rel: "stylesheet", href: "http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header.clearfix
.wrapper
#logo
%p David's Rails Forum
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

application.css.scss:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
p {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
}
}
#buttons {
float: right;
}
}

Updating the index.html.haml file now…

#posts
- @posts.each do |post|
.post
%h2.title= link_to post.title, post
%p.date
Published at
= time_ago_in_words(post.created_at)
= post.user.email

Now back inside the application.css.scss file, add:

#posts {
background: white;
padding: 2em 5%;
border-radius: .5em;
.post {
margin: 1em 0;
padding: 1em 0;
border-bottom: 1px solid #D1d1d1;
.title {
margin: 0;
a {
color: #397CAC;
text-decoration: none;
font-weight: 100;
font-size: 1.25rem;
}
}
.date {
margin-top: .25rem;
font-size: 0.9rem;
color: #B2BAC2;
}
}
}

Allllright, looking like a snack.

Image for post
Image for post

Next, style the New Post button…

#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
Image for post
Image for post

Not the worst!

Moving forward.

Inside the show.html.haml file…

#post_content 
%h1= @post.title
%p= @post.content
= link_to "Edit", edit_post_path(@post)
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }
= link_to "Home", root_path

And boom:

Image for post
Image for post

More style to follow.

#post_content {
background: white;
padding: 2em 5%;
border-radius: .5em;
h1 {
font-weight: 100;
font-size: 2em;
color: #397CAC;
margin-top: 0;
}
p {
color: #B2BAC2;
font-size: 0.9em;
line-height: 1.5;
font-size: 100;
}
}

Makes this junk happen:

Image for post
Image for post

Now we’re going to make “David’s Rails Forum” link back to the homepage…

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
%link{ rel: "stylesheet", href: "http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header.clearfix
.wrapper
#logo
%p= link_to "David's Rails Forum", root_path
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

Line updated:

%p= link_to "David's Rails Forum", root_path

And we need to style it a bit…

Updated the following:

.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
a {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
text-decoration: none;
color: #397CAC;
}
}
#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
}

The ENTIRE application.css.scss file looks like this right now:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
a {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
text-decoration: none;
color: #397CAC;
}
}
#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
}
#posts {
background: white;
padding: 2em 5%;
border-radius: .5em;
.post {
margin: 1em 0;
padding: 1em 0;
border-bottom: 1px solid #D1d1d1;
.title {
margin: 0;
a {
color: #397CAC;
text-decoration: none;
font-weight: 100;
font-size: 1.25rem;
}
}
.date {
margin-top: .25rem;
font-size: 0.9rem;
color: #B2BAC2;
}
}
}
#post_content {
background: white;
padding: 2em 5%;
border-radius: .5em;
h1 {
font-weight: 100;
font-size: 2em;
color: #397CAC;
margin-top: 0;
}
p {
color: #B2BAC2;
font-size: 0.9em;
line-height: 1.5;
font-size: 100;
}
}

And the ENTIRE application.html.haml file looks like this:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
%link{ rel: "stylesheet", href: "http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header.clearfix
.wrapper
#logo
%p= link_to "David's Rails Forum", root_path
#buttons
= link_to "New Post", new_post_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

Index.html.haml file looks like:

#posts
- @posts.each do |post|
.post
%h2.title= link_to post.title, post
%p.date
Published at
= time_ago_in_words(post.created_at)
= post.user.email

And the show.html.haml file looks like:

#post_content 
%h1= @post.title
%p= @post.content
= link_to "Edit", edit_post_path(@post)
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }
= link_to "Home", root_path

Onward…

Step 18: Users can sign in and out (video)

First, we’re going to add some links to the application template…

And you should know that when adding links, it’s useful to understand what kind of routes you have available in your application.

A link is generally in the formula of:

link_to "name of link", link_path

that “link_path” piece is a route. We can see all of our available routes in the application by running a rake route command in the terminal:

rake routes
Image for post
Image for post

So if I need to add a link to sign a user up, or sign a user in, I can run the command to see what the route is for a new user session or the route for a new user!

A link to sign up a new user would look like:

link_to "Sign up", new_user_registration_path

So I’ve added the links with some logic and the whole file looks like:

!!!
%html
%head
%title David's Rails Forum
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Lato", type: "text/css" }
%link{ rel: "stylesheet", href: "http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css" }
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%header.main_header.clearfix
.wrapper
#logo
%p= link_to "David's Rails Forum", root_path
#buttons
- if user_signed_in?
= link_to "New Post", new_post_path
- else
= link_to "Sign Up", new_user_registration_path
= link_to "Sign In", new_user_session_path
.wrapper
%p.notice= notice
%p.alert= alert
.wrapper
= yield

Testing this out in incognito mode in google chrome and we get:

Image for post
Image for post

Now we are going to create another user.

Image for post
Image for post
Image for post
Image for post

Great. Working. Now for a new post by the new user:

Image for post
Image for post

Now, we’re going to add a before action to the controller to authenticate the user…

class PostsController < ApplicationController
before_action :find_post, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]

This authenticate_user! action makes sure non-users can’t create, delete and edit content that they didn’t create, makes sure users can’t delete and edit other user’s content, and is a default feature provided by devise.

Next, we’ll create the ability to add comments!

But first, let’s push our changes to github. Go ahead and walk through the commands yourself. Use the screenshot below as a reference if you need it!

Image for post
Image for post

Step 19: Users can comment (video)

First, we are going to build a model for Comments:

rails g model Comment comment:text post:references user:references 

Translation: “Rails, please generate a model for me called “Comment”. Each comment will be text, and each comment will belong to a post and to a user.”

Now, let’s check out the migration file:

Image for post
Image for post

No changes to make, so let’s:

rake db:migrate
Image for post
Image for post

Now we need to update our post.rb model to associate it with Comments:

class Post < ApplicationRecord
belongs_to :user
has_many :comments
end

And user.rb model:

class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :posts
has_many :comments
end

Now to create some routes for our comments!

Inside the routes.rb file:

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

end
Image for post
Image for post

Now, let’s create a Comments controller!

rails g controller Comments
Image for post
Image for post

And now inside of the new controller file:

class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.create(params[:comment].permit(:comment))
if @comment.save
redirect_to post_path(@post)
else
render 'new'
end
end
end

And now let’s create some views for our comments! File names are:

_form.html.haml_comment.html.haml

And they should be saved underneath the view/comments folder…

Image for post
Image for post

Inside the _comment.html.haml file:

.comment
%p= comment.comment

Inside the _form.html.haml file:

= simple_form_for([@post, @post.comments.build]) do |f|
= f.input :comment
= f.submit

(here in MC’s video btw)

And now inside the posts/show.html.haml file:

#post_content 
%h1= @post.title
%p= @post.content
#comments
%h2= @post.comments.count
= render @post.comments
%h3 Reply to thread
= render "comments/form"
= link_to "Edit", edit_post_path(@post)
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }
= link_to "Home", root_path

Saving all these files and now checking out the browser. Fingers crossed…

Great! Style is weak but at least it’s working :)

Image for post
Image for post

Ok, now we need to do a little more work before we can add a comment…

Note: Consider checking out MC troubleshooting around minute 1:10:00! It’s a great look into working through a troubling spot. Link here. Watch through minute 1:13:50!

Comments controller should look like:

class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.create(params[:comment].permit(:comment))
@comment.user_id = current_user.id if current_user
@comment.save
if @comment.save
redirect_to post_path(@post)
else
render 'new'
end
end
end

_comments.html.haml should look like:

.comment
%p= comment.comment
%p= comment.user.email

And now let’s test it out:

Image for post
Image for post

Great! It’s working. I’m sure we’ll be styling this shortly :)

Next, let’s work on editing and deleting comments…

Inside the comments controller… Add a destroy action

def destroy
@post = Post.find(params[:post_id])
@comment = @post.comments.find(params[:id])
@comment.destroy
redirect_to post_path(@post)
end

Now, inside the comment partial, _comments.html.haml:

.comment
%p= comment.comment
%p= comment.user.email
= link_to "Delete Reply", [comment.post, comment], method: :delete, data: { confirm: "Are you sure?" }

Now, let’s refresh the page and see if this mess is working.

Image for post
Image for post

Yep. Great. It works.

Now for editing comments… Inside the _comment.html.haml partial:

.comment
%p= comment.comment
%p= comment.user.email
= link_to "Edit", edit_post_comment_path(comment.post, comment)
= link_to "Delete Reply", [comment.post, comment], method: :delete, data: { confirm: "Are you sure?" }

And inside the comments controller:

def edit
@post = Post.find(params[:post_id])
@comment = @post.comments.find(params[:id])
end
def update
@post = Post.find(params[:post_id])
@comment = @post.comments.find(params[:id])
if @comment.update(params[:comment].permit(:comment))
redirect_to post_path(@post)
else
render 'edit'
end
end
Image for post
Image for post

We can certainly see an edit option, but it doesn’t work quite yet:

Image for post
Image for post

We’re missing a template! Time to create a new view:

Create a new file: views/comments/edit.html.haml, and inside the file:

%h1 Edit Reply= simple_form_for([@post, @comment]) do |f| 
= f.input :comment
= f.submit

Annnnnnnd, refresh:

Image for post
Image for post

Great! It worked :)

One quick small change inside the Comments partial:

.comment
%p= comment.comment
%p= comment.user.email
= link_to "Edit", edit_post_comment_path(comment.post, comment)
= link_to "Delete", [comment.post, comment], method: :delete, data: { confirm: "Are you sure?" }

updated delete link to “Delete” instead of “Delete reply”

Git time!

Step 20: Style the Comments (video)

Whole style sheet:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
a {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
text-decoration: none;
color: #397CAC;
}
}
#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
}
#posts {
background: white;
padding: 2em 5%;
border-radius: .5em;
.post {
margin: 1em 0;
padding: 1em 0;
border-bottom: 1px solid #D1d1d1;
.title {
margin: 0;
a {
color: #397CAC;
text-decoration: none;
font-weight: 100;
font-size: 1.25rem;
}
}
.date {
margin-top: .25rem;
font-size: 0.9rem;
color: #B2BAC2;
}
}
}
#post_content {
background: white;
padding: 2em 5%;
border-radius: .5em;
h1 {
font-weight: 100;
font-size: 2em;
color: #397CAC;
margin-top: 0;
}
p {
color: #B2BAC2;
font-size: 0.9em;
line-height: 1.5;
font-size: 100;
}
#comments {
.comment {
border-bottom: 1px solid #d1d1d1;
padding-bottom: 1em;
margin-bottom: 1em;
.comment_content {
margin: 0;
padding: 0;
}
.comment_author {
color: #397CAC;
margin-top: 0.5rem;
font-size: 0.6rem;
font-weight: 700;
}
}
}
}

And now, inside the comment partial:

.comment
%p.comment_content= comment.comment
%p.comment_author= comment.user.email
= link_to "Edit", edit_post_comment_path(comment.post, comment)
= link_to "Delete", [comment.post, comment], method: :delete, data: { confirm: "Are you sure?" }

Looking better:

Image for post
Image for post

Now to tweak the buttons!

Tweak, tweak, tweak…

Image for post
Image for post

you know…. just sorta kinda make things like a little nicer.

Application.css.scss up to this point:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
a {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
text-decoration: none;
color: #397CAC;
}
}
#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
}
#posts {
background: white;
padding: 2em 5%;
border-radius: .5em;
.post {
margin: 1em 0;
padding: 1em 0;
border-bottom: 1px solid #D1d1d1;
.title {
margin: 0;
a {
color: #397CAC;
text-decoration: none;
font-weight: 100;
font-size: 1.25rem;
}
}
.date {
margin-top: .25rem;
font-size: 0.9rem;
color: #B2BAC2;
}
}
}
#post_content {
background: white;
padding: 2em 5%;
border-radius: .5em;
h1 {
font-weight: 100;
font-size: 2em;
color: #397CAC;
margin-top: 0;
}
p {
color: #B2BAC2;
font-size: 0.9em;
line-height: 1.5;
font-size: 100;
}
#comments {
.comment {
border-bottom: 1px solid #d1d1d1;
padding-bottom: 1em;
margin-bottom: 1em;
.content {
width: 75%;
float: left;
}
.buttons {
width: 25%;
float: left;
font-size: 0.8em;
text-align: right;
padding-top: 1.5em;
a {
color: #397CAC;
border: 1px solid #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
text-decoration: none;
margin-right: 2%;
}
}
.comment_content {
margin: 0;
padding: 0;
}
.comment_author {
color: #397CAC;
margin-top: 0.5rem;
font-size: 0.6rem;
font-weight: 700;
}
}
}
}

And the _comment.html.haml partial:

.comment.clearfix
.content
%p.comment_content= comment.comment
%p.comment_author= comment.user.email
.buttons
= link_to "Edit", edit_post_comment_path(comment.post, comment)
= link_to "Delete", [comment.post, comment], method: :delete, data: { confirm: "Are you sure?" }

Looking like:

Image for post
Image for post

How about that comment form though? We’ll tackle that soon.

Inside the show.html.haml file:

#post_content 
%h1= @post.title
%p= @post.content
#comments
%h2
= @post.comments.count
Comments
= render @post.comments
%h3 Reply to thread
= render "comments/form"
= link_to "Edit", edit_post_path(@post)
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }
= link_to "Home", root_path

Very small change…

But now instead of “2” we see “2 Comments”

Image for post
Image for post

NOW, let’s style the comment form…

(We are here in the video)

Following along with MC, I’ve updated the show.html.haml page to:

#post_content 
%h1= @post.title
%p= @post.content
#comments
%h2
= @post.comments.count
Comments
= render @post.comments
%h3 Reply to thread
= render "comments/form"
%br/
%hr/
%br/
= link_to "Edit", edit_post_path(@post), class: "button"
= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure you want to do this ?" }, class: "button"

And I’ve updated the application.css.scss file to:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
body {
font-family: 'Lato', sans-serif;
background: #EDEFF0;
}

.wrapper {
width: 60%;
max-width: 1140px;
margin: 0 auto;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.main_header {
width: 100%;
margin: 0 auto;
background: white;
#logo {
float: left;
a {
text-transform: uppercase;
font-weight: 700;
letter-spacing: -1px;
font-size: 1.2rem;
text-decoration: none;
color: #397CAC;
}
}
#buttons {
float: right;
a {
line-height: 60px;
background: #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
color: white;
text-decoration: none;
font-weight: 100;
}
}
}
#posts {
background: white;
padding: 2em 5%;
border-radius: .5em;
.post {
margin: 1em 0;
padding: 1em 0;
border-bottom: 1px solid #D1d1d1;
.title {
margin: 0;
a {
color: #397CAC;
text-decoration: none;
font-weight: 100;
font-size: 1.25rem;
}
}
.date {
margin-top: .25rem;
font-size: 0.9rem;
color: #B2BAC2;
}
}
}
.button {
color: #397CAC;
border: 1px solid #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
text-decoration: none;
margin-right: 2%;
}
#post_content {
background: white;
padding: 2em 5%;
border-radius: .5em;
h1 {
font-weight: 100;
font-size: 2em;
color: #397CAC;
margin-top: 0;
}
p {
color: #B2BAC2;
font-size: 0.9em;
line-height: 1.5;
font-size: 100;
}
#comments {
.comment {
border-bottom: 1px solid #d1d1d1;
padding-bottom: 1em;
margin-bottom: 1em;
.content {
width: 75%;
float: left;
}
.buttons {
width: 25%;
float: left;
font-size: 0.8em;
text-align: right;
padding-top: 1.5em;
a {
color: #397CAC;
border: 1px solid #397CAC;
padding: .5em 1em;
border-radius: 0.2em;
text-decoration: none;
margin-right: 2%;
}
}
.comment_content {
margin: 0;
padding: 0;
}
.comment_author {
color: #397CAC;
margin-top: 0.5rem;
font-size: 0.6rem;
font-weight: 700;
}
}
input[type="submit"] {
background: #397CAC;
border: none;
color: white;
font-weight: 100;
padding: .5em 1em;
border-radius: .2em;
}
textarea {
width: 100%;
min-height: 200px;
border: 1px solid #d1d1d1;
border-radius: .2em;
margin: 1em 0;
}
}
}

Great! Looking like:

Image for post
Image for post

Cool. I think we’re done here.

Yep, we’re definitely done here.

There’s still a few tings to do on the style side, but we’ve built out a great Rails app and we’re done with the heavy lifting. MC is signing out and so am I.

Image for post
Image for post

My github repository for this project is here.

Hope you enjoyed the tutorial!

Peace.

Thanks for reading! I hope this article was helpful.

If you want to support the author, the best way is to purchase your next Amazon shipment through my affiliate link here. Or, consider buying me a coffee through venmo (David-Allen-13), cash app (deallen7), or paypal (davideallen).

Your donations, comments, and claps keep me motivated to create more material. I appreciate you! :D

Documentation and tutorials on Python, Programming, and Data Analysis. FPL Addict.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store