make_taggable 0.7.4 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/CONTRIBUTING.md +65 -22
- data/LICENSE.md +18 -17
- data/README.md +73 -444
- data/UPGRADING.md +28 -2
- data/docs/caching.md +93 -0
- data/docs/configuration.md +108 -0
- data/docs/contexts.md +147 -0
- data/docs/database.md +106 -0
- data/docs/getting-started.md +114 -0
- data/docs/migrating-from-aato.md +67 -0
- data/docs/ownership.md +111 -0
- data/docs/parsers.md +109 -0
- data/docs/querying.md +163 -0
- data/docs/tag-clouds.md +74 -0
- data/lib/make_taggable/default_parser.rb +45 -32
- data/lib/make_taggable/engine.rb +6 -0
- data/lib/make_taggable/generic_parser.rb +30 -4
- data/lib/make_taggable/tag.rb +121 -19
- data/lib/make_taggable/tag_list.rb +77 -24
- data/lib/make_taggable/taggable/cache.rb +61 -11
- data/lib/make_taggable/taggable/collection.rb +102 -30
- data/lib/make_taggable/taggable/core.rb +154 -34
- data/lib/make_taggable/taggable/ownership.rb +82 -4
- data/lib/make_taggable/taggable/related.rb +66 -3
- data/lib/make_taggable/taggable/tag_list_type.rb +8 -0
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +15 -0
- data/lib/make_taggable/taggable/tagged_with_query.rb +17 -0
- data/lib/make_taggable/taggable.rb +34 -33
- data/lib/make_taggable/tagger.rb +73 -12
- data/lib/make_taggable/tagging.rb +30 -3
- data/lib/make_taggable/tags_helper.rb +22 -1
- data/lib/make_taggable/utils.rb +40 -5
- data/lib/make_taggable/version.rb +8 -1
- data/lib/make_taggable.rb +153 -8
- data/make_taggable.gemspec +13 -20
- metadata +50 -165
- data/.dummyrc +0 -17
- data/.github/workflows/ci.yml +0 -140
- data/.github/workflows/standard-ci.yml +0 -27
- data/.gitignore +0 -17
- data/.rspec +0 -3
- data/Appraisals +0 -15
- data/Gemfile +0 -16
- data/Rakefile +0 -13
- data/gemfiles/rails_5.gemfile +0 -9
- data/gemfiles/rails_6.gemfile +0 -9
- data/gemfiles/rails_6_1.gemfile +0 -9
- data/gemfiles/rails_master.gemfile +0 -9
- data/lib/tasks/setup_test_db.rake +0 -8
data/lib/make_taggable/tag.rb
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
4
|
+
##
|
|
5
|
+
# A tag name, shared by every record tagged with it.
|
|
6
|
+
#
|
|
7
|
+
# Tags are found and created through the class methods here rather than directly, so that the
|
|
8
|
+
# configured case sensitivity is applied consistently. Subclass it to keep a separate vocabulary
|
|
9
|
+
# for one context, and point at the subclass from
|
|
10
|
+
# {MakeTaggable::Taggable::Core#find_or_create_tags_from_list_with_context}.
|
|
11
|
+
#
|
|
12
|
+
# @!attribute [rw] name
|
|
13
|
+
# The tag itself.
|
|
14
|
+
# @return [String]
|
|
15
|
+
# @!attribute [rw] taggings_count
|
|
16
|
+
# How many taggings reference this tag, maintained as a counter cache.
|
|
17
|
+
# @return [Integer]
|
|
18
|
+
#
|
|
2
19
|
class Tag < ::ActiveRecord::Base
|
|
3
20
|
self.table_name = MakeTaggable.tags_table
|
|
4
21
|
|
|
@@ -10,7 +27,13 @@ module MakeTaggable
|
|
|
10
27
|
validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
|
|
11
28
|
validates_length_of :name, maximum: 255
|
|
12
29
|
|
|
13
|
-
|
|
30
|
+
##
|
|
31
|
+
# Whether the uniqueness validation on `name` runs.
|
|
32
|
+
#
|
|
33
|
+
# Override this in a subclass to allow tag names to repeat.
|
|
34
|
+
#
|
|
35
|
+
# @return [TrueClass, FalseClass] always `true` here
|
|
36
|
+
#
|
|
14
37
|
def validates_name_uniqueness?
|
|
15
38
|
true
|
|
16
39
|
end
|
|
@@ -19,26 +42,53 @@ module MakeTaggable
|
|
|
19
42
|
scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
|
|
20
43
|
scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }
|
|
21
44
|
|
|
45
|
+
##
|
|
46
|
+
# Tags matching a name exactly, honouring the configured case sensitivity.
|
|
47
|
+
#
|
|
48
|
+
# @param name [String] the name to match
|
|
49
|
+
# @return [ActiveRecord::Relation]
|
|
50
|
+
#
|
|
22
51
|
def self.named(name)
|
|
23
52
|
if MakeTaggable.strict_case_match
|
|
24
|
-
where(["name = #{binary}?",
|
|
53
|
+
where(["name = #{binary}?", name.to_s])
|
|
25
54
|
else
|
|
26
|
-
where(["LOWER(name) = LOWER(?)",
|
|
55
|
+
where(["LOWER(name) = LOWER(?)", name.to_s.downcase])
|
|
27
56
|
end
|
|
28
57
|
end
|
|
29
58
|
|
|
59
|
+
##
|
|
60
|
+
# Tags matching any of the given names exactly.
|
|
61
|
+
#
|
|
62
|
+
# @param list [Array<String>] the names to match
|
|
63
|
+
# @return [ActiveRecord::Relation]
|
|
64
|
+
#
|
|
30
65
|
def self.named_any(list)
|
|
31
66
|
clause = list.map { |tag|
|
|
32
|
-
sanitize_sql_for_named_any(tag)
|
|
67
|
+
sanitize_sql_for_named_any(tag)
|
|
33
68
|
}.join(" OR ")
|
|
34
69
|
where(clause)
|
|
35
70
|
end
|
|
36
71
|
|
|
72
|
+
##
|
|
73
|
+
# Tags whose name contains the given fragment.
|
|
74
|
+
#
|
|
75
|
+
# Case insensitive on PostgreSQL, which uses `ILIKE`; otherwise it follows the column's
|
|
76
|
+
# collation.
|
|
77
|
+
#
|
|
78
|
+
# @param name [String] the fragment to look for
|
|
79
|
+
# @return [ActiveRecord::Relation]
|
|
80
|
+
#
|
|
37
81
|
def self.named_like(name)
|
|
38
82
|
clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
|
|
39
83
|
where(clause)
|
|
40
84
|
end
|
|
41
85
|
|
|
86
|
+
##
|
|
87
|
+
# Tags whose name contains any of the given fragments.
|
|
88
|
+
#
|
|
89
|
+
# @param list [Array<String>] the fragments to look for
|
|
90
|
+
# @return [ActiveRecord::Relation]
|
|
91
|
+
#
|
|
42
92
|
def self.named_like_any(list)
|
|
43
93
|
clause = list.map { |tag|
|
|
44
94
|
sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
|
|
@@ -46,6 +96,15 @@ module MakeTaggable
|
|
|
46
96
|
where(clause)
|
|
47
97
|
end
|
|
48
98
|
|
|
99
|
+
##
|
|
100
|
+
# Tags used in a given context, whatever the record they were applied to.
|
|
101
|
+
#
|
|
102
|
+
# @param context [String, Symbol] the tagging context
|
|
103
|
+
# @return [ActiveRecord::Relation]
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# MakeTaggable::Tag.for_context(:skills)
|
|
107
|
+
#
|
|
49
108
|
def self.for_context(context)
|
|
50
109
|
joins(:taggings)
|
|
51
110
|
.where(["#{MakeTaggable.taggings_table}.context = ?", context])
|
|
@@ -53,29 +112,61 @@ module MakeTaggable
|
|
|
53
112
|
end
|
|
54
113
|
|
|
55
114
|
### CLASS METHODS:
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Finds a tag by name, creating it when it does not exist yet.
|
|
118
|
+
#
|
|
119
|
+
# The name is matched in full. Honours the configured case sensitivity.
|
|
120
|
+
#
|
|
121
|
+
# @param name [String] the tag name
|
|
122
|
+
# @return [MakeTaggable::Tag]
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# MakeTaggable::Tag.find_or_create_with_like_by_name("ruby")
|
|
126
|
+
#
|
|
56
127
|
def self.find_or_create_with_like_by_name(name)
|
|
57
128
|
if MakeTaggable.strict_case_match
|
|
58
129
|
find_or_create_all_with_like_by_name([name]).first
|
|
59
130
|
else
|
|
60
|
-
|
|
131
|
+
# Matching has to happen in Ruby's terms rather than the column's: the
|
|
132
|
+
# MySQL migration collates tag names as utf8mb4_bin, which would make a
|
|
133
|
+
# LIKE comparison case sensitive whatever strict_case_match says.
|
|
134
|
+
named(name).first || create(name: name)
|
|
61
135
|
end
|
|
62
136
|
end
|
|
63
137
|
|
|
138
|
+
##
|
|
139
|
+
# Finds every tag in a list by name, creating those that do not exist yet.
|
|
140
|
+
#
|
|
141
|
+
# A competing write that takes a name first is retried up to three times before giving up.
|
|
142
|
+
#
|
|
143
|
+
# @param list [Array<String>] the tag names
|
|
144
|
+
# @return [Array<MakeTaggable::Tag>] in the order the names were given
|
|
145
|
+
# @raise [MakeTaggable::DuplicateTagError] when a name stays taken after three attempts
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# MakeTaggable::Tag.find_or_create_all_with_like_by_name(%w[ruby rails])
|
|
149
|
+
#
|
|
64
150
|
def self.find_or_create_all_with_like_by_name(*list)
|
|
65
151
|
list = Array(list).flatten
|
|
66
152
|
|
|
67
153
|
return [] if list.empty?
|
|
68
154
|
|
|
69
|
-
existing_tags = named_any(list)
|
|
155
|
+
existing_tags = named_any(list).to_a
|
|
70
156
|
list.map do |tag_name|
|
|
71
157
|
tries ||= 3
|
|
72
158
|
comparable_tag_name = comparable_name(tag_name)
|
|
73
159
|
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
|
74
|
-
existing_tag
|
|
160
|
+
next existing_tag if existing_tag
|
|
161
|
+
|
|
162
|
+
# Tags created earlier in this call have to stay visible to the names
|
|
163
|
+
# that follow, or a list holding both "Ruby" and "ruby" resolves to two
|
|
164
|
+
# rows even though the two names compare equal.
|
|
165
|
+
create(name: tag_name).tap { |tag| existing_tags << tag }
|
|
75
166
|
rescue ActiveRecord::RecordNotUnique
|
|
76
167
|
if (tries -= 1).positive?
|
|
77
168
|
ActiveRecord::Base.connection.execute "ROLLBACK"
|
|
78
|
-
existing_tags = named_any(list)
|
|
169
|
+
existing_tags = named_any(list).to_a
|
|
79
170
|
retry
|
|
80
171
|
end
|
|
81
172
|
|
|
@@ -84,14 +175,33 @@ module MakeTaggable
|
|
|
84
175
|
end
|
|
85
176
|
|
|
86
177
|
### INSTANCE METHODS:
|
|
178
|
+
|
|
179
|
+
##
|
|
180
|
+
# Compares tags by name, so a saved tag and an unsaved one with the same name are equal.
|
|
181
|
+
#
|
|
182
|
+
# @param other [Object] the object to compare against
|
|
183
|
+
# @return [TrueClass, FalseClass]
|
|
184
|
+
#
|
|
87
185
|
def ==(other)
|
|
88
186
|
super || (other.is_a?(Tag) && name == other.name)
|
|
89
187
|
end
|
|
90
188
|
|
|
189
|
+
##
|
|
190
|
+
# The tag's name, so a tag renders as itself in a view or a string.
|
|
191
|
+
#
|
|
192
|
+
# @return [String]
|
|
193
|
+
#
|
|
91
194
|
def to_s
|
|
92
195
|
name
|
|
93
196
|
end
|
|
94
197
|
|
|
198
|
+
##
|
|
199
|
+
# How many times this tag matched, on relations that select a count alongside the tag columns.
|
|
200
|
+
#
|
|
201
|
+
# Zero on a tag loaded without one.
|
|
202
|
+
#
|
|
203
|
+
# @return [Integer]
|
|
204
|
+
#
|
|
95
205
|
def count
|
|
96
206
|
read_attribute(:count).to_i
|
|
97
207
|
end
|
|
@@ -103,7 +213,7 @@ module MakeTaggable
|
|
|
103
213
|
if MakeTaggable.strict_case_match
|
|
104
214
|
str
|
|
105
215
|
else
|
|
106
|
-
|
|
216
|
+
str.to_s.downcase
|
|
107
217
|
end
|
|
108
218
|
end
|
|
109
219
|
|
|
@@ -111,19 +221,11 @@ module MakeTaggable
|
|
|
111
221
|
MakeTaggable::Utils.using_mysql? ? "BINARY " : nil
|
|
112
222
|
end
|
|
113
223
|
|
|
114
|
-
def as_8bit_ascii(string)
|
|
115
|
-
string.to_s.mb_chars
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def unicode_downcase(string)
|
|
119
|
-
as_8bit_ascii(string).downcase
|
|
120
|
-
end
|
|
121
|
-
|
|
122
224
|
def sanitize_sql_for_named_any(tag)
|
|
123
225
|
if MakeTaggable.strict_case_match
|
|
124
|
-
sanitize_sql(["name = #{binary}?",
|
|
226
|
+
sanitize_sql(["name = #{binary}?", tag.to_s])
|
|
125
227
|
else
|
|
126
|
-
sanitize_sql(["LOWER(name) = LOWER(?)",
|
|
228
|
+
sanitize_sql(["LOWER(name) = LOWER(?)", tag.to_s.downcase])
|
|
127
229
|
end
|
|
128
230
|
end
|
|
129
231
|
end
|
|
@@ -1,22 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_support/core_ext/module/delegation"
|
|
2
4
|
|
|
3
5
|
module MakeTaggable
|
|
6
|
+
##
|
|
7
|
+
# The list of tag names held against one context of one record.
|
|
8
|
+
#
|
|
9
|
+
# A tag list is an Array, so everything Array offers works on it. What it adds is parsing, and
|
|
10
|
+
# cleaning: blank entries are dropped, entries are converted to strings and stripped, and
|
|
11
|
+
# duplicates are removed according to the configured case sensitivity.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# list = MakeTaggable::TagList.new("Fun", "Happy")
|
|
15
|
+
# list.add("Sad, Lonely", parse: true)
|
|
16
|
+
# list # => ["Fun", "Happy", "Sad", "Lonely"]
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [rw] owner
|
|
19
|
+
# The tagger whose tags these are, when the list belongs to an owner.
|
|
20
|
+
# @return [ActiveRecord::Base, NilClass]
|
|
21
|
+
# @!attribute [rw] parser
|
|
22
|
+
# The parser used when this list is asked to parse a string.
|
|
23
|
+
# @return [Class]
|
|
24
|
+
#
|
|
4
25
|
class TagList < Array
|
|
5
26
|
attr_accessor :owner
|
|
6
27
|
attr_accessor :parser
|
|
7
28
|
|
|
29
|
+
##
|
|
30
|
+
# Builds a tag list from the given names.
|
|
31
|
+
#
|
|
32
|
+
# @param args [Array<String, Symbol>] the tag names, optionally followed by an options hash
|
|
33
|
+
# accepted by {#add}
|
|
34
|
+
# @return [MakeTaggable::TagList]
|
|
35
|
+
#
|
|
8
36
|
def initialize(*args)
|
|
9
37
|
@parser = MakeTaggable.default_parser
|
|
10
38
|
add(*args)
|
|
11
39
|
end
|
|
12
40
|
|
|
13
41
|
##
|
|
14
|
-
#
|
|
15
|
-
#
|
|
42
|
+
# Adds tags to the list, ignoring duplicates and blanks.
|
|
43
|
+
#
|
|
44
|
+
# @param names [Array<String, Symbol>] the tags to add, optionally followed by an options hash
|
|
45
|
+
# @option names [TrueClass, FalseClass] :parse whether to parse the input as a delimited string
|
|
46
|
+
# @option names [Class] :parser a parser to use for this call only
|
|
47
|
+
# @return [MakeTaggable::TagList] self, so calls can be chained
|
|
16
48
|
#
|
|
17
|
-
#
|
|
49
|
+
# @example
|
|
18
50
|
# tag_list.add("Fun", "Happy")
|
|
19
|
-
# tag_list.add("Fun, Happy", :
|
|
51
|
+
# tag_list.add("Fun, Happy", parse: true)
|
|
52
|
+
#
|
|
20
53
|
def add(*names)
|
|
21
54
|
extract_and_apply_options!(names)
|
|
22
55
|
concat(names)
|
|
@@ -24,32 +57,48 @@ module MakeTaggable
|
|
|
24
57
|
self
|
|
25
58
|
end
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
#
|
|
60
|
+
##
|
|
61
|
+
# Adds one tag to the list.
|
|
62
|
+
#
|
|
63
|
+
# @param obj [String, Symbol] the tag to add
|
|
64
|
+
# @return [MakeTaggable::TagList] self, so appends can be chained
|
|
65
|
+
#
|
|
30
66
|
def <<(obj)
|
|
31
67
|
add(obj)
|
|
32
68
|
end
|
|
33
69
|
|
|
34
|
-
|
|
35
|
-
# two tag lists
|
|
70
|
+
##
|
|
71
|
+
# Joins two tag lists into a third, leaving both untouched.
|
|
72
|
+
#
|
|
73
|
+
# @param other [Array<String>] the tags to append
|
|
74
|
+
# @return [MakeTaggable::TagList] a new list
|
|
75
|
+
#
|
|
36
76
|
def +(other)
|
|
37
77
|
TagList.new.add(self).add(other)
|
|
38
78
|
end
|
|
39
79
|
|
|
40
|
-
|
|
80
|
+
##
|
|
81
|
+
# Appends another list's tags to this one.
|
|
82
|
+
#
|
|
83
|
+
# @param other_tag_list [Array<String>] the tags to append
|
|
84
|
+
# @return [MakeTaggable::TagList] self
|
|
85
|
+
#
|
|
41
86
|
def concat(other_tag_list)
|
|
42
|
-
super
|
|
87
|
+
super.send(:clean!)
|
|
43
88
|
self
|
|
44
89
|
end
|
|
45
90
|
|
|
46
91
|
##
|
|
47
|
-
#
|
|
48
|
-
#
|
|
92
|
+
# Removes tags from the list.
|
|
93
|
+
#
|
|
94
|
+
# @param names [Array<String, Symbol>] the tags to remove, optionally followed by an options
|
|
95
|
+
# hash accepted by {#add}
|
|
96
|
+
# @return [MakeTaggable::TagList] self
|
|
49
97
|
#
|
|
50
|
-
#
|
|
98
|
+
# @example
|
|
51
99
|
# tag_list.remove("Sad", "Lonely")
|
|
52
|
-
# tag_list.remove("Sad, Lonely", :
|
|
100
|
+
# tag_list.remove("Sad, Lonely", parse: true)
|
|
101
|
+
#
|
|
53
102
|
def remove(*names)
|
|
54
103
|
extract_and_apply_options!(names)
|
|
55
104
|
delete_if { |name| names.include?(name) }
|
|
@@ -57,20 +106,24 @@ module MakeTaggable
|
|
|
57
106
|
end
|
|
58
107
|
|
|
59
108
|
##
|
|
60
|
-
#
|
|
61
|
-
#
|
|
109
|
+
# Renders the list as a delimited string, suitable for a form field.
|
|
110
|
+
#
|
|
111
|
+
# Tags containing the delimiter are quoted, so the string parses back into the same list.
|
|
112
|
+
#
|
|
113
|
+
# @return [String]
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# MakeTaggable::TagList.new("Round", "Square,Cube").to_s
|
|
117
|
+
# # => 'Round, "Square,Cube"'
|
|
62
118
|
#
|
|
63
|
-
# Example:
|
|
64
|
-
# tag_list = TagList.new("Round", "Square,Cube")
|
|
65
|
-
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
66
119
|
def to_s
|
|
67
120
|
tags = frozen? ? dup : self
|
|
68
121
|
tags.send(:clean!)
|
|
69
122
|
|
|
123
|
+
delimiter = Regexp.union(Array(MakeTaggable.delimiter))
|
|
124
|
+
|
|
70
125
|
tags.map { |name|
|
|
71
|
-
|
|
72
|
-
d = Regexp.new d.join("|") if d.is_a? Array
|
|
73
|
-
name.index(d) ? "\"#{name}\"" : name
|
|
126
|
+
name.index(delimiter) ? "\"#{name}\"" : name
|
|
74
127
|
}.join(MakeTaggable.glue)
|
|
75
128
|
end
|
|
76
129
|
|
|
@@ -81,7 +134,7 @@ module MakeTaggable
|
|
|
81
134
|
reject!(&:blank?)
|
|
82
135
|
map!(&:to_s)
|
|
83
136
|
map!(&:strip)
|
|
84
|
-
map!
|
|
137
|
+
map!(&:downcase) if MakeTaggable.force_lowercase
|
|
85
138
|
map!(&:parameterize) if MakeTaggable.force_parameterize
|
|
86
139
|
|
|
87
140
|
MakeTaggable.strict_case_match ? uniq! : uniq! { |tag| tag.downcase }
|
|
@@ -1,22 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable
|
|
4
|
+
##
|
|
5
|
+
# Keeps a rendered copy of a tag list in a column on the taggable itself, so a list can be shown
|
|
6
|
+
# without loading its tags.
|
|
7
|
+
#
|
|
8
|
+
# Caching switches itself on for any context the model has a matching `cached_<context>_list`
|
|
9
|
+
# column for. Nothing else is required.
|
|
10
|
+
#
|
|
11
|
+
# @example Caching the default context
|
|
12
|
+
# add_column :books, :cached_tag_list, :string
|
|
13
|
+
#
|
|
2
14
|
module Cache
|
|
15
|
+
##
|
|
16
|
+
# @param base [Class] the model being made taggable
|
|
17
|
+
# @return [void]
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
#
|
|
3
21
|
def self.included(base)
|
|
4
22
|
# When included, conditionally adds tag caching methods when the model
|
|
5
23
|
# has any "cached_#{tag_type}_list" column
|
|
6
24
|
base.extend Columns
|
|
7
25
|
end
|
|
8
26
|
|
|
27
|
+
##
|
|
28
|
+
# Intercepts the model's column lookup so caching can be wired up the first time the schema is
|
|
29
|
+
# known, without opening a database connection when the class is loaded.
|
|
30
|
+
#
|
|
9
31
|
module Columns
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# the call to :columns as @make_taggable_cache_columns
|
|
16
|
-
# to mimic the underlying behavior. While processing this first
|
|
17
|
-
# call to columns, we do the caching column check and dynamically add
|
|
18
|
-
# the class and instance methods
|
|
19
|
-
# FIXME: this method cannot compile in rubinius
|
|
32
|
+
##
|
|
33
|
+
# The model's columns, injecting the caching methods the first time they are asked for.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>]
|
|
36
|
+
#
|
|
20
37
|
def columns
|
|
21
38
|
@make_taggable_cache_columns ||= begin
|
|
22
39
|
db_columns = super
|
|
@@ -25,6 +42,11 @@ module MakeTaggable::Taggable
|
|
|
25
42
|
end
|
|
26
43
|
end
|
|
27
44
|
|
|
45
|
+
##
|
|
46
|
+
# Forgets the cached column lookup along with Active Record's own.
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
28
50
|
def reset_column_information
|
|
29
51
|
super
|
|
30
52
|
@make_taggable_cache_columns = nil
|
|
@@ -51,7 +73,15 @@ module MakeTaggable::Taggable
|
|
|
51
73
|
end
|
|
52
74
|
end
|
|
53
75
|
|
|
76
|
+
##
|
|
77
|
+
# Added to a model once it has at least one caching column.
|
|
78
|
+
#
|
|
54
79
|
module ClassMethods
|
|
80
|
+
##
|
|
81
|
+
# Defines a `caching_<context>_list?` predicate for each context.
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
#
|
|
55
85
|
def initialize_tags_cache
|
|
56
86
|
tag_types.map(&:to_s).each do |tag_type|
|
|
57
87
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
@@ -62,17 +92,37 @@ module MakeTaggable::Taggable
|
|
|
62
92
|
end
|
|
63
93
|
end
|
|
64
94
|
|
|
95
|
+
##
|
|
96
|
+
# Adds contexts and refreshes the caching predicates.
|
|
97
|
+
#
|
|
98
|
+
# @param args [Array<Symbol, String>] the contexts to add
|
|
99
|
+
# @return [void]
|
|
100
|
+
#
|
|
65
101
|
def make_taggable(*args)
|
|
66
|
-
super
|
|
102
|
+
super
|
|
67
103
|
initialize_tags_cache
|
|
68
104
|
end
|
|
69
105
|
|
|
106
|
+
##
|
|
107
|
+
# Whether a context's tag list is cached on the model.
|
|
108
|
+
#
|
|
109
|
+
# @param context [Symbol, String] the tagging context
|
|
110
|
+
# @return [TrueClass, FalseClass]
|
|
111
|
+
#
|
|
70
112
|
def caching_tag_list_on?(context)
|
|
71
113
|
column_names.include?("cached_#{context.to_s.singularize}_list")
|
|
72
114
|
end
|
|
73
115
|
end
|
|
74
116
|
|
|
117
|
+
##
|
|
118
|
+
# Added to a model once it has at least one caching column.
|
|
119
|
+
#
|
|
75
120
|
module InstanceMethods
|
|
121
|
+
##
|
|
122
|
+
# Writes each cached tag list into its column. Runs before save.
|
|
123
|
+
#
|
|
124
|
+
# @return [TrueClass]
|
|
125
|
+
#
|
|
76
126
|
def save_cached_tag_list
|
|
77
127
|
tag_types.map(&:to_s).each do |tag_type|
|
|
78
128
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|