acts-as-taggable-on 2.0.6 → 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/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +2 -9
- data/Guardfile +5 -0
- data/README.rdoc +5 -2
- data/Rakefile +9 -55
- data/acts-as-taggable-on.gemspec +27 -0
- data/lib/acts-as-taggable-on.rb +7 -1
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +14 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +3 -3
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +35 -28
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +48 -17
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +7 -3
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +11 -3
- data/lib/acts_as_taggable_on/compatibility/Gemfile +1 -1
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
- data/lib/acts_as_taggable_on/tag.rb +19 -12
- data/lib/acts_as_taggable_on/utils.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +22 -1
- data/spec/acts_as_taggable_on/tag_spec.rb +20 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +32 -0
- data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
- data/spec/database.yml.sample +4 -2
- data/spec/models.rb +4 -0
- data/spec/schema.rb +6 -0
- data/spec/spec_helper.rb +26 -6
- data/uninstall.rb +1 -0
- metadata +129 -16
- data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
- data/spec/database.yml +0 -17
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/README.rdoc
CHANGED
@@ -96,7 +96,7 @@ 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,7 +108,10 @@ 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
|
-
|
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
|
+
|
112
115
|
=== Relationships
|
113
116
|
|
114
117
|
You can find objects of the same type based on similar tags on certain contexts.
|
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
|
@@ -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,6 +25,7 @@ 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
|
@@ -28,10 +28,19 @@ module ActsAsTaggableOn
|
|
28
28
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
29
29
|
|
30
30
|
if taggable?
|
31
|
-
|
31
|
+
if RAILS_3
|
32
|
+
self.tag_types = (self.tag_types + tag_types).uniq
|
33
|
+
else
|
34
|
+
write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
|
35
|
+
end
|
32
36
|
else
|
33
|
-
|
34
|
-
|
37
|
+
if RAILS_3
|
38
|
+
class_attribute :tag_types
|
39
|
+
self.tag_types = tag_types
|
40
|
+
else
|
41
|
+
write_inheritable_attribute(:tag_types, tag_types)
|
42
|
+
class_inheritable_reader(:tag_types)
|
43
|
+
end
|
35
44
|
|
36
45
|
class_eval do
|
37
46
|
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
|
@@ -40,7 +49,8 @@ module ActsAsTaggableOn
|
|
40
49
|
def self.taggable?
|
41
50
|
true
|
42
51
|
end
|
43
|
-
|
52
|
+
|
53
|
+
include ActsAsTaggableOn::Utils
|
44
54
|
include ActsAsTaggableOn::Taggable::Core
|
45
55
|
include ActsAsTaggableOn::Taggable::Collection
|
46
56
|
include ActsAsTaggableOn::Taggable::Cache
|
@@ -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)
|
@@ -61,65 +61,72 @@ module ActsAsTaggableOn::Taggable
|
|
61
61
|
|
62
62
|
## Generate conditions:
|
63
63
|
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
64
|
-
|
64
|
+
|
65
65
|
start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
66
66
|
end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
67
|
-
|
67
|
+
|
68
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)])
|
70
|
-
|
71
|
-
|
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
|
-
tagging_join = "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tag.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.tag_id"
|
81
|
-
tagging_join << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
82
|
-
|
83
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
90
|
].compact
|
91
91
|
|
92
|
-
|
92
|
+
tag_joins = [
|
93
|
+
].compact
|
93
94
|
|
95
|
+
[tagging_joins, tag_joins].each(&:reverse!) if ActiveRecord::VERSION::MAJOR < 3
|
94
96
|
|
95
97
|
## Generate scope:
|
96
|
-
|
97
|
-
|
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
|
+
|
98
101
|
# Joins and conditions
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
+
|
102
108
|
# GROUP BY and HAVING clauses:
|
103
|
-
at_least = sanitize_sql(['
|
104
|
-
at_most = sanitize_sql(['
|
105
|
-
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"
|
106
114
|
|
107
115
|
if ActiveRecord::VERSION::MAJOR >= 3
|
108
116
|
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
109
117
|
scoped_select = "#{table_name}.#{primary_key}"
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
having = having.blank? ? "COUNT(*) > 0" : "COUNT(*) > 0 AND #{having}"
|
114
|
-
scope = scope.group(grouped_column_names_for(ActsAsTaggableOn::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)
|
115
121
|
else
|
116
122
|
# Having is not available in 2.3.x:
|
117
|
-
group_by = "#{
|
123
|
+
group_by = "#{group_columns} HAVING COUNT(*) > 0"
|
118
124
|
group_by << " AND #{having}" unless having.blank?
|
119
|
-
|
125
|
+
tagging_scope = tagging_scope.group(group_by)
|
120
126
|
end
|
121
127
|
|
122
|
-
|
128
|
+
tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
|
129
|
+
tag_scope
|
123
130
|
end
|
124
131
|
end
|
125
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
|
@@ -21,7 +21,7 @@ module ActsAsTaggableOn::Taggable
|
|
21
21
|
|
22
22
|
class_eval do
|
23
23
|
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
|
24
|
-
|
24
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
|
25
25
|
has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
26
26
|
end
|
27
27
|
|
@@ -67,31 +67,51 @@ module ActsAsTaggableOn::Taggable
|
|
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
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(["#{ActsAsTaggableOn::Tag.table_name}.name
|
81
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
80
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
108
|
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
88
|
-
return
|
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
116
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
97
117
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
@@ -103,18 +123,20 @@ 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
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 "),
|
120
142
|
:order => options[:order],
|
@@ -176,8 +198,17 @@ module ActsAsTaggableOn::Taggable
|
|
176
198
|
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
177
199
|
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
178
200
|
|
179
|
-
opts
|
180
|
-
base_tags.where(opts)
|
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
|
181
212
|
end
|
182
213
|
|
183
214
|
##
|
@@ -238,4 +269,4 @@ module ActsAsTaggableOn::Taggable
|
|
238
269
|
end
|
239
270
|
end
|
240
271
|
end
|
241
|
-
end
|
272
|
+
end
|
@@ -30,9 +30,13 @@ module ActsAsTaggableOn::Taggable
|
|
30
30
|
|
31
31
|
module InstanceMethods
|
32
32
|
def owner_tags_on(owner, context)
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
if owner.nil?
|
34
|
+
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
|
35
|
+
else
|
36
|
+
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
37
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
38
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
|
39
|
+
end
|
36
40
|
end
|
37
41
|
|
38
42
|
def cached_owned_tag_list_on(context)
|
@@ -18,7 +18,11 @@ module ActsAsTaggableOn::Taggable
|
|
18
18
|
def find_related_#{tag_type}_for(klass, options = {})
|
19
19
|
related_tags_for('#{tag_type}', klass, options)
|
20
20
|
end
|
21
|
-
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
unless tag_types.empty?
|
25
|
+
class_eval %(
|
22
26
|
def find_matching_contexts(search_context, result_context, options = {})
|
23
27
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
24
28
|
end
|
@@ -42,10 +46,12 @@ module ActsAsTaggableOn::Taggable
|
|
42
46
|
|
43
47
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
44
48
|
|
49
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
50
|
+
|
45
51
|
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
|
46
52
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
47
53
|
:conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
|
48
|
-
:group =>
|
54
|
+
:group => group_columns,
|
49
55
|
:order => "count DESC" }.update(options))
|
50
56
|
end
|
51
57
|
|
@@ -54,10 +60,12 @@ module ActsAsTaggableOn::Taggable
|
|
54
60
|
|
55
61
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
56
62
|
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
64
|
+
|
57
65
|
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
|
58
66
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
59
67
|
:conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
|
60
|
-
:group =>
|
68
|
+
:group => group_columns,
|
61
69
|
:order => "count DESC" }.update(options))
|
62
70
|
end
|
63
71
|
end
|
@@ -9,7 +9,11 @@ module ActsAsTaggableOn
|
|
9
9
|
named_scope :order, lambda { |order| { :order => order } }
|
10
10
|
named_scope :select, lambda { |select| { :select => select } }
|
11
11
|
named_scope :limit, lambda { |limit| { :limit => limit } }
|
12
|
-
named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
|
12
|
+
named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
|
13
|
+
|
14
|
+
def self.to_sql
|
15
|
+
construct_finder_sql({})
|
16
|
+
end
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module ActsAsTaggableOn
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
3
|
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
4
|
-
|
4
|
+
include ActsAsTaggableOn::Utils
|
5
|
+
|
5
6
|
attr_accessible :name
|
6
7
|
|
7
8
|
### ASSOCIATIONS:
|
@@ -14,21 +15,21 @@ module ActsAsTaggableOn
|
|
14
15
|
validates_uniqueness_of :name
|
15
16
|
|
16
17
|
### SCOPES:
|
17
|
-
|
18
|
+
|
18
19
|
def self.named(name)
|
19
|
-
where(["name #{like_operator} ?", name])
|
20
|
+
where(["name #{like_operator} ?", escape_like(name)])
|
20
21
|
end
|
21
22
|
|
22
23
|
def self.named_any(list)
|
23
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", tag.to_s]) }.join(" OR "))
|
24
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", escape_like(tag.to_s)]) }.join(" OR "))
|
24
25
|
end
|
25
26
|
|
26
27
|
def self.named_like(name)
|
27
|
-
where(["name #{like_operator} ?", "%#{name}%"])
|
28
|
+
where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.named_like_any(list)
|
31
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
32
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
32
33
|
end
|
33
34
|
|
34
35
|
### CLASS METHODS:
|
@@ -43,7 +44,10 @@ module ActsAsTaggableOn
|
|
43
44
|
return [] if list.empty?
|
44
45
|
|
45
46
|
existing_tags = Tag.named_any(list).all
|
46
|
-
new_tag_names = list.reject
|
47
|
+
new_tag_names = list.reject do |name|
|
48
|
+
name = comparable_name(name)
|
49
|
+
existing_tags.any? { |tag| comparable_name(tag.name) == name }
|
50
|
+
end
|
47
51
|
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
48
52
|
|
49
53
|
existing_tags + created_tags
|
@@ -62,13 +66,16 @@ module ActsAsTaggableOn
|
|
62
66
|
def count
|
63
67
|
read_attribute(:count).to_i
|
64
68
|
end
|
65
|
-
|
69
|
+
|
70
|
+
def safe_name
|
71
|
+
name.gsub(/[^a-zA-Z0-9]/, '')
|
72
|
+
end
|
73
|
+
|
66
74
|
class << self
|
67
|
-
private
|
68
|
-
def
|
69
|
-
|
75
|
+
private
|
76
|
+
def comparable_name(str)
|
77
|
+
RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
|
70
78
|
end
|
71
79
|
end
|
72
|
-
|
73
80
|
end
|
74
81
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActsAsTaggableOn
|
2
|
+
module Utils
|
3
|
+
def self.included(base)
|
4
|
+
|
5
|
+
base.send :include, ActsAsTaggableOn::Utils::OverallMethods
|
6
|
+
base.extend ActsAsTaggableOn::Utils::OverallMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module OverallMethods
|
10
|
+
def using_postgresql?
|
11
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
12
|
+
end
|
13
|
+
|
14
|
+
def using_sqlite?
|
15
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def like_operator
|
20
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
21
|
+
end
|
22
|
+
|
23
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
24
|
+
def escape_like(str)
|
25
|
+
return str if using_sqlite? # skip escaping for SQLite
|
26
|
+
str.to_s.gsub("_", "\\\_").gsub("%", "\\\%")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -12,7 +12,7 @@ describe "Acts As Taggable On" do
|
|
12
12
|
describe "Taggable Method Generation" do
|
13
13
|
before(:each) do
|
14
14
|
clean_database!
|
15
|
-
TaggableModel.write_inheritable_attribute(:tag_types, [])
|
15
|
+
RAILS_3 ? TaggableModel.tag_types = [] : TaggableModel.write_inheritable_attribute(:tag_types, [])
|
16
16
|
TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
|
17
17
|
@taggable = TaggableModel.new(:name => "Bob Jones")
|
18
18
|
end
|
@@ -212,24 +212,36 @@ describe "Acts As Taggable On" do
|
|
212
212
|
describe 'Caching' do
|
213
213
|
before(:each) do
|
214
214
|
@taggable = CachedModel.new(:name => "Bob Jones")
|
215
|
+
@another_taggable = OtherCachedModel.new(:name => "John Smith")
|
215
216
|
end
|
216
217
|
|
217
218
|
it "should add saving of tag lists and cached tag lists to the instance" do
|
218
219
|
@taggable.should respond_to(:save_cached_tag_list)
|
220
|
+
@another_taggable.should respond_to(:save_cached_tag_list)
|
221
|
+
|
219
222
|
@taggable.should respond_to(:save_tags)
|
220
223
|
end
|
221
224
|
|
225
|
+
it "should add cached tag lists to the instance if cached column is not present" do
|
226
|
+
TaggableModel.new(:name => "Art Kram").should_not respond_to(:save_cached_tag_list)
|
227
|
+
end
|
228
|
+
|
222
229
|
it "should generate a cached column checker for each tag type" do
|
223
230
|
CachedModel.should respond_to(:caching_tag_list?)
|
231
|
+
OtherCachedModel.should respond_to(:caching_language_list?)
|
224
232
|
end
|
225
233
|
|
226
234
|
it 'should not have cached tags' do
|
227
235
|
@taggable.cached_tag_list.should be_blank
|
236
|
+
@another_taggable.cached_language_list.should be_blank
|
228
237
|
end
|
229
238
|
|
230
239
|
it 'should cache tags' do
|
231
240
|
@taggable.update_attributes(:tag_list => 'awesome, epic')
|
232
241
|
@taggable.cached_tag_list.should == 'awesome, epic'
|
242
|
+
|
243
|
+
@another_taggable.update_attributes(:language_list => 'ruby, .net')
|
244
|
+
@another_taggable.cached_language_list.should == 'ruby, .net'
|
233
245
|
end
|
234
246
|
|
235
247
|
it 'should keep the cache' do
|
@@ -265,4 +277,13 @@ describe "Acts As Taggable On" do
|
|
265
277
|
end
|
266
278
|
end
|
267
279
|
|
280
|
+
describe "taggings" do
|
281
|
+
before(:each) do
|
282
|
+
@taggable = TaggableModel.new(:name => "Art Kram")
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'should return [] taggings' do
|
286
|
+
@taggable.taggings.should == []
|
287
|
+
end
|
288
|
+
end
|
268
289
|
end
|
@@ -112,4 +112,24 @@ describe ActsAsTaggableOn::Tag do
|
|
112
112
|
@another_tag = ActsAsTaggableOn::Tag.create!(:name => "coolip")
|
113
113
|
ActsAsTaggableOn::Tag.named_like('cool').should include(@tag, @another_tag)
|
114
114
|
end
|
115
|
+
|
116
|
+
describe "escape wildcard symbols in like requests" do
|
117
|
+
before(:each) do
|
118
|
+
@tag.name = "cool"
|
119
|
+
@tag.save
|
120
|
+
@another_tag = ActsAsTaggableOn::Tag.create!(:name => "coo%")
|
121
|
+
@another_tag2 = ActsAsTaggableOn::Tag.create!(:name => "coolish")
|
122
|
+
end
|
123
|
+
|
124
|
+
it "return escaped result when '%' char present in tag" do
|
125
|
+
if @tag.using_sqlite?
|
126
|
+
ActsAsTaggableOn::Tag.named_like('coo%').should include(@tag)
|
127
|
+
ActsAsTaggableOn::Tag.named_like('coo%').should include(@another_tag)
|
128
|
+
else
|
129
|
+
ActsAsTaggableOn::Tag.named_like('coo%').should_not include(@tag)
|
130
|
+
ActsAsTaggableOn::Tag.named_like('coo%').should include(@another_tag)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
115
135
|
end
|
@@ -4,6 +4,7 @@ describe "Taggable" do
|
|
4
4
|
before(:each) do
|
5
5
|
clean_database!
|
6
6
|
@taggable = TaggableModel.new(:name => "Bob Jones")
|
7
|
+
@taggables = [@taggable, TaggableModel.new(:name => "John Doe")]
|
7
8
|
end
|
8
9
|
|
9
10
|
it "should have tag types" do
|
@@ -68,6 +69,22 @@ describe "Taggable" do
|
|
68
69
|
@taggable.should have(2).skills
|
69
70
|
end
|
70
71
|
|
72
|
+
it "should be able to select taggables by subset of tags using ActiveRelation methods" do
|
73
|
+
@taggables[0].tag_list = "bob"
|
74
|
+
@taggables[1].tag_list = "charlie"
|
75
|
+
@taggables[0].skill_list = "ruby"
|
76
|
+
@taggables[1].skill_list = "css"
|
77
|
+
@taggables.each{|taggable| taggable.save}
|
78
|
+
|
79
|
+
@found_taggables_by_tag = TaggableModel.joins(:tags).where(:tags => {:name => ["bob"]})
|
80
|
+
@found_taggables_by_skill = TaggableModel.joins(:skills).where(:tags => {:name => ["ruby"]})
|
81
|
+
|
82
|
+
@found_taggables_by_tag.should include @taggables[0]
|
83
|
+
@found_taggables_by_tag.should_not include @taggables[1]
|
84
|
+
@found_taggables_by_skill.should include @taggables[0]
|
85
|
+
@found_taggables_by_skill.should_not include @taggables[1]
|
86
|
+
end
|
87
|
+
|
71
88
|
it "should be able to find by tag" do
|
72
89
|
@taggable.skill_list = "ruby, rails, css"
|
73
90
|
@taggable.save
|
@@ -111,6 +128,15 @@ describe "Taggable" do
|
|
111
128
|
TaggableModel.all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
|
112
129
|
end
|
113
130
|
|
131
|
+
it "should be able to use named scopes to chain tag finds by any tags by context" do
|
132
|
+
bob = TaggableModel.create(:name => "Bob", :need_list => "rails", :offering_list => "c++")
|
133
|
+
frank = TaggableModel.create(:name => "Frank", :need_list => "css", :offering_list => "css")
|
134
|
+
steve = TaggableModel.create(:name => 'Steve', :need_list => "c++", :offering_list => "java")
|
135
|
+
|
136
|
+
# Let's only find those who need rails or css and are offering c++ or java
|
137
|
+
TaggableModel.tagged_with(['rails, css'], :on => :needs, :any => true).tagged_with(['c++', 'java'], :on => :offerings, :any => true).to_a.should == [bob]
|
138
|
+
end
|
139
|
+
|
114
140
|
if ActiveRecord::VERSION::MAJOR >= 3
|
115
141
|
it "should not return read-only records" do
|
116
142
|
TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
@@ -234,6 +260,12 @@ describe "Taggable" do
|
|
234
260
|
|
235
261
|
TaggableModel.tagged_with("lazy", :exclude => true).to_a.should == [frank, steve]
|
236
262
|
end
|
263
|
+
|
264
|
+
it "should return an empty scope for empty tags" do
|
265
|
+
TaggableModel.tagged_with('').should == []
|
266
|
+
TaggableModel.tagged_with(' ').should == []
|
267
|
+
TaggableModel.tagged_with(nil).should == []
|
268
|
+
end
|
237
269
|
|
238
270
|
it "should not create duplicate taggings" do
|
239
271
|
bob = TaggableModel.create(:name => "Bob")
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe ActsAsTaggableOn::Utils do
|
4
|
+
describe "like_operator" do
|
5
|
+
before(:each) do
|
6
|
+
clean_database!
|
7
|
+
TaggableModel.write_inheritable_attribute(:tag_types, [])
|
8
|
+
TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
|
9
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should return 'ILIKE' when the adapter is PostgreSQL" do
|
13
|
+
TaggableModel.connection.stub(:adapter_name).and_return("PostgreSQL")
|
14
|
+
TaggableModel.send(:like_operator).should == "ILIKE"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return 'LIKE' when the adapter is not PostgreSQL" do
|
18
|
+
TaggableModel.connection.stub(:adapter_name).and_return("MySQL")
|
19
|
+
TaggableModel.send(:like_operator).should == "LIKE"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/database.yml.sample
CHANGED
@@ -3,15 +3,17 @@ sqlite3:
|
|
3
3
|
database: acts_as_taggable_on.sqlite3
|
4
4
|
|
5
5
|
mysql:
|
6
|
-
adapter:
|
6
|
+
adapter: mysql2
|
7
7
|
hostname: localhost
|
8
8
|
username: root
|
9
9
|
password:
|
10
10
|
database: acts_as_taggable_on
|
11
|
-
|
11
|
+
charset: utf8
|
12
|
+
|
12
13
|
postgresql:
|
13
14
|
adapter: postgresql
|
14
15
|
hostname: localhost
|
15
16
|
username: postgres
|
16
17
|
password:
|
17
18
|
database: acts_as_taggable_on
|
19
|
+
encoding: utf8
|
data/spec/models.rb
CHANGED
@@ -10,6 +10,10 @@ class CachedModel < ActiveRecord::Base
|
|
10
10
|
acts_as_taggable
|
11
11
|
end
|
12
12
|
|
13
|
+
class OtherCachedModel < ActiveRecord::Base
|
14
|
+
acts_as_taggable_on :languages
|
15
|
+
end
|
16
|
+
|
13
17
|
class OtherTaggableModel < ActiveRecord::Base
|
14
18
|
acts_as_taggable_on :tags, :languages
|
15
19
|
acts_as_taggable_on :needs, :offerings
|
data/spec/schema.rb
CHANGED
@@ -32,6 +32,12 @@ ActiveRecord::Schema.define :version => 0 do
|
|
32
32
|
t.column :cached_tag_list, :string
|
33
33
|
end
|
34
34
|
|
35
|
+
create_table :other_cached_models, :force => true do |t|
|
36
|
+
t.column :name, :string
|
37
|
+
t.column :type, :string
|
38
|
+
t.column :cached_language_list, :string
|
39
|
+
end
|
40
|
+
|
35
41
|
create_table :taggable_users, :force => true do |t|
|
36
42
|
t.column :name, :string
|
37
43
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
$LOAD_PATH << "." unless $LOAD_PATH.include?(".")
|
2
|
+
require 'logger'
|
2
3
|
|
3
4
|
begin
|
4
5
|
require "rubygems"
|
@@ -13,7 +14,7 @@ begin
|
|
13
14
|
Bundler.setup
|
14
15
|
rescue Bundler::GemNotFound
|
15
16
|
raise RuntimeError, "Bundler couldn't find some gems." +
|
16
|
-
"Did you run
|
17
|
+
"Did you run \`bundlee install\`?"
|
17
18
|
end
|
18
19
|
|
19
20
|
Bundler.require
|
@@ -29,14 +30,33 @@ unless [].respond_to?(:freq)
|
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
ENV['DB']
|
33
|
-
|
33
|
+
db_name = ENV['DB'] || 'sqlite3'
|
34
34
|
database_yml = File.expand_path('../database.yml', __FILE__)
|
35
|
+
|
35
36
|
if File.exists?(database_yml)
|
36
|
-
active_record_configuration = YAML.load_file(database_yml)
|
37
|
+
active_record_configuration = YAML.load_file(database_yml)
|
37
38
|
|
38
|
-
ActiveRecord::Base.
|
39
|
+
ActiveRecord::Base.configurations = active_record_configuration
|
40
|
+
config = ActiveRecord::Base.configurations[db_name]
|
41
|
+
|
42
|
+
begin
|
43
|
+
ActiveRecord::Base.establish_connection(db_name)
|
44
|
+
ActiveRecord::Base.connection
|
45
|
+
rescue
|
46
|
+
case db_name
|
47
|
+
when /mysql/
|
48
|
+
ActiveRecord::Base.establish_connection(config.merge('database' => nil))
|
49
|
+
ActiveRecord::Base.connection.create_database(config['database'], {:charset => 'utf8', :collation => 'utf8_unicode_ci'})
|
50
|
+
when 'postgresql'
|
51
|
+
ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
|
52
|
+
ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
|
53
|
+
end
|
54
|
+
|
55
|
+
ActiveRecord::Base.establish_connection(config)
|
56
|
+
end
|
57
|
+
|
39
58
|
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
59
|
+
ActiveRecord::Base.default_timezone = :utc
|
40
60
|
|
41
61
|
ActiveRecord::Base.silence do
|
42
62
|
ActiveRecord::Migration.verbose = false
|
@@ -57,4 +77,4 @@ def clean_database!
|
|
57
77
|
end
|
58
78
|
end
|
59
79
|
|
60
|
-
clean_database!
|
80
|
+
clean_database!
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts-as-taggable-on
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 11
|
5
|
+
prerelease:
|
5
6
|
segments:
|
6
7
|
- 2
|
8
|
+
- 1
|
7
9
|
- 0
|
8
|
-
|
9
|
-
version: 2.0.6
|
10
|
+
version: 2.1.0
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Michael Bleigh
|
@@ -14,28 +15,133 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2010-05-19 00:00:00 +
|
18
|
+
date: 2010-05-19 00:00:00 +03:00
|
18
19
|
default_executable:
|
19
|
-
dependencies:
|
20
|
-
|
21
|
-
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 3
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
prerelease: false
|
32
|
+
name: rails
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
hash: 9
|
42
|
+
segments:
|
43
|
+
- 2
|
44
|
+
- 5
|
45
|
+
version: "2.5"
|
46
|
+
prerelease: false
|
47
|
+
name: rspec
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
prerelease: false
|
61
|
+
name: sqlite3
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - <
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 13
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
- 3
|
74
|
+
version: "0.3"
|
75
|
+
prerelease: false
|
76
|
+
name: mysql2
|
77
|
+
type: :development
|
78
|
+
version_requirements: *id004
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
prerelease: false
|
90
|
+
name: pg
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id005
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
prerelease: false
|
104
|
+
name: guard
|
105
|
+
type: :development
|
106
|
+
version_requirements: *id006
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
prerelease: false
|
118
|
+
name: guard-rspec
|
119
|
+
type: :development
|
120
|
+
version_requirements: *id007
|
121
|
+
description: With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.
|
22
122
|
email: michael@intridea.com
|
23
123
|
executables: []
|
24
124
|
|
25
125
|
extensions: []
|
26
126
|
|
27
|
-
extra_rdoc_files:
|
28
|
-
|
127
|
+
extra_rdoc_files: []
|
128
|
+
|
29
129
|
files:
|
130
|
+
- .gitignore
|
131
|
+
- .rspec
|
132
|
+
- .travis.yml
|
30
133
|
- CHANGELOG
|
31
134
|
- Gemfile
|
135
|
+
- Guardfile
|
32
136
|
- MIT-LICENSE
|
33
137
|
- README.rdoc
|
34
138
|
- Rakefile
|
35
139
|
- VERSION
|
140
|
+
- acts-as-taggable-on.gemspec
|
36
141
|
- generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
|
37
142
|
- generators/acts_as_taggable_on_migration/templates/migration.rb
|
38
143
|
- lib/acts-as-taggable-on.rb
|
144
|
+
- lib/acts-as-taggable-on/version.rb
|
39
145
|
- lib/acts_as_taggable_on/acts_as_taggable_on.rb
|
40
146
|
- lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb
|
41
147
|
- lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb
|
@@ -45,11 +151,11 @@ files:
|
|
45
151
|
- lib/acts_as_taggable_on/acts_as_tagger.rb
|
46
152
|
- lib/acts_as_taggable_on/compatibility/Gemfile
|
47
153
|
- lib/acts_as_taggable_on/compatibility/active_record_backports.rb
|
48
|
-
- lib/acts_as_taggable_on/compatibility/postgresql.rb
|
49
154
|
- lib/acts_as_taggable_on/tag.rb
|
50
155
|
- lib/acts_as_taggable_on/tag_list.rb
|
51
156
|
- lib/acts_as_taggable_on/tagging.rb
|
52
157
|
- lib/acts_as_taggable_on/tags_helper.rb
|
158
|
+
- lib/acts_as_taggable_on/utils.rb
|
53
159
|
- lib/generators/acts_as_taggable_on/migration/migration_generator.rb
|
54
160
|
- lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb
|
55
161
|
- rails/init.rb
|
@@ -61,42 +167,47 @@ files:
|
|
61
167
|
- spec/acts_as_taggable_on/tagger_spec.rb
|
62
168
|
- spec/acts_as_taggable_on/tagging_spec.rb
|
63
169
|
- spec/acts_as_taggable_on/tags_helper_spec.rb
|
170
|
+
- spec/acts_as_taggable_on/utils_spec.rb
|
64
171
|
- spec/bm.rb
|
65
|
-
- spec/database.yml
|
66
172
|
- spec/database.yml.sample
|
67
173
|
- spec/models.rb
|
68
174
|
- spec/schema.rb
|
69
175
|
- spec/spec_helper.rb
|
176
|
+
- uninstall.rb
|
70
177
|
has_rdoc: true
|
71
|
-
homepage:
|
178
|
+
homepage: ""
|
72
179
|
licenses: []
|
73
180
|
|
74
181
|
post_install_message:
|
75
|
-
rdoc_options:
|
76
|
-
|
182
|
+
rdoc_options: []
|
183
|
+
|
77
184
|
require_paths:
|
78
185
|
- lib
|
79
186
|
required_ruby_version: !ruby/object:Gem::Requirement
|
187
|
+
none: false
|
80
188
|
requirements:
|
81
189
|
- - ">="
|
82
190
|
- !ruby/object:Gem::Version
|
191
|
+
hash: 3
|
83
192
|
segments:
|
84
193
|
- 0
|
85
194
|
version: "0"
|
86
195
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
|
+
none: false
|
87
197
|
requirements:
|
88
198
|
- - ">="
|
89
199
|
- !ruby/object:Gem::Version
|
200
|
+
hash: 3
|
90
201
|
segments:
|
91
202
|
- 0
|
92
203
|
version: "0"
|
93
204
|
requirements: []
|
94
205
|
|
95
206
|
rubyforge_project:
|
96
|
-
rubygems_version: 1.
|
207
|
+
rubygems_version: 1.5.2
|
97
208
|
signing_key:
|
98
209
|
specification_version: 3
|
99
|
-
summary:
|
210
|
+
summary: Advanced tagging for Rails.
|
100
211
|
test_files:
|
101
212
|
- spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
|
102
213
|
- spec/acts_as_taggable_on/acts_as_tagger_spec.rb
|
@@ -106,7 +217,9 @@ test_files:
|
|
106
217
|
- spec/acts_as_taggable_on/tagger_spec.rb
|
107
218
|
- spec/acts_as_taggable_on/tagging_spec.rb
|
108
219
|
- spec/acts_as_taggable_on/tags_helper_spec.rb
|
220
|
+
- spec/acts_as_taggable_on/utils_spec.rb
|
109
221
|
- spec/bm.rb
|
222
|
+
- spec/database.yml.sample
|
110
223
|
- spec/models.rb
|
111
224
|
- spec/schema.rb
|
112
225
|
- spec/spec_helper.rb
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module ActsAsTaggableOn
|
2
|
-
module Taggable
|
3
|
-
module PostgreSQL
|
4
|
-
def self.included(base)
|
5
|
-
base.send :include, ActsAsTaggableOn::Taggable::PostgreSQL::InstanceMethods
|
6
|
-
base.extend ActsAsTaggableOn::Taggable::PostgreSQL::ClassMethods
|
7
|
-
|
8
|
-
ActsAsTaggableOn::Tag.class_eval do
|
9
|
-
def self.named(name)
|
10
|
-
where(["name ILIKE ?", name])
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.named_any(list)
|
14
|
-
where(list.map { |tag| sanitize_sql(["name ILIKE ?", tag.to_s]) }.join(" OR "))
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.named_like(name)
|
18
|
-
where(["name ILIKE ?", "%#{name}%"])
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.named_like_any(list)
|
22
|
-
where(list.map { |tag| sanitize_sql(["name ILIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
module InstanceMethods
|
28
|
-
end
|
29
|
-
|
30
|
-
module ClassMethods
|
31
|
-
# all column names are necessary for PostgreSQL group clause
|
32
|
-
def grouped_column_names_for(*objects)
|
33
|
-
object = objects.shift
|
34
|
-
columns = object.column_names.map { |column| "#{object.table_name}.#{column}" }
|
35
|
-
columns << objects.map do |object|
|
36
|
-
"#{object.table_name}.created_at"
|
37
|
-
end.flatten
|
38
|
-
|
39
|
-
columns.flatten.join(", ")
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/spec/database.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
sqlite3:
|
2
|
-
adapter: sqlite3
|
3
|
-
database: acts_as_taggable_on.sqlite3
|
4
|
-
|
5
|
-
mysql:
|
6
|
-
adapter: mysql
|
7
|
-
hostname: localhost
|
8
|
-
username: root
|
9
|
-
password:
|
10
|
-
database: acts_as_taggable_on
|
11
|
-
|
12
|
-
postgresql:
|
13
|
-
adapter: postgresql
|
14
|
-
hostname: localhost
|
15
|
-
username: postgres
|
16
|
-
password:
|
17
|
-
database: acts_as_taggable_on
|