acts-as-taggable-on 2.0.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +23 -0
- data/Appraisals +20 -0
- data/CHANGELOG.md +112 -0
- data/CONTRIBUTING.md +44 -0
- data/Gemfile +9 -5
- data/Guardfile +5 -0
- data/{MIT-LICENSE → LICENSE.md} +1 -1
- data/README.md +385 -0
- data/Rakefile +13 -53
- data/UPGRADING.md +13 -0
- data/acts-as-taggable-on.gemspec +38 -0
- data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
- data/db/migrate/2_add_missing_unique_indices.rb +19 -0
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
- data/gemfiles/activerecord_3.2.gemfile +15 -0
- data/gemfiles/activerecord_4.0.gemfile +15 -0
- data/gemfiles/activerecord_4.1.gemfile +15 -0
- data/gemfiles/activerecord_edge.gemfile +16 -0
- data/lib/acts-as-taggable-on.rb +65 -22
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -24
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +120 -42
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +35 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +309 -124
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +36 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +96 -72
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +43 -36
- data/lib/acts_as_taggable_on/engine.rb +5 -0
- data/lib/acts_as_taggable_on/tag.rb +133 -63
- data/lib/acts_as_taggable_on/tag_list.rb +162 -81
- data/lib/acts_as_taggable_on/taggable.rb +101 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +34 -13
- data/lib/acts_as_taggable_on/tagging.rb +28 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
- data/lib/acts_as_taggable_on/utils.rb +62 -0
- data/lib/acts_as_taggable_on/version.rb +4 -0
- data/lib/acts_as_taggable_on.rb +6 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +189 -195
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +78 -81
- data/spec/acts_as_taggable_on/caching_spec.rb +76 -0
- data/spec/acts_as_taggable_on/related_spec.rb +89 -0
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +210 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +149 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +245 -65
- data/spec/acts_as_taggable_on/taggable_spec.rb +776 -187
- data/spec/acts_as_taggable_on/tagger_spec.rb +119 -70
- data/spec/acts_as_taggable_on/tagging_spec.rb +17 -21
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +38 -22
- data/spec/acts_as_taggable_on/utils_spec.rb +15 -0
- data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
- data/spec/internal/app/models/cached_model.rb +3 -0
- data/spec/internal/app/models/cached_model_with_array.rb +5 -0
- data/spec/internal/app/models/company.rb +15 -0
- data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/internal/app/models/market.rb +2 -0
- data/spec/internal/app/models/models.rb +90 -0
- data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
- data/spec/internal/app/models/other_cached_model.rb +3 -0
- data/spec/internal/app/models/other_taggable_model.rb +4 -0
- data/spec/internal/app/models/student.rb +2 -0
- data/spec/internal/app/models/taggable_model.rb +13 -0
- data/spec/internal/app/models/untaggable_model.rb +3 -0
- data/spec/internal/app/models/user.rb +3 -0
- data/spec/internal/config/database.yml.sample +19 -0
- data/spec/internal/db/schema.rb +97 -0
- data/spec/schema.rb +57 -18
- data/spec/spec_helper.rb +12 -47
- data/spec/support/array.rb +9 -0
- data/spec/support/database.rb +42 -0
- data/spec/support/database_cleaner.rb +21 -0
- metadata +286 -63
- data/CHANGELOG +0 -25
- data/README.rdoc +0 -221
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
- data/rails/init.rb +0 -1
- data/spec/bm.rb +0 -52
- data/spec/models.rb +0 -30
- /data/{spec/spec.opts → .rspec} +0 -0
|
@@ -1,101 +1,125 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
2
|
module Ownership
|
|
3
3
|
def self.included(base)
|
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
|
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
|
|
6
|
-
|
|
5
|
+
|
|
7
6
|
base.class_eval do
|
|
8
|
-
after_save :save_owned_tags
|
|
7
|
+
after_save :save_owned_tags
|
|
9
8
|
end
|
|
10
|
-
|
|
9
|
+
|
|
11
10
|
base.initialize_acts_as_taggable_on_ownership
|
|
12
11
|
end
|
|
13
|
-
|
|
12
|
+
|
|
14
13
|
module ClassMethods
|
|
15
14
|
def acts_as_taggable_on(*args)
|
|
16
15
|
initialize_acts_as_taggable_on_ownership
|
|
17
16
|
super(*args)
|
|
18
17
|
end
|
|
19
|
-
|
|
20
|
-
def initialize_acts_as_taggable_on_ownership
|
|
18
|
+
|
|
19
|
+
def initialize_acts_as_taggable_on_ownership
|
|
21
20
|
tag_types.map(&:to_s).each do |tag_type|
|
|
22
|
-
class_eval
|
|
21
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
23
22
|
def #{tag_type}_from(owner)
|
|
24
23
|
owner_tag_list_on(owner, '#{tag_type}')
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
end
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
end
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
|
-
|
|
31
|
-
module InstanceMethods
|
|
32
|
-
def owner_tags_on(owner, context)
|
|
33
|
-
base_tags.where([%(#{Tagging.table_name}.context = ? AND
|
|
34
|
-
#{Tagging.table_name}.tagger_id = ? AND
|
|
35
|
-
#{Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
|
|
36
|
-
end
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
def owner_tags_on(owner, context)
|
|
31
|
+
if owner.nil?
|
|
32
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
|
33
|
+
else
|
|
34
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
|
35
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
|
36
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
|
|
41
37
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def set_owner_tag_list_on(owner, context, new_list)
|
|
53
|
-
add_custom_context(context)
|
|
54
|
-
|
|
55
|
-
cache = cached_owned_tag_list_on(context)
|
|
56
|
-
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
|
57
|
-
|
|
58
|
-
cache[owner] = TagList.from(new_list)
|
|
38
|
+
|
|
39
|
+
# when preserving tag order, return tags in created order
|
|
40
|
+
# if we added the order to the association this would always apply
|
|
41
|
+
if self.class.preserve_tag_order?
|
|
42
|
+
scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
|
|
43
|
+
else
|
|
44
|
+
scope
|
|
59
45
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cached_owned_tag_list_on(context)
|
|
49
|
+
variable_name = "@owned_#{context}_list"
|
|
50
|
+
(instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def owner_tag_list_on(owner, context)
|
|
54
|
+
add_custom_context(context)
|
|
55
|
+
|
|
56
|
+
cache = cached_owned_tag_list_on(context)
|
|
57
|
+
|
|
58
|
+
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def set_owner_tag_list_on(owner, context, new_list)
|
|
62
|
+
add_custom_context(context)
|
|
63
|
+
|
|
64
|
+
cache = cached_owned_tag_list_on(context)
|
|
65
|
+
|
|
66
|
+
cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reload(*args)
|
|
70
|
+
self.class.tag_types.each do |context|
|
|
71
|
+
instance_variable_set("@owned_#{context}_list", nil)
|
|
67
72
|
end
|
|
68
|
-
|
|
69
|
-
def save_owned_tags
|
|
70
|
-
tagging_contexts.each do |context|
|
|
71
|
-
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
72
|
-
# Find existing tags or create non-existing tags:
|
|
73
|
-
tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
74
|
-
|
|
75
|
-
owned_tags = owner_tags_on(owner, context)
|
|
76
|
-
old_tags = owned_tags - tag_list
|
|
77
|
-
new_tags = tag_list - owned_tags
|
|
78
|
-
|
|
79
|
-
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
80
|
-
# have the correct context, and are removed from the list.
|
|
81
|
-
old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
82
|
-
:tagger_type => owner.class.to_s, :tagger_id => owner.id,
|
|
83
|
-
:tag_id => old_tags, :context => context).all
|
|
84
|
-
|
|
85
|
-
if old_taggings.present?
|
|
86
|
-
# Destroy old taggings:
|
|
87
|
-
Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
88
|
-
end
|
|
89
73
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
super(*args)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def save_owned_tags
|
|
78
|
+
tagging_contexts.each do |context|
|
|
79
|
+
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
80
|
+
|
|
81
|
+
# Find existing tags or create non-existing tags:
|
|
82
|
+
tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context)
|
|
83
|
+
|
|
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
|
+
old_tags, new_tags = owned_tags - tags, tags - owned_tags
|
|
90
|
+
|
|
91
|
+
shared_tags = owned_tags & tags
|
|
92
|
+
|
|
93
|
+
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
|
94
|
+
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
|
95
|
+
|
|
96
|
+
# Update arrays of tag objects
|
|
97
|
+
old_tags |= owned_tags.from(index)
|
|
98
|
+
new_tags |= owned_tags.from(index) & shared_tags
|
|
99
|
+
|
|
100
|
+
# Order the array of tag objects to match the tag list
|
|
101
|
+
new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
|
|
93
102
|
end
|
|
103
|
+
else
|
|
104
|
+
# Delete discarded tags and create new tags
|
|
105
|
+
old_tags = owned_tags - tags
|
|
106
|
+
new_tags = tags - owned_tags
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
110
|
+
# have the correct context, and are removed from the list.
|
|
111
|
+
ActsAsTaggableOn::Tagging.destroy_all(taggable_id: id, taggable_type: self.class.base_class.to_s,
|
|
112
|
+
tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
|
|
113
|
+
tag_id: old_tags, context: context) if old_tags.present?
|
|
114
|
+
|
|
115
|
+
# Create new taggings:
|
|
116
|
+
new_tags.each do |tag|
|
|
117
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self)
|
|
94
118
|
end
|
|
95
119
|
end
|
|
96
|
-
|
|
97
|
-
true
|
|
98
120
|
end
|
|
121
|
+
|
|
122
|
+
true
|
|
99
123
|
end
|
|
100
124
|
end
|
|
101
|
-
end
|
|
125
|
+
end
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
2
|
module Related
|
|
3
3
|
def self.included(base)
|
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
|
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
|
|
5
|
+
base.initialize_acts_as_taggable_on_related
|
|
6
6
|
end
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
module ClassMethods
|
|
9
9
|
def initialize_acts_as_taggable_on_related
|
|
10
10
|
tag_types.map(&:to_s).each do |tag_type|
|
|
11
|
-
class_eval
|
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
12
12
|
def find_related_#{tag_type}(options = {})
|
|
13
13
|
related_tags_for('#{tag_type}', self.class, options)
|
|
14
14
|
end
|
|
@@ -17,48 +17,55 @@ module ActsAsTaggableOn::Taggable
|
|
|
17
17
|
def find_related_#{tag_type}_for(klass, options = {})
|
|
18
18
|
related_tags_for('#{tag_type}', klass, options)
|
|
19
19
|
end
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
|
26
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
27
|
-
end
|
|
28
|
-
)
|
|
29
|
-
end
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
30
22
|
end
|
|
31
|
-
|
|
23
|
+
|
|
32
24
|
def acts_as_taggable_on(*args)
|
|
33
25
|
super(*args)
|
|
34
26
|
initialize_acts_as_taggable_on_related
|
|
35
27
|
end
|
|
36
28
|
end
|
|
37
|
-
|
|
38
|
-
module InstanceMethods
|
|
39
|
-
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
40
|
-
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
def find_matching_contexts(search_context, result_context, options = {})
|
|
31
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
|
35
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
39
|
+
tags_to_find = tags_on(search_context).map { |t| t.name }
|
|
40
|
+
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, result_context])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def related_tags_for(context, klass, options = {})
|
|
44
|
+
tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
|
|
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])
|
|
47
|
+
end
|
|
53
48
|
|
|
54
|
-
|
|
49
|
+
private
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
def exclude_self(klass, id)
|
|
52
|
+
"#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def group_columns(klass)
|
|
56
|
+
if ActsAsTaggableOn::Utils.using_postgresql?
|
|
57
|
+
grouped_column_names_for(klass)
|
|
58
|
+
else
|
|
59
|
+
"#{klass.table_name}.#{klass.primary_key}"
|
|
61
60
|
end
|
|
62
61
|
end
|
|
62
|
+
|
|
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)
|
|
69
|
+
end
|
|
63
70
|
end
|
|
64
|
-
end
|
|
71
|
+
end
|
|
@@ -1,65 +1,135 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
module ActsAsTaggableOn
|
|
3
|
+
class Tag < ::ActiveRecord::Base
|
|
4
|
+
|
|
5
|
+
attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
|
|
6
|
+
|
|
7
|
+
### ASSOCIATIONS:
|
|
8
|
+
|
|
9
|
+
has_many :taggings, dependent: :destroy, class_name: 'ActsAsTaggableOn::Tagging'
|
|
10
|
+
|
|
11
|
+
### VALIDATIONS:
|
|
12
|
+
|
|
13
|
+
validates_presence_of :name
|
|
14
|
+
validates_uniqueness_of :name, if: :validates_name_uniqueness?
|
|
15
|
+
validates_length_of :name, maximum: 255
|
|
16
|
+
|
|
17
|
+
# monkey patch this method if don't need name uniqueness validation
|
|
18
|
+
def validates_name_uniqueness?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
### SCOPES:
|
|
23
|
+
|
|
24
|
+
def self.named(name)
|
|
25
|
+
if ActsAsTaggableOn.strict_case_match
|
|
26
|
+
where(["name = #{binary}?", as_8bit_ascii(name)])
|
|
27
|
+
else
|
|
28
|
+
where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.named_any(list)
|
|
33
|
+
if ActsAsTaggableOn.strict_case_match
|
|
34
|
+
clause = list.map { |tag|
|
|
35
|
+
sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
|
|
36
|
+
}.join(' OR ')
|
|
37
|
+
where(clause)
|
|
38
|
+
else
|
|
39
|
+
clause = list.map { |tag|
|
|
40
|
+
sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
|
|
41
|
+
}.join(' OR ')
|
|
42
|
+
where(clause)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.named_like(name)
|
|
47
|
+
clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
|
|
48
|
+
where(clause)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.named_like_any(list)
|
|
52
|
+
clause = list.map { |tag|
|
|
53
|
+
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
|
|
54
|
+
}.join(' OR ')
|
|
55
|
+
where(clause)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
### CLASS METHODS:
|
|
59
|
+
|
|
60
|
+
def self.find_or_create_with_like_by_name(name)
|
|
61
|
+
if ActsAsTaggableOn.strict_case_match
|
|
62
|
+
self.find_or_create_all_with_like_by_name([name]).first
|
|
63
|
+
else
|
|
64
|
+
named_like(name).first || create(name: name)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
69
|
+
list = Array(list).flatten
|
|
70
|
+
|
|
71
|
+
return [] if list.empty?
|
|
72
|
+
|
|
73
|
+
existing_tags = named_any(list)
|
|
74
|
+
|
|
75
|
+
list.map do |tag_name|
|
|
76
|
+
comparable_tag_name = comparable_name(tag_name)
|
|
77
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
|
78
|
+
begin
|
|
79
|
+
existing_tag || create(name: tag_name)
|
|
80
|
+
rescue ActiveRecord::RecordNotUnique
|
|
81
|
+
# Postgres aborts the current transaction with
|
|
82
|
+
# PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
|
|
83
|
+
# so we have to rollback this transaction
|
|
84
|
+
raise DuplicateTagError.new("'#{tag_name}' has already been taken")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
### INSTANCE METHODS:
|
|
90
|
+
|
|
91
|
+
def ==(object)
|
|
92
|
+
super || (object.is_a?(Tag) && name == object.name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_s
|
|
96
|
+
name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def count
|
|
100
|
+
read_attribute(:count).to_i
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def comparable_name(str)
|
|
107
|
+
if ActsAsTaggableOn.strict_case_match
|
|
108
|
+
str
|
|
109
|
+
else
|
|
110
|
+
unicode_downcase(str.to_s)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def binary
|
|
115
|
+
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def unicode_downcase(string)
|
|
119
|
+
if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
|
|
120
|
+
ActiveSupport::Multibyte::Unicode.downcase(string)
|
|
121
|
+
else
|
|
122
|
+
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def as_8bit_ascii(string)
|
|
127
|
+
if defined?(Encoding)
|
|
128
|
+
string.to_s.dup.force_encoding('BINARY')
|
|
129
|
+
else
|
|
130
|
+
string.to_s.mb_chars
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
37
134
|
end
|
|
38
|
-
|
|
39
|
-
def self.find_or_create_all_with_like_by_name(*list)
|
|
40
|
-
list = [list].flatten
|
|
41
|
-
|
|
42
|
-
return [] if list.empty?
|
|
43
|
-
|
|
44
|
-
existing_tags = Tag.named_any(list).all
|
|
45
|
-
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
|
|
46
|
-
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
47
|
-
|
|
48
|
-
existing_tags + created_tags
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
### INSTANCE METHODS:
|
|
52
|
-
|
|
53
|
-
def ==(object)
|
|
54
|
-
super || (object.is_a?(Tag) && name == object.name)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def to_s
|
|
58
|
-
name
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def count
|
|
62
|
-
read_attribute(:count).to_i
|
|
63
|
-
end
|
|
64
|
-
|
|
65
135
|
end
|