crowdint_acts-as-taggable-on 2.3.2

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.
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