require 'facet/inflect' module Glue # The default Tag implementation. A tag attaches semantics to # a given object. #-- # FIXME: use index and char() instead of String. #++ class Tag property :name, String, :uniq => true property :count, Fixnum # An alias for count. alias_method :freq, :count alias_method :frequency, :count def initialize(name = nil) @name = name @count = 0 end # Tag an object. def tag(obj) #-- # FIXME: this does not work as expected :( it alters # the @loaded flag in the obj.tags collection without # setting the @members. # INVESTIGATE: why this happens! # # return if obj.tagged_with?(@name) # class_name = obj.class.name # method_name = class_name.index('::') ? (class_name =~ /.*?\:\:(.*)/; $1) : class_name # send(method_name.pluralize.underscore.to_sym) << obj #++ unless obj.tagged_with?(name) obj.tags << self @count += 1 update_property :count end end alias_method :link, :tag # Untags an object. If no object is passed, it just decrements # the (reference) count. If the count reaches 0 the tag is # deleted (garbage collection). def untag(obj = nil) if obj # TODO: implement me. end @count -= 1 if @count > 0 p "-- dec ref count" update_property :count else p "-- count = 0 -> delete" self.delete() end end alias_method :unlink, :untag # Return all tagged objects from all categories. def tagged # TODO. end # Helper method def self.total_frequency(tags = Tag.all) tags.inject(1) { |total, t| total += t.count } end def to_s @name end end # Add tagging methods to the target class. # For more information on the algorithms used surf: # http://www.pui.ch/phred/archives/2005/04/tags-database-schemas.html # # === Example # # class Article # include Taggable # .. # end # # article.tag('navel', 'gmosx', 'nitro') # article.tags # article.tag_names # Article.find_with_tags('navel', 'gmosx') # Article.find_with_any_tag('name', 'gmosx') # # Tag.find_by_name('ruby').articles module Taggable # The tag string separator. setting :separator, :default => ' ', :doc => 'The tag string separator' include Og::EntityMixin many_to_many Tag # Add a tag for this object. def tag(the_tags, options = {}) options = { :clear => true }.merge(options) delete_all_tags() if options[:clear] for name in Taggable.tags_to_names(the_tags) the_tag = Tag.find_or_create_by_name(name) the_tag.tag(self) end end alias_method :tag!, :tag # Delete a single tag from this taggable object. def delete_tag(name) if dtag = (tags.delete_if { |t| t.name == name }).first dtag.unlink end end # Delete all tags from this taggable object. def delete_all_tags for tag in tags tag.unlink end tags.clear end alias_method :clear_tags, :delete_all_tags # Return the names of the tags. def tag_names tags.collect { |t| t.name } end # Return the tag string def tag_string(separator = Taggable.separator) tags.collect { |t| t.name }.join(separator) end # Checks to see if this object has been tagged # with +tag_name+. def tagged_with?(tag_name) tag_names.include?(tag_name) end alias_method :tagged_by?, :tagged_with? module ClassMethods # Find objects with all of the provided tags. # INTERSECTION (AND) def find_with_tags(*names) info = ogmanager.store.join_table_info(self, Tag) count = names.size names = names.map { |n| ogmanager.store.quote(n) }.join(',') sql = %{ SELECT t.* FROM #{info[:first_table]} AS o, #{info[:second_table]} as t, #{info[:table]} as j WHERE o.oid = j.#{info[:first_key]} AND t.oid = j.#{info[:second_key]} AND (o.name in (#{names})) GROUP BY j.article_oid HAVING COUNT(j.article_oid) = #{count}; } return self.select(sql) end alias_method :find_with_tag, :find_with_tags # Find objects with any of the provided tags. # UNION (OR) def find_with_any_tag(*names) info = ogmanager.store.join_table_info(self, Tag) count = names.size names = names.map { |n| ogmanager.store.quote(n) }.join(',') sql = %{ SELECT t.* FROM #{info[:first_table]} AS o, #{info[:second_table]} as t, #{info[:table]} as j WHERE o.oid = j.#{info[:first_key]} AND t.oid = j.#{info[:second_key]} AND (o.name in (#{names})) GROUP BY j.article_oid } return self.select(sql) end end def self.included(base) Tag.module_eval do many_to_many base end base.extend(ClassMethods) #-- # FIXME: Og should handle this automatically. #++ base.send :include, Glue::Aspects base.before 'tags.clear', :on => [:og_delete] end # Helper. def self.tags_to_names(the_tags, separator = Taggable.separator) if the_tags.is_a? Array names = the_tags elsif the_tags.is_a? String names = the_tags.split(separator) end names = names.flatten.uniq.compact return names end end end # * George Moschovitis