acts-as-taggable-on 3.3.0 → 3.4.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +13 -16
  3. data/Appraisals +6 -6
  4. data/CHANGELOG.md +103 -4
  5. data/Gemfile +1 -0
  6. data/README.md +63 -2
  7. data/UPGRADING.md +1 -7
  8. data/acts-as-taggable-on.gemspec +2 -2
  9. data/gemfiles/activerecord_3.2.gemfile +2 -1
  10. data/gemfiles/activerecord_4.0.gemfile +2 -1
  11. data/gemfiles/activerecord_4.1.gemfile +2 -1
  12. data/gemfiles/{activerecord_edge.gemfile → activerecord_4.2.gemfile} +3 -2
  13. data/lib/acts-as-taggable-on.rb +15 -2
  14. data/lib/acts_as_taggable_on/default_parser.rb +79 -0
  15. data/lib/acts_as_taggable_on/generic_parser.rb +19 -0
  16. data/lib/acts_as_taggable_on/tag.rb +17 -11
  17. data/lib/acts_as_taggable_on/tag_list.rb +8 -4
  18. data/lib/acts_as_taggable_on/tag_list_parser.rb +7 -64
  19. data/lib/acts_as_taggable_on/taggable/cache.rb +4 -0
  20. data/lib/acts_as_taggable_on/taggable/collection.rb +2 -2
  21. data/lib/acts_as_taggable_on/taggable/core.rb +45 -32
  22. data/lib/acts_as_taggable_on/taggable/ownership.rb +2 -2
  23. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  24. data/lib/acts_as_taggable_on/tagging.rb +6 -2
  25. data/lib/acts_as_taggable_on/utils.rb +1 -1
  26. data/lib/acts_as_taggable_on/version.rb +1 -1
  27. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +16 -0
  28. data/spec/acts_as_taggable_on/caching_spec.rb +6 -0
  29. data/spec/acts_as_taggable_on/default_parser_spec.rb +47 -0
  30. data/spec/acts_as_taggable_on/generic_parser_spec.rb +14 -0
  31. data/spec/acts_as_taggable_on/related_spec.rb +9 -0
  32. data/spec/acts_as_taggable_on/tag_list_spec.rb +29 -0
  33. data/spec/acts_as_taggable_on/tag_spec.rb +35 -1
  34. data/spec/acts_as_taggable_on/taggable_spec.rb +2 -1
  35. data/spec/acts_as_taggable_on/tagger_spec.rb +13 -0
  36. data/spec/acts_as_taggable_on/utils_spec.rb +7 -0
  37. data/spec/spec_helper.rb +4 -0
  38. metadata +16 -17
@@ -7,72 +7,15 @@ module ActsAsTaggableOn
7
7
  # tag_list # ["One", "Two", "Three"]
8
8
  module TagListParser
9
9
  class << self
10
+ ## DEPRECATED
10
11
  def parse(string)
11
- string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
12
- TagList.new.tap do |tag_list|
13
- string = string.to_s.dup
14
-
15
-
16
- string.gsub!(double_quote_pattern) {
17
- # Append the matched tag to the tag list
18
- tag_list << Regexp.last_match[2]
19
- # Return the matched delimiter ($3) to replace the matched items
20
- ''
21
- }
22
-
23
- string.gsub!(single_quote_pattern) {
24
- # Append the matched tag ($2) to the tag list
25
- tag_list << Regexp.last_match[2]
26
- # Return an empty string to replace the matched items
27
- ''
28
- }
29
-
30
- # split the string by the delimiter
31
- # and add to the tag_list
32
- tag_list.add(string.split(Regexp.new delimiter))
33
- end
12
+ ActiveRecord::Base.logger.warn <<WARNING
13
+ ActsAsTaggableOn::TagListParser.parse is deprecated \
14
+ and will be removed from v4.0+, use \
15
+ ActsAsTaggableOn::TagListParser.new instead
16
+ WARNING
17
+ DefaultParser.new(string).parse
34
18
  end
