Build Instagram by Ruby on Rails (Part 3)
Show-Delete Posts & Create Hashtags for Post & Search Posts
Previous Post:
- Part 1: medium.com/luanotes/build-instagram-by-ruby-on-rails-part-1
- Part 2: medium.com/luanotes/build-instagram-by-ruby-on-rails-part-2
- Full code on Github: https://github.com/thanhluanuit/instuigram
Table of Contents:
- Add show Post page: Show detail information of Post.
- Add delete Post function.
- Create Hashtags for Post.
- Implement Search Posts function.
What’ll you learn after reading the article?
- CRUD in Active Record: Read/Delete Posts and CRUD Hashtags
- Association in Active Record: The
has_many
:through
association - Active Record Query: Retrieve data from the database using Active Record
Show Post Details
Step 1: Add show action in Posts controller
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
end
end
In this action, a post variable gets through by params id
.
Step 2: Add show View: (app/views/posts/show.html.erb)
<div class="post-show">
</div>
The layout of this page: in the left of the page shows post image and right of the page shows user info and post description.
<div class="post-show row">
<div class="col-md-8">
<!-- show post image -->
</div><div class="col-md-4">
<!-- show user info and description of post -->
</div>
</div>
Full HTML code:
<div class="post-show row">
<div class="col-md-8">
<%=image_tag @post.image, class: 'image' %>
</div>
<div class="col-md-4">
<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>
<div class="description">
<%= @post.description %>
</div>
</div>
</div>
CSS code: (app/assets/stylesheets/posts.scss)
.post-show{
margin: 30px 5px 0;
border: 1px solid #dbdbdb; .col-md-8{
padding: 0;
.image{
width: 100%;
}
} .user{
display: flex;
padding: 10px 0;
border-bottom: 1px solid #efefef; .avatar{
width: 40px;
height: 40px;
margin: 8px;
img{
width: 100%;
height: 100%;
border: 1px solid #efefef;
border-radius: 50%;
}
}
.username{
padding-top: 15px;
color: #262626;
font-size: 14px;
font-weight: 600;
&:hover{
text-decoration: none;
}
}
} .description{
padding: 15px 0;
}
}
Step 3: Add to routes: (config/routes.rb)
resources :posts, only: [:new, :create, :show]
Update in the User Profile page:
<!--app/views/users/show.html.erb--><div class="user-images">
<% @posts.each do |post|%>
<div class="wrapper">
<%=link_to post_path(post) do %>
<%=image_tag post.image %>
<% end %>
</div>
<% end %>
</div>
Delete a Post
We add a function to help user can delete their posts.
Step 1: Add destroy action in Posts controller
def destroy
@post = current_user.posts.find(params[:id])
@post.destroy redirect_to user_path(current_user)
end
We find a post through posts of current user to avoid someone change params[:id]
in HTML and more secure.
@post = current_user.posts.find(params[:id])
Step 2: Add to routes: (config/routes.rb)
resources :posts, only: [:new, :create, :show, :destroy]
See new routes added:
rake routes | grep post
The method of destroy action is DELETE.
Step 3: Show a link to delete post in Post show page
We add a Remove link below description of post section and only show this link in posts of current user.
HTML: (app/views/posts/show.html.erb)
<div class="delete">
<% if current_user.posts.exists?(@post.id) %>
<%= link_to 'Remove', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
</div>
This Remove link use method :delete and show the confirm message before deleting a post.
CSS code:
.delete{
padding-top: 20px;
a{
font-size: 15px;
text-decoration: underline;
}
}
Now look like:
Add Hashtags to Post
Target:
In this section, we’re going to add Hashtags to Post by extracting them from the description of Post.
Example: When a user creates a post with the description is “My favorite #books, great #moment”. Our application will create 2 hashtags is book and moment.
Flow:
- Design Database
- Create HashTag Model
- Create PostHashTag Model
- Create Hashtags for Post
Design Database:
We can see that every post can contain one or many hashtags in the description. And every hashtag can use in one or many posts.
- A Post has many HashTags: one-to-many relationship
- HashTag belongs to Posts: one-to-many relationship
That mean, Post with HashTag have a many-to-many relationship.
The below image is our sub-database design. We’ll create 2 tables are: HashTag and PostsHashTag table. PostsHashTag table is where store information of post and hashtag relationship.
Create HashTag Model
We’ll use a model generator to generate a hash_tag model with name field type of string, a migration which creates a hash_tags table and test_unit files. Run command:
rails generate model hash_tag name:string
The output :
invoke active_record
create db/migrate/20181017134507_create_hash_tags.rb
create app/models/hash_tag.rb
invoke test_unit
create test/models/hash_tag_test.rb
create test/fixtures/hash_tags.yml
For Active Record it creates:
- Model: app/models/hash_tag.rb
- Migration: db/migrate/20181017134507_create_hash_tags.rb
Tips: Sometimes, you may not remember exactly format of the model generator command, you can use below sort command to see how to use:
rails generate model
Run migration to create hash_tags table:
rails db:migrate
Create PostHashTag Model
Before creating this model, I’ll tell you about a has_many :through association feature. This association is often used to set up a many-to-many connection with another model. It indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model.
In our case: Between Post and HashTag is a many-to-many connection and connected through a third model is PostHashTag.
Creating the model :
rails generate model post_hash_taginvoke active_record
create db/migrate/20181017142241_create_post_hash_tags.rb
create app/models/post_hash_tag.rb
invoke test_unit
create test/models/post_hash_tag_test.rb
create test/fixtures/post_hash_tags.yml
Update db/migrate/20181017142241_create_post_hash_tags.rb file:
class CreatePostHashTags < ActiveRecord::Migration[5.2]
def change
create_table :post_hash_tags do |t|
t.belongs_to :post, index: true
t.belongs_to :hash_tag, index: truet.timestamps
end
end
end
We add more 2 line to the migration file:
t.belongs_to :post, index: true
t.belongs_to :hash_tag, index: true
Run migration:
rails db:migrate
The migration will create a post_hash_tags table with more 2 columns are post_id and hash_tag_id and also index 2 these columns.
You can see it in db/schema.rb file:
create_table "post_hash_tags", force: :cascade do |t|
t.bigint "posts_id"
t.bigint "hash_tags_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["hash_tags_id"], name: "index_post_hash_tags_on_hash_tags_id"
t.index ["posts_id"], name: "index_post_hash_tags_on_posts_id"
end
Declare the association in Post, HashTag and PostHashTag
In models, code like this:
class Post < ApplicationRecord
has_many :post_hash_tags
has_many :hash_tags, through: :post_hash_tags
endclass PostHashTag < ApplicationRecord
belongs_to :post
belongs_to :hash_tag
endclass HashTag < ApplicationRecord
has_many :post_hash_tags
has_many :posts, through: :post_hash_tags
end
Check our association declarations:
We’ll use rails console to check our declarations by executing some query. The rails console
command help we interact with your Rails application from the command line. Run command:
rails c
Loading development environment (Rails 5.2.1)
2.4.0 :001 >
For Post:
post = Post.last=> #<Post id: 33, description: "Learn Ruby on Rails", user_id: 2, created_at: "2018-10-14 09:15:39", updated_at: "2018-10-14 09:15:39">post.hash_tags # Return hash_tags of this post=> HashTag Load (0.6ms) SELECT "hash_tags".* FROM "hash_tags" INNER JOIN "post_hash_tags" ON "hash_tags"."id" = "post_hash_tags"."hash_tag_id" WHERE "post_hash_tags"."post_id" = $1 LIMIT $2 [["post_id", 33], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
For HashTag:
hash_tag = HashTag.create(name: "rails")=> #<HashTag id: 2, name: "rails", created_at: "2018-10-17 15:02:36", updated_at: "2018-10-17 15:02:36">hash_tag.posts # Return posts using hash_tags=> Post Load (0.3ms) SELECT "posts".* FROM "posts" INNER JOIN "post_hash_tags" ON "posts"."id" = "post_hash_tags"."post_id" WHERE "post_hash_tags"."hash_tag_id" = $1 LIMIT $2 [["hash_tag_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
It works!
Create Hashtags for Post
When a user create a new post with a description like this: “I love #ruby, ruby is #awesome”. I expect our system will also create 2 hashtags with name are: ruby and awesome.
Step 1: Extract Hashtags from description of Post
Add an instance method to Post model to extract hashtags:
def extract_name_hash_tags
description.to_s.scan(/#\w+/).map{|name| name.gsub("#", "")}
end
This method will return to an array of name hashtags. For example:
post = Post.last=> #<Post id: 3, description: "I love #ruby, ruby is #awesome", user_id: 1, created_at: "2018-10-14 09:15:39", updated_at: "2018-10-14 09:15:39">post.extract_name_hash_tags=> ["ruby", "awesome"]
Step 2: Add a callback to create hashtags after creating a Post.
Active Record supports a lot of useful callbacks.
We use after_commit
callback for our Post model, this callback called after a post has been created, updated, or destroyed. But now, we just want to trigger this callback when a post is created
, so we add more :on
option for specific to create
action. You can learn more about after_commit
in Rails api document.
after_commit :create_hash_tags, on: :create
Define this callback in Post model:
class Post < ApplicationRecordafter_commit :create_hash_tags, on: :create
def create_hash_tags
# create hash_tags of Post
end
end
The create_hash_tags
method will be called after a post is created. Add logic to this method:
def create_hash_tags
extract_name_hash_tags.each do |name|
hash_tags.create(name: name)
end
enddef extract_name_hash_tags
description.to_s.scan(/#\w+/).map{|name| name.gsub("#", "")}
end
Explain create_hash_tags
method:
- The method will create hashtags related to a post based on the result of
extract_name_hash_tags
method. - Line
hash_tags.create(name: name)
: Because a post has many hash_tags, through post_hash_tags table, this line code will create a new HashTag record (H) and a new PostHashTag record which contains hash_tag_id just created (H.id) and post_id is current post id.
Finally, Post model look like this:
class Post < ApplicationRecord
after_commit :create_hash_tags, on: :createdef create_hash_tags
extract_name_hash_tags.each do |name|
hash_tags.create(name: name)
end
enddef extract_name_hash_tags
description.to_s.scan(/#\w+/).map{|name| name.gsub("#", "")}
end
end
Now, we come back to our application, add new post and see the results.
Search Posts
Target:
For this section, we’re going to implement:
- Search function which helps to users search posts by hashtag (#) or description of posts.
- Enable hashtags on post description, make it is clickable to search page.
Generate Search Controller with index action:
rails g controller Search index --no-javascripts --no-stylesheets --no-helper
Update to Routes:
get 'search' => 'search#index'
Add search_path
to Search Form:
<%= form_with url: search_path, method: :get, local: true, class: 'form-inline search-form' do |form| %>
<%= form.text_field :query, value: params[:query], class: 'form-control mr-sm-2', placeholder: '#search' %>
<% end %>
In Search Controller: (app/controllers/search_controller.rb)
We’re going to search posts by hashtag name or description.
class SearchController < ApplicationController
def index
if params[:query].start_with?('#')
query = params[:query].gsub('#', '')
@posts = Post.joins(:hash_tags).where(hash_tags: {name: query})
else
@posts = Post.where("description like ?", "%#{params[:query]}%")
end
end
end
Case 1: Search by a hashtag (#travel), we return posts which have hashtags name is query
value.
Query posts:
query = params[:query].gsub('#', '')Post.joins(:hash_tags).where(hash_tags: {name: query})
This produces:
# query = “ruby”SELECT “posts”.* FROM “posts” INNER JOIN “post_hash_tags” ON “post_hash_tags”.”post_id” = “posts”.”id” INNER JOIN “hash_tags” ON “hash_tags”.”id” = “post_hash_tags”.”hash_tag_id” WHERE “hash_tags”.”name” = $1 LIMIT $2 [[“name”, “ruby”], [“LIMIT”, 11]]
Case 2: Search by description (not a hashtag), we return posts which have description contain the query.
@posts = Post.where("description like ?", "%#{params[:query]}%")
You can learn more query in: guides.rubyonrails.org/active_record_querying
In View: (app/views/search/index.html.erb)
<div class="search-page">
<% if @posts.exists? %>
<h1>Top Posts</h1>
<% else %>
<h1>Oop! No matching posts ...</h1>
<% end %><div class="user-images">
<% @posts.each do |post|%>
<div class="wrapper">
<%=image_tag post.image %>
</div>
<% end %>
</div>
</div>
Show Hashtags in Post Description
We make hashtags is clickable and link to search posts by this hashtag. In homepage and post show page, change post description as below:
<%= post.description %>
to
<% post.description.to_s.split(' ').each do |word| %>
<% if word.start_with?('#') %>
<%= link_to word, search_path(query: word) %>
<%else %>
<%= word %>
<%end %>
<% end %>
Now a post look like:
Conclusion
In this article, I guide you learn about Association in Active Record, especially the has_many
:through
association. And how to retrieve data from the database using Active Record. I hope that you can understand deeper about Active Record.
Source Code on Github: https://github.com/thanhluanuit/instuigram
References:
- Association in Active Record: https://guides.rubyonrails.org/association_basics.html
- Active Record Query Interface: https://guides.rubyonrails.org/active_record_querying.html