tagtical 1.1.3 → 1.2.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 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.