35
-
36
-
37
- # private
38
- def delimiter
39
- # Parse the quoted tags
40
- d = ActsAsTaggableOn.delimiter
41
- # Separate multiple delimiters by bitwise operator
42
- d = d.join('|') if d.kind_of?(Array)
43
- d
44
- end
45
-
46
- # ( # Tag start delimiter ($1)
47
- # \A | # Either string start or
48
- # #{delimiter} # a delimiter
49
- # )
50
- # \s*" # quote (") optionally preceded by whitespace
51
- # (.*?) # Tag ($2)
52
- # "\s* # quote (") optionally followed by whitespace
53
- # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
54
- # #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
55
- # \z # string end
56
- # )
57
- def double_quote_pattern
58
- /(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
59
- end
60
-
61
- # ( # Tag start delimiter ($1)
62
- # \A | # Either string start or
63
- # #{delimiter} # a delimiter
64
- # )
65
- # \s*' # quote (') optionally preceded by whitespace
66
- # (.*?) # Tag ($2)
67
- # '\s* # quote (') optionally followed by whitespace
68
- # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
69
- # #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
70
- # \z # string end
71
- # )
72
- def single_quote_pattern
73
- /(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
74
- end
75
-
76
19
  end
77
20
  end
78
21
  end
@@ -40,6 +40,10 @@ module ActsAsTaggableOn::Taggable
40
40
  end
41
41
  end
42
42
 
43
+ def reset_column_information
44
+ super
45
+ @acts_as_taggable_on_cache_columns = nil
46
+ end
43
47
  end
44
48
  end
45
49
 
@@ -135,7 +135,7 @@ module ActsAsTaggableOn::Taggable
135
135
  table_name_pkey = "#{table_name}.#{primary_key}"
136
136
  if ActsAsTaggableOn::Utils.using_mysql?
137
137
  # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
138
- scoped_ids = select(table_name_pkey).map(&:id)
138
+ scoped_ids = pluck(table_name_pkey)
139
139
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
140
140
  else
141
141
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
@@ -169,7 +169,7 @@ module ActsAsTaggableOn::Taggable
169
169
  end
170
170
 
171
171
  module CalculationMethods
172
- def count(column_name=:all)
172
+ def count(column_name=:all, options = {})
173
173
  # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
174
174
  super
175
175
  end
@@ -27,7 +27,7 @@ module ActsAsTaggableOn::Taggable
27
27
  dependent: :destroy,
28
28
  class_name: 'ActsAsTaggableOn::Tagging',
29
29
  order: taggings_order,
30
- conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
30
+ conditions: {context: tags_type},
31
31
  include: :tag
32
32
 
33
33
  has_many_with_taggable_compatibility context_tags, through: context_taggings,
@@ -77,15 +77,15 @@ module ActsAsTaggableOn::Taggable
77
77
  # * <tt>:end_at</tt> - Restrict the tags to those created before a certain time
78
78
  #
79
79
  # Example:
80
- # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
81
- # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
82
- # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
83
- # User.tagged_with("awesome", "cool", :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
84
- # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
85
- # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
86
- # User.tagged_with("awesome", "cool", :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
80
+ # User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool
81
+ # User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool
82
+ # User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool
83
+ # User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
84
+ # User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool
85
+ # User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
86
+ # User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
87
87
  def tagged_with(tags, options = {})
88
- tag_list = ActsAsTaggableOn::TagListParser.parse(tags)
88
+ tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
89
89
  options = options.dup
90
90
  empty_result = where('1 = 0')
91
91
 
@@ -100,6 +100,7 @@ module ActsAsTaggableOn::Taggable
100
100
  context = options.delete(:on)
101
101
  owned_by = options.delete(:owned_by)
102
102
  alias_base_name = undecorated_table_name.gsub('.', '_')
103
+ # FIXME use ActiveRecord's connection quote_column_name
103
104
  quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
104
105
 
105
106
  if options.delete(:exclude)
@@ -117,7 +118,7 @@ module ActsAsTaggableOn::Taggable
117
118
  " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
118
119
  " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
119
120
  " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
120
-
121
+
121
122
  joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
122
123
  joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
123
124
  end
@@ -130,7 +131,7 @@ module ActsAsTaggableOn::Taggable
130
131
  ActsAsTaggableOn::Tag.named_any(tag_list)
131
132
  end
132
133
 
133
- return empty_result unless tags.length > 0
134
+ return empty_result if tags.length == 0
134
135
 
135
136
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
136
137
  # avoid ambiguous column name
@@ -140,21 +141,22 @@ module ActsAsTaggableOn::Taggable
140
141
  "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
141
142
  )
142
143
 
143
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
144
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
144
+ tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
145
+ " WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
145
146
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
146
147
 
147
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
148
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
148
+ tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
149
+ tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
149
150
 
150
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
151
+ tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
151
152
 
152
153
  # don't need to sanitize sql, map all ids and join with OR logic
153
- conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(' OR ')
154
+ tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
155
+ tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
154
156
  select_clause << " #{table_name}.*" unless context and tag_types.one?
155
157
 
156
158
  if owned_by
157
- tagging_join << ' AND ' +
159
+ tagging_cond << ' AND ' +
158
160
  sanitize_sql([
159
161
  "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
160
162
  owned_by.id,
@@ -162,10 +164,9 @@ module ActsAsTaggableOn::Taggable
162
164
  ])
163
165
  end
164
166
 
165
- joins << tagging_join
166
- unless any == 'distinct' # Fix issue #544
167
- group = "#{table_name}.#{primary_key}"
168
- select_clause << group
167
+ conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
168
+ if options.delete(:order_by_matching_tag_count)
169
+ order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
169
170
  end
170
171
  else
171
172
  tags = ActsAsTaggableOn::Tag.named_any(tag_list)
@@ -224,11 +225,11 @@ module ActsAsTaggableOn::Taggable
224
225
  query = self
225
226
  query = self.select(select_clause.join(',')) unless select_clause.empty?
226
227
  query.joins(joins.join(' '))
227
- .where(conditions.join(' AND '))
228
- .group(group)
229
- .having(having)
230
- .order(order_by.join(', '))
231
- .readonly(false)
228
+ .where(conditions.join(' AND '))
229
+ .group(group)
230
+ .having(having)
231
+ .order(order_by.join(', '))
232
+ .readonly(false)
232
233
  end
233
234
 
234
235
  def is_taggable?
@@ -278,7 +279,7 @@ module ActsAsTaggableOn::Taggable
278
279
  if instance_variable_get(variable_name)
279
280
  instance_variable_get(variable_name)
280
281
  elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
281
- instance_variable_set(variable_name, ActsAsTaggableOn::TagListParser.parse(cached_tag_list_on(context)))
282
+ instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
282
283
  else
283
284
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
284
285
  end
@@ -328,7 +329,7 @@ module ActsAsTaggableOn::Taggable
328
329
  variable_name = "@#{context.to_s.singularize}_list"
329
330
  process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
330
331
 
331
- instance_variable_set(variable_name, ActsAsTaggableOn::TagListParser.parse(new_list))
332
+ instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
332
333
  end
333
334
 
334
335
  def tagging_contexts
@@ -342,13 +343,13 @@ module ActsAsTaggableOn::Taggable
342
343
  if changed_attributes.include?(attrib)
343
344
  # The attribute already has an unsaved change.
344
345
  old = changed_attributes[attrib]
345
- changed_attributes.delete(attrib) if old.to_s == value.to_s
346
+ @changed_attributes.delete(attrib) if old.to_s == value.to_s
346
347
  else
347
348
  old = tag_list_on(context)
348
349
  if self.class.preserve_tag_order
349
- changed_attributes[attrib] = old if old.to_s != value.to_s
350
+ @changed_attributes[attrib] = old if old.to_s != value.to_s
350
351
  else
351
- changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn::TagListParser.parse(value).sort
352
+ @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
352
353
  end
353
354
  end
354
355
  end
@@ -420,6 +421,18 @@ module ActsAsTaggableOn::Taggable
420
421
 
421
422
  private
422
423
 
424
+ # Filters the tag lists from the attribute names.
425
+ def attributes_for_update(attribute_names)
426
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
427
+ super.delete_if {|attr| tag_lists.include? attr }
428
+ end
429
+
430
+ # Filters the tag lists from the attribute names.
431
+ def attributes_for_create(attribute_names)
432
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
433
+ super.delete_if {|attr| tag_lists.include? attr }
434
+ end
435
+
423
436
  ##
424
437
  # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
425
438
  # context is provided so that you may conditionally use a Tag subclass
@@ -63,7 +63,7 @@ module ActsAsTaggableOn::Taggable
63
63
 
64
64
  cache = cached_owned_tag_list_on(context)
65
65
 
66
- cache[owner] = ActsAsTaggableOn::TagListParser.parse(new_list)
66
+ cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse
67
67
  end
68
68
 
69
69
  def reload(*args)
@@ -82,7 +82,7 @@ module ActsAsTaggableOn::Taggable
82
82
  tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context)
