acts-as-taggable-on 3.2.1 → 3.4.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +21 -1
  3. data/Appraisals +8 -6
  4. data/CHANGELOG.md +99 -10
  5. data/README.md +78 -11
  6. data/acts-as-taggable-on.gemspec +2 -4
  7. data/db/migrate/4_add_missing_taggable_index.rb +9 -0
  8. data/gemfiles/activerecord_3.2.gemfile +0 -1
  9. data/gemfiles/activerecord_4.0.gemfile +1 -2
  10. data/gemfiles/activerecord_4.1.gemfile +1 -2
  11. data/gemfiles/activerecord_4.2.gemfile +16 -0
  12. data/gemfiles/activerecord_edge.gemfile +0 -1
  13. data/lib/acts-as-taggable-on.rb +40 -21
  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 +6 -1
  17. data/lib/acts_as_taggable_on/tag_list.rb +20 -79
  18. data/lib/acts_as_taggable_on/tag_list_parser.rb +21 -0
  19. data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/cache.rb +1 -0
  20. data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/collection.rb +3 -1
  21. data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/core.rb +87 -51
  22. data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/ownership.rb +1 -1
  23. data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/related.rb +6 -6
  24. data/lib/acts_as_taggable_on/taggable.rb +7 -8
  25. data/lib/acts_as_taggable_on/tagging.rb +7 -1
  26. data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
  27. data/lib/acts_as_taggable_on/utils.rb +26 -50
  28. data/lib/acts_as_taggable_on/version.rb +1 -1
  29. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +1 -1
  30. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -0
  31. data/spec/acts_as_taggable_on/caching_spec.rb +1 -0
  32. data/spec/acts_as_taggable_on/default_parser_spec.rb +47 -0
  33. data/spec/acts_as_taggable_on/generic_parser_spec.rb +14 -0
  34. data/spec/acts_as_taggable_on/related_spec.rb +1 -0
  35. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +1 -0
  36. data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +46 -0
  37. data/spec/acts_as_taggable_on/tag_list_spec.rb +23 -31
  38. data/spec/acts_as_taggable_on/tag_spec.rb +54 -38
  39. data/spec/acts_as_taggable_on/taggable/dirty_spec.rb +127 -0
  40. data/spec/acts_as_taggable_on/taggable_spec.rb +84 -164
  41. data/spec/acts_as_taggable_on/tagger_spec.rb +1 -1
  42. data/spec/acts_as_taggable_on/tagging_spec.rb +37 -1
  43. data/spec/acts_as_taggable_on/tags_helper_spec.rb +1 -0
  44. data/spec/acts_as_taggable_on/utils_spec.rb +6 -11
  45. data/spec/internal/app/models/cached_model_with_array.rb +1 -1
  46. data/spec/internal/app/models/models.rb +2 -2
  47. data/spec/internal/db/schema.rb +2 -2
  48. data/spec/spec_helper.rb +0 -1
  49. data/spec/support/0-helpers.rb +32 -0
  50. data/spec/support/database_cleaner.rb +4 -0
  51. metadata +29 -52
  52. data/spec/bm.rb +0 -52
  53. data/spec/schema.rb +0 -82
  54. /data/lib/acts_as_taggable_on/{acts_as_taggable_on/compatibility.rb → compatibility.rb} +0 -0
  55. /data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/dirty.rb +0 -0
@@ -1,89 +1,16 @@
1
+
1
2
  require 'active_support/core_ext/module/delegation'
2
3
 
3
4
  module ActsAsTaggableOn
4
5
  class TagList < Array
5
6
  attr_accessor :owner
7
+ attr_accessor :parser
6
8
 
7
9
  def initialize(*args)
10
+ @parser = ActsAsTaggableOn.default_parser
8
11
  add(*args)
9
12
  end
10
13
 
