bbenezech-acts-as-taggable-on 0.0.2

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