acts-as-taggable-on 0.0.0 → 1.0.6

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.
Files changed (38) hide show
  1. data/CHANGELOG +2 -9
  2. data/README +196 -0
  3. data/Rakefile +10 -47
  4. data/VERSION +1 -1
  5. data/lib/acts-as-taggable-on.rb +6 -30
  6. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +313 -28
  7. data/lib/acts_as_taggable_on/acts_as_tagger.rb +45 -40
  8. data/lib/acts_as_taggable_on/tag.rb +6 -48
  9. data/lib/acts_as_taggable_on/tag_list.rb +29 -31
  10. data/lib/acts_as_taggable_on/tagging.rb +1 -18
  11. data/lib/acts_as_taggable_on/tags_helper.rb +2 -8
  12. data/rails/init.rb +6 -1
  13. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +47 -148
  14. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +4 -46
  15. data/spec/acts_as_taggable_on/tag_list_spec.rb +0 -29
  16. data/spec/acts_as_taggable_on/tag_spec.rb +7 -95
  17. data/spec/acts_as_taggable_on/taggable_spec.rb +48 -178
  18. data/spec/acts_as_taggable_on/tagger_spec.rb +5 -57
  19. data/spec/acts_as_taggable_on/tagging_spec.rb +1 -25
  20. data/spec/schema.rb +2 -12
  21. data/spec/spec.opts +6 -1
  22. data/spec/spec_helper.rb +34 -35
  23. metadata +7 -31
  24. data/Gemfile +0 -6
  25. data/README.rdoc +0 -212
  26. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +0 -56
  27. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +0 -97
  28. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +0 -220
  29. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -29
  30. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +0 -101
  31. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +0 -64
  32. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  33. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  34. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  35. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +0 -28
  36. data/spec/acts_as_taggable_on/tags_helper_spec.rb +0 -28
  37. data/spec/bm.rb +0 -52
  38. data/spec/models.rb +0 -36
data/CHANGELOG CHANGED
@@ -1,10 +1,3 @@
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
1
  == 2008-07-17
9
2
 
10
3
  * Can now use a named_scope to find tags!
@@ -15,10 +8,10 @@
15
8
  * Removed extraneous down migration cruft (azabaj)
16
9
 
17
10
  == 2008-06-09
18
-
11
+
19
12
  * Added support for Single Table Inheritance
20
13
  * Adding gemspec and rails/init.rb for gemified plugin
21
-
14
+
22
15
  == 2007-12-12
23
16
 
24
17
  * Added ability to use dynamic tag contexts
