tagtical 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -35,7 +35,8 @@ parsed.
35
35
  # would give you a tag of "red, blue" instead of "red" and "blue".
36
36
  tag_list.add("red, blue", :parse => false)
37
37
 
38
- 7. Custom contexts are *not* supported since we use STI.
38
+ 7. Custom contexts are *not* supported since we use STI. That is, you must define a set of tag types
39
+ ahead of time.
39
40
 
40
41
  Additions include:
41
42
  1. Scopes are created on Tag so you can do photo.tags.color and grab all the tags of type Tag::Color, for example.
@@ -108,10 +109,12 @@ rake spec:plugins
108
109
  end
109
110
  end
110
111
 
112
+ # Basic TagList Functionality
111
113
  @user = User.new(:name => "Bobby")
112
114
  @user.tag_list = "awesome, slick, hefty" # this should be familiar
113
- @user.activity_list = "joking, clowning, boxing" # but you can do it for any context!
114
- @user.activity_list # => ["joking","clowning","boxing"] as TagList
115
+ @user.activity_list = "joking, clowning, boxing"
116
+ @user.activity_list # => ["joking","clowning","boxing"] as TagList
117
+ @user.activity_list.to_s # => "joking, clowning, boxing"
115
118
  @user.save
116
119
 
117
120
  # Cascade tag_list setters =)
@@ -122,7 +125,6 @@ rake spec:plugins
122
125
 
123
126
  @user.tags # => [<Tag value:"awesome">,<Tag value:"slick">,<Tag value:"hefty">]
124
127
  @user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Activity value:"boxing">]
125
- @user.activities.first.athletic? # => false
126
128
 
127
129
  @frankie = User.create(:name => "Frankie", :activity_list => "joking, flying, eating")
128
130
  User.activity_counts # => [<Tag::Activity value="joking" count=2>,<Tag::Activity value="clowning" count=1>...]
@@ -132,20 +134,39 @@ rake spec:plugins
132
134
  # from Activity to Sport and give it a relevance of 4.5.
133
135
  @user.save
134
136
 
137
+ # Possible Values Checking
135
138
  @user.sport_list = {"chess"}
136
- @user.save! # <=== will throw an error, chess is not a sport!
139
+ @user.save! # <=== will throw an error, chess is not in possible_values from Tag::Sport
137
140
 
141
+ # Tagging Scopes
138
142
  @user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Sport value:"boxing">]
139
143
  @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
142
- @user.activities(:scope => :<) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
144
+ @user.activities(:children) # => shorthand for above
145
+ @user.tags(:scope => :children, :except => :sports) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">]
146
+ # - filter list by excluding
147
+ @user.tags(:scope => :children, :only => :sports) # => [<Tag::Sport value:"boxing">]
148
+ # - filter list by including
149
+
150
+ @user.activities(:scope => ) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
143
151
  @user.activities(:scope => :current) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
144
- @user.activities(:scope => :==) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
152
+ @user.activities(:scope => :==) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
145
153
 
154
+ # Questioner Methods
146
155
  @user.activities.first.athletic? # => false
147
156
  @user.sports.all(&:ball?) # => true
148
157
 
158
+ # Database Access Optimizations
159
+ ** Sequence 1
160
+ @user.tags.to_a # load in the tags
161
+ @user.sports.to_a # will not trigger a database hit, instead will get the sports tags off tags
162
+ @user.activities.to_a # same as above
163
+
164
+ ** Sequence 2
165
+ @user.activities.to_a # load in the tags
166
+ @user.sports.to_a # will not trigger a database hit, instead will get the sports tags off activities, since it inherits from activities
167
+ @user.sports(:conditions => "value='Soccer'") # will actually hit the database if arguments are passed in.
168
+
169
+
149
170
 
150
171
  --- Defining Subclasses
151
172
 
@@ -196,8 +217,8 @@ compatibility with the will_paginate gem:
196
217
  named_scope :by_join_date, :order => "created_at DESC"
197
218
  end
198
219
 
