acts-as-taggable-on 2.0.3 → 2.1.0
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/.gitignore +7 -0
- data/.travis.yml +10 -0
- data/Gemfile +2 -5
- data/Guardfile +5 -0
- data/README.rdoc +19 -16
- data/Rakefile +9 -55
- data/VERSION +1 -1
- data/acts-as-taggable-on.gemspec +27 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +8 -2
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +40 -31
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +67 -32
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +16 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +17 -9
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +16 -6
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +2 -2
- data/lib/acts_as_taggable_on/compatibility/Gemfile +3 -1
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
- data/lib/acts_as_taggable_on/tag.rb +73 -57
- data/lib/acts_as_taggable_on/tag_list.rb +79 -78
- data/lib/acts_as_taggable_on/tagging.rb +19 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -12
- data/lib/acts_as_taggable_on/utils.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +3 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +25 -2
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_list_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_spec.rb +41 -21
- data/spec/acts_as_taggable_on/taggable_spec.rb +54 -12
- data/spec/acts_as_taggable_on/tagger_spec.rb +5 -5
- data/spec/acts_as_taggable_on/tagging_spec.rb +7 -7
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -3
- data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
- data/spec/database.yml.sample +19 -0
- data/spec/models.rb +4 -0
- data/spec/schema.rb +6 -0
- data/spec/spec_helper.rb +60 -33
- data/uninstall.rb +1 -0
- metadata +130 -15
- /data/{spec/spec.opts → .rspec} +0 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/README.rdoc
CHANGED
|
@@ -38,21 +38,21 @@ After that, you can run "rake gems:install" to install the gem if you don't alre
|
|
|
38
38
|
1. script/generate acts_as_taggable_on_migration
|
|
39
39
|
2. rake db:migrate
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
=== Rails 3.0
|
|
42
42
|
|
|
43
43
|
Acts As Taggable On is now useable in Rails 3.0, thanks to the excellent work of Szymon Nowak
|
|
44
44
|
and Jelle Vandebeeck.
|
|
45
45
|
|
|
46
46
|
To use it, add it to your Gemfile:
|
|
47
47
|
|
|
48
|
-
gem 'acts-as-taggable-on'
|
|
48
|
+
gem 'acts-as-taggable-on'
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
==== Post Installation
|
|
51
51
|
|
|
52
52
|
1. rails generate acts_as_taggable_on:migration
|
|
53
53
|
2. rake db:migrate
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
== Testing
|
|
56
56
|
|
|
57
57
|
Acts As Taggable On uses RSpec for its test coverage. Inside the plugin
|
|
58
58
|
directory, you can run the specs for RoR 3.0.0 with:
|
|
@@ -68,7 +68,7 @@ If you already have RSpec on your application, the specs will run while using:
|
|
|
68
68
|
rake spec:plugins
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
== Usage
|
|
72
72
|
|
|
73
73
|
class User < ActiveRecord::Base
|
|
74
74
|
# Alias for <tt>acts_as_taggable_on :tags</tt>:
|
|
@@ -89,14 +89,14 @@ rake spec:plugins
|
|
|
89
89
|
User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
|
|
90
90
|
@frankie.skill_counts
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
=== Finding Tagged Objects
|
|
93
93
|
|
|
94
94
|
Acts As Taggable On utilizes named_scopes to create an association for tags.
|
|
95
95
|
This way you can mix and match to filter down your results, and it also improves
|
|
96
96
|
compatibility with the will_paginate gem:
|
|
97
97
|
|
|
98
98
|
class User < ActiveRecord::Base
|
|
99
|
-
acts_as_taggable_on :tags
|
|
99
|
+
acts_as_taggable_on :tags, :skills
|
|
100
100
|
named_scope :by_join_date, :order => "created_at DESC"
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -108,8 +108,11 @@ compatibility with the will_paginate gem:
|
|
|
108
108
|
|
|
109
109
|
# Find a user with any of the tags:
|
|
110
110
|
User.tagged_with(["awesome", "cool"], :any => true)
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
|
|
112
|
+
# Find a user with any of tags based on context:
|
|
113
|
+
User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
|
|
114
|
+
|
|
115
|
+
=== Relationships
|
|
113
116
|
|
|
114
117
|
You can find objects of the same type based on similar tags on certain contexts.
|
|
115
118
|
Also, objects will be returned in descending order based on the total number of
|
|
@@ -128,7 +131,7 @@ matched tags.
|
|
|
128
131
|
@bobby.find_related_skills # => [<User name="Tom">]
|
|
129
132
|
@frankie.find_related_skills # => [<User name="Tom">]
|
|
130
133
|
|
|
131
|
-
|
|
134
|
+
=== Dynamic Tag Contexts
|
|
132
135
|
|
|
133
136
|
In addition to the generated tag contexts in the definition, it is also possible
|
|
134
137
|
to allow for dynamic tag contexts (this could be user generated tag contexts!)
|
|
@@ -139,9 +142,9 @@ to allow for dynamic tag contexts (this could be user generated tag contexts!)
|
|
|
139
142
|
@user.save
|
|
140
143
|
@user.tags_on(:customs) # => [<Tag name='same'>,...]
|
|
141
144
|
@user.tag_counts_on(:customs)
|
|
142
|
-
User.
|
|
145
|
+
User.tagged_with("same", :on => :customs) # => [@user]
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
=== Tag Ownership
|
|
145
148
|
|
|
146
149
|
Tags can have owners:
|
|
147
150
|
|
|
@@ -158,7 +161,7 @@ Tags can have owners:
|
|
|
158
161
|
@some_user.owned_tags
|
|
159
162
|
@some_photo.locations_from(@some_user)
|
|
160
163
|
|
|
161
|
-
|
|
164
|
+
=== Tag cloud calculations
|
|
162
165
|
|
|
163
166
|
To construct tag clouds, the frequency of each tag needs to be calculated.
|
|
164
167
|
Because we specified +acts_as_taggable_on+ on the <tt>User</tt> class, we can
|
|
@@ -174,7 +177,7 @@ Here is an example that generates a tag cloud.
|
|
|
174
177
|
Helper:
|
|
175
178
|
|
|
176
179
|
module PostsHelper
|
|
177
|
-
include TagsHelper
|
|
180
|
+
include ActsAsTaggableOn::TagsHelper
|
|
178
181
|
end
|
|
179
182
|
|
|
180
183
|
Controller:
|
|
@@ -198,7 +201,7 @@ CSS:
|
|
|
198
201
|
.css3 { font-size: 1.4em; }
|
|
199
202
|
.css4 { font-size: 1.6em; }
|
|
200
203
|
|
|
201
|
-
|
|
204
|
+
== Contributors
|
|
202
205
|
|
|
203
206
|
* TomEric (i76) - Maintainer
|
|
204
207
|
* Michael Bleigh - Original Author
|
|
@@ -208,7 +211,7 @@ CSS:
|
|
|
208
211
|
* Pradeep Elankumaran - Taggers
|
|
209
212
|
* Sinclair Bain - Patch King
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
=== Patch Contributors
|
|
212
215
|
|
|
213
216
|
* tristanzdunn - Related objects of other classes
|
|
214
217
|
* azabaj - Fixed migrate down
|
data/Rakefile
CHANGED
|
@@ -1,59 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler'
|
|
3
|
+
Bundler.setup :default, :development
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Spec::Rake::SpecTask.new do |t|
|
|
8
|
-
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
9
|
-
end
|
|
5
|
+
desc 'Default: run specs'
|
|
6
|
+
task :default => :spec
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
t.rcov_opts = ['--exclude', 'spec']
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
rescue LoadError
|
|
18
|
-
# Rspec 2.0
|
|
19
|
-
require 'rspec/core/rake_task'
|
|
20
|
-
|
|
21
|
-
desc 'Default: run specs'
|
|
22
|
-
task :default => :spec
|
|
23
|
-
Rspec::Core::RakeTask.new do |t|
|
|
24
|
-
t.pattern = "spec/**/*_spec.rb"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
Rspec::Core::RakeTask.new('rcov') do |t|
|
|
28
|
-
t.pattern = "spec/**/*_spec.rb"
|
|
29
|
-
t.rcov = true
|
|
30
|
-
t.rcov_opts = ['--exclude', 'spec']
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
rescue LoadError
|
|
34
|
-
puts "Rspec not available. Install it with: gem install rspec"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
namespace 'rails2.3' do
|
|
38
|
-
task :spec do
|
|
39
|
-
gemfile = File.join(File.dirname(__FILE__), 'lib', 'acts_as_taggable_on', 'compatibility', 'Gemfile')
|
|
40
|
-
ENV['BUNDLE_GEMFILE'] = gemfile
|
|
41
|
-
Rake::Task['spec'].invoke
|
|
42
|
-
end
|
|
8
|
+
require 'rspec/core/rake_task'
|
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
|
10
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
43
11
|
end
|
|
44
12
|
|
|
45
|
-
|
|
46
|
-
require 'jeweler'
|
|
47
|
-
Jeweler::Tasks.new do |gemspec|
|
|
48
|
-
gemspec.name = "acts-as-taggable-on"
|
|
49
|
-
gemspec.summary = "ActsAsTaggableOn is a tagging plugin for Rails that provides multiple tagging contexts on a single model."
|
|
50
|
-
gemspec.description = "With ActsAsTaggableOn, you could tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality."
|
|
51
|
-
gemspec.email = "michael@intridea.com"
|
|
52
|
-
gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
|
|
53
|
-
gemspec.authors = ["Michael Bleigh"]
|
|
54
|
-
gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
|
|
55
|
-
end
|
|
56
|
-
Jeweler::GemcutterTasks.new
|
|
57
|
-
rescue LoadError
|
|
58
|
-
puts "Jeweler not available. Install it with: gem install jeweler"
|
|
59
|
-
end
|
|
13
|
+
Bundler::GemHelper.install_tasks
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.0.
|
|
1
|
+
2.0.6
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
$:.push File.dirname(__FILE__) + '/lib'
|
|
2
|
+
require 'acts-as-taggable-on/version'
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.name = %q{acts-as-taggable-on}
|
|
6
|
+
gem.authors = ["Michael Bleigh"]
|
|
7
|
+
gem.date = %q{2010-05-19}
|
|
8
|
+
gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
|
|
9
|
+
gem.summary = "Advanced tagging for Rails."
|
|
10
|
+
gem.email = %q{michael@intridea.com}
|
|
11
|
+
gem.homepage = ''
|
|
12
|
+
|
|
13
|
+
gem.add_runtime_dependency 'rails'
|
|
14
|
+
gem.add_development_dependency 'rspec', '~> 2.5'
|
|
15
|
+
gem.add_development_dependency 'sqlite3'
|
|
16
|
+
gem.add_development_dependency 'mysql2', '< 0.3'
|
|
17
|
+
gem.add_development_dependency 'pg'
|
|
18
|
+
gem.add_development_dependency 'guard'
|
|
19
|
+
gem.add_development_dependency 'guard-rspec'
|
|
20
|
+
|
|
21
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
22
|
+
gem.files = `git ls-files`.split("\n")
|
|
23
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
24
|
+
gem.name = "acts-as-taggable-on"
|
|
25
|
+
gem.require_paths = ['lib']
|
|
26
|
+
gem.version = ActsAsTaggableOn::VERSION
|
|
27
|
+
end
|
data/lib/acts-as-taggable-on.rb
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
require "active_record"
|
|
2
|
+
require "active_record/version"
|
|
2
3
|
require "action_view"
|
|
4
|
+
RAILS_3 = ::ActiveRecord::VERSION::MAJOR >= 3
|
|
3
5
|
|
|
4
6
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
5
7
|
|
|
6
|
-
require "acts_as_taggable_on/compatibility/active_record_backports"
|
|
8
|
+
require "acts_as_taggable_on/compatibility/active_record_backports" unless RAILS_3
|
|
9
|
+
|
|
10
|
+
require "acts_as_taggable_on/utils"
|
|
7
11
|
|
|
8
12
|
require "acts_as_taggable_on/acts_as_taggable_on"
|
|
9
13
|
require "acts_as_taggable_on/acts_as_taggable_on/core"
|
|
@@ -12,6 +16,7 @@ require "acts_as_taggable_on/acts_as_taggable_on/cache"
|
|
|
12
16
|
require "acts_as_taggable_on/acts_as_taggable_on/ownership"
|
|
13
17
|
require "acts_as_taggable_on/acts_as_taggable_on/related"
|
|
14
18
|
|
|
19
|
+
#require "acts_as_taggable_on/utils"
|
|
15
20
|
require "acts_as_taggable_on/acts_as_tagger"
|
|
16
21
|
require "acts_as_taggable_on/tag"
|
|
17
22
|
require "acts_as_taggable_on/tag_list"
|
|
@@ -20,11 +25,12 @@ require "acts_as_taggable_on/tagging"
|
|
|
20
25
|
|
|
21
26
|
$LOAD_PATH.shift
|
|
22
27
|
|
|
28
|
+
|
|
23
29
|
if defined?(ActiveRecord::Base)
|
|
24
30
|
ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
|
|
25
31
|
ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
if defined?(ActionView::Base)
|
|
29
|
-
ActionView::Base.send :include, TagsHelper
|
|
35
|
+
ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
|
|
30
36
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
2
|
module Cache
|
|
3
3
|
def self.included(base)
|
|
4
|
-
# Skip adding caching capabilities if no cache columns exist
|
|
5
|
-
return unless base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
|
|
4
|
+
# Skip adding caching capabilities if table not exists or no cache columns exist
|
|
5
|
+
return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
|
|
6
6
|
|
|
7
7
|
base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
|
|
8
8
|
base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
|
|
@@ -11,11 +11,11 @@ module ActsAsTaggableOn::Taggable
|
|
|
11
11
|
before_save :save_cached_tag_list
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
base.
|
|
14
|
+
base.initialize_acts_as_taggable_on_cache
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
module ClassMethods
|
|
18
|
-
def
|
|
18
|
+
def initialize_acts_as_taggable_on_cache
|
|
19
19
|
tag_types.map(&:to_s).each do |tag_type|
|
|
20
20
|
class_eval %(
|
|
21
21
|
def self.caching_#{tag_type.singularize}_list?
|
|
@@ -27,7 +27,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
27
27
|
|
|
28
28
|
def acts_as_taggable_on(*args)
|
|
29
29
|
super(*args)
|
|
30
|
-
|
|
30
|
+
initialize_acts_as_taggable_on_cache
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def caching_tag_list_on?(context)
|
|
@@ -50,4 +50,4 @@ module ActsAsTaggableOn::Taggable
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
-
end
|
|
53
|
+
end
|
|
@@ -61,63 +61,72 @@ module ActsAsTaggableOn::Taggable
|
|
|
61
61
|
|
|
62
62
|
## Generate conditions:
|
|
63
63
|
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
|
64
|
-
|
|
65
|
-
start_at_conditions = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
66
|
-
end_at_conditions = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
66
|
+
end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
67
|
+
|
|
68
|
+
taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
|
|
69
|
+
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
|
|
70
|
+
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
|
71
|
+
|
|
72
|
+
tagging_conditions = [
|
|
72
73
|
taggable_conditions,
|
|
73
|
-
options[:conditions],
|
|
74
74
|
scope[:conditions],
|
|
75
75
|
start_at_conditions,
|
|
76
76
|
end_at_conditions
|
|
77
77
|
].compact.reverse
|
|
78
78
|
|
|
79
|
+
tag_conditions = [
|
|
80
|
+
options[:conditions]
|
|
81
|
+
].compact.reverse
|
|
82
|
+
|
|
79
83
|
## Generate joins:
|
|
80
|
-
|
|
81
|
-
tagging_join << sanitize_sql([" AND #{Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
|
82
|
-
|
|
83
|
-
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
|
84
|
+
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
|
|
84
85
|
taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
tagging_join,
|
|
87
|
+
tagging_joins = [
|
|
88
88
|
taggable_join,
|
|
89
89
|
scope[:joins]
|
|
90
|
-
].compact
|
|
90
|
+
].compact
|
|
91
91
|
|
|
92
|
+
tag_joins = [
|
|
93
|
+
].compact
|
|
94
|
+
|
|
95
|
+
[tagging_joins, tag_joins].each(&:reverse!) if ActiveRecord::VERSION::MAJOR < 3
|
|
92
96
|
|
|
93
97
|
## Generate scope:
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
|
|
99
|
+
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
|
|
100
|
+
|
|
96
101
|
# Joins and conditions
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
|
|
103
|
+
tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
|
|
104
|
+
|
|
105
|
+
tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
|
|
106
|
+
tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
|
|
107
|
+
|
|
100
108
|
# GROUP BY and HAVING clauses:
|
|
101
|
-
at_least = sanitize_sql(['
|
|
102
|
-
at_most = sanitize_sql(['
|
|
103
|
-
having = [at_least, at_most].compact.join(' AND ')
|
|
109
|
+
at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
|
|
110
|
+
at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
|
|
111
|
+
having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
|
|
112
|
+
|
|
113
|
+
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
|
|
104
114
|
|
|
105
115
|
if ActiveRecord::VERSION::MAJOR >= 3
|
|
106
116
|
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
|
107
117
|
scoped_select = "#{table_name}.#{primary_key}"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
having = having.blank? ? "COUNT(*) > 0" : "COUNT(*) > 0 AND #{having}"
|
|
112
|
-
scope = scope.group(grouped_column_names_for(Tag)).having(having)
|
|
118
|
+
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
|
|
119
|
+
group(group_columns).
|
|
120
|
+
having(having)
|
|
113
121
|
else
|
|
114
122
|
# Having is not available in 2.3.x:
|
|
115
|
-
group_by = "#{
|
|
123
|
+
group_by = "#{group_columns} HAVING COUNT(*) > 0"
|
|
116
124
|
group_by << " AND #{having}" unless having.blank?
|
|
117
|
-
|
|
125
|
+
tagging_scope = tagging_scope.group(group_by)
|
|
118
126
|
end
|
|
119
127
|
|
|
120
|
-
|
|
128
|
+
tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
|
|
129
|
+
tag_scope
|
|
121
130
|
end
|
|
122
131
|
end
|
|
123
132
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
|
-
module Core
|
|
2
|
+
module Core
|
|
3
3
|
def self.included(base)
|
|
4
4
|
base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
|
|
5
5
|
base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
|
@@ -20,9 +20,9 @@ module ActsAsTaggableOn::Taggable
|
|
|
20
20
|
context_tags = tags_type.to_sym
|
|
21
21
|
|
|
22
22
|
class_eval do
|
|
23
|
-
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagging",
|
|
24
|
-
|
|
25
|
-
has_many context_tags, :through => context_taggings, :source => :tag
|
|
23
|
+
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
|
|
24
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
|
|
25
|
+
has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
class_eval %(
|
|
@@ -66,34 +66,54 @@ module ActsAsTaggableOn::Taggable
|
|
|
66
66
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
|
67
67
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
|
68
68
|
def tagged_with(tags, options = {})
|
|
69
|
-
tag_list = TagList.from(tags)
|
|
69
|
+
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
|
70
|
+
empty_result = scoped(:conditions => "1 = 0")
|
|
70
71
|
|
|
71
|
-
return
|
|
72
|
+
return empty_result if tag_list.empty?
|
|
72
73
|
|
|
73
74
|
joins = []
|
|
74
75
|
conditions = []
|
|
75
76
|
|
|
76
77
|
context = options.delete(:on)
|
|
78
|
+
alias_base_name = undecorated_table_name.gsub('.','_')
|
|
77
79
|
|
|
78
80
|
if options.delete(:exclude)
|
|
79
|
-
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name
|
|
80
|
-
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)})"
|
|
81
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
|
82
|
+
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
|
81
83
|
|
|
82
84
|
elsif options.delete(:any)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
# get tags, drop out if nothing returned (we need at least one)
|
|
86
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
87
|
+
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
|
88
|
+
|
|
89
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
|
90
|
+
# avoid ambiguous column name
|
|
91
|
+
taggings_context = context ? "_#{context}" : ''
|
|
92
|
+
|
|
93
|
+
#TODO: fix alias to be smaller
|
|
94
|
+
taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
|
|
95
|
+
|
|
96
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
97
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
98
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
99
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
100
|
+
|
|
101
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
|
102
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
|
103
|
+
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
|
104
|
+
|
|
105
|
+
joins << tagging_join
|
|
85
106
|
|
|
86
107
|
else
|
|
87
|
-
tags = Tag.named_any(tag_list)
|
|
88
|
-
return
|
|
108
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
109
|
+
return empty_result unless tags.length == tag_list.length
|
|
89
110
|
|
|
90
111
|
tags.each do |tag|
|
|
91
|
-
|
|
92
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
112
|
+
prefix = "#{tag.safe_name}_#{rand(1024)}"
|
|
93
113
|
|
|
94
|
-
taggings_alias = "#{
|
|
114
|
+
taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
|
|
95
115
|
|
|
96
|
-
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
116
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
97
117
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
98
118
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
|
99
119
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
|
@@ -103,20 +123,23 @@ module ActsAsTaggableOn::Taggable
|
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
|
|
106
|
-
taggings_alias, tags_alias = "#{
|
|
126
|
+
taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
|
|
107
127
|
|
|
108
128
|
if options.delete(:match_all)
|
|
109
|
-
joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
129
|
+
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
110
130
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
111
131
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
112
132
|
|
|
113
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
114
|
-
end
|
|
115
133
|
|
|
134
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
135
|
+
group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
136
|
+
end
|
|
116
137
|
|
|
117
|
-
scoped(:
|
|
138
|
+
scoped(:select => select_clause,
|
|
139
|
+
:joins => joins.join(" "),
|
|
118
140
|
:group => group,
|
|
119
141
|
:conditions => conditions.join(" AND "),
|
|
142
|
+
:order => options[:order],
|
|
120
143
|
:readonly => false)
|
|
121
144
|
end
|
|
122
145
|
|
|
@@ -154,7 +177,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
154
177
|
|
|
155
178
|
def tag_list_cache_on(context)
|
|
156
179
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
157
|
-
instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name)))
|
|
180
|
+
instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
|
158
181
|
end
|
|
159
182
|
|
|
160
183
|
def tag_list_on(context)
|
|
@@ -166,40 +189,52 @@ module ActsAsTaggableOn::Taggable
|
|
|
166
189
|
variable_name = "@all_#{context.to_s.singularize}_list"
|
|
167
190
|
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
|
168
191
|
|
|
169
|
-
instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
192
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
170
193
|
end
|
|
171
194
|
|
|
172
195
|
##
|
|
173
196
|
# Returns all tags of a given context
|
|
174
197
|
def all_tags_on(context)
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
|
199
|
+
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
|
200
|
+
|
|
201
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
|
202
|
+
scope = base_tags.where(opts)
|
|
203
|
+
|
|
204
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
|
205
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
|
206
|
+
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
|
207
|
+
else
|
|
208
|
+
scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
scope.all
|
|
177
212
|
end
|
|
178
213
|
|
|
179
214
|
##
|
|
180
215
|
# Returns all tags that are not owned of a given context
|
|
181
216
|
def tags_on(context)
|
|
182
|
-
base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
|
|
217
|
+
base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
|
|
183
218
|
end
|
|
184
219
|
|
|
185
220
|
def set_tag_list_on(context, new_list)
|
|
186
221
|
add_custom_context(context)
|
|
187
222
|
|
|
188
223
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
189
|
-
instance_variable_set(variable_name, TagList.from(new_list))
|
|
224
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
|
190
225
|
end
|
|
191
226
|
|
|
192
227
|
def tagging_contexts
|
|
193
228
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
|
194
229
|
end
|
|
195
230
|
|
|
196
|
-
def reload
|
|
231
|
+
def reload(*args)
|
|
197
232
|
self.class.tag_types.each do |context|
|
|
198
233
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
|
199
234
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
|
200
235
|
end
|
|
201
236
|
|
|
202
|
-
super
|
|
237
|
+
super(*args)
|
|
203
238
|
end
|
|
204
239
|
|
|
205
240
|
def save_tags
|
|
@@ -209,7 +244,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
209
244
|
tag_list = tag_list_cache_on(context).uniq
|
|
210
245
|
|
|
211
246
|
# Find existing tags or create non-existing tags:
|
|
212
|
-
tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
247
|
+
tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
213
248
|
|
|
214
249
|
current_tags = tags_on(context)
|
|
215
250
|
old_tags = current_tags - tag_list
|
|
@@ -221,7 +256,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
221
256
|
|
|
222
257
|
if old_taggings.present?
|
|
223
258
|
# Destroy old taggings:
|
|
224
|
-
Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
259
|
+
ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
225
260
|
end
|
|
226
261
|
|
|
227
262
|
# Create new taggings:
|
|
@@ -234,4 +269,4 @@ module ActsAsTaggableOn::Taggable
|
|
|
234
269
|
end
|
|
235
270
|
end
|
|
236
271
|
end
|
|
237
|
-
end
|
|
272
|
+
end
|