data/README ADDED
@@ -0,0 +1,196 @@
1
+ ActsAsTaggableOn
2
+ ================
3
+
4
+ This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
5
+ It has evolved substantially since that point, but all credit goes to him for the
6
+ initial tagging functionality that so many people have used.
7
+
8
+ For instance, in a social network, a user might have tags that are called skills,
9
+ interests, sports, and more. There is no real way to differentiate between tags and
10
+ so an implementation of this type is not possible with acts as taggable on steroids.
11
+
12
+ Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
13
+ (namely "tags"), acts as taggable on allows you to specify an arbitrary number of
14
+ tag "contexts" that can be used locally or in combination in the same way steroids
15
+ was used.
16
+
17
+ Installation
18
+ ============
19
+
20
+ Plugin
21
+ ------
22
+
23
+ Acts As Taggable On is available both as a gem and as a traditional plugin. For the
24
+ traditional plugin you can install like so (Rails 2.1 or later):
25
+
26
+ script/plugin install git://github.com/mbleigh/acts-as-taggable-on.git
27
+
28
+ For earlier versions:
29
+
30
+ git clone git://github.com/mbleigh/acts-as-taggable-on.git vendor/plugins/acts-as-taggable-on
31
+
32
+ GemPlugin
33
+ ---------
34
+
35
+ Acts As Taggable On is also available as a gem plugin using Rails 2.1's gem dependencies.
36
+ To install the gem, add this to your config/environment.rb:
37
+
38
+ config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com", :lib => "acts-as-taggable-on"
39
+
40
+ After that, you can run "rake gems:install" to install the gem if you don't already have it.
41
+ See http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies for
42
+ additional details about gem dependencies in Rails.
43
+
44
+ ** NOTE **
45
+ Some issues have been experienced with "rake gems:install". If that doesn't work to install the gem,
46
+ try just installing it as a normal gem:
47
+
48
+ gem install mbleigh-acts-as-taggable-on --source http://gems.github.com
49
+
50
+ Post Installation (Rails)
51
+ -------------------------
52
+ 1. script/generate acts_as_taggable_on_migration
53
+ 2. rake db:migrate
54
+
55
+ Testing
56
+ =======
57
+
58
+ Acts As Taggable On uses RSpec for its test coverage. Inside the plugin
59
+ directory, you can run the specs with:
60
+
61
+ rake spec
62
+
63
+
64
+ If you already have RSpec on your application, the specs will run while using:
65
+
66
+ rake spec:plugins
67
+
68
+
69
+ Example
70
+ =======
71
+
72
+ class User < ActiveRecord::Base
73
+ acts_as_taggable_on :tags, :skills, :interests
74
+ end
75
+
76
+ @user = User.new(:name => "Bobby")
77
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
78
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
79
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
80
+ @user.save
81
+
82
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
83
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
84
+
85
+ # The old way
86
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
87
+ User.find_tagged_with("awesome", :on => :skills) # => []
88
+
89
+ # The better way (utilizes named_scope)
90
+ User.tagged_with("awesome", :on => :tags) # => [@user]
91
+ User.tagged_with("awesome", :on => :skills) # => []
92
+
93
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
94
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
95
+ @frankie.skill_counts
96
+
97
+ Finding Tagged Objects
98
+ ======================
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
+
115
+ You can find objects of the same type based on similar tags on certain contexts.
116
+ Also, objects will be returned in descending order based on the total number of
117
+ matched tags.
118
+
119
+ @bobby = User.find_by_name("Bobby")
120
+ @bobby.skill_list # => ["jogging", "diving"]
121
+
122
+ @frankie = User.find_by_name("Frankie")
123
+ @frankie.skill_list # => ["hacking"]
124
+
125
+ @tom = User.find_by_name("Tom")
126
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
127
+
128
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
129
+ @bobby.find_related_skills # => [<User name="Tom">]
130
+ @frankie.find_related_skills # => [<User name="Tom">]
131
+
132
+
133
+ Dynamic Tag Contexts
134
+ ====================
135
+
136
+ In addition to the generated tag contexts in the definition, it is also possible
137
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
138
+
139
+ @user = User.new(:name => "Bobby")
140
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
141
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
142
+ @user.save
143
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
144
+ @user.tag_counts_on(:customs)
145
+ User.find_tagged_with("same", :on => :customs) # => [@user]
146
+
147
+ Tag Ownership
148
+ =============
149
+
150
+ Tags can have owners:
151
+
152
+ class User < ActiveRecord::Base
153
+ acts_as_tagger
154
+ end
155
+
156
+ class Photo < ActiveRecord::Base
157
+ acts_as_taggable_on :locations
158
+ end
159
+
160
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
161
+ @some_user.owned_taggings
162
+ @some_user.owned_tags
163
+ @some_photo.locations_from(@some_user)
164
+
165
+ Caveats, Uncharted Waters
166
+ =========================
167
+
168
+ This plugin is still under active development. Tag caching has not
169
+ been thoroughly (or even casually) tested and may not work as expected.
170
+
171
+ Contributors
172
+ ============
173
+
174
+ * Michael Bleigh - Original Author
175
+ * Brendan Lim - Related Objects
176
+ * Pradeep Elankumaran - Taggers
177
+ * Sinclair Bain - Patch King
178
+
179
+ Patch Contributors
180
+ ------------------
181
+
182
+ * tristanzdunn - Related objects of other classes
183
+ * azabaj - Fixed migrate down
184
+ * Peter Cooper - named_scope fix
185
+ * slainer68 - STI fix
186
+ * harrylove - migration instructions and fix-ups
187
+ * lawrencepit - cached tag work
188
+
189
+ Resources
190
+ =========
191
+
192
+ * Acts As Community - http://www.actsascommunity.com/projects/acts-as-taggable-on
193
+ * GitHub - http://github.com/mbleigh/acts-as-taggable-on
194
+ * Lighthouse - http://mbleigh.lighthouseapp.com/projects/10116-acts-as-taggable-on
195
+
196
+ Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
data/Rakefile CHANGED
@@ -1,46 +1,4 @@
1
- begin
2
- # Rspec 1.3.0
3
- require 'spec/rake/spectask'
4
-
5
- desc 'Default: run specs'
6
- task :default => :spec
7
- Spec::Rake::SpecTask.new do |t|
8
- t.spec_files = FileList["spec/**/*_spec.rb"]
9
- end
10
-
11
- Spec::Rake::SpecTask.new('rcov') do |t|
12
- t.spec_files = FileList["spec/**/*_spec.rb"]
13
- t.rcov = true
14
- t.rcov_opts = ['--exclude', 'spec']
15
- end
16
-
17
- rescue LoadError
18
- # Rspec 2.0
19
- require 'rspec/core/rake_task'
20
-
21
- desc 'Default: run specs'
22
- task :default => :spec
23
- Rspec::Core::RakeTask.new do |t|
24
- t.pattern = "spec/**/*_spec.rb"
25
- end
26
-
27
- Rspec::Core::RakeTask.new('rcov') do |t|
28
- t.pattern = "spec/**/*_spec.rb"
29
- t.rcov = true
30
- t.rcov_opts = ['--exclude', 'spec']
31
- end
32
-
33
- rescue LoadError
34
- puts "Rspec not available. Install it with: gem install rspec"
35
- end
36
-
37
- namespace 'rails2.3' do
38
- task :spec do
39
- gemfile = File.join(File.dirname(__FILE__), 'lib', 'acts_as_taggable_on', 'compatibility', 'Gemfile')
40
- ENV['BUNDLE_GEMFILE'] = gemfile
41
- Rake::Task['spec'].invoke
42
- end
43
- end
1
+ require 'spec/rake/spectask'
44
2
 
