acts-as-taggable-on 2.2.2 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -5,4 +5,7 @@
5
5
  .rvmrc
6
6
  Gemfile.lock
7
7
  spec/database.yml
8
- tmp
8
+ tmp*.sw?
9
+ *.sw?
10
+ tmp
11
+ *.gem
data/README.rdoc CHANGED
@@ -16,17 +16,15 @@ was used.
16
16
 
17
17
  == Installation
18
18
 
19
- === Rails 2.3.x
19
+ === Rails 2.x
20
20
 
21
- To use it, add it to your Gemfile:
22
-
23
- gem 'acts-as-taggable-on', '~>2.1.0'
21
+ Not supported any more! It is time for update guys.
24
22
 
25
23
  === Rails 3.x
26
24
 
27
25
  To use it, add it to your Gemfile:
28
26
 
29
- gem 'acts-as-taggable-on', '~>2.2.0'
27
+ gem 'acts-as-taggable-on', '~> 2.3.1'
30
28
 
31
29
  ==== Post Installation
32
30
 
@@ -61,6 +59,25 @@ directory, you can run the specs for RoR 3.x with:
61
59
  User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
62
60
  @frankie.skill_counts
63
61
 
62
+ To preserve the order in which tags are created use acts_as_ordered_taggable:
63
+
64
+ class User < ActiveRecord::Base
65
+ # Alias for <tt>acts_as_ordered_taggable_on :tags</tt>:
66
+ acts_as_ordered_taggable
67
+ acts_as_ordered_taggable_on :skills, :interests
68
+ end
69
+
70
+ @user = User.new(:name => "Bobby")
71
+ @user.tag_list = "east, south"
72
+ @user.save
73
+
74
+ @user.tag_list = "north, east, south, west"
75
+ @user.save
76
+
77
+ @user.reload
78
+ @user.tag_list # => ["north", "east", "south", "west"]
79
+
80
+
64
81
  === Finding Tagged Objects
65
82
 
66
83
  Acts As Taggable On utilizes named_scopes to create an association for tags.
@@ -76,19 +93,19 @@ compatibility with the will_paginate gem:
76
93
  User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
77
94
 
78
95
  # Find a user with matching all tags, not just one
79
- User.tagged_with(["awesome", "cool"], :match_all => :true)
96
+ User.tagged_with(["awesome", "cool"], :match_all => true)
80
97
 
81
98
  # Find a user with any of the tags:
82
99
  User.tagged_with(["awesome", "cool"], :any => true)
83
100
 
84
101
  # Find a user that not tags with awesome or cool:
85
102
  User.tagged_with(["awesome", "cool"], :exclude => true)
86
-
103
+
87
104
  # Find a user with any of tags based on context:
88
105
  User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
89
106
 
90
107
  You can also use :wild => true option along with :any or :exclude option. It will looking for %awesome% and %cool% in sql.
91
-
108
+
92
109
  Tip: User.tagged_with([]) or '' will return [], but not all records.
93
110
 
94
111
  === Relationships
@@ -141,6 +158,21 @@ Tags can have owners:
141
158
  @some_photo.locations_from(@some_user) # => ["paris", "normandy"]
142
159
  @some_photo.owner_tags_on(@some_user, :locations) # => [#<ActsAsTaggableOn::Tag id: 1, name: "paris">...]
143
160
  @some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
161
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object
162
+
163
+ === Dirty objects
164
+
165
+ @bobby = User.find_by_name("Bobby")
166
+ @bobby.skill_list # => ["jogging", "diving"]
167
+
168
+ @boddy.skill_list_changed? #=> false
169
+ @boddy.changes #=> {}
170
+
171
+ @bobby.skill_list = "swimming"
172
+ @bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
173
+ @boddy.skill_list_changed? #=> true
174
+
175
+ @bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]
144
176
 
145
177
  === Tag cloud calculations
146
178
 
@@ -182,14 +214,21 @@ CSS:
182
214
  .css3 { font-size: 1.4em; }
183
215
  .css4 { font-size: 1.6em; }
184
216
 
