tagtical 1.1.3 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -0
- data/VERSION +1 -1
- data/lib/tagtical/tag.rb +41 -24
- data/lib/tagtical/tag_list.rb +1 -1
- data/lib/tagtical/taggable/cache.rb +1 -3
- data/lib/tagtical/taggable/core.rb +48 -24
- data/spec/tagtical/tag_list_spec.rb +1 -1
- data/spec/tagtical/taggable_spec.rb +35 -5
- metadata +14 -14
data/README.rdoc
CHANGED
@@ -115,6 +115,7 @@ rake spec:plugins
|
|
115
115
|
@user.save
|
116
116
|
|
117
117
|
# Cascade tag_list setters =)
|
118
|
+
# It will look at the "possible_values" if provided, and stuff the tags down at that level.
|
118
119
|
@user.set_activity_list(["clowning", "boxing"], :cascade => true)
|
119
120
|
@user.save!
|
120
121
|
@user.sport_list # => ["boxing"]
|
@@ -136,6 +137,8 @@ rake spec:plugins
|
|
136
137
|
|
137
138
|
@user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Sport value:"boxing">]
|
138
139
|
@user.activities(:scope => :children) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
|
140
|
+
@user.tags(:scope => :children, :except => :sports) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - filter list
|
141
|
+
@user.tags(:scope => :children, :only => :sports) # => [<Tag::Sport value:"boxing">] - filter list
|
139
142
|
@user.activities(:scope => :<) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
|
140
143
|
@user.activities(:scope => :current) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
|
141
144
|
@user.activities(:scope => :==) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
data/lib/tagtical/tag.rb
CHANGED
@@ -105,10 +105,15 @@ module Tagtical
|
|
105
105
|
super || (object.is_a?(self.class) && value == object.value)
|
106
106
|
end
|
107
107
|
|
108
|
+
# Relevance is transferred through "taggings" join.
|
108
109
|
def relevance
|
109
110
|
(v = self[:relevance]) && v.to_f
|
110
111
|
end
|
111
112
|
|
113
|
+
def relevance=(relevance)
|
114
|
+
self[:relevance] = relevance
|
115
|
+
end
|
116
|
+
|
112
117
|
# Try to sort by the relevance if provided.
|
113
118
|
def <=>(tag)
|
114
119
|
if (r1 = relevance) && (r2 = tag.relevance)
|
@@ -229,31 +234,8 @@ module Tagtical
|
|
229
234
|
def finder_type_condition(*args)
|
230
235
|
scopes, options = convert_finder_type_arguments(*args)
|
231
236
|
|
232
|
-
# If we want [:current, :children] or [:current, :children, :parents] and we don't need the finder type condition,
|
233
|
-
# then that means we don't need a condition at all since we are at the top-level sti class and we are essentially
|
234
|
-
# searching the whole range of sti classes.
|
235
|
-
if klass && !klass.finder_needs_type_condition?
|
236
|
-
scopes.delete(:parents) # we are at the topmost level.
|
237
|
-
scopes = [] if scopes.sort==[:current, :children].sort # no condition is required if we want the current AND the children.
|
238
|
-
end
|
239
|
-
|
240
|
-
sti_names = []
|
241
|
-
if scopes.include?(:current)
|
242
|
-
sti_names << klass.sti_name
|
243
|
-
end
|
244
|
-
if scopes.include?(:children) && klass
|
245
|
-
sti_names.concat(klass.descendants.map(&:sti_name))
|
246
|
-
end
|
247
|
-
if scopes.include?(:parents) && klass # include searches up the STI chain
|
248
|
-
parent_class = klass.superclass
|
249
|
-
while parent_class <= Tagtical::Tag
|
250
|
-
sti_names << parent_class.sti_name
|
251
|
-
parent_class = parent_class.superclass
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
237
|
sti_column = Tagtical::Tag.arel_table[Tagtical::Tag.inheritance_column]
|
256
|
-
condition =
|
238
|
+
condition = expand_tag_types(scopes, options).map { |x| x.klass.sti_name }.inject(nil) do |conds, sti_name|
|
257
239
|
cond = sti_column.eq(sti_name)
|
258
240
|
conds.nil? ? cond : conds.or(cond)
|
259
241
|
end
|
@@ -372,9 +354,44 @@ module Tagtical
|
|
372
354
|
def convert_finder_type_arguments(*args)
|
373
355
|
options = args.extract_options!
|
374
356
|
scopes = convert_scope_options(args.presence || options[:scope])
|
357
|
+
scopes.delete(:parents) if klass && !klass.finder_needs_type_condition? # we are at the topmost level.
|
375
358
|
[scopes, options]
|
376
359
|
end
|
377
360
|
|
361
|
+
def expand_tag_types(scopes, options)
|
362
|
+
classes, types = [], []
|
363
|
+
types.concat(Array(options[:types]).map { |t| taggable_class.find_tag_type!(t) }) if options[:types]
|
364
|
+
|
365
|
+
if scopes.include?(:current)
|
366
|
+
classes << klass
|
367
|
+
end
|
368
|
+
if scopes.include?(:children)
|
369
|
+
classes.concat(klass.descendants)
|
370
|
+
end
|
371
|
+
if scopes.include?(:parents) # include searches up the STI chain
|
372
|
+
parent_class = klass.superclass
|
373
|
+
while parent_class <= Tagtical::Tag
|
374
|
+
classes << parent_class
|
375
|
+
parent_class = parent_class.superclass
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
if options[:only]
|
380
|
+
classes &= find_tag_types(options[:only])
|
381
|
+
elsif options[:except]
|
382
|
+
except = find_tag_types(options[:except])
|
383
|
+
classes.reject! { |t| except.any? { |e| t <= e }}
|
384
|
+
end
|
385
|
+
tag_types_by_classes = taggable_class.tag_types.index_by(&:klass)
|
386
|
+
types.concat(classes.map { |k| tag_types_by_classes[k] }.uniq.compact)
|
387
|
+
|
388
|
+
types # for clarity
|
389
|
+
end
|
390
|
+
|
391
|
+
def find_tag_types(input)
|
392
|
+
Array(input).map { |o| taggable_class.find_tag_type!(o).klass }
|
393
|
+
end
|
394
|
+
|
378
395
|
end
|
379
396
|
end
|
380
397
|
end
|
data/lib/tagtical/tag_list.rb
CHANGED
@@ -39,9 +39,7 @@ module Tagtical::Taggable
|
|
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
|
-
|
43
|
-
self[tag_type.tag_list_name(:cached)] = tag_list.to_s
|
44
|
-
end
|
42
|
+
self[tag_type.tag_list_name(:cached)] = tag_list_on(tag_type).to_s if tag_list_on?(tag_type)
|
45
43
|
end
|
46
44
|
end
|
47
45
|
|
@@ -82,7 +82,7 @@ module Tagtical::Taggable
|
|
82
82
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
83
83
|
end
|
84
84
|
|
85
|
-
def find_tag_type!(input)
|
85
|
+
def find_tag_type!(input, options={})
|
86
86
|
(@tag_type ||= {})[input] ||= tag_types.find { |t| t.match?(input) } || raise("Cannot find tag type:'#{input}' in #{tag_types.inspect}")
|
87
87
|
end
|
88
88
|
|
@@ -205,12 +205,16 @@ module Tagtical::Taggable
|
|
205
205
|
# model.set_tag_list_on("skill", ["kung fu", "karate"], :scope => :==) # will not overwrite tags from inheriting tag classes
|
206
206
|
def set_tag_list_on(context, new_list, *args)
|
207
207
|
tag_list = Tagtical::TagList.from(new_list)
|
208
|
-
cascade_set_tag_list!(tag_list, context, *args)
|
209
|
-
tag_list_cache_on(context)[
|
208
|
+
cascade_set_tag_list!(tag_list, context, *args) if args[-1].is_a?(Hash) && args[-1].delete(:cascade)
|
209
|
+
tag_list_cache_on(context)[expand_tag_types(context, *args)] = tag_list
|
210
|
+
end
|
211
|
+
|
212
|
+
def tag_list_on?(context, *args)
|
213
|
+
!tag_list_cache_on(context)[expand_tag_types(context, *args)].nil?
|
210
214
|
end
|
211
215
|
|
212
216
|
def tag_list_on(context, *args)
|
213
|
-
tag_list_cache_on(context)[
|
217
|
+
tag_list_cache_on(context)[expand_tag_types(context, *args)] ||= Tagtical::TagList.new(tags_on(context, *args))
|
214
218
|
end
|
215
219
|
|
216
220
|
def tag_list_cache_on(context, prefix=nil)
|
@@ -219,7 +223,7 @@ module Tagtical::Taggable
|
|
219
223
|
end
|
220
224
|
|
221
225
|
def all_tags_list_on(context, *args)
|
222
|
-
tag_list_cache_on(context, :all)[
|
226
|
+
tag_list_cache_on(context, :all)[expand_tag_types(context, *args)] ||= Tagtical::TagList.new(all_tags_on(context, *args)).freeze
|
223
227
|
end
|
224
228
|
|
225
229
|
##
|
@@ -254,19 +258,21 @@ module Tagtical::Taggable
|
|
254
258
|
# Do the classes from top to bottom. We want the list from "tag" to run before "sub_tag" runs.
|
255
259
|
# Otherwise, we will end up removing taggings from "sub_tag" since they aren't on "tag'.
|
256
260
|
tag_types.sort_by(&:active_record_sti_level).each do |tag_type|
|
257
|
-
(tag_list_cache_on(tag_type) || {}).each do |
|
261
|
+
(tag_list_cache_on(tag_type) || {}).each do |expanded_tag_types, tag_list|
|
258
262
|
# Tag list saving only runs if its affecting the current scope or the current and children scope
|
259
|
-
next unless [:<=, :==].any? { |scope| scopes_for_tag_list(tag_type, scope)==scopes }
|
263
|
+
# next unless [:<=, :==].any? { |scope| scopes_for_tag_list(tag_type, scope)==scopes }
|
264
|
+
next unless expanded_tag_types.include?(tag_type)
|
260
265
|
tag_list = tag_list.uniq
|
261
266
|
|
262
267
|
# Find existing tags or create non-existing tags:
|
263
268
|
tag_value_lookup = tag_type.scoping { find_or_create_tags(tag_list) }
|
264
269
|
tags = tag_value_lookup.keys
|
265
270
|
|
266
|
-
|
271
|
+
|
272
|
+
current_tags = tags_on(tag_type, :types => expanded_tag_types, :scope => :parents) # add in the parents because we need them later on down.
|
267
273
|
old_tags = current_tags - tags
|
268
274
|
new_tags = tags - current_tags
|
269
|
-
|
275
|
+
|
270
276
|
unowned_taggings = taggings.where(:tagger_id => nil)
|
271
277
|
|
272
278
|
# If relevances are specified on current tags, make sure to update those
|
@@ -319,8 +325,11 @@ module Tagtical::Taggable
|
|
319
325
|
find_tag_type!(input).send(:convert_finder_type_arguments, *args)
|
320
326
|
end
|
321
327
|
|
322
|
-
def
|
323
|
-
|
328
|
+
def expand_tag_types(input, *args)
|
329
|
+
(@expand_tag_types ||= {})[[input, args]] ||= begin
|
330
|
+
scopes, options = finder_type_arguments_for_tag_list(input, *args)
|
331
|
+
find_tag_type!(input).send(:expand_tag_types, scopes, options)
|
332
|
+
end
|
324
333
|
end
|
325
334
|
|
326
335
|
# Lets say tag class A inherits from B and B has a tag with value "foo". If we tag A with value "foo",
|
@@ -332,24 +341,39 @@ module Tagtical::Taggable
|
|
332
341
|
end
|
333
342
|
end
|
334
343
|
|
344
|
+
# Extracts the valid tag types for the cascade option.
|
345
|
+
def extract_tag_types_from_cascade(input, base_tag_type)
|
346
|
+
case input
|
347
|
+
when Hash
|
348
|
+
if except = input[:except]
|
349
|
+
except = extract_tag_types_from_cascade(except, base_tag_type)
|
350
|
+
tag_types.reject { |t| except.any? { |e| t.klass <= e.klass }} # remove children as well.
|
351
|
+
elsif only = input[:only]
|
352
|
+
extract_tag_types_from_cascade(only, base_tag_type)
|
353
|
+
else raise("Please provide :except or :only")
|
354
|
+
end
|
355
|
+
when true
|
356
|
+
tag_types
|
357
|
+
else
|
358
|
+
Array(input).map { |c| find_tag_type!(c) }
|
359
|
+
end.select do |tag_type|
|
360
|
+
tag_type.klass <= base_tag_type.klass && tag_type.klass.possible_values
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
335
364
|
# If cascade tag types are specified, it will attempt to look at Tag subclasses with
|
336
365
|
# possible_values and try to set those tag_lists with values from the possible_values list.
|
337
366
|
def cascade_set_tag_list!(tag_list, context, *args)
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
tag_list.reject! do |tag_value|
|
346
|
-
if value = t.klass.detect_possible_value(tag_value)
|
347
|
-
new_tag_list << Tagtical::TagList::TagValue.new(value, tag_value.relevance)
|
348
|
-
true
|
349
|
-
end
|
367
|
+
expand_tag_types(context, *args).each do |tag_type|
|
368
|
+
if tag_type.klass.possible_values
|
369
|
+
new_tag_list = Tagtical::TagList.new
|
370
|
+
tag_list.reject! do |tag_value|
|
371
|
+
if value = tag_type.klass.detect_possible_value(tag_value)
|
372
|
+
new_tag_list << Tagtical::TagList::TagValue.new(value, tag_value.relevance)
|
373
|
+
true
|
350
374
|
end
|
351
|
-
tag_list_cache_on(t)[[:current]] = new_tag_list if !new_tag_list.empty?
|
352
375
|
end
|
376
|
+
set_tag_list_on(tag_type, new_tag_list, :current) if !new_tag_list.empty?
|
353
377
|
end
|
354
378
|
end
|
355
379
|
end
|
@@ -85,7 +85,7 @@ describe Tagtical::TagList do
|
|
85
85
|
|
86
86
|
it "should be able to add a list of tags" do
|
87
87
|
tags = [["foo", 1],["bar", 1.3],["car", 0.7]].map do |value, relevance|
|
88
|
-
Tagtical::Tag.new(:value => value
|
88
|
+
Tagtical::Tag.new(:value => value, :relevance => relevance)
|
89
89
|
end
|
90
90
|
@tag_list.add(tags)
|
91
91
|
tags.each do |tag|
|
@@ -24,7 +24,7 @@ describe Tagtical::Taggable do
|
|
24
24
|
|
25
25
|
it "should be able to create tags" do
|
26
26
|
@taggable.skill_list = "ruby, rails, css"
|
27
|
-
@taggable.
|
27
|
+
@taggable.tag_list_on(:skills).should be_an_instance_of(Tagtical::TagList)
|
28
28
|
|
29
29
|
lambda { @taggable.save }.should change(Tagtical::Tag, :count).by(3)
|
30
30
|
|
@@ -90,7 +90,7 @@ describe Tagtical::Taggable do
|
|
90
90
|
@taggable.tag_list.should have_same_elements %w{Ruby plain}
|
91
91
|
end
|
92
92
|
|
93
|
-
it "should set value on skill" do
|
93
|
+
it "should set value on skill even if different case" do
|
94
94
|
@taggable.skills.should have_only_tag_values %w{Ruby}
|
95
95
|
end
|
96
96
|
|
@@ -103,10 +103,9 @@ describe Tagtical::Taggable do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
end
|
106
|
-
|
107
|
-
context "when :cascade => :craft" do
|
106
|
+
context "when :cascade only on :craft" do
|
108
107
|
before do
|
109
|
-
@taggable.set_tag_list(["ruby", "plain"], :cascade => :craft)
|
108
|
+
@taggable.set_tag_list(["ruby", "plain"], :cascade => true, :types => :craft)
|
110
109
|
@taggable.save!
|
111
110
|
@taggable.reload
|
112
111
|
end
|
@@ -124,6 +123,37 @@ describe Tagtical::Taggable do
|
|
124
123
|
end
|
125
124
|
|
126
125
|
end
|
126
|
+
context "Adding tags with Exclusion" do
|
127
|
+
|
128
|
+
before do
|
129
|
+
@taggable.set_tag_list "Ruby, plain", :cascade => true, :except => :skill
|
130
|
+
@taggable.save!
|
131
|
+
@taggable.reload
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should not change the tags in skill" do
|
135
|
+
@taggable.skill_list.should have_same_elements ["basketball", "pottery"]
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should have set the values at the tag level" do
|
139
|
+
@taggable.tag_list(:current).should have_same_elements ["Ruby", "plain"]
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
context "Getting tag_list with :except" do
|
144
|
+
|
145
|
+
before do
|
146
|
+
@taggable.set_tag_list "Ruby, plain", :cascade => true
|
147
|
+
@taggable.save!
|
148
|
+
@taggable.reload
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should exclude all defined types" do
|
152
|
+
@taggable.tag_list(:except => :skill).should have(1).item
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
127
157
|
end
|
128
158
|
end
|
129
159
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tagtical
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-07-
|
12
|
+
date: 2011-07-23 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement: &
|
16
|
+
requirement: &2156407420 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - <=
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.0.5
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2156407420
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &2156406940 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - <=
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 2.6.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2156406940
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: sqlite3-ruby
|
38
|
-
requirement: &
|
38
|
+
requirement: &2156406460 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2156406460
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: mysql
|
49
|
-
requirement: &
|
49
|
+
requirement: &2156405980 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2156405980
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &2156405500 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :runtime
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *2156405500
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rcov
|
71
|
-
requirement: &
|
71
|
+
requirement: &2156405020 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *2156405020
|
80
80
|
description: Tagtical allows you do create subclasses for Tag and add additional functionality
|
81
81
|
in an STI fashion. For example. You could do Tag::Color.find_by_name('blue').to_rgb.
|
82
82
|
It also supports storing weights or relevance on the taggings.
|