199
- User.tagged_with("awesome").by_date
200
- User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
220
+ User.tagged_with("awesome").by_join_date
221
+ User.tagged_with("awesome").by_join_date.paginate(:page => params[:page], :per_page => 20)
201
222
 
202
223
  # Find a user with matching all tags, not just one
203
224
  User.tagged_with(["awesome", "cool"], :match_all => :true)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.3.0
data/lib/tagtical/tag.rb CHANGED
@@ -232,17 +232,17 @@ module Tagtical
232
232
  # <tt>only</tt> - An array of the following: :parents, :current, :children. Will construct conditions to query the current, parent, and/or children STI classes.
233
233
  #
234
234
  def finder_type_condition(*args)
235
- scopes, options = convert_finder_type_arguments(*args)
235
+ sql = args[-1].is_a?(Hash) && args[-1].delete(:sql)
236
236
 
237
237
  sti_column = Tagtical::Tag.arel_table[Tagtical::Tag.inheritance_column]
238
- condition = expand_tag_types(scopes, options).map { |x| x.klass.sti_name }.inject(nil) do |conds, sti_name|
238
+ condition = expand_tag_types(*args).map { |x| x.klass.sti_name }.inject(nil) do |conds, sti_name|
239
239
  cond = sti_column.eq(sti_name)
240
240
  conds.nil? ? cond : conds.or(cond)
241
241
  end
242
242
 
243
- if condition && options[:sql]
243
+ if condition && sql
244
244
  condition = condition.to_sql
245
- condition.insert(0, " AND ") if options[:sql]==:append
245
+ condition.insert(0, " AND ") if sql==:append
246
246
  end
247
247
  condition
248
248
  end
@@ -301,6 +301,38 @@ module Tagtical
301
301
  end
302
302
  end
303
303
 
304
+ def expand_tag_types(*args)
305
+ scopes, options = convert_finder_type_arguments(*args)
306
+ classes, types = [], []
307
+
308
+ types.concat(Array(options[:types]).map { |t| taggable_class.find_tag_type!(t) }) if options[:types]
309
+
310
+ if scopes.include?(:current)
311
+ classes << klass
312
+ end
313
+ if scopes.include?(:children)
314
+ classes.concat(klass.descendants)
315
+ end
316
+ if scopes.include?(:parents) # include searches up the STI chain
317
+ parent_class = klass.superclass
318
+ while parent_class <= Tagtical::Tag
319
+ classes << parent_class
320
+ parent_class = parent_class.superclass
321
+ end
322
+ end
323
+
324
+ if options[:only]
325
+ classes &= find_tag_classes_for(options[:only])
326
+ elsif options[:except]
327
+ except = find_tag_classes_for(options[:except])
328
+ classes.reject! { |t| except.any? { |e| t <= e }}
329
+ end
330
+ tag_types_by_classes = taggable_class.tag_types.index_by(&:klass)
331
+ types.concat(classes.map { |k| tag_types_by_classes[k] }.uniq.compact)
332
+
333
+ types # for clarity
334
+ end
335
+
304
336
  private
305
337
 
306
338
  # Returns an array of potential class names for this specific type.
@@ -358,37 +390,7 @@ module Tagtical
358
390
  [scopes, options]
359
391
  end
360
392
 
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)
393
+ def find_tag_classes_for(input)
392
394
  Array(input).map { |o| taggable_class.find_tag_type!(o).klass }
393
395
  end
394
396
 
@@ -72,7 +72,14 @@ module Tagtical::Taggable
72
72
  alias_method_chain :tags, :finder_type_options
73
73
  else
74
74
  define_method(tag_type.has_many_name) do |*args|
75
- tags.scoped.merge(tag_type.scoping(*args))
75
+ tags.scoped.merge(tag_type.scoping(*args)).tap do |scope|
76
+ if args.empty? &&
77
+ (loaded_parent_scope = tag_type.expand_tag_types(:parents).map { |t| send(t.has_many_name) }.detect(&:loaded?))
78
+
79
+ scope.instance_variable_set(:@loaded, true)
80
+ scope.instance_variable_set(:@records, loaded_parent_scope.select { |t| t.class <= tag_type.klass })
81
+ end
82
+ end
76
83
  end