83
83
 
84
84
  # Tag objects for owned tags
85
- owned_tags = owner_tags_on(owner, context)
85
+ owned_tags = owner_tags_on(owner, context).to_a
86
86
 
87
87
  # Tag maintenance based on whether preserving the created order of tags
88
88
  if self.class.preserve_tag_order?
@@ -43,7 +43,7 @@ module ActsAsTaggableOn::Taggable
43
43
  def related_tags_for(context, klass, options = {})
44
44
  tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
45
45
  tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
46
- related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find])
46
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, context])
47
47
  end
48
48
 
49
49
  private
@@ -11,7 +11,7 @@ module ActsAsTaggableOn
11
11
  :tagger_type,
12
12
  :tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
13
13
 
14
- belongs_to :tag, class_name: 'ActsAsTaggableOn::Tag' , counter_cache: true
14
+ belongs_to :tag, class_name: 'ActsAsTaggableOn::Tag', counter_cache: ActsAsTaggableOn.tags_counter
15
15
  belongs_to :taggable, polymorphic: true
16
16
  belongs_to :tagger, polymorphic: true
17
17
 
@@ -32,7 +32,11 @@ module ActsAsTaggableOn
32
32
 
33
33
  def remove_unused_tags
34
34
  if ActsAsTaggableOn.remove_unused_tags
