Module: MongoMapper::Plugins::Taggregator::ClassMethods

Defined in:
lib/taggregator.rb

Instance Method Summary collapse

Instance Method Details

#aggregation_collection_for(context) ⇒ Object

Collection name for storing results of tag count aggregation



172
173
174
# File 'lib/taggregator.rb', line 172

def aggregation_collection_for(context)
  "#{collection_name}_#{context}_aggregation"
end

#convert_array_to_string(ary = [], seperator = " ") ⇒ Object



233
234
235
# File 'lib/taggregator.rb', line 233

def convert_array_to_string(ary = [], seperator = " ")
  ary.uniq.compact.join(seperator)
end

#convert_string_to_array(str = "", seperator = " ") ⇒ Object

Helper method to convert a String to an Array based on the configured tag separator.



229
230
231
# File 'lib/taggregator.rb', line 229

def convert_string_to_array(str = "", seperator = " ")
  str.split(seperator).map(&:strip).uniq.compact
end

#find_similar(article, options = {:through => :tags}) ⇒ Object

an array returned with similar items



220
221
222
223
224
225
# File 'lib/taggregator.rb', line 220

def find_similar(article, options={:through => :tags})
  context = options[:through]
  array_field = tag_options_for(options[:through])[:array_field]
  tags = article[array_field]
  where(:"#{array_field}".in => tags)
end

#get_tag_separator_for(context) ⇒ Object



190
191
192
# File 'lib/taggregator.rb', line 190

def get_tag_separator_for(context)
  taggable_with_context_options[context][:separator] || ' '
end

#set_tag_separator_for(context, value) ⇒ Object



194
195
196
# File 'lib/taggregator.rb', line 194

def set_tag_separator_for(context, value)
  taggable_with_context_options[context][:separator] = value.nil? ? " " : value.to_s
end

#tag_contextsObject



163
164
165
# File 'lib/taggregator.rb', line 163

def tag_contexts
   taggable_with_context_options.keys
end

#tag_options_for(context) ⇒ Object



167
168
169
# File 'lib/taggregator.rb', line 167

def tag_options_for(context)
 taggable_with_context_options[context]
end

#taggable(*args) ⇒ Object

Macro to declare a document class as taggable, specify field name for tags, and set options for tagging behavior.

Examples:

Define a taggable document.


class Article
  include Mongoid::Document
  include Mongoid::Taggable
  taggable :keywords, :separator => ' ', :aggregation => true, :default_type => "seo"
end

Parameters:

  • field (Symbol)

    The name of the field for tags.

  • options (Hash)

    Options for taggable behavior.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/taggregator.rb', line 43

def taggable(*args)
  # init variables
  options = args.extract_options!
  tags_field = (args.blank? ? :tags : args.shift).to_sym
  options.reverse_merge!(
    :separator => ' ',
    :array_field => "#{tags_field}_array".to_sym
  )
  tags_array_field = options[:array_field]

  # register / update settings
  class_options = self.taggable_with_context_options || {}
  class_options[tags_field] = options
  self.taggable_with_context_options = class_options

  # setup fields & indexes
  key tags_field.to_sym, String, :default => ""
  key tags_array_field.to_sym, Array, :default => []
  self.ensure_index tags_array_field.to_sym

  # singleton methods
  class_eval <<-END
    class << self
      def #{tags_field}
        tags_for(:"#{tags_field}")
      end

      def #{tags_field}_with_weight
        tags_with_weight_for(:"#{tags_field}")
      end

      def #{tags_field}_separator
        get_tag_separator_for(:"#{tags_field}")
      end

      def #{tags_field}_separator=(value)
        set_tag_separator_for(:"#{tags_field}", value)
      end

      def #{tags_field}_tagged_with(tags)
        tagged_with(:"#{tags_field}", tags)
      end
    end
  END

  # instance methods
  class_eval <<-END
    def #{tags_field}=(s)
      super
      write_attribute(:#{tags_array_field}, self.convert_string_to_array(s, get_tag_separator_for(:"#{tags_field}")))
    end

    def #{tags_array_field}=(a)
      super
      write_attribute(:#{tags_field}, self.convert_array_to_string(a, get_tag_separator_for(:"#{tags_field}")))
    end
    
    def increment_tags_agregation
      # if document is created by using MyDocument.new
      # and attributes are individually assigned
      # #previous_changes won't be empty and aggregation
      # is updated in after_save, so we simply skip it.
      return unless changes.empty?

      # if the document is created by using MyDocument.create(:tags => "tag1 tag2")
      # #previous_changes hash is empty and we have to update aggregation here
      tag_contexts.each do |context|
        coll = MongoMapper.database.collection(self.class.aggregation_collection_for(context))
        field_name = self.class.tag_options_for(context)[:array_field]
        tags = self.send field_name || []
        tags.each do |t|
          coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
        end
      end
    end

    def decrement_tags_aggregation
      tag_contexts.each do |context|
        coll = MongoMapper.database.collection(self.class.aggregation_collection_for(context))
        field_name = self.class.tag_options_for(context)[:array_field]
        tags = self.send field_name || []
        tags.each do |t|
          coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
        end
      end
    end
    
    def update_tags_aggregation
      return unless self.need_update_tags_aggregation?

      changed_contexts.each do |context|
        coll = MongoMapper.database.collection(self.class.aggregation_collection_for(context))
        field_name = self.class.tag_options_for(context)[:array_field]        
        old_tags, new_tags = changes[field_name]
        old_tags ||= []
        new_tags ||= []
        unchanged_tags = old_tags & new_tags
        tags_removed = old_tags - unchanged_tags
        tags_added = new_tags - unchanged_tags

        tags_removed.each do |t|
          coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
        end

        tags_added.each do |t|
          coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
        end
      end
    end
    
    def need_update_tags_aggregation?
      !changed_contexts.empty?
    end
    
    def changed_contexts
      tag_contexts & changes.keys.map(&:to_sym)
    end
  END
end

#tagged_with(context, tags) ⇒ Criteria

Find documents tagged with all tags passed as a parameter, given as an Array or a String using the configured separator.

Examples:

Find matching all tags in an Array.

Article.tagged_with(['ruby', 'mongodb'])

Find matching all tags in a String.

Article.tagged_with('ruby, mongodb')

Parameters:

  • :field (String)

    The field name of the tag.

  • :tags (Array<String, Symbol>, String)

    Tags to match.

Returns:

  • (Criteria)

    A new criteria.



209
210
211
212
213
# File 'lib/taggregator.rb', line 209

def tagged_with(context, tags)
  tags = convert_string_to_array(tags, get_tag_separator_for(context)) if tags.is_a? String
  array_field = tag_options_for(context)[:array_field]
  where(:"#{array_field}".in => tags)
end

#tags_for(context, conditions = {}) ⇒ Object



176
177
178
179
# File 'lib/taggregator.rb', line 176

def tags_for(context, conditions={})
  conditions = {:sort => '_id'}.merge(conditions)
  MongoMapper.database.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| t["_id"] }
end

#tags_with_weight_for(context, conditions = {}) ⇒ Object

retrieve the list of tag with weight(count), this is useful for creating tag clouds



183
184
185
186
187
188
# File 'lib/taggregator.rb', line 183

def tags_with_weight_for(context, conditions={})
  conditions = {:sort => '_id'}.merge(conditions)
  MongoMapper.database.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).
    to_a.map{ |t| [t["_id"], t["value"]] }.
    sort{ |a,b| b[1] <=> a[1] }
end