crowdint_acts-as-taggable-on 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +35 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +5 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.rdoc +250 -0
  9. data/Rakefile +13 -0
  10. data/acts-as-taggable-on.gemspec +28 -0
  11. data/lib/acts-as-taggable-on.rb +59 -0
  12. data/lib/acts-as-taggable-on/version.rb +4 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +127 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +349 -0
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  17. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +99 -0
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +73 -0
  19. data/lib/acts_as_taggable_on/tag.rb +77 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +97 -0
  21. data/lib/acts_as_taggable_on/taggable.rb +102 -0
  22. data/lib/acts_as_taggable_on/tagger.rb +67 -0
  23. data/lib/acts_as_taggable_on/tagging.rb +34 -0
  24. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  25. data/lib/acts_as_taggable_on/utils.rb +34 -0
  26. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +39 -0
  27. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +30 -0
  28. data/rails/init.rb +1 -0
  29. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +514 -0
  30. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  31. data/spec/acts_as_taggable_on/tag_list_spec.rb +93 -0
  32. data/spec/acts_as_taggable_on/tag_spec.rb +153 -0
  33. data/spec/acts_as_taggable_on/taggable_spec.rb +543 -0
  34. data/spec/acts_as_taggable_on/tagger_spec.rb +112 -0
  35. data/spec/acts_as_taggable_on/tagging_spec.rb +28 -0
  36. data/spec/acts_as_taggable_on/tags_helper_spec.rb +44 -0
  37. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  38. data/spec/bm.rb +52 -0
  39. data/spec/database.yml.sample +19 -0
  40. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  41. data/spec/models.rb +49 -0
  42. data/spec/schema.rb +61 -0
  43. data/spec/spec_helper.rb +83 -0
  44. data/uninstall.rb +1 -0
  45. metadata +240 -0
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn
2
+ VERSION = '2.3.2'
3
+ end
4
+
@@ -0,0 +1,53 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Cache
3
+ def self.included(base)
4
+ # Skip adding caching capabilities if table not exists or no cache columns exist
5
+ return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6
+
7
+ base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
+ base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9
+
10
+ base.class_eval do
11
+ before_save :save_cached_tag_list
12
+ end
13
+
14
+ base.initialize_acts_as_taggable_on_cache
15
+ end
16
+
17
+ module ClassMethods
18
+ def initialize_acts_as_taggable_on_cache
19
+ tag_types.map(&:to_s).each do |tag_type|
20
+ class_eval %(
21
+ def self.caching_#{tag_type.singularize}_list?
22
+ caching_tag_list_on?("#{tag_type}")
23
+ end
24
+ )
25
+ end
26
+ end
27
+
28
+ def acts_as_taggable_on(*args)
29
+ super(*args)
30
+ initialize_acts_as_taggable_on_cache
31
+ end
32
+
33
+ def caching_tag_list_on?(context)
34
+ column_names.include?("cached_#{context.to_s.singularize}_list")
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ def save_cached_tag_list
40
+ tag_types.map(&:to_s).each do |tag_type|
41
+ if self.class.send("caching_#{tag_type.singularize}_list?")
42
+ if tag_list_cache_set_on(tag_type)
43
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
44
+ self["cached_#{tag_type.singularize}_list"] = list
45
+ end
46
+ end
47
+ end
48
+
49
+ true
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,127 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Collection
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
6
+ base.initialize_acts_as_taggable_on_collection
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_collection
11
+ tag_types.map(&:to_s).each do |tag_type|
12
+ class_eval %(
13
+ def self.#{tag_type.singularize}_counts(options={})
14
+ tag_counts_on('#{tag_type}', options)
15
+ end
16
+
17
+ def #{tag_type.singularize}_counts(options = {})
18
+ tag_counts_on('#{tag_type}', options)
19
+ end
20
+
21
+ def top_#{tag_type}(limit = 10)
22
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
23
+ end
24
+
25
+ def self.top_#{tag_type}(limit = 10)
26
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
27
+ end
28
+ )
29
+ end
30
+ end
31
+
32
+ def acts_as_taggable_on(*args)
33
+ super(*args)
34
+ initialize_acts_as_taggable_on_collection
35
+ end
36
+
37
+ def tag_counts_on(context, options = {})
38
+ all_tag_counts(options.merge({:on => context.to_s}))
39
+ end
40
+
41
+ ##
42
+ # Calculate the tag counts for all tags.
43
+ #
44
+ # @param [Hash] options Options:
45
+ # * :start_at - Restrict the tags to those created after a certain time
46
+ # * :end_at - Restrict the tags to those created before a certain time
47
+ # * :conditions - A piece of SQL conditions to add to the query
48
+ # * :limit - The maximum number of tags to return
49
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
50
+ # * :at_least - Exclude tags with a frequency less than the given value
51
+ # * :at_most - Exclude tags with a frequency greater than the given value
52
+ # * :on - Scope the find to only include a certain context
53
+ def all_tag_counts(options = {})
54
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
55
+
56
+ scope = {}
57
+
58
+ ## Generate conditions:
59
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
60
+
61
+ start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
62
+ end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
63
+
64
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
65
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
66
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
67
+
68
+ tagging_conditions = [
69
+ taggable_conditions,
70
+ scope[:conditions],
71
+ start_at_conditions,
72
+ end_at_conditions
73
+ ].compact.reverse
74
+
75
+ tag_conditions = [
76
+ options[:conditions]
77
+ ].compact.reverse
78
+
79
+ ## Generate joins:
80
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
81
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
82
+
83
+ tagging_joins = [
84
+ taggable_join,
85
+ scope[:joins]
86
+ ].compact
87
+
88
+ tag_joins = [
89
+ ].compact
90
+
91
+ ## Generate scope:
92
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
93
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
94
+
95
+ # Joins and conditions
96
+ tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
97
+ tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
98
+
99
+ tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
100
+ tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
101
+
102
+ # GROUP BY and HAVING clauses:
103
+ at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
104
+ at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
105
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
106
+
107
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
108
+
109
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
110
+ scoped_select = "#{table_name}.#{primary_key}"
111
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
112
+ group(group_columns).
113
+ having(having)
114
+
115
+
116
+ tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
117
+ tag_scope
118
+ end
119
+ end
120
+
121
+ module InstanceMethods
122
+ def tag_counts_on(context, options={})
123
+ self.class.tag_counts_on(context, options.merge(:id => id))
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,349 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Core
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
6
+
7
+ base.class_eval do
8
+ attr_writer :custom_contexts
9
+ after_save :save_tags
10
+ end
11
+
12
+ base.initialize_acts_as_taggable_on_core
13
+ end
14
+
15
+ module ClassMethods
16
+ def initialize_acts_as_taggable_on_core
17
+ tag_types.map(&:to_s).each do |tags_type|
18
+ tag_type = tags_type.to_s.singularize
19
+ context_taggings = "#{tag_type}_taggings".to_sym
20
+ context_tags = tags_type.to_sym
21
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
22
+
23
+ class_eval do
24
+ # when preserving tag order, include order option so that for a 'tags' context
25
+ # the associations tag_taggings & tags are always returned in created order
26
+ has_many context_taggings, :as => :taggable,
27
+ :dependent => :destroy,
28
+ :include => :tag,
29
+ :class_name => "ActsAsTaggableOn::Tagging",
30
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
31
+ :order => taggings_order
32
+
33
+ has_many context_tags, :through => context_taggings,
34
+ :source => :tag,
35
+ :class_name => "ActsAsTaggableOn::Tag",
36
+ :order => taggings_order
37
+ end
38
+
39
+ class_eval %(
40
+ def #{tag_type}_list
41
+ tag_list_on('#{tags_type}')
42
+ end
43
+
44
+ def #{tag_type}_list=(new_tags)
45
+ set_tag_list_on('#{tags_type}', new_tags)
46
+ end
47
+
48
+ def all_#{tags_type}_list
49
+ all_tags_list_on('#{tags_type}')
50
+ end
51
+ )
52
+ end
53
+ end
54
+
55
+ def taggable_on(preserve_tag_order, *tag_types)
56
+ super(preserve_tag_order, *tag_types)
57
+ initialize_acts_as_taggable_on_core
58
+ end
59
+
60
+ # all column names are necessary for PostgreSQL group clause
61
+ def grouped_column_names_for(object)
62
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
63
+ end
64
+
65
+ ##
66
+ # Return a scope of objects that are tagged with the specified tags.
67
+ #
68
+ # @param tags The tags that we want to query for
69
+ # @param [Hash] options A hash of options to alter you query:
70
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
71
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
72
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
73
+ # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
74
+ #
75
+ # Example:
76
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
77
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
78
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
79
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
80
+ # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
81
+ def tagged_with(tags, options = {})
82
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
83
+ empty_result = scoped(:conditions => "1 = 0")
84
+
85
+ return empty_result if tag_list.empty?
86
+
87
+ joins = []
88
+ conditions = []
89
+
90
+ context = options.delete(:on)
91
+ owned_by = options.delete(:owned_by)
92
+ alias_base_name = undecorated_table_name.gsub('.','_')
93
+
94
+ if options.delete(:exclude)
95
+ if options.delete(:wild)
96
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
97
+ else
98
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
99
+ end
100
+
101
+ conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
102
+
103
+ elsif options.delete(:any)
104
+ # get tags, drop out if nothing returned (we need at least one)
105
+ if options.delete(:wild)
106
+ tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
107
+ else
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
109
+ end
110
+
111
+ return scoped(:conditions => "1 = 0") unless tags.length > 0
112
+
113
+ # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
114
+ # avoid ambiguous column name
115
+ taggings_context = context ? "_#{context}" : ''
116
+
117
+ taggings_alias = adjust_taggings_alias(
118
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
119
+ )
120
+
121
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
122
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
123
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
124
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
125
+
126
+ # don't need to sanitize sql, map all ids and join with OR logic
127
+ conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
128
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
129
+
130
+ joins << tagging_join
131
+
132
+ else
133
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
134
+ return empty_result unless tags.length == tag_list.length
135
+
136
+ tags.each do |tag|
137
+
138
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
139
+
140
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
141
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
142
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
143
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
144
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
145
+
146
+ if owned_by
147
+ tagging_join << " AND " +
148
+ sanitize_sql([
149
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
150
+ owned_by.id,
151
+ owned_by.class.to_s
152
+ ])
153
+ end
154
+
155
+ joins << tagging_join
156
+ end
157
+ end
158
+
159
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
160
+
161
+ if options.delete(:match_all)
162
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
163
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
164
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
165
+
166
+
167
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
168
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
169
+ end
170
+
171
+ scoped(:select => select_clause,
172
+ :joins => joins.join(" "),
173
+ :group => group,
174
+ :conditions => conditions.join(" AND "),
175
+ :order => options[:order],
176
+ :readonly => false)
177
+ end
178
+
179
+ def is_taggable?
180
+ true
181
+ end
182
+
183
+ def adjust_taggings_alias(taggings_alias)
184
+ if taggings_alias.size > 75
185
+ taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
186
+ end
187
+ taggings_alias
188
+ end
189
+ end
190
+
191
+ module InstanceMethods
192
+ # all column names are necessary for PostgreSQL group clause
193
+ def grouped_column_names_for(object)
194
+ self.class.grouped_column_names_for(object)
195
+ end
196
+
197
+ def custom_contexts
198
+ @custom_contexts ||= []
199
+ end
200
+
201
+ def is_taggable?
202
+ self.class.is_taggable?
203
+ end
204
+
205
+ def add_custom_context(value)
206
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
207
+ end
208
+
209
+ def cached_tag_list_on(context)
210
+ self["cached_#{context.to_s.singularize}_list"]
211
+ end
212
+
213
+ def tag_list_cache_set_on(context)
214
+ variable_name = "@#{context.to_s.singularize}_list"
215
+ !instance_variable_get(variable_name).nil?
216
+ end
217
+
218
+ def tag_list_cache_on(context)
219
+ variable_name = "@#{context.to_s.singularize}_list"
220
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
221
+ end
222
+
223
+ def tag_list_on(context)
224
+ add_custom_context(context)
225
+ tag_list_cache_on(context)
226
+ end
227
+
228
+ def all_tags_list_on(context)
229
+ variable_name = "@all_#{context.to_s.singularize}_list"
230
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
231
+
232
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
233
+ end
234
+
235
+ ##
236
+ # Returns all tags of a given context
237
+ def all_tags_on(context)
238
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
239
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
240
+
241
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
242
+ scope = base_tags.where(opts)
243
+
244
+ if ActsAsTaggableOn::Tag.using_postgresql?
245
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
246
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
247
+ else
248
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
249
+ end
250
+
251
+ scope.all
252
+ end
253
+
254
+ ##
255
+ # Returns all tags that are not owned of a given context
256
+ def tags_on(context)
257
+ scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
258
+ # when preserving tag order, return tags in created order
259
+ # if we added the order to the association this would always apply
260
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
261
+ scope.all
262
+ end
263
+
264
+ def set_tag_list_on(context, new_list)
265
+ add_custom_context(context)
266
+
267
+ variable_name = "@#{context.to_s.singularize}_list"
268
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
269
+
270
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
271
+ end
272
+
273
+ def tagging_contexts
274
+ custom_contexts + self.class.tag_types.map(&:to_s)
275
+ end
276
+
277
+ def process_dirty_object(context,new_list)
278
+ value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
279
+ attrib = "#{context.to_s.singularize}_list"
280
+
281
+ if changed_attributes.include?(attrib)
282
+ # The attribute already has an unsaved change.
283
+ old = changed_attributes[attrib]
284
+ changed_attributes.delete(attrib) if (old.to_s == value.to_s)
285
+ else
286
+ old = tag_list_on(context).to_s
287
+ changed_attributes[attrib] = old if (old.to_s != value.to_s)
288
+ end
289
+ end
290
+
291
+ def reload(*args)
292
+ self.class.tag_types.each do |context|
293
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
294
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
295
+ end
296
+
297
+ super(*args)
298
+ end
299
+
300
+ def save_tags
301
+ tagging_contexts.each do |context|
302
+ next unless tag_list_cache_set_on(context)
303
+
304
+ # List of currently assigned tag names
305
+ tag_list = tag_list_cache_on(context).uniq
306
+
307
+ # Find existing tags or create non-existing tags:
308
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
309
+
310
+ # Tag objects for currently assigned tags
311
+ current_tags = tags_on(context)
312
+
313
+ # Tag maintenance based on whether preserving the created order of tags
314
+ if self.class.preserve_tag_order?
315
+ # First off order the array of tag objects to match the tag list
316
+ # rather than existing tags followed by new tags
317
+ tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
318
+ # To preserve tags in the order in which they were added
319
+ # delete all current tags and create new tags if the content or order has changed
320
+ old_tags = (tags == current_tags ? [] : current_tags)
321
+ new_tags = (tags == current_tags ? [] : tags)
322
+ else
323
+ # Delete discarded tags and create new tags
324
+ old_tags = current_tags - tags
325
+ new_tags = tags - current_tags
326
+ end
327
+
328
+ # Find taggings to remove:
329
+ if old_tags.present?
330
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
331
+ :context => context.to_s, :tag_id => old_tags).all
332
+ end
333
+
334
+ # Destroy old taggings:
335
+ if old_taggings.present?
336
+ ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
337
+ end
338
+
339
+ # Create new taggings:
340
+ new_tags.each do |tag|
341
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
342
+ end
343
+ end
344
+
345
+ true
346
+ end
347
+ end
348
+ end
349
+ end