11
- class << self
12
- ##
13
- # Returns a new TagList using the given tag string.
14
- #
15
- # Example:
16
- # tag_list = ActsAsTaggableOn::TagList.from("One , Two, Three")
17
- # tag_list # ["One", "Two", "Three"]
18
- def from(string)
19
- string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
20
-
21
- new.tap do |tag_list|
22
- string = string.to_s.dup
23
-
24
-
25
- string.gsub!(double_quote_pattern) {
26
- # Append the matched tag to the tag list
27
- tag_list << Regexp.last_match[2]
28
- # Return the matched delimiter ($3) to replace the matched items
29
- ''
30
- }
31
-
32
- string.gsub!(single_quote_pattern) {
33
- # Append the matched tag ($2) to the tag list
34
- tag_list << Regexp.last_match[2]
35
- # Return an empty string to replace the matched items
36
- ''
37
- }
38
-
39
- # split the string by the delimiter
40
- # and add to the tag_list
41
- tag_list.add(string.split(Regexp.new delimiter))
42
- end
43
- end
44
-
45
- def delimiter
46
- # Parse the quoted tags
47
- d = ActsAsTaggableOn.delimiter
48
- # Separate multiple delimiters by bitwise operator
49
- d = d.join('|') if d.kind_of?(Array)
50
-
51
- d
52
- end
53
-
54
- def single_quote_pattern
55
- %r{
56
- ( # Tag start delimiter ($1)
57
- \A | # Either string start or
58
- #{delimiter} # a delimiter
59
- )
60
- \s*' # quote (') optionally preceded by whitespace
61
- (.*?) # Tag ($2)
62
- '\s* # quote (') optionally followed by whitespace
63
- (?= # Tag end delimiter (not consumed; is zero-length lookahead)
64
- #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
65
- \z # string end
66
- )
67
- }x
68
- end
69
-
70
- def double_quote_pattern
71
- %r{
72
- ( # Tag start delimiter ($1)
73
- \A | # Either string start or
74
- #{delimiter} # a delimiter
75
- )
76
- \s*" # quote (") optionally preceded by whitespace
77
- (.*?) # Tag ($2)
78
- "\s* # quote (") optionally followed by whitespace
79
- (?= # Tag end delimiter (not consumed; is zero-length lookahead)
80
- #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
81
- \z # string end
82
- )
83
- }x
84
- end
85
-
86
- end
87
14
  ##
88
15
  # Add tags to the tag_list. Duplicate or blank tags will be ignored.
89
16
  # Use the <tt>:parse</tt> option to add an unparsed tag string.
@@ -149,9 +76,10 @@ module ActsAsTaggableOn
149
76
 
150
77
  private
151
78
 
152
- # Remove whitespace, duplicates, and blanks.
79
+ # Convert everything to string, remove whitespace, duplicates, and blanks.
153
80
  def clean!
154
81
  reject!(&:blank?)
82
+ map!(&:to_s)
155
83
  map!(&:strip)
156
84
  map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
157
85
  map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
@@ -162,14 +90,27 @@ module ActsAsTaggableOn
162
90
 
163
91
  def extract_and_apply_options!(args)
164
92
  options = args.last.is_a?(Hash) ? args.pop : {}
165
- options.assert_valid_keys :parse
93
+ options.assert_valid_keys :parse, :parser
166
94
 
167
- args.map! { |a| self.class.from(a) } if options[:parse]
95
+ parser = options[:parser] ? options[:parser] : @parser
96
+
97
+ args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
168
98
 
169
99
  args.flatten!
170
100
  end
171
101
 
172
102
 
103
+ ## DEPRECATED
104
+ def self.from(string)
105
+ ActiveRecord::Base.logger.warn <<WARNING
106
+ ActsAsTaggableOn::TagList.from is deprecated \
107
+ and will be removed from v4.0+, use \
108
+ ActsAsTaggableOn::DefaultParser.new instead
109
+ WARNING
110
+ @parser.new(string).parse
111
+ end
112
+
113
+
173
114
  end
174
115
  end
175
116
 
@@ -0,0 +1,21 @@
1
+ module ActsAsTaggableOn
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = ActsAsTaggableOn::TagListParser.parse("One , Two, Three")
7
+ # tag_list # ["One", "Two", "Three"]
8
+ module TagListParser
9
+ class << self
10
+ ## DEPRECATED
11
+ def parse(string)
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
18
+ end
19
+ end
20
+ end
21
+ end
@@ -31,6 +31,7 @@ module ActsAsTaggableOn::Taggable
31
31
  # to mimic the underlying behavior. While processing this first
