acts-as-taggable-on 1.0.11 → 1.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/README.rdoc +41 -0
- data/Rakefile +7 -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.rb +1 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +120 -68
- data/lib/acts_as_taggable_on/group_helper.rb +12 -0
- data/lib/acts_as_taggable_on/tag.rb +16 -1
- data/lib/acts_as_taggable_on/tag_list.rb +6 -11
- data/lib/acts_as_taggable_on/tagging.rb +8 -0
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -0
- data/rails/init.rb +1 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +55 -21
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +16 -2
- data/spec/acts_as_taggable_on/group_helper_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +46 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +81 -34
- data/spec/acts_as_taggable_on/tagger_spec.rb +17 -2
- data/spec/acts_as_taggable_on/tagging_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +30 -0
- data/spec/spec_helper.rb +17 -3
- metadata +9 -2
data/README.rdoc
CHANGED
|
@@ -137,6 +137,46 @@ Tags can have owners:
|
|
|
137
137
|
@some_user.owned_taggings
|
|
138
138
|
@some_user.owned_tags
|
|
139
139
|
@some_photo.locations_from(@some_user)
|
|
140
|
+
|
|
141
|
+
=== Tag cloud calculations
|
|
142
|
+
|
|
143
|
+
To construct tag clouds, the frequency of each tag needs to be calculated.
|
|
144
|
+
Because we specified +acts_as_taggable_on+ on the <tt>User</tt> class, we can
|
|
145
|
+
get a calculation of all the tag counts by using <tt>User.tag_counts_on(:customs)</tt>. But what if we wanted a tag count for
|
|
146
|
+
an single user's posts? To achieve this we call tag_counts on the association:
|
|
147
|
+
|
|
148
|
+
User.find(:first).posts.tag_counts_on(:tags)
|
|
149
|
+
|
|
150
|
+
A helper is included to assist with generating tag clouds.
|
|
151
|
+
|
|
152
|
+
Here is an example that generates a tag cloud.
|
|
153
|
+
|
|
154
|
+
Helper:
|
|
155
|
+
|
|
156
|
+
module PostsHelper
|
|
157
|
+
include TagsHelper
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
Controller:
|
|
161
|
+
|
|
162
|
+
class PostController < ApplicationController
|
|
163
|
+
def tag_cloud
|
|
164
|
+
@tags = Post.tag_counts_on(:tags)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
View:
|
|
169
|
+
|
|
170
|
+
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
|
|
171
|
+
<%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
|
|
172
|
+
<% end %>
|
|
173
|
+
|
|
174
|
+
CSS:
|
|
175
|
+
|
|
176
|
+
.css1 { font-size: 1.0em; }
|
|
177
|
+
.css2 { font-size: 1.2em; }
|
|
178
|
+
.css3 { font-size: 1.4em; }
|
|
179
|
+
.css4 { font-size: 1.6em; }
|
|
140
180
|
|
|
141
181
|
== Contributors
|
|
142
182
|
|
|
@@ -154,5 +194,6 @@ Tags can have owners:
|
|
|
154
194
|
* slainer68 - STI fix
|
|
155
195
|
* harrylove - migration instructions and fix-ups
|
|
156
196
|
* lawrencepit - cached tag work
|
|
197
|
+
* sobrinho - fixed tag_cloud helper
|
|
157
198
|
|
|
158
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
|
|
@@ -21,3 +21,9 @@ task :default => :spec
|
|
|
21
21
|
Spec::Rake::SpecTask.new do |t|
|
|
22
22
|
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
|
26
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
|
27
|
+
t.rcov = true
|
|
28
|
+
t.rcov_opts = ['--exclude', 'spec']
|
|
29
|
+
end
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0
|
|
1
|
+
1.1.0
|
|
@@ -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
|
data/lib/acts-as-taggable-on.rb
CHANGED
|
@@ -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
|
|
@@ -65,6 +65,14 @@ module ActiveRecord
|
|
|
65
65
|
related_tags_for('#{tag_type}', klass, options)
|
|
66
66
|
end
|
|
67
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
|
+
|
|
68
76
|
def top_#{tag_type}(limit = 10)
|
|
69
77
|
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
|
70
78
|
end
|
|
@@ -74,11 +82,10 @@ module ActiveRecord
|
|
|
74
82
|
end
|
|
75
83
|
RUBY
|
|
76
84
|
end
|
|
77
|
-
|
|
78
85
|
if respond_to?(:tag_types)
|
|
79
86
|
write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
|
|
80
87
|
else
|
|
81
|
-
|
|
88
|
+
class_eval do
|
|
82
89
|
write_inheritable_attribute(:tag_types, args.uniq)
|
|
83
90
|
class_inheritable_reader :tag_types
|
|
84
91
|
|
|
@@ -102,16 +109,14 @@ module ActiveRecord
|
|
|
102
109
|
alias_method_chain :reload, :tag_list
|
|
103
110
|
end
|
|
104
111
|
end
|
|
105
|
-
|
|
106
|
-
def is_taggable?
|
|
107
|
-
false
|
|
108
|
-
end
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
module SingletonMethods
|
|
115
|
+
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
112
116
|
# Pass either a tag string, or an array of strings or tags
|
|
113
117
|
#
|
|
114
118
|
# Options:
|
|
119
|
+
# :any - find models that match any of the given tags
|
|
115
120
|
# :exclude - Find models that are not tagged with the given tags
|
|
116
121
|
# :match_all - Find models that match all of the given tags, not just one
|
|
117
122
|
# :conditions - A piece of SQL conditions to add to the query
|
|
@@ -134,9 +139,9 @@ module ActiveRecord
|
|
|
134
139
|
end
|
|
135
140
|
|
|
136
141
|
def find_options_for_find_tagged_with(tags, options = {})
|
|
137
|
-
|
|
142
|
+
tag_list = TagList.from(tags)
|
|
138
143
|
|
|
139
|
-
return {} if
|
|
144
|
+
return {} if tag_list.empty?
|
|
140
145
|
|
|
141
146
|
joins = []
|
|
142
147
|
conditions = []
|
|
@@ -145,28 +150,30 @@ module ActiveRecord
|
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
if options.delete(:exclude)
|
|
148
|
-
tags_conditions =
|
|
153
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
149
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)})"
|
|
150
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
|
+
|
|
151
160
|
else
|
|
161
|
+
tags = Tag.named_like_any(tag_list)
|
|
162
|
+
return { :conditions => "1 = 0" } unless tags.length == tag_list.length
|
|
163
|
+
|
|
152
164
|
tags.each do |tag|
|
|
153
|
-
safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
|
|
165
|
+
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
|
154
166
|
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
155
167
|
|
|
156
168
|
taggings_alias = "#{table_name}_taggings_#{prefix}"
|
|
157
|
-
tags_alias = "#{table_name}_tags_#{prefix}"
|
|
158
169
|
|
|
159
170
|
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
160
171
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
161
|
-
" 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}"
|
|
162
174
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
163
175
|
|
|
164
|
-
tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
|
|
165
|
-
" ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
|
|
166
|
-
" AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
|
|
167
|
-
|
|
168
176
|
joins << tagging_join
|
|
169
|
-
joins << tag_join
|
|
170
177
|
end
|
|
171
178
|
end
|
|
172
179
|
|
|
@@ -177,12 +184,13 @@ module ActiveRecord
|
|
|
177
184
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
178
185
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
179
186
|
|
|
180
|
-
group = "#{
|
|
187
|
+
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
181
188
|
end
|
|
182
189
|
|
|
183
190
|
{ :joins => joins.join(" "),
|
|
184
191
|
:group => group,
|
|
185
|
-
:conditions => conditions.join(" AND ")
|
|
192
|
+
:conditions => conditions.join(" AND "),
|
|
193
|
+
:readonly => false }.update(options)
|
|
186
194
|
end
|
|
187
195
|
|
|
188
196
|
# Calculate the tag counts for all tags.
|
|
@@ -220,19 +228,37 @@ module ActiveRecord
|
|
|
220
228
|
|
|
221
229
|
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
|
222
230
|
joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
|
223
|
-
|
|
224
231
|
joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
|
225
|
-
|
|
232
|
+
|
|
233
|
+
unless descends_from_active_record?
|
|
226
234
|
# Current model is STI descendant, so add type checking to the join condition
|
|
227
|
-
joins << " AND #{table_name}.#{
|
|
235
|
+
joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
|
228
236
|
end
|
|
229
237
|
|
|
230
|
-
|
|
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
|
|
231
257
|
|
|
232
258
|
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
|
233
259
|
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
|
234
260
|
having = [at_least, at_most].compact.join(' AND ')
|
|
235
|
-
group_by = "#{
|
|
261
|
+
group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
|
|
236
262
|
group_by << " AND #{having}" unless having.blank?
|
|
237
263
|
|
|
238
264
|
{ :select => "#{Tag.table_name}.*, COUNT(*) AS count",
|
|
@@ -247,21 +273,10 @@ module ActiveRecord
|
|
|
247
273
|
def is_taggable?
|
|
248
274
|
true
|
|
249
275
|
end
|
|
250
|
-
|
|
251
|
-
def column_names_for_tag_group
|
|
252
|
-
Tag.column_names.map { |column| "#{Tag.table_name}.#{column}" }.join(", ")
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def column_names_for_tagging_group
|
|
256
|
-
column_names.map { |column| "#{table_name}.#{column}" }.join(", ")
|
|
257
|
-
end
|
|
258
276
|
end
|
|
259
277
|
|
|
260
278
|
module InstanceMethods
|
|
261
|
-
|
|
262
|
-
def tag_types
|
|
263
|
-
self.class.tag_types
|
|
264
|
-
end
|
|
279
|
+
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
265
280
|
|
|
266
281
|
def custom_contexts
|
|
267
282
|
@custom_contexts ||= []
|
|
@@ -270,29 +285,40 @@ module ActiveRecord
|
|
|
270
285
|
def is_taggable?
|
|
271
286
|
self.class.is_taggable?
|
|
272
287
|
end
|
|
273
|
-
|
|
288
|
+
|
|
274
289
|
def add_custom_context(value)
|
|
275
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)
|
|
276
291
|
end
|
|
277
292
|
|
|
278
|
-
def tag_list_on(context, owner=nil)
|
|
279
|
-
var_name = context.to_s.singularize + "_list"
|
|
293
|
+
def tag_list_on(context, owner = nil)
|
|
280
294
|
add_custom_context(context)
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
cache = tag_list_cache_on(context)
|
|
296
|
+
return owner ? cache[owner] : cache[owner] if cache[owner]
|
|
297
|
+
|
|
283
298
|
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
|
|
284
|
-
|
|
299
|
+
cache[owner] = TagList.from(cached_tag_list_on(context))
|
|
285
300
|
else
|
|
286
|
-
|
|
301
|
+
cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
|
|
287
302
|
end
|
|
288
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
|
|
289
315
|
|
|
290
|
-
def tags_on(context, owner=nil)
|
|
316
|
+
def tags_on(context, owner = nil)
|
|
291
317
|
if owner
|
|
292
318
|
opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
|
|
293
319
|
context.to_s, owner.id, owner.class.to_s]}
|
|
294
320
|
else
|
|
295
|
-
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]}
|
|
296
322
|
end
|
|
297
323
|
base_tags.find(:all, opts)
|
|
298
324
|
end
|
|
@@ -300,14 +326,21 @@ module ActiveRecord
|
|
|
300
326
|
def cached_tag_list_on(context)
|
|
301
327
|
self["cached_#{context.to_s.singularize}_list"]
|
|
302
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
|
|
303
336
|
|
|
304
|
-
def set_tag_list_on(context,new_list, tagger=nil)
|
|
305
|
-
|
|
337
|
+
def set_tag_list_on(context, new_list, tagger = nil)
|
|
338
|
+
tag_list_cache_on(context)[tagger] = TagList.from(new_list)
|
|
306
339
|
add_custom_context(context)
|
|
307
340
|
end
|
|
308
341
|
|
|
309
342
|
def tag_counts_on(context, options={})
|
|
310
|
-
self.class.tag_counts_on(context, options.merge(:id =>
|
|
343
|
+
self.class.tag_counts_on(context, options.merge(:id => id))
|
|
311
344
|
end
|
|
312
345
|
|
|
313
346
|
def related_tags_for(context, klass, options = {})
|
|
@@ -317,39 +350,58 @@ module ActiveRecord
|
|
|
317
350
|
end
|
|
318
351
|
|
|
319
352
|
def related_search_options(context, klass, options = {})
|
|
320
|
-
tags_to_find =
|
|
353
|
+
tags_to_find = tags_on(context).collect { |t| t.name }
|
|
321
354
|
|
|
322
|
-
exclude_self = "#{klass.table_name}.id != #{
|
|
355
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
323
356
|
|
|
324
357
|
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
325
358
|
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
326
359
|
: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],
|
|
327
|
-
:group =>
|
|
360
|
+
:group => grouped_column_names_for(klass),
|
|
328
361
|
:order => "count DESC"
|
|
329
362
|
}.update(options)
|
|
330
363
|
end
|
|
364
|
+
|
|
365
|
+
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
366
|
+
search_conditions = matching_context_search_options(search_context, result_context, klass, options)
|
|
367
|
+
|
|
368
|
+
klass.find(:all, search_conditions)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def matching_context_search_options(search_context, result_context, klass, options = {})
|
|
372
|
+
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
|
331
373
|
|
|
374
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
375
|
+
|
|
376
|
+
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
377
|
+
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
378
|
+
: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 (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
|
|
379
|
+
:group => grouped_column_names_for(klass),
|
|
380
|
+
:order => "count DESC"
|
|
381
|
+
}.update(options)
|
|
382
|
+
end
|
|
383
|
+
|
|
332
384
|
def save_cached_tag_list
|
|
333
385
|
self.class.tag_types.map(&:to_s).each do |tag_type|
|
|
334
386
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|
|
335
|
-
self["cached_#{tag_type.singularize}_list"] =
|
|
387
|
+
self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).tags.join(', ')
|
|
336
388
|
end
|
|
337
389
|
end
|
|
338
390
|
end
|
|
339
391
|
|
|
340
392
|
def save_tags
|
|
341
393
|
(custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
|
|
342
|
-
|
|
343
|
-
owner
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
394
|
+
tag_list_cache = tag_list_cache_on(tag_type)
|
|
395
|
+
for owner, tag_list in tag_list_cache
|
|
396
|
+
new_tag_names = tag_list - tags_on(tag_type, owner).map(&:name)
|
|
397
|
+
old_tags = tags_on(tag_type, owner).reject { |tag| tag_list.include?(tag.name) }
|
|
398
|
+
transaction do
|
|
399
|
+
base_tags.delete(*old_tags) if old_tags.any?
|
|
400
|
+
new_tag_names.each do |new_tag_name|
|
|
401
|
+
new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
|
|
402
|
+
Tagging.create(:tag_id => new_tag.id, :context => tag_type,
|
|
403
|
+
:taggable => self, :tagger => owner)
|
|
404
|
+
end
|
|
353
405
|
end
|
|
354
406
|
end
|
|
355
407
|
end
|
|
@@ -359,7 +411,7 @@ module ActiveRecord
|
|
|
359
411
|
|
|
360
412
|
def reload_with_tag_list(*args)
|
|
361
413
|
self.class.tag_types.each do |tag_type|
|
|
362
|
-
|
|
414
|
+
instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
|
363
415
|
end
|
|
364
416
|
|
|
365
417
|
reload_without_tag_list(*args)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module Acts
|
|
3
|
+
module TaggableOn
|
|
4
|
+
module GroupHelper
|
|
5
|
+
# all column names are necessary for PostgreSQL group clause
|
|
6
|
+
def grouped_column_names_for(object)
|
|
7
|
+
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
class Tag < ActiveRecord::Base
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
attr_accessible :name
|
|
4
|
+
|
|
5
|
+
### ASSOCIATIONS:
|
|
6
|
+
|
|
7
|
+
has_many :taggings, :dependent => :destroy
|
|
8
|
+
|
|
9
|
+
### VALIDATIONS:
|
|
3
10
|
|
|
4
11
|
validates_presence_of :name
|
|
5
12
|
validates_uniqueness_of :name
|
|
6
13
|
|
|
14
|
+
### NAMED SCOPES:
|
|
15
|
+
|
|
16
|
+
named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
|
|
17
|
+
named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
|
|
18
|
+
named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
|
|
19
|
+
|
|
20
|
+
### METHODS:
|
|
21
|
+
|
|
7
22
|
# LIKE is used for cross-database case-insensitivity
|
|
8
23
|
def self.find_or_create_with_like_by_name(name)
|
|
9
24
|
find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
|
|
@@ -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,6 +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
|
|
9
|
+
|
|
5
10
|
validates_presence_of :context
|
|
11
|
+
validates_presence_of :tag_id
|
|
12
|
+
|
|
13
|
+
validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
|
|
6
14
|
end
|
data/rails/init.rb
CHANGED
|
@@ -2,5 +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
|
-
|
|
6
|
-
RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
|
|
5
|
+
ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
|
|
@@ -18,6 +18,10 @@ describe "Acts As Taggable On" do
|
|
|
18
18
|
it "should create a class attribute for tag types" do
|
|
19
19
|
@taggable.class.should respond_to(:tag_types)
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
it "should create an instance attribute for tag types" do
|
|
23
|
+
@taggable.should respond_to(:tag_types)
|
|
24
|
+
end
|
|
21
25
|
|
|
22
26
|
it "should generate an association for each tag type" do
|
|
23
27
|
@taggable.should respond_to(:tags, :skills, :languages)
|
|
@@ -40,14 +44,6 @@ describe "Acts As Taggable On" do
|
|
|
40
44
|
@taggable.should respond_to(:tag_list, :skill_list, :language_list)
|
|
41
45
|
@taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
|
|
42
46
|
end
|
|
43
|
-
|
|
44
|
-
it "should return all column names joined for Tag GROUP clause" do
|
|
45
|
-
TaggableModel.column_names_for_tag_group.should == "tags.id, tags.name"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "should return all column names joined for TaggableModel GROUP clause" do
|
|
49
|
-
TaggableModel.column_names_for_tagging_group.should == "taggable_models.id, taggable_models.name, taggable_models.type"
|
|
50
|
-
end
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
describe "Single Table Inheritance" do
|
|
@@ -127,17 +123,59 @@ describe "Acts As Taggable On" do
|
|
|
127
123
|
end
|
|
128
124
|
end
|
|
129
125
|
|
|
130
|
-
describe
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
describe "Matching Contexts" do
|
|
127
|
+
it "should find objects with tags of matching contexts" do
|
|
128
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
|
129
|
+
taggable2 = TaggableModel.create!(:name => "Taggable 2")
|
|
130
|
+
taggable3 = TaggableModel.create!(:name => "Taggable 3")
|
|
131
|
+
|
|
132
|
+
taggable1.offering_list = "one, two"
|
|
133
|
+
taggable1.save!
|
|
134
|
+
|
|
135
|
+
taggable2.need_list = "one, two"
|
|
136
|
+
taggable2.save!
|
|
137
|
+
|
|
138
|
+
taggable3.offering_list = "one, two"
|
|
139
|
+
taggable3.save!
|
|
140
|
+
|
|
141
|
+
taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
|
|
142
|
+
taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
|
|
139
143
|
end
|
|
140
144
|
|
|
145
|
+
it "should find other related objects with tags of matching contexts" do
|
|
146
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
|
147
|
+
taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
|
|
148
|
+
taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
|
|
149
|
+
|
|
150
|
+
taggable1.offering_list = "one, two"
|
|
151
|
+
taggable1.save
|
|
152
|
+
|
|
153
|
+
taggable2.need_list = "one, two"
|
|
154
|
+
taggable2.save
|
|
155
|
+
|
|
156
|
+
taggable3.offering_list = "one, two"
|
|
157
|
+
taggable3.save
|
|
158
|
+
|
|
159
|
+
taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
|
|
160
|
+
taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "should not include the object itself in the list of related objects" do
|
|
164
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
|
165
|
+
taggable2 = TaggableModel.create!(:name => "Taggable 2")
|
|
166
|
+
|
|
167
|
+
taggable1.tag_list = "one"
|
|
168
|
+
taggable1.save
|
|
169
|
+
|
|
170
|
+
taggable2.tag_list = "one, two"
|
|
171
|
+
taggable2.save
|
|
172
|
+
|
|
173
|
+
taggable1.find_related_tags.should include(taggable2)
|
|
174
|
+
taggable1.find_related_tags.should_not include(taggable1)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe 'Tagging Contexts' do
|
|
141
179
|
it 'should eliminate duplicate tagging contexts ' do
|
|
142
180
|
TaggableModel.acts_as_taggable_on(:skills, :skills)
|
|
143
181
|
TaggableModel.tag_types.freq[:skills].should_not == 3
|
|
@@ -164,10 +202,6 @@ describe "Acts As Taggable On" do
|
|
|
164
202
|
TaggableModel.acts_as_taggable_on([nil])
|
|
165
203
|
}.should_not raise_error
|
|
166
204
|
end
|
|
167
|
-
|
|
168
|
-
after(:all) do
|
|
169
|
-
class Array; remove_method :freq; end
|
|
170
|
-
end
|
|
171
205
|
end
|
|
172
206
|
|
|
173
207
|
end
|
|
@@ -48,8 +48,22 @@ describe "acts_as_tagger" do
|
|
|
48
48
|
|
|
49
49
|
it 'should by default create the tag context on-the-fly' do
|
|
50
50
|
@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).
|
|
51
|
+
@tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
|
|
52
|
+
@taggable.tag_list_on(:here_ond_now).should_not include('that')
|
|
53
|
+
@taggable.all_tags_list_on(:here_ond_now).should include('that')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "should show all the tag list when both public and owned tags exist" do
|
|
57
|
+
@taggable.tag_list = 'ruby, python'
|
|
58
|
+
@tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
|
|
59
|
+
@taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "should not add owned tags to the common list" do
|
|
63
|
+
@taggable.tag_list = 'ruby, python'
|
|
64
|
+
@tagger.tag(@taggable, :with => 'java, lisp', :on => :foo)
|
|
65
|
+
@tagger.tag(@taggable, :with => '', :on => :foo)
|
|
66
|
+
@taggable.tag_list.should == %w(ruby python)
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
it "should throw an exception when the default is over-ridden" do
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Group Helper" do
|
|
4
|
+
|
|
5
|
+
describe "grouped_column_names_for method" do
|
|
6
|
+
before(:each) do
|
|
7
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "should return all column names joined for Tag GROUP clause" do
|
|
11
|
+
@taggable.grouped_column_names_for(Tag).should == "tags.id, tags.name"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "should return all column names joined for TaggableModel GROUP clause" do
|
|
15
|
+
@taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
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
|
|
@@ -4,8 +4,41 @@ describe Tag do
|
|
|
4
4
|
before(:each) do
|
|
5
5
|
@tag = Tag.new
|
|
6
6
|
@user = TaggableModel.create(:name => "Pablo")
|
|
7
|
+
Tag.delete_all
|
|
7
8
|
end
|
|
8
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
|
+
|
|
21
|
+
describe "find or create by name" do
|
|
22
|
+
before(:each) do
|
|
23
|
+
@tag.name = "awesome"
|
|
24
|
+
@tag.save
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should find by name" do
|
|
28
|
+
Tag.find_or_create_with_like_by_name("awesome").should == @tag
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should find by name case insensitive" do
|
|
32
|
+
Tag.find_or_create_with_like_by_name("AWESOME").should == @tag
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "should create by name" do
|
|
36
|
+
lambda {
|
|
37
|
+
Tag.find_or_create_with_like_by_name("epic")
|
|
38
|
+
}.should change(Tag, :count).by(1)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
9
42
|
it "should require a name" do
|
|
10
43
|
@tag.valid?
|
|
11
44
|
@tag.errors.on(:name).should == "can't be blank"
|
|
@@ -24,4 +57,17 @@ describe Tag do
|
|
|
24
57
|
@tag.name = "cool"
|
|
25
58
|
@tag.to_s.should == "cool"
|
|
26
59
|
end
|
|
60
|
+
|
|
61
|
+
it "have named_scope named(something)" do
|
|
62
|
+
@tag.name = "cool"
|
|
63
|
+
@tag.save!
|
|
64
|
+
Tag.named('cool').should include(@tag)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "have named_scope named_like(something)" do
|
|
68
|
+
@tag.name = "cool"
|
|
69
|
+
@tag.save!
|
|
70
|
+
@another_tag = Tag.create!(:name => "coolip")
|
|
71
|
+
Tag.named_like('cool').should include(@tag, @another_tag)
|
|
72
|
+
end
|
|
27
73
|
end
|
|
@@ -5,22 +5,42 @@ describe "Taggable" do
|
|
|
5
5
|
[TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
|
|
6
6
|
@taggable = TaggableModel.new(:name => "Bob Jones")
|
|
7
7
|
end
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
it "should have tag types" do
|
|
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
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "should have tag_counts_on" do
|
|
17
|
+
TaggableModel.tag_counts_on(:tags).should be_empty
|
|
18
|
+
|
|
19
|
+
@taggable.tag_list = ["awesome", "epic"]
|
|
20
|
+
@taggable.save
|
|
21
|
+
|
|
22
|
+
TaggableModel.tag_counts_on(:tags).count.should == 2
|
|
23
|
+
@taggable.tag_counts_on(:tags).count.should == 2
|
|
24
|
+
end
|
|
25
|
+
|
|
9
26
|
it "should be able to create tags" do
|
|
10
27
|
@taggable.skill_list = "ruby, rails, css"
|
|
11
|
-
@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
|
|
12
30
|
@taggable.save
|
|
13
|
-
|
|
31
|
+
|
|
14
32
|
Tag.find(:all).size.should == 3
|
|
15
33
|
end
|
|
16
|
-
|
|
34
|
+
|
|
17
35
|
it "should be able to create tags through the tag list directly" do
|
|
18
36
|
@taggable.tag_list_on(:test).add("hello")
|
|
19
|
-
@taggable.
|
|
37
|
+
@taggable.tag_list_cache_on(:test).should_not be_empty
|
|
38
|
+
@taggable.save
|
|
39
|
+
@taggable.save_tags
|
|
20
40
|
@taggable.reload
|
|
21
41
|
@taggable.tag_list_on(:test).should == ["hello"]
|
|
22
42
|
end
|
|
23
|
-
|
|
43
|
+
|
|
24
44
|
it "should differentiate between contexts" do
|
|
25
45
|
@taggable.skill_list = "ruby, rails, css"
|
|
26
46
|
@taggable.tag_list = "ruby, bob, charlie"
|
|
@@ -29,7 +49,7 @@ describe "Taggable" do
|
|
|
29
49
|
@taggable.skill_list.include?("ruby").should be_true
|
|
30
50
|
@taggable.skill_list.include?("bob").should be_false
|
|
31
51
|
end
|
|
32
|
-
|
|
52
|
+
|
|
33
53
|
it "should be able to remove tags through list alone" do
|
|
34
54
|
@taggable.skill_list = "ruby, rails, css"
|
|
35
55
|
@taggable.save
|
|
@@ -40,13 +60,13 @@ describe "Taggable" do
|
|
|
40
60
|
@taggable.reload
|
|
41
61
|
@taggable.should have(2).skills
|
|
42
62
|
end
|
|
43
|
-
|
|
63
|
+
|
|
44
64
|
it "should be able to find by tag" do
|
|
45
65
|
@taggable.skill_list = "ruby, rails, css"
|
|
46
66
|
@taggable.save
|
|
47
67
|
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
|
48
68
|
end
|
|
49
|
-
|
|
69
|
+
|
|
50
70
|
it "should be able to find by tag with context" do
|
|
51
71
|
@taggable.skill_list = "ruby, rails, css"
|
|
52
72
|
@taggable.tag_list = "bob, charlie"
|
|
@@ -55,25 +75,27 @@ describe "Taggable" do
|
|
|
55
75
|
TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
56
76
|
TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
|
|
57
77
|
end
|
|
58
|
-
|
|
78
|
+
|
|
59
79
|
it "should be able to use the tagged_with named scope" do
|
|
60
80
|
@taggable.skill_list = "ruby, rails, css"
|
|
61
81
|
@taggable.tag_list = "bob, charlie"
|
|
62
82
|
@taggable.save
|
|
63
|
-
|
|
83
|
+
|
|
64
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
|
|
65
87
|
TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
66
88
|
TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
|
|
67
89
|
end
|
|
68
|
-
|
|
90
|
+
|
|
69
91
|
it "should not care about case" do
|
|
70
92
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
|
|
71
93
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
|
|
72
|
-
|
|
94
|
+
|
|
73
95
|
Tag.find(:all).size.should == 1
|
|
74
96
|
TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
|
|
75
97
|
end
|
|
76
|
-
|
|
98
|
+
|
|
77
99
|
it "should be able to get tag counts on model as a whole" do
|
|
78
100
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
79
101
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
@@ -81,33 +103,39 @@ describe "Taggable" do
|
|
|
81
103
|
TaggableModel.tag_counts.should_not be_empty
|
|
82
104
|
TaggableModel.skill_counts.should_not be_empty
|
|
83
105
|
end
|
|
84
|
-
|
|
106
|
+
|
|
85
107
|
it "should be able to get all tag counts on model as whole" do
|
|
86
108
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
87
109
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
88
110
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
89
|
-
|
|
111
|
+
|
|
90
112
|
TaggableModel.all_tag_counts.should_not be_empty
|
|
91
113
|
TaggableModel.all_tag_counts.first.count.should == 3 # ruby
|
|
92
114
|
end
|
|
93
|
-
|
|
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
|
+
|
|
94
122
|
it "should be able to get scoped tag counts" do
|
|
95
123
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
96
124
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
97
125
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
98
|
-
|
|
126
|
+
|
|
99
127
|
TaggableModel.tagged_with("ruby").tag_counts.first.count.should == 2 # ruby
|
|
100
128
|
TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
|
|
101
129
|
end
|
|
102
|
-
|
|
130
|
+
|
|
103
131
|
it "should be able to get all scoped tag counts" do
|
|
104
132
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
105
133
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
106
134
|
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
107
|
-
|
|
135
|
+
|
|
108
136
|
TaggableModel.tagged_with("ruby").all_tag_counts.first.count.should == 3 # ruby
|
|
109
137
|
end
|
|
110
|
-
|
|
138
|
+
|
|
111
139
|
it "should be able to set a custom tag context list" do
|
|
112
140
|
bob = TaggableModel.create(:name => "Bob")
|
|
113
141
|
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
@@ -116,17 +144,27 @@ describe "Taggable" do
|
|
|
116
144
|
bob.reload
|
|
117
145
|
bob.tags_on(:rotors).should_not be_empty
|
|
118
146
|
end
|
|
119
|
-
|
|
147
|
+
|
|
120
148
|
it "should be able to find tagged" do
|
|
121
149
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
|
122
150
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
|
123
151
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
|
|
124
|
-
|
|
152
|
+
|
|
125
153
|
TaggableModel.find_tagged_with("ruby", :order => 'taggable_models.name').should == [bob, frank, steve]
|
|
126
154
|
TaggableModel.find_tagged_with("ruby, rails", :order => 'taggable_models.name').should == [bob, frank]
|
|
127
155
|
TaggableModel.find_tagged_with(["ruby", "rails"], :order => 'taggable_models.name').should == [bob, frank]
|
|
128
156
|
end
|
|
129
|
-
|
|
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
|
+
|
|
130
168
|
it "should be able to find tagged on a custom tag context" do
|
|
131
169
|
bob = TaggableModel.create(:name => "Bob")
|
|
132
170
|
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
@@ -139,29 +177,38 @@ describe "Taggable" do
|
|
|
139
177
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
|
140
178
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
|
141
179
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
|
|
142
|
-
|
|
180
|
+
|
|
143
181
|
# Let's only find those productive Rails developers
|
|
144
182
|
TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').should == [bob, frank]
|
|
145
183
|
TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').should == [bob, steve]
|
|
146
184
|
TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).should == [bob]
|
|
147
185
|
TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).should == [bob]
|
|
148
186
|
end
|
|
149
|
-
|
|
187
|
+
|
|
150
188
|
it "should be able to find tagged with only the matching tags" do
|
|
151
189
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
|
|
152
190
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
|
|
153
191
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
|
|
154
|
-
|
|
192
|
+
|
|
155
193
|
TaggableModel.find_tagged_with("fitter, happier", :match_all => true).should == [steve]
|
|
156
194
|
end
|
|
157
|
-
|
|
195
|
+
|
|
158
196
|
it "should be able to find tagged with some excluded tags" do
|
|
159
197
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
|
|
160
198
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
|
|
161
199
|
steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
|
|
162
|
-
|
|
200
|
+
|
|
163
201
|
TaggableModel.find_tagged_with("lazy", :exclude => true).should == [frank, steve]
|
|
164
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
|
|
165
212
|
|
|
166
213
|
describe "Single Table Inheritance" do
|
|
167
214
|
before do
|
|
@@ -170,32 +217,32 @@ describe "Taggable" do
|
|
|
170
217
|
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
|
171
218
|
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
|
172
219
|
end
|
|
173
|
-
|
|
220
|
+
|
|
174
221
|
it "should be able to save tags for inherited models" do
|
|
175
222
|
@inherited_same.tag_list = "bob, kelso"
|
|
176
223
|
@inherited_same.save
|
|
177
224
|
InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
178
225
|
end
|
|
179
|
-
|
|
226
|
+
|
|
180
227
|
it "should find STI tagged models on the superclass" do
|
|
181
228
|
@inherited_same.tag_list = "bob, kelso"
|
|
182
229
|
@inherited_same.save
|
|
183
230
|
TaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
184
231
|
end
|
|
185
|
-
|
|
232
|
+
|
|
186
233
|
it "should be able to add on contexts only to some subclasses" do
|
|
187
234
|
@inherited_different.part_list = "fork, spoon"
|
|
188
235
|
@inherited_different.save
|
|
189
236
|
InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
|
|
190
237
|
AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
|
|
191
238
|
end
|
|
192
|
-
|
|
239
|
+
|
|
193
240
|
it "should have different tag_counts_on for inherited models" do
|
|
194
241
|
@inherited_same.tag_list = "bob, kelso"
|
|
195
242
|
@inherited_same.save!
|
|
196
243
|
@inherited_different.tag_list = "fork, spoon"
|
|
197
244
|
@inherited_different.save!
|
|
198
|
-
|
|
245
|
+
|
|
199
246
|
InheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso)
|
|
200
247
|
AlteredInheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(fork spoon)
|
|
201
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).should == %w(ruby scheme)
|
|
28
|
+
@user2.owned_tags.map(&:name).sort.should == %w(java python lisp ruby).sort
|
|
29
|
+
@taggable.tags_from(@user).should == %w(ruby scheme)
|
|
30
|
+
@taggable.tags_from(@user2).should == %w(java python lisp 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
|
|
@@ -4,4 +4,22 @@ describe Tagging do
|
|
|
4
4
|
before(:each) do
|
|
5
5
|
@tagging = Tagging.new
|
|
6
6
|
end
|
|
7
|
+
|
|
8
|
+
it "should not be valid with a invalid tag" do
|
|
9
|
+
@tagging.taggable = TaggableModel.create(:name => "Bob Jones")
|
|
10
|
+
@tagging.tag = Tag.new(:name => "")
|
|
11
|
+
@tagging.context = "tags"
|
|
12
|
+
|
|
13
|
+
@tagging.should_not be_valid
|
|
14
|
+
@tagging.errors.on(:tag_id).should == "can't be blank"
|
|
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
|
|
7
25
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe TagsHelper do
|
|
4
|
+
before(:each) do
|
|
5
|
+
[TaggableModel, Tag, Tagging].each(&:delete_all)
|
|
6
|
+
|
|
7
|
+
@bob = TaggableModel.create(:name => "Bob Jones", :language_list => "ruby, php")
|
|
8
|
+
@tom = TaggableModel.create(:name => "Tom Marley", :language_list => "ruby, java")
|
|
9
|
+
@eve = TaggableModel.create(:name => "Eve Nodd", :language_list => "ruby, c++")
|
|
10
|
+
|
|
11
|
+
@helper = class Helper
|
|
12
|
+
include TagsHelper
|
|
13
|
+
end.new
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should yield the proper css classes" do
|
|
19
|
+
tags = { }
|
|
20
|
+
|
|
21
|
+
@helper.tag_cloud(TaggableModel.tag_counts_on(:languages), ["sucky", "awesome"]) do |tag, css_class|
|
|
22
|
+
tags[tag.name] = css_class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
tags["ruby"].should == "awesome"
|
|
26
|
+
tags["java"].should == "sucky"
|
|
27
|
+
tags["c++"].should == "sucky"
|
|
28
|
+
tags["php"].should == "sucky"
|
|
29
|
+
end
|
|
30
|
+
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,18 +24,24 @@ 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')
|
|
23
34
|
|
|
24
35
|
class TaggableModel < ActiveRecord::Base
|
|
25
|
-
|
|
36
|
+
acts_as_taggable
|
|
37
|
+
acts_as_taggable_on :languages
|
|
26
38
|
acts_as_taggable_on :skills
|
|
39
|
+
acts_as_taggable_on :needs, :offerings
|
|
27
40
|
end
|
|
28
41
|
|
|
29
42
|
class OtherTaggableModel < ActiveRecord::Base
|
|
30
43
|
acts_as_taggable_on :tags, :languages
|
|
44
|
+
acts_as_taggable_on :needs, :offerings
|
|
31
45
|
end
|
|
32
46
|
|
|
33
47
|
class InheritingTaggableModel < TaggableModel
|
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.0
|
|
4
|
+
version: 1.1.0
|
|
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-03 00:00:00 +01:00
|
|
13
13
|
default_executable:
|
|
14
14
|
dependencies: []
|
|
15
15
|
|
|
@@ -27,9 +27,12 @@ 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
|
|
35
|
+
- lib/acts_as_taggable_on/group_helper.rb
|
|
33
36
|
- lib/acts_as_taggable_on/tag.rb
|
|
34
37
|
- lib/acts_as_taggable_on/tag_list.rb
|
|
35
38
|
- lib/acts_as_taggable_on/tagging.rb
|
|
@@ -37,11 +40,13 @@ files:
|
|
|
37
40
|
- rails/init.rb
|
|
38
41
|
- spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
|
|
39
42
|
- spec/acts_as_taggable_on/acts_as_tagger_spec.rb
|
|
43
|
+
- spec/acts_as_taggable_on/group_helper_spec.rb
|
|
40
44
|
- spec/acts_as_taggable_on/tag_list_spec.rb
|
|
41
45
|
- spec/acts_as_taggable_on/tag_spec.rb
|
|
42
46
|
- spec/acts_as_taggable_on/taggable_spec.rb
|
|
43
47
|
- spec/acts_as_taggable_on/tagger_spec.rb
|
|
44
48
|
- spec/acts_as_taggable_on/tagging_spec.rb
|
|
49
|
+
- spec/acts_as_taggable_on/tags_helper_spec.rb
|
|
45
50
|
- spec/schema.rb
|
|
46
51
|
- spec/spec.opts
|
|
47
52
|
- spec/spec_helper.rb
|
|
@@ -76,10 +81,12 @@ summary: ActsAsTaggableOn is a tagging plugin for Rails that provides multiple t
|
|
|
76
81
|
test_files:
|
|
77
82
|
- spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
|
|
78
83
|
- spec/acts_as_taggable_on/acts_as_tagger_spec.rb
|
|
84
|
+
- spec/acts_as_taggable_on/group_helper_spec.rb
|
|
79
85
|
- spec/acts_as_taggable_on/tag_list_spec.rb
|
|
80
86
|
- spec/acts_as_taggable_on/tag_spec.rb
|
|
81
87
|
- spec/acts_as_taggable_on/taggable_spec.rb
|
|
82
88
|
- spec/acts_as_taggable_on/tagger_spec.rb
|
|
83
89
|
- spec/acts_as_taggable_on/tagging_spec.rb
|
|
90
|
+
- spec/acts_as_taggable_on/tags_helper_spec.rb
|
|
84
91
|
- spec/schema.rb
|
|
85
92
|
- spec/spec_helper.rb
|