acts-as-taggable-on 1.0.13 → 1.1.1
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/README.rdoc +9 -1
- data/Rakefile +1 -1
- 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/acts_as_taggable_on.rb +107 -53
- data/lib/acts_as_taggable_on/tag.rb +27 -3
- data/lib/acts_as_taggable_on/tag_list.rb +6 -11
- data/lib/acts_as_taggable_on/tagging.rb +6 -0
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -0
- data/rails/init.rb +1 -3
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +0 -14
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +41 -5
- data/spec/acts_as_taggable_on/tag_list_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +38 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +71 -39
- data/spec/acts_as_taggable_on/tagger_spec.rb +17 -2
- data/spec/acts_as_taggable_on/tagging_spec.rb +9 -0
- data/spec/spec_helper.rb +13 -2
- metadata +4 -2
data/README.rdoc
CHANGED
|
@@ -151,6 +151,12 @@ A helper is included to assist with generating tag clouds.
|
|
|
151
151
|
|
|
152
152
|
Here is an example that generates a tag cloud.
|
|
153
153
|
|
|
154
|
+
Helper:
|
|
155
|
+
|
|
156
|
+
module PostsHelper
|
|
157
|
+
include TagsHelper
|
|
158
|
+
end
|
|
159
|
+
|
|
154
160
|
Controller:
|
|
155
161
|
|
|
156
162
|
class PostController < ApplicationController
|
|
@@ -160,7 +166,8 @@ Controller:
|
|
|
160
166
|
end
|
|
161
167
|
|
|
162
168
|
View:
|
|
163
|
-
|
|
169
|
+
|
|
170
|
+
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
|
|
164
171
|
<%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
|
|
165
172
|
<% end %>
|
|
166
173
|
|
|
@@ -187,5 +194,6 @@ CSS:
|
|
|
187
194
|
* slainer68 - STI fix
|
|
188
195
|
* harrylove - migration instructions and fix-ups
|
|
189
196
|
* lawrencepit - cached tag work
|
|
197
|
+
* sobrinho - fixed tag_cloud helper
|
|
190
198
|
|
|
191
199
|
Copyright (c) 2007-2009 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
|
data/Rakefile
CHANGED
|
@@ -9,7 +9,7 @@ begin
|
|
|
9
9
|
gemspec.email = "michael@intridea.com"
|
|
10
10
|
gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
|
|
11
11
|
gemspec.authors = ["Michael Bleigh"]
|
|
12
|
-
gemspec.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"] - FileList["**/*.log"]
|
|
12
|
+
gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
|
|
13
13
|
end
|
|
14
14
|
Jeweler::GemcutterTasks.new
|
|
15
15
|
rescue LoadError
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.1.1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
|
2
|
+
def self.up
|
|
3
|
+
create_table :tags do |t|
|
|
4
|
+
t.column :name, :string
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
create_table :taggings do |t|
|
|
8
|
+
t.column :tag_id, :integer
|
|
9
|
+
t.column :taggable_id, :integer
|
|
10
|
+
t.column :tagger_id, :integer
|
|
11
|
+
t.column :tagger_type, :string
|
|
12
|
+
|
|
13
|
+
# You should make sure that the column created is
|
|
14
|
+
# long enough to store the required class names.
|
|
15
|
+
t.column :taggable_type, :string
|
|
16
|
+
t.column :context, :string
|
|
17
|
+
|
|
18
|
+
t.column :created_at, :datetime
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :taggings, :tag_id
|
|
22
|
+
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.down
|
|
26
|
+
drop_table :taggings
|
|
27
|
+
drop_table :tags
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -19,15 +19,15 @@ module ActiveRecord
|
|
|
19
19
|
args.compact! if args
|
|
20
20
|
for tag_type in args
|
|
21
21
|
tag_type = tag_type.to_s
|
|
22
|
-
# use aliased_join_table_name for context condition so that
|
|
22
|
+
# use aliased_join_table_name for context condition so that sphinx can join multiple
|
|
23
23
|
# tag references from same model without getting an ambiguous column error
|
|
24
|
-
|
|
24
|
+
class_eval do
|
|
25
25
|
has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
|
|
26
|
-
:include => :tag, :conditions => ['#{aliased_join_table_name rescue
|
|
26
|
+
:include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
|
|
27
27
|
has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
class_eval <<-RUBY
|
|
31
31
|
def self.taggable?
|
|
32
32
|
true
|
|
33
33
|
end
|
|
@@ -82,11 +82,10 @@ module ActiveRecord
|
|
|
82
82
|
end
|
|
83
83
|
RUBY
|
|
84
84
|
end
|
|
85
|
-
|
|
86
85
|
if respond_to?(:tag_types)
|
|
87
86
|
write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
|
|
88
87
|
else
|
|
89
|
-
|
|
88
|
+
class_eval do
|
|
90
89
|
write_inheritable_attribute(:tag_types, args.uniq)
|
|
91
90
|
class_inheritable_reader :tag_types
|
|
92
91
|
|
|
@@ -117,6 +116,7 @@ module ActiveRecord
|
|
|
117
116
|
# Pass either a tag string, or an array of strings or tags
|
|
118
117
|
#
|
|
119
118
|
# Options:
|
|
119
|
+
# :any - find models that match any of the given tags
|
|
120
120
|
# :exclude - Find models that are not tagged with the given tags
|
|
121
121
|
# :match_all - Find models that match all of the given tags, not just one
|
|
122
122
|
# :conditions - A piece of SQL conditions to add to the query
|
|
@@ -139,9 +139,9 @@ module ActiveRecord
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def find_options_for_find_tagged_with(tags, options = {})
|
|
142
|
-
|
|
142
|
+
tag_list = TagList.from(tags)
|
|
143
143
|
|
|
144
|
-
return {} if
|
|
144
|
+
return {} if tag_list.empty?
|
|
145
145
|
|
|
146
146
|
joins = []
|
|
147
147
|
conditions = []
|
|
@@ -150,28 +150,30 @@ module ActiveRecord
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
if options.delete(:exclude)
|
|
153
|
-
tags_conditions =
|
|
153
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
154
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
155
|
|
|
156
|
+
elsif options.delete(:any)
|
|
157
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
158
|
+
conditions << "#{table_name}.#{primary_key} 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)})"
|
|
159
|
+
|
|
156
160
|
else
|
|
161
|
+
tags = Tag.named_like_any(tag_list)
|
|
162
|
+
return { :conditions => "1 = 0" } unless tags.length == tag_list.length
|
|
163
|
+
|
|
157
164
|
tags.each do |tag|
|
|
158
|
-
safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
|
|
165
|
+
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
|
159
166
|
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
160
167
|
|
|
161
168
|
taggings_alias = "#{table_name}_taggings_#{prefix}"
|
|
162
|
-
tags_alias = "#{table_name}_tags_#{prefix}"
|
|
163
169
|
|
|
164
170
|
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
165
171
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
166
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
172
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
|
173
|
+
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
|
167
174
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
168
175
|
|
|
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
176
|
joins << tagging_join
|
|
174
|
-
joins << tag_join
|
|
175
177
|
end
|
|
176
178
|
end
|
|
177
179
|
|
|
@@ -187,7 +189,8 @@ module ActiveRecord
|
|
|
187
189
|
|
|
188
190
|
{ :joins => joins.join(" "),
|
|
189
191
|
:group => group,
|
|
190
|
-
:conditions => conditions.join(" AND ")
|
|
192
|
+
:conditions => conditions.join(" AND "),
|
|
193
|
+
:readonly => false }.update(options)
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
# Calculate the tag counts for all tags.
|
|
@@ -225,14 +228,32 @@ module ActiveRecord
|
|
|
225
228
|
|
|
226
229
|
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
|
227
230
|
joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
|
228
|
-
|
|
229
231
|
joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
|
230
|
-
|
|
232
|
+
|
|
233
|
+
unless descends_from_active_record?
|
|
231
234
|
# Current model is STI descendant, so add type checking to the join condition
|
|
232
|
-
joins << " AND #{table_name}.#{
|
|
235
|
+
joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
|
233
236
|
end
|
|
234
237
|
|
|
235
|
-
|
|
238
|
+
# Based on a proposed patch by donV to ActiveRecord Base
|
|
239
|
+
# This is needed because merge_joins and construct_join are private in ActiveRecord Base
|
|
240
|
+
if scope && scope[:joins]
|
|
241
|
+
case scope[:joins]
|
|
242
|
+
when Array
|
|
243
|
+
scope_joins = scope[:joins].flatten
|
|
244
|
+
strings = scope_joins.select{|j| j.is_a? String}
|
|
245
|
+
joins << strings.join(' ') + " "
|
|
246
|
+
symbols = scope_joins - strings
|
|
247
|
+
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, symbols, nil)
|
|
248
|
+
joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
|
|
249
|
+
joins.flatten!
|
|
250
|
+
when Symbol, Hash
|
|
251
|
+
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, scope[:joins], nil)
|
|
252
|
+
joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
|
|
253
|
+
when String
|
|
254
|
+
joins << scope[:joins]
|
|
255
|
+
end
|
|
256
|
+
end
|
|
236
257
|
|
|
237
258
|
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
|
238
259
|
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
|
@@ -264,29 +285,40 @@ module ActiveRecord
|
|
|
264
285
|
def is_taggable?
|
|
265
286
|
self.class.is_taggable?
|
|
266
287
|
end
|
|
267
|
-
|
|
288
|
+
|
|
268
289
|
def add_custom_context(value)
|
|
269
290
|
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
291
|
end
|
|
271
292
|
|
|
272
|
-
def tag_list_on(context, owner=nil)
|
|
273
|
-
var_name = context.to_s.singularize + "_list"
|
|
293
|
+
def tag_list_on(context, owner = nil)
|
|
274
294
|
add_custom_context(context)
|
|
275
|
-
|
|
276
|
-
|
|
295
|
+
cache = tag_list_cache_on(context)
|
|
296
|
+
return owner ? cache[owner] : cache[owner] if cache[owner]
|
|
297
|
+
|
|
277
298
|
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
|
|
278
|
-
|
|
299
|
+
cache[owner] = TagList.from(cached_tag_list_on(context))
|
|
279
300
|
else
|
|
280
|
-
|
|
301
|
+
cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
|
|
281
302
|
end
|
|
282
303
|
end
|
|
304
|
+
|
|
305
|
+
def all_tags_list_on(context)
|
|
306
|
+
variable_name = "@all_#{context.to_s.singularize}_list"
|
|
307
|
+
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
|
308
|
+
instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def all_tags_on(context)
|
|
312
|
+
opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
|
|
313
|
+
base_tags.find(:all, opts.merge(:order => "#{Tagging.table_name}.created_at"))
|
|
314
|
+
end
|
|
283
315
|
|
|
284
|
-
def tags_on(context, owner=nil)
|
|
316
|
+
def tags_on(context, owner = nil)
|
|
285
317
|
if owner
|
|
286
318
|
opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
|
|
287
319
|
context.to_s, owner.id, owner.class.to_s]}
|
|
288
320
|
else
|
|
289
|
-
opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
|
|
321
|
+
opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]}
|
|
290
322
|
end
|
|
291
323
|
base_tags.find(:all, opts)
|
|
292
324
|
end
|
|
@@ -294,14 +326,21 @@ module ActiveRecord
|
|
|
294
326
|
def cached_tag_list_on(context)
|
|
295
327
|
self["cached_#{context.to_s.singularize}_list"]
|
|
296
328
|
end
|
|
329
|
+
|
|
330
|
+
def tag_list_cache_on(context)
|
|
331
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
|
332
|
+
cache = instance_variable_get(variable_name)
|
|
333
|
+
instance_variable_set(variable_name, cache = {}) unless cache
|
|
334
|
+
cache
|
|
335
|
+
end
|
|
297
336
|
|
|
298
|
-
def set_tag_list_on(context,new_list, tagger=nil)
|
|
299
|
-
|
|
337
|
+
def set_tag_list_on(context, new_list, tagger = nil)
|
|
338
|
+
tag_list_cache_on(context)[tagger] = TagList.from(new_list)
|
|
300
339
|
add_custom_context(context)
|
|
301
340
|
end
|
|
302
341
|
|
|
303
342
|
def tag_counts_on(context, options={})
|
|
304
|
-
self.class.tag_counts_on(context, options.merge(:id =>
|
|
343
|
+
self.class.tag_counts_on(context, options.merge(:id => id))
|
|
305
344
|
end
|
|
306
345
|
|
|
307
346
|
def related_tags_for(context, klass, options = {})
|
|
@@ -311,9 +350,9 @@ module ActiveRecord
|
|
|
311
350
|
end
|
|
312
351
|
|
|
313
352
|
def related_search_options(context, klass, options = {})
|
|
314
|
-
tags_to_find =
|
|
353
|
+
tags_to_find = tags_on(context).collect { |t| t.name }
|
|
315
354
|
|
|
316
|
-
exclude_self = "#{klass.table_name}.id != #{
|
|
355
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
317
356
|
|
|
318
357
|
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
319
358
|
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
@@ -330,9 +369,9 @@ module ActiveRecord
|
|
|
330
369
|
end
|
|
331
370
|
|
|
332
371
|
def matching_context_search_options(search_context, result_context, klass, options = {})
|
|
333
|
-
tags_to_find =
|
|
372
|
+
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
|
334
373
|
|
|
335
|
-
exclude_self = "#{klass.table_name}.id != #{
|
|
374
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
336
375
|
|
|
337
376
|
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
338
377
|
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
@@ -345,34 +384,49 @@ module ActiveRecord
|
|
|
345
384
|
def save_cached_tag_list
|
|
346
385
|
self.class.tag_types.map(&:to_s).each do |tag_type|
|
|
347
386
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|
|
348
|
-
self["cached_#{tag_type.singularize}_list"] =
|
|
387
|
+
self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).tags.join(', ')
|
|
349
388
|
end
|
|
350
389
|
end
|
|
351
390
|
end
|
|
352
391
|
|
|
353
392
|
def save_tags
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
393
|
+
contexts = custom_contexts + self.class.tag_types.map(&:to_s)
|
|
394
|
+
|
|
395
|
+
transaction do
|
|
396
|
+
contexts.each do |context|
|
|
397
|
+
cache = tag_list_cache_on(context)
|
|
398
|
+
|
|
399
|
+
cache.each do |owner, list|
|
|
400
|
+
new_tags = Tag.find_or_create_all_with_like_by_name(list.uniq)
|
|
401
|
+
|
|
402
|
+
# Destroy old taggings:
|
|
403
|
+
if owner
|
|
404
|
+
old_tags = tags_on(context, owner) - new_tags
|
|
405
|
+
old_taggings = taggings.find(:all, :conditions => { :tag_id => old_tags, :tagger_id => owner, :tagger_type => owner.class.to_s, :context => context })
|
|
406
|
+
old_taggings.each(&:destroy)
|
|
407
|
+
else
|
|
408
|
+
old_tags = tags_on(context) - new_tags
|
|
409
|
+
base_tags.delete(*old_tags)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
new_tags.reject! { |tag| taggings.any? { |tagging| tagging.tag == tag &&
|
|
413
|
+
tagging.tagger == owner &&
|
|
414
|
+
tagging.context == context } }
|
|
415
|
+
|
|
416
|
+
# create new taggings:
|
|
417
|
+
new_tags.each do |tag|
|
|
418
|
+
taggings.create!(:tag_id => tag.id, :context => context, :tagger => owner)
|
|
419
|
+
end
|
|
366
420
|
end
|
|
367
421
|
end
|
|
368
|
-
end
|
|
422
|
+
end
|
|
369
423
|
|
|
370
424
|
true
|
|
371
425
|
end
|
|
372
426
|
|
|
373
427
|
def reload_with_tag_list(*args)
|
|
374
428
|
self.class.tag_types.each do |tag_type|
|
|
375
|
-
|
|
429
|
+
instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
|
376
430
|
end
|
|
377
431
|
|
|
378
432
|
reload_without_tag_list(*args)
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
class Tag < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
attr_accessible :name
|
|
4
|
+
|
|
5
|
+
### ASSOCIATIONS:
|
|
6
|
+
|
|
2
7
|
has_many :taggings, :dependent => :destroy
|
|
3
8
|
|
|
9
|
+
### VALIDATIONS:
|
|
10
|
+
|
|
4
11
|
validates_presence_of :name
|
|
5
12
|
validates_uniqueness_of :name
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
### NAMED SCOPES:
|
|
15
|
+
|
|
16
|
+
named_scope :named, lambda { |name| { :conditions => ["name LIKE ?", name] } }
|
|
17
|
+
named_scope :named_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
|
|
8
18
|
named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
|
|
19
|
+
named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ") } }
|
|
20
|
+
|
|
21
|
+
### CLASS METHODS:
|
|
9
22
|
|
|
10
|
-
# LIKE is used for cross-database case-insensitivity
|
|
11
23
|
def self.find_or_create_with_like_by_name(name)
|
|
12
|
-
|
|
24
|
+
named_like(name).first || create(:name => name)
|
|
13
25
|
end
|
|
14
26
|
|
|
27
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
28
|
+
list = [list].flatten
|
|
29
|
+
|
|
30
|
+
existing_tags = Tag.named_any(list).all
|
|
31
|
+
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
|
|
32
|
+
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
33
|
+
|
|
34
|
+
existing_tags + created_tags
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
### INSTANCE METHODS:
|
|
38
|
+
|
|
15
39
|
def ==(object)
|
|
16
40
|
super || (object.is_a?(Tag) && name == object.name)
|
|
17
41
|
end
|
|
@@ -41,9 +41,10 @@ class TagList < Array
|
|
|
41
41
|
# tag_list = TagList.new("Round", "Square,Cube")
|
|
42
42
|
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
43
43
|
def to_s
|
|
44
|
-
|
|
44
|
+
tags = frozen? ? self.dup : self
|
|
45
|
+
tags.send(:clean!)
|
|
45
46
|
|
|
46
|
-
map do |name|
|
|
47
|
+
tags.map do |name|
|
|
47
48
|
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
48
49
|
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
|
49
50
|
end
|
|
@@ -55,7 +56,7 @@ class TagList < Array
|
|
|
55
56
|
map!(&:strip)
|
|
56
57
|
uniq!
|
|
57
58
|
end
|
|
58
|
-
|
|
59
|
+
|
|
59
60
|
def extract_and_apply_options!(args)
|
|
60
61
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
61
62
|
options.assert_valid_keys :parse
|
|
@@ -79,17 +80,11 @@ class TagList < Array
|
|
|
79
80
|
string = string.to_s.dup
|
|
80
81
|
|
|
81
82
|
# Parse the quoted tags
|
|
82
|
-
string.gsub!(/"(.*?)"\s
|
|
83
|
-
string.gsub!(/'(.*?)'\s
|
|
83
|
+
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
84
|
+
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
84
85
|
|
|
85
86
|
tag_list.add(string.split(delimiter))
|
|
86
87
|
end
|
|
87
88
|
end
|
|
88
|
-
|
|
89
|
-
def from_owner(owner, *tags)
|
|
90
|
-
returning from(*tags) do |taglist|
|
|
91
|
-
taglist.owner = owner
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
89
|
end
|
|
95
90
|
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
class Tagging < ActiveRecord::Base #:nodoc:
|
|
2
|
+
attr_accessible :tag, :tag_id, :context,
|
|
3
|
+
:taggable, :taggable_type, :taggable_id,
|
|
4
|
+
:tagger, :tagger_type, :tagger_id
|
|
5
|
+
|
|
2
6
|
belongs_to :tag
|
|
3
7
|
belongs_to :taggable, :polymorphic => true
|
|
4
8
|
belongs_to :tagger, :polymorphic => true
|
|
5
9
|
|
|
6
10
|
validates_presence_of :context
|
|
7
11
|
validates_presence_of :tag_id
|
|
12
|
+
|
|
13
|
+
validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
|
|
8
14
|
end
|
data/rails/init.rb
CHANGED
|
@@ -2,6 +2,4 @@ require 'acts-as-taggable-on'
|
|
|
2
2
|
|
|
3
3
|
ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
|
|
4
4
|
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
|
|
5
|
-
ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
|
|
6
|
-
|
|
7
|
-
RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
|
|
5
|
+
ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
|
|
@@ -176,16 +176,6 @@ describe "Acts As Taggable On" do
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
describe 'Tagging Contexts' do
|
|
179
|
-
before(:all) do
|
|
180
|
-
class Array
|
|
181
|
-
def freq
|
|
182
|
-
k=Hash.new(0)
|
|
183
|
-
self.each {|e| k[e]+=1}
|
|
184
|
-
k
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
179
|
it 'should eliminate duplicate tagging contexts ' do
|
|
190
180
|
TaggableModel.acts_as_taggable_on(:skills, :skills)
|
|
191
181
|
TaggableModel.tag_types.freq[:skills].should_not == 3
|
|
@@ -212,10 +202,6 @@ describe "Acts As Taggable On" do
|
|
|
212
202
|
TaggableModel.acts_as_taggable_on([nil])
|
|
213
203
|
}.should_not raise_error
|
|
214
204
|
end
|
|
215
|
-
|
|
216
|
-
after(:all) do
|
|
217
|
-
class Array; remove_method :freq; end
|
|
218
|
-
end
|
|
219
205
|
end
|
|
220
206
|
|
|
221
207
|
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
2
|
|
|
3
3
|
describe "acts_as_tagger" do
|
|
4
|
+
before(:each) do
|
|
5
|
+
[TaggableUser, TaggableModel, Tagging, Tag].each(&:destroy_all)
|
|
6
|
+
end
|
|
7
|
+
|
|
4
8
|
context "Tagger Method Generation" do
|
|
5
|
-
|
|
6
9
|
before(:each) do
|
|
7
10
|
@tagger = TaggableUser.new()
|
|
8
11
|
end
|
|
@@ -48,8 +51,22 @@ describe "acts_as_tagger" do
|
|
|
48
51
|
|
|
49
52
|
it 'should by default create the tag context on-the-fly' do
|
|
50
53
|
@taggable.tag_list_on(:here_ond_now).should be_empty
|
|
51
|
-
@tagger.tag(@taggable, :with=>'that', :on
|
|
52
|
-
@taggable.tag_list_on(:here_ond_now).
|
|
54
|
+
@tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
|
|
55
|
+
@taggable.tag_list_on(:here_ond_now).should_not include('that')
|
|
56
|
+
@taggable.all_tags_list_on(:here_ond_now).should include('that')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should show all the tag list when both public and owned tags exist" do
|
|
60
|
+
@taggable.tag_list = 'ruby, python'
|
|
61
|
+
@tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
|
|
62
|
+
@taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "should not add owned tags to the common list" do
|
|
66
|
+
@taggable.tag_list = 'ruby, python'
|
|
67
|
+
@tagger.tag(@taggable, :with => 'java, lisp', :on => :foo)
|
|
68
|
+
@tagger.tag(@taggable, :with => '', :on => :foo)
|
|
69
|
+
@taggable.tag_list.should == %w(ruby python)
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
it "should throw an exception when the default is over-ridden" do
|
|
@@ -64,9 +81,28 @@ describe "acts_as_tagger" do
|
|
|
64
81
|
@tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
|
|
65
82
|
@taggable.tag_list_on(:foo_boo).should be_empty
|
|
66
83
|
end
|
|
67
|
-
|
|
68
84
|
end
|
|
69
|
-
|
|
85
|
+
|
|
86
|
+
context "when called by multiple tagger's" do
|
|
87
|
+
before(:each) do
|
|
88
|
+
@user_x = TaggableUser.new
|
|
89
|
+
@user_y = TaggableUser.new
|
|
90
|
+
@taggable = TaggableModel.new(:name => 'acts_as_taggable_on', :tag_list => 'plugin')
|
|
91
|
+
|
|
92
|
+
@user_x.tag(@taggable, :with => 'ruby, rails', :on => :tags)
|
|
93
|
+
@user_y.tag(@taggable, :with => 'ruby, plugin', :on => :tags)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "should not delete other taggers tags" do
|
|
97
|
+
@user_y.tag(@taggable, :with => '', :on => :tags)
|
|
98
|
+
@taggable.all_tags_list_on(:tags).should include('ruby')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "should not delete original tags" do
|
|
102
|
+
@user_y.tag(@taggable, :with => '', :on => :tags)
|
|
103
|
+
@taggable.all_tags_list_on(:tags).should include('plugin')
|
|
104
|
+
end
|
|
105
|
+
end
|
|
70
106
|
end
|
|
71
107
|
|
|
72
108
|
end
|
|
@@ -20,6 +20,18 @@ describe TagList do
|
|
|
20
20
|
@tag_list.include?("wicked").should be_true
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
it "should be able to add delimited list of words with quoted delimiters" do
|
|
24
|
+
@tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
|
|
25
|
+
@tag_list.include?("cool, wicked").should be_true
|
|
26
|
+
@tag_list.include?("really cool, really wicked").should be_true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should be able to handle other uses of quotation marks correctly" do
|
|
30
|
+
@tag_list.add("john's cool car, mary's wicked toy", :parse => true)
|
|
31
|
+
@tag_list.include?("john's cool car").should be_true
|
|
32
|
+
@tag_list.include?("mary's wicked toy").should be_true
|
|
33
|
+
end
|
|
34
|
+
|
|
23
35
|
it "should be able to add an array of words" do
|
|
24
36
|
@tag_list.add(["cool", "wicked"], :parse => true)
|
|
25
37
|
@tag_list.include?("cool").should be_true
|
|
@@ -49,4 +61,10 @@ describe TagList do
|
|
|
49
61
|
@tag_list.add("cool","rad,bodacious")
|
|
50
62
|
@tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
|
|
51
63
|
end
|
|
64
|
+
|
|
65
|
+
it "should be able to call to_s on a frozen tag list" do
|
|
66
|
+
@tag_list.freeze
|
|
67
|
+
lambda { @tag_list.add("cool","rad,bodacious") }.should raise_error
|
|
68
|
+
lambda { @tag_list.to_s }.should_not raise_error
|
|
69
|
+
end
|
|
52
70
|
end
|
|
@@ -7,6 +7,17 @@ describe Tag do
|
|
|
7
7
|
Tag.delete_all
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
describe "named like any" do
|
|
11
|
+
before(:each) do
|
|
12
|
+
Tag.create(:name => "awesome")
|
|
13
|
+
Tag.create(:name => "epic")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "should find both tags" do
|
|
17
|
+
Tag.named_like_any(["awesome", "epic"]).should have(2).items
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
10
21
|
describe "find or create by name" do
|
|
11
22
|
before(:each) do
|
|
12
23
|
@tag.name = "awesome"
|
|
@@ -27,6 +38,33 @@ describe Tag do
|
|
|
27
38
|
}.should change(Tag, :count).by(1)
|
|
28
39
|
end
|
|
29
40
|
end
|
|
41
|
+
|
|
42
|
+
describe "find or create all by any name" do
|
|
43
|
+
before(:each) do
|
|
44
|
+
@tag.name = "awesome"
|
|
45
|
+
@tag.save
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "should find by name" do
|
|
49
|
+
Tag.find_or_create_all_with_like_by_name("awesome").should == [@tag]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "should find by name case insensitive" do
|
|
53
|
+
Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "should create by name" do
|
|
57
|
+
lambda {
|
|
58
|
+
Tag.find_or_create_all_with_like_by_name("epic")
|
|
59
|
+
}.should change(Tag, :count).by(1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "should find or create by name" do
|
|
63
|
+
lambda {
|
|
64
|
+
Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name).should == ["awesome", "epic"]
|
|
65
|
+
}.should change(Tag, :count).by(1)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
30
68
|
|
|
31
69
|
it "should require a name" do
|
|
32
70
|
@tag.valid?
|
|
@@ -7,44 +7,49 @@ describe "Taggable" do
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
it "should have tag types" do
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
for type in [:tags, :languages, :skills, :needs, :offerings]
|
|
11
|
+
TaggableModel.tag_types.should include type
|
|
12
|
+
end
|
|
13
|
+
@taggable.tag_types.should == TaggableModel.tag_types
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
it "should have tag_counts_on" do
|
|
15
17
|
TaggableModel.tag_counts_on(:tags).should be_empty
|
|
16
|
-
|
|
18
|
+
|
|
17
19
|
@taggable.tag_list = ["awesome", "epic"]
|
|
18
20
|
@taggable.save
|
|
19
21
|
|
|
20
22
|
TaggableModel.tag_counts_on(:tags).count.should == 2
|
|
21
23
|
@taggable.tag_counts_on(:tags).count.should == 2
|
|
22
24
|
end
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
it "should be able to create tags" do
|
|
25
27
|
@taggable.skill_list = "ruby, rails, css"
|
|
26
|
-
@taggable.instance_variable_get("@skill_list").instance_of?(
|
|
28
|
+
@taggable.instance_variable_get("@skill_list").instance_of?(Hash).should be_true
|
|
29
|
+
@taggable.instance_variable_get("@skill_list")[nil].instance_of?(TagList).should be_true
|
|
27
30
|
@taggable.save
|
|
28
|
-
|
|
31
|
+
|
|
29
32
|
Tag.find(:all).size.should == 3
|
|
30
33
|
end
|
|
31
|
-
|
|
34
|
+
|
|
32
35
|
it "should be able to create tags through the tag list directly" do
|
|
33
36
|
@taggable.tag_list_on(:test).add("hello")
|
|
34
|
-
@taggable.
|
|
37
|
+
@taggable.tag_list_cache_on(:test).should_not be_empty
|
|
38
|
+
@taggable.save
|
|
39
|
+
@taggable.save_tags
|
|
35
40
|
@taggable.reload
|
|
36
41
|
@taggable.tag_list_on(:test).should == ["hello"]
|
|
37
42
|
end
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
it "should differentiate between contexts" do
|
|
40
45
|
@taggable.skill_list = "ruby, rails, css"
|
|
41
46
|
@taggable.tag_list = "ruby, bob, charlie"
|
|
42
47
|
@taggable.save
|
|
43
48
|
@taggable.reload
|
|
44
|
-
@taggable.skill_list.include
|
|
45
|
-
@taggable.skill_list.include
|
|
49
|
+
@taggable.skill_list.should include("ruby")
|
|
50
|
+
@taggable.skill_list.should_not include("bob")
|
|
46
51
|
end
|
|
47
|
-
|
|
52
|
+
|
|
48
53
|
it "should be able to remove tags through list alone" do
|
|
49
54
|
@taggable.skill_list = "ruby, rails, css"
|
|
50
55
|
@taggable.save
|
|
@@ -55,13 +60,13 @@ describe "Taggable" do
|
|
|
55
60
|
@taggable.reload
|
|
56
61
|
@taggable.should have(2).skills
|
|
57
62
|
end
|
|
58
|
-
|
|
63
|
+
|
|
59
64
|
it "should be able to find by tag" do
|
|
60
65
|
@taggable.skill_list = "ruby, rails, css"
|
|
61
66
|
@taggable.save
|
|
62
67
|
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
|
63
68
|
end
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
it "should be able to find by tag with context" do
|
|
66
71
|
@taggable.skill_list = "ruby, rails, css"
|
|
67
72
|
@taggable.tag_list = "bob, charlie"
|
|
@@ -70,25 +75,27 @@ describe "Taggable" do
|
|
|
70
75
|
TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
71
76
|
TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
|
|
72
77
|
end
|
|
73
|
-
|
|
78
|
+
|
|
74
79
|
it "should be able to use the tagged_with named scope" do
|
|
75
80
|
@taggable.skill_list = "ruby, rails, css"
|
|
76
81
|
@taggable.tag_list = "bob, charlie"
|
|
77
82
|
@taggable.save
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
TaggableModel.tagged_with("ruby").first.should == @taggable
|
|
85
|
+
TaggableModel.tagged_with("ruby, css").first.should == @taggable
|
|
86
|
+
TaggableModel.tagged_with("ruby, nonexistingtag").should be_empty
|
|
80
87
|
TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
81
88
|
TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
|
|
82
89
|
end
|
|
83
|
-
|
|
90
|
+
|
|
84
91
|
it "should not care about case" do
|
|
85
92
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
|
|
86
93
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
|
|
87
|
-
|
|
94
|
+
|
|
88
95
|
Tag.find(:all).size.should == 1
|
|
89
96
|
TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
|
|
90
97
|
end
|
|
91
|
-
|
|
98
|
+
|
|
92
99
|
it "should be able to get tag counts on model as a whole" do
|
|
93
100
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
94
101
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
@@ -96,33 +103,39 @@ describe "Taggable" do
|
|
|
96
103
|
TaggableModel.tag_counts.should_not be_empty
|
|
97
104
|
TaggableModel.skill_counts.should_not be_empty
|
|
98
105
|
end
|
|
99
|
-
|
|
106
|
+
|
|
100
107
|
it "should be able to get all tag counts on model as whole" do
|
|
101
108
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
102
109
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
103
110
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
104
|
-
|
|
111
|
+
|
|
105
112
|
TaggableModel.all_tag_counts.should_not be_empty
|
|
106
113
|
TaggableModel.all_tag_counts.first.count.should == 3 # ruby
|
|
107
114
|
end
|
|
108
|
-
|
|
115
|
+
|
|
116
|
+
it "should not return read-only records" do
|
|
117
|
+
TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
118
|
+
|
|
119
|
+
TaggableModel.tagged_with("ruby").first.should_not be_readonly
|
|
120
|
+
end
|
|
121
|
+
|
|
109
122
|
it "should be able to get scoped tag counts" do
|
|
110
123
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
111
124
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
112
125
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
113
|
-
|
|
126
|
+
|
|
114
127
|
TaggableModel.tagged_with("ruby").tag_counts.first.count.should == 2 # ruby
|
|
115
128
|
TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
|
|
116
129
|
end
|
|
117
|
-
|
|
130
|
+
|
|
118
131
|
it "should be able to get all scoped tag counts" do
|
|
119
132
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
120
133
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
121
134
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
122
|
-
|
|
135
|
+
|
|
123
136
|
TaggableModel.tagged_with("ruby").all_tag_counts.first.count.should == 3 # ruby
|
|
124
137
|
end
|
|
125
|
-
|
|
138
|
+
|
|
126
139
|
it "should be able to set a custom tag context list" do
|
|
127
140
|
bob = TaggableModel.create(:name => "Bob")
|
|
128
141
|
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
@@ -131,17 +144,27 @@ describe "Taggable" do
|
|
|
131
144
|
bob.reload
|
|
132
145
|
bob.tags_on(:rotors).should_not be_empty
|
|
133
146
|
end
|
|
134
|
-
|
|
147
|
+
|
|
135
148
|
it "should be able to find tagged" do
|
|
136
149
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
|
137
150
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
|
138
151
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
|
|
139
|
-
|
|
152
|
+
|
|
140
153
|
TaggableModel.find_tagged_with("ruby", :order => 'taggable_models.name').should == [bob, frank, steve]
|
|
141
154
|
TaggableModel.find_tagged_with("ruby, rails", :order => 'taggable_models.name').should == [bob, frank]
|
|
142
155
|
TaggableModel.find_tagged_with(["ruby", "rails"], :order => 'taggable_models.name').should == [bob, frank]
|
|
143
156
|
end
|
|
144
|
-
|
|
157
|
+
|
|
158
|
+
it "should be able to find tagged with any tag" do
|
|
159
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
|
160
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
|
161
|
+
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
|
|
162
|
+
|
|
163
|
+
TaggableModel.find_tagged_with(["ruby", "java"], :order => 'taggable_models.name', :any => true).should == [bob, frank, steve]
|
|
164
|
+
TaggableModel.find_tagged_with(["c++", "fitter"], :order => 'taggable_models.name', :any => true).should == [bob, steve]
|
|
165
|
+
TaggableModel.find_tagged_with(["depressed", "css"], :order => 'taggable_models.name', :any => true).should == [bob, frank]
|
|
166
|
+
end
|
|
167
|
+
|
|
145
168
|
it "should be able to find tagged on a custom tag context" do
|
|
146
169
|
bob = TaggableModel.create(:name => "Bob")
|
|
147
170
|
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
@@ -154,29 +177,38 @@ describe "Taggable" do
|
|
|
154
177
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
|
155
178
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
|
156
179
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
|
|
157
|
-
|
|
180
|
+
|
|
158
181
|
# Let's only find those productive Rails developers
|
|
159
182
|
TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').should == [bob, frank]
|
|
160
183
|
TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').should == [bob, steve]
|
|
161
184
|
TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).should == [bob]
|
|
162
185
|
TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).should == [bob]
|
|
163
186
|
end
|
|
164
|
-
|
|
187
|
+
|
|
165
188
|
it "should be able to find tagged with only the matching tags" do
|
|
166
189
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
|
|
167
190
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
|
|
168
191
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
|
|
169
|
-
|
|
192
|
+
|
|
170
193
|
TaggableModel.find_tagged_with("fitter, happier", :match_all => true).should == [steve]
|
|
171
194
|
end
|
|
172
|
-
|
|
195
|
+
|
|
173
196
|
it "should be able to find tagged with some excluded tags" do
|
|
174
197
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
|
|
175
198
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
|
|
176
199
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
|
|
177
|
-
|
|
200
|
+
|
|
178
201
|
TaggableModel.find_tagged_with("lazy", :exclude => true).should == [frank, steve]
|
|
179
202
|
end
|
|
203
|
+
|
|
204
|
+
it "should not create duplicate taggings" do
|
|
205
|
+
bob = TaggableModel.create(:name => "Bob")
|
|
206
|
+
lambda {
|
|
207
|
+
bob.tag_list << "happier"
|
|
208
|
+
bob.tag_list << "happier"
|
|
209
|
+
bob.save
|
|
210
|
+
}.should change(Tagging, :count).by(1)
|
|
211
|
+
end
|
|
180
212
|
|
|
181
213
|
describe "Single Table Inheritance" do
|
|
182
214
|
before do
|
|
@@ -185,32 +217,32 @@ describe "Taggable" do
|
|
|
185
217
|
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
|
186
218
|
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
|
187
219
|
end
|
|
188
|
-
|
|
220
|
+
|
|
189
221
|
it "should be able to save tags for inherited models" do
|
|
190
222
|
@inherited_same.tag_list = "bob, kelso"
|
|
191
223
|
@inherited_same.save
|
|
192
224
|
InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
193
225
|
end
|
|
194
|
-
|
|
226
|
+
|
|
195
227
|
it "should find STI tagged models on the superclass" do
|
|
196
228
|
@inherited_same.tag_list = "bob, kelso"
|
|
197
229
|
@inherited_same.save
|
|
198
230
|
TaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
199
231
|
end
|
|
200
|
-
|
|
232
|
+
|
|
201
233
|
it "should be able to add on contexts only to some subclasses" do
|
|
202
234
|
@inherited_different.part_list = "fork, spoon"
|
|
203
235
|
@inherited_different.save
|
|
204
236
|
InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
|
|
205
237
|
AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
|
|
206
238
|
end
|
|
207
|
-
|
|
239
|
+
|
|
208
240
|
it "should have different tag_counts_on for inherited models" do
|
|
209
241
|
@inherited_same.tag_list = "bob, kelso"
|
|
210
242
|
@inherited_same.save!
|
|
211
243
|
@inherited_different.tag_list = "fork, spoon"
|
|
212
244
|
@inherited_different.save!
|
|
213
|
-
|
|
245
|
+
|
|
214
246
|
InheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso)
|
|
215
247
|
AlteredInheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(fork spoon)
|
|
216
248
|
TaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso fork spoon)
|
|
@@ -6,17 +6,32 @@ describe "Tagger" do
|
|
|
6
6
|
@user = TaggableUser.new
|
|
7
7
|
@taggable = TaggableModel.new(:name => "Bob Jones")
|
|
8
8
|
end
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
it "should have taggings" do
|
|
11
11
|
@user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
|
|
12
12
|
@user.owned_taggings.size == 2
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
it "should have tags" do
|
|
16
16
|
@user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
|
|
17
17
|
@user.owned_tags.size == 2
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
it "should not overlap or lose tags from different users" do
|
|
21
|
+
@user2 = TaggableUser.new
|
|
22
|
+
lambda{
|
|
23
|
+
@user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
|
|
24
|
+
@user2.tag(@taggable, :with => 'java, python, lisp, ruby', :on => :tags)
|
|
25
|
+
}.should change(Tagging, :count).by(6)
|
|
26
|
+
|
|
27
|
+
@user.owned_tags.map(&:name).sort.should == %w(ruby scheme)
|
|
28
|
+
@user2.owned_tags.map(&:name).sort.should == %w(java python lisp ruby).sort
|
|
29
|
+
@taggable.tags_from(@user).sort.should == %w(ruby scheme)
|
|
30
|
+
@taggable.tags_from(@user2).sort.should == %w(java lisp python ruby)
|
|
31
|
+
@taggable.all_tags_list_on(:tags).sort.should == %w(ruby scheme java python lisp).sort
|
|
32
|
+
@taggable.all_tags_on(:tags).size.should == 6
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
it "is tagger" do
|
|
21
36
|
@user.is_tagger?.should(be_true)
|
|
22
37
|
end
|
|
@@ -13,4 +13,13 @@ describe Tagging do
|
|
|
13
13
|
@tagging.should_not be_valid
|
|
14
14
|
@tagging.errors.on(:tag_id).should == "can't be blank"
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
it "should not create duplicate taggings" do
|
|
18
|
+
@taggable = TaggableModel.create(:name => "Bob Jones")
|
|
19
|
+
@tag = Tag.create(:name => "awesome")
|
|
20
|
+
|
|
21
|
+
lambda {
|
|
22
|
+
2.times { Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags') }
|
|
23
|
+
}.should change(Tagging, :count).by(1)
|
|
24
|
+
end
|
|
16
25
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
|
|
2
2
|
require 'rubygems'
|
|
3
|
-
require '
|
|
3
|
+
require 'active_record'
|
|
4
4
|
require 'spec'
|
|
5
5
|
|
|
6
6
|
module Spec::Example::ExampleGroupMethods
|
|
7
7
|
alias :context :describe
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
class Array
|
|
11
|
+
def freq
|
|
12
|
+
k=Hash.new(0)
|
|
13
|
+
each {|e| k[e]+=1}
|
|
14
|
+
k
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
TEST_DATABASE_FILE = File.join(File.dirname(__FILE__), '..', 'test.sqlite3')
|
|
11
19
|
|
|
12
20
|
File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
|
|
@@ -16,7 +24,10 @@ ActiveRecord::Base.establish_connection(
|
|
|
16
24
|
|
|
17
25
|
RAILS_DEFAULT_LOGGER = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
ActiveRecord::Base.silence do
|
|
28
|
+
ActiveRecord::Migration.verbose = false
|
|
29
|
+
load(File.dirname(__FILE__) + '/schema.rb')
|
|
30
|
+
end
|
|
20
31
|
|
|
21
32
|
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
|
22
33
|
require File.join(File.dirname(__FILE__), '..', 'init')
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acts-as-taggable-on
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Bleigh
|
|
@@ -9,7 +9,7 @@ autorequire:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
|
|
12
|
-
date:
|
|
12
|
+
date: 2010-02-06 00:00:00 +01:00
|
|
13
13
|
default_executable:
|
|
14
14
|
dependencies: []
|
|
15
15
|
|
|
@@ -27,6 +27,8 @@ files:
|
|
|
27
27
|
- README.rdoc
|
|
28
28
|
- Rakefile
|
|
29
29
|
- VERSION
|
|
30
|
+
- generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
|
|
31
|
+
- generators/acts_as_taggable_on_migration/templates/migration.rb
|
|
30
32
|
- lib/acts-as-taggable-on.rb
|
|
31
33
|
- lib/acts_as_taggable_on/acts_as_taggable_on.rb
|
|
32
34
|
- lib/acts_as_taggable_on/acts_as_tagger.rb
|