Build Instagram by Ruby on Rails (Part 3)

Show-Delete Posts & Create Hashtags for Post & Search Posts

Previous Post:

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:

Post show page with Remove link

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.

Relation between Post and Hashtag

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_tag
invoke  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: true
t.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
end
class PostHashTag < ApplicationRecord
belongs_to :post
belongs_to :hash_tag
end
class 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 < ApplicationRecord
after_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
end
def 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: :create
def create_hash_tags
extract_name_hash_tags.each do |name|
hash_tags.create(name: name)
end
end
def 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>
Search Result page

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: