tagtical 1.2.0 → 1.3.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
@@ -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.