Mackenzie Child's video really inspired me. So I decided to follow all of his rails video tutorial to learn how to build a web app. Through the video, I would try to build the web app by my self and record the courses step by step in text to facilitate the review.
This time we're going to build a Pinterest like application which includes the Pins, Users, Image uploading, and Voting as well. A user must sign-in in order to submit a new pin or vote, as well as to delete a pin. And then we got some amazingly going on, so the Pins will scale to the size of the PinBoard(window size).
https://mackenziechild.me/12-in-12/4/
- Users
- Pins
- Image Uploading
- Voting
- HAML
- Masonry
- Bootstrap
$ rails new pin_board
Chage directory to the pin_board. Under pin_board/Gemfile
, add gem 'therubyracer'
, save and run bundle install
.
Note:
Because there is no Javascript interpreter for Rails on Ubuntu Operation System, we have to install Node.js
or therubyracer
to get the Javascript interpreter.
$ bundle install
Then run the rails server
and go to http://localhost:3000
to make sure everything is correct.
Then we're going to install a few gems that we're going to use in this project.
gem 'haml', '~>4.0.5'
gem 'bootstrap-sass', '~> 3.2.0.2'
gem 'simple_form', github: 'kesha-antonov/simple_form', branch: 'rails-5-0'
gem 'devise'
Let's do bundle install and restart the server.
Then, we're going to generate our model for our Pins.
$ rails g model Pin title:string description:text
$ rake db:migrate
And let's go ahead to generate our controller.
$ rails g controller Pins
Then we're going to setup a few pages. First thing we'll do index to hold the Pins.
In app/controllers/pins_controller.rb
class PinsController < ApplicationController
def index
end
end
And then in our config/routes.rb
Rails.application.routes.draw do
resources :pins
root "pins#index"
end
Then let's make up template under app/views/pins
, and create a new file index.html.haml
.
In app/views/pins/index.html.haml
%h1 This is the index placeholder.
Let's add the ability of CRUD(the Create, Read ,Update , and Destroy)
In app/controllers/pins_controller.rb
class PinsController < ApplicationController
def index
end
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
end
private
def pin_params
params.require(:pin).permit(:title, :description)
end
end
Next, we need to create a new file new.html.haml
for our new form.
So in app/views/pins/new.html.haml
%h1 New Form
= render 'form'
= link_to "Back", root_path
Let's create another file _form.html.haml
under app/views/pins
.
To use this on simple_form
, we're gonna do
$ rails g simple_form:install --bootstrap
Go back to our app/views/pins/_form.html.haml
= simple_form_for @pin, html: { multipart: true } do |f|
- if @pin.errors.any?
#errors
%h2
= pluralize(@pin.error.count, "error")
prevented this Pin form saving
%ul
-@pin.errors.full_messages.each do |msg|
%li = msg
.form-group
= f.input :title, input_html: { class: 'form-control'}
.form-group
= f.input :description, input_html: { class: 'form-control'}
= f.button :submit, class: "btn btn-primary"
Note:
pluralize
is a rails helper. What the pluralize does is anything past account of one, so at 2,3 on , it will automatically pluralize it for us.
http://rails.ruby.tw/getting_started.html
http://apidock.com/rails/ActionView/Helpers/TextHelper/pluralize
Back to the browser, go to http://localhost:3000/pins/new
Then, let's go back to our app/controllers/pins_controller.rb
class PinsController < ApplicationController
...
...
def create
@pin = Pin.new(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end
...
...
And in app/views/layouts
, first thing we wonna do is rename application.html.erb
to application.html.haml
.
Then convert the html tag to haml and add the flash message.
!!! 5
%html
%head
%title PinBoard
= csrf_meta_tags
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body
- flash.each do |name, msg|
= content_tag :div, msg, class: "alert alert-info"
= yield
To show the Pin, we have to back to the controller.
In app/controllers/pins_controller.rb
class PinsController < ApplicationController
before_action :find_pin, only: [:show, :edit, :update, :destroy]
def index
end
def show
end
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end
private
def pin_params
params.require(:pin).permit(:title, :description)
end
def find_pin
@pin = Pin.find(params[:id])
end
end
Then we next need to create a view show.html.haml
for show.
In app/views/pins/show.html.haml
%h1= @pin.title
%p= simple_format @pin.description
= link_to "Back", root_path
Now, we can create our first Pin and show it up.
So far, we have the flash message, title and description.
Then, on the index.html.haml
, let's list out all the Pins.
In app/controllers/pins_controller.rb
...
def index
@pins = Pin.all.order("created_at DESC")
end
...
In app/views/pins/index.html.haml
- @pins.each do |pin|
%h2= link_to pin.title, pin
Now, we need to add the ability to edit
and destroy
a Pin as well.
In app/controllers/pins_controller.rb
class PinsController < ApplicationController
before_action :find_pin, only: [:show, :edit, :update, :destroy]
...
...
def edit
end
def update
if @pin.update(pin_params)
redirect_to @pin, notice: "Pin was Successfully updated!"
else
render 'edit'
end
end
def destroy
end
...
...
Next, we need to create a new file edit.html.haml
for the edit page
In app/views/pins/edit.html.haml
%h1 Edit Pin
= render 'form'
= link_to 'Cancel', pin_path
So on the show page, let's add a link to the edit form.
In app/views/pins/show.html.haml
%h1= @pin.title
%p= simple_format @pin.description
= link_to "Back", root_path
= link_to "Edit", edit_pin_path
Now, let's add the ability to destroy. So in our controller. let's do:
app/controllers/pins_controller.rb
...
...
def destroy
@pin.destroy
redirect_to root_path
end
...
...
Then, in our show, we need to add a destroy link
.
In app/views/pins/show.html.haml
%h1= @pin.title
%p= simple_format @pin.description
= link_to "Back", root_path
= link_to "Edit", edit_pin_path
= link_to "Delete", pin_path, method: :delete, data: {confirm: "Are you sure?"}
And we want to add a New Pin
link in our index.html.haml
page.
app/views/pins/index.html.haml
= link_to "New Pin", new_pin_path
- @pins.each do |pin|
%h2= link_to pin.title, pin
We're gonna to use devise
gem.
$ rails g devise:install
Some setup you must do manually if you haven't yet:
1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
And
$ rails g devise:views
Next step is generate a devise model.
$ rails g devise User
$ rake db:migrate
Let's restart the server and go to http://localhost:3000/users/sign_up
to make sure everything is correct.
So what we need to do next is make sure that each Pin that is created has a user assigned to it.
In app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :pins
end
And in app/models/pin.rb
class Pin < ApplicationRecord
belongs_to :user
end
Then we need to generate migration so that our Pin has a user_id column.
$ rails g migration add_user_id_to_pins user_id:integer:index
$ rake db:migrate
That's pop into the rails console
$ rails c
In rails console
> @pin = Pin.first
irb(main):001:0> @pin = Pin.first
Pin Load (0.5ms) SELECT "pins".* FROM "pins" ORDER BY "pins"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Pin id: 2, title: "Pinterest", description: "Pinterest is a web and mobile application company ...", created_at: "2016-07-29 08:26:03", updated_at: "2016-07-29 08:26:03", user_id: nil>
You can see the user_id:
nil`. So what I'll do is:
> @user = User.first
> @pin.user = @user
> @pin
irb(main):001:0> @pin = Pin.first
Pin Load (0.5ms) SELECT "pins".* FROM "pins" ORDER BY "pins"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Pin id: 2, title: "Pinterest", description: "Pinterest is a web and mobile application company ...",
created_at: "2016-07-29 08:26:03", updated_at: "2016-07-29 08:26:03", user_id: nil>
You can see the user_id: 1
. Then
> @pin.save
Let's test in app/views/pins/show.html.haml
%h1= @pin.title
%p= simple_format @pin.description
%p
Submitted by
= @pin.user.email
%br
= link_to "Back", root_path
= link_to "Edit", edit_pin_path
= link_to "Delete", pin_path, method: :delete, data: {confirm: "Are you sure?"}
If we create a new Pin right now, it's not going to save because we didn't have @pin.user.email
for that Pin.
So back to our app/controllers/pins_controller.rb
, we need to tweak the new action
and create action
a bit.
We change
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end
to
def new
@pin = current_user.pins.build
end
def create
@pin = current_user.pins.build(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end
https://github.com/twbs/bootstrap-sass
We already have the gem install.
Next, we need to go into app/assets/stylesheets
and rename application.css
to application.css.scss
.
And then, we gonna to import Bootstrap styles in app/assets/stylesheets/application.css.scss
:
@import "bootstrap-sprockets";
@import "bootstrap";
Require Bootstrap Javascripts in app/assets/javascripts/application.js
:
//= require jquery
//= require bootstrap-sprockets
So to start, I'm going to add the application layout file.
Under app/views/layouts/application.html.haml
!!! 5
%html
%head
%title Pin Board
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%nav.navbar.navbar-default
.container
.navbar-brand= link_to "Pin Board", root_path
- if user_signed_in?
%ul.nav.navbar-nav.navbar-right
%li= link_to "New Pin", new_pin_path
%li= link_to "Account", edit_user_registration_path
%li= link_to "Sign Out", destroy_user_session_path, method: :delete
- else
%ul.nav.navbar-nav.navbar-right
%li= link_to "Sign Up", new_user_registration_path
%li= link_to "Sign In", new_user_session_path
.container
- flash.each do |name, msg|
= content_tag :div, msg, class: "alert alert-info"
= yield
Let's add wrapper around the new and edit page.
Under app/views/pins/new.html.haml
.col-md-8.col-md-offset-3
%h1 New Form
= render 'form'
= link_to "Back", root_path
Under app/views/pins/edit.html.haml
.col-md-8.col-md-offset-3
%h1 Edit Pin
= render 'form'
= link_to 'Cancel', pin_path
Next, we want to add the ability to upload images.
So we add paperclip
to our Gemfile
.
gem 'paperclip', '~> 4.2.0'
https://github.com/thoughtbot/paperclip
Then we run bundle install
and restart our server.
Now, we need to add has_attached_file
and the validates_attachment_content_type
to
app/models/pin.rb
class Pin < ApplicationRecord
belongs_to :user
has_attached_file :image, :styles => { :medium => "300x300>" }
validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/
end
And next, we need to add migration. We can do that by running:
$ rails g paperclip pin image
$ rake db:migrate
Next, in our form app/views/_form.html.haml
= simple_form_for @pin, html: { multipart: true } do |f|
- if @pin.errors.any?
#errors
%h2
= pluralize(@pin.error.count, "error")
prevented this Pin form saving
%ul
-@pin.errors.full_messages.each do |msg|
%li = msg
.form-group
= f.input :image, input_html: { class: 'form-control'}
.form-group
= f.input :title, input_html: { class: 'form-control'}
.form-group
= f.input :description, input_html: { class: 'form-control'}
= f.button :submit, class: "btn btn-primary"
Now, let's do New Pin
in browser:
One last thing we need to do is add image
to our pin_params
action.
app/controllers/pins_controller.rb
def pin_params
params.require(:pin).permit(:title, :description, :image)
end
And let's go into the app/views/show.html.haml
, and add image_tag
to the top.
= image_tag @pin.image.url(:medium)
%h1= @pin.title
%p= simple_format @pin.description
%p
Submitted by
= @pin.user.email
%br
= link_to "Back", root_path
= link_to "Edit", edit_pin_path
= link_to "Delete", pin_path, method: :delete, data: {confirm: "Are you sure?"}
Now, let's add this to the index.html.haml
as well.
In app/views/index.html.haml
- @pins.each do |pin|
= link_to (image_tag pin.image.url(:medium)), pin
%h2= link_to pin.title, pin
Then, I want to show the user which image they are editing or which image they are currently uploading.
In app/views/pins/edit.html.haml
.col-md-8.col-md-offset-3
%h1 Edit Pin
= image_tag @pin.image.url(:medium)
= render 'form'
= link_to 'Cancel', pin_path
Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax.
https://github.com/kristianmandrup/masonry-rails
Let's go to our Gemfile
, we need a gem called masonry-rails
.
In Gemfile
, we add this line, run bundle install and restart the server.
gem 'masonry-rails', '~> 0.2.1'
In app/assets/javascripts/application.js
, under jquery
//= require jquery
//= require jquery_ujs
//= require masonry/jquery.masonry
//= require bootstrap-sprockets
//= require turbolinks
//= require_tree .
To get this work, I'm going to add some styling and coffescript.
In app/assets/javascripts/pin.coffee
$ ->
$('#pins').imagesLoaded ->
$('#pins').masonry
itemSelector: '.box'
isFitWidth: true
In our app/views/pins/index.html.haml
, we need to add:
#pins.transitions-enabled
- @pins.each do |pin|
.box.panel.panel-default
= link_to (image_tag pin.image.url), pin
.panel-body
%h2= link_to pin.title, pin
Then, let's add some styles (*= require 'masonry/transitions'
and some css).
In app/assets/stylesheets/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, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require 'masonry/transitions'
*= require_tree .
*= require_self
*/
@import "bootstrap-sprockets";
@import "bootstrap";
body {
background: #E9E9E9;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 100;
}
nav {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
.navbar-brand {
a {
color: #BD1E23;
font-weight: bold;
&:hover {
text-decoration: none;
}
}
}
}
#pins {
margin: 0 auto;
width: 100%;
.box {
margin: 10px;
width: 350px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
border-radius: 7px;
text-align: center;
img {
max-width: 100%;
height: auto;
}
h2 {
font-size: 22px;
margin: 0;
padding: 25px 10px;
a {
color: #474747;
}
}
.user {
font-size: 12px;
border-top: 1px solid #EAEAEA;
padding: 15px;
margin: 0;
}
}
}
#edit_page {
.current_image {
img {
display: block;
margin: 20px 0;
}
}
}
#pin_show {
.panel-heading {
padding: 0;
}
.pin_image {
img {
max-width: 100%;
width: 100%;
display: block;
margin: 0 auto;
}
}
.panel-body {
padding: 35px;
h1 {
margin: 0 0 10px 0;
}
.description {
color: #868686;
line-height: 1.75;
margin: 0;
}
}
.panel-footer {
padding: 20px 35px;
p {
margin: 0;
}
.user {
padding-top: 8px;
}
}
}
textarea {
min-height: 250px;
}
Then, let's styling the show page.
In app/views/pins/show.html.haml
#pin_show.row
.col-md-8.col-md-offset-2
.panel.panel-default
.panel-heading.pin_image
= image_tag @pin.image.url
.panel-body
%h1= @pin.title
%p.description= @pin.description
.panel-footer
.row
.col-md-6
%p.user
Submitted by
= @pin.user.email
.col-md-6
.btn-group.pull-right
= link_to "Edit", edit_pin_path, class: "btn btn-default"
= link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-default"
Last, I wanna add a user under the title.
In app/views/pins/index.html.haml
#pins.transitions-enabled
- @pins.each do |pin|
.box.panel.panel-default
= link_to (image_tag pin.image.url), pin
.panel-body
%h2= link_to pin.title, pin
%p.user
Submitted by
= pin.user.email
To do voting, we're going to need to use acts_as_votable
gem.
https://github.com/ryanto/acts_as_votable
gem 'acts_as_votable', '~> 0.10.0'
Do bundel install and restar our server as well. Next we need to do is migration.
$ rails g acts_as_votable:migration
$ rake db:migrate
Then, inside of our pin model app/models/pin.rb
, we add acts_as_votable
on the top.
class Pin < ApplicationRecord
acts_as_votable
belongs_to :user
has_attached_file :image, :styles => { :medium => "300x300>" }
validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/
end
The next we need to add some routes.
In config/routes.rb
Rails.application.routes.draw do
devise_for :users
resources :pins do
member do
put "like", to: "pins#upvote"
end
end
root "pins#index"
end
Next, inside of our app/controllers/pins_controller.rb
, I'm going to create a upvote action
.
class PinsController < ApplicationController
before_action :find_pin, only: [:show, :edit, :update, :destroy, :upvote]
...
...
def upvote
@pin.upvote_by current_user
redirect_to :back
end
...
...
Then, in our show page app/views/pins/show.html.haml
#pin_show.row
.col-md-8.col-md-offset-2
.panel.panel-default
.panel-heading.pin_image
= image_tag @pin.image.url
.panel-body
%h1= @pin.title
%p.description= @pin.description
.panel-footer
.row
.col-md-6
%p.user
Submitted by
= @pin.user.email
.col-md-6
.btn-group.pull-right
= link_to like_pin_path(@pin), method: :put, class: "btn btn-default" do
%span.glyphicon.glyphicon-heart
= @pin.get_upvotes.size
= link_to "Edit", edit_pin_path, class: "btn btn-default"
= link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-default"
In app/views/pins/show.html.haml
...
...
- if user_signed_in?
= link_to "Edit", edit_pin_path, class: "btn btn-default"
= link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-default"
Users unable to create a post or like a post if they don't sign in.
In app/controllers/pins_controller.rb
class PinsController < ApplicationController
before_action :find_pin, only: [:show, :edit, :update, :destroy, :upvote]
before_action :authenticate_user!, except: [:index, :show]
...
...
In app/views/pins/new.html.haml
.col-md-8.col-md-offset-2
.row
.panel.panel-default
.panel-heading
%h1 Create A New Pin
.panel-body
= render 'form'
In app/views/pins/edit.html.haml
#edit_page.col-md-8.col-md-offset-2
.row
.panel.panel-default
.panel-heading
%h1 Edit Your Pin
.panel-body
.current_image
%strong.center Current Image
= image_tag @pin.image.url(:medium)
= render 'form'
In app/views/devise/registrations/new.html.erb
<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Sign up</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %> <% if @validatable %><i>(<%= @minimum_password_length %> characters minimum)</i><% end %><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.submit "Sign up", class: "btn btn-primary" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>
</div>
</div>
</div>
In app/views/devise/registrations/edit.html.erb
<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Edit Your Account</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="form-group">
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.submit "Update", class: "btn btn-primary" %>
</div>
<% end %>
</div>
<div class="panel-footer">
<h3>Cancel my account</h3>
<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: "btn btn-default" %></p>
<br>
<%= link_to "Back", :back, class: "btn btn-default" %>
</div>
</div>
</div>
</div>
In app/views/devise/sessions/new.html.erb
<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Sign In</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<% if devise_mapping.rememberable? -%>
<div class="form-group">
<%= f.check_box :remember_me %> <%= f.label :remember_me %>
</div>
<% end -%>
<div class="form-group">
<%= f.submit "Log in", class: "btn btn-primary" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>
</div>
</div>
</div>
Finished!