acts-as-taggable-on 2.0.6 → 2.4.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +35 -0
- data/Gemfile +2 -9
- data/Guardfile +5 -0
- data/{MIT-LICENSE → MIT-LICENSE.md} +1 -1
- data/README.md +297 -0
- data/Rakefile +9 -55
- data/UPGRADING +14 -0
- data/acts-as-taggable-on.gemspec +32 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +37 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +99 -45
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +162 -45
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +40 -15
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +28 -18
- data/lib/acts_as_taggable_on/tag.rb +41 -16
- data/lib/acts_as_taggable_on/tag_list.rb +19 -14
- data/lib/acts_as_taggable_on/taggable.rb +102 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +12 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
- data/lib/acts_as_taggable_on/utils.rb +34 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +333 -54
- data/spec/acts_as_taggable_on/tag_list_spec.rb +117 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +111 -14
- data/spec/acts_as_taggable_on/taggable_spec.rb +330 -34
- data/spec/acts_as_taggable_on/tagger_spec.rb +62 -15
- data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
- data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
- data/spec/database.yml.sample +4 -2
- data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
- data/spec/models.rb +28 -1
- data/spec/schema.rb +18 -0
- data/spec/spec_helper.rb +30 -7
- data/uninstall.rb +1 -0
- metadata +174 -57
- data/CHANGELOG +0 -25
- data/README.rdoc +0 -221
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
- data/spec/database.yml +0 -17
@@ -9,7 +9,7 @@ module ActsAsTaggableOn::Taggable
|
|
9
9
|
module ClassMethods
|
10
10
|
def initialize_acts_as_taggable_on_collection
|
11
11
|
tag_types.map(&:to_s).each do |tag_type|
|
12
|
-
class_eval
|
12
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
13
|
def self.#{tag_type.singularize}_counts(options={})
|
14
14
|
tag_counts_on('#{tag_type}', options)
|
15
15
|
end
|
@@ -25,7 +25,7 @@ module ActsAsTaggableOn::Taggable
|
|
25
25
|
def self.top_#{tag_type}(limit = 10)
|
26
26
|
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
27
27
|
end
|
28
|
-
|
28
|
+
RUBY
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -37,6 +37,62 @@ module ActsAsTaggableOn::Taggable
|
|
37
37
|
def tag_counts_on(context, options = {})
|
38
38
|
all_tag_counts(options.merge({:on => context.to_s}))
|
39
39
|
end
|
40
|
+
|
41
|
+
def tags_on(context, options = {})
|
42
|
+
all_tags(options.merge({:on => context.to_s}))
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Calculate the tag names.
|
47
|
+
# To be used when you don't need tag counts and want to avoid the taggable joins.
|
48
|
+
#
|
49
|
+
# @param [Hash] options Options:
|
50
|
+
# * :start_at - Restrict the tags to those created after a certain time
|
51
|
+
# * :end_at - Restrict the tags to those created before a certain time
|
52
|
+
# * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
|
53
|
+
# * :limit - The maximum number of tags to return
|
54
|
+
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
55
|
+
# * :on - Scope the find to only include a certain context
|
56
|
+
def all_tags(options = {})
|
57
|
+
options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
|
58
|
+
|
59
|
+
## Generate conditions:
|
60
|
+
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
61
|
+
|
62
|
+
start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
63
|
+
end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
64
|
+
|
65
|
+
taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
|
66
|
+
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
67
|
+
|
68
|
+
tagging_conditions = [
|
69
|
+
taggable_conditions,
|
70
|
+
start_at_conditions,
|
71
|
+
end_at_conditions
|
72
|
+
].compact.reverse
|
73
|
+
|
74
|
+
tag_conditions = [
|
75
|
+
options[:conditions]
|
76
|
+
].compact.reverse
|
77
|
+
|
78
|
+
## Generate scope:
|
79
|
+
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
|
80
|
+
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
|
81
|
+
|
82
|
+
# Joins and conditions
|
83
|
+
tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
|
84
|
+
tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
|
85
|
+
|
86
|
+
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
|
87
|
+
|
88
|
+
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
89
|
+
scoped_select = "#{table_name}.#{primary_key}"
|
90
|
+
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
|
91
|
+
group(group_columns)
|
92
|
+
|
93
|
+
tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
|
94
|
+
tag_scope
|
95
|
+
end
|
40
96
|
|
41
97
|
##
|
42
98
|
# Calculate the tag counts for all tags.
|
@@ -53,73 +109,71 @@ module ActsAsTaggableOn::Taggable
|
|
53
109
|
def all_tag_counts(options = {})
|
54
110
|
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
55
111
|
|
56
|
-
scope =
|
57
|
-
{}
|
58
|
-
else
|
59
|
-
scope(:find) || {}
|
60
|
-
end
|
112
|
+
scope = {}
|
61
113
|
|
62
114
|
## Generate conditions:
|
63
115
|
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
64
|
-
|
116
|
+
|
65
117
|
start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
66
118
|
end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
67
|
-
|
119
|
+
|
68
120
|
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
|
-
|
121
|
+
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
|
122
|
+
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
|
123
|
+
|
124
|
+
tagging_conditions = [
|
72
125
|
taggable_conditions,
|
73
|
-
options[:conditions],
|
74
126
|
scope[:conditions],
|
75
127
|
start_at_conditions,
|
76
128
|
end_at_conditions
|
77
129
|
].compact.reverse
|
78
130
|
|
131
|
+
tag_conditions = [
|
132
|
+
options[:conditions]
|
133
|
+
].compact.reverse
|
134
|
+
|
79
135
|
## 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
136
|
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
|
84
137
|
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
138
|
|
86
|
-
|
87
|
-
tagging_join,
|
139
|
+
tagging_joins = [
|
88
140
|
taggable_join,
|
89
141
|
scope[:joins]
|
90
142
|
].compact
|
91
143
|
|
92
|
-
|
93
|
-
|
144
|
+
tag_joins = [
|
145
|
+
].compact
|
94
146
|
|
95
147
|
## Generate scope:
|
96
|
-
|
97
|
-
|
148
|
+
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
|
149
|
+
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
|
150
|
+
|
98
151
|
# Joins and conditions
|
99
|
-
|
100
|
-
|
101
|
-
|
152
|
+
tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
|
153
|
+
tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
|
154
|
+
|
155
|
+
tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
|
156
|
+
tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
|
157
|
+
|
102
158
|
# GROUP BY and HAVING clauses:
|
103
|
-
at_least = sanitize_sql([
|
104
|
-
at_most = sanitize_sql([
|
105
|
-
having = [at_least, at_most].compact.join(' AND ')
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
scope
|
159
|
+
at_least = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
|
160
|
+
at_most = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
|
161
|
+
having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
|
162
|
+
|
163
|
+
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
|
164
|
+
|
165
|
+
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
|
166
|
+
scoped_select = "#{table_name}.#{primary_key}"
|
167
|
+
select_query = "#{select(scoped_select).to_sql}"
|
168
|
+
|
169
|
+
res = ActiveRecord::Base.connection.select_all(select_query).map { |item| item.values }.flatten.join(",")
|
170
|
+
res = "NULL" if res.blank?
|
171
|
+
|
172
|
+
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{res})")
|
173
|
+
tagging_scope = tagging_scope.group(group_columns).having(having)
|
174
|
+
|
175
|
+
tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
|
176
|
+
tag_scope
|
123
177
|
end
|
124
178
|
end
|
125
179
|
|
@@ -8,24 +8,36 @@ module ActsAsTaggableOn::Taggable
|
|
8
8
|
attr_writer :custom_contexts
|
9
9
|
after_save :save_tags
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
base.initialize_acts_as_taggable_on_core
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
module ClassMethods
|
16
16
|
def initialize_acts_as_taggable_on_core
|
17
|
+
include taggable_mixin
|
17
18
|
tag_types.map(&:to_s).each do |tags_type|
|
18
19
|
tag_type = tags_type.to_s.singularize
|
19
20
|
context_taggings = "#{tag_type}_taggings".to_sym
|
20
21
|
context_tags = tags_type.to_sym
|
21
|
-
|
22
|
+
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
|
23
|
+
|
22
24
|
class_eval do
|
23
|
-
|
24
|
-
|
25
|
-
has_many
|
25
|
+
# when preserving tag order, include order option so that for a 'tags' context
|
26
|
+
# the associations tag_taggings & tags are always returned in created order
|
27
|
+
has_many context_taggings, :as => :taggable,
|
28
|
+
:dependent => :destroy,
|
29
|
+
:include => :tag,
|
30
|
+
:class_name => "ActsAsTaggableOn::Tagging",
|
31
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
|
32
|
+
:order => taggings_order
|
33
|
+
|
34
|
+
has_many context_tags, :through => context_taggings,
|
35
|
+
:source => :tag,
|
36
|
+
:class_name => "ActsAsTaggableOn::Tag",
|
37
|
+
:order => taggings_order
|
26
38
|
end
|
27
39
|
|
28
|
-
class_eval
|
40
|
+
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
29
41
|
def #{tag_type}_list
|
30
42
|
tag_list_on('#{tags_type}')
|
31
43
|
end
|
@@ -37,12 +49,12 @@ module ActsAsTaggableOn::Taggable
|
|
37
49
|
def all_#{tags_type}_list
|
38
50
|
all_tags_list_on('#{tags_type}')
|
39
51
|
end
|
40
|
-
|
41
|
-
end
|
52
|
+
RUBY
|
53
|
+
end
|
42
54
|
end
|
43
|
-
|
44
|
-
def
|
45
|
-
super(*
|
55
|
+
|
56
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
57
|
+
super(preserve_tag_order, *tag_types)
|
46
58
|
initialize_acts_as_taggable_on_core
|
47
59
|
end
|
48
60
|
|
@@ -59,63 +71,110 @@ module ActsAsTaggableOn::Taggable
|
|
59
71
|
# * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
|
60
72
|
# * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
|
61
73
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
74
|
+
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
62
75
|
#
|
63
76
|
# Example:
|
64
77
|
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
65
78
|
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
66
79
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
67
80
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
81
|
+
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
68
82
|
def tagged_with(tags, options = {})
|
69
83
|
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
84
|
+
empty_result = scoped(:conditions => "1 = 0")
|
70
85
|
|
71
|
-
return
|
86
|
+
return empty_result if tag_list.empty?
|
72
87
|
|
73
88
|
joins = []
|
74
89
|
conditions = []
|
75
90
|
|
76
91
|
context = options.delete(:on)
|
92
|
+
owned_by = options.delete(:owned_by)
|
93
|
+
alias_base_name = undecorated_table_name.gsub('.','_')
|
94
|
+
quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : ''
|
77
95
|
|
78
96
|
if options.delete(:exclude)
|
79
|
-
|
80
|
-
|
97
|
+
if options.delete(:wild)
|
98
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
|
99
|
+
else
|
100
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
101
|
+
end
|
102
|
+
|
103
|
+
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}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
81
104
|
|
82
105
|
elsif options.delete(:any)
|
83
|
-
|
84
|
-
|
106
|
+
# get tags, drop out if nothing returned (we need at least one)
|
107
|
+
if options.delete(:wild)
|
108
|
+
tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
109
|
+
else
|
110
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
111
|
+
end
|
112
|
+
|
113
|
+
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
114
|
+
|
115
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
116
|
+
# avoid ambiguous column name
|
117
|
+
taggings_context = context ? "_#{context}" : ''
|
118
|
+
|
119
|
+
taggings_alias = adjust_taggings_alias(
|
120
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
|
121
|
+
)
|
122
|
+
|
123
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
124
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
125
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
126
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
127
|
+
|
128
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
129
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
130
|
+
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
131
|
+
|
132
|
+
joins << tagging_join
|
85
133
|
|
86
134
|
else
|
87
135
|
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
88
|
-
return
|
136
|
+
return empty_result unless tags.length == tag_list.length
|
89
137
|
|
90
138
|
tags.each do |tag|
|
91
|
-
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
92
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
93
139
|
|
94
|
-
taggings_alias = "#{
|
140
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
95
141
|
|
96
142
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
97
|
-
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
143
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
98
144
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
99
145
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
100
146
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
101
147
|
|
148
|
+
if owned_by
|
149
|
+
tagging_join << " AND " +
|
150
|
+
sanitize_sql([
|
151
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
152
|
+
owned_by.id,
|
153
|
+
owned_by.class.base_class.to_s
|
154
|
+
])
|
155
|
+
end
|
156
|
+
|
102
157
|
joins << tagging_join
|
103
158
|
end
|
104
159
|
end
|
105
160
|
|
106
|
-
taggings_alias, tags_alias = "#{
|
161
|
+
taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
107
162
|
|
108
163
|
if options.delete(:match_all)
|
109
164
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
110
|
-
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
165
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
111
166
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
112
167
|
|
113
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
114
|
-
end
|
115
168
|
|
169
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
170
|
+
group = group_columns
|
171
|
+
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
172
|
+
end
|
116
173
|
|
117
|
-
scoped(:
|
174
|
+
scoped(:select => select_clause,
|
175
|
+
:joins => joins.join(" "),
|
118
176
|
:group => group,
|
177
|
+
:having => having,
|
119
178
|
:conditions => conditions.join(" AND "),
|
120
179
|
:order => options[:order],
|
121
180
|
:readonly => false)
|
@@ -124,8 +183,19 @@ module ActsAsTaggableOn::Taggable
|
|
124
183
|
def is_taggable?
|
125
184
|
true
|
126
185
|
end
|
127
|
-
|
128
|
-
|
186
|
+
|
187
|
+
def adjust_taggings_alias(taggings_alias)
|
188
|
+
if taggings_alias.size > 75
|
189
|
+
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
190
|
+
end
|
191
|
+
taggings_alias
|
192
|
+
end
|
193
|
+
|
194
|
+
def taggable_mixin
|
195
|
+
@taggable_mixin ||= Module.new
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
129
199
|
module InstanceMethods
|
130
200
|
# all column names are necessary for PostgreSQL group clause
|
131
201
|
def grouped_column_names_for(object)
|
@@ -150,12 +220,13 @@ module ActsAsTaggableOn::Taggable
|
|
150
220
|
|
151
221
|
def tag_list_cache_set_on(context)
|
152
222
|
variable_name = "@#{context.to_s.singularize}_list"
|
153
|
-
!instance_variable_get(variable_name).nil?
|
223
|
+
instance_variable_defined?(variable_name) && !instance_variable_get(variable_name).nil?
|
154
224
|
end
|
155
225
|
|
156
226
|
def tag_list_cache_on(context)
|
157
227
|
variable_name = "@#{context.to_s.singularize}_list"
|
158
|
-
instance_variable_get(variable_name)
|
228
|
+
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
229
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
159
230
|
end
|
160
231
|
|
161
232
|
def tag_list_on(context)
|
@@ -165,7 +236,7 @@ module ActsAsTaggableOn::Taggable
|
|
165
236
|
|
166
237
|
def all_tags_list_on(context)
|
167
238
|
variable_name = "@all_#{context.to_s.singularize}_list"
|
168
|
-
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
239
|
+
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
169
240
|
|
170
241
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
171
242
|
end
|
@@ -176,20 +247,35 @@ module ActsAsTaggableOn::Taggable
|
|
176
247
|
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
177
248
|
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
178
249
|
|
179
|
-
opts
|
180
|
-
base_tags.where(opts)
|
250
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
251
|
+
scope = base_tags.where(opts)
|
252
|
+
|
253
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
254
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
255
|
+
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
256
|
+
else
|
257
|
+
scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
258
|
+
end
|
259
|
+
|
260
|
+
scope.all
|
181
261
|
end
|
182
262
|
|
183
263
|
##
|
184
264
|
# Returns all tags that are not owned of a given context
|
185
265
|
def tags_on(context)
|
186
|
-
base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
266
|
+
scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
267
|
+
# when preserving tag order, return tags in created order
|
268
|
+
# if we added the order to the association this would always apply
|
269
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
270
|
+
scope.all
|
187
271
|
end
|
188
272
|
|
189
273
|
def set_tag_list_on(context, new_list)
|
190
274
|
add_custom_context(context)
|
191
275
|
|
192
276
|
variable_name = "@#{context.to_s.singularize}_list"
|
277
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
278
|
+
|
193
279
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
194
280
|
end
|
195
281
|
|
@@ -197,12 +283,26 @@ module ActsAsTaggableOn::Taggable
|
|
197
283
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
198
284
|
end
|
199
285
|
|
286
|
+
def process_dirty_object(context,new_list)
|
287
|
+
value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
|
288
|
+
attrib = "#{context.to_s.singularize}_list"
|
289
|
+
|
290
|
+
if changed_attributes.include?(attrib)
|
291
|
+
# The attribute already has an unsaved change.
|
292
|
+
old = changed_attributes[attrib]
|
293
|
+
changed_attributes.delete(attrib) if (old.to_s == value.to_s)
|
294
|
+
else
|
295
|
+
old = tag_list_on(context).to_s
|
296
|
+
changed_attributes[attrib] = old if (old.to_s != value.to_s)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
200
300
|
def reload(*args)
|
201
301
|
self.class.tag_types.each do |context|
|
202
302
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
203
303
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
204
304
|
end
|
205
|
-
|
305
|
+
|
206
306
|
super(*args)
|
207
307
|
end
|
208
308
|
|
@@ -210,22 +310,39 @@ module ActsAsTaggableOn::Taggable
|
|
210
310
|
tagging_contexts.each do |context|
|
211
311
|
next unless tag_list_cache_set_on(context)
|
212
312
|
|
313
|
+
# List of currently assigned tag names
|
213
314
|
tag_list = tag_list_cache_on(context).uniq
|
214
315
|
|
215
316
|
# Find existing tags or create non-existing tags:
|
216
|
-
|
317
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
217
318
|
|
319
|
+
# Tag objects for currently assigned tags
|
218
320
|
current_tags = tags_on(context)
|
219
|
-
|
220
|
-
|
221
|
-
|
321
|
+
|
322
|
+
# Tag maintenance based on whether preserving the created order of tags
|
323
|
+
if self.class.preserve_tag_order?
|
324
|
+
# First off order the array of tag objects to match the tag list
|
325
|
+
# rather than existing tags followed by new tags
|
326
|
+
tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
|
327
|
+
# To preserve tags in the order in which they were added
|
328
|
+
# delete all current tags and create new tags if the content or order has changed
|
329
|
+
old_tags = (tags == current_tags ? [] : current_tags)
|
330
|
+
new_tags = (tags == current_tags ? [] : tags)
|
331
|
+
else
|
332
|
+
# Delete discarded tags and create new tags
|
333
|
+
old_tags = current_tags - tags
|
334
|
+
new_tags = tags - current_tags
|
335
|
+
end
|
336
|
+
|
222
337
|
# Find taggings to remove:
|
223
|
-
|
224
|
-
|
338
|
+
if old_tags.present?
|
339
|
+
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
340
|
+
:context => context.to_s, :tag_id => old_tags).all
|
341
|
+
end
|
225
342
|
|
343
|
+
# Destroy old taggings:
|
226
344
|
if old_taggings.present?
|
227
|
-
#
|
228
|
-
ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
|
345
|
+
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
229
346
|
end
|
230
347
|
|
231
348
|
# Create new taggings:
|
@@ -238,4 +355,4 @@ module ActsAsTaggableOn::Taggable
|
|
238
355
|
end
|
239
356
|
end
|
240
357
|
end
|
241
|
-
end
|
358
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActsAsTaggableOn::Taggable
|
2
|
+
module Dirty
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
|
5
|
+
|
6
|
+
base.initialize_acts_as_taggable_on_dirty
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def initialize_acts_as_taggable_on_dirty
|
11
|
+
tag_types.map(&:to_s).each do |tags_type|
|
12
|
+
tag_type = tags_type.to_s.singularize
|
13
|
+
context_tags = tags_type.to_sym
|
14
|
+
|
15
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
16
|
+
def #{tag_type}_list_changed?
|
17
|
+
changed_attributes.include?("#{tag_type}_list")
|
18
|
+
end
|
19
|
+
|
20
|
+
def #{tag_type}_list_was
|
21
|
+
changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
|
22
|
+
end
|
23
|
+
|
24
|
+
def #{tag_type}_list_change
|
25
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
26
|
+
end
|
27
|
+
|
28
|
+
def #{tag_type}_list_changes
|
29
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
30
|
+
end
|
31
|
+
RUBY
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|