185
- == Remove unused tags
217
+ == Configuration
218
+
219
+ If you would like to remove unused tag objects after removing taggings, add
220
+
221
+ ActsAsTaggableOn.remove_unused_tags = true
222
+
223
+ If you want force tags to be saved downcased:
224
+
225
+ ActsAsTaggableOn.force_lowercase = true
226
+
227
+ If you want tags to be saved parametrized (you can redefine to_param as well):
228
+
229
+ ActsAsTaggableOn.force_parameterize = true
230
+
186
231
 
187
- If you would like to remove unused tag objects after removing taggings, add
188
-
189
- ActsAsTaggableOn::Tag.remove_unused = true
190
-
191
- to initializer file.
192
-
193
232
  == Contributors
194
233
 
195
234
  We have a long list of valued contributors. {Check them all}[https://github.com/mbleigh/acts-as-taggable-on/contributors]
@@ -4,14 +4,14 @@ require 'acts-as-taggable-on/version'
4
4
  Gem::Specification.new do |gem|
5
5
  gem.name = %q{acts-as-taggable-on}
6
6
  gem.authors = ["Michael Bleigh"]
7
- gem.date = %q{2012-01-06}
7
+ gem.date = %q{2012-07-16}
8
8
  gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
9
9
  gem.summary = "Advanced tagging for Rails."
10
10
  gem.email = %q{michael@intridea.com}
11
11
  gem.homepage = ''
12
12
 
13
13
  gem.add_runtime_dependency 'rails', '~> 3.0'
14
- gem.add_development_dependency 'rspec', '~> 2.5'
14
+ gem.add_development_dependency 'rspec', '~> 2.6'
15
15
  gem.add_development_dependency 'ammeter', '~> 0.1.3'
16
16
  gem.add_development_dependency 'sqlite3'
17
17
  gem.add_development_dependency 'mysql2', '~> 0.3.7'
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '2.2.2'
2
+ VERSION = '2.3.3'
3
3
  end
4
4
 
@@ -6,6 +6,30 @@ require "digest/sha1"
6
6
 
7
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
8
 
9
+ module ActsAsTaggableOn
10
+ mattr_accessor :delimiter
11
+ @@delimiter = ','
12
+
13
+ mattr_accessor :force_lowercase
14
+ @@force_lowercase = false
15
+
16
+ mattr_accessor :force_parameterize
17
+ @@force_parameterize = false
18
+
19
+ mattr_accessor :remove_unused_tags
20
+ self.remove_unused_tags = false
21
+
22
+ def self.glue
23
+ delimiter = @@delimiter.kind_of?(Array) ? @@delimiter[0] : @@delimiter
24
+ delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
25
+ end
26
+
27
+ def self.setup
28
+ yield self
29
+ end
30
+ end
31
+
32
+
9
33
  require "acts_as_taggable_on/utils"
10
34
 
11
35
  require "acts_as_taggable_on/taggable"
@@ -14,6 +38,7 @@ require "acts_as_taggable_on/acts_as_taggable_on/collection"
14
38
  require "acts_as_taggable_on/acts_as_taggable_on/cache"
15
39
  require "acts_as_taggable_on/acts_as_taggable_on/ownership"
16
40
  require "acts_as_taggable_on/acts_as_taggable_on/related"
41
+ require "acts_as_taggable_on/acts_as_taggable_on/dirty"
17
42
 
18
43
  require "acts_as_taggable_on/tagger"
19
44
  require "acts_as_taggable_on/tag"
@@ -31,4 +56,5 @@ end
31
56
 
32
57
  if defined?(ActionView::Base)
33
58
  ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
34
- end
59
+ end
60
+
@@ -18,11 +18,22 @@ module ActsAsTaggableOn::Taggable
18
18
  tag_type = tags_type.to_s.singularize
19
19
  context_taggings = "#{tag_type}_taggings".to_sym
20
20
  context_tags = tags_type.to_sym
21
-
21
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
22
+
22
23
  class_eval do
23
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
- :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
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
26
37
  end
27
38
 
28
39
  class_eval %(
@@ -41,11 +52,11 @@ module ActsAsTaggableOn::Taggable
41
52
  end
42
53
  end
43
54
 
44
- def acts_as_taggable_on(*args)
45
- super(*args)
55
+ def taggable_on(preserve_tag_order, *tag_types)
56
+ super(preserve_tag_order, *tag_types)
46
57
  initialize_acts_as_taggable_on_core
47
58
  end
48
-
59
+
49
60
  # all column names are necessary for PostgreSQL group clause
50
61
  def grouped_column_names_for(object)
51
62
  object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
@@ -79,14 +90,15 @@ module ActsAsTaggableOn::Taggable
79
90
  context = options.delete(:on)
80
91
  owned_by = options.delete(:owned_by)
81
92
  alias_base_name = undecorated_table_name.gsub('.','_')
93
+ quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : ''
82
94
 
83
95
  if options.delete(:exclude)
84
96
  if options.delete(:wild)
85
97
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
86
98
  else
87
99
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
88
- end
89
-
100
+ end
101
+
90
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)})"
91
103
 
