acts-as-taggable-on 2.2.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,4 +5,6 @@
5
5
  .rvmrc
6
6
  Gemfile.lock
7
7
  spec/database.yml
8
- tmp
8
+ tmp*.sw?
9
+ *.sw?
10
+ tmp
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.2.2'
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]
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
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'
@@ -6,6 +6,29 @@ 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.ends_with?(" ") ? @@delimiter : "#{@@delimiter} "
24
+ end
25
+
26
+ def self.setup
27
+ yield self
28
+ end
29
+ end
30
+
31
+
9
32
  require "acts_as_taggable_on/utils"
10
33
 
11
34
  require "acts_as_taggable_on/taggable"
@@ -14,6 +37,7 @@ require "acts_as_taggable_on/acts_as_taggable_on/collection"
14
37
  require "acts_as_taggable_on/acts_as_taggable_on/cache"
15
38
  require "acts_as_taggable_on/acts_as_taggable_on/ownership"
16
39
  require "acts_as_taggable_on/acts_as_taggable_on/related"
40
+ require "acts_as_taggable_on/acts_as_taggable_on/dirty"
17
41
 
18
42
  require "acts_as_taggable_on/tagger"
19
43
  require "acts_as_taggable_on/tag"
@@ -31,4 +55,5 @@ end
31
55
 
32
56
  if defined?(ActionView::Base)
33
57
  ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
34
- end
58
+ end
59
+
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '2.2.2'
2
+ VERSION = '2.3.0'
3
3
  end
4
4
 
@@ -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(", ")
@@ -85,8 +96,8 @@ module ActsAsTaggableOn::Taggable
85
96
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
86
97
  else
87
98
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
88
- end
89
-
99
+ end
100
+
90
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)})"
91
102
 
92
103
  elsif options.delete(:any)
@@ -94,16 +105,18 @@ module ActsAsTaggableOn::Taggable
94
105
  if options.delete(:wild)
95
106
  tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
96
107
  else
97
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
98
109
  end
99
-
110
+
100
111
  return scoped(:conditions => "1 = 0") unless tags.length > 0
101
112
 
102
113
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
103
114
  # avoid ambiguous column name
104
115
  taggings_context = context ? "_#{context}" : ''
105
116
 
106
- taggings_alias = "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:safe_name).join('_'))}"
117
+ taggings_alias = adjust_taggings_alias(
118
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
119
+ )
107
120
 
108
121
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
109
122
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -117,12 +130,12 @@ module ActsAsTaggableOn::Taggable
117
130
  joins << tagging_join
118
131
 
119
132
  else
120
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
121
- return empty_result unless tags.length == tag_list.length
133
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
134
+ return empty_result unless tags.length == tag_list.length
122
135
 
123
136
  tags.each do |tag|
124
137
 
125
- taggings_alias = "#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.safe_name)}"
138
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
126
139
 
127
140
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
128
141
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -143,7 +156,7 @@ module ActsAsTaggableOn::Taggable
143
156
  end
144
157
  end
145
158
 
146
- taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
159
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
147
160
 
148
161
  if options.delete(:match_all)
149
162
  joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
@@ -166,6 +179,13 @@ module ActsAsTaggableOn::Taggable
166
179
  def is_taggable?
167
180
  true
168
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
169
189
  end
170
190
 
171
191
  module InstanceMethods
@@ -234,13 +254,19 @@ module ActsAsTaggableOn::Taggable
234
254
  ##
235
255
  # Returns all tags that are not owned of a given context
236
256
  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
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
238
262
  end
239
263
 
240
264
  def set_tag_list_on(context, new_list)
241
265
  add_custom_context(context)
242
266
 
243
267
  variable_name = "@#{context.to_s.singularize}_list"
268
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
269
+
244
270
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
245
271
  end
246
272
 
@@ -248,6 +274,20 @@ module ActsAsTaggableOn::Taggable
248
274
  custom_contexts + self.class.tag_types.map(&:to_s)
249
275
  end
250
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
+
251
291
  def reload(*args)
252
292
  self.class.tag_types.each do |context|
253
293
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
@@ -261,21 +301,38 @@ module ActsAsTaggableOn::Taggable
261
301
  tagging_contexts.each do |context|
262
302
  next unless tag_list_cache_set_on(context)
263
303
 
304
+ # List of currently assigned tag names
264
305
  tag_list = tag_list_cache_on(context).uniq
265
306
 
266
307
  # Find existing tags or create non-existing tags:
267
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
308
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
268
309
 
310
+ # Tag objects for currently assigned tags
269
311
  current_tags = tags_on(context)
270
- old_tags = current_tags - tag_list
271
- new_tags = tag_list - current_tags
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
272
327
 
273
328
  # 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
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
276
333
 
334
+ # Destroy old taggings:
277
335
  if old_taggings.present?
278
- # Destroy old taggings:
279
336
  ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
280
337
  end
281
338
 
@@ -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.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.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