acts-as-taggable-on 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.travis.yml +10 -0
- data/Gemfile +2 -5
- data/Guardfile +5 -0
- data/README.rdoc +19 -16
- data/Rakefile +9 -55
- data/VERSION +1 -1
- data/acts-as-taggable-on.gemspec +27 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +8 -2
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +72 -31
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +67 -32
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +16 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +18 -9
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +16 -6
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +2 -2
- data/lib/acts_as_taggable_on/compatibility/Gemfile +3 -1
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
- data/lib/acts_as_taggable_on/tag.rb +73 -57
- data/lib/acts_as_taggable_on/tag_list.rb +79 -78
- data/lib/acts_as_taggable_on/tagging.rb +19 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -12
- data/lib/acts_as_taggable_on/utils.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +3 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +25 -2
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_list_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_spec.rb +41 -21
- data/spec/acts_as_taggable_on/taggable_spec.rb +69 -12
- data/spec/acts_as_taggable_on/tagger_spec.rb +5 -5
- data/spec/acts_as_taggable_on/tagging_spec.rb +7 -7
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -3
- data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
- data/spec/database.yml.sample +19 -0
- data/spec/models.rb +5 -0
- data/spec/schema.rb +6 -0
- data/spec/spec_helper.rb +60 -33
- data/uninstall.rb +1 -0
- metadata +130 -15
- /data/{spec/spec.opts → .rspec} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
|
-
module Core
|
|
2
|
+
module Core
|
|
3
3
|
def self.included(base)
|
|
4
4
|
base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
|
|
5
5
|
base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
|
@@ -20,9 +20,9 @@ module ActsAsTaggableOn::Taggable
|
|
|
20
20
|
context_tags = tags_type.to_sym
|
|
21
21
|
|
|
22
22
|
class_eval do
|
|
23
|
-
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagging",
|
|
24
|
-
|
|
25
|
-
has_many context_tags, :through => context_taggings, :source => :tag
|
|
23
|
+
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
|
|
24
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
|
|
25
|
+
has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
class_eval %(
|
|
@@ -66,34 +66,54 @@ module ActsAsTaggableOn::Taggable
|
|
|
66
66
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
|
67
67
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
|
68
68
|
def tagged_with(tags, options = {})
|
|
69
|
-
tag_list = TagList.from(tags)
|
|
69
|
+
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
|
70
|
+
empty_result = scoped(:conditions => "1 = 0")
|
|
70
71
|
|
|
71
|
-
return
|
|
72
|
+
return empty_result if tag_list.empty?
|
|
72
73
|
|
|
73
74
|
joins = []
|
|
74
75
|
conditions = []
|
|
75
76
|
|
|
76
77
|
context = options.delete(:on)
|
|
78
|
+
alias_base_name = undecorated_table_name.gsub('.','_')
|
|
77
79
|
|
|
78
80
|
if options.delete(:exclude)
|
|
79
|
-
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name
|
|
80
|
-
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
|
81
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
|
82
|
+
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
|
81
83
|
|
|
82
84
|
elsif options.delete(:any)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
# get tags, drop out if nothing returned (we need at least one)
|
|
86
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
87
|
+
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
|
88
|
+
|
|
89
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
|
90
|
+
# avoid ambiguous column name
|
|
91
|
+
taggings_context = context ? "_#{context}" : ''
|
|
92
|
+
|
|
93
|
+
#TODO: fix alias to be smaller
|
|
94
|
+
taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
|
|
95
|
+
|
|
96
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
97
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
98
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
99
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
100
|
+
|
|
101
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
|
102
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
|
103
|
+
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
|
104
|
+
|
|
105
|
+
joins << tagging_join
|
|
85
106
|
|
|
86
107
|
else
|
|
87
|
-
tags = Tag.named_any(tag_list)
|
|
88
|
-
return
|
|
108
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
109
|
+
return empty_result unless tags.length == tag_list.length
|
|
89
110
|
|
|
90
111
|
tags.each do |tag|
|
|
91
|
-
|
|
92
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
112
|
+
prefix = "#{tag.safe_name}_#{rand(1024)}"
|
|
93
113
|
|
|
94
|
-
taggings_alias = "#{
|
|
114
|
+
taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
|
|
95
115
|
|
|
96
|
-
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
116
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
97
117
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
98
118
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
|
99
119
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
|
@@ -103,20 +123,23 @@ module ActsAsTaggableOn::Taggable
|
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
|
|
106
|
-
taggings_alias, tags_alias = "#{
|
|
126
|
+
taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
|
|
107
127
|
|
|
108
128
|
if options.delete(:match_all)
|
|
109
|
-
joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
129
|
+
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
110
130
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
111
131
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
112
132
|
|
|
113
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
114
|
-
end
|
|
115
133
|
|
|
134
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
135
|
+
group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
136
|
+
end
|
|
116
137
|
|
|
117
|
-
scoped(:
|
|
138
|
+
scoped(:select => select_clause,
|
|
139
|
+
:joins => joins.join(" "),
|
|
118
140
|
:group => group,
|
|
119
141
|
:conditions => conditions.join(" AND "),
|
|
142
|
+
:order => options[:order],
|
|
120
143
|
:readonly => false)
|
|
121
144
|
end
|
|
122
145
|
|
|
@@ -154,7 +177,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
154
177
|
|
|
155
178
|
def tag_list_cache_on(context)
|
|
156
179
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
157
|
-
instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name)))
|
|
180
|
+
instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
|
158
181
|
end
|
|
159
182
|
|
|
160
183
|
def tag_list_on(context)
|
|
@@ -166,40 +189,52 @@ module ActsAsTaggableOn::Taggable
|
|
|
166
189
|
variable_name = "@all_#{context.to_s.singularize}_list"
|
|
167
190
|
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
|
168
191
|
|
|
169
|
-
instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
192
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
170
193
|
end
|
|
171
194
|
|
|
172
195
|
##
|
|
173
196
|
# Returns all tags of a given context
|
|
174
197
|
def all_tags_on(context)
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
|
199
|
+
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
|
200
|
+
|
|
201
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
|
202
|
+
scope = base_tags.where(opts)
|
|
203
|
+
|
|
204
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
|
205
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
|
206
|
+
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
|
207
|
+
else
|
|
208
|
+
scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
scope.all
|
|
177
212
|
end
|
|
178
213
|
|
|
179
214
|
##
|
|
180
215
|
# Returns all tags that are not owned of a given context
|
|
181
216
|
def tags_on(context)
|
|
182
|
-
base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
|
|
217
|
+
base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
|
|
183
218
|
end
|
|
184
219
|
|
|
185
220
|
def set_tag_list_on(context, new_list)
|
|
186
221
|
add_custom_context(context)
|
|
187
222
|
|
|
188
223
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
189
|
-
instance_variable_set(variable_name, TagList.from(new_list))
|
|
224
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
|
190
225
|
end
|
|
191
226
|
|
|
192
227
|
def tagging_contexts
|
|
193
228
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
|
194
229
|
end
|
|
195
230
|
|
|
196
|
-
def reload
|
|
231
|
+
def reload(*args)
|
|
197
232
|
self.class.tag_types.each do |context|
|
|
198
233
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
|
199
234
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
|
200
235
|
end
|
|
201
236
|
|
|
202
|
-
super
|
|
237
|
+
super(*args)
|
|
203
238
|
end
|
|
204
239
|
|
|
205
240
|
def save_tags
|
|
@@ -209,7 +244,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
209
244
|
tag_list = tag_list_cache_on(context).uniq
|
|
210
245
|
|
|
211
246
|
# Find existing tags or create non-existing tags:
|
|
212
|
-
tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
247
|
+
tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
213
248
|
|
|
214
249
|
current_tags = tags_on(context)
|
|
215
250
|
old_tags = current_tags - tag_list
|
|
@@ -221,7 +256,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
221
256
|
|
|
222
257
|
if old_taggings.present?
|
|
223
258
|
# Destroy old taggings:
|
|
224
|
-
Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
259
|
+
ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
225
260
|
end
|
|
226
261
|
|
|
227
262
|
# Create new taggings:
|
|
@@ -234,4 +269,4 @@ module ActsAsTaggableOn::Taggable
|
|
|
234
269
|
end
|
|
235
270
|
end
|
|
236
271
|
end
|
|
237
|
-
end
|
|
272
|
+
end
|
|
@@ -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)
|
|
@@ -46,7 +50,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
46
50
|
cache = cached_owned_tag_list_on(context)
|
|
47
51
|
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
|
48
52
|
|
|
49
|
-
cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
53
|
+
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def set_owner_tag_list_on(owner, context, new_list)
|
|
@@ -55,22 +59,22 @@ module ActsAsTaggableOn::Taggable
|
|
|
55
59
|
cache = cached_owned_tag_list_on(context)
|
|
56
60
|
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
|
57
61
|
|
|
58
|
-
cache[owner] = TagList.from(new_list)
|
|
62
|
+
cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
def reload
|
|
65
|
+
def reload(*args)
|
|
62
66
|
self.class.tag_types.each do |context|
|
|
63
67
|
instance_variable_set("@owned_#{context}_list", nil)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
super
|
|
70
|
+
super(*args)
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def save_owned_tags
|
|
70
74
|
tagging_contexts.each do |context|
|
|
71
75
|
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
72
76
|
# Find existing tags or create non-existing tags:
|
|
73
|
-
tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
77
|
+
tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
74
78
|
|
|
75
79
|
owned_tags = owner_tags_on(owner, context)
|
|
76
80
|
old_tags = owned_tags - tag_list
|
|
@@ -78,13 +82,13 @@ module ActsAsTaggableOn::Taggable
|
|
|
78
82
|
|
|
79
83
|
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
80
84
|
# have the correct context, and are removed from the list.
|
|
81
|
-
old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
86
|
+
:tagger_type => owner.class.to_s, :tagger_id => owner.id,
|
|
87
|
+
:tag_id => old_tags, :context => context).all
|
|
84
88
|
|
|
85
89
|
if old_taggings.present?
|
|
86
90
|
# Destroy old taggings:
|
|
87
|
-
Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
91
|
+
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
# Create new taggings:
|
|
@@ -3,6 +3,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
3
3
|
def self.included(base)
|
|
4
4
|
base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
|
|
5
5
|
base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
|
|
6
|
+
base.initialize_acts_as_taggable_on_related
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
module ClassMethods
|
|
@@ -17,7 +18,11 @@ module ActsAsTaggableOn::Taggable
|
|
|
17
18
|
def find_related_#{tag_type}_for(klass, options = {})
|
|
18
19
|
related_tags_for('#{tag_type}', klass, options)
|
|
19
20
|
end
|
|
20
|
-
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless tag_types.empty?
|
|
25
|
+
class_eval %(
|
|
21
26
|
def find_matching_contexts(search_context, result_context, options = {})
|
|
22
27
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
23
28
|
end
|
|
@@ -41,10 +46,12 @@ module ActsAsTaggableOn::Taggable
|
|
|
41
46
|
|
|
42
47
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:
|
|
49
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
50
|
+
|
|
51
|
+
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
|
|
52
|
+
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
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],
|
|
54
|
+
:group => group_columns,
|
|
48
55
|
:order => "count DESC" }.update(options))
|
|
49
56
|
end
|
|
50
57
|
|
|
@@ -53,10 +60,12 @@ module ActsAsTaggableOn::Taggable
|
|
|
53
60
|
|
|
54
61
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
:
|
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
64
|
+
|
|
65
|
+
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
|
|
66
|
+
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
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],
|
|
68
|
+
:group => group_columns,
|
|
60
69
|
:order => "count DESC" }.update(options))
|
|
61
70
|
end
|
|
62
71
|
end
|
|
@@ -28,19 +28,29 @@ 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
|
-
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
|
38
|
-
has_many :base_tags, :
|
|
46
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
|
|
47
|
+
has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
|
39
48
|
|
|
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
|
|
@@ -16,8 +16,8 @@ module ActsAsTaggableOn
|
|
|
16
16
|
def acts_as_tagger(opts={})
|
|
17
17
|
class_eval do
|
|
18
18
|
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
|
19
|
-
:include => :tag, :class_name => "Tagging")
|
|
20
|
-
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
|
19
|
+
:include => :tag, :class_name => "ActsAsTaggableOn::Tagging")
|
|
20
|
+
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true, :class_name => "ActsAsTaggableOn::Tag"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
include ActsAsTaggableOn::Tagger::InstanceMethods
|
|
@@ -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,65 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tag < ::ActiveRecord::Base
|
|
3
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
4
|
+
include ActsAsTaggableOn::Utils
|
|
5
|
+
|
|
6
|
+
attr_accessible :name
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### ASSOCIATIONS:
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
validates_uniqueness_of :name
|
|
12
|
+
### VALIDATIONS:
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
validates_presence_of :name
|
|
15
|
+
validates_uniqueness_of :name
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
### SCOPES:
|
|
18
|
+
|
|
19
|
+
def self.named(name)
|
|
20
|
+
where(["name #{like_operator} ?", escape_like(name)])
|
|
21
|
+
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
def self.named_any(list)
|
|
24
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", escape_like(tag.to_s)]) }.join(" OR "))
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
def self.named_like(name)
|
|
28
|
+
where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.named_like_any(list)
|
|
32
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
### CLASS METHODS:
|
|
36
|
+
|
|
37
|
+
def self.find_or_create_with_like_by_name(name)
|
|
38
|
+
named_like(name).first || create(:name => name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
42
|
+
list = [list].flatten
|
|
43
|
+
|
|
44
|
+
return [] if list.empty?
|
|
45
|
+
|
|
46
|
+
existing_tags = Tag.named_any(list).all
|
|
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
|
|
51
|
+
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
52
|
+
|
|
53
|
+
existing_tags + created_tags
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
### INSTANCE METHODS:
|
|
57
|
+
|
|
58
|
+
def ==(object)
|
|
59
|
+
super || (object.is_a?(Tag) && name == object.name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def count
|
|
67
|
+
read_attribute(:count).to_i
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def safe_name
|
|
71
|
+
name.gsub(/[^a-zA-Z0-9]/, '')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
private
|
|
76
|
+
def comparable_name(str)
|
|
77
|
+
RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
|
|
78
|
+
end
|
|
79
|
+
end
|
|
55
80
|
end
|
|
56
|
-
|
|
57
|
-
def to_s
|
|
58
|
-
name
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def count
|
|
62
|
-
read_attribute(:count).to_i
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
end
|
|
81
|
+
end
|