Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f736dcf
Ditch Jeweler and use Bundler
ches Nov 29, 2010
752593e
New: add tagged_with for search objects by tags
petRUShka Nov 28, 2010
0118efe
Fix: remove self. from tagged_with
petRUShka Nov 28, 2010
bc332f8
Fix: add field :attr
petRUShka Nov 28, 2010
ba35684
Fix: migrate from make (machinist) to create
petRUShka Nov 28, 2010
2785aea
Use ActiveSupport::Concern in the style of Mongoid 2.x
ches Nov 27, 2010
48d531b
Ascending index is default
ches Nov 27, 2010
bafe282
Remove rails/init.rb deprecated in Rails 3
ches Nov 29, 2010
3cff878
Make the spec suite run standalone
ches Nov 29, 2010
a08afc9
Fix class method call
ches Dec 14, 2010
1cf0c00
Don't track Gemfile.lock
ches Jan 12, 2011
40e0467
Refactoring to use declarative macro for options
ches Jan 12, 2011
738bb10
Rename weighting "indexing" => "aggregation" & default to off
ches Jan 12, 2011
3edae59
Use let in specs
ches Jan 12, 2011
07dae2f
Do not run map/reduce on edit if tags are unchanged
ches Jan 12, 2011
6742730
Major changes: customize field name, return array by default
ches Jan 13, 2011
254e62d
Docs
ches Jan 14, 2011
ba15672
Remove unneeded config calls
ches Jan 15, 2011
55f894f
Encapsulation
ches Jan 15, 2011
acbf731
I think I can call myself an author at this point
ches Jan 15, 2011
da383a2
Merge branch 'ches-fork'
fagiani Mar 6, 2011
a24211a
fixing event callback for new records
fagiani Mar 9, 2011
8fe2584
deleted tags are no longer aggregated
fagiani Mar 9, 2011
2caa066
no reason for Gemfile.lock not be gitted
fagiani Mar 9, 2011
a711716
adding myself as an author
fagiani Mar 9, 2011
8e5d801
reproducing Teddy's reject empty tags
fagiani Mar 10, 2011
2c57412
removing blanks from array instead of string benefiting both uses
fagiani Mar 10, 2011
cd9c67f
split strings within the input array
fagiani Mar 11, 2011
25d3ed4
improving array conversion code
fagiani Mar 12, 2011
c5a013b
fixing mongoid_taggable url
fagiani Mar 12, 2011
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.redcar
.redcar
.bundle
rdoc
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--color
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Use `bundle install` in order to install these gems
# Use `bundle exec rake` in order to run the specs using the bundle
source "http://rubygems.org"
gemspec
46 changes: 46 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
PATH
remote: .
specs:
mongoid_taggable (0.3.0)
mongoid (~> 2.0.0.beta.20)

GEM
remote: http://rubygems.org/
specs:
activemodel (3.0.5)
activesupport (= 3.0.5)
builder (~> 2.1.2)
i18n (~> 0.4)
activesupport (3.0.5)
bson (1.2.4)
builder (2.1.2)
database_cleaner (0.6.5)
diff-lcs (1.1.2)
i18n (0.5.0)
mongo (1.2.4)
bson (>= 1.2.4)
mongoid (2.0.0.rc.7)
activemodel (~> 3.0)
mongo (~> 1.2)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
rake (0.8.7)
rspec (2.1.0)
rspec-core (~> 2.1.0)
rspec-expectations (~> 2.1.0)
rspec-mocks (~> 2.1.0)
rspec-core (2.1.0)
rspec-expectations (2.1.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.1.0)
tzinfo (0.3.24)
will_paginate (3.0.pre2)

PLATFORMS
ruby

DEPENDENCIES
database_cleaner (~> 0.6.0)
mongoid_taggable!
rake (~> 0.8.7)
rspec (~> 2.1.0)
49 changes: 25 additions & 24 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Mongoid Taggable provides some helpers to create taggable documents.

