acts-as-taggable-on-simonwh 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 ADDED
@@ -0,0 +1,25 @@
1
+ == 2010-02-17
2
+ * Converted the plugin to be compatible with Rails3
3
+
4
+ == 2009-12-02
5
+
6
+ * PostgreSQL is now supported (via morgoth)
7
+
8
+ == 2008-07-17
9
+
10
+ * Can now use a named_scope to find tags!
11
+
12
+ == 2008-06-23
13
+
14
+ * Can now find related objects of another class (tristanzdunn)
15
+ * Removed extraneous down migration cruft (azabaj)
16
+
17
+ == 2008-06-09
18
+
19
+ * Added support for Single Table Inheritance
20
+ * Adding gemspec and rails/init.rb for gemified plugin
21
+
22
+ == 2007-12-12
23
+
24
+ * Added ability to use dynamic tag contexts
25
+ * Fixed missing migration generator
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'sqlite3-ruby', :require => 'sqlite3'
4
+ gem 'rails', '3.0.0.beta'
5
+ gem 'rspec', '2.0.0.beta.1'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Michael Bleigh and Intridea Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,212 @@
1
+ = ActsAsTaggableOn
2
+
3
+ This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
4
+ It has evolved substantially since that point, but all credit goes to him for the
5
+ initial tagging functionality that so many people have used.
6
+
7
+ For instance, in a social network, a user might have tags that are called skills,
8
+ interests, sports, and more. There is no real way to differentiate between tags and
9
+ so an implementation of this type is not possible with acts as taggable on steroids.
10
+
11
+ Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
12
+ (namely "tags"), acts as taggable on allows you to specify an arbitrary number of
13
+ tag "contexts" that can be used locally or in combination in the same way steroids
14
+ was used.
15
+
16
+ == Installation
17
+
18
+ === Plugin
19
+
20
+ Acts As Taggable On is available both as a gem and as a traditional plugin. For the
21
+ traditional plugin you can install like so (Rails 2.1 or later):
22
+
23
+ script/plugin install git://github.com/mbleigh/acts-as-taggable-on.git
24
+
25
+ === GemPlugin
26
+
27
+ Acts As Taggable On is also available as a gem plugin using Rails 2.1's gem dependencies.
28
+ To install the gem, add this to your config/environment.rb:
29
+
30
+ config.gem "acts-as-taggable-on", :source => "http://gemcutter.org"
31
+
32
+ After that, you can run "rake gems:install" to install the gem if you don't already have it.
33
+
34
+ == Rails 3.0
35
+
36
+ Acts As Taggable On is now useable in Rails 3.0, thanks to the excellent work of Szymon Nowak
37
+ and Jelle Vandebeeck. Because backwards compatibility is hard to maintain, their work is available
38
+ in the feature/rails3_compatibility branch.
39
+
40
+ A Rails 3.0 compatible version of the gem is also available:
41
+
42
+ gem install acts-as-taggable-on -v=2.0.0.pre1
43
+
44
+ === Post Installation (Rails)
45
+
46
+ 1. script/generate acts_as_taggable_on_migration
47
+ 2. rake db:migrate
48
+
49
+ === Testing
50
+
51
+ Acts As Taggable On uses RSpec for its test coverage. Inside the plugin
52
+ directory, you can run the specs with:
53
+
54
+ rake spec
55
+
56
+ If you already have RSpec on your application, the specs will run while using:
57
+
58
+ rake spec:plugins
59
+
60
+
61
+ == Usage
62
+
63
+ class User < ActiveRecord::Base
64
+ acts_as_taggable_on :tags, :skills, :interests
65
+ end
66
+
67
+ @user = User.new(:name => "Bobby")
68
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
69
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
70
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
71
+ @user.save
72
+
73
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
74
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
75
+
76
+ # The old way
77
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
78
+ User.find_tagged_with("awesome", :on => :skills) # => []
79
+
80
+ # The better way (utilizes named_scope)
81
+ User.tagged_with("awesome", :on => :tags) # => [@user]
82
+ User.tagged_with("awesome", :on => :skills) # => []
83
+
84
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
85
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
86
+ @frankie.skill_counts
87
+
88
+ === Finding Tagged Objects
89
+
90
+ Acts As Taggable On utilizes Rails 2.1's named_scope to create an association
91
+ for tags. This way you can mix and match to filter down your results, and it
92
+ also improves compatibility with the will_paginate gem:
93
+
94
+ class User < ActiveRecord::Base
95
+ acts_as_taggable_on :tags
96
+ named_scope :by_join_date, :order => "created_at DESC"
97
+ end
98
+
99
+ User.tagged_with("awesome").by_date
100
+ User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
101
+
102
+ #Find a user with matching all tags, not just one
103
+ User.tagged_with(["awesome", "cool"], :match_all => :true)
104
+
105
+ === Relationships
106
+
107
+ You can find objects of the same type based on similar tags on certain contexts.
108
+ Also, objects will be returned in descending order based on the total number of
109
+ matched tags.
110
+
111
+ @bobby = User.find_by_name("Bobby")
112
+ @bobby.skill_list # => ["jogging", "diving"]
113
+
114
+ @frankie = User.find_by_name("Frankie")
115
+ @frankie.skill_list # => ["hacking"]
116
+
117
+ @tom = User.find_by_name("Tom")
118
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
119
+
120
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
121
+ @bobby.find_related_skills # => [<User name="Tom">]
122
+ @frankie.find_related_skills # => [<User name="Tom">]
123
+
124
+ === Dynamic Tag Contexts
125
+
126
+ In addition to the generated tag contexts in the definition, it is also possible
127
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
128
+
129
+ @user = User.new(:name => "Bobby")
130
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
131
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
132
+ @user.save
133
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
134
+ @user.tag_counts_on(:customs)
135
+ User.find_tagged_with("same", :on => :customs) # => [@user]
136
+
137
+ === Tag Ownership
138
+
139
+ Tags can have owners:
140
+
141
+ class User < ActiveRecord::Base
142
+ acts_as_tagger
143
+ end
144
+
145
+ class Photo < ActiveRecord::Base
146
+ acts_as_taggable_on :locations
147
+ end
148
+
149
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
150
+ @some_user.owned_taggings
151
+ @some_user.owned_tags
152
+ @some_photo.locations_from(@some_user)
153
+
154
+ === Tag cloud calculations
155
+
156
+ To construct tag clouds, the frequency of each tag needs to be calculated.
157
+ Because we specified +acts_as_taggable_on+ on the <tt>User</tt> class, we can
158
+ 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
159
+ an single user's posts? To achieve this we call tag_counts on the association:
160
+
161
+ User.find(:first).posts.tag_counts_on(:tags)
162
+
163
+ A helper is included to assist with generating tag clouds.
164
+
165
+ Here is an example that generates a tag cloud.
166
+
167
+ Helper:
168
+
169
+ module PostsHelper
170
+ include TagsHelper
171
+ end
172
+
173
+ Controller:
174
+
175
+ class PostController < ApplicationController
176
+ def tag_cloud
177
+ @tags = Post.tag_counts_on(:tags)
178
+ end
179
+ end
180
+
181
+ View:
182
+
183
+ <% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
184
+ <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
185
+ <% end %>
186
+
187
+ CSS:
188
+
189
+ .css1 { font-size: 1.0em; }
190
+ .css2 { font-size: 1.2em; }
191
+ .css3 { font-size: 1.4em; }
192
+ .css4 { font-size: 1.6em; }
193
+
194
+ == Contributors
195
+
196
+ * TomEric (i76) - Maintainer
197
+ * Michael Bleigh - Original Author
198
+ * Brendan Lim - Related Objects
199
+ * Pradeep Elankumaran - Taggers
200
+ * Sinclair Bain - Patch King
201
+
202
+ == Patch Contributors
203
+
204
+ * tristanzdunn - Related objects of other classes
205
+ * azabaj - Fixed migrate down
206
+ * Peter Cooper - named_scope fix
207
+ * slainer68 - STI fix
208
+ * harrylove - migration instructions and fix-ups
209
+ * lawrencepit - cached tag work
210
+ * sobrinho - fixed tag_cloud helper
211
+
212
+ Copyright (c) 2007-2009 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ gem 'rspec', '2.0.0.beta.1'
2
+ require 'rspec/core/rake_task'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = "acts-as-taggable-on"
8
+ gemspec.summary = "ActsAsTaggableOn is a tagging plugin for Rails that provides multiple tagging contexts on a single model."
9
+ gemspec.description = "With ActsAsTaggableOn, you could tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality."
10
+ gemspec.email = "michael@intridea.com"
11
+ gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
12
+ gemspec.authors = ["Michael Bleigh"]
13
+ gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ desc 'Default: run specs'
21
+ task :default => :spec
22
+ Rspec::Core::RakeTask.new do |t|
23
+ t.pattern = "spec/**/*_spec.rb"
24
+ end
25
+
26
+ Rspec::Core::RakeTask.new('rcov') do |t|
27
+ t.pattern = "spec/**/*_spec.rb"
28
+ t.rcov = true
29
+ t.rcov_opts = ['--exclude', 'spec']
30
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.0.pre1
@@ -0,0 +1,15 @@
1
+ require "active_record"
2
+ require "action_view"
3
+
4
+ require "acts_as_taggable_on/group_helper"
5
+ require "acts_as_taggable_on/acts_as_taggable_on"
6
+ require "acts_as_taggable_on/acts_as_tagger"
7
+ require "acts_as_taggable_on/tag"
8
+ require "acts_as_taggable_on/tag_list"
9
+ require "acts_as_taggable_on/tags_helper"
10
+ require "acts_as_taggable_on/tagging"
11
+
12
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
13
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
14
+
15
+ ActionView::Base.send :include, TagsHelper
@@ -0,0 +1,426 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def taggable?
12
+ false
13
+ end
14
+
15
+ def acts_as_taggable
16
+ acts_as_taggable_on :tags
17
+ end
18
+
19
+ def acts_as_taggable_on(*args)
20
+ args.flatten! if args
21
+ args.compact! if args
22
+
23
+ for tag_type in args
24
+ tag_type = tag_type.to_s
25
+ # use aliased_join_table_name for context condition so that sphinx can join multiple
26
+ # tag references from same model without getting an ambiguous column error
27
+ class_eval do
28
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
29
+ :include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
30
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
31
+ end
32
+
33
+ class_eval <<-RUBY
34
+
35
+ def self.taggable?
36
+ true
37
+ end
38
+
39
+ def self.caching_#{tag_type.singularize}_list?
40
+ caching_tag_list_on?("#{tag_type}")
41
+ end
42
+
43
+ def self.#{tag_type.singularize}_counts(options={})
44
+ tag_counts_on('#{tag_type}',options)
45
+ end
46
+
47
+ def #{tag_type.singularize}_list
48
+ tag_list_on('#{tag_type}')
49
+ end
50
+
51
+ def #{tag_type.singularize}_list=(new_tags)
52
+ set_tag_list_on('#{tag_type}',new_tags)
53
+ end
54
+
55
+ def #{tag_type.singularize}_counts(options = {})
56
+ tag_counts_on('#{tag_type}',options)
57
+ end
58
+
59
+ def #{tag_type}_from(owner)
60
+ tag_list_on('#{tag_type}', owner)
61
+ end
62
+
63
+ def find_related_#{tag_type}(options = {})
64
+ related_tags_for('#{tag_type}', self.class, options)
65
+ end
66
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
67
+
68
+ def find_related_#{tag_type}_for(klass, options = {})
69
+ related_tags_for('#{tag_type}', klass, options)
70
+ end
71
+
72
+ def find_matching_contexts(search_context, result_context, options = {})
73
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
74
+ end
75
+
76
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
77
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
78
+ end
79
+
80
+ def top_#{tag_type}(limit = 10)
81
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
82
+ end
83
+
84
+ def self.top_#{tag_type}(limit = 10)
85
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
86
+ end
87
+
88
+ RUBY
89
+ end
90
+
91
+ if respond_to?(:tag_types)
92
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
93
+ else
94
+ class_eval do
95
+ write_inheritable_attribute(:tag_types, args.uniq)
96
+ class_inheritable_reader :tag_types
97
+
98
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
99
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
100
+
101
+ attr_writer :custom_contexts
102
+
103
+ before_save :save_cached_tag_list
104
+
105
+ after_save:save_tags
106
+
107
+ scope :tagged_with, lambda{ |*args|
108
+ find_options_for_find_tagged_with(*args)
109
+ }
110
+ end
111
+
112
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
113
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
114
+ alias_method_chain :reload, :tag_list
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ module SingletonMethods
121
+
122
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
123
+
124
+ # Pass either a tag string, or an array of strings or tags
125
+ #
126
+ # Options:
127
+ # :any - find models that match any of the given tags
128
+ # :exclude - Find models that are not tagged with the given tags
129
+ # :match_all - Find models that match all of the given tags, not just one
130
+ # :conditions - A piece of SQL conditions to add to the query
131
+ # :on - scopes the find to a context
132
+ # def find_tagged_with(*args)
133
+ # find_options_for_find_tagged_with(*args)
134
+ # end
135
+
136
+ def caching_tag_list_on?(context)
137
+ column_names.include?("cached_#{context.to_s.singularize}_list")
138
+ end
139
+
140
+ def tag_counts_on(context, options = {})
141
+ find_for_tag_counts(options.merge({:on => context.to_s}))
142
+ end
143
+
144
+ def all_tag_counts(options = {})
145
+ find_for_tag_counts(options)
146
+ end
147
+
148
+ def find_options_for_find_tagged_with(tags, options = {})
149
+ tag_list = TagList.from(tags)
150
+
151
+ return {} if tag_list.empty?
152
+
153
+ joins = []
154
+ conditions = []
155
+
156
+ context = options.delete(:on)
157
+
158
+ if options.delete(:exclude)
159
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
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)})"
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
+
166
+ else
167
+ tags = Tag.named_any(tag_list)
168
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
169
+
170
+ tags.each do |tag|
171
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
172
+ prefix = "#{safe_tag}_#{rand(1024)}"
173
+
174
+ taggings_alias = "#{table_name}_taggings_#{prefix}"
175
+
176
+ tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
177
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
178
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
179
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
180
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
181
+
182
+ joins << tagging_join
183
+ end
184
+ end
185
+
186
+ taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
187
+
188
+ if options.delete(:match_all)
189
+ joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
190
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
191
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
192
+
193
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
194
+ end
195
+
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)
202
+ end
203
+
204
+ # Calculate the tag counts for all tags.
205
+ #
206
+ # Options:
207
+ # :start_at - Restrict the tags to those created after a certain time
208
+ # :end_at - Restrict the tags to those created before a certain time
209
+ # :conditions - A piece of SQL conditions to add to the query
210
+ # :limit - The maximum number of tags to return
211
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
212
+ # :at_least - Exclude tags with a frequency less than the given value
213
+ # :at_most - Exclude tags with a frequency greater than the given value
214
+ # :on - Scope the find to only include a certain context
215
+ def find_for_tag_counts(options = {})
216
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
217
+
218
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
219
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
220
+
221
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
222
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
223
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
224
+
225
+ conditions = [
226
+ taggable_type,
227
+ taggable_id,
228
+ options[:conditions],
229
+ start_at,
230
+ end_at
231
+ ]
232
+
233
+ conditions = conditions.compact.join(' AND ')
234
+
235
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
236
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
237
+ joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
238
+
239
+ unless descends_from_active_record?
240
+ # Current model is STI descendant, so add type checking to the join condition
241
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
242
+ end
243
+
244
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
245
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
246
+ having = [at_least, at_most].compact.join(' AND ')
247
+ group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
248
+ group_by << " AND #{having}" unless having.blank?
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
+
252
+ end
253
+
254
+ def is_taggable?
255
+ true
256
+ end
257
+
258
+ end
259
+
260
+ module InstanceMethods
261
+
262
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
263
+
264
+ def custom_contexts
265
+ @custom_contexts ||= []
266
+ end
267
+
268
+ def is_taggable?
269
+ self.class.is_taggable?
270
+ end
271
+
272
+ def add_custom_context(value)
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)
274
+ end
275
+
276
+ def tag_list_on(context, owner = nil)
277
+ add_custom_context(context)
278
+ cache = tag_list_cache_on(context)
279
+ return owner ? cache[owner] : cache[owner] if cache[owner]
280
+
281
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
282
+ cache[owner] = TagList.from(cached_tag_list_on(context))
283
+ else
284
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
285
+ end
286
+ end
287
+
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)
300
+ if owner
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]
302
+ else
303
+ opts = ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]
304
+ end
305
+ base_tags.where(opts)
306
+ end
307
+
308
+ def cached_tag_list_on(context)
309
+ self["cached_#{context.to_s.singularize}_list"]
310
+ end
311
+
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)
321
+ add_custom_context(context)
322
+ end
323
+
324
+ def tag_counts_on(context, options={})
325
+ self.class.tag_counts_on(context, options.merge(:id => id))
326
+ end
327
+
328
+ def related_tags_for(context, klass, options = {})
329
+ search_conditions = related_search_options(context, klass, options)
330
+
331
+ klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
332
+ end
333
+
334
+ def related_search_options(context, klass, options = {})
335
+ tags_to_find = tags_on(context).collect { |t| t.name }
336
+
337
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
338
+
339
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
340
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
341
+ :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],
342
+ :group => grouped_column_names_for(klass),
343
+ :order => "count DESC"
344
+ }.update(options)
345
+ end
346
+
347
+ def matching_contexts_for(search_context, result_context, klass, options = {})
348
+ search_conditions = matching_context_search_options(search_context, result_context, klass, options)
349
+
350
+ klass.select(search_conditions[:select]).from(search_conditions[:from]).where(search_conditions[:conditions]).group(search_conditions[:group]).order(search_conditions[:order])
351
+ end
352
+
353
+ def matching_context_search_options(search_context, result_context, klass, options = {})
354
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
355
+
356
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
357
+
358
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
359
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
360
+ :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],
361
+ :group => grouped_column_names_for(klass),
362
+ :order => "count DESC"
363
+ }.update(options)
364
+ end
365
+
366
+ def save_cached_tag_list
367
+ self.class.tag_types.map(&:to_s).each do |tag_type|
368
+ if self.class.send("caching_#{tag_type.singularize}_list?")
369
+ self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
370
+ end
371
+ end
372
+ end
373
+
374
+ def save_tags
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
408
+ end
409
+ end
410
+ end
411
+
412
+ true
413
+ end
414
+
415
+ def reload_with_tag_list(*args)
416
+ self.class.tag_types.each do |tag_type|
417
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
418
+ end
419
+
420
+ reload_without_tag_list(*args)
421
+ end
422
+
423
+ end
424
+ end
425
+ end
426
+ end