Luanotes
Published in

Luanotes

Build Instagram by Ruby on Rails (Part 2)

Upload Photo for Post & Upload Avatar for Users

Previous Post:

What’ll you learn after reading the article?

  • CRUD in Active Record
  • Association in Active Record
  • Validation in Active Record
  • Active Storage feature in Rails 5.2
  • Pagination with Kaminari gem.

Table of Contents:

  • Create the Post model.
  • Association between User and Post models.
  • Using Active Storage to attach an image in a Post.
  • Create a new Post and add validation to Post.
  • Show posts on the Homepage and User profile page.
  • Using Kaminari gem to paginate Posts.
  • Upload Avatar to User.

Creating the Post model

rails generate model Post description:string user_id:integer
Running via Spring preloader in process 5011
invoke active_record
create db/migrate/20180922091640_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
class CreatePosts < ActiveRecord::Migration[5.1]
def change
create_table :posts do |t|
t.string :description
t.integer :user_id
t.timestamps
end
end
end
rails db:migrate
== 20180922091640 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0404s
== 20180922091640 CreatePosts: migrated (0.0405s) =============================
class Post < ApplicationRecord
end

Add an Association between User and Post models

class User < ApplicationRecord
end
class Post < ApplicationRecord
end
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
end

Introduce Active Storage

rails active_storage:install
$ rails db:migrate== 20180924134051 CreateActiveStorageTables: migrating ============ 
— create_table(:active_storage_blobs)
-> 0.0576s
— create_table(:active_storage_attachments)
-> 0.0106s
== 20180924134051 CreateActiveStorageTables: migrated (0.0684s) ====
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
access_key_id: your_access_key_id
secret_access_key: your_secret_access_key
region: us-east-1
bucket: your_own_bucket
# Store uploaded files on the local file system
config.active_storage.service = :local
# Store uploaded files on Amazon S3
config.active_storage.service = :amazon

Using Active Storage to store Image in Post

Attaching Image to Post

class Post < ApplicationRecord
belongs_to :user
has_one_attached :image
end

Create a New Post

  • Create Post controller
  • Add create action into Post controller
  • Add a Form to create Post

Create Post controller:

rails generate controller Posts
>>
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
invoke test_unit
create test/controllers/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/posts.coffee
invoke scss
create app/assets/stylesheets/posts.scss

Add create action:

def create
Post.create(post_params)
redirect_to root_path
end
privatedef post_params
params.require(:post).permit(:description, :image, :user_id)
end

Add a form to create a new Post in Homepage

<%= form_for Post.new do |f| %>
<div class="form-group">
<%= f.text_field :description %>
</div>
<div class="form-group">
<%= f.file_field :image %>
</div>
<div class="form-group">
<%= f.text_field :user_id, value: current_user.id, class:'d-none'%>
</div>
<br>
<div class="text-center">
<%= f.submit 'Create Post', class: 'btn btn-primary' %>
</div>
<% end %>
<%= f.file_field :image %>
<%= f.text_field :user_id, value: current_user.id, class:'d-none' %>
resources :posts, only: [:new, :create]
.form-upload{
border: 1px solid #dbdbdb;
height: auto;
margin: auto 160px;
padding: 30px;
border-radius: 3px;
input[type='text']{
width: 100%;
}
}
Form Create a new Post

Add validation to Post

Can't resolve image into URL: to_model delegated to attachment, but attachment is nil

Add validate to Post model:

class Post < ApplicationRecord

...
validate :image_presence def image_presence
errors.add(:image, "can't be blank") unless image.attached?
end
end

When validation methods run?

Try it in your console:

post = Post.new(description: "abc", user_id: User.last.id)
=> #<Post id: nil, description: "abc", user_id: 1, created_at: nil, updated_at: nil
post.save
#=> false
post.valid?
#=> false
post.errors.messages
#=> {:image=>["can't be blank"]}
post.image.attached?
#=> false

Show Posts on Homepage