h2. Installation

You can simple install from rubygems:
You can simply install from rubygems:

bc. gem install mongoid_taggable

Expand All @@ -18,17 +18,19 @@ bc. script/plugin install git://github.com/wilkerlucio/mongoid_taggable.git

h2. Basic Usage

To make a document taggable you just need to include Mongoid::Taggable into your document:
To make a document taggable you need to include Mongoid::Taggable into your document and call the @taggable@ macro with optional arguments:

bc.. class Post
include Mongoid::Document
include Mongoid::Taggable

field :title
field :content

taggable
end

p. Them in your form:
p. Then in your form, for example:

bc.. <% form_for @post do |f| %>
<p>
Expand All @@ -41,22 +43,32 @@ bc.. <% form_for @post do |f| %>
</p>
<p>
<%= f.label :tags %><br />
<%= f.text_field :tags %>
<%= text_field_tag 'post[tags]', (@post.tags.join(', ') if @post.tags) %>
</p>
<p>
<button type="submit">Send</button>
</p>
<% end %>

p. In this case, the text fields for tags should receive the list of tags separated by comma (below in this document you will see how to change the separator)
p. You can of course use helpers or a @FormBuilder@ extension to express this in a prettier way. In this case, the text field for tags should receive the list of tags separated by comma (below in this document you will see how to change the separator).

p. Your document will have a custom @tags=@ setter which can accept either an ordinary Array or this separator-delineated String.

h2. Tag Aggregation with Counts

p. Then your document will have the @tags@ and @tags_array@ getter and setter. The @tags@ you use as a plain string with tags separated by comma, and the @tags_array@ is an array with tags. These two properties are automatically synchronized.
This lib can automatically create an aggregate collection of tags and their counts for you, updated as documents are saved. This is useful for getting a list of all tags used in documents of this collection or to create a tag cloud. This is disabled by default for sake of performance where it is unneeded -- see the following example to understand how to use it:

bc.. class Post
include Mongoid::Document
include Mongoid::Taggable

h2. Tags Indexing
field :title
field :content

This lib will automatically create an index of tags and their counts for you after saving the document, useful for getting a list of all tags used in documents of this collection or to create a tag cloud. See the following example to understand how to use it:
taggable :aggregation => true
end

bc.. Post.create!(:tags => "food,ant,bee")
Post.create!(:tags => "food,ant,bee")
Post.create!(:tags => "juice,food,bee,zip")
Post.create!(:tags => "honey,strip,food")

Expand All @@ -72,28 +84,17 @@ Post.tags_with_weight # will retrieve:
# ['zip', 1]
# ]

p. If you don't want to use this feature, it is good to disable it to improve performance:

bc.. class Post
include Mongoid::Document
include Mongoid::Taggable

disable_tags_index! # will disable index creation

field :title
field :content
end

h2. Changing default separator

To change the default separator you may call the @tags_separator@ class method:
To change the default separator you may pass a @separator@ argument to the macro:

bc.. class Post
include Mongoid::Document
include Mongoid::Taggable

tags_separator ';' # will change tags separator to ;

field :title
field :content

taggable :separator => ' ' # tags will be delineated by spaces
end

25 changes: 12 additions & 13 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
require 'bundler'
Bundler.setup

require 'rake'
require 'rake/rdoctask'
require 'rspec'
require 'rspec/core/rake_task'

begin
require 'jeweler'
Jeweler::Tasks.new do |gemspec|
gemspec.name = "mongoid_taggable"
gemspec.version = "0.1.2"
gemspec.summary = "Mongoid taggable behaviour"
gemspec.description = "Mongoid Taggable provides some helpers to create taggable documents."
gemspec.email = "wilkerlucio@gmail.com"
gemspec.homepage = "http://github.com/wilkerlucio/mongo_taggable"
gemspec.authors = ["Wilker Lucio", "Kris Kowalik"]
end
rescue LoadError
puts "Jeweler not available. Install it with: gem install jeweler"
task :gem => :build
task :build do
system "gem build mongoid_taggable.gemspec"
end

