[RUBY] [Rails] Implementation procedure of the function to tag posts without gem + the function to narrow down and display posts by tags

Overview

__-Implement a function that allows you to tag your own posts. __ __ ・ Implement a function that allows you to narrow down the search using the tags attached to the post. __

Premise

·environment Ruby 2.6 series Rails 5.2 series

·Library  Slim

__ ・ Rails application template of the above environment __ Procedure to set up Rails application and install devise and Slim

__ ↓ Completed image ↓ __ ezgif.com-video-to-gif (2).gif

Implementation

1. Model design and association

スクリーンショット 2020-06-13 20.14.13.png -User: Post = User has many Posts for each person, so there is a `1 to many` relationship.

- Post: Tag = One Post has many tags` ``, `` `One Tag also has many Posts` `` Many-to-many relationship. → Many-to-many relationship requires an intermediate table = TagMap

→ An intermediate table is a table that stores only the foreign keys (post_id and tag_id) of the many-to-many table (post and tag table in this case), and manages each other's tables with only the foreign keys. Make it possible.

2. Model creation

$ rails g devise User //Create User model from devise $ rails g migration add_columns_to_users name:string //name column added   $ rails g model Post content:string user:references //Post model creation   $ rails g model Tag tag_name:string //Tag model creation   $ rails g model TagMap post:references tag:references //TagMap model creation


 >> -Since columns cannot be added to the User model created by devise, when adding the name column, it is necessary to add it as a new migration.
  
 -<Table name>: A foreign key is set for the table specified in references. (Required)


### 3. Check the created migration file
 > #### 3-1. users migration file
>>```db/migrate/[timestamps]_create_devise_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
 
    #<abridgement>
 
  end
end

3-2. Add name column to users table

class AddUsernameToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :name, :string end end


 > #### 3-3.posts migration file

>>```ruby:db/migrate/[timestamps]_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.text :content
      t.references :user, foreign_key: true
 
      t.timestamps
    end
  end
end

3-4.tags migration file

class CreatePostTags < ActiveRecord::Migration[5.2] def change create_table :post_tags do |t| t.string :tag_name   t.timestamps end end end


 > #### 3-5.tag_maps migration file

>>```ruby:db/migrate/[timestamps]_create_tag_maps.rb
class CreatePostTagMaps < ActiveRecord::Migration[5.2]
  def change
    create_table :tag_maps do |t|
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true
 
      t.timestamps
    end
  end
end

4. Confirmation of the created model file

4-1. User model file

class User < ApplicationRecord

Include default devise modules. Others available are:

:confirmable, :lockable, :timeoutable, :trackable and :omniauthable

devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable   has_many :posts, dependent: :destroy end


 >> ・ `dependent:: destroy`: An option that can be attached to the parent class so that the child class can be deleted when the parent class is deleted.

 > #### 4-2. Post model file

>>```ruby:app/models/user.rb
class Post < ApplicationRecord
  belongs_to :user
 
  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps
end

__ ・ The through option is used to associate with the tags table through the tag_maps table. By doing this, you can get the tags associated with Post by using Post.tags`. This can be used when you want to get the tag attached to the post and display it on the post details screen. __

__ ・ When using the through option, it is necessary to associate it with the intermediate table first. __

__-By adding the dependent :: destroy option to the intermediate table, the relationship between Post and Tag will be deleted at the same time as Post is deleted. __

4-4.Tag model file

class Tag < ApplicationRecord has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id' has_many :posts, through: :tag_maps end


 >> __` ・ After associating with the tag_maps table, it is associated with the posts table through tag_maps`. If you use `Tag.posts`, you can get the Posts associated with the tags. This can be used when you want to search for posts with a specific tag, such as "sports". __

 > #### 4-5. TagMap model file

>>```ruby:app/models/tag_map.rub
class PostTagMap < ApplicationRecord
  belongs_to :post
  belongs_to :tag
 
  validates :post_id, presence: true
  validates :tag_id, presence: true
end

__ ・ Since it is owned by multiple Posts and multiple Tags, it is associated with belongs_to. __

__ ・ When building the relationship between Post and Tag, it is absolute that there are two foreign keys, so validate it. __

5. Routing settings

Rails.application.routes.draw do devise_for :users   root to: 'posts#index'   resources :posts end


 >> __ ・ At this point, for the time being, set the routing provided by devise, the root path, and the path to the posts resource. __

## First, implement from the function that can tag posts

### 6. Create a controller

 > #### 6-1. Write the code to create posts and tags in the create action of the posts controller.

>>```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
 
 def create
    @post = current_user.posts.new(post_params)           
    tag_list = params[:post][:tag_name].split(nil)  
    if @post.save                                         
      @post.save_tag(tag_list)                         
      redirect_back(fallback_location: root_path)          
    else
      redirect_back(fallback_location: root_path)          
    end
 end
  
 private
   def post_params
     params.require(:post).permit(:content)
   end