32
32
  # call to columns, we do the caching column check and dynamically add
33
33
  # the class and instance methods
34
+ # FIXME: this method cannot compile in rubinius
34
35
  def columns
35
36
  @acts_as_taggable_on_cache_columns ||= begin
36
37
  db_columns = super
@@ -53,6 +53,7 @@ module ActsAsTaggableOn::Taggable
53
53
  # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
54
54
  # * :on - Scope the find to only include a certain context
55
55
  def all_tags(options = {})
56
+ options = options.dup
56
57
  options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
57
58
 
58
59
  ## Generate conditions:
@@ -87,6 +88,7 @@ module ActsAsTaggableOn::Taggable
87
88
  # * :at_most - Exclude tags with a frequency greater than the given value
88
89
  # * :on - Scope the find to only include a certain context
89
90
  def all_tag_counts(options = {})
91
+ options = options.dup
90
92
  options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
91
93
 
92
94
  ## Generate conditions:
@@ -133,7 +135,7 @@ module ActsAsTaggableOn::Taggable
133
135
  table_name_pkey = "#{table_name}.#{primary_key}"
134
136
  if ActsAsTaggableOn::Utils.using_mysql?
135
137
  # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
136
- scoped_ids = select(table_name_pkey).map(&:id)
138
+ scoped_ids = pluck(table_name_pkey)
137
139
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
138
140
  else
139
141
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
@@ -24,16 +24,16 @@ module ActsAsTaggableOn::Taggable
24
24
  # when preserving tag order, include order option so that for a 'tags' context
25
25
  # the associations tag_taggings & tags are always returned in created order
26
26
  has_many_with_taggable_compatibility context_taggings, as: :taggable,
27
- dependent: :destroy,
28
- class_name: 'ActsAsTaggableOn::Tagging',
29
- order: taggings_order,
30
- conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
31
- include: :tag
27
+ dependent: :destroy,
28
+ class_name: 'ActsAsTaggableOn::Tagging',
29
+ order: taggings_order,
30
+ conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
31
+ include: :tag
32
32
 
33
33
  has_many_with_taggable_compatibility context_tags, through: context_taggings,
34
- source: :tag,
35
- class_name: 'ActsAsTaggableOn::Tag',
36
- order: taggings_order
34
+ source: :tag,
35
+ class_name: 'ActsAsTaggableOn::Tag',
36
+ order: taggings_order
37
37
 
38
38
  end
39
39
 
@@ -73,16 +73,20 @@ module ActsAsTaggableOn::Taggable
73
73
  # * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
74
74
  # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
75
75
  # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
76
+ # * <tt>:start_at</tt> - Restrict the tags to those created after a certain time
77
+ # * <tt>:end_at</tt> - Restrict the tags to those created before a certain time
76
78
  #
77
79
  # Example:
78
- # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
79
- # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
80
- # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
81
- # User.tagged_with("awesome", "cool", :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
82
- # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
83
- # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
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
84
87
  def tagged_with(tags, options = {})
85
- tag_list = ActsAsTaggableOn::TagList.from(tags)
88
+ tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
89
+ options = options.dup
86
90
  empty_result = where('1 = 0')
87
91
 
88
92
  return empty_result if tag_list.empty?
@@ -108,14 +112,17 @@ module ActsAsTaggableOn::Taggable
108
112
  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, nil)})"
109
113
 
110
114
  if owned_by
111
- joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" \
112
- " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
113
- " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" \
114
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" \
115
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
115
+ joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
116
+ " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
117
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
118
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
119
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
120
+
121
+ joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
122
+ joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
116
123
  end
117
124
 
118
- elsif options.delete(:any)
125
+ elsif any = options.delete(:any)
119
126
  # get tags, drop out if nothing returned (we need at least one)
120
127
  tags = if options.delete(:wild)
121
128
  ActsAsTaggableOn::Tag.named_like_any(tag_list)
@@ -130,49 +137,59 @@ module ActsAsTaggableOn::Taggable
130
137
  taggings_context = context ? "_#{context}" : ''
131
138
 
132
139
  taggings_alias = adjust_taggings_alias(
133
- "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
140
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
134
141
  )
