acts-as-taggable-on 0.0.0 → 1.0.6

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