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 +37 -24
- data/VERSION +1 -1
- data/lib/rocket_tag/taggable.rb +90 -63
- data/rocket_tag.gemspec +1 -1
- data/spec/models.rb +5 -0
- data/spec/rocket_tag/taggable_spec.rb +54 -8
- data/spec/schema.rb +2 -1
- metadata +2 -2
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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.
|
1
|
+
0.5.4
|
data/lib/rocket_tag/taggable.rb
CHANGED
@@ -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
|
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.
|
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
|
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(
|
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
|
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
|
-
|
222
|
-
|
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
|
-
|
233
|
-
# along with an extra column :tags_count.
|
234
|
-
def tagged_with tags_list, options = {}
|
215
|
+
q = joins{taggings.tag}
|
235
216
|
|
236
|
-
|
237
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
#
|
253
|
-
#
|
254
|
-
|
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
|
-
|
257
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
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
|
-
|
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
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
|
-
@
|
91
|
-
@
|
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
|
-
|
99
|
-
@
|
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
|
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 :
|
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.
|
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:
|
147
|
+
hash: -4496343297606785835
|
148
148
|
segments:
|
149
149
|
- 0
|
150
150
|
version: "0"
|