Search code examples
ruby-on-railsrubyruby-on-rails-4deviseacts-as-taggable-on

Rails - Devise/Acts-as-taggable-on Gems - Make tag ownership unique to a user?


A user can log-in (devise gem), create a text post, & tag that post (via acts-as-taggable-on gem). If a tag is used more than once across posts, the CSS of that tag changes. In the example below, for instance, the underline style on #foobar disappears. However, as it stands, this is happening across user accounts. This means the tags a user inputs to tag their posts are not unique to their account. In other words, if User1 tags #foobar and after, on a separate account, User2 also tags #foobar - the underline goes away on both user accounts. My goal is to make the tags a user inputs in the posting form unique to that user.

Example:

User1 Account

https://i.sstatic.net/dgIQ8.jpg

User2 Account

http://imgur.com/29gzutj

I have tried lots of solutions - none of which are working!! Please help!

posts_controller.rb

class PostsController < ApplicationController

 def index    
    if params[:tag]
     @posts = current_user.posts.tagged_with(params[:tag]).order(created_at: :desc)
    else 
     @posts = current_user.posts.includes(:things).all.order(created_at: :desc)
    end 
 end 

 def new
    @post = current_user.posts.build
 end

 def create
    @post = current_user.posts.build(post_params)
    if @post.save
        redirect_to '/posts'
    else
        render 'new'
    end

end

new.html.erb

<button id='addtext'>text</button>

<%= form_for @post, html: { multipart: true } do |f| %>

<%= f.text_field :tag_list %>
 <%= f.fields_for :things do |ff| %>
<% end %> 

<%= f.submit %>
<% end %>

index.html.erb

<div id="posts">
  <%= render @posts %>
</div>

_post.html.erb

<div class="post">
   <%= raw post.tags.order("taggings_count DESC").map {|t| link_to t.name, tag_path(t.name), class: css_class_for_tag(t.taggings_count)}.join(' ') %> 

   <% post.things.each do |thing| %>
     <%= thing.try(:text) %>
   <% end %>
</div>

post_helper.rb

module PostsHelper
 def css_class_for_tag count
  case count
   when 1 
     'new_tag'
   else
    'used_tag' 
   end
 end
end

post model

class Post < ActiveRecord::Base
  acts_as_taggable
  ActsAsTaggableOn.delimiter = ' '
  ActsAsTaggableOn.force_lowercase = true
  belongs_to :user 
  has_many :things, dependent: :destroy
  accepts_nested_attributes_for :things, allow_destroy: true 
end

thing model

class Thing < ActiveRecord::Base
  belongs_to :post
  has_attached_file :image, styles: { small: "300x300>", medium: "600x600>" }
  validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
end

user model

class User < ActiveRecord::Base
  acts_as_tagger
  has_many :posts 
  devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable
end

Schema

ActiveRecord::Schema.define(version: 20160303110608) do

create_table "posts", force: :cascade do |t|
 t.datetime "created_at", null: false
 t.datetime "updated_at", null: false
 t.integer  "user_id"
end

create_table "taggings", force: :cascade do |t|
 t.integer  "tag_id"
 t.integer  "taggable_id"
 t.string   "taggable_type"
 t.integer  "tagger_id"
 t.string   "tagger_type"
 t.string   "context",       limit: 128
 t.datetime "created_at"
end

add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"

create_table "tags", force: :cascade do |t|
 t.string  "name"
 t.integer "taggings_count", default: 0
end

add_index "tags", ["name"], name: "index_tags_on_name", unique: true

create_table "things", force: :cascade do |t|
 t.text     "text"
 t.integer  "post_id"
 t.datetime "created_at",         null: false
 t.datetime "updated_at",         null: false
 t.integer  "order"
end

create_table "users", force: :cascade do |t|
 t.string   "email",                  default: "", null: false
 t.string   "encrypted_password",     default: "", null: false
 t.string   "reset_password_token"
 t.datetime "reset_password_sent_at"
 t.datetime "remember_created_at"
 t.integer  "sign_in_count",          default: 0,  null: false
 t.datetime "current_sign_in_at"
 t.datetime "last_sign_in_at"
 t.string   "current_sign_in_ip"
 t.string   "last_sign_in_ip"
 t.datetime "created_at",                          null: false
 t.datetime "updated_at",                          null: false
end

add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

end

Solution

  • One way of doing it:

    tag_counts = ActsAsTaggableOn::Tag.joins(:taggings).
      where(taggings: { taggable_type: "Post", taggable_id: current_user.post_ids }).
      group("tags.id").count
    

    Gives you something like this:

    { 7=>2, 1=>2, 5=>4 }
    

    The first number is the tag id, the second number is the number of occurrences of that tag for current_user.

    Now you can do this:

    <div class="post">
      <%= raw post.tags.map{ |t| [t.name, tag_counts[t.id]] }.sort_by{ |t| -t[1] }.
                        map{ |t| link_to t[0], tag_path(t[0]), class: css_class_for_tag(t[1])}.join(' ') %>
    </div> 
    

    This can be written more expressively, but it gets longer than... ;-) Basically we loop over the post's tags, just as we did before, but then connect each tag with its count specific to current_user in the first map statement. Then we sort by the count, in reverse order. And in the final map statement we generate the links, as we did before, just using the user specific count again.