tagtools 0.0.1

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/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