Adam O'Grady

Rails Blog: Part 3

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:

<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.