vincecima-acts-as-taggable-on 2.3.3

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 +11 -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 +248 -0
  9. data/Rakefile +13 -0
  10. data/acts-as-taggable-on.gemspec +28 -0
  11. data/lib/acts-as-taggable-on.rb +63 -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 +352 -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 +126 -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 +94 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +101 -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 +126 -0
  32. data/spec/acts_as_taggable_on/tag_spec.rb +185 -0
  33. data/spec/acts_as_taggable_on/taggable_spec.rb +543 -0
  34. data/spec/acts_as_taggable_on/tagger_spec.rb +138 -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 +52 -0
  42. data/spec/schema.rb +61 -0
  43. data/spec/spec_helper.rb +83 -0
  44. data/uninstall.rb +1 -0
  45. metadata +217 -0
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn
2
+ VERSION = '2.3.3'
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(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
104
+ at_most = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", 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}) #{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,352 @@
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
+ quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : ''
94
+
95
+ if options.delete(:exclude)
96
+ if options.delete(:wild)
97
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
98
+ else
99
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
100
+ end
101
+
102
+ 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)})"
103
+
104
+ elsif options.delete(:any)
105
+ # get tags, drop out if nothing returned (we need at least one)
106
+ if options.delete(:wild)
107
+ tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
108
+ else
109
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
110
+ end
111
+
112
+ return scoped(:conditions => "1 = 0") unless tags.length > 0
113
+
114
+ # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
115
+ # avoid ambiguous column name
116
+ taggings_context = context ? "_#{context}" : ''
117
+
118
+ taggings_alias = adjust_taggings_alias(
119
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
120
+ )
121
+
122
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
123
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
124
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
125
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
126
+
127
+ # don't need to sanitize sql, map all ids and join with OR logic
128
+ conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
129
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
130
+
131
+ joins << tagging_join
132
+
133
+ else
134
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
135
+ return empty_result unless tags.length == tag_list.length
136
+
137
+ tags.each do |tag|
138
+
139
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
140
+
141
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
142
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
143
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
144
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
145
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
146
+
147
+ if owned_by
148
+ tagging_join << " AND " +
149
+ sanitize_sql([
150
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
151
+ owned_by.id,
152
+ owned_by.class.base_class.to_s
153
+ ])
154
+ end
155
+
156
+ joins << tagging_join
157
+ end
158
+ end
159
+
160
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
161
+
162
+ if options.delete(:match_all)
163
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
164
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
165
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
166
+
167
+
168
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
169
+ group = group_columns
170
+ having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
171
+ end
172
+
173
+ scoped(:select => select_clause,
174
+ :joins => joins.join(" "),
175
+ :group => group,
176
+ :having => having,
177
+ :conditions => conditions.join(" AND "),
178
+ :order => options[:order],
179
+ :readonly => false)
180
+ end
181
+
182
+ def is_taggable?
183
+ true
184
+ end
185
+
186
+ def adjust_taggings_alias(taggings_alias)
187
+ if taggings_alias.size > 75
188
+ taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
189
+ end
190
+ taggings_alias
191
+ end
192
+ end
193
+
194
+ module InstanceMethods
195
+ # all column names are necessary for PostgreSQL group clause
196
+ def grouped_column_names_for(object)
197
+ self.class.grouped_column_names_for(object)
198
+ end
199
+
200
+ def custom_contexts
201
+ @custom_contexts ||= []
202
+ end
203
+
204
+ def is_taggable?
205
+ self.class.is_taggable?
206
+ end
207
+
208
+ def add_custom_context(value)
209
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
210
+ end
211
+
212
+ def cached_tag_list_on(context)
213
+ self["cached_#{context.to_s.singularize}_list"]
214
+ end
215
+
216
+ def tag_list_cache_set_on(context)
217
+ variable_name = "@#{context.to_s.singularize}_list"
218
+ !instance_variable_get(variable_name).nil?
219
+ end
220
+
221
+ def tag_list_cache_on(context)
222
+ variable_name = "@#{context.to_s.singularize}_list"
223
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
224
+ end
225
+
226
+ def tag_list_on(context)
227
+ add_custom_context(context)
228
+ tag_list_cache_on(context)
229
+ end
230
+
231
+ def all_tags_list_on(context)
232
+ variable_name = "@all_#{context.to_s.singularize}_list"
233
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
234
+
235
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
236
+ end
237
+
238
+ ##
239
+ # Returns all tags of a given context
240
+ def all_tags_on(context)
241
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
242
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
243
+
244
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
245
+ scope = base_tags.where(opts)
246
+
247
+ if ActsAsTaggableOn::Tag.using_postgresql?
248
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
249
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
250
+ else
251
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
252
+ end
253
+
254
+ scope.all
255
+ end
256
+
257
+ ##
258
+ # Returns all tags that are not owned of a given context
259
+ def tags_on(context)
260
+ scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
261
+ # when preserving tag order, return tags in created order
262
+ # if we added the order to the association this would always apply
263
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
264
+ scope.all
265
+ end
266
+
267
+ def set_tag_list_on(context, new_list)
268
+ add_custom_context(context)
269
+
270
+ variable_name = "@#{context.to_s.singularize}_list"
271
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
272
+
273
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
274
+ end
275
+
276
+ def tagging_contexts
277
+ custom_contexts + self.class.tag_types.map(&:to_s)
278
+ end
279
+
280
+ def process_dirty_object(context,new_list)
281
+ value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
282
+ attrib = "#{context.to_s.singularize}_list"
283
+
284
+ if changed_attributes.include?(attrib)
285
+ # The attribute already has an unsaved change.
286
+ old = changed_attributes[attrib]
287
+ changed_attributes.delete(attrib) if (old.to_s == value.to_s)
288
+ else
289
+ old = tag_list_on(context).to_s
290
+ changed_attributes[attrib] = old if (old.to_s != value.to_s)
291
+ end
292
+ end
293
+
294
+ def reload(*args)
295
+ self.class.tag_types.each do |context|
296
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
297
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
298
+ end
299
+
300
+ super(*args)
301
+ end
302
+
303
+ def save_tags
304
+ tagging_contexts.each do |context|
305
+ next unless tag_list_cache_set_on(context)
306
+
307
+ # List of currently assigned tag names
308
+ tag_list = tag_list_cache_on(context).uniq
309
+
310
+ # Find existing tags or create non-existing tags:
311
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
312
+
313
+ # Tag objects for currently assigned tags
314
+ current_tags = tags_on(context)
315
+
316
+ # Tag maintenance based on whether preserving the created order of tags
317
+ if self.class.preserve_tag_order?
318
+ # First off order the array of tag objects to match the tag list
319
+ # rather than existing tags followed by new tags
320
+ tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
321
+ # To preserve tags in the order in which they were added
322
+ # delete all current tags and create new tags if the content or order has changed
323
+ old_tags = (tags == current_tags ? [] : current_tags)
324
+ new_tags = (tags == current_tags ? [] : tags)
325
+ else
326
+ # Delete discarded tags and create new tags
327
+ old_tags = current_tags - tags
328
+ new_tags = tags - current_tags
329
+ end
330
+
331
+ # Find taggings to remove:
332
+ if old_tags.present?
333
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
334
+ :context => context.to_s, :tag_id => old_tags).all
335
+ end
336
+
337
+ # Destroy old taggings:
338
+ if old_taggings.present?
339
+ ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
340
+ end
341
+
342
+ # Create new taggings:
343
+ new_tags.each do |tag|
344
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
345
+ end
346
+ end
347
+
348
+ true
349
+ end
350
+ end
351
+ end
352
+ end