92
104
  elsif options.delete(:any)
@@ -94,19 +106,21 @@ module ActsAsTaggableOn::Taggable
94
106
  if options.delete(:wild)
95
107
  tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
96
108
  else
97
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
109
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
98
110
  end
99
-
111
+
100
112
  return scoped(:conditions => "1 = 0") unless tags.length > 0
101
113
 
102
114
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
103
115
  # avoid ambiguous column name
104
116
  taggings_context = context ? "_#{context}" : ''
105
117
 
106
- taggings_alias = "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:safe_name).join('_'))}"
118
+ taggings_alias = adjust_taggings_alias(
119
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
120
+ )
107
121
 
108
122
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
109
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
123
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
110
124
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
111
125
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
112
126
 
@@ -117,15 +131,15 @@ module ActsAsTaggableOn::Taggable
117
131
  joins << tagging_join
118
132
 
119
133
  else
120
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
121
- return empty_result unless tags.length == tag_list.length
134
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
135
+ return empty_result unless tags.length == tag_list.length
122
136
 
123
137
  tags.each do |tag|
124
138
 
125
- taggings_alias = "#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.safe_name)}"
139
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
126
140
 
127
141
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
128
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
142
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
129
143
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
130
144
  " AND #{taggings_alias}.tag_id = #{tag.id}"
131
145
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
@@ -135,7 +149,7 @@ module ActsAsTaggableOn::Taggable
135
149
  sanitize_sql([
136
150
  "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
137
151
  owned_by.id,
138
- owned_by.class.to_s
152
+ owned_by.class.base_class.to_s
139
153
  ])
140
154
  end
141
155
 
@@ -143,21 +157,23 @@ module ActsAsTaggableOn::Taggable
143
157
  end
144
158
  end
145
159
 
146
- taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
160
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
147
161
 
148
162
  if options.delete(:match_all)
149
163
  joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
150
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
164
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
151
165
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
152
166
 
153
167
 
154
168
  group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
155
- group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
169
+ group = group_columns
170
+ having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
156
171
  end
157
172
 
158
173
  scoped(:select => select_clause,
159
174
  :joins => joins.join(" "),
160
175
  :group => group,
176
+ :having => having,
161
177
  :conditions => conditions.join(" AND "),
162
178
  :order => options[:order],
163
179
  :readonly => false)
@@ -166,6 +182,13 @@ module ActsAsTaggableOn::Taggable
166
182
  def is_taggable?
167
183
  true
168
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
169
192
  end
170
193
 
171
194
  module InstanceMethods
@@ -234,13 +257,19 @@ module ActsAsTaggableOn::Taggable
234
257
  ##
235
258
  # Returns all tags that are not owned of a given context
236
259
  def tags_on(context)
237
- base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
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
238
265
  end
239
266
 
240
267
  def set_tag_list_on(context, new_list)
241
268
  add_custom_context(context)
242
269
 
243
270
  variable_name = "@#{context.to_s.singularize}_list"
271
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
272
+
244
273
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
245
274
  end
246
275
 
@@ -248,6 +277,20 @@ module ActsAsTaggableOn::Taggable
248
277
  custom_contexts + self.class.tag_types.map(&:to_s)
