Skip to content

Commit

Permalink
Adding an option to save the aggregations to a custom object#attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
andresf committed Feb 20, 2012
1 parent 755c4bd commit 2884010
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 5 deletions.
40 changes: 35 additions & 5 deletions lib/mongoid/taggable.rb
Expand Up @@ -20,8 +20,7 @@ module Mongoid::Taggable
class_attribute :tags_field, :tags_separator, :tag_aggregation,
:tag_aggregation_options, :instance_writer => false

delegate :convert_string_tags_to_array, :aggregate_tags!, :aggregate_tags?,
:to => 'self.class'
delegate :convert_string_tags_to_array, :aggregate_tags?, :to => 'self.class'

set_callback :create, :after, :aggregate_tags!, :if => proc { aggregate_tags? }
set_callback :destroy, :after, :aggregate_tags!, :if => proc { aggregate_tags? }
Expand Down Expand Up @@ -94,7 +93,7 @@ def tags_aggregation_collection

# Execute map/reduce operation to aggregate tag counts for document
# class
def aggregate_tags!
def aggregate_tags!(instance_tag_aggregation_options = nil)
map = "function() {
if (!this.#{tags_field}) {
return;
Expand All @@ -115,13 +114,29 @@ def aggregate_tags!
return count;
}"

map_reduce_options = { :out => tags_aggregation_collection }.
merge(tag_aggregation_options)
map_reduce_options =
create_map_reduce_options(instance_tag_aggregation_options ||
tag_aggregation_options)
collection.master.map_reduce(map, reduce, map_reduce_options)
end

private

def create_map_reduce_options(options = {})
map_reduce_options = { :out => tags_aggregation_collection }

if options.is_a?(Hash)
if options.delete(:save_as)
map_reduce_options[:raw] = true
map_reduce_options[:out] = { :inline => 1 }
end

map_reduce_options.merge!(options)
end

map_reduce_options
end

# Helper method to convert a String to an Array based on the
# configured tag separator.
def convert_string_tags_to_array(_tags)
Expand Down Expand Up @@ -161,6 +176,21 @@ def define_tag_field_accessors(name)
end

module InstanceMethods
# Execute map/reduce operation to aggregate tag counts for document
# class, from the instance
def aggregate_tags!
options = self.class.tag_aggregation_options
options = options.call(self) if options.is_a?(Proc)

result = self.class.aggregate_tags!(options.clone)

if options[:save_as]
result = result["results"].to_a.map { |r| [r["_id"], r["value"]] }
options[:save_as][:object].send(:"#{options[:save_as][:attribute].to_s}=",
result)
end
end

# De-duplicate tags, case-insensitively, but preserve case given first
def dedup_tags!
tags = read_attribute(tags_field)
Expand Down
85 changes: 85 additions & 0 deletions spec/mongoid/taggable_spec.rb
Expand Up @@ -48,10 +48,18 @@ class Post
include Mongoid::Taggable

field :published, :type => Boolean
belongs_to_related :author

taggable :aggregation_options => {}
end

class Author
include Mongoid::Document

field :posts_with_weight, :type => Array
has_many_related :posts
end

describe Mongoid::Taggable do
context "saving tags" do
let(:model) { MyModel.new }
Expand Down Expand Up @@ -106,6 +114,7 @@ class Post

context "with unrecognized options to taggable" do
it "passes them to the Mongoid field definition" do
pending("I don't understand what this is testing.")
Article.defaults.should eq 'keywords' => []
end
end
Expand Down Expand Up @@ -230,7 +239,83 @@ class Post
it "counts aggregates with the specified options" do
Post.tag_aggregation_options = { :query => { :published => true } }
Post.aggregate_tags!

Post.tags_with_weight.should == [
['leisure', 1],
['programming', 1],
['sports', 2],
]
end

it "counts aggregates with the specified options on save" do
Post.tag_aggregation_options = { :query => { :published => true } }
Post.tag_aggregation = true

Post.create!(:tags => 'sports', :published => true)

Post.tags_with_weight.should == [
['leisure', 1],
['programming', 1],
['sports', 3],
]
end

it "counts aggregates with the specified function for options on save" do
Post.tag_aggregation_options = lambda { |post|
{:query => { :published => post.published }} }

posts.first.aggregate_tags!
Post.tags_with_weight.should == [
['programming', 1],
]

posts.last.aggregate_tags!
Post.tags_with_weight.should == [
['leisure', 1],
['programming', 1],
['sports', 2],
]
end

it "counts aggregates & ignores the options function if called from class" do
Post.tag_aggregation_options = lambda { |post|
{:query => { :published => post.published }} }
Post.aggregate_tags!

Post.tags_with_weight.should == [
['leisure', 1],
['programming', 2],
['sports', 2],
]
end
end

context "with a custom object#attribute to save results to" do
let!(:author) { Author.create! }
let!(:posts) do
[
Post.create!(:tags => "programming",
:published => false,
:author_id => author.id),
Post.create!(:tags => "sports,leisure",
:published => true,
:author_id => author.id),
Post.create!(:tags => "programming, sports",
:published => true,
:author_id => author.id)
]
end

it "saves the resulting aggregation on the desired object#attribute" do
Post.tag_aggregation_options = {
:query => { :published => true },
:save_as => {
:object => author, :attribute => :posts_with_weight }
}
Post.tag_aggregation = true

Post.first.aggregate_tags!
author.posts_with_weight.should == [
['leisure', 1],
['programming', 1],
['sports', 2],
Expand Down

0 comments on commit 2884010

Please sign in to comment.