task :install => :build do
system "gem install mongoid_taggable-#{Mongoid::Taggable::VERSION}.gem"
end

desc 'Default: run unit tests.'
task :default => :spec
Expand All @@ -34,3 +30,6 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end

desc 'Default: run unit tests.'
task :default => :spec
145 changes: 95 additions & 50 deletions lib/mongoid/taggable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,102 @@
# limitations under the License.

module Mongoid::Taggable
def self.included(base)
# create fields for tags and index it
base.field :tags_array, :type => Array
base.index [['tags_array', Mongo::ASCENDING]]

# add callback to save tags index
base.after_save do |document|
document.class.save_tags_index!
end

# extend model
base.extend ClassMethods
base.send :include, InstanceMethods

# enable indexing as default
base.enable_tags_index!
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, :aggregate_tags, :to => 'self.class'

set_callback :create, :after, :aggregate_tags
set_callback :destroy, :after, :aggregate_tags
set_callback :save, :after, :aggregate_tags, :if => proc { previous_changes.include?(tags_field.to_s) }
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 = Mongoid::Config.master
db.collection(tags_index_collection).find.to_a.map{ |r| r["_id"] }
db.collection(tags_aggregation_collection).find.to_a.map{ |r| r["_id"] }
end

# retrieve the list of tags with weight(count), this is usefull for
# retrieve the list of tags with weight(count), this is useful for
# creating tag clouds
def tags_with_weight
db = Mongoid::Config.master
db.collection(tags_index_collection).find.to_a.map{ |r| [r["_id"], r["value"]] }
db.collection(tags_aggregation_collection).find.to_a.map{ |r| [r["_id"], r["value"]] }
end

def disable_tags_index!
@do_tags_index = false
# 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

def enable_tags_index!
@do_tags_index = true
# Collection name for storing results of tag count aggregation
def tags_aggregation_collection
@tags_aggregation_collection ||= "#{collection_name}_tags_aggregation"
end

def tags_separator(separator = nil)
@tags_separator = separator if separator
@tags_separator || ','
end

def tags_index_collection
"#{collection_name}_tags_index"
end

def save_tags_index!
return unless @do_tags_index

db = Mongoid::Config.master
coll = db.collection(collection_name)
# Execute map/reduce operation to aggregate tag counts for document
# class
def aggregate_tags
return unless tag_aggregation

map = "function() {
if (!this.tags_array) {
if (!this.#{tags_field}) {
return;
}

for (index in this.tags_array) {
emit(this.tags_array[index], 1);
for (index in this.#{tags_field}) {
emit(this.#{tags_field}[index], 1);
}
}"

Expand All @@ -90,17 +122,30 @@ def save_tags_index!
return count;
}"

coll.map_reduce(map, reduce, :out => tags_index_collection)
collection.master.map_reduce(map, reduce, :out => tags_aggregation_collection)
end
end

module InstanceMethods
def tags
(tags_array || []).join(self.class.tags_separator)
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)
end

def tags=(tags)
self.tags_array = tags.split(self.class.tags_separator).map(&:strip)
# 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 |values|
case values
when String
values = convert_string_tags_to_array(values)
when Array
values = values.inject([]) { |final, value| final.concat convert_string_tags_to_array(value) }
end
send("#{name}_without_taggable=", values.map(&:strip).reject(&:blank?))
end
alias_method_chain "#{name}=", :taggable
end
end
end
6 changes: 6 additions & 0 deletions lib/mongoid/taggable/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# encoding: utf-8
module Mongoid #:nodoc
module Taggable #:nodoc
VERSION = '0.3.0'
end
end
2 changes: 2 additions & 0 deletions lib/mongoid_taggable.rb
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
require 'active_support/concern'

require File.join(File.dirname(__FILE__), 'mongoid/taggable')
Loading