rocket_tag 0.5.3 → 0.5.4

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.md CHANGED
@@ -52,6 +52,10 @@ Match all tags on a specific context
52
52
  Match a miniumum number of tags
53
53
 
54
54
  TaggableModel.tagged_with ["math", "kiting", "coding", "sleeping"], :min => 2, :on => "skills"
55
+
56
+ Match tags to specific contexts
57
+
58
+ TaggableModel.tagged_with { :skills => ["math", "kiting"], :languages => ["english", "german"]
55
59
 
56
60
  Take advantage of the tags_count synthetic column returned with every query
57
61
 
@@ -61,6 +65,21 @@ Mix with active relation
61
65
 
62
66
  TaggableModel.tagged_with(["forking", "kiting"]).where( ["created_at > ?", Time.zone.now.ago(5.hours)])
63
67
 
68
+ or even downstream
69
+
70
+ User.where{email="bradphelan@xtargets.com"}.documents.tagged_with ['kiting', 'math'] , :on => :skills
71
+
72
+ where we might have
73
+
74
+ class User < ActiveRecord::Base
75
+ has_many :documents
76
+ end
77
+
78
+ class Document < ActiveRecord::Base
79
+ belongs_to :user
80
+ attr_taggable :tags
81
+ end
82
+
64
83
  Find similar models based on tags on a specific context and return in decending order
65
84
  of 'tags_count'
66
85
 
@@ -79,30 +98,24 @@ of 'tags_count'. Note that each tag is still scoped according to it's context
79
98
 
80
99
  model.tagged_similar
81
100
 
82
- For reference the SQL generated for model.tagged_similar when there are
83
- context [:skills, :languages] available is
84
-
85
- SELECT "taggable_models".* FROM
86
- (
87
- SELECT COUNT("taggable_models"."id") AS tags_count,
88
- taggable_models.*
89
- FROM "taggable_models"
90
- INNER JOIN "taggings"
91
- ON "taggings"."taggable_id" = "taggable_models"."id"
92
- AND "taggings"."taggable_type" = 'TaggableModel'
93
- INNER JOIN "tags"
94
- ON "tags"."id" = "taggings"."tag_id"
95
- WHERE "taggable_models"."id" != 2
96
- AND (( ( "tags"."name" IN ( 'german', 'french' ) AND "taggings"."context" = 'languages' )
97
- OR ( "tags"."name" IN ( 'a', 'b', 'x' ) AND "taggings"."context" = 'skills' )
98
- ))
99
- GROUP BY "taggable_models"."id"
100
- ORDER BY tags_count DESC
101
- ) taggable_models
102
-
103
-
104
- Note the aliasing of the inner select to shield the GROUP BY from downstream active relation
105
- queries
101
+ Find popular tags and generate tags clouds for specific scopes
102
+
103
+ User.where{email="bradphelan@xtargets.com"}.documents.popular_tags
104
+
105
+ where we might have
106
+
107
+ class User < ActiveRecord::Base
108
+ has_many :documents
109
+ end
110
+
111
+ class Document < ActiveRecord::Base
112
+ belongs_to :user
113
+ attr_taggable :tags
114
+ end
115
+
116
+ and you can access the field *tags_count* on each Tag instance returned
117
+ by the above query. Generating the CSS and html for your tag cloud
118
+ is outside the scope of this project but it should be easy to do.
106
119
 
107
120
  == Contributing to rocket_tag
108
121
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.3
1
+ 0.5.4
@@ -24,9 +24,6 @@ module Squeel
24
24
  group { cn.map { |col| __send__(col) } }
25
25
  end
26
26
 
27
- def exists
28
- end
29
-
30
27
  end
31
28
  end
32
29
  end
@@ -113,28 +110,21 @@ module RocketTag
113
110
  @contexts[context.to_sym] || []
114
111
  end
115
112
 
113
+ # Find models with similar tags to the
114
+ # current model ordered is decending
115
+ # order of the number of matches
116
116
  def tagged_similar options = {}
117
117
  context = options.delete :on
118
118
 
119
- contexts = self.class.normalize_contexts(context, self.class.rocket_tag.contexts)
120
-
121
- condition = contexts.reject do |c|
122
- tags_for_context(c).size == 0
123
- end.map do |context|
124
- self.class.squeel do
125
- tags.name.in(my{tags_for_context(context)}) &
126
- (taggings.context == context.to_s)
127
- end
128
- end.inject do |s, t|
129
- s | t
130
- end
131
-
132
- r = self.class.
133
- joins{tags}.
134
- where{condition}.
135
- where{~id != my{id}}
119
+ contexts = self.class.normalize_contexts context,
120
+ self.class.rocket_tag.contexts
136
121
 
137
- self.class.count_tags(r)
122
+ q = self.class.tagged_with Hash[*contexts.map{|c|
123
+ [c, tags_for_context(c)]
124
+ }.flatten(1)]
125
+
126
+ # Exclude self from the results
127
+ q.where{id!=my{id}}
138
128
 
139
129
  end
140
130
  end
@@ -156,7 +146,7 @@ module RocketTag
156
146
  #
157
147
  # rel can be passed as the last expression in a block
158
148
  # if desired.
159
- def count_tags(rel=nil)
149
+ def tags_count(type=self)
160
150
  if block_given?
161
151
  rel = yield
162
152
  end
@@ -164,7 +154,7 @@ module RocketTag
164
154
  select{count(~id).as(tags_count)}.
165
155
  group_by_all_columns.
166
156
  order("tags_count DESC").
167
- isolate_group_by_as(self)
157
+ isolate_group_by_as(type)
168
158
  end
169
159
 
170
160
  def is_valid_context? context
@@ -190,7 +180,7 @@ module RocketTag
190
180
  # Verify contexts are valid for the taggable type
191
181
  def validate_contexts contexts
192
182
  contexts.each do |context|
193
- unless self.is_valid_context? context
183
+ unless is_valid_context? context
194
184
  raise Exception.new("#{context} is not a valid tag context for #{self}")
195
185
  end
196
186
  end
@@ -217,54 +207,95 @@ module RocketTag
217
207
 
218
208
  end
219
209
 
210
+ def tagged_with tags_list, options = {}
220
211
 
221
- # Generates a sifter or a where clause depending on options.
222
- # The sifter generates a subselect with the body of the
223
- # clause wrapped up so that it can be used as a condition
224
- # within another squeel statement.
225
- #
226
- # Query optimization is left up to the SQL engine.
227
- def tagged_with_sifter tags_list, options = {}
228
- options[:sifter] = true
229
- tagged_with tags_list, options
230
- end
212
+ # Grab table name
213
+ t = self.to_s
231
214
 
232
- # Generates a query that provides the matches
233
- # along with an extra column :tags_count.
234
- def tagged_with tags_list, options = {}
215
+ q = joins{taggings.tag}
235
216
 
236
- r = count_tags do
237
- joins{tags}.
217
+ case tags_list
218
+ when Hash
219
+ # A tag can only match it's context
220
+
221
+ c = tags_list.each_key.map do |context|
222
+ squeel do
223
+ tags.name.in(tags_list[context]) & (taggings.context == context.to_s)
224
+ end
225
+ end.inject do |s,t|
226
+ s | t
227
+ end
228
+
229
+ q = q.where(c)
230
+
231
+ else
232
+ # Any tag can match any context
233
+ q = q.
238
234
  where{tags.name.in(tags_list)}.
239
235
  where(with_tag_context(options.delete(:on)))
240
236
  end
241
237
 
242
- if options.delete :all
243
- r.where{tags_count==tags_list.length}
244
- elsif min = options.delete(:min)
245
- r.where{tags_count>=min}
246
- else
247
- r
248
- end
238
+ q = q.group_by_all_columns.
239
+ select{count(tags.id).as(tags_count)}.
240
+ select('*').
241
+ order("tags_count desc")
249
242
 
243
+ # Isolate the aggregate uery by wrapping it as
244
+ #
245
+ # select * from ( ..... ) tags
246
+ q = from(q.arel.as(self.table_name))
247
+
248
+ # Restrict by minimum tag counts if required
249
+ min = options.delete :min
250
+ q = q.where{tags_count>=min} if min
251
+
252
+ # Require all the tags if required
253
+ all = options.delete :all
254
+ q = q.where{tags_count==tags_list.length} if all
255
+
256
+ # Return the relation
257
+ q
250
258
  end
251
259
 
252
- # Generates a query that returns list of popular tags
253
- # for given model with an extra column :tags_count.
254
- def popular_tags options={}
260
+ # Get the tags associated with this model class
261
+ # This can be chained such as
262
+ #
263
+ # User.documents.tags
264
+ def tags(options = {})
255
265
 
256
- r = count_tags do
257
- RocketTag::Tag.
258
- joins{taggings}.
259
- where(with_tag_context(options.delete(:on))).
260
- by_taggable_type(self)
261
- end
266
+ # Grab the current scope
267
+ s = select{id}
262
268
 
263
- if min = options.delete(:min)
264
- r = r.where{tags_count>=min}
265
- end
269
+ # Grab table name
270
+ t = self.to_s
271
+
272
+ q = RocketTag::Tag.joins{taggings}.
273
+ where{taggings.taggable_type==t}. # Apply taggable type
274
+ where{taggings.taggable_id.in(s)}. # Apply current scope
275
+ where(with_tag_context(options.delete(:on))). # Restrict by context
276
+ group_by_all_columns.
277
+ select{count(tags.id).as(tags_count)}.
278
+ select('*').
279
+ order("tags_count desc")
280
+
281
+ # Isolate the aggregate uery by wrapping it as
282
+ #
283
+ # select * from ( ..... ) tags
284
+ q = RocketTag::Tag.from(q.arel.as(RocketTag::Tag.table_name))
285
+
286
+ # Restrict by minimum tag counts if required
287
+ min = options.delete :min
288
+ q = q.where{tags_count>=min} if min
266
289
 
267
- r
290
+ # Return the relation
291
+ q
292
+
293
+ end
294
+
295
+ # Generates a query that returns list of popular tags
296
+ # for given model with an extra column :tags_count.
297
+ def popular_tags options={}
298
+ tags(options)
268
299
  end
269
300
 
270
301
  def setup_for_rocket_tag
@@ -338,21 +369,18 @@ module RocketTag
338
369
  :through => :taggings,
339
370
  :conditions => [ "taggings.context = ?", context ]
340
371
 
341
-
342
372
  validate context do
343
373
  if not send(context).kind_of? Enumerable
344
374
  errors.add context, :invalid
345
375
  end
346
376
  end
347
377
 
348
-
349
378
  # Return an array of RocketTag::Tags for the context
350
379
  define_method "#{context}" do
351
380
  cache_tags
352
381
  tags_for_context(context)
353
382
  end
354
383
 
355
-
356
384
  define_method "#{context}=" do |list|
357
385
  list = Manager.parse_tags list
358
386
 
@@ -362,7 +390,6 @@ module RocketTag
362
390
 
363
391
  (@tag_dirty ||= Set.new) << context
364
392
 
365
-
366
393
  end
367
394
  end
368
395
  end
data/rocket_tag.gemspec CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "rocket_tag"
8
- s.version = "0.5.3"
8
+ s.version = "0.5.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Brad Phelan"]
data/spec/models.rb CHANGED
@@ -8,6 +8,11 @@ class TaggableModel < ActiveRecord::Base
8
8
  attr_taggable :skills
