bbenezech-acts-as-taggable-on 0.0.2

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