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 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. However, a tag subclass is not required! If you have many custom tag types that you
17
- create on the fly, you can wait until a later time to create a tag subclass.
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(:type => :children) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
119
- @user.activities(:type => :<) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
120
- @user.activities(:type => :current) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
121
- @user.activities(:type => :==) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
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.8
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
- options = args.extract_options!
10
- options[:type] = args[0] if args[0]
11
- Type[context].scoping(options)
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 = where_any_like(tag_list).all
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
- return @sti_name if instance_variable_defined?(:@sti_name)
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
- @@compute_type ||= {}
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
- def comparable_value(str)
89
- RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
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
- # Try to sort by the relevance if provided.
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
- lambda = (klass = Type.new(method_name[0..-2]).klass) ?
137
- lambda { is_a?(klass) } :
138
- lambda { method_name[0..-2]==type }
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
- return input.map { |c| self[c] } if input.is_a?(Array)
160
- input.is_a?(self) ? input : new(sanitize(input))
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
- # The STI name for the Tag model is the same as the tag type.
173
- def to_sti_name
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(options={})
185
- type = convert_type_options(options[:type])
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 << (klass ? klass.sti_name : to_sti_name)
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
- def scoping(options={}, &block)
224
- finder_type_condition = finder_type_condition(options)
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 => self}) do
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
- instance_variable_get(:@klass) || instance_variable_set(:@klass, find_tag_class)
289
+ @klass ||= find_tag_class!
241
290
  end
242
291
 
243
- # Return the Tag class or return top-level
244
- def klass!
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"].each do |name| # support Interest and InterestTag class names.
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
- candidates = derive_class_candidates
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
- candidates.each do |candidate|
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
- nil
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
@@ -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
- def initialize(value, relevance=nil)
6
- @relevance = relevance
7
- super(value.to_s)
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(string)
27
- if string.is_a?(Hash)
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", :parse => true)
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
- concat(values)
65
- clean!
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", :parse => true)
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
- tags = frozen? ? self.dup : self
91
- tags.send(:clean!)
89
+ tag_list = frozen? ? self.dup : self
90
+ tag_list.send(:clean!)
92
91
 
93
- tags.map do |value|
94
- value.include?(delimiter) ? "\"#{value}\"" : value
95
- end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
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
- if args.size==1 && args[0].is_a?(Hash)
115
- args.replace(args[0].map { |value, relevance| TagValue.new(value, relevance) })
116
- else
117
- options = args.last.is_a?(Hash) ? args.pop : {}
118
- options.assert_valid_keys :parse
119
-
120
- if options[:parse]
121
- args.map! { |a| self.class.from(a) }
122
- end
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
- args.flatten!
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.new(value)
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.map(&:to_s).each do |tag_type|
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 tag_list_cache_set_on?(tag_type)
43
- list = tag_list_cache_on(tag_type.singularize).to_a.flatten.compact.join(', ')
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 = Tagtical::Tag::Type[options[:on]].finder_type_condition if options[:on]
71
+ sti_conditions = find_tag_type!(options[:on]).finder_type_condition if options[:on]
72
72
 
73
73
  tagging_conditions = [
74
74
  taggable_conditions,