9
9
  attr_taggable :needs, :offerings
10
10
  has_many :untaggable_models
11
+ belongs_to :user
12
+ end
13
+
14
+ class User < ActiveRecord::Base
15
+ has_many :taggable_models
11
16
  end
12
17
 
13
18
  class CachedModel < ActiveRecord::Base
@@ -87,16 +87,19 @@ describe TaggableModel do
87
87
  describe "querying tags" do
88
88
 
89
89
  before :each do
90
- @t00 = TaggableModel.create :name => "00", :foo => "A"
91
- @t01 = TaggableModel.create :name => "01", :foo => "B"
90
+ @user0 = User.create :name => "brad"
91
+ @user1 = User.create :name => "hannah"
92
92
 
93
+ @t00 = TaggableModel.create :name => "00", :foo => "A", :user => @user0
94
+ @t01 = TaggableModel.create :name => "01", :foo => "B", :user => @user1
93
95
 
94
- @t10 = TaggableModel.create :name => "10", :foo => "A"
95
- @t11 = TaggableModel.create :name => "11", :foo => "B"
96
96
 
97
+ @t10 = TaggableModel.create :name => "10", :foo => "A", :user => @user0
98
+ @t11 = TaggableModel.create :name => "11", :foo => "B", :user => @user1
97
99
 
