Created
March 9, 2011 19:22
-
-
Save fagiani/862783 to your computer and use it in GitHub Desktop.
This is the current refactor I am working on
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright (c) 2010 Wilker Lúcio <[email protected]> | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
module Mongoid | |
module Taggable | |
extend ActiveSupport::Concern | |
included do | |
class_inheritable_reader :tags_field | |
class_inheritable_accessor :tags_separator, :tag_aggregation, | |
:instance_writer => false | |
delegate :convert_string_tags_to_array, :to => 'self.class' | |
set_callback :create, :after, :aggregate_tags | |
set_callback :save, :after, :aggregate_tags, :if => proc { previous_changes.include?(tags_field.to_s) } | |
end | |
# Execute map/reduce operation to aggregate tag counts for document | |
# class | |
def aggregate_tags | |
return unless tag_aggregation | |
map = "function() { | |
if (!this.#{tags_field}) { | |
return; | |
} | |
for (index in this.#{tags_field}) { | |
emit(this.#{tags_field}[index], 1); | |
} | |
}" | |
reduce = "function(previous, current) { | |
var count = 0; | |
for (index in current) { | |
count += current[index] | |
} | |
return count; | |
}" | |
collection.master.map_reduce(map, reduce, :out => "#{collection_name}_tags_aggregation") | |
end | |
module ClassMethods | |
# Macro to declare a document class as taggable, specify field name | |
# for tags, and set options for tagging behavior. | |
# | |
# @example Define a taggable document. | |
# | |
# class Article | |
# include Mongoid::Document | |
# include Mongoid::Taggable | |
# taggable :keywords, :separator => ' ', :aggregation => true | |
# end | |
# | |
# @param [ Symbol ] field The name of the field for tags. | |
# @param [ Hash ] options Options for taggable behavior. | |
# | |
# @option options [ String ] :separator The tag separator to | |
# convert from; defaults to ',' | |
# @option options [ true, false ] :aggregation Whether or not to | |
# aggregate counts of tags within the document collection using | |
# map/reduce; defaults to false | |
def taggable(*args) | |
options = args.extract_options! | |
options.reverse_merge!( | |
:separator => ',', | |
:aggregation => false | |
) | |
write_inheritable_attribute(:tags_field, args.blank? ? :tags : args.shift) | |
self.tags_separator = options[:separator] | |
self.tag_aggregation = options[:aggregation] | |
field tags_field, :type => Array | |
index tags_field | |
define_tag_field_accessors(tags_field) | |
end | |
# get an array with all defined tags for this model, this list returns | |
# an array of distinct ordered list of tags defined in all documents | |
# of this model | |
def tags | |
db.collection(tags_aggregation_collection).find.to_a.map{ |r| r["_id"] } | |
end | |
# retrieve the list of tags with weight(count), this is useful for | |
# creating tag clouds | |
def tags_with_weight | |
db.collection(tags_aggregation_collection).find.to_a.map{ |r| [r["_id"], r["value"]] } | |
end | |
# Find documents tagged with all tags passed as a parameter, given | |
# as an Array or a String using the configured separator. | |
# | |
# @example Find matching all tags in an Array. | |
# Article.tagged_with(['ruby', 'mongodb']) | |
# @example Find matching all tags in a String. | |
# Article.tagged_with('ruby, mongodb') | |
# | |
# @param [ Array<String, Symbol>, String ] _tags Tags to match. | |
# @return [ Criteria ] A new criteria. | |
def tagged_with(_tags) | |
_tags = convert_string_tags_to_array(_tags) if _tags.is_a? String | |
criteria.all_in(tags_field => _tags) | |
end | |
# Collection name for storing results of tag count aggregation | |
def tags_aggregation_collection | |
@tags_aggregation_collection ||= "#{collection_name}_tags_aggregation" | |
end | |
private | |
# Helper method to convert a String to an Array based on the | |
# configured tag separator. | |
def convert_string_tags_to_array(_tags) | |
(_tags).split(tags_separator).map(&:strip) | |
end | |
# Define modifier for the configured tag field name that overrides | |
# the default to transparently convert tags given as a String. | |
def define_tag_field_accessors(name) | |
define_method "#{name}_with_taggable=" do |value| | |
value = convert_string_tags_to_array(value) if value.is_a? String | |
send("#{name}_without_taggable=", value) | |
end | |
alias_method_chain "#{name}=", :taggable | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright (c) 2010 Wilker Lúcio <[email protected]> | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
require File.join(File.dirname(__FILE__), %w[.. spec_helper]) | |
class MyModel | |
include Mongoid::Document | |
include Mongoid::Taggable | |
field :attr | |
taggable | |
end | |
class Article | |
include Mongoid::Document | |
include Mongoid::Taggable | |
taggable :keywords | |
end | |
class Editorial < Article | |
self.tags_separator = ' ' | |
self.tag_aggregation = true | |
end | |
class Template | |
include Mongoid::Document | |
include Mongoid::Taggable | |
include Mongoid::Timestamps | |
taggable :aggregation => true | |
end | |
describe Mongoid::Taggable do | |
context "saving tags from plain text" do | |
let(:model) { MyModel.new } | |
it "should set tags array from string" do | |
model.tags = "some,new,tag" | |
model.tags.should == %w[some new tag] | |
end | |
it "should strip tags before put in array" do | |
model.tags = "now , with, some spaces , in places " | |
model.tags.should == ["now", "with", "some spaces", "in places"] | |
end | |
end | |
context "with customized tag field name" do | |
let(:article) { Article.new } | |
it "should set tags array from string" do | |
article.keywords = "some,new,tag" | |
article.keywords.should == %w[some new tag] | |
end | |
end | |
context "changing separator" do | |
before :all do | |
MyModel.tags_separator = ";" | |
end | |
after :all do | |
MyModel.tags_separator = "," | |
end | |
let(:model) { MyModel.new } | |
it "should split with custom separator" do | |
model.tags = "some;other;separator" | |
model.tags.should == %w[some other separator] | |
end | |
end | |
context "tag & count aggregation" do | |
it "should generate the aggregate collection name based on model" do | |
MyModel.tags_aggregation_collection.should == "my_models_tags_aggregation" | |
end | |
it "should be disabled by default" do | |
MyModel.create!(:tags => "sample,tags") | |
MyModel.tags.should == [] | |
end | |
context "when enabled" do | |
before :all do | |
MyModel.tag_aggregation = true | |
end | |
after :all do | |
MyModel.tag_aggregation = false | |
end | |
let!(:models) do | |
[ | |
MyModel.create!(:tags => "food,ant,bee"), | |
MyModel.create!(:tags => "juice,food,bee,zip"), | |
MyModel.create!(:tags => "honey,strip,food") | |
] | |
end | |
it "should list all saved tags distinct and ordered" do | |
MyModel.tags.should == %w[ant bee food honey juice strip zip] | |
end | |
it "should list all tags with their weights" do | |
MyModel.tags_with_weight.should == [ | |
['ant', 1], | |
['bee', 2], | |
['food', 3], | |
['honey', 1], | |
['juice', 1], | |
['strip', 1], | |
['zip', 1] | |
] | |
end | |
it "should update when tags are edited" do | |
MyModel.should_receive(:aggregate_tags) | |
models.first.update_attributes(:tags => 'changed') | |
end | |
it "should not update if tags are unchanged" do | |
MyModel.should_not_receive(:aggregate_tags) | |
models.first.update_attributes(:attr => "changed") | |
end | |
end | |
end | |
context "#self.tagged_with" do | |
let!(:models) do | |
[ | |
MyModel.create!(:tags => "tag1,tag2,tag3"), | |
MyModel.create!(:tags => "tag2"), | |
MyModel.create!(:tags => "tag1", :attr => "value") | |
] | |
end | |
it "should return all tags with single tag input" do | |
MyModel.tagged_with("tag2").sort_by{|a| a.id.to_s}.should == [models.first, models.second].sort_by{|a| a.id.to_s} | |
end | |
it "should return all tags with tags array input" do | |
MyModel.tagged_with(%w{tag2 tag1}).should == [models.first] | |
end | |
it "should return all tags with tags string input" do | |
MyModel.tagged_with("tag2,tag1").should == [models.first] | |
end | |
it "should be able to be part of methods chain" do | |
MyModel.tagged_with("tag1").where(:attr => "value").should == [models.last] | |
end | |
end | |
context "a subclass of a taggable document" do | |
let(:editorial) { Editorial.new } | |
it "can enable tag aggregation exclusively" do | |
Article.tag_aggregation.should == false | |
Editorial.tag_aggregation.should == true | |
end | |
it "can split with a different separator" do | |
editorial.keywords = 'opinion politics' | |
editorial.keywords.should == %w[opinion politics] | |
end | |
end | |
context "using Mongoid::Timestamps along" do | |
it "should list all saved tags distinct and ordered with custom tag attribute" do | |
Template.create!(:tags => 'food, ant, bee') | |
Template.tags.should == %w[ant bee food] | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment