rocket_tag 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
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"