Build Instagram by Ruby on Rails (Part 2)

Upload Photo for Post & Upload Avatar for Users

Luan Nguyen
Luanotes

--

Previous Post:

Part 1: medium.com/luanotes/build-instagram-by-ruby-on-rails-part-1

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

Models in Rails use a singular name, and their corresponding database tables use a plural name. To create Post model, we use generator of Rails and type command in your terminal:

rails generate model Post description:string user_id:integer

This command will generate a bunch of files:

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

Now, we need to consider 2 files are app/models/post.rb and db/migrate/20180922091640_create_posts.rb

Open db/migrate/20180922091640_create_posts.rb in editor:

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

This file is a migration will create Posts table with 2 columns: description (type string) and user_id (type integer).

Run the migration to execute creating Posts table:

rails db:migrate

and see the result:

== 20180922091640 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0404s
== 20180922091640 CreatePosts: migrated (0.0405s) =============================

More about migrations, refer to Active Record Migrations.

Open Post model: app/models/post.rb

class Post < ApplicationRecord
end

This is Post model which mapped to a posts table in the our database.

Add an Association between User and Post models

In our design database, each User has many Posts in relations, so we need associations between User and Post models. It’ll make common operations simpler and easier in your code.

Active Record support features to declare associations easily. Without associations:

class User < ApplicationRecord
end
class Post < ApplicationRecord
end

After declaring a association:

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

A has_many association indicates a one-to-many connection with another model.

And dependent: :destroy option indicates that all the associated posts will be destroyed when user destroyed.

Introduce Active Storage

Active Storage is a great feature in Rails 5.2. It’s designed to upload files to a cloud storage service like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those files to Active Record objects.

Active Storage comes with a local disk-based service for development and testing.

Setup Active Storage:

Run command:

rails active_storage:install

It’ll create a migration file like this: db/migrate/20180924134051_create_active_storage_tables.active_storage.rb

And then, run rails db:migrate to execute the migration.

$ 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) ====

The migration will create 2 tables is: active_storage_blobs and active_storage_attachments which Active Storage uses to store files.

Config services where store files:

Active Storage declare services in config/storage.yml. Open this file, you can see default config like below:

test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>

This configuration that means our application declare two services named test and local. Both of these services use Disk service.

You can add more other services. For example, we can add new service with named amazon to config/storage.yml:

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

Each environment often uses a different service.

In the development environment, we can use the Disk service by adding the config code to config/environments/development.rb:

# Store uploaded files on the local file system
config.active_storage.service = :local

To use the Amazon S3 service in production, you can add the following code to config/environments/production.rb:

# Store uploaded files on Amazon S3
config.active_storage.service = :amazon

Don’t forget restarting the server when updating the configuration in each environment.

Using Active Storage to store Image in Post

Attaching Image to Post

We use has_one_attached macro to sets up a one-to-one relationship between Post and Image. Each post can have one image attached to it.

class Post < ApplicationRecord
belongs_to :user
has_one_attached :image
end

Create a New Post

Post images to our application is a main function. Each post will contain an image, description and user who create post.

Flow to a new Post includes:

  • 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:

We going to add a create action to Post controller

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

This action is going to create a new Post from params and reload page (actually redirect to homepage).

Add a form to create a new Post in Homepage

HTML code of Form:

<%= 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 %>

We use file_field helper method to upload image.

<%= f.file_field :image %>

user_id is user create post, that mean it is current_user and we hide this field in the form by ‘d-none’ CSS class.

<%= f.text_field :user_id, value: current_user.id, class:'d-none' %>

Add routes new and create actions of Post:

resources :posts, only: [:new, :create]

CSS code:

.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

Currently, we have a small bug when submit form create Post when do not add any image. It’ll create a new Post without image and show the errors in the homepage:

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

The above errors that mean image attachment of post is nil. So now, we’ll validate Post before creating to make sure each posts have to an image.

Because Active Storage haven’t yet support validate for attach files, we have to use a custom method to validate image in Post.

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

We use image_presence to validate Post. The method will add an error with message is can’t be blank when creating a new Post without image (unless image.attached?) and then post will be invalid and the post can NOT create.

When validation methods run?

Before saving an Active Record object, Rails runs our validations. If these validations produce any errors, Rails does not save the object.

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

Now, our posts always have images after using validation in Post object.

Show Posts on Homepage

After adding new posts successful, in this step we’re going to show them in the Homepage. We’ll show all posts and order by created_at field.

Post.all.order(created_at: :desc)

In HomeController, add posts variable which contain all ordered posts to index action.

