acts-as-taggable-on-simonwh 2.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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