ozataman-acts-as-taggable-on 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ == 2008-07-17
2
+
3
+ * Can now use a named_scope to find tags!
4
+
5
+ == 2008-06-23
6
+
7
+ * Can now find related objects of another class (tristanzdunn)
8
+ * Removed extraneous down migration cruft (azabaj)
9
+
10
+ == 2008-06-09
11
+
12
+ * Added support for Single Table Inheritance
13
+ * Adding gemspec and rails/init.rb for gemified plugin
14
+
15
+ == 2007-12-12
16
+
17
+ * Added ability to use dynamic tag contexts
18
+ * Fixed missing migration generator
@@ -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,29 @@
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 :tag_id, :integer
9
+ t.column :taggable_id, :integer
10
+ t.column :tagger_id, :integer
11
+ t.column :tagger_type, :string
12
+
13
+ # You should make sure that the column created is
14
+ # long enough to store the required class names.
15
+ t.column :taggable_type, :string
16
+ t.column :context, :string
17
+
18
+ t.column :created_at, :datetime
19
+ end
20
+
21
+ add_index :taggings, :tag_id
22
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
23
+ end
24
+
25
+ def self.down
26
+ drop_table :taggings
27
+ drop_table :tags
28
+ end
29
+ 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