98
- @t20 = TaggableModel.create :name => "20", :foo => "A"
99
- @t21 = TaggableModel.create :name => "21", :foo => "B"
100
+
101
+ @t20 = TaggableModel.create :name => "20", :foo => "A", :user => @user0
102
+ @t21 = TaggableModel.create :name => "21", :foo => "B", :user => @user1
100
103
 
101
104
  @t00.skills = [ "a" , "b", "x"]
102
105
  @t00.languages = [ "german" , "french"]
@@ -167,6 +170,14 @@ describe TaggableModel do
167
170
  r.find{|i|i.name == "11"}.tags_count.should == 1
168
171
  r.find{|i|i.name == "21"}.should be_nil
169
172
 
173
+ # It should be possible to narrow scopes with tagged_with
174
+ r = @user0.taggable_models.tagged_with(["a", "b", "german"], :on => :skills).all
175
+ r.find{|i|i.name == "00"}.tags_count.should == 2
176
+ r.find{|i|i.name == "01"}.should be_nil
177
+ r.find{|i|i.name == "10"}.tags_count.should == 1
178
+ r.find{|i|i.name == "11"}.should be_nil
179
+ r.find{|i|i.name == "21"}.should be_nil
180
+
170
181
  end
171
182
  end
172
183
 
@@ -314,10 +325,11 @@ describe TaggableModel do
314
325
  describe "Using in subqueries" do
