Build Instagram by Ruby on Rails (Part 2)
Upload Photo for Post & Upload Avatar for Users
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
endclass Post < ApplicationRecord
end
After declaring a association:
class User < ApplicationRecord
has_many :posts, dependent: :destroy
endclass 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
endprivatedef 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%;
}
}
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: nilpost.save
#=> falsepost.valid?
#=> falsepost.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:
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:
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:
- Part 1: medium.com/luanotes/build-instagram-by-ruby-on-rails-part-1
- Part 3: medium.com/luanotes/build-instagram-by-ruby-on-rails-part-3
References:
- Active Record: guides.rubyonrails.org/active_record_basics.html
- Active Storage: guides.rubyonrails.org/active_storage_overview.html
- Kaminari: https://github.com/kaminari/kaminari