manis
manis

Reputation: 729

How to DRY up this code

I have implemented a tagging system for the models Unit, Group and Event, and currently, each one have their own instance of the methods add_tags and self.tagged_with.

def add_tags(options=[])
    transaction do
      options.each do |tag|
        self.tags << Tag.find_by(name: tag)
      end
    end
end

and

def self.tagged_with(tags=[])
  units = Unit.all
    tags.each do |tag|
      units = units & Tag.find_by_name(tag).units
    end
    units
  end
end

I want to move these into a module and include them in the model, but as you can see, the tagged_with method is not polymorphic, as I don't know how I would refer the parenting class (Unit, Group etc.) and called methods like "all" on them. Any advice?

Tag model:

Class Tag < ActiveRecord::Base
  has_and_belongs_to_many: units, :join_table => :unit_taggings
  has_and_belongs_to_many: groups, :join_table => :group_taggings
  has_and_belongs_to_many: events, :join_table => :event_taggings
end

Upvotes: 1

Views: 145

Answers (2)

Max Williams
Max Williams

Reputation: 32933

I would do it like so:

#table: taggings, fields: tag_id, taggable type (string), taggable id
class Tagging
  belongs_to :tag
  belongs_to :taggable, :polymorphic => true

Now make a module in lib - let's call it "ActsAsTaggable"*

module ActsAsTaggable
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do 
      #common associations, callbacks, validations etc go here
      has_many :taggings, :as => :taggable, :dependent => :destroy
      has_many :tags, :through => :taggings
    end
  end

  #instance methods can be defined in the normal way

  #class methods go in here
  module ClassMethods  

  end
end      

Now you can do this in any class you want to make taggable

include ActsAsTaggable 
  • there is already a gem (or plugin perhaps) called ActsAsTaggable, which basically works in this way. But it's nicer to see the explanation rather than just get told to use the gem.

EDIT: here's the code you need to set up the association at the Tag end: note the source option.

class Tag
  has_many :taggings
  has_many :taggables, :through => :taggings, :source => :taggable

Upvotes: 1

Nicos Karalis
Nicos Karalis

Reputation: 3773

You could call self.class to get the current class, like this:

def self.tagged_with(tags=[])
  klass = self.class
  units = klass.all
    tags.each do |tag|
      units = units & Tag.find_by_name(tag).units
    end
    units
  end
end

self.class should return Unit or any class, calling any method on a class object (self.class.tagged_with) is the same as Unit.tagged_with

I would recommend that you use Concerns, take a look here

EDIT Answer to your comment

Using concerns you could do something like this, each class have that methods you mentioned before, but you dont have to rewrite all that code on every class (or file):

# app/models/concerns/taggable.rb
module Taggable
  extend ActiveSupport::Concern

  module ClassMethods
    def self.tagged_with(tags=[])
      klass = self.class
      units = klass.all
        tags.each do |tag|
          units = units & Tag.find_by_name(tag).units
        end
        units
      end
    end
  end
end

# app/models/unit.rb
class Unit
  include Taggable

  ...
end

# app/models/group.rb
class Group
  include Taggable

  ...
end

# app/models/event.rb
class Event
  include Taggable

  ...
end

Upvotes: 1

Related Questions