315
326
  it "should be possible to select the 'id' of the relation to use in a subquery" do
316
327
 
317
- TaggableModel.where do
328
+ q = TaggableModel.where do
318
329
  id.in(TaggableModel.tagged_with(["a", "b"]).select{id}) &
319
330
  id.in(TaggableModel.tagged_with(["c"]).select{id})
320
- end.count.should == 2
331
+ end
332
+ q.count.should == 2
321
333
 
322
334
  TaggableModel.where do
323
335
  id.in(TaggableModel.tagged_with(["a", "b"]).select{id})
@@ -338,6 +350,40 @@ describe TaggableModel do
338
350
  TaggableModel.popular_tags(:on=>[:skills, :languages]).order('id asc').first.name.should == 'a'
339
351
  TaggableModel.popular_tags(:on=>[:skills, :languages]).order('id asc').last.name.should == 'jinglish'
340
352
  TaggableModel.popular_tags(:min=>2).all.length.should == 6 ## dirty!
353
+
354
+
355
+
356
+ end
357
+ end
358
+
359
+ describe "tag cloud calculations" do
360
+ it "should return tags on an association and the counts thereof" do
361
+ @user0.taggable_models.popular_tags.each do |tag|
362
+ puts "#{tag.name}\t#{tag.tags_count}"
363
+ end
364
+ puts "-------------"
365
+ @user1.taggable_models.popular_tags.each do |tag|
366
+ puts "#{tag.name}\t#{tag.tags_count}"
367
+ end
368
+
369
+ # Check that the tags_count on each tag is in
370
+ # descending order.
371
+ @user0.taggable_models.popular_tags.count.should == 8
372
+ @user0.taggable_models.popular_tags.inject do |s, t|
373
+ s.tags_count.should >= t.tags_count
374
+ t
375
+ end
376
+
377
+ @user1.taggable_models.popular_tags.count.should == 8
378
+ @user1.taggable_models.popular_tags.inject do |s, t|
379
+ s.tags_count.should >= t.tags_count
380
+ t
381
+ end
382
+
383
+ # Sanity check the two queries are not identical
384
+ @user0.taggable_models.popular_tags.should_not ==
385
+ @user1.taggable_models.popular_tags
386
+
341
387
  end
342
388
  end
343
389
  end
data/spec/schema.rb CHANGED
@@ -17,6 +17,7 @@ ActiveRecord::Schema.define :version => 0 do
17
17
  end
18
18
 
19
19
  create_table :taggable_models, :force => true do |t|
20
+ t.column :user_id, :integer
20
21
  t.column :name, :string
21
22
  t.column :type, :string
22
23
  t.column :foo, :string
@@ -46,7 +47,7 @@ ActiveRecord::Schema.define :version => 0 do
46
47
  t.column :cached_glass_list, :string
47
48
  end
48
49
 
49
- create_table :taggable_users, :force => true do |t|
50
+ create_table :users, :force => true do |t|
50
51
  t.column :name, :string
51
52
  end
52
53
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rocket_tag
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.5.3
5
+ version: 0.5.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Brad Phelan
@@ -144,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
144
  requirements:
145
145
  - - ">="
146
146
  - !ruby/object:Gem::Version
147
- hash: 293230795738832648
147
+ hash: -4496343297606785835
148
148
  segments:
149
149
  - 0
150
150
  version: "0"