class HomeController < ApplicationController
def index
@posts = Post.order(created_at: :desc)
end
end

In view: app/views/home/index.html.erb, we display posts:

<div class="posts">
<% @posts.each do |post| %>
<section class="post">
<!-- post view -->
</section>
<% end %>
</div>

A post section contains avatar user’s, username, image and description. We temporarily use a default image for the avatar users. And show image of post by:

<%= image_tag post.image, class: 'main-image' %>

HTML code of section:

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

CSS style:

.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;
}
}
}

Post looks like:

A Post in Homepage

Show Posts on the User profile page

In this step, we’ll show posts of user in their profile page. I’ll replace temporary images by images which users posted.

Because User and Post have has_many association, that mean a user has many posts, so we can easily retrieves posts of user by current_user.posts

In show action of UsersController, add a posts instance variable same in homepage, it is posts of current user which order by created_at field.

def show
@posts = current_user.posts.order(created_at: :desc)
end

Update HTML code in user-images section:

<div class="user-images">
<% @posts.each do |post|%>
<div class="wrapper">
<%=image_tag post.image %>
</div>
<% end %>
</div>

Update number of posts of current user:

<div class="col-md-8 basic-info">
...
<ul class="posts">
<li><span><%= @posts.count %></span> posts</li>
</ul>
</div>

Yeah, look awesome!!!

Add pagination for Posts

We can easily implement pagination in Rails with the kaminari gem.

Kaminari is a Scope & Engine based, clean, powerful, customizable and sophisticated paginator for modern web app frameworks and ORMs.

Kaminari gem easy to use: we just bundle the gem, then your models are ready to be paginated. No configuration required. Don’t have to define anything in your models or helpers.

Installation

To install kaminari to our application, put this line in your Gemfile:

gem 'kaminari'

Then run bundle:

bundle install

How to use Pagination for Posts

In Home controller:

Code will look like this:

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

The page scope: To fetch the (n)th page of Post.

Example: Post.page(3) that means it’ll fetch the 3rd page of Post.

Note: Pagination starts at page 1, not at page 0 , page(0) will return the same results as page(1).

The per scope: To show the number of posts per each page, default per_page is 25.

Example: Post.page(3).per(5) that means is 5

In Views:

Call the paginate helper:

<%= paginate @users %>

This will render several ?page=N pagination links surrounded by an HTML5 <nav> tag. This would output several pagination links such as:

« First ‹ Prev ... 2 3 4 5 6 7 8 9 10 ... Next › Last »

Add paginate helper to view: home/index.html.erb

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

<%= paginate @posts %>
</div>

Go to the homepage and reload the page, see what happen (don’t forget to add more than 5 posts, because we config per_page is 5).

Scroll to the bottom of the page, we’ll see the pagination links:

Pagination links

You can see the pagination links: 1 2 3 4 Next ›Last », the UI is not pretty. We’ll add some below CSS to make it better:

.homepage{
[...]
.pagination{
span{
padding-right: 10px;
}
}
}

Add an Avatar to User

Using Active Storage to add an avatar for Users

In Model

Because each user to have an avatar, so define the User model like this:

class User < ApplicationRecord

...
has_one_attached :avatarend

In View (Form edit User)

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

In Controller:

Add avatar to strong params

def user_params
params.require(:user).permit(:username, :name, :website,
:bio, :email, :phone, :gender, :avatar)
end

Now we can go to the Edit User page to upload an avatar for User.

Show Avatar of User:

Replace all where using a temporary image by the 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)

Avatar users of each post:

<section class="post">
<div class="user">
<div class="avatar">
<% if post.user.avatar.attached? %>
<%= image_tag post.user.avatar %>
<% end %>
</div>
...
</div>
</section>

Call user.avatar.attached? to determine whether a user has an avatar.

View others User profile by click to their Avatar.

Update show action in UsersController:

Change current_user by a user which query by id

class UsersController < ApplicationController  def show
@user = User.find(params[:id])
@posts = @user.posts.order(created_at: :desc)
end

In the homepage (home/index.html.erb):

Add links for avatar and username of user which go to user profile page.

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

Now, in the homepage, we can click to avatar or username of user to view detail their profile.

Conclusion

In this article, I help you learn detail about Model (Active Record) such as: create a new model, validations, association. Introduction about Active Storage feature and how to use it to upload the image. Finally, how to use Kaminari gem in pagination.

Full Code on Github: https://github.com/thanhluanuit/instuigram

Related posts:

References:

--

--

Luan Nguyen
Luanotes

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