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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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