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 +2 -0
- data/README +40 -0
- data/install.rb +30 -0
- data/lib/tagtools.rb +1026 -0
- data/rakefile +112 -0
- data/test/global_tags_test.rb +103 -0
- data/test/user_tags_test.rb +116 -0
- metadata +58 -0
data/CHANGELOG
ADDED
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
|