45
3
  begin
46
4
  require 'jeweler'
@@ -51,9 +9,14 @@ begin
51
9
  gemspec.email = "michael@intridea.com"
52
10
  gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
53
11
  gemspec.authors = ["Michael Bleigh"]
54
- gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
12
+ gemspec.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"] - FileList["**/*.log"]
55
13
  end
56
- Jeweler::GemcutterTasks.new
57
14
  rescue LoadError
58
- puts "Jeweler not available. Install it with: gem install jeweler"
59
- end
15
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
16
+ end
17
+
18
+ desc 'Default: run specs'
19
+ task :default => :spec
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.spec_files = FileList["spec/**/*_spec.rb"]
22
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 1.0.6
@@ -1,30 +1,6 @@
1
- require "active_record"
2
- require "action_view"
3
-
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
5
-
6
- require "acts_as_taggable_on/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
7
-
8
- require "acts_as_taggable_on/acts_as_taggable_on"
9
- require "acts_as_taggable_on/acts_as_taggable_on/core"
10
- require "acts_as_taggable_on/acts_as_taggable_on/collection"
11
- require "acts_as_taggable_on/acts_as_taggable_on/cache"
12
- require "acts_as_taggable_on/acts_as_taggable_on/ownership"
13
- require "acts_as_taggable_on/acts_as_taggable_on/related"
14
-
15
- require "acts_as_taggable_on/acts_as_tagger"
16
- require "acts_as_taggable_on/tag"
17
- require "acts_as_taggable_on/tag_list"
18
- require "acts_as_taggable_on/tags_helper"
19
- require "acts_as_taggable_on/tagging"
20
-
21
- $LOAD_PATH.shift
22
-
23
- if defined?(ActiveRecord::Base)
24
- ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
25
- ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
26
- end
27
-
28
- if defined?(ActionView::Base)
29
- ActionView::Base.send :include, TagsHelper
30
- end
1
+ require 'acts_as_taggable_on/acts_as_taggable_on'
2
+ require 'acts_as_taggable_on/acts_as_tagger'
3
+ require 'acts_as_taggable_on/tag'
4
+ require 'acts_as_taggable_on/tag_list'
5
+ require 'acts_as_taggable_on/tags_helper'
6
+ require 'acts_as_taggable_on/tagging'
@@ -1,39 +1,324 @@
1
- module ActsAsTaggableOn
2
- module Taggable
3
- def taggable?
4
- false
5
- end
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 "taggings"}.context = ?',tag_type], :class_name => "Tagging"
27
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
28
+ end
29
+
30
+ self.class_eval <<-RUBY
31
+ def self.taggable?
32
+ true
33
+ end
34
+
35
+ def self.caching_#{tag_type.singularize}_list?
36
+ caching_tag_list_on?("#{tag_type}")
37
+ end
38
+
39
+ def self.#{tag_type.singularize}_counts(options={})
40
+ tag_counts_on('#{tag_type}',options)
41
+ end
42
+
43
+ def #{tag_type.singularize}_list
44
+ tag_list_on('#{tag_type}')
45
+ end
46
+
47
+ def #{tag_type.singularize}_list=(new_tags)
48
+ set_tag_list_on('#{tag_type}',new_tags)
49
+ end
50
+
51
+ def #{tag_type.singularize}_counts(options = {})
52
+ tag_counts_on('#{tag_type}',options)
53
+ end
54
+
55
+ def #{tag_type}_from(owner)
56
+ tag_list_on('#{tag_type}', owner)
57
+ end
58
+
59
+ def find_related_#{tag_type}(options = {})
60
+ related_tags_for('#{tag_type}', self.class, options)
61
+ end
62
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
6
63
 