135
142
 
136
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
137
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
138
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
139
- tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
143
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
144
+ " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
145
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
146
+
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]
149
+
150
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
140
151
 
141
152
  # don't need to sanitize sql, map all ids and join with OR logic
142
153
  conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(' OR ')
143
- select_clause = " #{table_name}.*" unless context and tag_types.one?
154
+ select_clause << " #{table_name}.*" unless context and tag_types.one?
144
155
 
145
156
  if owned_by
146
157
  tagging_join << ' AND ' +
147
158
  sanitize_sql([
148
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
149
- owned_by.id,
150
- owned_by.class.base_class.to_s
159
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
160
+ owned_by.id,
161
+ owned_by.class.base_class.to_s
151
162
  ])
152
163
  end
153
164
 
154
165
  joins << tagging_join
155
- group = "#{table_name}.#{primary_key}"
166
+ unless any == 'distinct' # Fix issue #544
167
+ group = "#{table_name}.#{primary_key}"
168
+ select_clause << group
169
+ end
156
170
  else
157
171
  tags = ActsAsTaggableOn::Tag.named_any(tag_list)
158
172
 
159
173
  return empty_result unless tags.length == tag_list.length
160
174
 
161
175
  tags.each do |tag|
162
- taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
176
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
163
177
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
164
178
  " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
165
179
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
166
180
  " AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
167
181
 
168
- tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
182
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
183
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
184
+
185
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
169
186
 
170
187
  if owned_by
171
188
  tagging_join << ' AND ' +
172
189
  sanitize_sql([
173
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
174
- owned_by.id,
175
- owned_by.class.base_class.to_s
190
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
191
+ owned_by.id,
192
+ owned_by.class.base_class.to_s
176
193
  ])
177
194
  end
178
195
 
@@ -182,7 +199,7 @@ module ActsAsTaggableOn::Taggable
182
199
 
183
200
  group ||= [] # Rails interprets this as a no-op in the group() call below
184
201
  if options.delete(:order_by_matching_tag_count)
185
- select_clause = "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
202
+ select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
186
203
  group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
187
204
  group = group_columns
188
205
  order_by << "#{taggings_alias}_count DESC"
@@ -193,7 +210,9 @@ module ActsAsTaggableOn::Taggable
193
210
  " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
194
211
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
195
212
 
196
- joins << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
213
+ joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
214
+ joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
215
+ joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
197
216
 
198
217
  group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
199
218
  group = group_columns
@@ -202,13 +221,14 @@ module ActsAsTaggableOn::Taggable
202
221
 
203
222
  order_by << options[:order] if options[:order].present?
204
223
 
205
- request = select(select_clause).
206
- joins(joins.join(' ')).
207
- where(conditions.join(' AND ')).
208
- group(group).
209
- having(having).
210
- order(order_by.join(', ')).
211
- readonly(false)
224
+ query = self
225
+ query = self.select(select_clause.join(',')) unless select_clause.empty?
226
+ query.joins(joins.join(' '))
227
+ .where(conditions.join(' AND '))
228
+ .group(group)
229
+ .having(having)
230
+ .order(order_by.join(', '))
231
+ .readonly(false)
212
232
  end
213
233
 
214
234
  def is_taggable?
@@ -258,7 +278,7 @@ module ActsAsTaggableOn::Taggable
258
278
  if instance_variable_get(variable_name)
259
279
  instance_variable_get(variable_name)
260
280
  elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
261
- instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(cached_tag_list_on(context)))
281
+ instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
262
282
  else
263
283
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
264
284
  end
@@ -308,7 +328,7 @@ module ActsAsTaggableOn::Taggable
308
328
  variable_name = "@#{context.to_s.singularize}_list"
309
329
  process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
310
330
 
311
- instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
331
+ instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
312
332
  end
313
333
 
314
334
  def tagging_contexts
@@ -322,10 +342,14 @@ module ActsAsTaggableOn::Taggable
322
342
  if changed_attributes.include?(attrib)
323
343
  # The attribute already has an unsaved change.
324
344
  old = changed_attributes[attrib]
