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 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.3
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 = sti_names.inject(nil) do |conds, sti_name|
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
@@ -37,7 +37,7 @@ module Tagtical
37
37
  # tag_list = TagList.from("One , Two, Three")
38
38
  # tag_list # ["One", "Two", "Three"] <=== as TagValue
39
39
  def self.from(*args)
40
- new(*args)
40
+ args[0].is_a?(self) ? args[0] : new(*args)
41
41
  end
42
42
 
43
43
  def concat(values)
@@ -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
- if tag_list = tag_list_cache_on(tag_type)[[:children, :current]]
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)[scopes_for_tag_list(context, *args)] = tag_list
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)[scopes_for_tag_list(context, *args)] ||= Tagtical::TagList.new(tags_on(context, *args))
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)[scopes_for_tag_list(context, *args)] ||= Tagtical::TagList.new(all_tags_on(context, *args)).freeze
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 |scopes, tag_list|
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
- current_tags = tags_on(tag_type, *[:parents].concat(scopes)) # add in the parents because we need them later on down.
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 scopes_for_tag_list(*args)
323
- finder_type_arguments_for_tag_list(*args)[0].sort
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
- scopes, options = finder_type_arguments_for_tag_list(context, *args)
339
- raise("You must include children if you are cascading") if options[:cascade] && !scopes.include?(:current)
340
- if (cascade = options.delete(:cascade)) && (tag_type = find_tag_type!(context)).klass
341
- tag_types = cascade==true ? self.tag_types : Array(cascade).map { |c| find_tag_type!(c) }
342
- tag_types.each do |t|
343
- if t.klass && t.klass <= tag_type.klass && t.klass.possible_values
344
- new_tag_list = Tagtical::TagList.new
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).tap { |t| t["relevance"] = relevance }
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.tag_list_cache_on(:skill)[[:children, :current]].should be_an_instance_of(Tagtical::TagList)
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.1.3
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-22 00:00:00.000000000Z
12
+ date: 2011-07-23 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &2153720180 !ruby/object:Gem::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: *2153720180
24
+ version_requirements: *2156407420
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &2153719700 !ruby/object:Gem::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: *2153719700
35
+ version_requirements: *2156406940
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3-ruby
38
- requirement: &2153719220 !ruby/object:Gem::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: *2153719220
46
+ version_requirements: *2156406460
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mysql
49
- requirement: &2153718740 !ruby/object:Gem::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: *2153718740
57
+ version_requirements: *2156405980
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &2153718260 !ruby/object:Gem::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: *2153718260
68
+ version_requirements: *2156405500
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rcov
71
- requirement: &2153717780 !ruby/object:Gem::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: *2153717780
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.