taxonomy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +31 -0
- data/lib/generators/taxonomy/migration/migration_generator.rb +39 -0
- data/lib/generators/taxonomy/migration/templates/active_record/migration.rb +36 -0
- data/lib/tasks/taxonomy_tasks.rake +4 -0
- data/lib/taxonomy.rb +25 -0
- data/lib/taxonomy/group_helper.rb +12 -0
- data/lib/taxonomy/has_tagger.rb +52 -0
- data/lib/taxonomy/has_taxonomy.rb +502 -0
- data/lib/taxonomy/tag.rb +485 -0
- data/lib/taxonomy/tag_list.rb +97 -0
- data/lib/taxonomy/tagging.rb +12 -0
- data/lib/taxonomy/tags_helper.rb +13 -0
- data/lib/taxonomy/version.rb +3 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +3 -0
- data/spec/dummy/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/dummy/app/models/other_taggable_model.rb +4 -0
- data/spec/dummy/app/models/post.rb +2 -0
- data/spec/dummy/app/models/taggable_model.rb +6 -0
- data/spec/dummy/app/models/taggable_user.rb +3 -0
- data/spec/dummy/app/models/treed_model.rb +3 -0
- data/spec/dummy/app/models/untaggable_model.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +19 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/migrate/20111221004133_create_posts.rb +8 -0
- data/spec/dummy/db/migrate/20111221023928_taxonomy_migration.rb +35 -0
- data/spec/dummy/db/migrate/20111221024100_create_bulk.rb +18 -0
- data/spec/dummy/db/schema.rb +65 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +100915 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/posts.rb +6 -0
- data/spec/generators/taxonomy/migration/migration_generator_spec.rb +22 -0
- data/spec/models/post_spec.rb +5 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/taxonomy/group_helper_spec.rb +21 -0
- data/spec/taxonomy/has_tagger_spec.rb +113 -0
- data/spec/taxonomy/has_taxonomy_spec.rb +226 -0
- data/spec/taxonomy/tag_list_spec.rb +70 -0
- data/spec/taxonomy/tag_spec.rb +462 -0
- data/spec/taxonomy/taggable_spec.rb +262 -0
- data/spec/taxonomy/tagger_spec.rb +40 -0
- data/spec/taxonomy/tagging_spec.rb +25 -0
- data/spec/taxonomy/tags_helper_spec.rb +29 -0
- metadata +225 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2011 Seth Faxon
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
= Taxonomy
|
2
|
+
|
3
|
+
this gem is an active record plugin. think of it as a merger of http://rubygems.org/gems/acts-as-taggable-on and http://rubygems.org/gems/awesome_nested_set because, well it is. With some glue code and updates to the models.
|
4
|
+
|
5
|
+
|
6
|
+
== Setup Rails 3.x
|
7
|
+
|
8
|
+
Add to Gemfile:
|
9
|
+
|
10
|
+
gem 'taxonomy'
|
11
|
+
|
12
|
+
Run migration:
|
13
|
+
|
14
|
+
rails generate taxonomy:migration
|
15
|
+
rake db:migrate
|
16
|
+
|
17
|
+
|
18
|
+
== Usage
|
19
|
+
|
20
|
+
to setup plain tags and a treed set of categories
|
21
|
+
|
22
|
+
has_taxonomy_on :tags, {:treed => [:categories]}
|
23
|
+
|
24
|
+
|
25
|
+
Tag.roots.where(:context => "category")
|
26
|
+
|
27
|
+
|
28
|
+
Tag.find_context_with_slug!("category", params[:slug])
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rake/dsl_definition'
|
9
|
+
require 'rdoc/task'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
14
|
+
end
|
15
|
+
|
16
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'Taxonomy'
|
19
|
+
rdoc.options << '--line-numbers'
|
20
|
+
rdoc.rdoc_files.include('README.rdoc')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
25
|
+
# load 'rails/tasks/engine.rake'
|
26
|
+
|
27
|
+
Bundler::GemHelper.install_tasks
|
28
|
+
|
29
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
30
|
+
|
31
|
+
task :default => :spec
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Taxonomy
|
5
|
+
class MigrationGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
desc "Generates migration for Tag and Tagging models"
|
9
|
+
|
10
|
+
def self.orm
|
11
|
+
Rails::Generators.options[:rails][:orm]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.source_root
|
15
|
+
File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.orm_has_migration?
|
19
|
+
[:active_record].include? orm
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.next_migration_number(dirname)
|
23
|
+
if ActiveRecord::Base.timestamped_migrations
|
24
|
+
migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
25
|
+
migration_number += 1
|
26
|
+
migration_number.to_s
|
27
|
+
else
|
28
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_migration_file
|
33
|
+
if self.class.orm_has_migration?
|
34
|
+
migration_template 'migration.rb', 'db/migrate/taxonomy_migration'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class TaxonomyMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :tags do |t|
|
4
|
+
t.integer :parent_id
|
5
|
+
t.integer :lft
|
6
|
+
t.integer :rgt
|
7
|
+
t.string :name
|
8
|
+
t.string :context
|
9
|
+
t.string :slug
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :taggings do |t|
|
13
|
+
t.references :tag
|
14
|
+
|
15
|
+
# You should make sure that the column created is
|
16
|
+
# long enough to store the required class names.
|
17
|
+
t.references :taggable, :polymorphic => true
|
18
|
+
t.references :tagger, :polymorphic => true
|
19
|
+
|
20
|
+
t.datetime :created_at
|
21
|
+
end
|
22
|
+
|
23
|
+
add_index :tags, [:parent_id]
|
24
|
+
add_index :tags, [:lft, :rgt]
|
25
|
+
add_index :tags, :context
|
26
|
+
add_index :tags, :slug
|
27
|
+
|
28
|
+
add_index :taggings, :tag_id
|
29
|
+
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.down
|
33
|
+
drop_table :taggings
|
34
|
+
drop_table :tags
|
35
|
+
end
|
36
|
+
end
|
data/lib/taxonomy.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Taxonomy
|
2
|
+
mattr_accessor :nested_set_options
|
3
|
+
@@nested_set_options = { :parent_column => 'parent_id',
|
4
|
+
:left_column => 'lft',
|
5
|
+
:right_column => 'rgt',
|
6
|
+
:dependent => :destroy
|
7
|
+
}
|
8
|
+
|
9
|
+
def self.setup
|
10
|
+
yield self
|
11
|
+
@@nested_set_options.symbolize_keys!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'taxonomy/group_helper'
|
16
|
+
require 'taxonomy/has_taxonomy'
|
17
|
+
require 'taxonomy/has_tagger'
|
18
|
+
require 'taxonomy/tag'
|
19
|
+
require 'taxonomy/tag_list'
|
20
|
+
require 'taxonomy/tags_helper'
|
21
|
+
require 'taxonomy/tagging'
|
22
|
+
|
23
|
+
ActiveRecord::Base.send :include, ActiveRecord::Acts::Taxonomy
|
24
|
+
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
|
25
|
+
ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Taxonomy
|
4
|
+
module GroupHelper
|
5
|
+
# all column names are necessary for PostgreSQL group clause
|
6
|
+
def grouped_column_names_for(object)
|
7
|
+
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Tagger
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def acts_as_tagger(opts={})
|
10
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
11
|
+
:include => :tag, :class_name => "Tagging")
|
12
|
+
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
13
|
+
include ActiveRecord::Acts::Tagger::InstanceMethods
|
14
|
+
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_tagger?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def self.included(base)
|
24
|
+
end
|
25
|
+
|
26
|
+
def tag(taggable, opts={})
|
27
|
+
opts.reverse_merge!(:force => true)
|
28
|
+
|
29
|
+
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
30
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
31
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
32
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
|
33
|
+
( opts[:force] || taggable.tag_types.include?(opts[:on]) )
|
34
|
+
|
35
|
+
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
36
|
+
taggable.save
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_tagger?
|
40
|
+
self.class.is_tagger?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module SingletonMethods
|
45
|
+
def is_tagger?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,502 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Taxonomy
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def taggable?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def acts_as_taggable
|
14
|
+
has_taxonomy_on :tags
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_taxonomy_on(*args)
|
18
|
+
treed_args = []
|
19
|
+
args.flatten! if args
|
20
|
+
args.compact! if args
|
21
|
+
# pull out treed args
|
22
|
+
args.each do |tag_type|
|
23
|
+
if tag_type.is_a?(Hash)
|
24
|
+
treed_tags = tag_type.values.flatten
|
25
|
+
treed_tags.compact!
|
26
|
+
|
27
|
+
treed_tags.each do |tree|
|
28
|
+
treed_args << tree
|
29
|
+
end
|
30
|
+
args.delete(tag_type)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if !(args & treed_args).empty?
|
35
|
+
raise "duplicate taxonomy keys"
|
36
|
+
end
|
37
|
+
args.concat(treed_args)
|
38
|
+
args.flatten!
|
39
|
+
args.map!{|x| x.to_s} # convert to strings
|
40
|
+
treed_args.map!{|x| x.to_s}
|
41
|
+
args.each do |tag_type|
|
42
|
+
# use aliased_join_table_name for context condition so that sphinx can join multiple
|
43
|
+
# tag references from same model without getting an ambiguous column error
|
44
|
+
class_eval do
|
45
|
+
has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
|
46
|
+
:include => :tag, :conditions => ['tags.context = ?',tag_type.singularize], :class_name => "Tagging"
|
47
|
+
has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
|
48
|
+
end
|
49
|
+
|
50
|
+
class_eval <<-RUBY
|
51
|
+
def self.taggable?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.#{tag_type}_treed?
|
56
|
+
treed_list_on?("#{tag_type}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.caching_#{tag_type.singularize}_list?
|
60
|
+
caching_tag_list_on?("#{tag_type}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.#{tag_type.singularize}_counts(options={})
|
64
|
+
tag_counts_on('#{tag_type}',options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def #{tag_type.singularize}_list
|
68
|
+
tag_list_on('#{tag_type}')
|
69
|
+
end
|
70
|
+
|
71
|
+
def #{tag_type.singularize}_list=(new_tags)
|
72
|
+
set_tag_list_on('#{tag_type}',new_tags)
|
73
|
+
end
|
74
|
+
|
75
|
+
def #{tag_type.singularize}_counts(options = {})
|
76
|
+
tag_counts_on('#{tag_type}',options)
|
77
|
+
end
|
78
|
+
|
79
|
+
def #{tag_type}_from(owner)
|
80
|
+
tag_list_on('#{tag_type}', owner)
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_related_#{tag_type}(options = {})
|
84
|
+
related_tags_for('#{tag_type}', self.class, options)
|
85
|
+
end
|
86
|
+
alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
|
87
|
+
|
88
|
+
def find_related_#{tag_type}_for(klass, options = {})
|
89
|
+
related_tags_for('#{tag_type}', klass, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_matching_contexts(search_context, result_context, options = {})
|
93
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
97
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
98
|
+
end
|
99
|
+
|
100
|
+
def top_#{tag_type}(limit = 10)
|
101
|
+
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.top_#{tag_type}(limit = 10)
|
105
|
+
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
106
|
+
end
|
107
|
+
RUBY
|
108
|
+
|
109
|
+
end
|
110
|
+
if respond_to?(:tag_types)
|
111
|
+
write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
|
112
|
+
write_inheritable_attribute( :treed_tag_types, treed_args)
|
113
|
+
else
|
114
|
+
class_eval do
|
115
|
+
write_inheritable_attribute(:tag_types, args.uniq)
|
116
|
+
class_inheritable_reader :tag_types
|
117
|
+
|
118
|
+
write_inheritable_attribute( :treed_tag_types, treed_args.uniq)
|
119
|
+
class_inheritable_reader :treed_tag_types
|
120
|
+
|
121
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
122
|
+
has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
|
123
|
+
|
124
|
+
attr_writer :custom_contexts
|
125
|
+
|
126
|
+
before_save :save_cached_tag_list
|
127
|
+
after_save :save_tags
|
128
|
+
|
129
|
+
if respond_to?(:scope)
|
130
|
+
scope :tagged_with, lambda{ |*args|
|
131
|
+
find_options_for_find_tagged_with(*args)
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
include ActiveRecord::Acts::Taxonomy::InstanceMethods
|
138
|
+
extend ActiveRecord::Acts::Taxonomy::SingletonMethods
|
139
|
+
alias_method_chain :reload, :tag_list
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
module SingletonMethods
|
145
|
+
include ActiveRecord::Acts::Taxonomy::GroupHelper
|
146
|
+
# Pass either a tag string, or an array of strings or tags
|
147
|
+
#
|
148
|
+
# Options:
|
149
|
+
# :any - find models that match any of the given tags
|
150
|
+
# :exclude - Find models that are not tagged with the given tags
|
151
|
+
# :match_all - Find models that match all of the given tags, not just one
|
152
|
+
# :conditions - A piece of SQL conditions to add to the query
|
153
|
+
# :on - scopes the find to a context
|
154
|
+
def find_tagged_with(context, name, *args)
|
155
|
+
find_opts = {:on => context}
|
156
|
+
case args #fixme: args will probably always be an array
|
157
|
+
when Array
|
158
|
+
if args.first.is_a?(Hash)
|
159
|
+
find_opts.merge!(args.first)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
options = find_options_for_find_tagged_with(name, find_opts)
|
163
|
+
options.blank? ? [] : find(:all,options)
|
164
|
+
end
|
165
|
+
|
166
|
+
def caching_tag_list_on?(context)
|
167
|
+
column_names.include?("cached_#{context.to_s.singularize}_list")
|
168
|
+
end
|
169
|
+
|
170
|
+
def treed_list_on?(context)
|
171
|
+
treed_tag_types.include?(context)
|
172
|
+
end
|
173
|
+
|
174
|
+
def tag_counts_on(context, options = {})
|
175
|
+
Tag.all(find_options_for_tag_counts(options.merge({:on => context.to_s.singularize})))
|
176
|
+
end
|
177
|
+
|
178
|
+
def all_tag_counts(options = {})
|
179
|
+
Tag.all(find_options_for_tag_counts(options))
|
180
|
+
end
|
181
|
+
|
182
|
+
def find_options_for_find_tagged_with(tags, options = {})
|
183
|
+
tag_list = TagList.from(tags)
|
184
|
+
|
185
|
+
return {} if tag_list.empty?
|
186
|
+
|
187
|
+
joins = []
|
188
|
+
conditions = []
|
189
|
+
|
190
|
+
context = options.delete(:on)
|
191
|
+
context = context.to_s.singularize
|
192
|
+
|
193
|
+
if options.delete(:exclude)
|
194
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
195
|
+
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
196
|
+
|
197
|
+
elsif options.delete(:any)
|
198
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
199
|
+
conditions << "#{table_name}.#{primary_key} IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
200
|
+
|
201
|
+
else
|
202
|
+
tags = Tag.named_any(context, tag_list)
|
203
|
+
return { :conditions => "1 = 0" } unless tags.length == tag_list.length
|
204
|
+
|
205
|
+
tags.each do |tag|
|
206
|
+
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
207
|
+
prefix = "#{safe_tag}_#{rand(1024)}"
|
208
|
+
|
209
|
+
taggings_alias = "#{table_name}_taggings_#{prefix}"
|
210
|
+
|
211
|
+
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
212
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
213
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
214
|
+
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
215
|
+
# tagging_join << " AND " + sanitize_sql(["tags.context = ?", context.to_s]) if context
|
216
|
+
|
217
|
+
joins << tagging_join
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
|
222
|
+
|
223
|
+
if options.delete(:match_all)
|
224
|
+
joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
|
225
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
226
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
227
|
+
|
228
|
+
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
229
|
+
end
|
230
|
+
|
231
|
+
{ :joins => joins.join(" "),
|
232
|
+
:group => group,
|
233
|
+
:conditions => conditions.join(" AND "),
|
234
|
+
:readonly => false }.update(options)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Calculate the tag counts for all tags.
|
238
|
+
#
|
239
|
+
# Options:
|
240
|
+
# :start_at - Restrict the tags to those created after a certain time
|
241
|
+
# :end_at - Restrict the tags to those created before a certain time
|
242
|
+
# :conditions - A piece of SQL conditions to add to the query
|
243
|
+
# :limit - The maximum number of tags to return
|
244
|
+
# :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
245
|
+
# :at_least - Exclude tags with a frequency less than the given value
|
246
|
+
# :at_most - Exclude tags with a frequency greater than the given value
|
247
|
+
# :on - Scope the find to only include a certain context
|
248
|
+
def find_options_for_tag_counts(options = {})
|
249
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
250
|
+
|
251
|
+
# scope = scope(:find)
|
252
|
+
|
253
|
+
start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
254
|
+
end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
255
|
+
|
256
|
+
taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
|
257
|
+
taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
|
258
|
+
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
259
|
+
|
260
|
+
conditions = [
|
261
|
+
taggable_type,
|
262
|
+
taggable_id,
|
263
|
+
options[:conditions],
|
264
|
+
start_at,
|
265
|
+
end_at
|
266
|
+
]
|
267
|
+
|
268
|
+
conditions = conditions.compact.join(' AND ')
|
269
|
+
# conditions = merge_conditions(conditions, scope[:conditions]) if scope
|
270
|
+
|
271
|
+
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
272
|
+
joins << sanitize_sql(["AND #{Tag.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
273
|
+
joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
274
|
+
|
275
|
+
unless descends_from_active_record?
|
276
|
+
# Current model is STI descendant, so add type checking to the join condition
|
277
|
+
joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
278
|
+
end
|
279
|
+
|
280
|
+
# Based on a proposed patch by donV to ActiveRecord Base
|
281
|
+
# This is needed because merge_joins and construct_join are private in ActiveRecord Base
|
282
|
+
# if scope && scope[:joins]
|
283
|
+
# case scope[:joins]
|
284
|
+
# when Array
|
285
|
+
# scope_joins = scope[:joins].flatten
|
286
|
+
# strings = scope_joins.select{|j| j.is_a? String}
|
287
|
+
# joins << strings.join(' ') + " "
|
288
|
+
# symbols = scope_joins - strings
|
289
|
+
# join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, symbols, nil)
|
290
|
+
# joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
|
291
|
+
# joins.flatten!
|
292
|
+
# when Symbol, Hash
|
293
|
+
# join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, scope[:joins], nil)
|
294
|
+
# joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
|
295
|
+
# when String
|
296
|
+
# joins << scope[:joins]
|
297
|
+
# end
|
298
|
+
# end
|
299
|
+
|
300
|
+
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
301
|
+
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
302
|
+
having = [at_least, at_most].compact.join(' AND ')
|
303
|
+
if joins.include?(".context") # if :on is passed
|
304
|
+
group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
|
305
|
+
else
|
306
|
+
group_by = "tags.name HAVING COUNT(*) > 0"
|
307
|
+
end
|
308
|
+
group_by << " AND #{having}" unless having.blank?
|
309
|
+
|
310
|
+
{ :select => "#{Tag.table_name}.*, COUNT(*) AS count",
|
311
|
+
:joins => joins.join(" "),
|
312
|
+
:conditions => conditions,
|
313
|
+
:group => group_by,
|
314
|
+
:limit => options[:limit],
|
315
|
+
:order => options[:order]
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
def is_taggable?
|
320
|
+
true
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
module InstanceMethods
|
325
|
+
include ActiveRecord::Acts::Taxonomy::GroupHelper
|
326
|
+
|
327
|
+
def custom_contexts
|
328
|
+
@custom_contexts ||= []
|
329
|
+
end
|
330
|
+
|
331
|
+
def is_taggable?
|
332
|
+
self.class.is_taggable?
|
333
|
+
end
|
334
|
+
|
335
|
+
def add_custom_context(value)
|
336
|
+
custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) || self.class.tag_types.map(&:to_s).include?(value.to_s)
|
337
|
+
end
|
338
|
+
|
339
|
+
def tag_list_on(context, owner = nil)
|
340
|
+
context = context.to_s.singularize
|
341
|
+
add_custom_context(context)
|
342
|
+
cache = tag_list_cache_on(context)
|
343
|
+
return owner ? cache[owner] : cache[owner] if cache[owner]
|
344
|
+
|
345
|
+
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
|
346
|
+
cache[owner] = TagList.from(cached_tag_list_on(context))
|
347
|
+
else
|
348
|
+
cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def all_tags_list_on(context)
|
353
|
+
context = context.to_s.singularize
|
354
|
+
variable_name = "@all_#{context}_list"
|
355
|
+
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
356
|
+
instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
|
357
|
+
end
|
358
|
+
|
359
|
+
def all_tags_on(context)
|
360
|
+
context = context.to_s.singularize
|
361
|
+
opts = {:conditions => ["#{Tag.table_name}.context = ?", context.to_s]}
|
362
|
+
base_tags.find(:all, opts.merge(:order => "#{Tagging.table_name}.created_at"))
|
363
|
+
end
|
364
|
+
|
365
|
+
def tags_on(context, owner = nil)
|
366
|
+
context = context.to_s.singularize
|
367
|
+
if owner
|
368
|
+
base_tags.where("#{Tag.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
|
369
|
+
context.to_s, owner.id, owner.class.to_s)
|
370
|
+
else
|
371
|
+
base_tags.where("#{Tag.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def cached_tag_list_on(context)
|
376
|
+
self["cached_#{context.to_s.singularize}_list"]
|
377
|
+
end
|
378
|
+
|
379
|
+
def tag_list_cache_on(context)
|
380
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
381
|
+
cache = instance_variable_get(variable_name)
|
382
|
+
instance_variable_set(variable_name, cache = {}) unless cache
|
383
|
+
cache
|
384
|
+
end
|
385
|
+
|
386
|
+
def set_tag_list_on(context, new_list, tagger = nil)
|
387
|
+
context = context.to_s.singularize
|
388
|
+
tag_list_cache_on(context)[tagger] = TagList.from(new_list)
|
389
|
+
add_custom_context(context)
|
390
|
+
end
|
391
|
+
|
392
|
+
def tag_counts_on(context, options={})
|
393
|
+
self.class.tag_counts_on(context.to_s.singularize, options.merge(:id => id))
|
394
|
+
end
|
395
|
+
|
396
|
+
def related_tags_for(context, klass, options = {})
|
397
|
+
search_conditions = related_search_options(context.to_s.singularize, klass, options)
|
398
|
+
|
399
|
+
klass.find(:all, search_conditions)
|
400
|
+
end
|
401
|
+
|
402
|
+
def related_search_options(context, klass, options = {})
|
403
|
+
tags_to_find = tags_on(context.to_s.singularize).collect { |t| t.name }
|
404
|
+
|
405
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
406
|
+
|
407
|
+
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
408
|
+
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
409
|
+
:conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
|
410
|
+
:group => grouped_column_names_for(klass),
|
411
|
+
:order => "count DESC"
|
412
|
+
}.update(options)
|
413
|
+
end
|
414
|
+
|
415
|
+
def matching_contexts_for(search_context, result_context, klass, options = {})
|
416
|
+
search_conditions = matching_context_search_options(search_context, result_context, klass, options)
|
417
|
+
|
418
|
+
klass.find(:all, search_conditions)
|
419
|
+
end
|
420
|
+
|
421
|
+
def matching_context_search_options(search_context, result_context, klass, options = {})
|
422
|
+
search_context = search_context.to_s.singularize
|
423
|
+
result_context = result_context.to_s.singularize
|
424
|
+
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
425
|
+
|
426
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
427
|
+
|
428
|
+
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
429
|
+
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
430
|
+
:conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tag.table_name}.context = ?", tags_to_find, result_context],
|
431
|
+
:group => grouped_column_names_for(klass),
|
432
|
+
:order => "count DESC"
|
433
|
+
}.update(options)
|
434
|
+
end
|
435
|
+
|
436
|
+
def save_cached_tag_list
|
437
|
+
self.class.tag_types.map(&:to_s).each do |tag_type|
|
438
|
+
if self.class.send("caching_#{tag_type.singularize}_list?")
|
439
|
+
self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def save_tags
|
445
|
+
contexts = custom_contexts + self.class.tag_types.map(&:to_s)
|
446
|
+
transaction do
|
447
|
+
contexts.each do |context|
|
448
|
+
context = context.to_s.singularize
|
449
|
+
|
450
|
+
cache = tag_list_cache_on(context)
|
451
|
+
|
452
|
+
cache.each do |owner, list|
|
453
|
+
new_tags = Tag.find_or_create_all_with_like_by_name(context, list.uniq)
|
454
|
+
taggings = Tagging.joins(:tag).where(:taggable_id => self.id, :taggable_type => self.class.base_class.to_s, "tags.context" => context ).all
|
455
|
+
|
456
|
+
# Destroy old taggings:
|
457
|
+
if owner
|
458
|
+
old_tags = tags_on(context, owner) - new_tags
|
459
|
+
|
460
|
+
old_taggings = Tagging.joins(:tag).where(:taggable_id => self.id,
|
461
|
+
:taggable_type => self.class.base_class.to_s,
|
462
|
+
:tag_id => old_tags,
|
463
|
+
:tagger_id => owner.id,
|
464
|
+
:tagger_type => owner.class.to_s,
|
465
|
+
"tags.context" => context)
|
466
|
+
|
467
|
+
Tagging.destroy_all :id => old_taggings.map(&:id)
|
468
|
+
else
|
469
|
+
old_tags = tags_on(context) - new_tags
|
470
|
+
base_tags.delete(*old_tags)
|
471
|
+
end
|
472
|
+
|
473
|
+
new_tags.reject! { |tag| taggings.any? { |tagging|
|
474
|
+
tagging.tag_id == tag.id &&
|
475
|
+
tagging.tagger_id == (owner ? owner.id : nil) &&
|
476
|
+
tagging.tagger_type == (owner ? owner.class.to_s : nil) &&
|
477
|
+
tagging.tag.context == context
|
478
|
+
}
|
479
|
+
}
|
480
|
+
|
481
|
+
# create new taggings:
|
482
|
+
new_tags.each do |tag|
|
483
|
+
Tagging.create!(:tag_id => tag.id, :tagger => owner, :taggable => self)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
true
|
490
|
+
end
|
491
|
+
|
492
|
+
def reload_with_tag_list(*args)
|
493
|
+
self.class.tag_types.each do |tag_type|
|
494
|
+
instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
495
|
+
end
|
496
|
+
|
497
|
+
reload_without_tag_list(*args)
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|