325
- changed_attributes.delete(attrib) if old.to_s == value.to_s
345
+ @changed_attributes.delete(attrib) if old.to_s == value.to_s
326
346
  else
327
- old = tag_list_on(context).to_s
328
- changed_attributes[attrib] = old if old.to_s != value.to_s
347
+ old = tag_list_on(context)
348
+ if self.class.preserve_tag_order
349
+ @changed_attributes[attrib] = old if old.to_s != value.to_s
350
+ else
351
+ @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
352
+ end
329
353
  end
330
354
  end
331
355
 
@@ -382,7 +406,7 @@ module ActsAsTaggableOn::Taggable
382
406
 
383
407
  # Destroy old taggings:
384
408
  if old_tags.present?
385
- ActsAsTaggableOn::Tagging.destroy_all(tagger_type: nil, tagger_id: nil, context: context.to_s, tag_id: old_tags)
409
+ taggings.not_owned.by_context(context).destroy_all(tag_id: old_tags)
386
410
  end
387
411
 
388
412
  # Create new taggings:
@@ -396,6 +420,18 @@ module ActsAsTaggableOn::Taggable
396
420
 
397
421
  private
398
422
 
423
+ # Filters the tag lists from the attribute names.
424
+ def attributes_for_update(attribute_names)
425
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
426
+ super.delete_if {|attr| tag_lists.include? attr }
427
+ end
428
+
429
+ # Filters the tag lists from the attribute names.
430
+ def attributes_for_create(attribute_names)
431
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
432
+ super.delete_if {|attr| tag_lists.include? attr }
433
+ end
434
+
399
435
  ##
400
436
  # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
401
437
  # 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::TagList.from(new_list)
66
+ cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse
67
67
  end
68
68
 
69
69
  def reload(*args)
@@ -41,7 +41,7 @@ module ActsAsTaggableOn::Taggable
41
41
  end
42
42
 
43
43
  def related_tags_for(context, klass, options = {})
44
- tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
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
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])
47
47
  end
@@ -61,11 +61,11 @@ module ActsAsTaggableOn::Taggable
61
61
  end
62
62
 
63
63
  def related_where(klass, conditions)
64
- klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count").
65
- from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}").
66
- group(group_columns(klass)).
67
- order('count DESC').
68
- where(conditions)
64
+ klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count")
65
+ .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}")
66
+ .group(group_columns(klass))
67
+ .order('count DESC')
68
+ .where(conditions)
69
69
  end
70
70
  end
71
71
  end
@@ -1,5 +1,6 @@
1
1
  module ActsAsTaggableOn
2
2
  module Taggable
3
+
3
4
  def taggable?
4
5
  false
5
6
  end
@@ -85,19 +86,17 @@ module ActsAsTaggableOn
85
86
  def self.taggable?
86
87
  true
87
88
  end
88
-
89
- extend ActsAsTaggableOn::Utils
90
89
  end
91
90
  end
92
91
 
93
92
  # each of these add context-specific methods and must be
94
93
  # called on each call of taggable_on
95
- include ActsAsTaggableOn::Taggable::Core
96
- include ActsAsTaggableOn::Taggable::Collection
97
- include ActsAsTaggableOn::Taggable::Cache
98
- include ActsAsTaggableOn::Taggable::Ownership
99
- include ActsAsTaggableOn::Taggable::Related
100
- include ActsAsTaggableOn::Taggable::Dirty
94
+ include Core
95
+ include Collection
96
+ include Cache
97
+ include Ownership
98
+ include Related
99
+ include Dirty
101
100
  end
102
101
  end
103
102
  end
@@ -15,6 +15,12 @@ module ActsAsTaggableOn
15
15
  belongs_to :taggable, polymorphic: true
16
16
  belongs_to :tagger, polymorphic: true
17
17
 
18
+ scope :owned_by, ->(owner) { where(tagger: owner) }
19
+ scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
20
+
21
+ scope :by_contexts, ->(contexts = ['tags']) { where(context: contexts) }
22
+ scope :by_context, ->(context= 'tags') { by_contexts(context.to_s) }
23
+
18
24
  validates_presence_of :context
19
25
  validates_presence_of :tag_id
20
26
 