7
- def acts_as_taggable
8
- acts_as_taggable_on :tags
9
- end
64
+ def find_related_#{tag_type}_for(klass, options = {})
65
+ related_tags_for('#{tag_type}', klass, options)
66
+ end
67
+
68
+ def top_#{tag_type}(limit = 10)
69
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
70
+ end
71
+ RUBY
72
+ end
73
+
74
+ if respond_to?(:tag_types)
75
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
76
+ else
77
+ self.class_eval do
78
+ write_inheritable_attribute(:tag_types, args.uniq)
79
+ class_inheritable_reader :tag_types
80
+
81
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
82
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
83
+
84
+ attr_writer :custom_contexts
85
+
86
+ before_save :save_cached_tag_list
87
+ after_save :save_tags
88
+
89
+ if respond_to?(:named_scope)
90
+ named_scope :tagged_with, lambda{ |tags, options|
91
+ find_options_for_find_tagged_with(tags, options)
92
+ }
93
+ end
94
+ end
95
+
96
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
97
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
98
+ alias_method_chain :reload, :tag_list
99
+ end
100
+ end
101
+
102
+ def is_taggable?
103
+ false
104
+ end
105
+ end
106
+
107
+ module SingletonMethods
108
+ # Pass either a tag string, or an array of strings or tags
109
+ #
110
+ # Options:
111
+ # :exclude - Find models that are not tagged with the given tags
112
+ # :match_all - Find models that match all of the given tags, not just one
113
+ # :conditions - A piece of SQL conditions to add to the query
114
+ # :on - scopes the find to a context
115
+ def find_tagged_with(*args)
116
+ options = find_options_for_find_tagged_with(*args)
117
+ options.blank? ? [] : find(:all,options)
118
+ end
119
+
120
+ def caching_tag_list_on?(context)
121
+ column_names.include?("cached_#{context.to_s.singularize}_list")
122
+ end
123
+
124
+ def tag_counts_on(context, options = {})
125
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
126
+ end
127
+
128
+ def find_options_for_find_tagged_with(tags, options = {})
129
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
10
130
 
11
- def acts_as_taggable_on(*tag_types)
12
- tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
131
+ return {} if tags.empty?
13
132
 
14
- if taggable?
15
- write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
16
- else
17
- if ::ActiveRecord::VERSION::MAJOR < 3
18
- include ActsAsTaggableOn::ActiveRecord::Backports
19
- end
133
+ conditions = []
134
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
135
+
136
+ unless (on = options.delete(:on)).nil?
137
+ conditions << sanitize_sql(["context = ?",on.to_s])
138
+ end
139
+
140
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
20
141
 
21
- write_inheritable_attribute(:tag_types, tag_types)
22
- class_inheritable_reader(:tag_types)
142
+ if options.delete(:exclude)
143
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
144
+ conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
145
+ else
146
+ conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")
147
+
148
+ if options.delete(:match_all)
149
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
150
+ end
151
+ end
152
+
153
+ { :select => "DISTINCT #{table_name}.*",
154
+ :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
155
+ "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
156
+ :conditions => conditions.join(" AND "),
157
+ :group => group
158
+ }.update(options)
159
+ end
23
160
 
