tagtools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ == TagTools 0.0.1
2
+ * folksonomy!
data/README ADDED
@@ -0,0 +1,40 @@
1
+ TagTools is a simple ruby library for managing folksonomy tags within a Rails
2
+ applications. It supports tagging of any number of database tables (i.e.,
3
+ you can tag different types of data using the same mechanism). It can merge
4
+ tags, delete tags, and rename tags. TagTools uses a fully normalized tag
5
+ schema.
6
+
7
+ == Example
8
+ class User < ActiveRecord::Base
9
+ end
10
+ class Tag < ActiveRecord::Base
11
+ def inspect
12
+ return "tag:" + self.name.inspect
13
+ end
14
+ def to_s
15
+ return self.name
16
+ end
17
+ end
18
+ class Bookmark < ActiveRecord::Base
19
+ acts_as_taggable :scope => :user
20
+ end
21
+
22
+ current_user = User.new
23
+ current_user.name = "Joe Schmoe"
24
+ current_user.save
25
+ slashdot_bookmark = Bookmark.new
26
+ slashdot_bookmark.url = "http://slashdot.org"
27
+ slashdot_bookmark.save
28
+ slashdot_bookmark.user_tags(current_user.id).concat( "geeky", "news", "technology" )
29
+ slashdot_bookmark.tags
30
+ => [ tag:"geeky", tag:"news", tag:"technology" ]
31
+ slashdot_bookmark.user_tags(current_user).delete( "news" )
32
+ slashdot_bookmark.tags
33
+ => [ tag:"geeky", tag:"technology" ]
34
+ slashdot_bookmark.user_tags(current_user).concat( "technology", "linux" )
35
+ slashdot_bookmark.tags
36
+ => [ tag:"geeky", tag:"technology", tag:"linux" ]
37
+ Bookmark.tag_query( :user_id => current_user, :with_any_tags => ["linux"] )
38
+ => []
39
+ Bookmark.tag_query( :with_any_tags => ["linux"] )
40
+ => [ #<Bookmark:0x77dc54 @attributes={"url"=>"http://slashdot.org"}> ]
data/install.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ # this was adapted from rdoc's install.rb by ways of Log4r
8
+
9
+ $sitedir = CONFIG["sitelibdir"]
10
+ unless $sitedir
11
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
12
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
13
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
14
+ if !$sitedir
15
+ $sitedir = File.join($libdir, "site_ruby")
16
+ elsif $sitedir !~ Regexp.quote(version)
17
+ $sitedir = File.join($sitedir, version)
18
+ end
19
+ end
20
+
21
+ # the acual gruntwork
22
+ Dir.chdir("lib")
23
+
24
+ Find.find("tagtools", "tagtools.rb") { |f|
25
+ if f[-3..-1] == ".rb"
26
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
27
+ else
28
+ File::makedirs(File.join($sitedir, *f.split(/\//)))
29
+ end
30
+ }
data/lib/tagtools.rb ADDED
@@ -0,0 +1,1026 @@
1
+ #--
2
+ # Copyright (c) 2005 Robert Aman
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ TAG_TOOLS_VERSION = "0.0.1"
25
+
26
+ $:.unshift(File.dirname(__FILE__))
27
+ $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
28
+
29
+ require 'rubygems'
30
+ require 'active_record'
31
+
32
+ module ActiveRecord #:nodoc:
33
+ module Associations #:nodoc:
34
+ class GlobalTagsAssociation < AssociationCollection #:nodoc:
35
+ def initialize(owner, tag_class, item_class, options)
36
+ @owner = owner
37
+
38
+ @tag_class = tag_class
39
+ @tag_class_name = tag_class.name
40
+ @tag_foreign_key = options[:tag_foreign_key] ||
41
+ Inflector.underscore(
42
+ Inflector.demodulize(@tag_class.name)) + "_id"
43
+ @item_class = item_class
44
+ @item_class_name = item_class.name
45
+ @item_foreign_key = options[:item_foreign_key] ||
46
+ Inflector.underscore(
47
+ Inflector.demodulize(@item_class.name)) + "_id"
48
+
49
+ @join_table = options[:join_table]
50
+ @options = options
51
+
52
+ # For the sake of the classes we inheritted from,
53
+ # since we're not calling super
54
+ @association_class = @tag_class
55
+ @association_name = Inflector.pluralize(
56
+ Inflector.underscore(Inflector.demodulize(@tag_class.name)))
57
+ @association_class_primary_key_name = @item_foreign_key
58
+
59
+ reset
60
+ construct_sql
61
+ end
62
+
63
+ def build(attributes = {})
64
+ load_target
65
+ record = @item_class.new(attributes)
66
+ record[@item_foreign_key] = @owner.id unless @owner.new_record?
67
+ @target << record
68
+ return record
69
+ end
70
+
71
+ # Removes all records from this association.
72
+ # Returns +self+ so method calls may be chained.
73
+ def clear
74
+
75
+ # forces load_target if hasn't happened already
76
+ return self if size == 0
77
+
78
+ @owner.connection.execute("DELETE FROM #{@join_table} " +
79
+ "WHERE #{@join_table}.#{@item_foreign_key} = " +
80
+ " '#{@owner.id}'")
81
+
82
+ @target = []
83
+ self
84
+ end
85
+
86
+ def find_first
87
+ load_target.first
88
+ end
89
+
90
+ def find(*args)
91
+ # Return an Array if multiple ids are given.
92
+ expects_array = args.first.kind_of?(Array)
93
+
94
+ ids = args.flatten.compact.uniq
95
+
96
+ # If no block is given, raise RecordNotFound.
97
+ if ids.empty?
98
+ raise RecordNotFound, "Couldn't find #{@tag_class.name} without an ID"
99
+ else
100
+ if ids.size == 1
101
+ id = ids.first
102
+ record = load_target.detect { |record| id == record.id }
103
+ expects_array? ? [record] : record
104
+ else
105
+ load_target.select { |record| ids.include?(record.id) }
106
+ end
107
+ end
108
+ end
109
+
110
+ def push_with_attributes(record, join_attributes = {})
111
+ unless record.kind_of? @tag_class
112
+ record = record.to_s.create_tag
113
+ end
114
+ join_attributes.each { |key, value| record[key.to_s] = value }
115
+ callback(:before_add, record)
116
+ insert_record(record, join_attributes) unless @owner.new_record?
117
+ @target << record
118
+ callback(:after_add, record)
119
+ @target.sort!
120
+ return self
121
+ end
122
+
123
+ alias :concat_with_attributes :push_with_attributes
124
+
125
+ def size
126
+ count_records
127
+ end
128
+
129
+ def <<(*records)
130
+ result = true
131
+ load_target
132
+ @owner.transaction do
133
+ flatten_deeper(records).each do |record|
134
+ record = create_tag(record)
135
+ callback(:before_add, record)
136
+ result &&= insert_record(record) unless @owner.new_record?
137
+ @target << record
138
+ callback(:after_add, record)
139
+ end
140
+ end
141
+ @target.sort!
142
+ result and self
143
+ end
144
+
145
+ alias_method :push, :<<
146
+ alias_method :concat, :<<
147
+
148
+ # Remove +records+ from this association. Does not destroy +records+.
149
+ def delete(*records)
150
+ records = flatten_deeper(records)
151
+ for index in 0..records.size
152
+ records[index] = create_tag(records[index])
153
+ end
154
+ records.reject! do |record|
155
+ @target.delete(record) if record.new_record?
156
+ end
157
+ return if records.empty?
158
+
159
+ @owner.transaction do
160
+ records.each { |record| callback(:before_remove, record) }
161
+ delete_records(records)
162
+ records.each do |record|
163
+ @target.delete(record)
164
+ callback(:after_remove, record)
165
+ end
166
+ end
167
+ end
168
+
169
+ protected
170
+ def create_tag(raw_tag)
171
+ if raw_tag.kind_of? @tag_class
172
+ return raw_tag
173
+ end
174
+ tag_object = @tag_class.find_by_name(raw_tag.to_s)
175
+ if tag_object.nil?
176
+ tag_object = @tag_class.new
177
+ tag_object.name = raw_tag.to_s
178
+ tag_object.save
179
+ end
180
+ return tag_object
181
+ end
182
+
183
+ def find_target(sql = @finder_sql)
184
+ records = @tag_class.find_by_sql(sql)
185
+ uniq(records)
186
+ end
187
+
188
+ def count_records
189
+ load_target.size
190
+ end
191
+
192
+ def insert_record(record, join_attributes = {})
193
+ record = create_tag(record)
194
+ if record.new_record?
195
+ return false unless record.save
196
+ end
197
+
198
+ columns = @owner.connection.columns(@join_table, "#{@join_table} Columns")
199
+
200
+ attributes = columns.inject({}) do |attributes, column|
201
+ case column.name
202
+ when @item_foreign_key
203
+ attributes[column.name] = @owner.quoted_id
204
+ when @tag_foreign_key
205
+ attributes[column.name] = record.quoted_id
206
+ else
207
+ value = record[column.name]
208
+ attributes[column.name] = value unless value.nil?
209
+ end
210
+ attributes
211
+ end
212
+
213
+ columns_list = @owner.send(:quoted_column_names,
214
+ attributes).join(', ')
215
+ values_list = attributes.values.collect { |value|
216
+ @owner.send(:quote, value)
217
+ }.join(', ')
218
+
219
+ sql =
220
+ "INSERT INTO #{@join_table} (#{columns_list}) " +
221
+ "VALUES (#{values_list})"
222
+
223
+ @owner.connection.execute(sql)
224
+
225
+ return true
226
+ end
227
+
228
+ def delete_records(records)
229
+ ids = quoted_record_ids(records)
230
+ sql = "DELETE FROM #{@join_table} " +
231
+ "WHERE #{@item_foreign_key} = " +
232
+ " #{@owner.quoted_id} " +
233
+ "AND #{@tag_foreign_key} IN (#{ids})"
234
+ @owner.connection.execute(sql)
235
+ end
236
+
237
+ def construct_sql
238
+ @finder_sql =
239
+ "SELECT t.*, j.* FROM #{@join_table} j, " +
240
+ " #{@tag_class.table_name} t " +
241
+ "WHERE t.#{@tag_class.primary_key} = " +
242
+ " j.#{@tag_foreign_key} " +
243
+ "AND j.#{@item_foreign_key} = " +
244
+ " #{@owner.quoted_id} " +
245
+ "ORDER BY t.name"
246
+ end
247
+ end
248
+
249
+ class UserTagsAssociation < AssociationCollection #:nodoc:
250
+ def initialize(owner, user_id, tag_class, user_class, item_class,
251
+ options)
252
+ @owner = owner
253
+ @user_id = user_id
254
+
255
+ @tag_class = tag_class
256
+ @tag_class_name = tag_class.name
257
+ @tag_foreign_key = options[:tag_foreign_key] ||
258
+ Inflector.underscore(
259
+ Inflector.demodulize(@tag_class.name)) + "_id"
260
+ @user_class = user_class
261
+ @user_class_name = user_class.name
262
+ @user_foreign_key = options[:user_foreign_key] ||
263
+ Inflector.underscore(
264
+ Inflector.demodulize(@user_class.name)) + "_id"
265
+ @item_class = item_class
266
+ @item_class_name = item_class.name
267
+ @item_foreign_key = options[:item_foreign_key] ||
268
+ Inflector.underscore(
269
+ Inflector.demodulize(@item_class.name)) + "_id"
270
+
271
+ @join_table = options[:join_table]
272
+ @options = options
273
+
274
+ # For the sake of the classes we inheritted from,
275
+ # since we're not calling super
276
+ @association_class = @tag_class
277
+ @association_name = Inflector.pluralize(
278
+ Inflector.underscore(Inflector.demodulize(@tag_class.name)))
279
+ @association_class_primary_key_name = @item_foreign_key
280
+
281
+ reset
282
+ construct_sql
283
+ end
284
+
285
+ def build(attributes = {})
286
+ if @user_id.nil?
287
+ raise "Can only build object if a tagging user has been specified."
288
+ else
289
+ load_target
290
+ record = @item_class.new(attributes)
291
+ record[@item_foreign_key] = @owner.id unless @owner.new_record?
292
+ @target << record
293
+ return record
294
+ end
295
+ end
296
+
297
+ # Removes all records from this association.
298
+ # Returns +self+ so method calls may be chained.
299
+ def clear
300
+
301
+ # forces load_target if hasn't happened already
302
+ return self if size == 0
303
+
304
+ if @user_id.nil?
305
+ raise "Tags on an item can only be cleared for one user at a time."
306
+ else
307
+ @owner.connection.execute("DELETE FROM #{@join_table} " +
308
+ "WHERE #{@join_table}.#{@item_foreign_key} = " +
309
+ " '#{@owner.id}' AND #{@join_table}.#{@user_foreign_key} = " +
310
+ " '#{@user_id}'")
311
+ end
312
+
313
+ @target = []
314
+ self
315
+ end
316
+
317
+ def find_first
318
+ load_target.first
319
+ end
320
+
321
+ def find(*args)
322
+ # Return an Array if multiple ids are given.
323
+ expects_array = args.first.kind_of?(Array)
324
+
325
+ ids = args.flatten.compact.uniq
326
+
327
+ # If no block is given, raise RecordNotFound.
328
+ if ids.empty?
329
+ raise RecordNotFound, "Couldn't find #{@tag_class.name} without an ID"
330
+ else
331
+ if ids.size == 1
332
+ id = ids.first
333
+ record = load_target.detect { |record| id == record.id }
334
+ expects_array? ? [record] : record
335
+ else
336
+ load_target.select { |record| ids.include?(record.id) }
337
+ end
338
+ end
339
+ end
340
+
341
+ def push_with_attributes(record, join_attributes = {})
342
+ if @user_id.nil?
343
+ raise "Cannot add record without a specific user id."
344
+ else
345
+ unless record.kind_of? @tag_class
346
+ record = record.to_s.create_tag
347
+ end
348
+ join_attributes.each { |key, value| record[key.to_s] = value }
349
+ callback(:before_add, record)
350
+ insert_record(record, join_attributes) unless @owner.new_record?
351
+ @target << record
352
+ @owner.instance_variable_set("@#{@options[:collection]}", nil)
353
+ callback(:after_add, record)
354
+ @target.sort!
355
+ return self
356
+ end
357
+ end
358
+
359
+ alias :concat_with_attributes :push_with_attributes
360
+
361
+ def size
362
+ count_records
363
+ end
364
+
365
+ def <<(*records)
366
+ result = true
367
+ load_target
368
+ @owner.transaction do
369
+ flatten_deeper(records).each do |record|
370
+ record = create_tag(record)
371
+ callback(:before_add, record)
372
+ result &&= insert_record(record) unless @owner.new_record?
373
+ @target << record
374
+ @owner.instance_variable_set("@#{@options[:collection]}", nil)
375
+ callback(:after_add, record)
376
+ end
377
+ end
378
+ @target.sort!
379
+ result and self
380
+ end
381
+
382
+ alias_method :push, :<<
383
+ alias_method :concat, :<<
384
+
385
+ # Remove +records+ from this association. Does not destroy +records+.
386
+ def delete(*records)
387
+ records = flatten_deeper(records)
388
+ for index in 0..records.size
389
+ records[index] = create_tag(records[index])
390
+ end
391
+ records.reject! do |record|
392
+ @target.delete(record) if record.new_record?
393
+ end
394
+ return if records.empty?
395
+
396
+ @owner.transaction do
397
+ records.each { |record| callback(:before_remove, record) }
398
+ delete_records(records)
399
+ records.each do |record|
400
+ @target.delete(record)
401
+ callback(:after_remove, record)
402
+ end
403
+ end
404
+ end
405
+
406
+ protected
407
+ def create_tag(raw_tag)
408
+ if raw_tag.kind_of? @tag_class
409
+ return raw_tag
410
+ end
411
+ tag_object = @tag_class.find_by_name(raw_tag.to_s)
412
+ if tag_object.nil?
413
+ tag_object = @tag_class.new
414
+ tag_object.name = raw_tag.to_s
415
+ tag_object.save
416
+ end
417
+ return tag_object
418
+ end
419
+
420
+ def find_target(sql = @finder_sql)
421
+ records = @tag_class.find_by_sql(sql)
422
+ uniq(records)
423
+ end
424
+
425
+ def count_records
426
+ load_target.size
427
+ end
428
+
429
+ def insert_record(record, join_attributes = {})
430
+ if @user_id.nil?
431
+ raise "Cannot insert record if the user id is not set."
432
+ end
433
+ record = create_tag(record)
434
+
435
+ if record.new_record?
436
+ return false unless record.save
437
+ end
438
+
439
+ columns = @owner.connection.columns(@join_table, "#{@join_table} Columns")
440
+
441
+ attributes = columns.inject({}) do |attributes, column|
442
+ case column.name
443
+ when @item_foreign_key
444
+ attributes[column.name] = @owner.quoted_id
445
+ when @tag_foreign_key
446
+ attributes[column.name] = record.quoted_id
447
+ when @user_foreign_key
448
+ attributes[column.name] = @user_id.to_s
449
+ else
450
+ value = record[column.name]
451
+ attributes[column.name] = value unless value.nil?
452
+ end
453
+ attributes
454
+ end
455
+
456
+ columns_list = @owner.send(:quoted_column_names,
457
+ attributes).join(', ')
458
+ values_list = attributes.values.collect { |value|
459
+ @owner.send(:quote, value)
460
+ }.join(', ')
461
+
462
+ sql =
463
+ "INSERT INTO #{@join_table} (#{columns_list}) " +
464
+ "VALUES (#{values_list})"
465
+
466
+ @owner.connection.execute(sql)
467
+
468
+ return true
469
+ end
470
+
471
+ def delete_records(records)
472
+ if @user_id.nil?
473
+ raise "Cannot delete records unless the user id is set."
474
+ end
475
+ ids = quoted_record_ids(records)
476
+ sql = "DELETE FROM #{@join_table} " +
477
+ "WHERE #{@item_foreign_key} = " +
478
+ " #{@owner.quoted_id} " +
479
+ "AND #{@user_foreign_key} = '#{@user_id.to_s}'"
480
+ "AND #{@tag_foreign_key} IN (#{ids})"
481
+ @owner.connection.execute(sql)
482
+ end
483
+
484
+ def construct_sql
485
+ if @user_id.nil?
486
+ @finder_sql =
487
+ "SELECT t.*, j.* FROM #{@join_table} j, " +
488
+ " #{@tag_class.table_name} t " +
489
+ "WHERE t.#{@tag_class.primary_key} = " +
490
+ " j.#{@tag_foreign_key} " +
491
+ "AND j.#{@item_foreign_key} = " +
492
+ " #{@owner.quoted_id} " +
493
+ "ORDER BY t.name"
494
+ else
495
+ @finder_sql =
496
+ "SELECT t.*, j.* FROM #{@join_table} j, " +
497
+ " #{@tag_class.table_name} t " +
498
+ "WHERE t.#{@tag_class.primary_key} = " +
499
+ " j.#{@tag_foreign_key} " +
500
+ "AND j.#{@item_foreign_key} = " +
501
+ " #{@owner.quoted_id} " +
502
+ "AND j.#{@user_foreign_key} = " +
503
+ " '#{@user_id}' " +
504
+ "ORDER BY t.name"
505
+ end
506
+ end
507
+ end
508
+ end
509
+
510
+ module Acts #:nodoc:
511
+ module Taggable #:nodoc:
512
+ def self.append_features(base) #:nodoc:
513
+ super
514
+ base.extend(ClassMethods)
515
+ end
516
+
517
+ module ClassMethods
518
+ def acts_as_taggable(options = {})
519
+ validate_options([ :scope, :tag_class_name, :user_class_name,
520
+ :item_foreign_key, :tag_foreign_key,
521
+ :user_foreign_key, :collection, :user_collection,
522
+ :conditions, :join_table, :before_add,
523
+ :after_add, :before_remove, :after_remove ],
524
+ options.keys)
525
+ options = { :scope => :global,
526
+ :collection => :tags,
527
+ :user_collection => :user_tags,
528
+ :tag_class_name => 'Tag',
529
+ :user_class_name => 'User' }.merge(options)
530
+
531
+ unless [:global, :user].include? options[:scope]
532
+ raise(ActiveRecord::ActiveRecordError,
533
+ ":scope must either be set to :global or :user.")
534
+ end
535
+
536
+ require_association(
537
+ Inflector.underscore(options[:tag_class_name]))
538
+
539
+ tag_class = eval(options[:tag_class_name])
540
+ tag_foreign_key = options[:tag_foreign_key] ||
541
+ Inflector.underscore(
542
+ Inflector.demodulize(tag_class.name)) + "_id"
543
+ item_class = self
544
+ item_foreign_key = options[:item_foreign_key] ||
545
+ Inflector.underscore(
546
+ Inflector.demodulize(item_class.name)) + "_id"
547
+
548
+ # Make sure we can sort tags
549
+ unless tag_class.method_defined?("<=>")
550
+ tag_class.module_eval do
551
+ define_method("<=>") do |raw_tag|
552
+ if raw_tag.kind_of? self.class
553
+ return self.name <=> raw_tag.name
554
+ else
555
+ return self.name <=> raw_tag.to_s
556
+ end
557
+ end
558
+ end
559
+ end
560
+
561
+ if options[:scope] == :user
562
+
563
+ require_association(
564
+ Inflector.underscore(options[:user_class_name]))
565
+
566
+ user_class = eval(options[:user_class_name])
567
+ user_foreign_key = options[:user_foreign_key] ||
568
+ Inflector.underscore(
569
+ Inflector.demodulize(user_class.name)) + "_id"
570
+
571
+ options[:join_table] ||=
572
+ join_table_name(
573
+ undecorated_table_name(tag_class.name),
574
+ undecorated_table_name(user_class.name),
575
+ undecorated_table_name(item_class.name))
576
+
577
+ # Create the two collections
578
+ define_method(options[:collection]) do |*params|
579
+ force_reload = params.first unless params.empty?
580
+ association = instance_variable_get(
581
+ "@#{options[:collection]}")
582
+ unless association.respond_to?(:loaded?)
583
+ association =
584
+ ActiveRecord::Associations::UserTagsAssociation.new(self,
585
+ nil, tag_class, user_class, item_class, options)
586
+ instance_variable_set(
587
+ "@#{options[:collection]}", association)
588
+ end
589
+ association.reload if force_reload
590
+ return association
591
+ end
592
+
593
+ define_method(options[:user_collection]) do |user_id, *params|
594
+ unless user_id.kind_of? Fixnum
595
+ raise(ActiveRecord::ActiveRecordError,
596
+ "Expected Fixnum, got #{user_id.class.name}")
597
+ end
598
+ force_reload = params.first unless params.empty?
599
+ association = instance_variable_get(
600
+ "@#{options[:user_collection]}_#{user_id}")
601
+ unless association.respond_to?(:loaded?)
602
+ association =
603
+ ActiveRecord::Associations::UserTagsAssociation.new(self,
604
+ user_id, tag_class, user_class, item_class, options)
605
+ instance_variable_set(
606
+ "@#{options[:user_collection]}_#{user_id}", association)
607
+ end
608
+ association.reload if force_reload
609
+ return association
610
+ end
611
+
612
+ singleton_class = class << self; self; end
613
+ singleton_class.module_eval do
614
+ define_method(:tag_query) do |*params|
615
+ query_options = params.first unless params.empty?
616
+ validate_options([:with_any_tags, :with_all_tags,
617
+ :without_tags, :user_id], query_options.keys)
618
+ with_any_tags = query_options[:with_any_tags]
619
+ unless with_any_tags.nil?
620
+ with_any_tags.collect! { |tag| tag.to_s }
621
+ with_any_tags.uniq!
622
+ end
623
+ with_all_tags = query_options[:with_all_tags]
624
+ unless with_all_tags.nil?
625
+ with_all_tags.collect! { |tag| tag.to_s }
626
+ with_all_tags.uniq!
627
+ end
628
+ without_tags = query_options[:without_tags]
629
+ unless without_tags.nil?
630
+ without_tags.collect! { |tag| tag.to_s }
631
+ without_tags.uniq!
632
+ end
633
+ if without_tags != nil && with_any_tags == nil &&
634
+ with_all_tags == nil
635
+ raise(ActiveRecord::ActiveRecordError,
636
+ "Cannot run this query, nothing to search for.")
637
+ end
638
+ tagging_user_id = query_options[:user_id]
639
+ results = []
640
+
641
+ group_by_string = item_class.table_name + "." +
642
+ item_class.column_names.join(
643
+ ", #{item_class.table_name}.")
644
+ tagging_user_id_string = ""
645
+ unless tagging_user_id.nil?
646
+ tagging_user_id_string =
647
+ "AND #{options[:join_table]}.#{user_foreign_key} = " +
648
+ "#{tagging_user_id}"
649
+ end
650
+
651
+ with_all_tags_results = nil
652
+ if with_all_tags != nil && with_all_tags.size > 0
653
+ with_all_tags_query_thread = Thread.new do
654
+ tag_name_condition = "#{tag_class.table_name}.name = '" +
655
+ with_all_tags.join(
656
+ "\' OR #{tag_class.table_name}.name=\'") + "'"
657
+ with_all_tags_sql = <<-SQL
658
+ SELECT #{item_class.table_name}.*
659
+ FROM #{options[:join_table]}, #{item_class.table_name},
660
+ #{tag_class.table_name}
661
+ WHERE #{options[:join_table]}.#{tag_foreign_key} =
662
+ #{tag_class.table_name}.#{tag_class.primary_key}
663
+ AND (#{tag_name_condition})
664
+ AND #{item_class.table_name}.#{item_class.primary_key} =
665
+ #{options[:join_table]}.#{item_foreign_key}
666
+ #{tagging_user_id_string}
667
+ GROUP BY #{group_by_string}
668
+ HAVING COUNT(
669
+ #{item_class.table_name}.#{item_class.primary_key}) =
670
+ #{with_all_tags.size}
671
+ SQL
672
+ with_all_tags_results =
673
+ item_class.find_by_sql(with_all_tags_sql)
674
+ end
675
+ end
676
+
677
+ with_any_tags_results = nil
678
+ if with_any_tags != nil && with_any_tags.size > 0
679
+ with_any_tags_query_thread = Thread.new do
680
+ with_any_tags_sql = <<-SQL
681
+ SELECT #{item_class.table_name}.*
682
+ FROM #{options[:join_table]}, #{item_class.table_name},
683
+ #{tag_class.table_name}
684
+ WHERE #{tag_class.table_name}.name
685
+ IN ('#{with_any_tags.join('\', \'')}')
686
+ AND #{tag_class.table_name}.#{tag_class.primary_key} =
687
+ #{options[:join_table]}.#{tag_foreign_key}
688
+ AND #{item_class.table_name}.#{item_class.primary_key} =
689
+ #{options[:join_table]}.#{item_foreign_key}
690
+ #{tagging_user_id_string}
691
+ GROUP BY #{group_by_string}
692
+ SQL
693
+ with_any_tags_results =
694
+ item_class.find_by_sql(with_any_tags_sql)
695
+ end
696
+ end
697
+
698
+ if with_all_tags != nil && with_all_tags.size > 0
699
+ with_all_tags_query_thread.join
700
+ end
701
+ if with_any_tags != nil && with_any_tags.size > 0
702
+ with_any_tags_query_thread.join
703
+ end
704
+ if with_any_tags_results != nil &&
705
+ with_any_tags_results.size > 0 &&
706
+ with_all_tags_results != nil &&
707
+ with_all_tags_results.size > 0
708
+ results = with_all_tags_results & with_any_tags_results
709
+ elsif with_any_tags_results != nil &&
710
+ with_any_tags_results.size > 0
711
+ results = with_any_tags_results
712
+ elsif with_all_tags_results != nil &&
713
+ with_all_tags_results.size > 0
714
+ results = with_all_tags_results
715
+ end
716
+ if without_tags != nil && without_tags.size > 0
717
+ for result in results
718
+ if ((result.tags.map { |tag| tag.name }) &
719
+ without_tags).size > 0
720
+ results.delete(result)
721
+ end
722
+ end
723
+ end
724
+ return results
725
+ end
726
+ end
727
+
728
+ module_eval do
729
+ before_save <<-end_eval
730
+ @new_record_before_save = new_record?
731
+ associations =
732
+ instance_variables.inject([]) do |associations, iv|
733
+ if (iv =~ /^@#{options[:user_collection]}_/) == 0
734
+ associations << iv
735
+ end
736
+ associations
737
+ end
738
+ associations.each do |association_name|
739
+ association = instance_variable_get("\#{association_name}")
740
+ if association.respond_to?(:loaded?)
741
+ if new_record?
742
+ records_to_save = association
743
+ else
744
+ records_to_save = association.select do |record|
745
+ record.new_record?
746
+ end
747
+ end
748
+ records_to_save.inject(true) do |result,record|
749
+ result &&= record.valid?
750
+ end
751
+ end
752
+ end
753
+ end_eval
754
+ end
755
+
756
+ module_eval do
757
+ after_callback = <<-end_eval
758
+ associations =
759
+ instance_variables.inject([]) do |associations, iv|
760
+ if (iv =~ /^@#{options[:user_collection]}_/) == 0
761
+ associations << iv
762
+ end
763
+ associations
764
+ end
765
+ associations.each do |association_name|
766
+ association = instance_variable_get("\#{association_name}")
767
+ if association.respond_to?(:loaded?)
768
+ if @new_record_before_save
769
+ records_to_save = association
770
+ else
771
+ records_to_save = association.select do |record|
772
+ record.new_record?
773
+ end
774
+ end
775
+ records_to_save.each do |record|
776
+ association.send(:insert_record, record)
777
+ end
778
+ # reconstruct the SQL queries now that we know
779
+ # the owner's id
780
+ association.send(:construct_sql)
781
+ end
782
+ end
783
+ end_eval
784
+
785
+ # Doesn't use after_save as that would save associations
786
+ # added in after_create/after_update twice
787
+ after_create(after_callback)
788
+ after_update(after_callback)
789
+ end
790
+
791
+ # When the item gets destroyed, clear out all relationships
792
+ # that reference it.
793
+ before_destroy_sql = "DELETE FROM #{options[:join_table]} " +
794
+ "WHERE #{item_foreign_key} = " +
795
+ "\\\#{self.quoted_id}"
796
+ module_eval(
797
+ "before_destroy \"self.connection.delete(" +
798
+ "%{#{before_destroy_sql}})\"")
799
+
800
+ class_eval do
801
+ include ActiveRecord::Acts::Taggable::InstanceMethods
802
+ end
803
+
804
+ elsif options[:scope] == :global
805
+
806
+ require_association(
807
+ Inflector.underscore(options[:tag_class_name]))
808
+
809
+ tag_class = eval(options[:tag_class_name])
810
+ item_class = self
811
+
812
+ options[:join_table] ||=
813
+ join_table_name(
814
+ undecorated_table_name(tag_class.name),
815
+ undecorated_table_name(item_class.name))
816
+
817
+ define_method(options[:collection]) do |*params|
818
+ force_reload = params.first unless params.empty?
819
+ association = instance_variable_get(
820
+ "@#{options[:collection]}")
821
+ unless association.respond_to?(:loaded?)
822
+ association =
823
+ ActiveRecord::Associations::GlobalTagsAssociation.new(self,
824
+ tag_class, item_class, options)
825
+ instance_variable_set(
826
+ "@#{options[:collection]}", association)
827
+ end
828
+ association.reload if force_reload
829
+ return association
830
+ end
831
+
832
+ singleton_class = class << self; self; end
833
+ singleton_class.module_eval do
834
+ define_method(:tag_query) do |*params|
835
+ query_options = params.first unless params.empty?
836
+ validate_options([:with_any_tags, :with_all_tags,
837
+ :without_tags], query_options.keys)
838
+ with_any_tags = query_options[:with_any_tags]
839
+ unless with_any_tags.nil?
840
+ with_any_tags.collect! { |tag| tag.to_s }
841
+ with_any_tags.uniq!
842
+ end
843
+ with_all_tags = query_options[:with_all_tags]
844
+ unless with_all_tags.nil?
845
+ with_all_tags.collect! { |tag| tag.to_s }
846
+ with_all_tags.uniq!
847
+ end
848
+ without_tags = query_options[:without_tags]
849
+ unless without_tags.nil?
850
+ without_tags.collect! { |tag| tag.to_s }
851
+ without_tags.uniq!
852
+ end
853
+ if without_tags != nil && with_any_tags == nil &&
854
+ with_all_tags == nil
855
+ raise(ActiveRecord::ActiveRecordError,
856
+ "Cannot run this query, nothing to search for.")
857
+ end
858
+ results = []
859
+
860
+ group_by_string = item_class.table_name + "." +
861
+ item_class.column_names.join(
862
+ ", #{item_class.table_name}.")
863
+
864
+ with_all_tags_results = nil
865
+ if with_all_tags != nil && with_all_tags.size > 0
866
+ with_all_tags_query_thread = Thread.new do
867
+ tag_name_condition = "#{tag_class.table_name}.name = '" +
868
+ with_all_tags.join(
869
+ "\' OR #{tag_class.table_name}.name=\'") + "'"
870
+ with_all_tags_sql = <<-SQL
871
+ SELECT #{item_class.table_name}.*
872
+ FROM #{options[:join_table]}, #{item_class.table_name},
873
+ #{tag_class.table_name}
874
+ WHERE #{options[:join_table]}.#{tag_foreign_key} =
875
+ #{tag_class.table_name}.#{tag_class.primary_key}
876
+ AND (#{tag_name_condition})
877
+ AND #{item_class.table_name}.#{item_class.primary_key} =
878
+ #{options[:join_table]}.#{item_foreign_key}
879
+ GROUP BY #{group_by_string}
880
+ HAVING COUNT(
881
+ #{item_class.table_name}.#{item_class.primary_key}) =
882
+ #{with_all_tags.size}
883
+ SQL
884
+ with_all_tags_results =
885
+ item_class.find_by_sql(with_all_tags_sql)
886
+ end
887
+ end
888
+
889
+ with_any_tags_results = nil
890
+ if with_any_tags != nil && with_any_tags.size > 0
891
+ with_any_tags_query_thread = Thread.new do
892
+ with_any_tags_sql = <<-SQL
893
+ SELECT #{item_class.table_name}.*
894
+ FROM #{options[:join_table]}, #{item_class.table_name},
895
+ #{tag_class.table_name}
896
+ WHERE #{tag_class.table_name}.name
897
+ IN ('#{with_any_tags.join('\', \'')}')
898
+ AND #{tag_class.table_name}.#{tag_class.primary_key} =
899
+ #{options[:join_table]}.#{tag_foreign_key}
900
+ AND #{item_class.table_name}.#{item_class.primary_key} =
901
+ #{options[:join_table]}.#{item_foreign_key}
902
+ GROUP BY #{group_by_string}
903
+ SQL
904
+ with_any_tags_results =
905
+ item_class.find_by_sql(with_any_tags_sql)
906
+ end
907
+ end
908
+
909
+ if with_all_tags != nil && with_all_tags.size > 0
910
+ with_all_tags_query_thread.join
911
+ end
912
+ if with_any_tags != nil && with_any_tags.size > 0
913
+ with_any_tags_query_thread.join
914
+ end
915
+ if with_any_tags_results != nil &&
916
+ with_any_tags_results.size > 0 &&
917
+ with_all_tags_results != nil &&
918
+ with_all_tags_results.size > 0
919
+ results = with_all_tags_results & with_any_tags_results
920
+ elsif with_any_tags_results != nil &&
921
+ with_any_tags_results.size > 0
922
+ results = with_any_tags_results
923
+ elsif with_all_tags_results != nil &&
924
+ with_all_tags_results.size > 0
925
+ results = with_all_tags_results
926
+ end
927
+ if without_tags != nil && without_tags.size > 0
928
+ for result in results
929
+ if ((result.tags.map { |tag| tag.name }) &
930
+ without_tags).size > 0
931
+ results.delete(result)
932
+ end
933
+ end
934
+ end
935
+ return results
936
+ end
937
+ end
938
+
939
+ module_eval do
940
+ before_save <<-end_eval
941
+ @new_record_before_save = new_record?
942
+ association =
943
+ instance_variable_get("@#{options[:collection]}")
944
+ if association.respond_to?(:loaded?)
945
+ if new_record?
946
+ records_to_save = association
947
+ else
948
+ records_to_save = association.select do |record|
949
+ record.new_record?
950
+ end
951
+ end
952
+ records_to_save.inject(true) do |result,record|
953
+ result &&= record.valid?
954
+ end
955
+ end
956
+ end_eval
957
+ end
958
+
959
+ module_eval do
960
+ after_callback = <<-end_eval
961
+ association =
962
+ instance_variable_get("@#{options[:collection]}")
963
+ if association.respond_to?(:loaded?)
964
+ if @new_record_before_save
965
+ records_to_save = association
966
+ else
967
+ records_to_save = association.select do |record|
968
+ record.new_record?
969
+ end
970
+ end
971
+ records_to_save.each do |record|
972
+ association.send(:insert_record, record)
973
+ end
974
+ # reconstruct the SQL queries now that we know
975
+ # the owner's id
976
+ association.send(:construct_sql)
977
+ end
978
+ end_eval
979
+
980
+ # Doesn't use after_save as that would save associations
981
+ # added in after_create/after_update twice
982
+ after_create(after_callback)
983
+ after_update(after_callback)
984
+ end
985
+
986
+ # When the item gets destroyed, clear out all relationships
987
+ # that reference it.
988
+ before_destroy_sql = "DELETE FROM #{options[:join_table]} " +
989
+ "WHERE #{item_foreign_key} = " +
990
+ "\\\#{self.quoted_id}"
991
+ module_eval(
992
+ "before_destroy \"self.connection.delete(" +
993
+ "%{#{before_destroy_sql}})\"")
994
+
995
+ class_eval do
996
+ include ActiveRecord::Acts::Taggable::InstanceMethods
997
+ end
998
+ end
999
+ end
1000
+ private
1001
+
1002
+ private
1003
+ # Raises an exception if an invalid option has been specified to
1004
+ # prevent misspellings from slipping through
1005
+ def validate_options(valid_option_keys, supplied_option_keys)
1006
+ unknown_option_keys = supplied_option_keys - valid_option_keys
1007
+ unless unknown_option_keys.empty?
1008
+ raise(ActiveRecord::ActiveRecordError,
1009
+ "Unknown options: #{unknown_option_keys}")
1010
+ end
1011
+ end
1012
+
1013
+ def join_table_name(*table_names)
1014
+ table_name_prefix + table_names.sort.join("_") + table_name_suffix
1015
+ end
1016
+ end
1017
+
1018
+ module InstanceMethods
1019
+ end
1020
+ end
1021
+ end
1022
+ end
1023
+
1024
+ ActiveRecord::Base.class_eval do
1025
+ include ActiveRecord::Acts::Taggable
1026
+ end