@@ -26,7 +32,7 @@ module ActsAsTaggableOn
26
32
 
27
33
  def remove_unused_tags
28
34
  if ActsAsTaggableOn.remove_unused_tags
29
- tag.destroy if tag.taggings_count.zero?
35
+ tag.destroy if tag.reload.taggings_count.zero?
30
36
  end
31
37
  end
32
38
  end
@@ -4,10 +4,10 @@ module ActsAsTaggableOn
4
4
  def tag_cloud(tags, classes)
5
5
  return [] if tags.empty?
6
6
 
7
- max_count = tags.sort_by(&:count).last.count.to_f
7
+ max_count = tags.sort_by(&:taggings_count).last.taggings_count.to_f
8
8
 
9
9
  tags.each do |tag|
10
- index = ((tag.count / max_count) * (classes.size - 1))
10
+ index = ((tag.taggings_count / max_count) * (classes.size - 1))
11
11
  yield tag, classes[index.nan? ? 0 : index.round]
12
12
  end
13
13
  end
@@ -1,62 +1,38 @@
1
+ # This module is deprecated and will be removed in the incoming versions
2
+
1
3
  module ActsAsTaggableOn
2
4
  module Utils
3
- extend self
4
-
5
- # Use ActsAsTaggableOn::Tag connection
6
- def connection
7
- ActsAsTaggableOn::Tag.connection
8
- end
9
-
10
- def using_postgresql?
11
- connection && connection.adapter_name == 'PostgreSQL'
12
- end
13
-
14
- def postgresql_version
15
- if using_postgresql?
16
- connection.execute("SHOW SERVER_VERSION").first["server_version"].to_f
5
+ class << self
6
+ # Use ActsAsTaggableOn::Tag connection
7
+ def connection
8
+ ActsAsTaggableOn::Tag.connection
17
9
  end
18
- end
19
-
20
- def postgresql_support_json?
21
- postgresql_version >= 9.2
22
- end
23
-
24
- def using_sqlite?
25
- connection && connection.adapter_name == 'SQLite'
26
- end
27
-
28
- def using_mysql?
29
- #We should probably use regex for mysql to support prehistoric adapters
30
- connection && connection.adapter_name == 'Mysql2'
31
- end
32
10
 
33
- def using_case_insensitive_collation?
34
- using_mysql? && connection.collation =~ /_ci\Z/
35
- end
36
-
37
- def supports_concurrency?
38
- !using_sqlite?
39
- end
11
+ def using_postgresql?
12
+ connection && connection.adapter_name == 'PostgreSQL'
13
+ end
40
14
 
41
- def sha_prefix(string)
42
- Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
43
- end
15
+ def using_mysql?
16
+ #We should probably use regex for mysql to support prehistoric adapters
17
+ connection && connection.adapter_name == 'Mysql2'
18
+ end
44
19
 
45
- def active_record4?
46
- ::ActiveRecord::VERSION::MAJOR == 4
47
- end
20
+ def sha_prefix(string)
21
+ Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
22
+ end
48
23
 
49
- def active_record42?
50
- active_record4? && ::ActiveRecord::VERSION::MINOR >= 2
51
- end
24
+ def active_record4?
25
+ ::ActiveRecord::VERSION::MAJOR == 4
26
+ end
52
27
 
53
- def like_operator
54
- using_postgresql? ? 'ILIKE' : 'LIKE'
55
- end
28
+ def like_operator
29
+ using_postgresql? ? 'ILIKE' : 'LIKE'
30
+ end
56
31
 
57
- # escape _ and % characters in strings, since these are wildcards in SQL.
58
- def escape_like(str)
59
- str.gsub(/[!%_]/) { |x| '!' + x }
32
+ # escape _ and % characters in strings, since these are wildcards in SQL.
33
+ def escape_like(str)
34
+ str.gsub(/[!%_]/) { |x| '!' + x }
35
+ end
60
36
  end
61
37
  end
62
38
  end
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '3.2.1'
2
+ VERSION = '3.4.0'
3
3
  end
4
4
 
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+ # -*- encoding : utf-8 -*-
2
2
  require 'spec_helper'
3
3
 
4
4
  describe 'Acts As Taggable On' do