acts-as-taggable-on 1.0.13 → 2.0.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/CHANGELOG +5 -2
- data/Gemfile +6 -0
- data/README.rdoc +61 -31
- data/Rakefile +46 -16
- data/VERSION +1 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
- data/lib/acts-as-taggable-on.rb +30 -7
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +98 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +237 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +64 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +43 -373
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +58 -43
- data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
- data/lib/acts_as_taggable_on/tag.rb +47 -8
- data/lib/acts_as_taggable_on/tag_list.rb +45 -45
- data/lib/acts_as_taggable_on/tagging.rb +17 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +8 -2
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
- data/rails/init.rb +1 -7
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +98 -53
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +46 -4
- data/spec/acts_as_taggable_on/tag_list_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +66 -13
- data/spec/acts_as_taggable_on/taggable_spec.rb +142 -70
- data/spec/acts_as_taggable_on/tagger_spec.rb +73 -5
- data/spec/acts_as_taggable_on/tagging_spec.rb +18 -3
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +1 -3
- data/spec/bm.rb +52 -0
- data/spec/models.rb +30 -0
- data/spec/schema.rb +13 -2
- data/spec/spec.opts +1 -2
- data/spec/spec_helper.rb +39 -34
- metadata +28 -8
- data/lib/acts_as_taggable_on/group_helper.rb +0 -12
- data/spec/acts_as_taggable_on/group_helper_spec.rb +0 -18
|
@@ -1,381 +1,51 @@
|
|
|
1
|
-
module
|
|
2
|
-
module
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
module ClassMethods
|
|
9
|
-
def taggable?
|
|
10
|
-
false
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def acts_as_taggable
|
|
14
|
-
acts_as_taggable_on :tags
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def acts_as_taggable_on(*args)
|
|
18
|
-
args.flatten! if args
|
|
19
|
-
args.compact! if args
|
|
20
|
-
for tag_type in args
|
|
21
|
-
tag_type = tag_type.to_s
|
|
22
|
-
# use aliased_join_table_name for context condition so that sphix can join multiple
|
|
23
|
-
# tag references from same model without getting an ambiguous column error
|
|
24
|
-
self.class_eval do
|
|
25
|
-
has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
|
|
26
|
-
:include => :tag, :conditions => ['#{aliased_join_table_name rescue "taggings"}.context = ?',tag_type], :class_name => "Tagging"
|
|
27
|
-
has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
self.class_eval <<-RUBY
|
|
31
|
-
def self.taggable?
|
|
32
|
-
true
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.caching_#{tag_type.singularize}_list?
|
|
36
|
-
caching_tag_list_on?("#{tag_type}")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def self.#{tag_type.singularize}_counts(options={})
|
|
40
|
-
tag_counts_on('#{tag_type}',options)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def #{tag_type.singularize}_list
|
|
44
|
-
tag_list_on('#{tag_type}')
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def #{tag_type.singularize}_list=(new_tags)
|
|
48
|
-
set_tag_list_on('#{tag_type}',new_tags)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def #{tag_type.singularize}_counts(options = {})
|
|
52
|
-
tag_counts_on('#{tag_type}',options)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def #{tag_type}_from(owner)
|
|
56
|
-
tag_list_on('#{tag_type}', owner)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def find_related_#{tag_type}(options = {})
|
|
60
|
-
related_tags_for('#{tag_type}', self.class, options)
|
|
61
|
-
end
|
|
62
|
-
alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
|
|
63
|
-
|
|
64
|
-
def find_related_#{tag_type}_for(klass, options = {})
|
|
65
|
-
related_tags_for('#{tag_type}', klass, options)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def find_matching_contexts(search_context, result_context, options = {})
|
|
69
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
|
73
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def top_#{tag_type}(limit = 10)
|
|
77
|
-
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def self.top_#{tag_type}(limit = 10)
|
|
81
|
-
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
|
82
|
-
end
|
|
83
|
-
RUBY
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
if respond_to?(:tag_types)
|
|
87
|
-
write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
|
|
88
|
-
else
|
|
89
|
-
self.class_eval do
|
|
90
|
-
write_inheritable_attribute(:tag_types, args.uniq)
|
|
91
|
-
class_inheritable_reader :tag_types
|
|
92
|
-
|
|
93
|
-
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
|
94
|
-
has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
|
|
95
|
-
|
|
96
|
-
attr_writer :custom_contexts
|
|
97
|
-
|
|
98
|
-
before_save :save_cached_tag_list
|
|
99
|
-
after_save :save_tags
|
|
100
|
-
|
|
101
|
-
if respond_to?(:named_scope)
|
|
102
|
-
named_scope :tagged_with, lambda{ |*args|
|
|
103
|
-
find_options_for_find_tagged_with(*args)
|
|
104
|
-
}
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
include ActiveRecord::Acts::TaggableOn::InstanceMethods
|
|
109
|
-
extend ActiveRecord::Acts::TaggableOn::SingletonMethods
|
|
110
|
-
alias_method_chain :reload, :tag_list
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
module SingletonMethods
|
|
116
|
-
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
117
|
-
# Pass either a tag string, or an array of strings or tags
|
|
118
|
-
#
|
|
119
|
-
# Options:
|
|
120
|
-
# :exclude - Find models that are not tagged with the given tags
|
|
121
|
-
# :match_all - Find models that match all of the given tags, not just one
|
|
122
|
-
# :conditions - A piece of SQL conditions to add to the query
|
|
123
|
-
# :on - scopes the find to a context
|
|
124
|
-
def find_tagged_with(*args)
|
|
125
|
-
options = find_options_for_find_tagged_with(*args)
|
|
126
|
-
options.blank? ? [] : find(:all,options)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def caching_tag_list_on?(context)
|
|
130
|
-
column_names.include?("cached_#{context.to_s.singularize}_list")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def tag_counts_on(context, options = {})
|
|
134
|
-
Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def all_tag_counts(options = {})
|
|
138
|
-
Tag.find(:all, find_options_for_tag_counts(options))
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def find_options_for_find_tagged_with(tags, options = {})
|
|
142
|
-
tags = TagList.from(tags)
|
|
143
|
-
|
|
144
|
-
return {} if tags.empty?
|
|
145
|
-
|
|
146
|
-
joins = []
|
|
147
|
-
conditions = []
|
|
148
|
-
|
|
149
|
-
context = options.delete(:on)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if options.delete(:exclude)
|
|
153
|
-
tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
154
|
-
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)})"
|
|
155
|
-
|
|
156
|
-
else
|
|
157
|
-
tags.each do |tag|
|
|
158
|
-
safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
|
|
159
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
160
|
-
|
|
161
|
-
taggings_alias = "#{table_name}_taggings_#{prefix}"
|
|
162
|
-
tags_alias = "#{table_name}_tags_#{prefix}"
|
|
163
|
-
|
|
164
|
-
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
165
|
-
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
166
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
167
|
-
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
168
|
-
|
|
169
|
-
tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
|
|
170
|
-
" ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
|
|
171
|
-
" AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
|
|
172
|
-
|
|
173
|
-
joins << tagging_join
|
|
174
|
-
joins << tag_join
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
|
|
179
|
-
|
|
180
|
-
if options.delete(:match_all)
|
|
181
|
-
joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
182
|
-
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
183
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
184
|
-
|
|
185
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
{ :joins => joins.join(" "),
|
|
189
|
-
:group => group,
|
|
190
|
-
:conditions => conditions.join(" AND ") }.update(options)
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Calculate the tag counts for all tags.
|
|
194
|
-
#
|
|
195
|
-
# Options:
|
|
196
|
-
# :start_at - Restrict the tags to those created after a certain time
|
|
197
|
-
# :end_at - Restrict the tags to those created before a certain time
|
|
198
|
-
# :conditions - A piece of SQL conditions to add to the query
|
|
199
|
-
# :limit - The maximum number of tags to return
|
|
200
|
-
# :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
|
201
|
-
# :at_least - Exclude tags with a frequency less than the given value
|
|
202
|
-
# :at_most - Exclude tags with a frequency greater than the given value
|
|
203
|
-
# :on - Scope the find to only include a certain context
|
|
204
|
-
def find_options_for_tag_counts(options = {})
|
|
205
|
-
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
|
206
|
-
|
|
207
|
-
scope = scope(:find)
|
|
208
|
-
start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
209
|
-
end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
210
|
-
|
|
211
|
-
taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
|
|
212
|
-
taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
|
|
213
|
-
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
|
214
|
-
|
|
215
|
-
conditions = [
|
|
216
|
-
taggable_type,
|
|
217
|
-
taggable_id,
|
|
218
|
-
options[:conditions],
|
|
219
|
-
start_at,
|
|
220
|
-
end_at
|
|
221
|
-
]
|
|
222
|
-
|
|
223
|
-
conditions = conditions.compact.join(' AND ')
|
|
224
|
-
conditions = merge_conditions(conditions, scope[:conditions]) if scope
|
|
225
|
-
|
|
226
|
-
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
|
227
|
-
joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
|
228
|
-
|
|
229
|
-
joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
|
230
|
-
unless self.descends_from_active_record?
|
|
231
|
-
# Current model is STI descendant, so add type checking to the join condition
|
|
232
|
-
joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
joins << scope[:joins] if scope && scope[:joins]
|
|
236
|
-
|
|
237
|
-
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
|
238
|
-
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
|
239
|
-
having = [at_least, at_most].compact.join(' AND ')
|
|
240
|
-
group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
|
|
241
|
-
group_by << " AND #{having}" unless having.blank?
|
|
242
|
-
|
|
243
|
-
{ :select => "#{Tag.table_name}.*, COUNT(*) AS count",
|
|
244
|
-
:joins => joins.join(" "),
|
|
245
|
-
:conditions => conditions,
|
|
246
|
-
:group => group_by,
|
|
247
|
-
:limit => options[:limit],
|
|
248
|
-
:order => options[:order]
|
|
249
|
-
}
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def is_taggable?
|
|
253
|
-
true
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
module InstanceMethods
|
|
258
|
-
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
259
|
-
|
|
260
|
-
def custom_contexts
|
|
261
|
-
@custom_contexts ||= []
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def is_taggable?
|
|
265
|
-
self.class.is_taggable?
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def add_custom_context(value)
|
|
269
|
-
custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def tag_list_on(context, owner=nil)
|
|
273
|
-
var_name = context.to_s.singularize + "_list"
|
|
274
|
-
add_custom_context(context)
|
|
275
|
-
return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
|
|
276
|
-
|
|
277
|
-
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
|
|
278
|
-
instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
|
|
279
|
-
else
|
|
280
|
-
instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def tags_on(context, owner=nil)
|
|
285
|
-
if owner
|
|
286
|
-
opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
|
|
287
|
-
context.to_s, owner.id, owner.class.to_s]}
|
|
288
|
-
else
|
|
289
|
-
opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
|
|
290
|
-
end
|
|
291
|
-
base_tags.find(:all, opts)
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def cached_tag_list_on(context)
|
|
295
|
-
self["cached_#{context.to_s.singularize}_list"]
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def set_tag_list_on(context,new_list, tagger=nil)
|
|
299
|
-
instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
|
|
300
|
-
add_custom_context(context)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def tag_counts_on(context, options={})
|
|
304
|
-
self.class.tag_counts_on(context, options.merge(:id => self.id))
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def related_tags_for(context, klass, options = {})
|
|
308
|
-
search_conditions = related_search_options(context, klass, options)
|
|
309
|
-
|
|
310
|
-
klass.find(:all, search_conditions)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def related_search_options(context, klass, options = {})
|
|
314
|
-
tags_to_find = self.tags_on(context).collect { |t| t.name }
|
|
315
|
-
|
|
316
|
-
exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
|
|
317
|
-
|
|
318
|
-
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
319
|
-
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
320
|
-
:conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
|
|
321
|
-
:group => grouped_column_names_for(klass),
|
|
322
|
-
:order => "count DESC"
|
|
323
|
-
}.update(options)
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
327
|
-
search_conditions = matching_context_search_options(search_context, result_context, klass, options)
|
|
328
|
-
|
|
329
|
-
klass.find(:all, search_conditions)
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
def matching_context_search_options(search_context, result_context, klass, options = {})
|
|
333
|
-
tags_to_find = self.tags_on(search_context).collect { |t| t.name }
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module Taggable
|
|
3
|
+
def taggable?
|
|
4
|
+
false
|
|
5
|
+
end
|
|
334
6
|
|
|
335
|
-
|
|
7
|
+
##
|
|
8
|
+
# This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# class Book < ActiveRecord::Base
|
|
12
|
+
# acts_as_taggable
|
|
13
|
+
# end
|
|
14
|
+
def acts_as_taggable
|
|
15
|
+
acts_as_taggable_on :tags
|
|
16
|
+
end
|
|
336
17
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
18
|
+
##
|
|
19
|
+
# Make a model taggable on specified contexts.
|
|
20
|
+
#
|
|
21
|
+
# @param [Array] tag_types An array of taggable contexts
|
|
22
|
+
#
|
|
23
|
+
# Example:
|
|
24
|
+
# class User < ActiveRecord::Base
|
|
25
|
+
# acts_as_taggable_on :languages, :skills
|
|
26
|
+
# end
|
|
27
|
+
def acts_as_taggable_on(*tag_types)
|
|
28
|
+
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
29
|
+
|
|
30
|
+
if taggable?
|
|
31
|
+
write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
|
|
32
|
+
else
|
|
33
|
+
write_inheritable_attribute(:tag_types, tag_types)
|
|
34
|
+
class_inheritable_reader(:tag_types)
|
|
344
35
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
end
|
|
36
|
+
class_eval do
|
|
37
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
|
38
|
+
has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
|
|
352
39
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
next unless instance_variable_get("@#{tag_type.singularize}_list")
|
|
356
|
-
owner = instance_variable_get("@#{tag_type.singularize}_list").owner
|
|
357
|
-
new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
|
|
358
|
-
old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
|
|
359
|
-
|
|
360
|
-
self.class.transaction do
|
|
361
|
-
base_tags.delete(*old_tags) if old_tags.any?
|
|
362
|
-
new_tag_names.each do |new_tag_name|
|
|
363
|
-
new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
|
|
364
|
-
Tagging.create(:tag_id => new_tag.id, :context => tag_type,
|
|
365
|
-
:taggable => self, :tagger => owner)
|
|
366
|
-
end
|
|
367
|
-
end
|
|
40
|
+
def self.taggable?
|
|
41
|
+
true
|
|
368
42
|
end
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
reload_without_tag_list(*args)
|
|
43
|
+
|
|
44
|
+
include ActsAsTaggableOn::Taggable::Core
|
|
45
|
+
include ActsAsTaggableOn::Taggable::Collection
|
|
46
|
+
include ActsAsTaggableOn::Taggable::Cache
|
|
47
|
+
include ActsAsTaggableOn::Taggable::Ownership
|
|
48
|
+
include ActsAsTaggableOn::Taggable::Related
|
|
379
49
|
end
|
|
380
50
|
end
|
|
381
51
|
end
|
|
@@ -1,52 +1,67 @@
|
|
|
1
|
-
module
|
|
2
|
-
module
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module Tagger
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ClassMethods
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
##
|
|
9
|
+
# Make a model a tagger. This allows an instance of a model to claim ownership
|
|
10
|
+
# of tags.
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# class User < ActiveRecord::Base
|
|
14
|
+
# acts_as_tagger
|
|
15
|
+
# end
|
|
16
|
+
def acts_as_tagger(opts={})
|
|
17
|
+
class_eval do
|
|
18
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
|
11
19
|
:include => :tag, :class_name => "Tagging")
|
|
12
20
|
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
|
13
|
-
include ActiveRecord::Acts::Tagger::InstanceMethods
|
|
14
|
-
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def is_tagger?
|
|
18
|
-
false
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
include ActsAsTaggableOn::Tagger::InstanceMethods
|
|
24
|
+
extend ActsAsTaggableOn::Tagger::SingletonMethods
|
|
20
25
|
end
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def tag(taggable, opts={})
|
|
27
|
-
opts.reverse_merge!(:force => true)
|
|
28
|
-
|
|
29
|
-
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
30
|
-
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
|
31
|
-
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
|
32
|
-
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
|
|
33
|
-
( opts[:force] || taggable.tag_types.include?(opts[:on]) )
|
|
34
|
-
|
|
35
|
-
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
|
36
|
-
taggable.save
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def is_tagger?
|
|
40
|
-
self.class.is_tagger?
|
|
41
|
-
end
|
|
26
|
+
|
|
27
|
+
def is_tagger?
|
|
28
|
+
false
|
|
42
29
|
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module InstanceMethods
|
|
33
|
+
##
|
|
34
|
+
# Tag a taggable model with tags that are owned by the tagger.
|
|
35
|
+
#
|
|
36
|
+
# @param taggable The object that will be tagged
|
|
37
|
+
# @param [Hash] options An hash with options. Available options are:
|
|
38
|
+
# * <tt>:with</tt> - The tags that you want to
|
|
39
|
+
# * <tt>:on</tt> - The context on which you want to tag
|
|
40
|
+
#
|
|
41
|
+
# Example:
|
|
42
|
+
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
|
43
|
+
def tag(taggable, opts={})
|
|
44
|
+
opts.reverse_merge!(:force => true)
|
|
45
|
+
|
|
46
|
+
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
47
|
+
|
|
48
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
|
49
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
|
50
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
|
|
51
|
+
|
|
52
|
+
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
|
|
53
|
+
taggable.save
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def is_tagger?
|
|
57
|
+
self.class.is_tagger?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module SingletonMethods
|
|
62
|
+
def is_tagger?
|
|
63
|
+
true
|
|
48
64
|
end
|
|
49
|
-
|
|
50
65
|
end
|
|
51
66
|
end
|
|
52
67
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module Backports
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.class_eval do
|
|
6
|
+
named_scope :where, lambda { |conditions| { :conditions => conditions } }
|
|
7
|
+
named_scope :joins, lambda { |joins| { :joins => joins } }
|
|
8
|
+
named_scope :group, lambda { |group| { :group => group } }
|
|
9
|
+
named_scope :order, lambda { |order| { :order => order } }
|
|
10
|
+
named_scope :select, lambda { |select| { :select => select } }
|
|
11
|
+
named_scope :limit, lambda { |limit| { :limit => limit } }
|
|
12
|
+
named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,26 +1,65 @@
|
|
|
1
1
|
class Tag < ActiveRecord::Base
|
|
2
|
-
|
|
2
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
|
|
3
3
|
|
|
4
|
+
attr_accessible :name
|
|
5
|
+
|
|
6
|
+
### ASSOCIATIONS:
|
|
7
|
+
|
|
8
|
+
has_many :taggings, :dependent => :destroy
|
|
9
|
+
|
|
10
|
+
### VALIDATIONS:
|
|
11
|
+
|
|
4
12
|
validates_presence_of :name
|
|
5
13
|
validates_uniqueness_of :name
|
|
14
|
+
|
|
15
|
+
### SCOPES:
|
|
16
|
+
|
|
17
|
+
def self.named(name)
|
|
18
|
+
where(["name LIKE ?", name])
|
|
19
|
+
end
|
|
6
20
|
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
def self.named_any(list)
|
|
22
|
+
where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
|
|
23
|
+
end
|
|
9
24
|
|
|
10
|
-
|
|
25
|
+
def self.named_like(name)
|
|
26
|
+
where(["name LIKE ?", "%#{name}%"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.named_like_any(list)
|
|
30
|
+
where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
### CLASS METHODS:
|
|
34
|
+
|
|
11
35
|
def self.find_or_create_with_like_by_name(name)
|
|
12
|
-
|
|
36
|
+
named_like(name).first || create(:name => name)
|
|
13
37
|
end
|
|
14
|
-
|
|
38
|
+
|
|
39
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
40
|
+
list = [list].flatten
|
|
41
|
+
|
|
42
|
+
return [] if list.empty?
|
|
43
|
+
|
|
44
|
+
existing_tags = Tag.named_any(list).all
|
|
45
|
+
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
|
|
46
|
+
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
47
|
+
|
|
48
|
+
existing_tags + created_tags
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
### INSTANCE METHODS:
|
|
52
|
+
|
|
15
53
|
def ==(object)
|
|
16
54
|
super || (object.is_a?(Tag) && name == object.name)
|
|
17
55
|
end
|
|
18
|
-
|
|
56
|
+
|
|
19
57
|
def to_s
|
|
20
58
|
name
|
|
21
59
|
end
|
|
22
|
-
|
|
60
|
+
|
|
23
61
|
def count
|
|
24
62
|
read_attribute(:count).to_i
|
|
25
63
|
end
|
|
64
|
+
|
|
26
65
|
end
|