24
- class_eval do
25
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
26
- has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
161
+ # Calculate the tag counts for all tags.
162
+ #
163
+ # Options:
164
+ # :start_at - Restrict the tags to those created after a certain time
165
+ # :end_at - Restrict the tags to those created before a certain time
166
+ # :conditions - A piece of SQL conditions to add to the query
167
+ # :limit - The maximum number of tags to return
168
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
169
+ # :at_least - Exclude tags with a frequency less than the given value
170
+ # :at_most - Exclude tags with a frequency greater than the given value
171
+ # :on - Scope the find to only include a certain context
172
+ def find_options_for_tag_counts(options = {})
173
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
174
+
175
+ scope = scope(:find)
176
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
177
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
178
+
179
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
180
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
181
+
182
+ conditions = [
183
+ taggable_type,
184
+ taggable_id,
185
+ options[:conditions],
186
+ start_at,
187
+ end_at
188
+ ]
189
+
190
+ conditions = conditions.compact.join(' AND ')
191
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
27
192
 
28
- def self.taggable?
29
- true
193
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
194
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
195
+ joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
196
+ joins << scope[:joins] if scope && scope[:joins]
197
+
198
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
199
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
200
+ having = [at_least, at_most].compact.join(' AND ')
201
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
202
+ group_by << " AND #{having}" unless having.blank?
203
+
204
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
205
+ :joins => joins.join(" "),
206
+ :conditions => conditions,
207
+ :group => group_by
208
+ }
209
+ end
210
+
211
+ def is_taggable?
212
+ true
213
+ end
214
+ end
215
+
216
+ module InstanceMethods
217
+
218
+ def tag_types
219
+ self.class.tag_types
220
+ end
221
+
222
+ def custom_contexts
223
+ @custom_contexts ||= []
224
+ end
225
+
226
+ def is_taggable?
227
+ self.class.is_taggable?
228
+ end
229
+
230
+ def add_custom_context(value)
231
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
232
+ end
233
+
234
+ def tag_list_on(context, owner=nil)
235
+ var_name = context.to_s.singularize + "_list"
236
+ add_custom_context(context)
237
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
238
+
239
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
240
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
241
+ else
242
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
243
+ end
244
+ end
245
+
246
+ def tags_on(context, owner=nil)
247
+ if owner
248
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
249
+ context.to_s, owner.id, owner.class.to_s]}
250
+ else
251
+ opts = {:conditions => ["context = ?", context.to_s]}
252
+ end
253
+ base_tags.find(:all, opts)
254
+ end
255
+
256
+ def cached_tag_list_on(context)
257
+ self["cached_#{context.to_s.singularize}_list"]
258
+ end
259
+
260
+ def set_tag_list_on(context,new_list, tagger=nil)
261
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
262
+ add_custom_context(context)
263
+ end
264
+
265
+ def tag_counts_on(context, options={})
266
+ self.class.tag_counts_on(context, options.merge(:id => self.id))
267
+ end
268
+
269
+ def related_tags_for(context, klass, options = {})
270
+ search_conditions = related_search_options(context, klass, options)
271
+
272
+ klass.find(:all, search_conditions)
273
+ end
274
+
275
+ def related_search_options(context, klass, options = {})
276
+ tags_to_find = self.tags_on(context).collect { |t| t.name }
277
+
278
+ exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
279
+
280
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
281
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
282
+ :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],
283
+ :group => "#{klass.table_name}.id",
284
+ :order => "count DESC"
285
+ }.update(options)
286
+ end
287
+
288
+ def save_cached_tag_list
289
+ self.class.tag_types.map(&:to_s).each do |tag_type|
290
+ if self.class.send("caching_#{tag_type.singularize}_list?")
291
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
292
+ end
30
293
  end
294
+ end
31
295
 
32
- include ActsAsTaggableOn::Taggable::Core
33
- include ActsAsTaggableOn::Taggable::Collection
34
- include ActsAsTaggableOn::Taggable::Cache
35
- include ActsAsTaggableOn::Taggable::Ownership
36
- include ActsAsTaggableOn::Taggable::Related
296
+ def save_tags
297
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
298
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
299
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
300
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
301
+ old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
302
+
303
+ self.class.transaction do
304
+ base_tags.delete(*old_tags) if old_tags.any?
305
+ new_tag_names.each do |new_tag_name|
306
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
307
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
308
+ :taggable => self, :tagger => owner)
309
+ end
310
+ end
311
+ end
312
+
313
+ true
314
+ end
315
+
316
+ def reload_with_tag_list(*args)
317
+ self.class.tag_types.each do |tag_type|
318
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
319
+ end
320
+
321
+ reload_without_tag_list(*args)
37
322
  end
38
323
  end
39
324
  end