35
- tag.destroy if tag.reload.taggings_count.zero?
35
+ if ActsAsTaggableOn.tags_counter
36
+ tag.destroy if tag.reload.taggings_count.zero?
37
+ else
38
+ tag.destroy if tag.reload.taggings.count.zero?
39
+ end
36
40
  end
37
41
  end
38
42
  end
@@ -18,7 +18,7 @@ module ActsAsTaggableOn
18
18
  end
19
19
 
20
20
  def sha_prefix(string)
21
- Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
21
+ Digest::SHA1.hexdigest(string)[0..6]
22
22
  end
23
23
 
24
24
  def active_record4?
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '3.3.0'
2
+ VERSION = '3.4.4'
3
3
  end
4
4
 
@@ -133,6 +133,22 @@ describe 'Acts As Taggable On' do
133
133
  expect(taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to_not include(taggable1)
134
134
  end
135
135
 
136
+ it 'should ensure joins to multiple taggings maintain their contexts when aliasing' do
137
+ taggable1 = TaggableModel.create!(name: 'Taggable 1')
138
+
139
+ taggable1.offering_list = 'one'
140
+ taggable1.need_list = 'two'
141
+
142
+ taggable1.save
143
+
144
+ column = TaggableModel.connection.quote_column_name("context")
145
+ offer_alias = TaggableModel.connection.quote_table_name("taggings")
146
+ need_alias = TaggableModel.connection.quote_table_name("need_taggings_taggable_models_join")
147
+
148
+ expect(TaggableModel.joins(:offerings, :needs).to_sql).to include "#{offer_alias}.#{column}"
149
+ expect(TaggableModel.joins(:offerings, :needs).to_sql).to include "#{need_alias}.#{column}"
150
+ end
151
+
136
152
  end
137
153
 
138
154
  describe 'Tagging Contexts' do
@@ -69,6 +69,12 @@ describe 'Acts As Taggable On' do
69
69
  @taggable.save!
70
70
  expect(@taggable.tag_list.sort).to eq(%w(awesome epic).sort)
71
71
  end
72
+
73
+ it 'should clear the cache on reset_column_information' do
74
+ CachedModel.column_names
75
+ CachedModel.reset_column_information
76
+ expect(CachedModel.instance_variable_get(:@acts_as_taggable_on_cache_columns)).to eql(nil)
77
+ end
72
78
  end
73
79
 
74
80
  describe 'CachingWithArray' do
@@ -0,0 +1,47 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe ActsAsTaggableOn::DefaultParser do
5
+ it '#parse should return empty array if empty array is passed' do
6
+ parser = ActsAsTaggableOn::DefaultParser.new([])
7
+ expect(parser.parse).to be_empty
8
+ end
9
+
10
+ describe 'Multiple Delimiter' do
11
+ before do
12
+ @old_delimiter = ActsAsTaggableOn.delimiter
13
+ end
14
+
15
+ after do
16
+ ActsAsTaggableOn.delimiter = @old_delimiter
17
+ end
18
+
19
+ it 'should separate tags by delimiters' do
20
+ ActsAsTaggableOn.delimiter = [',', ' ', '\|']
21
+ parser = ActsAsTaggableOn::DefaultParser.new('cool, data|I have')
22
+ expect(parser.parse.to_s).to eq('cool, data, I, have')
23
+ end
24
+
25
+ it 'should escape quote' do
26
+ ActsAsTaggableOn.delimiter = [',', ' ', '\|']
27
+ parser = ActsAsTaggableOn::DefaultParser.new("'I have'|cool, data")
28
+ expect(parser.parse.to_s).to eq('"I have", cool, data')
29
+
30
+ parser = ActsAsTaggableOn::DefaultParser.new('"I, have"|cool, data')
31
+ expect(parser.parse.to_s).to eq('"I, have", cool, data')
32
+ end
33
+
34
+ it 'should work for utf8 delimiter and long delimiter' do
35
+ ActsAsTaggableOn.delimiter = [',', '的', '可能是']
36
+ parser = ActsAsTaggableOn::DefaultParser.new('我的东西可能是不见了,还好有备份')
37
+ expect(parser.parse.to_s).to eq('我, 东西, 不见了, 还好有备份')
38
+ end
39
+
40
+ it 'should work for multiple quoted tags' do
41
+ ActsAsTaggableOn.delimiter = [',']
42
+ parser = ActsAsTaggableOn::DefaultParser.new('"Ruby Monsters","eat Katzenzungen"')
43
+ expect(parser.parse.to_s).to eq('Ruby Monsters, eat Katzenzungen')
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe ActsAsTaggableOn::GenericParser do
5
+ it '#parse should return empty array if empty tag string is passed' do
6
+ tag_list = ActsAsTaggableOn::GenericParser.new('')
7
+ expect(tag_list.parse).to be_empty
8
+ end
9
+
10
+ it '#parse should separate tags by comma' do
11
+ tag_list = ActsAsTaggableOn::GenericParser.new('cool,data,,I,have')
12
+ expect(tag_list.parse).to eq(%w(cool data I have))
13
+ end
14
+ end
@@ -42,6 +42,15 @@ describe 'Acts As Taggable On' do
42
42
  expect(taggable1.find_related_tags_for(OtherTaggableModel)).to_not include(taggable2)
43
43
  end
44
44
 
45
+ it 'should find other related objects based on tags only from particular context' do
46
+ taggable1 = TaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
47
+ taggable2 = TaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four', skill_list: 'one, two')
48
+ taggable3 = TaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
49
+
50
+ expect(taggable1.find_related_tags).to include(taggable3)
51
+ expect(taggable1.find_related_tags).to_not include(taggable2)
52
+ end
53
+
45
54
 
46
55
  shared_examples "a collection" do
47
56
  it do
@@ -114,7 +114,36 @@ describe ActsAsTaggableOn::TagList do
114
114
 
115
115
  ActsAsTaggableOn.force_lowercase = false
116
116
  end
117
+ end
118
+
119
+ describe 'custom parser' do
120
+ let(:parser) { double(parse: %w(cool wicked)) }
121
+ let(:parser_class) { stub_const('MyParser', Class) }
122
+
123
+ it 'should use a the default parser if none is set as parameter' do
124
+ allow(ActsAsTaggableOn.default_parser).to receive(:new).and_return(parser)
125
+ ActsAsTaggableOn::TagList.new('cool, wicked', parse: true)
126
+
127
+ expect(parser).to have_received(:parse)
128
+ end
129
+
130
+ it 'should use the custom parser passed as parameter' do
131
+ allow(parser_class).to receive(:new).and_return(parser)
117
132
 
133
+ ActsAsTaggableOn::TagList.new('cool, wicked', parser: parser_class)
134
+
135
+ expect(parser).to have_received(:parse)
136
+ end
137
+
138
+ it 'should use the parser setted as attribute' do
139
+ allow(parser_class).to receive(:new).with('new, tag').and_return(parser)
140
+
141
+ tag_list = ActsAsTaggableOn::TagList.new('example')
142
+ tag_list.parser = parser_class
143
+ tag_list.add('new, tag', parse: true)
144
+
145
+ expect(parser).to have_received(:parse)
146
+ end
118
147
  end
119
148
 
120
149
 
@@ -46,6 +46,14 @@ describe ActsAsTaggableOn::Tag do
46
46
  end
47
47
  end
48
48
 
49
+ describe 'named any' do
50
+ context 'with some special characters combinations', if: using_mysql? do
51
+ it 'should not raise an invalid encoding exception' do
52
+ expect{ActsAsTaggableOn::Tag.named_any(["holä", "hol'ä"])}.not_to raise_error
53
+ end
54
+ end
55
+ end
56
+
49
57
  describe 'find or create by name' do
50
58
  before(:each) do
51
59
  @tag.name = 'awesome'
@@ -250,13 +258,19 @@ describe ActsAsTaggableOn::Tag do
250
258
  end
251
259
  end
252
260
 
253
- it 'should not change enconding' do
261
+ it 'should not change encoding' do
254
262
  name = "\u3042"
255
263
  original_encoding = name.encoding
256
264
  record = ActsAsTaggableOn::Tag.find_or_create_with_like_by_name(name)
257
265
  record.reload
258
266
  expect(record.name.encoding).to eq(original_encoding)
259
267
  end
268
+
269
+ context 'named any with some special characters combinations', if: using_mysql? do
270
+ it 'should not raise an invalid encoding exception' do
271
+ expect{ActsAsTaggableOn::Tag.named_any(["holä", "hol'ä"])}.not_to raise_error
272
+ end
273
+ end
260
274
  end
261
275
 
262
276
  describe 'name uniqeness validation' do
@@ -286,4 +300,24 @@ describe ActsAsTaggableOn::Tag do
286
300
  end
287
301
  end
288
302
  end
303
+
304
+ describe 'popular tags' do
305
+ before do
306
+ %w(sports rails linux tennis golden_syrup).each_with_index do |t, i|
307
+ tag = ActsAsTaggableOn::Tag.new(name: t)
308
+ tag.taggings_count = i
309
+ tag.save!
310
+ end
311
+ end
312
+
313
+ it 'should find the most popular tags' do
314
+ expect(ActsAsTaggableOn::Tag.most_used(3).first.name).to eq("golden_syrup")
315
+ expect(ActsAsTaggableOn::Tag.most_used(3).length).to eq(3)
316
+ end
317
+
318
+ it 'should find the least popular tags' do
319
+ expect(ActsAsTaggableOn::Tag.least_used(3).first.name).to eq("sports")
320
+ expect(ActsAsTaggableOn::Tag.least_used(3).length).to eq(3)
321
+ end
322
+ end
289
323
  end
@@ -261,7 +261,7 @@ describe 'Taggable' do
261
261
  @taggable.tag_list = 'bob, charlie'
262
262
  @taggable.save
263
263
 
264
- expect(TaggableModel.group(:created_at).tagged_with(['bob', 'css'], :any => true).first).to eq(@taggable)
264
+ expect(TaggableModel.tagged_with(['bob', 'css'], :any => true).to_a).to eq([@taggable])
265
265
 
266
266
  bob = TaggableModel.create(:name => 'Bob', :tag_list => 'ruby, rails, css')
267
267
  frank = TaggableModel.create(:name => 'Frank', :tag_list => 'ruby, rails')
@@ -459,6 +459,7 @@ describe 'Taggable' do
459
459
 
460
460
  expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, any: true).to_a.sort_by { |o| o.id }).to eq([bob, frank, steve])