249
278
  end
250
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
+
251
294
  def reload(*args)
252
295
  self.class.tag_types.each do |context|
253
296
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
@@ -261,21 +304,38 @@ module ActsAsTaggableOn::Taggable
261
304
  tagging_contexts.each do |context|
262
305
  next unless tag_list_cache_set_on(context)
263
306
 
307
+ # List of currently assigned tag names
264
308
  tag_list = tag_list_cache_on(context).uniq
265
309
 
266
310
  # Find existing tags or create non-existing tags:
267
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
311
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
268
312
 
313
+ # Tag objects for currently assigned tags
269
314
  current_tags = tags_on(context)
270
- old_tags = current_tags - tag_list
271
- new_tags = tag_list - current_tags
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
272
330
 
273
331
  # Find taggings to remove:
274
- old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
275
- :context => context.to_s, :tag_id => old_tags).all
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
276
336
 
337
+ # Destroy old taggings:
277
338
  if old_taggings.present?
278
- # Destroy old taggings:
279
339
  ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
280
340
  end
281
341
 
@@ -0,0 +1,37 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Dirty
3
+ def self.included(base)
4
+ base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
5
+
6
+ base.initialize_acts_as_taggable_on_dirty
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_dirty
11
+ tag_types.map(&:to_s).each do |tags_type|
12
+ tag_type = tags_type.to_s.singularize
13
+ context_tags = tags_type.to_sym
14
+
15
+ class_eval %(
16
+ def #{tag_type}_list_changed?
17
+ changed_attributes.include?("#{tag_type}_list")
18
+ end
19
+
20
+ def #{tag_type}_list_was
21
+ changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
22
+ end
23
+
24
+ def #{tag_type}_list_change
25
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
26
+ end
27
+
28
+ def #{tag_type}_list_changes
29
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
+ end
31
+ )
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -31,12 +31,16 @@ module ActsAsTaggableOn::Taggable
31
31
  module InstanceMethods
32
32
  def owner_tags_on(owner, context)
33
33
  if owner.nil?
34
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
34
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
35
35
  else
36
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
36
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
39
39
  end
40
+ # when preserving tag order, return tags in created order
41
+ # if we added the order to the association this would always apply
42
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
43
+ scope.all
40
44
  end
41
45
 
42
46
  def cached_owned_tag_list_on(context)
@@ -73,21 +77,38 @@ module ActsAsTaggableOn::Taggable
73
77
  def save_owned_tags
74
78
  tagging_contexts.each do |context|
75
79
  cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
76
81
  # Find existing tags or create non-existing tags:
77
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
82
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
78
83
 
79
- owned_tags = owner_tags_on(owner, context)
80
- old_tags = owned_tags - tag_list
81
- new_tags = tag_list - owned_tags
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+
87
+ # Tag maintenance based on whether preserving the created order of tags
88
+ if self.class.preserve_tag_order?
89
+ # First off order the array of tag objects to match the tag list
90
+ # rather than existing tags followed by new tags
91
+ tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
92
+ # To preserve tags in the order in which they were added
93
+ # delete all owned tags and create new tags if the content or order has changed
94
+ old_tags = (tags == owned_tags ? [] : owned_tags)
95
+ new_tags = (tags == owned_tags ? [] : tags)
96
+ else
97
+ # Delete discarded tags and create new tags
98
+ old_tags = owned_tags - tags
99
+ new_tags = tags - owned_tags
100
+ end
82
101
 
83
102
  # Find all taggings that belong to the taggable (self), are owned by the owner,
84
103
  # have the correct context, and are removed from the list.
85
- old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
86
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
87
- :tag_id => old_tags, :context => context).all
104
+ if old_tags.present?
105
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
106
+ :tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
107
+ :tag_id => old_tags, :context => context).all
108
+ end
88
109
 
110
+ # Destroy old taggings:
89
111
  if old_taggings.present?
90
- # Destroy old taggings:
91
112
  ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
92
113
  end
93
114
 
@@ -102,4 +123,4 @@ module ActsAsTaggableOn::Taggable
102
123
  end
103
124
  end
