acts-as-taggable-on 2.0.6 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|