acts-as-taggable-on-for-domains 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ spec/debug.log
7
+ *.sqlite3
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Josh N. Abbott
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,194 @@
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 "acts-as-taggable-on", :source => "http://gemcutter.org"
39
+
40
+ After that, you can run "rake gems:install" to install the gem if you don't already have it.
41
+
42
+ ** NOTE **
43
+ Some issues have been experienced with "rake gems:install". If that doesn't work to install the gem,
44
+ try just installing it as a normal gem:
45
+
46
+ gem install acts-as-taggable-on --source http://gemcutter.org
47
+
48
+ Post Installation (Rails)
49
+ -------------------------
50
+ 1. script/generate acts_as_taggable_on_migration
51
+ 2. rake db:migrate
52
+
53
+ Testing
54
+ =======
55
+
56
+ Acts As Taggable On uses RSpec for its test coverage. Inside the plugin
57
+ directory, you can run the specs with:
58
+
59
+ rake spec
60
+
61
+
62
+ If you already have RSpec on your application, the specs will run while using:
63
+
64
+ rake spec:plugins
65
+
66
+
67
+ Example
68
+ =======
69
+
70
+ class User < ActiveRecord::Base
71
+ acts_as_taggable_on :tags, :skills, :interests
72
+ end
73
+
74
+ @user = User.new(:name => "Bobby")
75
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
76
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
77
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
78
+ @user.save
79
+
80
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
81
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
82
+
83
+ # The old way
84
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
85
+ User.find_tagged_with("awesome", :on => :skills) # => []
86
+
87
+ # The better way (utilizes named_scope)
88
+ User.tagged_with("awesome", :on => :tags) # => [@user]
89
+ User.tagged_with("awesome", :on => :skills) # => []
90
+
91
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
92
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
93
+ @frankie.skill_counts
94
+
95
+ Finding Tagged Objects
96
+ ======================
97
+
98
+ Acts As Taggable On utilizes Rails 2.1's named_scope to create an association
99
+ for tags. This way you can mix and match to filter down your results, and it
100
+ also improves compatibility with the will_paginate gem:
101
+
102
+ class User < ActiveRecord::Base
103
+ acts_as_taggable_on :tags
104
+ named_scope :by_join_date, :order => "created_at DESC"
105
+ end
106
+
107
+ User.tagged_with("awesome").by_date
108
+ User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
109
+
110
+ Relationships
111
+ =============
112
+
113
+ You can find objects of the same type based on similar tags on certain contexts.
114
+ Also, objects will be returned in descending order based on the total number of
115
+ matched tags.
116
+
117
+ @bobby = User.find_by_name("Bobby")
118
+ @bobby.skill_list # => ["jogging", "diving"]
119
+
120
+ @frankie = User.find_by_name("Frankie")
121
+ @frankie.skill_list # => ["hacking"]
122
+
123
+ @tom = User.find_by_name("Tom")
124
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
125
+
126
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
127
+ @bobby.find_related_skills # => [<User name="Tom">]
128
+ @frankie.find_related_skills # => [<User name="Tom">]
129
+
130
+
131
+ Dynamic Tag Contexts
132
+ ====================
133
+
134
+ In addition to the generated tag contexts in the definition, it is also possible
135
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
136
+
137
+ @user = User.new(:name => "Bobby")
138
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
139
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
140
+ @user.save
141
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
142
+ @user.tag_counts_on(:customs)
143
+ User.find_tagged_with("same", :on => :customs) # => [@user]
144
+
145
+ Tag Ownership
146
+ =============
147
+
148
+ Tags can have owners:
149
+
150
+ class User < ActiveRecord::Base
151
+ acts_as_tagger
152
+ end
153
+
154
+ class Photo < ActiveRecord::Base
155
+ acts_as_taggable_on :locations
156
+ end
157
+
158
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
159
+ @some_user.owned_taggings
160
+ @some_user.owned_tags
161
+ @some_photo.locations_from(@some_user)
162
+
163
+ Caveats, Uncharted Waters
164
+ =========================
165
+
166
+ This plugin is still under active development. Tag caching has not
167
+ been thoroughly (or even casually) tested and may not work as expected.
168
+
169
+ Contributors
170
+ ============
171
+
172
+ * Michael Bleigh - Original Author
173
+ * Brendan Lim - Related Objects
174
+ * Pradeep Elankumaran - Taggers
175
+ * Sinclair Bain - Patch King
176
+
177
+ Patch Contributors
178
+ ------------------
179
+
180
+ * tristanzdunn - Related objects of other classes
181
+ * azabaj - Fixed migrate down
182
+ * Peter Cooper - named_scope fix
183
+ * slainer68 - STI fix
184
+ * harrylove - migration instructions and fix-ups
185
+ * lawrencepit - cached tag work
186
+
187
+ Resources
188
+ =========
189
+
190
+ * Acts As Community - http://www.actsascommunity.com/projects/acts-as-taggable-on
191
+ * GitHub - http://github.com/mbleigh/acts-as-taggable-on
192
+ * Lighthouse - http://mbleigh.lighthouseapp.com/projects/10116-acts-as-taggable-on
193
+
194
+ Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
data/README.rdoc ADDED
@@ -0,0 +1,18 @@
1
+ = acts-as-taggable-on-for-domains
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but
13
+ bump version in a commit by itself I can ignore when I pull)
14
+ * Send me a pull request. Bonus points for topic branches.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2009 Josh N. Abbott. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "acts-as-taggable-on-for-domains"
7
+ gemspec.summary = "The same as ActsAsTaggableOn but implements domain scoping."
8
+ gemspec.description = "Same as ActsAsTaggableOn but tagging happens within the context of a domain id. This means an object could be tagged with something for one domain, but the same object, in the context of a different domain id, could have completely different taggings."
9
+ gemspec.email = "joshnabbott@gmail.com"
10
+ gemspec.homepage = "http://github.com/joshnabbott/acts-as-taggable-on-for-domains"
11
+ gemspec.authors = ["Michael Bleigh", 'Josh N. Abbott']
12
+ # gemspec.files = FileList["[A-Z]*", "{generatorslib,spec,rails}/**/*"] - FileList["**/*.log"]
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ desc 'Default: run specs'
20
+ task :default => :spec
21
+ Spec::Rake::SpecTask.new do |t|
22
+ t.spec_files = FileList["spec/**/*_spec.rb"]
23
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,79 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{acts-as-taggable-on-for-domains}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Michael Bleigh", "Josh N. Abbott"]
12
+ s.date = %q{2009-11-13}
13
+ s.description = %q{Same as ActsAsTaggableOn but tagging happens within the context of a domain id. This means an object could be tagged with something for one domain, but the same object, in the context of a different domain id, could have completely different taggings.}
14
+ s.email = %q{joshnabbott@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ "LICENSE",
24
+ "README",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "acts-as-taggable-on-for-domains.gemspec",
29
+ "generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb",
30
+ "generators/acts_as_taggable_on_migration/templates/migration.rb",
31
+ "init.rb",
32
+ "lib/acts-as-taggable-on.rb",
33
+ "lib/acts_as_taggable_on/acts_as_taggable_on.rb",
34
+ "lib/acts_as_taggable_on/acts_as_taggable_on_scoped_by_domain.rb",
35
+ "lib/acts_as_taggable_on/acts_as_tagger.rb",
36
+ "lib/acts_as_taggable_on/tag.rb",
37
+ "lib/acts_as_taggable_on/tag_list.rb",
38
+ "lib/acts_as_taggable_on/tagging.rb",
39
+ "lib/acts_as_taggable_on/tags_helper.rb",
40
+ "rails/init.rb",
41
+ "spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb",
42
+ "spec/acts_as_taggable_on/acts_as_tagger_spec.rb",
43
+ "spec/acts_as_taggable_on/tag_list_spec.rb",
44
+ "spec/acts_as_taggable_on/tag_spec.rb",
45
+ "spec/acts_as_taggable_on/taggable_spec.rb",
46
+ "spec/acts_as_taggable_on/tagger_spec.rb",
47
+ "spec/acts_as_taggable_on/tagging_spec.rb",
48
+ "spec/schema.rb",
49
+ "spec/spec.opts",
50
+ "spec/spec_helper.rb",
51
+ "uninstall.rb"
52
+ ]
53
+ s.homepage = %q{http://github.com/joshnabbott/acts-as-taggable-on-for-domains}
54
+ s.rdoc_options = ["--charset=UTF-8"]
55
+ s.require_paths = ["lib"]
56
+ s.rubygems_version = %q{1.3.5}
57
+ s.summary = %q{The same as ActsAsTaggableOn but implements domain scoping.}
58
+ s.test_files = [
59
+ "spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb",
60
+ "spec/acts_as_taggable_on/acts_as_tagger_spec.rb",
61
+ "spec/acts_as_taggable_on/tag_list_spec.rb",
62
+ "spec/acts_as_taggable_on/tag_spec.rb",
63
+ "spec/acts_as_taggable_on/taggable_spec.rb",
64
+ "spec/acts_as_taggable_on/tagger_spec.rb",
65
+ "spec/acts_as_taggable_on/tagging_spec.rb",
66
+ "spec/schema.rb",
67
+ "spec/spec_helper.rb"
68
+ ]
69
+
70
+ if s.respond_to? :specification_version then
71
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
72
+ s.specification_version = 3
73
+
74
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
75
+ else
76
+ end
77
+ else
78
+ end
79
+ end
@@ -0,0 +1,7 @@
1
+ class ActsAsTaggableOnMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_taggable_on_migration"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ class ActsAsTaggableOnMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.column :name, :string
5
+ end
6
+
7
+ create_table :taggings do |t|
8
+ t.column :domain_id, :integer
9
+ t.column :tag_id, :integer
10
+ t.column :taggable_id, :integer
11
+ t.column :tagger_id, :integer
12
+ t.column :tagger_type, :string
13
+
14
+ # You should make sure that the column created is
15
+ # long enough to store the required class names.
16
+ t.column :taggable_type, :string
17
+ t.column :context, :string
18
+
19
+ t.column :created_at, :datetime
20
+ end
21
+
22
+ add_index :taggings, :tag_id
23
+ add_index :taggings, [:domain_id, :taggable_id, :taggable_type, :context]
24
+ end
25
+
26
+ def self.down
27
+ drop_table :taggings
28
+ drop_table :tags
29
+ end
30
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,8 @@
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'
7
+
8
+ require 'acts_as_taggable_on/acts_as_taggable_on_scoped_by_domain'
@@ -0,0 +1,358 @@
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}
63
+
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
+
72
+ def self.top_#{tag_type}(limit = 10)
73
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
74
+ end
75
+ RUBY
76
+ end
77
+
78
+ if respond_to?(:tag_types)
79
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
80
+ else
81
+ self.class_eval do
82
+ write_inheritable_attribute(:tag_types, args.uniq)
83
+ class_inheritable_reader :tag_types
84
+
85
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
86
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
87
+
88
+ attr_writer :custom_contexts
89
+
90
+ before_save :save_cached_tag_list
91
+ after_save :save_tags
92
+
93
+ if respond_to?(:named_scope)
94
+ named_scope :tagged_with, lambda{ |*args|
95
+ find_options_for_find_tagged_with(*args)
96
+ }
97
+ end
98
+ end
99
+
100
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
101
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
102
+ alias_method_chain :reload, :tag_list
103
+ end
104
+ end
105
+
106
+ def is_taggable?
107
+ false
108
+ end
109
+ end
110
+
111
+ module SingletonMethods
112
+ # Pass either a tag string, or an array of strings or tags
113
+ #
114
+ # Options:
115
+ # :exclude - Find models that are not tagged with the given tags
116
+ # :match_all - Find models that match all of the given tags, not just one
117
+ # :conditions - A piece of SQL conditions to add to the query
118
+ # :on - scopes the find to a context
119
+ def find_tagged_with(*args)
120
+ options = find_options_for_find_tagged_with(*args)
121
+ options.blank? ? [] : find(:all,options)
122
+ end
123
+
124
+ def caching_tag_list_on?(context)
125
+ column_names.include?("cached_#{context.to_s.singularize}_list")
126
+ end
127
+
128
+ def tag_counts_on(context, options = {})
129
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
130
+ end
131
+
132
+ def find_options_for_find_tagged_with(tags, options = {})
133
+ tags = TagList.from(tags)
134
+
135
+ return {} if tags.empty?
136
+
137
+ joins = []
138
+ conditions = []
139
+
140
+ context = options.delete(:on)
141
+
142
+
143
+ if options.delete(:exclude)
144
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
145
+ 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)})"
146
+
147
+ else
148
+ tags.each do |tag|
149
+ safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
150
+ prefix = "#{safe_tag}_#{rand(1024)}"
151
+
152
+ taggings_alias = "#{table_name}_taggings_#{prefix}"
153
+ tags_alias = "#{table_name}_tags_#{prefix}"
154
+
155
+ tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
156
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
157
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
158
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
159
+
160
+ tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
161
+ " ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
162
+ " AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
163
+
164
+ joins << tagging_join
165
+ joins << tag_join
166
+ end
167
+ end
168
+
169
+ taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
170
+
171
+ if options.delete(:match_all)
172
+ joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
173
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
174
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
175
+
176
+ group = "#{table_name}.#{primary_key} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
177
+ end
178
+
179
+ { :joins => joins.join(" "),
180
+ :group => group,
181
+ :conditions => conditions.join(" AND ") }.update(options)
182
+ end
183
+
184
+ # Calculate the tag counts for all tags.
185
+ #
186
+ # Options:
187
+ # :start_at - Restrict the tags to those created after a certain time
188
+ # :end_at - Restrict the tags to those created before a certain time
189
+ # :conditions - A piece of SQL conditions to add to the query
190
+ # :limit - The maximum number of tags to return
191
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
192
+ # :at_least - Exclude tags with a frequency less than the given value
193
+ # :at_most - Exclude tags with a frequency greater than the given value
194
+ # :on - Scope the find to only include a certain context
195
+ def find_options_for_tag_counts(options = {})
196
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
197
+
198
+ scope = scope(:find)
199
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
200
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
201
+
202
+ taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
203
+ taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
204
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
205
+
206
+ conditions = [
207
+ taggable_type,
208
+ taggable_id,
209
+ options[:conditions],
210
+ start_at,
211
+ end_at
212
+ ]
213
+
214
+ conditions = conditions.compact.join(' AND ')
215
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
216
+
217
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
218
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
219
+
220
+ joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
221
+ unless self.descends_from_active_record?
222
+ # Current model is STI descendant, so add type checking to the join condition
223
+ joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
224
+ end
225
+
226
+ joins << scope[:joins] if scope && scope[:joins]
227
+
228
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
229
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
230
+ having = [at_least, at_most].compact.join(' AND ')
231
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
232
+ group_by << " AND #{having}" unless having.blank?
233
+
234
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
235
+ :joins => joins.join(" "),
236
+ :conditions => conditions,
237
+ :group => group_by,
238
+ :limit => options[:limit],
239
+ :order => options[:order]
240
+ }
241
+ end
242
+
243
+ def is_taggable?
244
+ true
245
+ end
246
+ end
247
+
248
+ module InstanceMethods
249
+
250
+ def tag_types
251
+ self.class.tag_types
252
+ end
253
+
254
+ def custom_contexts
255
+ @custom_contexts ||= []
256
+ end
257
+
258
+ def is_taggable?
259
+ self.class.is_taggable?
260
+ end
261
+
262
+ def add_custom_context(value)
263
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
264
+ end
265
+
266
+ def tag_list_on(context, owner=nil)
267
+ var_name = context.to_s.singularize + "_list"
268
+ add_custom_context(context)
269
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
270
+
271
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
272
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
273
+ else
274
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
275
+ end
276
+ end
277
+
278
+ def tags_on(context, owner=nil)
279
+ if owner
280
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
281
+ context.to_s, owner.id, owner.class.to_s]}
282
+ else
283
+ opts = {:conditions => ["context = ?", context.to_s]}
284
+ end
285
+ base_tags.find(:all, opts)
286
+ end
287
+
288
+ def cached_tag_list_on(context)
289
+ self["cached_#{context.to_s.singularize}_list"]
290
+ end
291
+
292
+ def set_tag_list_on(context,new_list, tagger=nil)
293
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
294
+ add_custom_context(context)
295
+ end
296
+
297
+ def tag_counts_on(context, options={})
298
+ self.class.tag_counts_on(context, options.merge(:id => self.id))
299
+ end
300
+
301
+ def related_tags_for(context, klass, options = {})
302
+ search_conditions = related_search_options(context, klass, options)
303
+
304
+ klass.find(:all, search_conditions)
305
+ end
306
+
307
+ def related_search_options(context, klass, options = {})
308
+ tags_to_find = self.tags_on(context).collect { |t| t.name }
309
+
310
+ exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
311
+
312
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
313
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
314
+ :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],
315
+ :group => "#{klass.table_name}.id",
316
+ :order => "count DESC"
317
+ }.update(options)
318
+ end
319
+
320
+ def save_cached_tag_list
321
+ self.class.tag_types.map(&:to_s).each do |tag_type|
322
+ if self.class.send("caching_#{tag_type.singularize}_list?")
323
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
324
+ end
325
+ end
326
+ end
327
+
328
+ def save_tags
329
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
330
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
331
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
332
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
333
+ old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
334
+
335
+ self.class.transaction do
336
+ base_tags.delete(*old_tags) if old_tags.any?
337
+ new_tag_names.each do |new_tag_name|
338
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
339
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
340
+ :taggable => self, :tagger => owner)
341
+ end
342
+ end
343
+ end
344
+
345
+ true
346
+ end
347
+
348
+ def reload_with_tag_list(*args)
349
+ self.class.tag_types.each do |tag_type|
350
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
351
+ end
352
+
353
+ reload_without_tag_list(*args)
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end