461
461
  expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, exclude: true).to_a).to eq([jim])
462
+ expect(TaggableModel.tagged_with('ji', wild: true, any: true).to_a).to eq([frank, jim])
462
463
  end
463
464
  end
464
465
 
@@ -137,4 +137,17 @@ describe 'Tagger' do
137
137
  }).to_not change(ActsAsTaggableOn::Tagging, :count)
138
138
  end
139
139
 
140
+ it 'should change tags order in ordered taggable' do
141
+ @ordered_taggable = OrderedTaggableModel.create name: 'hey!'
142
+
143
+ @user.tag @ordered_taggable, with: 'tag, tag1', on: :tags
144
+ expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag', 'tag1'])
145
+
146
+ @user.tag @ordered_taggable, with: 'tag2, tag1', on: :tags
147
+ expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag2', 'tag1'])
148
+
149
+ @user.tag @ordered_taggable, with: 'tag1, tag2', on: :tags
150
+ expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag1', 'tag2'])
151
+ end
152
+
140
153
  end
@@ -13,4 +13,11 @@ describe ActsAsTaggableOn::Utils do
13
13
  expect(ActsAsTaggableOn::Utils.like_operator).to eq('LIKE')
14
14
  end
15
15
  end
16
+
17
+ describe '#sha_prefix' do
18
+ it 'should return a consistent prefix for a given word' do
19
+ expect(ActsAsTaggableOn::Utils.sha_prefix('kittens')).to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
20
+ expect(ActsAsTaggableOn::Utils.sha_prefix('puppies')).not_to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
21
+ end
22
+ end
16
23
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,7 @@
1
+ begin
2
+ require 'byebug'
3
+ rescue LoadError
4
+ end
1
5
  $LOAD_PATH << '.' unless $LOAD_PATH.include?('.')
2
6
  $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
7
  require 'logger'