tagtical 1.0.8 → 1.1.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.
- 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,
|