Post.all.order(created_at: :desc)
class HomeController < ApplicationController
def index
@posts = Post.order(created_at: :desc)
end
end
<div class="posts">
<% @posts.each do |post| %>
<section class="post">
<!-- post view -->
</section>
<% end %>
</div>
<%= image_tag post.image, class: 'main-image' %>
<section class="post">
<div class="user">
<div class="avatar">
<img src="user_avatar.jpg">
</div>
<div class="username">
<%= post.user.username %>
</div>
</div>
<%= image_tag post.image, class: 'main-image' %>
<div class="description">
<%= post.description %>
</div>
</section>
.posts{
margin: 50px 180px 10px;
.post{
border: 1px solid #efefef;
margin: 60px 0;
border-radius: 3px;
.user{
border-bottom: 1px solid #efefef;
display: flex;
.avatar{
width: 30px;
margin: 8px;
img{
width: 100%;
border: 1px solid #efefef;
border-radius: 50%;
}
}
.username{
padding-top: 13px;
color: #262626;
font-size: 14px;
font-weight: 600;
}
}
.main-image{
width: 100%;
border-bottom: 1px solid #f4f4f4;
}
.description{
padding: 20px;
}
}
}
A Post in Homepage

Show Posts on the User profile page

def show
@posts = current_user.posts.order(created_at: :desc)
end
<div class="user-images">
<% @posts.each do |post|%>
<div class="wrapper">
<%=image_tag post.image %>
</div>
<% end %>
</div>
<div class="col-md-8 basic-info">
...
<ul class="posts">
<li><span><%= @posts.count %></span> posts</li>
</ul>
</div>

Add pagination for Posts

Installation

gem 'kaminari'
bundle install

How to use Pagination for Posts

In Home controller:

@posts = Post.order(created_at: :desc).page(params[:page]).per(5)

In Views:

<%= paginate @users %>
<div class="posts">
<% @posts.each do |post| %>
<section class="post"> ... </section>
<% end %>

<%= paginate @posts %>
</div>
Pagination links
.homepage{
[...]
.pagination{
span{
padding-right: 10px;
}
}
}

Add an Avatar to User

class User < ApplicationRecord

...
has_one_attached :avatarend
<div class="form-group row">
<%= f.label :avatar, class: 'col-sm-3 col-form-label' %>
<div class="col-sm-9">
<%= f.file_field :avatar, class: 'form-control' %>
</div>
</div>
def user_params
params.require(:user).permit(:username, :name, :website,
:bio, :email, :phone, :gender, :avatar)
end

Show Avatar of User:

  • In the User profile page (users/show.html.erb)
<div class="wrapper">
<% if current_user.avatar.attached? %>
<%= image_tag current_user.avatar %>
<%end %>
</div>
  • In the Form edit profile page (users/edit.html.erb)
<%= form_with model: current_user, local: true, html: {class: "form-horizontal form-edit-user"} do |f| %>

<div class="form-group row">
<div class="col-sm-3 col-form-label">
<% if current_user.avatar.attached? %>
<%= image_tag current_user.avatar, class: 'avatar' %>
<%end %>
</div>
<div class="col-sm-9">
...
</div>
</div>
<% end %>
  • In the homepage (home/index.html.erb)
<section class="post">
<div class="user">
<div class="avatar">
<% if post.user.avatar.attached? %>
<%= image_tag post.user.avatar %>
<% end %>
</div>
...
</div>
</section>

View others User profile by click to their Avatar.

class UsersController < ApplicationController  def show
@user = User.find(params[:id])
@posts = @user.posts.order(created_at: :desc)
end
<div class="user">
<div class="avatar">
<% if post.user.avatar.attached? %>
<%= link_to user_path(post.user) do %>
<%= image_tag post.user.avatar %>
<% end %>
<% end %>
</div>
<%= link_to post.user.username, user_path(post.user), class: 'username' %>
</div>

Conclusion

Related posts:

References:

--

--

Sharing about Ruby on Rails, Design Pattern, Clean Code, Performance, Scalability.

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
Luan Nguyen

🚀 Software Engineer at ITviec — Interested in Ruby on Rails, Design Pattern, Clean Code, TDD, Performance, Scalability — github.com/thanhluanuit