end

__ Let's break down the above code. __

@post = current_user.posts.new(post_params)

 >> __ ・ By setting `current_user.posts`, the id of the logged-in user will be saved in the user_id of the posts table, but if ʻUser and Post are not associated, an error will occur. Masu`. Strong Parameters is used as an argument. __

>>```ruby:Get the tags that have been sent
tag_list = params[:post][:tag_name].split(nil)

__ ・ params [: post] [: tag_name]: From form, refer to @post object and send the tag name together, so get it in this form. For example, it is sent like "" sports "" study "" work "". __

__ ・ .split (nil): Arrange the sent values separated by spaces. In the above example, it feels like ["sports", "study", "work"]. The reason for arranging is that it is necessary to retrieve this value one by one by iterative processing when saving it in the database later. __

@post.save_tag(tag_list)

 __ ・ `Process to save the array of tags acquired earlier in the database`. The definition of save_tag that performs the processing will be described later. __

 > #### 6-2. Write the code to get the post and tag in the index action.

>>```ruby:app/controllers/posts_controller.rb
def index
    @tag_list = Tag.all              #Get all to display the tag list in the view.
    @posts = Post.all                #Get all to display post list in view.
    @post = current_user.posts.new   #View form_Used for model with.
end

def show @post = Post.find(params[:id]) #Get the post you clicked. @post_tags = @post.tags #Get the tag associated with the clicked post. end


### 7. Define the save_tag instance method in the Post model file.

 > __ ・ Define the contents of the save_tag instance method described earlier in the create action. __

>```ruby:app/models/post.rb
class Post < ApplicationRecord
 
  #<abridgement>
 
  def save_tag(sent_tags)
    current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
    old_tags = current_tags - sent_tags
    new_tags = sent_tags - current_tags
 
    old_tags.each do |old|
      self.post_tags.delete PostTag.find_by(post_tag_name: old)
    end
 
    new_tags.each do |new|
      new_post_tag = PostTag.find_or_create_by(post_tag_name: new)
      self.post_tags << new_post_tag
    end
  end
end

current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?

 > __ ・ If there is a tag associated with @post saved by the create action earlier, get all "tag names as an array". __

>```ruby:Get old tags
old_tags = current_tags - sent_tags

__ ・ Old_tags are the tags that exist in the currently acquired @post, excluding the sent tags. __

new_tags = sent_tags - current_tags

 > __ ・ New_tags are the tags that are sent, excluding the tags that currently exist. __

>```ruby:Delete old tags
old_tags.each do |old|
    self.tags.delete Tag.find_by(tag_name: old)
end

__-Delete old tags. __

new_tags.each do |new| new_post_tag = Tag.find_or_create_by(tag_name: new) self.post_tags << new_post_tag end

 > __-Save the new tag in the database. __

 > __ ・ The reason why it takes time to get old tags and new tags one by one and delete them as described above is not only to get and save tags one by one, for example, when editing a post. Because it needs to work.
 However, I'm sorry, I will omit the implementation of the editing function this time. __

### 8. Create a view.

 > #### 8-1. Create a view of the post list.

>>```ruby:app/views/posts/index.html.slim
h3 tag list
- @tag_list.each do |list|
  span
    = link_to list.tag_name, tag_posts_path(tag_id: list.id)
    = "(#{list.posts.count})"
 
hr
 
h3 post
= form_with(model: @post, url: posts_path, local: true) do |f|
  = f.text_area :content
  br
  = "You can add multiple tags by entering a space."
  = "Example: Music, Literature, Sports"
  = f.text_field :tag_name
  br
  = f.submit
 
hr
 
h3 post list
- @posts.each do |post|
    = link_to post.content, post

Let's break down the above code.

 >> __ ・ All the tags acquired by the index action are displayed, and `a link to the path to display posts related to that tag is provided`. Click this link to view the posts associated with that `tag`. The routing settings to get this link and the creation of actions will be described later. __

 >> __ ・ `" (# {list.posts.count}) "` counts and displays how many posts currently have that tag. __

>>```ruby:New post form (can be tagged)
= form_with(model: @post, url: posts_path, local: true) do |f|
  = f.text_area :content
  br
  = "You can add multiple tags by entering a space."
  = "Example: Music, Literature, Sports"
  = f.text_field :tag_name
  br
  = f.submit