104
125
  end
105
- end
126
+ end
@@ -2,9 +2,6 @@ module ActsAsTaggableOn
2
2
  class Tag < ::ActiveRecord::Base
3
3
  include ActsAsTaggableOn::Utils
4
4
 
5
- cattr_accessor :remove_unused
6
- self.remove_unused = false
7
-
8
5
  attr_accessible :name
9
6
 
10
7
  ### ASSOCIATIONS:
@@ -15,15 +12,16 @@ module ActsAsTaggableOn
15
12
 
16
13
  validates_presence_of :name
17
14
  validates_uniqueness_of :name
15
+ validates_length_of :name, :maximum => 255
18
16
 
19
17
  ### SCOPES:
20
18
 
21
19
  def self.named(name)
22
- where(["name #{like_operator} ? ESCAPE '!'", escape_like(name)])
20
+ where(["lower(name) = ?", name.downcase])
23
21
  end
24
22
 
25
23
  def self.named_any(list)
26
- where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", escape_like(tag.to_s)]) }.join(" OR "))
24
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
27
25
  end
28
26
 
29
27
  def self.named_like(name)
@@ -69,15 +67,11 @@ module ActsAsTaggableOn
69
67
  read_attribute(:count).to_i
70
68
  end
71
69
 
72
- def safe_name
73
- name.gsub(/[^a-zA-Z0-9]/, '')
74
- end
75
-
76
70
  class << self
77
71
  private
78
72
  def comparable_name(str)
79
- RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
73
+ str.mb_chars.downcase.to_s
80
74
  end
81
75
  end
82
76
  end
83
- end
77
+ end
@@ -1,14 +1,13 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
1
3
  module ActsAsTaggableOn
2
4
  class TagList < Array
3
- cattr_accessor :delimiter
4
- self.delimiter = ','
5
-
6
5
  attr_accessor :owner
7
6
 
8
7
  def initialize(*args)
9
8
  add(*args)
10
9
  end
11
-
10
+
12
11
  ##
13
12
  # Returns a new TagList using the given tag string.
14
13
  #
@@ -16,17 +15,18 @@ module ActsAsTaggableOn
16
15
  # tag_list = TagList.from("One , Two, Three")
17
16
  # tag_list # ["One", "Two", "Three"]
18
17
  def self.from(string)
19
- glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
20
- string = string.join(glue) if string.respond_to?(:join)
18
+ string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
21
19
 
22
20
  new.tap do |tag_list|
23
21
  string = string.to_s.dup
24
22
 
25
23
  # Parse the quoted tags
26
- string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
27
- string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
24
+ d = ActsAsTaggableOn.delimiter
25
+ d = d.join("|") if d.kind_of?(Array)
26
+ string.gsub!(/(\A|#{d})\s*"(.*?)"\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
27
+ string.gsub!(/(\A|#{d})\s*'(.*?)'\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
28
28
 
29
- tag_list.add(string.split(delimiter))
29
+ tag_list.add(string.split(Regexp.new d))
30
30
  end
31
31
  end
32
32
 
@@ -69,16 +69,21 @@ module ActsAsTaggableOn
69
69
  tags.send(:clean!)
70
70
 
71
71
  tags.map do |name|
72
- name.include?(delimiter) ? "\"#{name}\"" : name
73
- end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
72
+ d = ActsAsTaggableOn.delimiter
73
+ d = Regexp.new d.join("|") if d.kind_of? Array
74
+ name.index(d) ? "\"#{name}\"" : name
75
+ end.join(ActsAsTaggableOn.glue)
74
76
  end
75
77
 
76
78
  private
77
-
79
+
78
80
  # Remove whitespace, duplicates, and blanks.
79
81
  def clean!
80
82
  reject!(&:blank?)
81
83
  map!(&:strip)
84
+ map!(&:downcase) if ActsAsTaggableOn.force_lowercase
85
+ map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
86
+
82
87
  uniq!
83
88
  end
84
89
 
@@ -93,4 +98,4 @@ module ActsAsTaggableOn
93
98
  args.flatten!
94
99
  end
95
100
  end
96
- end
101
+ end