77
84
  end
78
85
  end
@@ -321,15 +328,8 @@ module Tagtical::Taggable
321
328
  self.class.find_tag_type!(input)
322
329
  end
323
330
 
324
- def finder_type_arguments_for_tag_list(input, *args)
325
- find_tag_type!(input).send(:convert_finder_type_arguments, *args)
326
- end
327
-
328
331
  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
332
+ (@expand_tag_types ||= {})[[input, args]] ||= find_tag_type!(input).expand_tag_types(*args)
333
333
  end
334
334
 
335
335
  # Lets say tag class A inherits from B and B has a tag with value "foo". If we tag A with value "foo",
@@ -341,26 +341,6 @@ module Tagtical::Taggable
341
341
  end
342
342
  end
343
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
-
364
344
  # If cascade tag types are specified, it will attempt to look at Tag subclasses with
365
345
  # possible_values and try to set those tag_lists with values from the possible_values list.
366
346
  def cascade_set_tag_list!(tag_list, context, *args)
@@ -53,6 +53,36 @@ describe Tagtical::Taggable do
53
53
  @taggable.should have(2).skills
54
54
  end
55
55
 
56
+ describe "Tag Type Scopes" do
57
+ before do
58
+ @taggable.update_attributes!(:tag_list => "tree, train", :skill_list => "basketball", :craft_list => "pottery")
59
+ @taggable.reload
60
+ end
61
+
62
+ describe "inherited tags scope optimizations" do
63
+ before do
64
+ @taggable.tags.to_a # load the target
65
+ end
66
+
67
+ it "should not access the database when top level tags are already loaded" do
68
+ ActiveRecord::Base.connection.expects(:execute).never
69
+ @taggable.skills.to_a
70
+ @taggable.crafts.to_a
71
+ end
72
+
73
+ it "should select the correct tags" do
74
+ @taggable.skills.each { |tag| tag.should be_skill }
75
+ @taggable.crafts.each { |tag| tag.should be_craft }
76
+ end
77
+
78
+ it "should access the database when args are passed in" do
79
+ ActiveRecord::Base.connection.expects(:execute).once.returns([])
80
+ @taggable.skills(:conditions => "value='Foo'").to_a
81
+ end
82
+
83
+ end
84
+ end
85
+
56
86
  when_possible_values_specified(:values => %w{Knitting Ruby Pottery}) do
57
87
 
58
88
  before do
@@ -67,7 +97,7 @@ describe Tagtical::Taggable do
67
97
  @taggable.craft_list.add("ruby", "pottery")
68
98
  @taggable.save!
69
99
  @taggable.reload
70
- should have_only_tag_values %w{Knitting Ruby Pottery}
100
+ @taggable.should have_only_tag_values %w{Knitting Ruby Pottery}
71
101
  end
72
102
 
73
103
  end
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.2.0
4
+ version: 1.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2011-07-23 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &2156407420 !ruby/object:Gem::Requirement
16
+ requirement: &2152023000 !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: *2156407420
24
+ version_requirements: *2152023000
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &2156406940 !ruby/object:Gem::Requirement
27
+ requirement: &2152022520 !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: *2156406940
35
+ version_requirements: *2152022520
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3-ruby
38
- requirement: &2156406460 !ruby/object:Gem::Requirement
38
+ requirement: &2152022040 !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: *2156406460
46
+ version_requirements: *2152022040
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mysql
49
- requirement: &2156405980 !ruby/object:Gem::Requirement
49
+ requirement: &2152021560 !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: *2156405980
57
+ version_requirements: *2152021560
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: jeweler
60
- requirement: &2156405500 !ruby/object:Gem::Requirement
60
+ requirement: &2152021080 !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: *2156405500
68
+ version_requirements: *2152021080
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rcov
71
- requirement: &2156405020 !ruby/object:Gem::Requirement
71
+ requirement: &2152020600 !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: *2156405020
79
+ version_requirements: *2152020600
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.