acts-as-taggable-on 1.0.13 → 2.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +5 -2
- data/Gemfile +8 -0
- data/README.rdoc +12 -1
- data/Rakefile +7 -6
- data/VERSION +1 -1
- data/lib/acts-as-taggable-on.rb +27 -7
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +127 -84
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +21 -13
- data/lib/acts_as_taggable_on/group_helper.rb +2 -0
- data/lib/acts_as_taggable_on/tag.rb +37 -10
- data/lib/acts_as_taggable_on/tag_list.rb +29 -29
- data/lib/acts_as_taggable_on/tagging.rb +16 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +8 -2
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +7 -17
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +45 -4
- data/spec/acts_as_taggable_on/group_helper_spec.rb +3 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +18 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +55 -13
- data/spec/acts_as_taggable_on/taggable_spec.rb +101 -67
- data/spec/acts_as_taggable_on/tagger_spec.rb +20 -3
- data/spec/acts_as_taggable_on/tagging_spec.rb +13 -3
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +1 -1
- data/spec/spec.opts +1 -2
- data/spec/spec_helper.rb +26 -9
- metadata +20 -8
- data/rails/init.rb +0 -7
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
== 2010-02-17
|
|
2
|
+
* Converted the plugin to be compatible with Rails3
|
|
3
|
+
|
|
1
4
|
== 2009-12-02
|
|
2
5
|
|
|
3
6
|
* PostgreSQL is now supported (via morgoth)
|
|
@@ -12,10 +15,10 @@
|
|
|
12
15
|
* Removed extraneous down migration cruft (azabaj)
|
|
13
16
|
|
|
14
17
|
== 2008-06-09
|
|
15
|
-
|
|
18
|
+
|
|
16
19
|
* Added support for Single Table Inheritance
|
|
17
20
|
* Adding gemspec and rails/init.rb for gemified plugin
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
== 2007-12-12
|
|
20
23
|
|
|
21
24
|
* Added ability to use dynamic tag contexts
|
data/Gemfile
ADDED
data/README.rdoc
CHANGED
|
@@ -89,6 +89,9 @@ also improves compatibility with the will_paginate gem:
|
|
|
89
89
|
User.tagged_with("awesome").by_date
|
|
90
90
|
User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
|
|
91
91
|
|
|
92
|
+
#Find a user with matching all tags, not just one
|
|
93
|
+
User.tagged_with(["awesome", "cool"], :match_all => :true)
|
|
94
|
+
|
|
92
95
|
=== Relationships
|
|
93
96
|
|
|
94
97
|
You can find objects of the same type based on similar tags on certain contexts.
|
|
@@ -151,6 +154,12 @@ A helper is included to assist with generating tag clouds.
|
|
|
151
154
|
|
|
152
155
|
Here is an example that generates a tag cloud.
|
|
153
156
|
|
|
157
|
+
Helper:
|
|
158
|
+
|
|
159
|
+
module PostsHelper
|
|
160
|
+
include TagsHelper
|
|
161
|
+
end
|
|
162
|
+
|
|
154
163
|
Controller:
|
|
155
164
|
|
|
156
165
|
class PostController < ApplicationController
|
|
@@ -160,7 +169,8 @@ Controller:
|
|
|
160
169
|
end
|
|
161
170
|
|
|
162
171
|
View:
|
|
163
|
-
|
|
172
|
+
|
|
173
|
+
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
|
|
164
174
|
<%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
|
|
165
175
|
<% end %>
|
|
166
176
|
|
|
@@ -187,5 +197,6 @@ CSS:
|
|
|
187
197
|
* slainer68 - STI fix
|
|
188
198
|
* harrylove - migration instructions and fix-ups
|
|
189
199
|
* lawrencepit - cached tag work
|
|
200
|
+
* sobrinho - fixed tag_cloud helper
|
|
190
201
|
|
|
191
202
|
Copyright (c) 2007-2009 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
gem 'rspec', '2.0.0.beta.1'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
2
3
|
|
|
3
4
|
begin
|
|
4
5
|
require 'jeweler'
|
|
@@ -9,7 +10,7 @@ begin
|
|
|
9
10
|
gemspec.email = "michael@intridea.com"
|
|
10
11
|
gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
|
|
11
12
|
gemspec.authors = ["Michael Bleigh"]
|
|
12
|
-
gemspec.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"] - FileList["**/*.log"]
|
|
13
|
+
gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
|
|
13
14
|
end
|
|
14
15
|
Jeweler::GemcutterTasks.new
|
|
15
16
|
rescue LoadError
|
|
@@ -18,12 +19,12 @@ end
|
|
|
18
19
|
|
|
19
20
|
desc 'Default: run specs'
|
|
20
21
|
task :default => :spec
|
|
21
|
-
|
|
22
|
-
t.
|
|
22
|
+
Rspec::Core::RakeTask.new do |t|
|
|
23
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
t.
|
|
26
|
+
Rspec::Core::RakeTask.new('rcov') do |t|
|
|
27
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
27
28
|
t.rcov = true
|
|
28
29
|
t.rcov_opts = ['--exclude', 'spec']
|
|
29
30
|
end
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.0.0.pre1
|
data/lib/acts-as-taggable-on.rb
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require
|
|
7
|
-
require
|
|
1
|
+
begin
|
|
2
|
+
# Try to require the preresolved locked set of gems.
|
|
3
|
+
require File.expand_path("../.bundle/environment", __FILE__)
|
|
4
|
+
rescue LoadError
|
|
5
|
+
# Fall back on doing an unlocked resolve at runtime.
|
|
6
|
+
require "rubygems"
|
|
7
|
+
require "bundler"
|
|
8
|
+
Bundler.setup
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Bundler.require
|
|
12
|
+
|
|
13
|
+
require "active_record"
|
|
14
|
+
require "action_view"
|
|
15
|
+
|
|
16
|
+
require "acts_as_taggable_on/group_helper"
|
|
17
|
+
require "acts_as_taggable_on/acts_as_taggable_on"
|
|
18
|
+
require "acts_as_taggable_on/acts_as_tagger"
|
|
19
|
+
require "acts_as_taggable_on/tag"
|
|
20
|
+
require "acts_as_taggable_on/tag_list"
|
|
21
|
+
require "acts_as_taggable_on/tags_helper"
|
|
22
|
+
require "acts_as_taggable_on/tagging"
|
|
23
|
+
|
|
24
|
+
ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
|
|
25
|
+
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
|
|
26
|
+
|
|
27
|
+
ActionView::Base.send :include, TagsHelper
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
module ActiveRecord
|
|
2
2
|
module Acts
|
|
3
3
|
module TaggableOn
|
|
4
|
+
|
|
4
5
|
def self.included(base)
|
|
5
6
|
base.extend(ClassMethods)
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
module ClassMethods
|
|
10
|
+
|
|
9
11
|
def taggable?
|
|
10
12
|
false
|
|
11
13
|
end
|
|
@@ -17,17 +19,19 @@ module ActiveRecord
|
|
|
17
19
|
def acts_as_taggable_on(*args)
|
|
18
20
|
args.flatten! if args
|
|
19
21
|
args.compact! if args
|
|
22
|
+
|
|
20
23
|
for tag_type in args
|
|
21
24
|
tag_type = tag_type.to_s
|
|
22
|
-
# use aliased_join_table_name for context condition so that
|
|
25
|
+
# use aliased_join_table_name for context condition so that sphinx can join multiple
|
|
23
26
|
# tag references from same model without getting an ambiguous column error
|
|
24
|
-
|
|
27
|
+
class_eval do
|
|
25
28
|
has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
|
|
26
|
-
:include => :tag, :conditions => ['#{aliased_join_table_name rescue
|
|
29
|
+
:include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
|
|
27
30
|
has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
class_eval <<-RUBY
|
|
34
|
+
|
|
31
35
|
def self.taggable?
|
|
32
36
|
true
|
|
33
37
|
end
|
|
@@ -68,7 +72,7 @@ module ActiveRecord
|
|
|
68
72
|
def find_matching_contexts(search_context, result_context, options = {})
|
|
69
73
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
70
74
|
end
|
|
71
|
-
|
|
75
|
+
|
|
72
76
|
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
|
73
77
|
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
74
78
|
end
|
|
@@ -80,13 +84,14 @@ module ActiveRecord
|
|
|
80
84
|
def self.top_#{tag_type}(limit = 10)
|
|
81
85
|
tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
|
|
82
86
|
end
|
|
87
|
+
|
|
83
88
|
RUBY
|
|
84
89
|
end
|
|
85
90
|
|
|
86
91
|
if respond_to?(:tag_types)
|
|
87
92
|
write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
|
|
88
93
|
else
|
|
89
|
-
|
|
94
|
+
class_eval do
|
|
90
95
|
write_inheritable_attribute(:tag_types, args.uniq)
|
|
91
96
|
class_inheritable_reader :tag_types
|
|
92
97
|
|
|
@@ -96,13 +101,12 @@ module ActiveRecord
|
|
|
96
101
|
attr_writer :custom_contexts
|
|
97
102
|
|
|
98
103
|
before_save :save_cached_tag_list
|
|
99
|
-
after_save :save_tags
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
after_save:save_tags
|
|
106
|
+
|
|
107
|
+
scope :tagged_with, lambda{ |*args|
|
|
108
|
+
find_options_for_find_tagged_with(*args)
|
|
109
|
+
}
|
|
106
110
|
end
|
|
107
111
|
|
|
108
112
|
include ActiveRecord::Acts::TaggableOn::InstanceMethods
|
|
@@ -110,68 +114,72 @@ module ActiveRecord
|
|
|
110
114
|
alias_method_chain :reload, :tag_list
|
|
111
115
|
end
|
|
112
116
|
end
|
|
117
|
+
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
module SingletonMethods
|
|
121
|
+
|
|
116
122
|
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
123
|
+
|
|
117
124
|
# Pass either a tag string, or an array of strings or tags
|
|
118
125
|
#
|
|
119
126
|
# Options:
|
|
127
|
+
# :any - find models that match any of the given tags
|
|
120
128
|
# :exclude - Find models that are not tagged with the given tags
|
|
121
129
|
# :match_all - Find models that match all of the given tags, not just one
|
|
122
130
|
# :conditions - A piece of SQL conditions to add to the query
|
|
123
131
|
# :on - scopes the find to a context
|
|
124
|
-
def find_tagged_with(*args)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
end
|
|
132
|
+
# def find_tagged_with(*args)
|
|
133
|
+
# find_options_for_find_tagged_with(*args)
|
|
134
|
+
# end
|
|
128
135
|
|
|
129
136
|
def caching_tag_list_on?(context)
|
|
130
137
|
column_names.include?("cached_#{context.to_s.singularize}_list")
|
|
131
138
|
end
|
|
132
139
|
|
|
133
140
|
def tag_counts_on(context, options = {})
|
|
134
|
-
|
|
141
|
+
find_for_tag_counts(options.merge({:on => context.to_s}))
|
|
135
142
|
end
|
|
136
143
|
|
|
137
144
|
def all_tag_counts(options = {})
|
|
138
|
-
|
|
145
|
+
find_for_tag_counts(options)
|
|
139
146
|
end
|
|
140
147
|
|
|
141
148
|
def find_options_for_find_tagged_with(tags, options = {})
|
|
142
|
-
|
|
149
|
+
tag_list = TagList.from(tags)
|
|
143
150
|
|
|
144
|
-
return {} if
|
|
151
|
+
return {} if tag_list.empty?
|
|
145
152
|
|
|
146
153
|
joins = []
|
|
147
154
|
conditions = []
|
|
148
155
|
|
|
149
156
|
context = options.delete(:on)
|
|
150
157
|
|
|
151
|
-
|
|
152
158
|
if options.delete(:exclude)
|
|
153
|
-
tags_conditions =
|
|
159
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
154
160
|
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
161
|
|
|
162
|
+
elsif options.delete(:any)
|
|
163
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
|
|
164
|
+
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)})"
|
|
165
|
+
|
|
156
166
|
else
|
|
167
|
+
tags = Tag.named_any(tag_list)
|
|
168
|
+
return { :conditions => "1 = 0" } unless tags.length == tag_list.length
|
|
169
|
+
|
|
157
170
|
tags.each do |tag|
|
|
158
|
-
safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
|
|
171
|
+
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
|
159
172
|
prefix = "#{safe_tag}_#{rand(1024)}"
|
|
160
173
|
|
|
161
174
|
taggings_alias = "#{table_name}_taggings_#{prefix}"
|
|
162
|
-
tags_alias = "#{table_name}_tags_#{prefix}"
|
|
163
175
|
|
|
164
176
|
tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
165
177
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
166
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
178
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
|
179
|
+
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
|
167
180
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
168
181
|
|
|
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
182
|
joins << tagging_join
|
|
174
|
-
joins << tag_join
|
|
175
183
|
end
|
|
176
184
|
end
|
|
177
185
|
|
|
@@ -185,9 +193,12 @@ module ActiveRecord
|
|
|
185
193
|
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
186
194
|
end
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
Tag.joins(joins.join(" ")).group(group).where(conditions.join(" AND ")).readonly(false)
|
|
197
|
+
|
|
198
|
+
# { :joins => joins.join(" "),
|
|
199
|
+
# :group => group,
|
|
200
|
+
# :conditions => conditions.join(" AND "),
|
|
201
|
+
# :readonly => false }.update(options)
|
|
191
202
|
end
|
|
192
203
|
|
|
193
204
|
# Calculate the tag counts for all tags.
|
|
@@ -201,10 +212,9 @@ module ActiveRecord
|
|
|
201
212
|
# :at_least - Exclude tags with a frequency less than the given value
|
|
202
213
|
# :at_most - Exclude tags with a frequency greater than the given value
|
|
203
214
|
# :on - Scope the find to only include a certain context
|
|
204
|
-
def
|
|
215
|
+
def find_for_tag_counts(options = {})
|
|
205
216
|
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
|
206
217
|
|
|
207
|
-
scope = scope(:find)
|
|
208
218
|
start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
209
219
|
end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
210
220
|
|
|
@@ -221,40 +231,34 @@ module ActiveRecord
|
|
|
221
231
|
]
|
|
222
232
|
|
|
223
233
|
conditions = conditions.compact.join(' AND ')
|
|
224
|
-
conditions = merge_conditions(conditions, scope[:conditions]) if scope
|
|
225
234
|
|
|
226
235
|
joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
|
|
227
236
|
joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
|
|
228
|
-
|
|
229
237
|
joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
|
|
230
|
-
|
|
238
|
+
|
|
239
|
+
unless descends_from_active_record?
|
|
231
240
|
# Current model is STI descendant, so add type checking to the join condition
|
|
232
|
-
joins << " AND #{table_name}.#{
|
|
241
|
+
joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
|
233
242
|
end
|
|
234
243
|
|
|
235
|
-
joins << scope[:joins] if scope && scope[:joins]
|
|
236
|
-
|
|
237
244
|
at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
|
|
238
245
|
at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
|
|
239
246
|
having = [at_least, at_most].compact.join(' AND ')
|
|
240
247
|
group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
|
|
241
248
|
group_by << " AND #{having}" unless having.blank?
|
|
242
249
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
:conditions => conditions,
|
|
246
|
-
:group => group_by,
|
|
247
|
-
:limit => options[:limit],
|
|
248
|
-
:order => options[:order]
|
|
249
|
-
}
|
|
250
|
+
Tag.select("#{Tag.table_name}.*, COUNT(*) AS count").joins(joins.join(" ")).where(conditions).group(group_by).limit(options[:limit]).order(options[:order])
|
|
251
|
+
|
|
250
252
|
end
|
|
251
253
|
|
|
252
254
|
def is_taggable?
|
|
253
255
|
true
|
|
254
256
|
end
|
|
257
|
+
|
|
255
258
|
end
|
|
256
259
|
|
|
257
260
|
module InstanceMethods
|
|
261
|
+
|
|
258
262
|
include ActiveRecord::Acts::TaggableOn::GroupHelper
|
|
259
263
|
|
|
260
264
|
def custom_contexts
|
|
@@ -269,51 +273,68 @@ module ActiveRecord
|
|
|
269
273
|
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
274
|
end
|
|
271
275
|
|
|
272
|
-
def tag_list_on(context, owner=nil)
|
|
273
|
-
var_name = context.to_s.singularize + "_list"
|
|
276
|
+
def tag_list_on(context, owner = nil)
|
|
274
277
|
add_custom_context(context)
|
|
275
|
-
|
|
278
|
+
cache = tag_list_cache_on(context)
|
|
279
|
+
return owner ? cache[owner] : cache[owner] if cache[owner]
|
|
276
280
|
|
|
277
281
|
if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
|
|
278
|
-
|
|
282
|
+
cache[owner] = TagList.from(cached_tag_list_on(context))
|
|
279
283
|
else
|
|
280
|
-
|
|
284
|
+
cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
|
|
281
285
|
end
|
|
282
286
|
end
|
|
283
287
|
|
|
284
|
-
def
|
|
288
|
+
def all_tags_list_on(context)
|
|
289
|
+
variable_name = "@all_#{context.to_s.singularize}_list"
|
|
290
|
+
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
|
291
|
+
instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def all_tags_on(context)
|
|
295
|
+
opts = ["#{Tagging.table_name}.context = ?", context.to_s]
|
|
296
|
+
base_tags.where(opts).order("#{Tagging.table_name}.created_at")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def tags_on(context, owner = nil)
|
|
285
300
|
if owner
|
|
286
|
-
opts =
|
|
287
|
-
context.to_s, owner.id, owner.class.to_s]}
|
|
301
|
+
opts = ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?", context.to_s, owner.id, owner.class.to_s]
|
|
288
302
|
else
|
|
289
|
-
opts =
|
|
303
|
+
opts = ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]
|
|
290
304
|
end
|
|
291
|
-
base_tags.
|
|
305
|
+
base_tags.where(opts)
|
|
292
306
|
end
|
|
293
307
|
|
|
294
308
|
def cached_tag_list_on(context)
|
|
295
309
|
self["cached_#{context.to_s.singularize}_list"]
|
|
296
310
|
end
|
|
297
311
|
|
|
298
|
-
def
|
|
299
|
-
|
|
312
|
+
def tag_list_cache_on(context)
|
|
313
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
|
314
|
+
cache = instance_variable_get(variable_name)
|
|
315
|
+
instance_variable_set(variable_name, cache = {}) unless cache
|
|
316
|
+
cache
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def set_tag_list_on(context, new_list, tagger = nil)
|
|
320
|
+
tag_list_cache_on(context)[tagger] = TagList.from(new_list)
|
|
300
321
|
add_custom_context(context)
|
|
301
322
|
end
|
|
302
323
|
|
|
303
324
|
def tag_counts_on(context, options={})
|
|
304
|
-
self.class.tag_counts_on(context, options.merge(:id =>
|
|
325
|
+
self.class.tag_counts_on(context, options.merge(:id => id))
|
|
305
326
|
end
|
|
306
327
|
|
|
307
328
|
def related_tags_for(context, klass, options = {})
|
|
308
329
|
search_conditions = related_search_options(context, klass, options)
|
|
309
330
|
|
|
310
|
-
klass.
|
|
331
|
+
klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
|
|
311
332
|
end
|
|
312
333
|
|
|
313
334
|
def related_search_options(context, klass, options = {})
|
|
314
|
-
tags_to_find =
|
|
335
|
+
tags_to_find = tags_on(context).collect { |t| t.name }
|
|
315
336
|
|
|
316
|
-
exclude_self = "#{klass.table_name}.id != #{
|
|
337
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
317
338
|
|
|
318
339
|
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
319
340
|
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
@@ -322,17 +343,17 @@ module ActiveRecord
|
|
|
322
343
|
:order => "count DESC"
|
|
323
344
|
}.update(options)
|
|
324
345
|
end
|
|
325
|
-
|
|
346
|
+
|
|
326
347
|
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
327
348
|
search_conditions = matching_context_search_options(search_context, result_context, klass, options)
|
|
328
349
|
|
|
329
|
-
klass.
|
|
350
|
+
klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
|
|
330
351
|
end
|
|
331
|
-
|
|
352
|
+
|
|
332
353
|
def matching_context_search_options(search_context, result_context, klass, options = {})
|
|
333
|
-
tags_to_find =
|
|
354
|
+
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
|
334
355
|
|
|
335
|
-
exclude_self = "#{klass.table_name}.id != #{
|
|
356
|
+
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
336
357
|
|
|
337
358
|
{ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
|
|
338
359
|
:from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
|
|
@@ -341,28 +362,49 @@ module ActiveRecord
|
|
|
341
362
|
:order => "count DESC"
|
|
342
363
|
}.update(options)
|
|
343
364
|
end
|
|
344
|
-
|
|
365
|
+
|
|
345
366
|
def save_cached_tag_list
|
|
346
367
|
self.class.tag_types.map(&:to_s).each do |tag_type|
|
|
347
368
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|
|
348
|
-
self["cached_#{tag_type.singularize}_list"] =
|
|
369
|
+
self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
|
|
349
370
|
end
|
|
350
371
|
end
|
|
351
372
|
end
|
|
352
373
|
|
|
353
374
|
def save_tags
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
contexts = custom_contexts + self.class.tag_types.map(&:to_s)
|
|
376
|
+
|
|
377
|
+
transaction do
|
|
378
|
+
contexts.each do |context|
|
|
379
|
+
cache = tag_list_cache_on(context)
|
|
380
|
+
|
|
381
|
+
cache.each do |owner, list|
|
|
382
|
+
new_tags = Tag.find_or_create_all_with_like_by_name(list.uniq)
|
|
383
|
+
taggings = Tagging.where({ :taggable_id => self.id, :taggable_type => self.class.base_class.to_s })
|
|
384
|
+
|
|
385
|
+
# Destroy old taggings:
|
|
386
|
+
if owner
|
|
387
|
+
old_tags = tags_on(context, owner) - new_tags
|
|
388
|
+
old_taggings = Tagging.where({ :taggable_id => self.id, :taggable_type => self.class.base_class.to_s, :tag_id => old_tags, :tagger_id => owner.id, :tagger_type => owner.class.to_s, :context => context })
|
|
389
|
+
|
|
390
|
+
Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
391
|
+
else
|
|
392
|
+
old_tags = tags_on(context) - new_tags
|
|
393
|
+
base_tags.delete(*old_tags)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
new_tags.reject! { |tag| taggings.any? { |tagging|
|
|
397
|
+
tagging.tag_id == tag.id &&
|
|
398
|
+
tagging.tagger_id == (owner ? owner.id : nil) &&
|
|
399
|
+
tagging.tagger_type == (owner ? owner.class.to_s : nil) &&
|
|
400
|
+
tagging.context == context
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# create new taggings:
|
|
405
|
+
new_tags.each do |tag|
|
|
406
|
+
Tagging.create!(:tag_id => tag.id, :context => context, :tagger => owner, :taggable => self)
|
|
407
|
+
end
|
|
366
408
|
end
|
|
367
409
|
end
|
|
368
410
|
end
|
|
@@ -372,11 +414,12 @@ module ActiveRecord
|
|
|
372
414
|
|
|
373
415
|
def reload_with_tag_list(*args)
|
|
374
416
|
self.class.tag_types.each do |tag_type|
|
|
375
|
-
|
|
417
|
+
instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
|
|
376
418
|
end
|
|
377
419
|
|
|
378
420
|
reload_without_tag_list(*args)
|
|
379
421
|
end
|
|
422
|
+
|
|
380
423
|
end
|
|
381
424
|
end
|
|
382
425
|
end
|
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
module ActiveRecord
|
|
2
2
|
module Acts
|
|
3
3
|
module Tagger
|
|
4
|
+
|
|
4
5
|
def self.included(base)
|
|
5
6
|
base.extend ClassMethods
|
|
6
7
|
end
|
|
7
|
-
|
|
8
|
+
|
|
8
9
|
module ClassMethods
|
|
10
|
+
|
|
9
11
|
def acts_as_tagger(opts={})
|
|
10
|
-
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
|
12
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
|
11
13
|
:include => :tag, :class_name => "Tagging")
|
|
12
14
|
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
|
15
|
+
|
|
13
16
|
include ActiveRecord::Acts::Tagger::InstanceMethods
|
|
14
|
-
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
|
17
|
+
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
|
15
18
|
end
|
|
16
|
-
|
|
19
|
+
|
|
17
20
|
def is_tagger?
|
|
18
21
|
false
|
|
19
22
|
end
|
|
23
|
+
|
|
20
24
|
end
|
|
21
|
-
|
|
25
|
+
|
|
22
26
|
module InstanceMethods
|
|
27
|
+
|
|
23
28
|
def self.included(base)
|
|
24
29
|
end
|
|
25
|
-
|
|
30
|
+
|
|
26
31
|
def tag(taggable, opts={})
|
|
27
32
|
opts.reverse_merge!(:force => true)
|
|
28
33
|
|
|
29
34
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
30
|
-
|
|
31
|
-
raise "You need to specify
|
|
32
|
-
raise "
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
|
37
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
|
38
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
|
|
34
39
|
|
|
35
40
|
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
|
36
41
|
taggable.save
|
|
37
42
|
end
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
def is_tagger?
|
|
40
45
|
self.class.is_tagger?
|
|
41
46
|
end
|
|
47
|
+
|
|
42
48
|
end
|
|
43
|
-
|
|
49
|
+
|
|
44
50
|
module SingletonMethods
|
|
51
|
+
|
|
45
52
|
def is_tagger?
|
|
46
53
|
true
|
|
47
54
|
end
|
|
55
|
+
|
|
48
56
|
end
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
@@ -2,10 +2,12 @@ module ActiveRecord
|
|
|
2
2
|
module Acts
|
|
3
3
|
module TaggableOn
|
|
4
4
|
module GroupHelper
|
|
5
|
+
|
|
5
6
|
# all column names are necessary for PostgreSQL group clause
|
|
6
7
|
def grouped_column_names_for(object)
|
|
7
8
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
|
8
9
|
end
|
|
10
|
+
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|