__ ・ = f.text_field: tag_name: By writing this line, the tag entered in the form will be sent to the create action as a parameter called params [: post] [: tag_name]. .. __

8-2. Create a view of the post detail page.

h1 post details   p= @post.content   br   = "tag: "

 >> __ ・ The part that displays the tag is the same as the method described in the list display. In this case, the part called `tag associated with a specific post` is different. __

 __ · This completes the implementation of the function that allows you to tag posts. __

## Implement the function to narrow down and display posts by tags

### 1. Add routing.

>```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts
 
  #Routing to actions to view posts filtered by tags
  resources :tags do
    get 'posts', to: 'posts#search'
  end
end

__ ・ By nesting, you can use the path to transition to the post page associated with a specific tag called tag_posts_path (tag_id: tag.id) as described earlier. __

2. Create a search action in the posts controller.

def search @tag_list = Tag.all #Get all tags to display all tags on this post list display page @tag = Tag.find(params[:tag_id]) #Get the clicked tag @posts = @tag.posts.all #Show all posts associated with the clicked tag end


### 3. Create a page view that displays a list of posts narrowed down by tags.

 > __ ・ First, create a search.html.slim file in the app / views / posts directory. __

>```ruby:app/views/posts/search.html.slim
h2 post list
 
#Tag list
- @tag_list.each do |list|
  span
    = link_to list.tag_name, tag_posts_path(post_tag_id: list.id)
    = "(#{list.posts.count})"
br
#Post list narrowed down by tag
= "The tag is ─"
strong= "#{@tag.tag_name}"
= "─ Post list"
br
- @posts.each do |post|
  = link_to post.content, post
br
= link_to 'home', root_path

__ ・ The tag list part is the same as the index part. __

= "The tag is ─" strong= "#{@tag.tag_name}" = "─ Post list"

 > __ ・ In this part, the relevant tag is displayed so that you can see what kind of tag was narrowed down. __

 __ ・ This completes the implementation of the function to display posts narrowed down by tags. As you can see, there are some overlaps in the view, so if you partial and refactor those parts, the view will look cleaner and look better. __

## Article that was very helpful
 [Note when implementing the tag function in Rails without using gem](https://qiita.com/tobita0000/items/daaf015fb98fb918b6b8)


Recommended Posts

[Rails] Implementation procedure of the function to tag posts without gem + the function to narrow down and display posts by tags
[Rails 6.0] I implemented a tag search function (a function to narrow down by tags) [no gem]
[Rails] How to display the list of posts by category
[Rails] Implementation of tagging function using intermediate table (without Gem)
I want to narrow down the display of docker ps
[Rails] Implementation of tag function using acts-as-taggable-on and tag input completion function using tag-it
[Rails] I will explain the implementation procedure of the follow function using form_with.
Use the where method to narrow down by referring to the value of another model.
[Rails] Implementation procedure when public / private functions are added to the posting function
[Rails] Implementation of drag and drop function (with effect)
Implementation policy to dynamically save and display Timezone in Rails
[Rails 6] Implementation of search function
[Rails] Implementation of category function
[Rails] Implementation of tutorial function
[Rails] Implementation of like function
Method definition location Summary of how to check When defined in the project and Rails / Gem
[Rails] How to get the URL of the transition source and redirect
[Rails 6] Implementation of new registration function by SNS authentication (Facebook, Google)
[Rails] How to omit the display of the character string of the link_to method
[Rails] Read the RSS of the site and return the contents to the front
[Rails] Don't use the select method just to narrow down the columns!
twitter-4 selections of certain errors with Twitter login function created by omniauth gem and how to deal with them
[Rails] Implementation of CSV import function
[Rails] Asynchronous implementation of like function
[Rails] About implementation of like function
[Rails] Implementation of user withdrawal function
[Rails] Implementation of CSV export function
[Rails] gem ancestry category function implementation
[Rails] Comment function implementation procedure memo
Strict_loading function to suppress the occurrence of N + 1 problem added from rails 6.1
[Rails] Articles for beginners to organize and understand the flow of form_with
How to make the schema of the URL generated by Rails URL helper https