citrusbyte-is_taggable 0.85

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/CHANGELOG ADDED
@@ -0,0 +1,30 @@
1
+ is_taggable:
2
+
3
+ == 2008-12-11
4
+
5
+ * Added support for Rails 2.2.2 (new Multibyte::Chars handling for tag normalization)
6
+
7
+ == 2008-something-something
8
+
9
+ * Forked and totally changed
10
+
11
+ acts-as-taggable-on:
12
+
13
+ == 2008-07-17
14
+
15
+ * Can now use a named_scope to find tags!
16
+
17
+ == 2008-06-23
18
+
19
+ * Can now find related objects of another class (tristanzdunn)
20
+ * Removed extraneous down migration cruft (azabaj)
21
+
22
+ == 2008-06-09
23
+
24
+ * Added support for Single Table Inheritance
25
+ * Adding gemspec and rails/init.rb for gemified plugin
26
+
27
+ == 2007-12-12
28
+
29
+ * Added ability to use dynamic tag contexts
30
+ * Fixed missing migration generator
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2008 Citrusbyte, LLC (is_taggable)
2
+ Copyright (c) 2007 Michael Bleigh and Intridea Inc.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,243 @@
1
+ is_taggable
2
+ ===============================================================================
3
+
4
+ This plugin is almost entirely based on acts-as-taggable-on by Michael Bleigh +
5
+ contributors.
6
+
7
+ is_taggable supports the awesome contextual tagging of acts-as-taggable-on, but
8
+ with a couple substantial changes to the underlying architecture.
9
+
10
+ Architecture Differences
11
+ ===============================================================================
12
+
13
+ We had a couple key issues with the underlying architecture in
14
+ acts-as-taggable-on which we felt deserved a forking into a new direction.
15
+ There are two main architectural differences between is_taggable and
16
+ acts-as-taggable-on:
17
+
18
+ 1) is_taggable does *not* use a normalized data model -- there is one table
19
+ "taggings" and not two tables "taggings" and "tags". This means that tags are
20
+ duplicated in the taggings table. The reason behind this was that we could not
21
+ find a reason justifying the join that couldn't be easily overcome with unique
22
+ indexing and grouped selects.
23
+
24
+ 2) (This is the big one) -- is_taggable skips validations and callbacks on the
25
+ Tagging model when saving. This totally breaks the normal AR behavior and the
26
+ behavior used by all other AR tagging plugins. The reason for this is that the
27
+ taggings are updated with a multi-insert -- which also means this plugin can
28
+ only be used on databases which support multi-insert (MySQL, Postgres, Oracle,
29
+ ...). The reason we use a multi-insert is because we ran into massive problems
30
+ with large numbers of writes on taggings. Take this scenario:
31
+
32
+ I upload a photo and fill in my tags when I upload it. I want people to find it
33
+ so I tag it with about 20 different keywords. In order to save the tags on my
34
+ photo the original plugin would have to (at least):
35
+
36
+ 2) do 20 SELECTs on the taggings table for validates_uniqueness_of
37
+ 4) do some number (at most 20) of INSERTs on the tags table to save the tags
38
+ 3) do 20 INSERTs on the taggings table to save the taggings
39
+
40
+ So best case 20 INSERTs, 20 SELECTs -- worst case 40 INSERTs, 20 SELECTs.
41
+
42
+ Now get a few users adding lots of tags on things concurrently and you can see
43
+ writes quickly becoming a problem, and as the number of tags I'm adding grows,
44
+ the problem gets worse. Individual INSERTs are fast, but once you have
45
+ concurrency you have lock waits and the the problem gets massively compounded.
46
+
47
+ By using a multi-insert, a non-normalized table, and a "manual" validation (do
48
+ all the validates_uniqueness_of checks at once) we can get it down to:
49
+
50
+ 2) do 1 SELECT to check for duplicated taggings
51
+ 3) do 1 multi-INSERT to INSERT all the taggings
52
+
53
+ Furthermore, no matter how many tags your inserting, it's always 1 SELECT and 1
54
+ INSERT.
55
+
56
+ Another thing to consider is the impact of updating tags. In the original
57
+ plugin this was done as a loop causing multiple DELETEs followed by multiple
58
+ INSERTs. We've taken this down to 1 DELETE followed by 1 multi-INSERT.
59
+
60
+ Compatibility
61
+ ===============================================================================
62
+
63
+ is_taggable requires that your underlying database support multi-INSERT
64
+ statements (i.e. INSERT INTO taggings (tag) VALUES ('foo', 'bar', 'baz')). Most
65
+ "major" databases do -- including MySQL, PostgreSQL and Oracle.
66
+
67
+ It has only been tested with Rails 2.1+ and makes use of named_scope (introduced
68
+ in Rails 2.1).
69
+
70
+ Installation
71
+ ===============================================================================
72
+
73
+ GemPlugin
74
+ -------------------------------------------------------------------------------
75
+
76
+ Rails 2.1+ introduces gem dependencies, to use them add this line to
77
+ environment.rb:
78
+
79
+ config.gem "citrusbyte-is_taggable", :source => "http://gems.github.com", :lib => "is_taggable"
80
+
81
+ Then run "rake gems:install" to install the gem.
82
+
83
+ Plugin
84
+ -------------------------------------------------------------------------------
85
+
86
+ script/plugin install git://github.com/citrusbyte/is_taggable.git
87
+
88
+ Gem
89
+ -------------------------------------------------------------------------------
90
+
91
+ gem install citrusbyte-is_taggable --source http://gems.github.com
92
+
93
+ Post Installation
94
+ -------------------------------------------------------------------------------
95
+ 1. script/generate is_taggable_migration
96
+ 2. rake db/migrate
97
+
98
+ Testing
99
+ ===============================================================================
100
+
101
+ is_taggable uses RSpec for its test coverage, if you're using RSpec type:
102
+
103
+ rake spec:plugins
104
+
105
+ Examples (all stolen from acts-as-taggable-on docs)
106
+ ===============================================================================
107
+
108
+ class User < ActiveRecord::Base
109
+ is_taggable :tags, :skills, :interests
110
+ end
111
+
112
+ @user = User.new(:name => "Bobby")
113
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
114
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
115
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
116
+ @user.save
117
+
118
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
119
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
120
+
121
+ # The old way
122
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
123
+ User.find_tagged_with("awesome", :on => :skills) # => []
124
+
125
+ # The better way (utilizes named_scope)
126
+ User.tagged_with("awesome", :on => :tags) # => [@user]
127
+ User.tagged_with("awesome", :on => :skills) # => []
128
+
129
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
130
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
131
+ @frankie.skill_counts
132
+
133
+ Finding Tagged Objects
134
+ ======================
135
+
136
+ is_taggable utilizes Rails 2.1's named_scope to create an association
137
+ for tags. This way you can mix and match to filter down your results, and it
138
+ also improves compatibility with the will_paginate gem:
139
+
140
+ class User < ActiveRecord::Base
141
+ is_taggable :tags
142
+ named_scope :by_join_date, :order => "created_at DESC"
143
+ end
144
+
145
+ User.tagged_with("awesome").by_date
146
+ User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
147
+
148
+ Relationships
149
+ =============
150
+
151
+ You can find objects of the same type based on similar tags on certain contexts.
152
+ Also, objects will be returned in descending order based on the total number of
153
+ matched tags.
154
+
155
+ @bobby = User.find_by_name("Bobby")
156
+ @bobby.skill_list # => ["jogging", "diving"]
157
+
158
+ @frankie = User.find_by_name("Frankie")
159
+ @frankie.skill_list # => ["hacking"]
160
+
161
+ @tom = User.find_by_name("Tom")
162
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
163
+
164
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
165
+ @bobby.find_related_skills # => [<User name="Tom">]
166
+ @frankie.find_related_skills # => [<User name="Tom">]
167
+
168
+
169
+ Dynamic Tag Contexts
170
+ ====================
171
+
172
+ In addition to the generated tag contexts in the definition, it is also possible
173
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
174
+
175
+ @user = User.new(:name => "Bobby")
176
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
177
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
178
+ @user.save
179
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
180
+ @user.tag_counts_on(:customs)
181
+ User.find_tagged_with("same", :on => :customs) # => [@user]
182
+
183
+ Tag Ownership
184
+ =============
185
+
186
+ Tags can have owners:
187
+
188
+ class User < ActiveRecord::Base
189
+ is_tagger
190
+ end
191
+
192
+ class Photo < ActiveRecord::Base
193
+ is_taggable :locations
194
+ end
195
+
196
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
197
+ @some_user.owned_taggings
198
+ @some_user.owned_tags
199
+ @some_photo.locations_from(@some_user)
200
+
201
+ Caveats
202
+ ===============================================================================
203
+
204
+ 1) Your underlying database *must* support multi-INSERT
205
+ 2) You probably need Rails 2.1+, but seriously named_scope is so cool you need
206
+ it anyways.
207
+ 3) You cannot use callbacks/validations on the Tagging model (you can still use
208
+ them like normal on your Taggables and Taggers...)
209
+
210
+ Contributors
211
+ ===============================================================================
212
+
213
+ is_taggable:
214
+ * Ben Alavi & Michel Martens - Ruthless hackers of acts-as-taggable-on
215
+
216
+ acts-as-taggable-on:
217
+ * Michael Bleigh - Original Author
218
+ * Brendan Lim - Related Objects
219
+ * Pradeep Elankumaran - Taggers
220
+ * Sinclair Bain - Patch King
221
+
222
+ Patch Contributors
223
+ -------------------------------------------------------------------------------
224
+
225
+ acts-as-taggable-on:
226
+ * tristanzdunn - Related objects of other classes
227
+ * azabaj - Fixed migrate down
228
+ * Peter Cooper - named_scope fix
229
+ * slainer68 - STI fix
230
+ * harrylove - migration instructions and fix-ups
231
+ * lawrencepit - cached tag work
232
+
233
+ Resources
234
+ ===============================================================================
235
+
236
+ * GitHub - http://github.com/citrusbyte/is_taggable
237
+ * Lighthouse - http://citrusbyte.lighthouseapp.com/projects/
238
+
239
+ is_taggable:
240
+ Copyright (c) 2008 Citrusbyte, LLC, released under the MIT license
241
+
242
+ acts-as-taggable-on:
243
+ Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
@@ -0,0 +1,7 @@
1
+ class IsTaggableMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "is_taggable_migration"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ class IsTaggableMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :taggings do |t|
4
+ t.column :tagger_type, :string
5
+ t.column :tagger_id, :integer
6
+ t.column :taggable_type, :string
7
+ t.column :taggable_id, :integer
8
+
9
+ t.column :tag, :string
10
+ t.column :normalized, :string
11
+ t.column :context, :string
12
+
13
+ t.column :created_at, :datetime
14
+ end
15
+
16
+ add_index :taggings, [:taggable_id, :taggable_type]
17
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
18
+ add_index :taggings, [:taggable_id, :taggable_type, :context, :normalized], :uniq => true, :name => 'taggable_and_context_and_normalized'
19
+ end
20
+
21
+ def self.down
22
+ drop_table :taggings
23
+ end
24
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,354 @@
1
+ module ActiveRecord
2
+ module Is
3
+ module Taggable
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 is_taggable
14
+ is_taggable :tags
15
+ end
16
+
17
+ def is_taggable(*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
+ self.class_eval do
23
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy, :conditions => ["context = ?",tag_type], :class_name => "Tagging"
24
+ end
25
+
26
+ self.class_eval <<-RUBY
27
+ def self.taggable?
28
+ true
29
+ end
30
+
31
+ def self.caching_#{tag_type.singularize}_list?
32
+ caching_tag_list_on?("#{tag_type}")
33
+ end
34
+
35
+ def self.#{tag_type.singularize}_counts(options={})
36
+ tag_counts_on('#{tag_type}',options)
37
+ end
38
+
39
+ def #{tag_type.singularize}_list
40
+ tag_list_on('#{tag_type}').to_s
41
+ end
42
+
43
+ def #{tag_type}
44
+ tag_list_on('#{tag_type}')
45
+ end
46
+
47
+ def #{tag_type}=(tags)
48
+ set_tags_on('#{tag_type}', tags)
49
+ end
50
+
51
+ def #{tag_type.singularize}_list=(new_tags)
52
+ set_tag_list_on('#{tag_type}', new_tags)
53
+ end
54
+
55
+ def #{tag_type.singularize}_counts(options = {})
56
+ tag_counts_on('#{tag_type}',options)
57
+ end
58
+
59
+ def #{tag_type}_from(owner)
60
+ tag_list_on('#{tag_type}', owner)
61
+ end
62
+
63
+ def find_related_#{tag_type}(options = {})
64
+ related_tags_for('#{tag_type}', self.class, options)
65
+ end
66
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
67
+
68
+ def find_related_#{tag_type}_for(klass, options = {})
69
+ related_tags_for('#{tag_type}', klass, options)
70
+ end
71
+ 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
82
+
83
+ attr_writer :custom_contexts
84
+
85
+ before_save :save_cached_tag_list
86
+ after_save :save_tags
87
+
88
+ if respond_to?(:named_scope)
89
+ named_scope :tagged_with, lambda{ |tags, options|
90
+ find_options_for_find_tagged_with(tags, options)
91
+ }
92
+ end
93
+ end
94
+
95
+ include ActiveRecord::Is::Taggable::InstanceMethods
96
+ extend ActiveRecord::Is::Taggable::SingletonMethods
97
+ alias_method_chain :reload, :tag_list
98
+ end
99
+ end
100
+
101
+ def is_taggable?
102
+ false
103
+ end
104
+ end
105
+
106
+ module SingletonMethods
107
+ # Pass either a tag string, or an array of strings or tags
108
+ #
109
+ # Options:
110
+ # :exclude - Find models that are not tagged with the given tags
111
+ # :match_all - Find models that match all of the given tags, not just one
112
+ # :conditions - A piece of SQL conditions to add to the query
113
+ # :on - scopes the find to a context
114
+ def find_tagged_with(*args)
115
+ options = find_options_for_find_tagged_with(*args)
116
+ options.blank? ? [] : find(:all,options)
117
+ end
118
+
119
+ def caching_tag_list_on?(context)
120
+ column_names.include?("cached_#{context.to_s.singularize}_list")
121
+ end
122
+
123
+ def tag_counts_on(context, options = {})
124
+ Tagging.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
125
+ end
126
+
127
+ def find_options_for_find_tagged_with(tags, options = {})
128
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
129
+
130
+ return {} if tags.empty?
131
+
132
+ conditions = []
133
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
134
+ unless (on = options.delete(:on)).nil?
135
+ conditions << sanitize_sql(["context = ?",on.to_s])
136
+ end
137
+
138
+ taggings_alias = "#{table_name}_taggings"
139
+
140
+ if options.delete(:exclude)
141
+ conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} WHERE (#{Tagging.table_name}.normalized IN(?)) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags.normalized])
142
+ else
143
+ conditions << sanitize_sql(["#{taggings_alias}.normalized IN(?)", tags.normalized])
144
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{taggings.size}" if options.delete(:match_all)
145
+ end
146
+
147
+ { :select => "DISTINCT #{table_name}.*",
148
+ :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)}",
149
+ :conditions => conditions.join(" AND "),
150
+ :group => group
151
+ }.update(options)
152
+ end
153
+
154
+ # Calculate the tag counts for all tags.
155
+ #
156
+ # Options:
157
+ # :start_at - Restrict the tags to those created after a certain time
158
+ # :end_at - Restrict the tags to those created before a certain time
159
+ # :conditions - A piece of SQL conditions to add to the query
160
+ # :limit - The maximum number of tags to return
161
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
162
+ # :at_least - Exclude tags with a frequency less than the given value
163
+ # :at_most - Exclude tags with a frequency greater than the given value
164
+ # :on - Scope the find to only include a certain context
165
+ def find_options_for_tag_counts(options = {})
166
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on
167
+
168
+ scope = scope(:find)
169
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
170
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
171
+
172
+ type_and_context = "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}"
173
+ type_and_context << sanitize_sql(["AND #{Tagging.table_name}.context = ?", options.delete(:on).to_s]) unless options[:on].nil?
174
+
175
+ conditions = [
176
+ type_and_context,
177
+ start_at,
178
+ end_at
179
+ ]
180
+
181
+ conditions = conditions.compact.join(' AND ')
182
+ conditions = merge_conditions(conditions, options.delete(:conditions)) if options[:conditions]
183
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
184
+
185
+ joins = ["LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"]
186
+ joins << scope[:joins] if scope && scope[:joins]
187
+
188
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
189
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
190
+ having = [at_least, at_most].compact.join(' AND ')
191
+
192
+ # note that it makes sense here to group by both (and allow both to
193
+ # be selected) since we're enforcing that tags that normalize to the
194
+ # same thing can't exist. this means there will never be a case when
195
+ # one normalized tag has multiple non-normalized representations,
196
+ # meaning we still have a proper set when grouping by either column
197
+ group_by = "#{Tagging.table_name}.normalized, #{Tagging.table_name}.tag HAVING COUNT(*) > 0"
198
+ group_by << " AND #{having}" unless having.blank?
199
+
200
+ { :select => "#{Tagging.table_name}.tag, COUNT(*) AS count",
201
+ :joins => joins.join(" "),
202
+ :conditions => conditions,
203
+ :group => group_by
204
+ }.update(options)
205
+ end
206
+
207
+ def is_taggable?
208
+ true
209
+ end
210
+ end
211
+
212
+ module InstanceMethods
213
+
214
+ def tag_types
215
+ self.class.tag_types
216
+ end
217
+
218
+ def custom_contexts
219
+ @custom_contexts ||= []
220
+ end
221
+
222
+ def is_taggable?
223
+ self.class.is_taggable?
224
+ end
225
+
226
+ def add_custom_context(value)
227
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
228
+ end
229
+
230
+ def tag_list_on(context, owner=nil)
231
+ var_name = context.to_s.singularize + "_list"
232
+ add_custom_context(context)
233
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
234
+
235
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
236
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
237
+ else
238
+ instance_variable_set("@#{var_name}", TagList.new(*taggings_on(context, owner).map(&:tag)))
239
+ end
240
+ end
241
+
242
+ def taggings_on(context, owner=nil)
243
+ if owner
244
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?", context.to_s, owner.id, owner.class.to_s]}
245
+ else
246
+ opts = {:conditions => ["context = ?", context.to_s]}
247
+ end
248
+ taggings.find(:all, opts)
249
+ end
250
+
251
+ def cached_tag_list_on(context)
252
+ self["cached_#{context.to_s.singularize}_list"]
253
+ end
254
+
255
+ def set_tag_list_on(context,new_list, tagger=nil)
256
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
257
+ add_custom_context(context)
258
+ end
259
+
260
+ def set_tags_on(context, new_tags, tagger=nil)
261
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.new_from_owner(tagger, *new_tags))
262
+ add_custom_context(context)
263
+ end
264
+
265
+ def tag_counts_on(context,options={})
266
+ self.class.tag_counts_on(context,{:conditions => ["#{Tagging.table_name}.normalized IN (?)", tag_list_on(context).normalized]}.reverse_merge!(options))
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.taggings_on(context).collect(&:normalized)
277
+
278
+ { :select => "#{klass.table_name}.*, related_ids.count AS count",
279
+ :from => "#{klass.table_name}",
280
+ :joins => sanitize_sql(["INNER JOIN(
281
+ SELECT #{klass.table_name}.id, COUNT(#{Tagging.table_name}.id) AS count
282
+ FROM #{klass.table_name}, #{Tagging.table_name}
283
+ WHERE #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}'
284
+ AND #{Tagging.table_name}.context = '#{context}' AND #{Tagging.table_name}.normalized IN (?)
285
+ GROUP BY #{klass.table_name}.id
286
+ ) AS related_ids ON(#{klass.table_name}.id = related_ids.id)", tags_to_find]),
287
+ :order => "count DESC"
288
+ }.update(options)
289
+ end
290
+
291
+ def save_cached_tag_list
292
+ self.class.tag_types.map(&:to_s).each do |tag_type|
293
+ if self.class.send("caching_#{tag_type.singularize}_list?")
294
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
295
+ end
296
+ end
297
+ end
298
+
299
+ def save_tags
300
+ all_taggings = {}
301
+ self.taggings.find(:all, :order => 'context ASC').each do |tagging|
302
+ all_taggings[tagging.context] ||= []
303
+ all_taggings[tagging.context] << tagging
304
+ end
305
+
306
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
307
+ next unless contextual_tag_list = instance_variable_get("@#{tag_type.singularize}_list")
308
+ normalized_tag_list = contextual_tag_list.normalized
309
+ owner = contextual_tag_list.owner
310
+ existing_taggings = all_taggings[tag_type.to_sym] || []
311
+ new_tag_names = normalized_tag_list - existing_taggings.map(&:normalized)
312
+ old_tags = existing_taggings.reject { |tagging| normalized_tag_list.include?(tagging.normalized) }
313
+
314
+ self.class.transaction do
315
+ self.taggings.delete(*old_tags) if old_tags.any?
316
+ if new_tag_names.any? # it's possible we're just removing existing tags
317
+ sql = "INSERT INTO taggings (tag, normalized, context, taggable_id, taggable_type, tagger_id, tagger_type, created_at) VALUES "
318
+ sql += new_tag_names.collect { |tag| tag_insert_value(tag, tag_type, self, owner) }.join(", ")
319
+ ActiveRecord::Base.connection.execute(sql)
320
+ end
321
+ end
322
+ end
323
+
324
+ true
325
+ end
326
+
327
+ def sanitize_sql(attrs)
328
+ ActiveRecord::Base.send(:sanitize_sql, attrs)
329
+ end
330
+
331
+ def tag_insert_value(tag, type, taggable, owner=nil)
332
+ sanitize_sql(["(?, ?, ?, ?, ?, ?, ?, ?)",
333
+ tag,
334
+ TagList.normalize(tag),
335
+ type,
336
+ taggable.id,
337
+ taggable.class.base_class.to_s, # base_class to support STI properly
338
+ owner ? owner.id : nil,
339
+ owner ? owner.class.to_s : nil,
340
+ Time.now.utc.to_s(:db)
341
+ ])
342
+ end
343
+
344
+ def reload_with_tag_list(*args)
345
+ self.class.tag_types.each do |tag_type|
346
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
347
+ end
348
+
349
+ reload_without_tag_list(*args)
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end