tagtical 1.0.8 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +27 -6
- data/VERSION +1 -1
- data/lib/tagtical/tag.rb +117 -78
- data/lib/tagtical/tag_list.rb +64 -46
- data/lib/tagtical/taggable/cache.rb +7 -8
- data/lib/tagtical/taggable/collection.rb +1 -1
- data/lib/tagtical/taggable/core.rb +119 -91
- data/lib/tagtical/taggable/ownership.rb +4 -4
- data/lib/tagtical/taggable/related.rb +1 -1
- data/lib/tagtical/taggable.rb +1 -1
- data/spec/models.rb +2 -2
- data/spec/spec_helper.rb +12 -1
- data/spec/tagtical/acts_as_tagger_spec.rb +4 -7
- data/spec/tagtical/tag_list_spec.rb +33 -7
- data/spec/tagtical/tag_spec.rb +61 -52
- data/spec/tagtical/taggable_spec.rb +167 -50
- data/spec/tagtical/tagtical_spec.rb +45 -41
- metadata +14 -14
data/README.rdoc
CHANGED
@@ -13,8 +13,8 @@ as a designator for the STI class. Subsequently, you could also add a "relevance
|
|
13
13
|
that mood is.
|
14
14
|
|
15
15
|
Tagtical allows for an arbitrary number of Tag subclasses, each of which can be extended to the needs
|
16
|
-
of the application.
|
17
|
-
|
16
|
+
of the application. Note: Tag subclasses are required! You cannot do custom "contexts" as you could in
|
17
|
+
acts_as_taggable_on/
|
18
18
|
|
19
19
|
Here are the main differences between tagtical and acts_as_taggable_on:
|
20
20
|
|
@@ -28,6 +28,14 @@ could be a serialized field of long and lat if you wanted.
|
|
28
28
|
5. Support a config/tagtical.yml to further configure the application. For example, since most people
|
29
29
|
usually have one User class for their application, there is no reason to do polymorphic on "tagger",
|
30
30
|
so I give the user the option to specify the class_name specifically for tagger.
|
31
|
+
6. :parse option to tag_list is inverted. Specify :parse => false if you don't want your tag values
|
32
|
+
parsed.
|
33
|
+
|
34
|
+
Example:
|
35
|
+
# would give you a tag of "red, blue" instead of "red" and "blue".
|
36
|
+
tag_list.add("red, blue", :parse => false)
|
37
|
+
|
38
|
+
7. Custom contexts are *not* supported since we use STI.
|
31
39
|
|
32
40
|
Additions include:
|
33
41
|
1. Scopes are created on Tag so you can do photo.tags.color and grab all the tags of type Tag::Color, for example.
|
@@ -90,6 +98,10 @@ rake spec:plugins
|
|
90
98
|
class Activity < Tagtical::Tag
|
91
99
|
end
|
92
100
|
class Sport < Activity
|
101
|
+
|
102
|
+
# You can also set the possible values for a specific tag type
|
103
|
+
self.possible_values = %w{boxing basketball tennis hockey footabll soccer}
|
104
|
+
|
93
105
|
def ball?
|
94
106
|
value=~/ball$/i
|
95
107
|
end
|
@@ -102,6 +114,11 @@ rake spec:plugins
|
|
102
114
|
@user.activity_list # => ["joking","clowning","boxing"] as TagList
|
103
115
|
@user.save
|
104
116
|
|
117
|
+
# Cascade tag_list setters =)
|
118
|
+
@user.set_activity_list(["clowning", "boxing"], :cascade => true)
|
119
|
+
@user.save!
|
120
|
+
@user.sport_list # => ["boxing"]
|
121
|
+
|
105
122
|
@user.tags # => [<Tag value:"awesome">,<Tag value:"slick">,<Tag value:"hefty">]
|
106
123
|
@user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Activity value:"boxing">]
|
107
124
|
@user.activities.first.athletic? # => false
|
@@ -114,15 +131,19 @@ rake spec:plugins
|
|
114
131
|
# from Activity to Sport and give it a relevance of 4.5.
|
115
132
|
@user.save
|
116
133
|
|
134
|
+
@user.sport_list = {"chess"}
|
135
|
+
@user.save! # <=== will throw an error, chess is not a sport!
|
136
|
+
|
117
137
|
@user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Sport value:"boxing">]
|
118
|
-
@user.activities(:
|
119
|
-
@user.activities(:
|
120
|
-
@user.activities(:
|
121
|
-
@user.activities(:
|
138
|
+
@user.activities(:scope => :children) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
|
139
|
+
@user.activities(:scope => :<) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
|
140
|
+
@user.activities(:scope => :current) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
|
141
|
+
@user.activities(:scope => :==) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
|
122
142
|
|
123
143
|
@user.activities.first.athletic? # => false
|
124
144
|
@user.sports.all(&:ball?) # => true
|
125
145
|
|
146
|
+
|
126
147
|
--- Defining Subclasses
|
127
148
|
|
128
149
|
There is a lot of flexibility when it comes to naming subclasses. Lets say the type column had a value
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0
|
1
|
+
1.1.0
|
data/lib/tagtical/tag.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
module Tagtical
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
|
-
|
3
|
+
|
4
4
|
attr_accessible :value
|
5
5
|
|
6
6
|
has_many :taggings, :dependent => :destroy, :class_name => 'Tagtical::Tagging'
|
7
7
|
|
8
8
|
scope(:type, lambda do |context, *args|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
Type.cache[Type.send(:sanitize, context)].inject(nil) do |scoping, tag_type|
|
10
|
+
scope = tag_type.scoping(*args)
|
11
|
+
scoping ? scoping.merge(scope) : scope
|
12
|
+
end
|
12
13
|
end)
|
13
14
|
|
14
15
|
validates :value, :uniqueness => {:scope => :type}, :presence => true # type is not required, it can be blank
|
15
16
|
|
16
17
|
class_attribute :possible_values
|
18
|
+
before_validation :ensure_possible_values
|
17
19
|
validate :validate_possible_values
|
18
|
-
|
19
|
-
self.store_full_sti_class = false
|
20
20
|
|
21
21
|
### CLASS METHODS:
|
22
22
|
|
@@ -35,7 +35,7 @@ module Tagtical
|
|
35
35
|
connection.adapter_name=='PostgreSQL'
|
36
36
|
end
|
37
37
|
|
38
|
-
# Use this for case insensitive
|
38
|
+
# Use this for case insensitive
|
39
39
|
def where_any_like(list, options={})
|
40
40
|
where_any(list, options.update(:case_insensitive => true))
|
41
41
|
end
|
@@ -52,41 +52,49 @@ module Tagtical
|
|
52
52
|
tag_list = [tag_list].flatten
|
53
53
|
return {} if tag_list.empty?
|
54
54
|
|
55
|
-
existing_tags
|
55
|
+
existing_tags = where_any_like(tag_list).all
|
56
56
|
tag_list.each_with_object({}) do |value, tag_lookup|
|
57
57
|
tag_lookup[detect_comparable(existing_tags, value) || create!(:value => value)] = value
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
# Save disc space by not having to put in "Tagtical::Tag" repeatedly
|
61
62
|
def sti_name
|
62
|
-
|
63
|
-
@sti_name = Tagtical::Tag==self ? nil : Type[name.demodulize].to_sti_name
|
63
|
+
Tagtical::Tag==self ? nil : super
|
64
64
|
end
|
65
65
|
|
66
66
|
def define_scope_for_type(tag_type)
|
67
|
-
scope(tag_type.scope_name, lambda { |*args| type(tag_type, *args) }) unless respond_to?(tag_type.scope_name)
|
67
|
+
scope(tag_type.scope_name, lambda { |*args| type(tag_type.to_s, *args) }) unless respond_to?(tag_type.scope_name)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Checks to see if a tags value is present in a set of tags and returns that tag.
|
71
|
+
def detect_comparable(objects, value)
|
72
|
+
value = comparable_value(value)
|
73
|
+
objects.detect { |obj| comparable_value(obj) == value }
|
74
|
+
end
|
75
|
+
|
76
|
+
def detect_possible_value(value)
|
77
|
+
detect_comparable(possible_values, value) if possible_values
|
68
78
|
end
|
69
79
|
|
70
80
|
protected
|
71
81
|
|
72
82
|
def compute_type(type_name)
|
73
|
-
|
74
|
-
# super is required when it gets called from a reflection.
|
75
|
-
@@compute_type[type_name] || super
|
76
|
-
rescue Exception => e
|
77
|
-
@@compute_type[type_name] = Type.new(type_name).klass!
|
83
|
+
type_name.nil? ? Tagtical::Tag : super
|
78
84
|
end
|
79
85
|
|
80
86
|
private
|
81
|
-
|
82
|
-
# Checks to see if a tags value is present in a set of tags and returns that tag.
|
83
|
-
def detect_comparable(tags, value)
|
84
|
-
value = comparable_value(value)
|
85
|
-
tags.detect { |tag| comparable_value(tag.value) == value }
|
86
|
-
end
|
87
87
|
|
88
|
-
|
89
|
-
|
88
|
+
if RUBY_VERSION >= "1.9"
|
89
|
+
def comparable_value(str)
|
90
|
+
str = str.value if str.is_a?(self)
|
91
|
+
str.downcase
|
92
|
+
end
|
93
|
+
else
|
94
|
+
def comparable_value(str)
|
95
|
+
str = str.value if str.is_a?(self)
|
96
|
+
str.mb_chars.downcase
|
97
|
+
end
|
90
98
|
end
|
91
99
|
|
92
100
|
end
|
@@ -101,7 +109,7 @@ module Tagtical
|
|
101
109
|
(v = self[:relevance]) && v.to_f
|
102
110
|
end
|
103
111
|
|
104
|
-
|
112
|
+
# Try to sort by the relevance if provided.
|
105
113
|
def <=>(tag)
|
106
114
|
if (r1 = relevance) && (r2 = tag.relevance)
|
107
115
|
r1 <=> r2
|
@@ -120,11 +128,6 @@ module Tagtical
|
|
120
128
|
end
|
121
129
|
end
|
122
130
|
|
123
|
-
# We return nil if we are *not* an STI class.
|
124
|
-
def type
|
125
|
-
(type = self[:type]) && Type.new(type)
|
126
|
-
end
|
127
|
-
|
128
131
|
def count
|
129
132
|
self[:count].to_i
|
130
133
|
end
|
@@ -132,17 +135,24 @@ module Tagtical
|
|
132
135
|
private
|
133
136
|
|
134
137
|
def method_missing(method_name, *args, &block)
|
135
|
-
if method_name[-1]=="?"
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
self.class.send(:define_method, method_name, &lambda)
|
138
|
+
if method_name[-1]=="?" && (types = Type.cache[method_name[0..-2]])
|
139
|
+
self.class.send(:define_method, method_name) do
|
140
|
+
types.any? { |type| is_a?(type.klass) }
|
141
|
+
end
|
140
142
|
send(method_name)
|
141
143
|
else
|
142
144
|
super
|
143
145
|
end
|
144
146
|
end
|
145
147
|
|
148
|
+
# Ensure that the value follows the case-sensitivity from the possible_values.
|
149
|
+
def ensure_possible_values
|
150
|
+
if value = self.class.detect_possible_value(self.value)
|
151
|
+
self.value = value
|
152
|
+
end
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
146
156
|
def validate_possible_values
|
147
157
|
if possible_values && !possible_values.include?(value)
|
148
158
|
errors.add(:value, %{Value "#{value}" not found in list: #{possible_values.inspect}})
|
@@ -154,24 +164,59 @@ module Tagtical
|
|
154
164
|
# "tag" should always correspond with demodulize name of the base Tag class (ie Tagtical::Tag).
|
155
165
|
BASE = "tag".freeze
|
156
166
|
|
167
|
+
attr_reader :taggable_class
|
168
|
+
|
169
|
+
def initialize(str, taggable_class, options={})
|
170
|
+
options.each { |k, v| instance_variable_set("@#{k}", v) }
|
171
|
+
@taggable_class = taggable_class
|
172
|
+
super(str)
|
173
|
+
end
|
174
|
+
|
175
|
+
@@cache = {}
|
176
|
+
cattr_reader :cache
|
177
|
+
|
157
178
|
class << self
|
158
|
-
def find(input)
|
159
|
-
|
160
|
-
|
179
|
+
def find(input, taggable_class)
|
180
|
+
case input
|
181
|
+
when self then input
|
182
|
+
when String, Symbol then new(sanitize(input), taggable_class)
|
183
|
+
when Hash then input.map { |input, klass| new(sanitize(input), taggable_class, :klass => klass) }
|
184
|
+
when Array then input.map { |c| find(c, taggable_class) }.flatten
|
185
|
+
end
|
161
186
|
end
|
162
187
|
alias :[] :find
|
163
188
|
|
189
|
+
# Stores the tag types in memory
|
190
|
+
def register(inputs, taggable_class)
|
191
|
+
find(inputs, taggable_class).each do |tag_type|
|
192
|
+
cache[tag_type] ||= []
|
193
|
+
cache[tag_type] << tag_type unless cache[tag_type].include?(tag_type)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
164
197
|
private
|
165
198
|
|
166
|
-
# Sanitize the input for type name consistency.
|
199
|
+
# Sanitize the input for type name consistency and klass.
|
167
200
|
def sanitize(input)
|
168
201
|
input.to_s.singularize.underscore.gsub(/_tag$/, '')
|
169
202
|
end
|
170
203
|
end
|
171
204
|
|
172
|
-
#
|
173
|
-
def
|
174
|
-
self
|
205
|
+
# Matches the string against the type after sanitizing it.
|
206
|
+
def match?(input)
|
207
|
+
self==self.class.send(:sanitize, input)
|
208
|
+
end
|
209
|
+
|
210
|
+
def comparable_array
|
211
|
+
[to_s, klass]
|
212
|
+
end
|
213
|
+
|
214
|
+
def ==(input)
|
215
|
+
case input
|
216
|
+
when self.class then comparable_array==input.comparable_array
|
217
|
+
when String then input==self
|
218
|
+
else false
|
219
|
+
end
|
175
220
|
end
|
176
221
|
|
177
222
|
# Leverages current type_condition logic from ActiveRecord while also allowing for type conditions
|
@@ -181,8 +226,9 @@ module Tagtical
|
|
181
226
|
# <tt>sql</tt> - Set to true to return sql string. Set to :append to return a sql string which can be appended as a condition.
|
182
227
|
# <tt>only</tt> - An array of the following: :parents, :current, :children. Will construct conditions to query the current, parent, and/or children STI classes.
|
183
228
|
#
|
184
|
-
def finder_type_condition(
|
185
|
-
|
229
|
+
def finder_type_condition(*args)
|
230
|
+
options = convert_finder_type_arguments(*args)
|
231
|
+
type = convert_type_options(options[:scope])
|
186
232
|
|
187
233
|
# If we want [:current, :children] or [:current, :children, :parents] and we don't need the finder type condition,
|
188
234
|
# then that means we don't need a condition at all since we are at the top-level sti class and we are essentially
|
@@ -194,7 +240,7 @@ module Tagtical
|
|
194
240
|
|
195
241
|
sti_names = []
|
196
242
|
if type.include?(:current)
|
197
|
-
sti_names <<
|
243
|
+
sti_names << klass.sti_name
|
198
244
|
end
|
199
245
|
if type.include?(:children) && klass
|
200
246
|
sti_names.concat(klass.descendants.map(&:sti_name))
|
@@ -212,7 +258,7 @@ module Tagtical
|
|
212
258
|
cond = sti_column.eq(sti_name)
|
213
259
|
conds.nil? ? cond : conds.or(cond)
|
214
260
|
end
|
215
|
-
|
261
|
+
|
216
262
|
if condition && options[:sql]
|
217
263
|
condition = condition.to_sql
|
218
264
|
condition.insert(0, " AND ") if options[:sql]==:append
|
@@ -220,11 +266,14 @@ module Tagtical
|
|
220
266
|
condition
|
221
267
|
end
|
222
268
|
|
223
|
-
|
224
|
-
|
269
|
+
# Accepts:
|
270
|
+
# scoping(:<=)
|
271
|
+
# scoping(:scoping => :<=)
|
272
|
+
def scoping(*args, &block)
|
273
|
+
finder_type_condition = finder_type_condition(*args)
|
225
274
|
if block_given?
|
226
275
|
if finder_type_condition
|
227
|
-
Tagtical::Tag.send(:with_scope, :find => Tagtical::Tag.where(finder_type_condition), :create => {:type =>
|
276
|
+
Tagtical::Tag.send(:with_scope, :find => Tagtical::Tag.where(finder_type_condition), :create => {:type => klass.sti_name}) do
|
228
277
|
Tagtical::Tag.instance_exec(&block)
|
229
278
|
end
|
230
279
|
else
|
@@ -234,15 +283,14 @@ module Tagtical
|
|
234
283
|
Tagtical::Tag.send(*(finder_type_condition ? [:where, finder_type_condition] : :unscoped))
|
235
284
|
end
|
236
285
|
end
|
237
|
-
|
286
|
+
|
238
287
|
# Return the Tag subclass
|
239
288
|
def klass
|
240
|
-
|
289
|
+
@klass ||= find_tag_class!
|
241
290
|
end
|
242
291
|
|
243
|
-
|
244
|
-
|
245
|
-
klass || Tagtical::Tag
|
292
|
+
def base?
|
293
|
+
BASE==self
|
246
294
|
end
|
247
295
|
|
248
296
|
def has_many_name
|
@@ -250,14 +298,6 @@ module Tagtical
|
|
250
298
|
end
|
251
299
|
alias scope_name has_many_name
|
252
300
|
|
253
|
-
def base?
|
254
|
-
!!klass && klass.descends_from_active_record?
|
255
|
-
end
|
256
|
-
|
257
|
-
def ==(val)
|
258
|
-
super(self.class[val])
|
259
|
-
end
|
260
|
-
|
261
301
|
def tag_list_name(prefix=nil)
|
262
302
|
prefix = prefix.to_s.dup
|
263
303
|
prefix << "_" unless prefix.blank?
|
@@ -271,7 +311,7 @@ module Tagtical
|
|
271
311
|
# Returns the level from which it extends from Tagtical::Tag
|
272
312
|
def active_record_sti_level
|
273
313
|
@active_record_sti_level ||= begin
|
274
|
-
count, current_class = 0, klass
|
314
|
+
count, current_class = 0, klass
|
275
315
|
while !current_class.descends_from_active_record?
|
276
316
|
current_class = current_class.superclass
|
277
317
|
count += 1
|
@@ -285,12 +325,13 @@ module Tagtical
|
|
285
325
|
# Returns an array of potential class names for this specific type.
|
286
326
|
def derive_class_candidates
|
287
327
|
[].tap do |arr|
|
288
|
-
[classify, "#{classify}Tag"
|
328
|
+
suffixes = [classify, "#{classify}Tag", "#{taggable_class}::#{classify}", "#{taggable_class}::#{classify}Tag"]
|
329
|
+
suffixes.each do |name| # support Interest and InterestTag class names.
|
289
330
|
"Tagtical::Tag".tap do |longest_candidate|
|
290
331
|
longest_candidate << "::#{name}" unless name=="Tag"
|
291
332
|
end.scan(/^|::/) { arr << $' } # Klass, Tag::Klass, Tagtical::Tag::Klass
|
292
333
|
end
|
293
|
-
end
|
334
|
+
end.uniq.sort_by { |candidate| -candidate.split("::").size } # more nested classnames first
|
294
335
|
end
|
295
336
|
|
296
337
|
# Take operator types (ie <, >, =) and convert them into :children, :current, or :parents.
|
@@ -306,19 +347,11 @@ module Tagtical
|
|
306
347
|
end.flatten.uniq
|
307
348
|
end
|
308
349
|
|
309
|
-
def find_tag_class
|
310
|
-
|
350
|
+
def find_tag_class!
|
351
|
+
return Tagtical::Tag if base?
|
311
352
|
|
312
|
-
# Attempt to find the preloaded class instead of having to do NameError catching below.
|
313
|
-
candidates.each do |candidate|
|
314
|
-
constants = ActiveSupport::Dependencies::Reference.send(:class_variable_get, :@@constants)
|
315
|
-
if constants.key?(candidate) && (constant = constants[candidate]) <= Tagtical::Tag # must check for key first, do not want to trigger default proc.
|
316
|
-
return constant
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
353
|
# Logic comes from ActiveRecord::Base#compute_type.
|
321
|
-
|
354
|
+
derive_class_candidates.each do |candidate|
|
322
355
|
begin
|
323
356
|
constant = ActiveSupport::Dependencies.constantize(candidate)
|
324
357
|
return constant if candidate == constant.to_s && constant <= Tagtical::Tag
|
@@ -329,9 +362,15 @@ module Tagtical
|
|
329
362
|
end
|
330
363
|
end
|
331
364
|
|
332
|
-
|
365
|
+
raise("Cannot find tag class for type: #{self} with taggable class: #{taggable_class}")
|
333
366
|
end
|
334
|
-
end
|
335
367
|
|
368
|
+
def convert_finder_type_arguments(*args)
|
369
|
+
options = args.extract_options!
|
370
|
+
options[:scope] = args[0] if args[0] # allow for adding this in the front
|
371
|
+
options
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|
336
375
|
end
|
337
376
|
end
|
data/lib/tagtical/tag_list.rb
CHANGED
@@ -1,45 +1,43 @@
|
|
1
1
|
module Tagtical
|
2
2
|
class TagList < Array
|
3
3
|
class TagValue < String
|
4
|
+
|
4
5
|
attr_accessor :relevance
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
cattr_accessor :relevance_delimiter
|
8
|
+
self.relevance_delimiter = ':'
|
9
|
+
|
10
|
+
def initialize(value="", relevance=nil)
|
11
|
+
@relevance = relevance.to_f if relevance
|
12
|
+
super(value)
|
8
13
|
end
|
14
|
+
|
15
|
+
def self.parse(input)
|
16
|
+
new(*input.to_s.split(relevance_delimiter, 2).each(&:strip!))
|
17
|
+
end
|
18
|
+
|
9
19
|
end
|
10
|
-
|
20
|
+
|
11
21
|
cattr_accessor :delimiter
|
12
22
|
self.delimiter = ','
|
13
23
|
|
24
|
+
cattr_accessor :value_quotes
|
25
|
+
self.value_quotes = ["'", "\""]
|
26
|
+
|
14
27
|
attr_accessor :owner
|
15
28
|
|
16
29
|
def initialize(*args)
|
17
|
-
add(*args)
|
30
|
+
add(*args) unless args.empty?
|
18
31
|
end
|
19
|
-
|
32
|
+
|
20
33
|
##
|
21
34
|
# Returns a new TagList using the given tag string.
|
22
35
|
#
|
23
36
|
# Example:
|
24
37
|
# tag_list = TagList.from("One , Two, Three")
|
25
|
-
# tag_list # ["One", "Two", "Three"]
|
26
|
-
def self.from(
|
27
|
-
|
28
|
-
new(string)
|
29
|
-
else
|
30
|
-
glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
|
31
|
-
string = string.join(glue) if string.respond_to?(:join)
|
32
|
-
|
33
|
-
new.tap do |tag_list|
|
34
|
-
string = string.to_s.dup
|
35
|
-
|
36
|
-
# Parse the quoted tags
|
37
|
-
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
38
|
-
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
39
|
-
|
40
|
-
tag_list.add(string.split(delimiter))
|
41
|
-
end
|
42
|
-
end
|
38
|
+
# tag_list # ["One", "Two", "Three"] <=== as TagValue
|
39
|
+
def self.from(*args)
|
40
|
+
new(*args)
|
43
41
|
end
|
44
42
|
|
45
43
|
def concat(values)
|
@@ -57,12 +55,13 @@ module Tagtical
|
|
57
55
|
#
|
58
56
|
# Example:
|
59
57
|
# tag_list.add("Fun", "Happy")
|
60
|
-
# tag_list.add("Fun, Happy"
|
58
|
+
# tag_list.add("Fun, Happy")
|
61
59
|
# tag_list.add("Fun" => "0.546", "Happy" => 0.465) # add relevance
|
62
60
|
def add(*values)
|
63
61
|
extract_and_apply_options!(values)
|
64
|
-
|
65
|
-
|
62
|
+
clean!(values) do
|
63
|
+
concat(values)
|
64
|
+
end
|
66
65
|
self
|
67
66
|
end
|
68
67
|
|
@@ -72,7 +71,7 @@ module Tagtical
|
|
72
71
|
#
|
73
72
|
# Example:
|
74
73
|
# tag_list.remove("Sad", "Lonely")
|
75
|
-
# tag_list.remove("Sad, Lonely"
|
74
|
+
# tag_list.remove("Sad, Lonely")
|
76
75
|
def remove(*values)
|
77
76
|
extract_and_apply_options!(values)
|
78
77
|
delete_if { |value| values.include?(value) }
|
@@ -87,12 +86,13 @@ module Tagtical
|
|
87
86
|
# tag_list = TagList.new("Round", "Square,Cube")
|
88
87
|
# tag_list.to_s # 'Round, "Square,Cube"'
|
89
88
|
def to_s
|
90
|
-
|
91
|
-
|
89
|
+
tag_list = frozen? ? self.dup : self
|
90
|
+
tag_list.send(:clean!)
|
92
91
|
|
93
|
-
|
94
|
-
value.include?(delimiter) ? "
|
95
|
-
|
92
|
+
tag_list.map do |tag_value|
|
93
|
+
value = tag_value.include?(delimiter) ? %{"#{tag_value}"} : tag_value
|
94
|
+
[value, tag_value.relevance].compact.join(TagValue.relevance_delimiter)
|
95
|
+
end.join(delimiter.gsub(/(\S)$/, '\1 '))
|
96
96
|
end
|
97
97
|
|
98
98
|
# Builds an option statement for an ActiveRecord table.
|
@@ -102,31 +102,49 @@ module Tagtical
|
|
102
102
|
end
|
103
103
|
|
104
104
|
private
|
105
|
-
|
105
|
+
|
106
106
|
# Remove whitespace, duplicates, and blanks.
|
107
|
-
def clean!
|
107
|
+
def clean!(values=nil)
|
108
|
+
delete_if { |value| values.include?(value) } if values.present? # Allow editing of relevance
|
109
|
+
yield if block_given?
|
108
110
|
reject!(&:blank?)
|
109
111
|
each(&:strip!)
|
110
112
|
uniq!(&:downcase)
|
111
113
|
end
|
112
114
|
|
113
115
|
def extract_and_apply_options!(args)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
116
|
+
options = args.last.is_a?(Hash) && args.size > 1 ? args.pop : {}
|
117
|
+
options.assert_valid_keys :parse
|
118
|
+
|
119
|
+
args.map! { |a| extract(a, options) }
|
120
|
+
args.flatten!
|
121
|
+
end
|
122
|
+
|
123
|
+
def extract(input, options={})
|
124
|
+
case input
|
125
|
+
when String
|
126
|
+
if !input.include?(delimiter) || options[:parse]==false
|
127
|
+
[input]
|
128
|
+
else
|
129
|
+
input, arr = input.dup, []
|
123
130
|
|
124
|
-
|
131
|
+
# Parse the quoted tags
|
132
|
+
value_quotes.each do |value_quote|
|
133
|
+
input.gsub!(/(\A|#{delimiter})\s*#{value_quote}(.*?)#{value_quote}\s*(#{delimiter}\s*|\z)/) { arr << $2 ; $3 }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Parse the unquoted tags
|
137
|
+
arr.concat(input.split(delimiter).each(&:strip!))
|
138
|
+
end
|
139
|
+
when Hash
|
140
|
+
input.map { |value, relevance| TagValue.new(value, relevance) }
|
141
|
+
when Array
|
142
|
+
input
|
125
143
|
end
|
126
144
|
end
|
127
145
|
|
128
146
|
def convert_tag_value(value)
|
129
|
-
value.is_a?(TagValue) ? value : TagValue.
|
147
|
+
value.is_a?(TagValue) ? value : TagValue.parse(value)
|
130
148
|
end
|
131
149
|
|
132
150
|
end
|
@@ -16,12 +16,12 @@ module Tagtical::Taggable
|
|
16
16
|
|
17
17
|
module ClassMethods
|
18
18
|
def initialize_tagtical_cache
|
19
|
-
tag_types.
|
20
|
-
class_eval
|
19
|
+
tag_types.each do |tag_type|
|
20
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
21
21
|
def self.caching_#{tag_type.singularize}_list?
|
22
22
|
caching_tag_list_on?("#{tag_type}")
|
23
|
-
end
|
24
|
-
|
23
|
+
end
|
24
|
+
RUBY
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
@@ -34,14 +34,13 @@ module Tagtical::Taggable
|
|
34
34
|
column_names.include?("cached_#{context.to_s.singularize}_list")
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
module InstanceMethods
|
39
39
|
def save_cached_tag_list
|
40
40
|
tag_types.each do |tag_type|
|
41
41
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|
42
|
-
if
|
43
|
-
|
44
|
-
self["cached_#{tag_type.singularize}_list"] = list
|
42
|
+
if tag_list = tag_list_cache_on(tag_type)[{}]
|
43
|
+
self[tag_type.tag_list_name(:cached)] = tag_list.to_a.flatten.compact.join(', ')
|
45
44
|
end
|
46
45
|
end
|
47
46
|
end
|
@@ -68,7 +68,7 @@ module Tagtical::Taggable
|
|
68
68
|
taggable_conditions = sanitize_sql(["#{Tagtical::Tagging.table_name}.taggable_type = ?", base_class.name])
|
69
69
|
taggable_conditions << sanitize_sql([" AND #{Tagtical::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
|
70
70
|
|
71
|
-
sti_conditions =
|
71
|
+
sti_conditions = find_tag_type!(options[:on]).finder_type_condition if options[:on]
|
72
72
|
|
73
73
|
tagging_conditions = [
|
74
74
|
taggable_conditions,
|