Continuing on from Part 2 of our Rails Blog tutorial, now we’re going to be adding in a tag system so users can tag posts and check out what posts are available under a particular tag.
To start with, run rails g model Tag name:string
to generate a model for the tags then run rails g migration posts_tags
and find the migration this generated (typically db/migrate/YYYYMMDDHHMMSS_create_tags.rb with a different time stamp) and add the following inside the change
method:
create_table :posts_tags, :id => false do |t|
t.integer :post_id
t.integer :tag_id
end
This will create a new table without a primary key (:id => false
) with two attributes, both being foreign keys and leading to the post and tag tables respectively. Run rake db:migrate
to add these changes to the database. Next you’ll need to open up app/models/tag.rb and add:
has_and_belongs_to_many :posts
Then open up app/models/post.rb and add:
has_and_belongs_to_many :tags
This creates the relationship that we want between the tags and posts; a many-to-many relationship where each tag can have multiple posts related to it and each post can contain multiple tags.
Now with a lot of blogging platforms, you enter a list of comma-separated values, each value becoming a tag. We’re going to try and implement something similar in this project. Open up app/views/posts/new.html.erb and before the hidden field we added with the user ID, add the following code:
<div>
<%= f.label :tag_ids %><br>
<% if @post.try(:tags) %>
<%= f.text_field :tag_ids, value: "", value: @post.tags.map {|c| c.name}.join(', ') %>
<% else %>
<%= f.text_field :tag_ids, value: "" %>
<% end %>
</div>
This creates a new element for the tags and if there’s no existing tags collection on @post
, indicating a new post or empty tag collection on an existing post, it won’t error out (by looking for the map method of a nil tags variable). Next we’ll add some code above the User section in app/views/posts/shot.html.erb:
<p>
<strong>Tags:</strong>
<% @post.tags.each do |t| %>
<%= link_to t.name, t %><% if @post.tags.last != t %>, <% end %>
<% end %>
</p>
Now open up app/controllers/posts_controller.rb and after the post_params
function, we’ll add a couple more private functions to help reduce the amount of code we need to duplicate afterwards:
def tag_params
params.require(:post).permit(:tag_ids)
end
def ready_tags
tags = post_params[:tag_ids].split(/,\s*/)
tags_ready = []
tags.each do |tag|
temp = Tag.find_by name: tag
if temp == nil
temp = Tag.create(name: tag)
end
tags_ready.push(temp)
end
tags_ready
end
def destroy_orphaned_tags(tags, limit)
tags.each do |tag|
if (tag.posts.count <= limit)
tag.destroy
end
end
end
tag_params
allows us to use the :tag_ids
variable passed through from app/views/posts/_form.html.erb. The second function (ready_tags
) readies the tag_ids from app/views/posts/_form.html.erb to be used by the post in the create
and update
methods while destroy_orphaned_tags
deletes tags that have are not attached to any post (makes more sense in context of the controller). Next we’ll need to alter the create
, update
and destroy
methods to look like below:
# POST /posts
def create
@post = Post.new(post_params)
@post.tags = ready_tags
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
destroy_orphaned_tags(@post.tags, 0)
render :new
end
end
# PATCH/PUT /posts/1
def update
destroy_orphaned_tags(@post.tags, 1)
@post.tags = ready_tags
if author_exists = User.where(:id => @post.user_id).first
if current_user == author_exists || current_user.try(:admin?)
if @post.update(post_params)
redirect_to @post, notice: 'Post was successfully updated.'
else
destroy_orphaned_tags(@post.tags, 0)
render :edit
end
else
render :show
end
else
if current_user.try(:admin?)
if @post.update(post_params)
redirect_to @post, notice: 'Post was successfully updated.'
else
destroy_orphaned_tags(@post.tags, 0)
render :edit
end
else
render :show
end
end
end
# DELETE /posts/1
def destroy
if author_exists = User.where(:id => @post.user_id).first
if current_user == author_exists || current_user.try(:admin?)
destroy_orphaned_tags(@post.tags, 1)
@post.destroy
redirect_to posts_url, notice: 'Post was successfully destroyed.'
else
render :show
end
else
if current_user.try(:admin?)
destroy_orphaned_tags(@post.tags, 1)
@post.destroy
redirect_to posts_url, notice: 'Post was successfully destroyed.'
else
render :show
end
end
end
As you can see, we utilise the new methods available to add tags to post, update tags on existing posts and delete unneeded tags when posts are deleted. Next we’ll edit some more views, firstly adding the following code near the bottom of app/views/posts/index.html.erb
<p>
<%= link_to 'Tags', tags_path %>
</p>
Then create a file called app/views/tags/index.html.erb and add the following code:
Listing tags
<table>
<tr>
<th>Tag</th>
<th colspan="2"></th>
</tr>
<% @tags.each do |tag| %>
<tr>
<td><%= tag.name %></td>
</td>
<td><%= link_to 'Show', tag %></td>
<% if current_user.try(:admin?) %>
<td><%= link_to 'Destroy', tag, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
<% end %>
</table>
<p>
<%= link_to 'Index', posts_path %>
</p>
Next create app/views/tags/show.html.erb and use the following code:
<h1><%= @tag.name %></h1>
<% if current_user.try(:admin?) %>
<p>
<%= link_to 'Destroy', @tag, method: :delete, data: { confirm: 'Are you sure?' } %>
</p>
<% end %>
<table>
<tr>
<th>Post</th>
<th colspan="2"></th>
</tr>
<% @tag.posts.each do |post| %>
<tr>
<td><%= post.title %></td>
</td>
<td><%= link_to 'Show', post %></td>
<% if (current_user == post.user && post.user != nil) || current_user.try(:admin?) %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
<% end %>
</table>
<p>
<%= link_to 'Users List', users_path %>
</p>
These files create a way for users to see all tags, see tags connected to posts and delete either tags or particular posts connected to a tag (with the right authorisation levels). Next run rails g controller tags
and open up the new controller file located at app/controllers/tags_controller.rb and add the following code inside the class:
before_filter :authenticate_user!, :only => [:destroy]
# GET /tags
def index
@tags = Tag.all
end
# GET /tags/1
def show
@tag = Tag.find(params[:id])
end
# DELETE /tags/1
def destroy
@tag = Tag.find(params[:id])
if current_user.try(:admin?)
@tag.destroy
redirect_to tags_url, notice: 'Tag was successfully destroyed.'
else
redirect_to tags_url, notice: 'Tag unsuccessfully destroyed.'
end
end
This creates the necessary framework to show the index of tags, show a particular tag or delete a particular tag as well as requiring authentication before tags can be destroyed.. To make all these resources accessible, add resources :tags
inside your config/routes.rb file.