wideopenspaces-acts-as-taggable-on 1.0.3

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,22 @@
1
+ == 2009-01-21
2
+
3
+ * Added counter cache for taggings on tag (Tag#tag_count) (wideopenspaces)
4
+
5
+ == 2008-07-17
6
+
7
+ * Can now use a named_scope to find tags!
8
+
9
+ == 2008-06-23
10
+
11
+ * Can now find related objects of another class (tristanzdunn)
12
+ * Removed extraneous down migration cruft (azabaj)
13
+
14
+ == 2008-06-09
15
+
16
+ * Added support for Single Table Inheritance
17
+ * Adding gemspec and rails/init.rb for gemified plugin
18
+
19
+ == 2007-12-12
20
+
21
+ * Added ability to use dynamic tag contexts
22
+ * Fixed missing migration generator
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Michael Bleigh and Intridea Inc.
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,190 @@
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. If you already have RSpec on your
59
+ application, the specs will run while using:
60
+
61
+ rake spec:plugins
62
+
63
+ Example
64
+ =======
65
+
66
+ class User < ActiveRecord::Base
67
+ acts_as_taggable_on :tags, :skills, :interests
68
+ end
69
+
70
+ @user = User.new(:name => "Bobby")
71
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
72
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
73
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
74
+ @user.save
75
+
76
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
77
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
78
+
79
+ # The old way
80
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
81
+ User.find_tagged_with("awesome", :on => :skills) # => []
82
+
83
+ # The better way (utilizes named_scope)
84
+ User.tagged_with("awesome", :on => :tags) # => [@user]
85
+ User.tagged_with("awesome", :on => :skills) # => []
86
+
87
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
88
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
89
+ @frankie.skill_counts
90
+
91
+ Finding Tagged Objects
92
+ ======================
93
+
94
+ Acts As Taggable On utilizes Rails 2.1's named_scope to create an association
95
+ for tags. This way you can mix and match to filter down your results, and it
96
+ also improves compatibility with the will_paginate gem:
97
+
98
+ class User < ActiveRecord::Base
99
+ acts_as_taggable_on :tags
100
+ named_scope :by_join_date, :order => "created_at DESC"
101
+ end
102
+
103
+ User.tagged_with("awesome").by_date
104
+ User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
105
+
106
+ Relationships
107
+ =============
108
+
109
+ You can find objects of the same type based on similar tags on certain contexts.
110
+ Also, objects will be returned in descending order based on the total number of
111
+ matched tags.
112
+
113
+ @bobby = User.find_by_name("Bobby")
114
+ @bobby.skill_list # => ["jogging", "diving"]
115
+
116
+ @frankie = User.find_by_name("Frankie")
117
+ @frankie.skill_list # => ["hacking"]
118
+
119
+ @tom = User.find_by_name("Tom")
120
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
121
+
122
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
123
+ @bobby.find_related_skills # => [<User name="Tom">]
124
+ @frankie.find_related_skills # => [<User name="Tom">]
125
+
126
+
127
+ Dynamic Tag Contexts
128
+ ====================
129
+
130
+ In addition to the generated tag contexts in the definition, it is also possible
131
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
132
+
133
+ @user = User.new(:name => "Bobby")
134
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
135
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
136
+ @user.save
137
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
138
+ @user.tag_counts_on(:customs)
139
+ User.find_tagged_with("same", :on => :customs) # => [@user]
140
+
141
+ Tag Ownership
142
+ =============
143
+
144
+ Tags can have owners:
145
+
146
+ class User < ActiveRecord::Base
147
+ acts_as_tagger
148
+ end
149
+
150
+ class Photo < ActiveRecord::Base
151
+ acts_as_taggable_on :locations
152
+ end
153
+
154
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
155
+ @some_user.owned_taggings
156
+ @some_user.owned_tags
157
+ @some_photo.locations_from(@some_user)
158
+
159
+ Caveats, Uncharted Waters
160
+ =========================
161
+
162
+ This plugin is still under active development. Tag caching has not
163
+ been thoroughly (or even casually) tested and may not work as expected.
164
+
165
+ Contributors
166
+ ============
167
+
168
+ * Michael Bleigh - Original Author
169
+ * Brendan Lim - Related Objects
170
+ * Pradeep Elankumaran - Taggers
171
+ * Sinclair Bain - Patch King
172
+
173
+ Patch Contributors
174
+ ------------------
175
+
176
+ * tristanzdunn - Related objects of other classes
177
+ * azabaj - Fixed migrate down
178
+ * Peter Cooper - named_scope fix
179
+ * slainer68 - STI fix
180
+ * harrylove - migration instructions and fix-ups
181
+ * lawrencepit - cached tag work
182
+
183
+ Resources
184
+ =========
185
+
186
+ * Acts As Community - http://www.actsascommunity.com/projects/acts-as-taggable-on
187
+ * GitHub - http://github.com/mbleigh/acts-as-taggable-on
188
+ * Lighthouse - http://mbleigh.lighthouseapp.com/projects/10116-acts-as-taggable-on
189
+
190
+ 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 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
+ t.column :tag_count, :integer, :default => 0
6
+ end
7
+
8
+ create_table :taggings do |t|
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, [: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,6 @@
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'
@@ -0,0 +1,316 @@
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
+ self.class_eval do
23
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
24
+ :include => :tag, :conditions => ["context = ?",tag_type], :class_name => "Tagging"
25
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
26
+ end
27
+
28
+ self.class_eval <<-RUBY
29
+ def self.taggable?
30
+ true
31
+ end
32
+
33
+ def self.caching_#{tag_type.singularize}_list?
34
+ caching_tag_list_on?("#{tag_type}")
35
+ end
36
+
37
+ def self.#{tag_type.singularize}_counts(options={})
38
+ tag_counts_on('#{tag_type}',options)
39
+ end
40
+
41
+ def #{tag_type.singularize}_list
42
+ tag_list_on('#{tag_type}')
43
+ end
44
+
45
+ def #{tag_type.singularize}_list=(new_tags)
46
+ set_tag_list_on('#{tag_type}',new_tags)
47
+ end
48
+
49
+ def #{tag_type.singularize}_counts(options = {})
50
+ tag_counts_on('#{tag_type}',options)
51
+ end
52
+
53
+ def #{tag_type}_from(owner)
54
+ tag_list_on('#{tag_type}', owner)
55
+ end
56
+
57
+ def find_related_#{tag_type}(options = {})
58
+ related_tags_for('#{tag_type}', self.class, options)
59
+ end
60
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
61
+
62
+ def find_related_#{tag_type}_for(klass, options = {})
63
+ related_tags_for('#{tag_type}', klass, options)
64
+ end
65
+ RUBY
66
+ end
67
+
68
+ if respond_to?(:tag_types)
69
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
70
+ else
71
+ self.class_eval do
72
+ write_inheritable_attribute(:tag_types, args.uniq)
73
+ class_inheritable_reader :tag_types
74
+
75
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
76
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
77
+
78
+ attr_writer :custom_contexts
79
+
80
+ before_save :save_cached_tag_list
81
+ after_save :save_tags
82
+
83
+ if respond_to?(:named_scope)
84
+ named_scope :tagged_with, lambda{ |tags, options|
85
+ find_options_for_find_tagged_with(tags, options)
86
+ }
87
+ end
88
+ end
89
+
90
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
91
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
92
+ alias_method_chain :reload, :tag_list
93
+ end
94
+ end
95
+
96
+ def is_taggable?
97
+ false
98
+ end
99
+ end
100
+
101
+ module SingletonMethods
102
+ # Pass either a tag string, or an array of strings or tags
103
+ #
104
+ # Options:
105
+ # :exclude - Find models that are not tagged with the given tags
106
+ # :match_all - Find models that match all of the given tags, not just one
107
+ # :conditions - A piece of SQL conditions to add to the query
108
+ # :on - scopes the find to a context
109
+ def find_tagged_with(*args)
110
+ options = find_options_for_find_tagged_with(*args)
111
+ options.blank? ? [] : find(:all,options)
112
+ end
113
+
114
+ def caching_tag_list_on?(context)
115
+ column_names.include?("cached_#{context.to_s.singularize}_list")
116
+ end
117
+
118
+ def tag_counts_on(context, options = {})
119
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
120
+ end
121
+
122
+ def find_options_for_find_tagged_with(tags, options = {})
123
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
124
+
125
+ return {} if tags.empty?
126
+
127
+ conditions = []
128
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
129
+
130
+ unless (on = options.delete(:on)).nil?
131
+ conditions << sanitize_sql(["context = ?",on.to_s])
132
+ end
133
+
134
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
135
+
136
+ if options.delete(:exclude)
137
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
138
+ 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])
139
+ else
140
+ conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")
141
+
142
+ if options.delete(:match_all)
143
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
144
+ end
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
+ "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
150
+ :conditions => conditions.join(" AND "),
151
+ :group => group
152
+ }.update(options)
153
+ end
154
+
155
+ # Calculate the tag counts for all tags.
156
+ #
157
+ # Options:
158
+ # :start_at - Restrict the tags to those created after a certain time
159
+ # :end_at - Restrict the tags to those created before a certain time
160
+ # :conditions - A piece of SQL conditions to add to the query
161
+ # :limit - The maximum number of tags to return
162
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
163
+ # :at_least - Exclude tags with a frequency less than the given value
164
+ # :at_most - Exclude tags with a frequency greater than the given value
165
+ # :on - Scope the find to only include a certain context
166
+ def find_options_for_tag_counts(options = {})
167
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on
168
+
169
+ scope = scope(:find)
170
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
171
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
172
+
173
+ type_and_context = "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}"
174
+
175
+ conditions = [
176
+ type_and_context,
177
+ options[:conditions],
178
+ start_at,
179
+ end_at
180
+ ]
181
+
182
+ conditions = conditions.compact.join(' AND ')
183
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
184
+
185
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
186
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
187
+ joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
188
+ joins << scope[:joins] if scope && scope[:joins]
189
+
190
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
191
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
192
+ having = [at_least, at_most].compact.join(' AND ')
193
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
194
+ group_by << " AND #{having}" unless having.blank?
195
+
196
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
197
+ :joins => joins.join(" "),
198
+ :conditions => conditions,
199
+ :group => group_by
200
+ }.update(options)
201
+ end
202
+
203
+ def is_taggable?
204
+ true
205
+ end
206
+ end
207
+
208
+ module InstanceMethods
209
+
210
+ def tag_types
211
+ self.class.tag_types
212
+ end
213
+
214
+ def custom_contexts
215
+ @custom_contexts ||= []
216
+ end
217
+
218
+ def is_taggable?
219
+ self.class.is_taggable?
220
+ end
221
+
222
+ def add_custom_context(value)
223
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
224
+ end
225
+
226
+ def tag_list_on(context, owner=nil)
227
+ var_name = context.to_s.singularize + "_list"
228
+ add_custom_context(context)
229
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
230
+
231
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
232
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
233
+ else
234
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
235
+ end
236
+ end
237
+
238
+ def tags_on(context, owner=nil)
239
+ if owner
240
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
241
+ context.to_s, owner.id, owner.class.to_s]}
242
+ else
243
+ opts = {:conditions => ["context = ?", context.to_s]}
244
+ end
245
+ base_tags.find(:all, opts)
246
+ end
247
+
248
+ def cached_tag_list_on(context)
249
+ self["cached_#{context.to_s.singularize}_list"]
250
+ end
251
+
252
+ def set_tag_list_on(context,new_list, tagger=nil)
253
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
254
+ add_custom_context(context)
255
+ end
256
+
257
+ def tag_counts_on(context,options={})
258
+ self.class.tag_counts_on(context,{:conditions => ["#{Tag.table_name}.name IN (?)", tag_list_on(context)]}.reverse_merge!(options))
259
+ end
260
+
261
+ def related_tags_for(context, klass, options = {})
262
+ search_conditions = related_search_options(context, klass, options)
263
+
264
+ klass.find(:all, search_conditions)
265
+ end
266
+
267
+ def related_search_options(context, klass, options = {})
268
+ tags_to_find = self.tags_on(context).collect { |t| t.name }
269
+
270
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
271
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
272
+ :conditions => ["#{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],
273
+ :group => "#{klass.table_name}.id",
274
+ :order => "count DESC"
275
+ }.update(options)
276
+ end
277
+
278
+ def save_cached_tag_list
279
+ self.class.tag_types.map(&:to_s).each do |tag_type|
280
+ if self.class.send("caching_#{tag_type.singularize}_list?")
281
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
282
+ end
283
+ end
284
+ end
285
+
286
+ def save_tags
287
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
288
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
289
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
290
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
291
+ old_tags = tags_on(tag_type).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
292
+
293
+ self.class.transaction do
294
+ base_tags.delete(*old_tags) if old_tags.any?
295
+ new_tag_names.each do |new_tag_name|
296
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
297
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
298
+ :taggable => self, :tagger => owner)
299
+ end
300
+ end
301
+ end
302
+
303
+ true
304
+ end
305
+
306
+ def reload_with_tag_list(*args)
307
+ self.class.tag_types.each do |tag_type|
308
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
309
+ end
310
+
311
+ reload_without_tag_list(*args)
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end