has_many_polymorphs 2.2

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.
Files changed (177) hide show
  1. data/CHANGELOG +86 -0
  2. data/LICENSE +184 -0
  3. data/Manifest +173 -0
  4. data/README +205 -0
  5. data/Rakefile +28 -0
  6. data/TODO +2 -0
  7. data/examples/hmph.rb +69 -0
  8. data/generators/tagging/tagging_generator.rb +97 -0
  9. data/generators/tagging/templates/migration.rb +28 -0
  10. data/generators/tagging/templates/tag.rb +39 -0
  11. data/generators/tagging/templates/tag_test.rb +15 -0
  12. data/generators/tagging/templates/tagging.rb +16 -0
  13. data/generators/tagging/templates/tagging_extensions.rb +203 -0
  14. data/generators/tagging/templates/tagging_test.rb +85 -0
  15. data/generators/tagging/templates/taggings.yml +23 -0
  16. data/generators/tagging/templates/tags.yml +7 -0
  17. data/has_many_polymorphs.gemspec +36 -0
  18. data/init.rb +2 -0
  19. data/lib/has_many_polymorphs/association.rb +160 -0
  20. data/lib/has_many_polymorphs/autoload.rb +69 -0
  21. data/lib/has_many_polymorphs/base.rb +60 -0
  22. data/lib/has_many_polymorphs/class_methods.rb +600 -0
  23. data/lib/has_many_polymorphs/configuration.rb +19 -0
  24. data/lib/has_many_polymorphs/debugging_tools.rb +103 -0
  25. data/lib/has_many_polymorphs/rake_task_redefine_task.rb +35 -0
  26. data/lib/has_many_polymorphs/reflection.rb +58 -0
  27. data/lib/has_many_polymorphs/support_methods.rb +88 -0
  28. data/lib/has_many_polymorphs.rb +27 -0
  29. data/test/fixtures/bow_wows.yml +10 -0
  30. data/test/fixtures/cats.yml +18 -0
  31. data/test/fixtures/eaters_foodstuffs.yml +0 -0
  32. data/test/fixtures/fish.yml +12 -0
  33. data/test/fixtures/frogs.yml +5 -0
  34. data/test/fixtures/keep_your_enemies_close.yml +0 -0
  35. data/test/fixtures/little_whale_pupils.yml +0 -0
  36. data/test/fixtures/people.yml +7 -0
  37. data/test/fixtures/petfoods.yml +11 -0
  38. data/test/fixtures/whales.yml +5 -0
  39. data/test/fixtures/wild_boars.yml +10 -0
  40. data/test/generator/tagging_generator_test.rb +42 -0
  41. data/test/integration/app/README +182 -0
  42. data/test/integration/app/Rakefile +19 -0
  43. data/test/integration/app/app/controllers/application.rb +7 -0
  44. data/test/integration/app/app/controllers/bones_controller.rb +5 -0
  45. data/test/integration/app/app/helpers/addresses_helper.rb +2 -0
  46. data/test/integration/app/app/helpers/application_helper.rb +3 -0
  47. data/test/integration/app/app/helpers/bones_helper.rb +2 -0
  48. data/test/integration/app/app/helpers/sellers_helper.rb +28 -0
  49. data/test/integration/app/app/helpers/states_helper.rb +2 -0
  50. data/test/integration/app/app/helpers/users_helper.rb +2 -0
  51. data/test/integration/app/app/models/bone.rb +2 -0
  52. data/test/integration/app/app/models/double_sti_parent.rb +2 -0
  53. data/test/integration/app/app/models/double_sti_parent_relationship.rb +2 -0
  54. data/test/integration/app/app/models/organic_substance.rb +2 -0
  55. data/test/integration/app/app/models/single_sti_parent.rb +4 -0
  56. data/test/integration/app/app/models/single_sti_parent_relationship.rb +4 -0
  57. data/test/integration/app/app/models/stick.rb +2 -0
  58. data/test/integration/app/app/models/stone.rb +2 -0
  59. data/test/integration/app/app/views/addresses/edit.html.erb +12 -0
  60. data/test/integration/app/app/views/addresses/index.html.erb +18 -0
  61. data/test/integration/app/app/views/addresses/new.html.erb +11 -0
  62. data/test/integration/app/app/views/addresses/show.html.erb +3 -0
  63. data/test/integration/app/app/views/bones/index.rhtml +5 -0
  64. data/test/integration/app/app/views/layouts/addresses.html.erb +17 -0
  65. data/test/integration/app/app/views/layouts/sellers.html.erb +17 -0
  66. data/test/integration/app/app/views/layouts/states.html.erb +17 -0
  67. data/test/integration/app/app/views/layouts/users.html.erb +17 -0
  68. data/test/integration/app/app/views/sellers/edit.html.erb +12 -0
  69. data/test/integration/app/app/views/sellers/index.html.erb +20 -0
  70. data/test/integration/app/app/views/sellers/new.html.erb +11 -0
  71. data/test/integration/app/app/views/sellers/show.html.erb +3 -0
  72. data/test/integration/app/app/views/states/edit.html.erb +12 -0
  73. data/test/integration/app/app/views/states/index.html.erb +19 -0
  74. data/test/integration/app/app/views/states/new.html.erb +11 -0
  75. data/test/integration/app/app/views/states/show.html.erb +3 -0
  76. data/test/integration/app/app/views/users/edit.html.erb +12 -0
  77. data/test/integration/app/app/views/users/index.html.erb +22 -0
  78. data/test/integration/app/app/views/users/new.html.erb +11 -0
  79. data/test/integration/app/app/views/users/show.html.erb +3 -0
  80. data/test/integration/app/config/boot.rb +110 -0
  81. data/test/integration/app/config/database.yml +17 -0
  82. data/test/integration/app/config/environment.rb +19 -0
  83. data/test/integration/app/config/environment.rb.canonical +19 -0
  84. data/test/integration/app/config/environments/development.rb +9 -0
  85. data/test/integration/app/config/environments/production.rb +18 -0
  86. data/test/integration/app/config/environments/test.rb +19 -0
  87. data/test/integration/app/config/locomotive.yml +6 -0
  88. data/test/integration/app/config/routes.rb +33 -0
  89. data/test/integration/app/config/ultrasphinx/default.base +56 -0
  90. data/test/integration/app/config/ultrasphinx/development.conf.canonical +155 -0
  91. data/test/integration/app/db/migrate/001_create_sticks.rb +11 -0
  92. data/test/integration/app/db/migrate/002_create_stones.rb +11 -0
  93. data/test/integration/app/db/migrate/003_create_organic_substances.rb +11 -0
  94. data/test/integration/app/db/migrate/004_create_bones.rb +8 -0
  95. data/test/integration/app/db/migrate/005_create_single_sti_parents.rb +11 -0
  96. data/test/integration/app/db/migrate/006_create_double_sti_parents.rb +11 -0
  97. data/test/integration/app/db/migrate/007_create_single_sti_parent_relationships.rb +13 -0
  98. data/test/integration/app/db/migrate/008_create_double_sti_parent_relationships.rb +14 -0
  99. data/test/integration/app/db/migrate/009_create_library_model.rb +11 -0
  100. data/test/integration/app/doc/README_FOR_APP +2 -0
  101. data/test/integration/app/generators/commenting_generator_test.rb +83 -0
  102. data/test/integration/app/lib/library_model.rb +2 -0
  103. data/test/integration/app/public/404.html +30 -0
  104. data/test/integration/app/public/500.html +30 -0
  105. data/test/integration/app/public/dispatch.cgi +10 -0
  106. data/test/integration/app/public/dispatch.fcgi +24 -0
  107. data/test/integration/app/public/dispatch.rb +10 -0
  108. data/test/integration/app/public/favicon.ico +0 -0
  109. data/test/integration/app/public/images/rails.png +0 -0
  110. data/test/integration/app/public/index.html +277 -0
  111. data/test/integration/app/public/javascripts/application.js +2 -0
  112. data/test/integration/app/public/javascripts/controls.js +833 -0
  113. data/test/integration/app/public/javascripts/dragdrop.js +942 -0
  114. data/test/integration/app/public/javascripts/effects.js +1088 -0
  115. data/test/integration/app/public/javascripts/prototype.js +2515 -0
  116. data/test/integration/app/public/robots.txt +1 -0
  117. data/test/integration/app/public/stylesheets/scaffold.css +74 -0
  118. data/test/integration/app/script/about +3 -0
  119. data/test/integration/app/script/breakpointer +3 -0
  120. data/test/integration/app/script/console +3 -0
  121. data/test/integration/app/script/destroy +3 -0
  122. data/test/integration/app/script/generate +3 -0
  123. data/test/integration/app/script/performance/benchmarker +3 -0
  124. data/test/integration/app/script/performance/profiler +3 -0
  125. data/test/integration/app/script/plugin +3 -0
  126. data/test/integration/app/script/process/inspector +3 -0
  127. data/test/integration/app/script/process/reaper +3 -0
  128. data/test/integration/app/script/process/spawner +3 -0
  129. data/test/integration/app/script/runner +3 -0
  130. data/test/integration/app/script/server +3 -0
  131. data/test/integration/app/test/fixtures/double_sti_parent_relationships.yml +7 -0
  132. data/test/integration/app/test/fixtures/double_sti_parents.yml +7 -0
  133. data/test/integration/app/test/fixtures/organic_substances.yml +5 -0
  134. data/test/integration/app/test/fixtures/single_sti_parent_relationships.yml +7 -0
  135. data/test/integration/app/test/fixtures/single_sti_parents.yml +7 -0
  136. data/test/integration/app/test/fixtures/sticks.yml +7 -0
  137. data/test/integration/app/test/fixtures/stones.yml +7 -0
  138. data/test/integration/app/test/functional/addresses_controller_test.rb +57 -0
  139. data/test/integration/app/test/functional/bones_controller_test.rb +8 -0
  140. data/test/integration/app/test/functional/sellers_controller_test.rb +57 -0
  141. data/test/integration/app/test/functional/states_controller_test.rb +57 -0
  142. data/test/integration/app/test/functional/users_controller_test.rb +57 -0
  143. data/test/integration/app/test/test_helper.rb +8 -0
  144. data/test/integration/app/test/unit/bone_test.rb +8 -0
  145. data/test/integration/app/test/unit/double_sti_parent_relationship_test.rb +8 -0
  146. data/test/integration/app/test/unit/double_sti_parent_test.rb +8 -0
  147. data/test/integration/app/test/unit/organic_substance_test.rb +8 -0
  148. data/test/integration/app/test/unit/single_sti_parent_relationship_test.rb +8 -0
  149. data/test/integration/app/test/unit/single_sti_parent_test.rb +8 -0
  150. data/test/integration/app/test/unit/stick_test.rb +8 -0
  151. data/test/integration/app/test/unit/stone_test.rb +8 -0
  152. data/test/integration/server_test.rb +43 -0
  153. data/test/models/aquatic/fish.rb +5 -0
  154. data/test/models/aquatic/pupils_whale.rb +7 -0
  155. data/test/models/aquatic/whale.rb +15 -0
  156. data/test/models/beautiful_fight_relationship.rb +26 -0
  157. data/test/models/canine.rb +9 -0
  158. data/test/models/cat.rb +5 -0
  159. data/test/models/dog.rb +18 -0
  160. data/test/models/eaters_foodstuff.rb +8 -0
  161. data/test/models/frog.rb +4 -0
  162. data/test/models/kitten.rb +3 -0
  163. data/test/models/parentship.rb +4 -0
  164. data/test/models/person.rb +9 -0
  165. data/test/models/petfood.rb +39 -0
  166. data/test/models/tabby.rb +2 -0
  167. data/test/models/wild_boar.rb +3 -0
  168. data/test/modules/extension_module.rb +9 -0
  169. data/test/modules/other_extension_module.rb +9 -0
  170. data/test/patches/symlinked_plugins_1.2.6.diff +46 -0
  171. data/test/schema.rb +87 -0
  172. data/test/setup.rb +14 -0
  173. data/test/test_helper.rb +52 -0
  174. data/test/unit/has_many_polymorphs_test.rb +713 -0
  175. data.tar.gz.sig +1 -0
  176. metadata +279 -0
  177. metadata.gz.sig +0 -0
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+
2
+ * Tag cloud method
data/examples/hmph.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'camping'
2
+ require 'has_many_polymorphs'
3
+
4
+ Camping.goes :Hmph
5
+
6
+ module Hmph::Models
7
+ class GuestsKennel < Base
8
+ belongs_to :kennel
9
+ belongs_to :guest, :polymorphic => true
10
+ end
11
+
12
+ class Dog < Base
13
+ end
14
+
15
+ class Cat < Base
16
+ end
17
+
18
+ class Bird < Base
19
+ end
20
+
21
+ class Kennel < Base
22
+ has_many_polymorphs :guests,
23
+ :from => [:dogs, :cats, :birds],
24
+ :through => :guests_kennels,
25
+ :namespace => :"hmph/models/"
26
+ end
27
+
28
+ class InitialSchema < V 1.0
29
+ def self.up
30
+ create_table :hmph_kennels do |t|
31
+ t.column :created_at, :datetime
32
+ t.column :modified_at, :datetime
33
+ t.column :name, :string, :default => 'Anonymous Kennel'
34
+ end
35
+
36
+ create_table :hmph_guests_kennels do |t|
37
+ t.column :guest_id, :integer
38
+ t.column :guest_type, :string
39
+ t.column :kennel_id, :integer
40
+ end
41
+
42
+ create_table :hmph_dogs do |t|
43
+ t.column :name, :string, :default => 'Fido'
44
+ end
45
+
46
+ create_table :hmph_cats do |t|
47
+ t.column :name, :string, :default => 'Morris'
48
+ end
49
+
50
+ create_table :hmph_birds do |t|
51
+ t.column :name, :string, :default => 'Polly'
52
+ end
53
+ end
54
+
55
+ def self.down
56
+ drop_table :hmph_kennels
57
+ drop_table :hmph_guests_kennels
58
+ drop_table :hmph_dogs
59
+ drop_table :hmph_cats
60
+ drop_table :hmph_birds
61
+ end
62
+ end
63
+ end
64
+
65
+ module Hmph::Controllers
66
+ end
67
+
68
+ module Hmph::Views
69
+ end
@@ -0,0 +1,97 @@
1
+ require 'ruby-debug' and Debugger.start if ENV['USER'] == 'eweaver'
2
+
3
+ class TaggingGenerator < Rails::Generator::NamedBase
4
+ default_options :skip_migration => false
5
+ default_options :self_referential => false
6
+ attr_reader :parent_association_name
7
+ attr_reader :taggable_models
8
+
9
+ def initialize(runtime_args, runtime_options = {})
10
+ parse!(runtime_args, runtime_options)
11
+
12
+ @parent_association_name = (runtime_args.include?("--self-referential") ? "tagger" : "tag")
13
+ @taggable_models = runtime_args.reject{|opt| opt =~ /^--/}.map do |taggable|
14
+ ":" + taggable.underscore.pluralize
15
+ end
16
+ @taggable_models += [":tags"] if runtime_args.include?("--self-referential")
17
+ @taggable_models.uniq!
18
+
19
+ verify @taggable_models
20
+ hacks
21
+ runtime_args.unshift("placeholder")
22
+ super
23
+ end
24
+
25
+ def verify models
26
+ puts "** Warning: only one taggable model specified; tests may not run properly." if models.size < 2
27
+ models.each do |model|
28
+ model = model[1..-1].classify
29
+ next if model == "Tag" # don't load ourselves when --self-referential is used
30
+ self.class.const_get(model) rescue puts "** Error: model #{model[1..-1].classify} could not be loaded." or exit
31
+ end
32
+ end
33
+
34
+ def hacks
35
+ # add the extension require in environment.rb
36
+ phrase = "require 'tagging_extensions'"
37
+ filename = "#{RAILS_ROOT}/config/environment.rb"
38
+ unless (open(filename) do |file|
39
+ file.grep(/#{Regexp.escape phrase}/).any?
40
+ end)
41
+ open(filename, 'a+') do |file|
42
+ file.puts "\n" + phrase + "\n"
43
+ end
44
+ end
45
+ end
46
+
47
+ def manifest
48
+ record do |m|
49
+ m.class_collisions class_path, class_name, "#{class_name}Test"
50
+
51
+ m.directory File.join('app/models', class_path)
52
+ m.directory File.join('test/unit', class_path)
53
+ m.directory File.join('test/fixtures', class_path)
54
+ m.directory File.join('test/fixtures', class_path)
55
+ m.directory File.join('lib')
56
+
57
+ m.template 'tag.rb', File.join('app/models', class_path, "tag.rb")
58
+ m.template 'tag_test.rb', File.join('test/unit', class_path, "tag_test.rb")
59
+ m.template 'tags.yml', File.join('test/fixtures', class_path, "tags.yml")
60
+
61
+ m.template 'tagging.rb', File.join('app/models', class_path, "tagging.rb")
62
+ m.template 'tagging_test.rb', File.join('test/unit', class_path, "tagging_test.rb")
63
+ m.template 'taggings.yml', File.join('test/fixtures', class_path, "taggings.yml")
64
+
65
+ m.template 'tagging_extensions.rb', File.join('lib', 'tagging_extensions.rb')
66
+
67
+ unless options[:skip_migration]
68
+ m.migration_template 'migration.rb', 'db/migrate',
69
+ :migration_file_name => "create_tags_and_taggings"
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ protected
76
+ def banner
77
+ "Usage: #{$0} generate tagging [TaggableModelA TaggableModelB ...]"
78
+ end
79
+
80
+ def add_options!(opt)
81
+ opt.separator ''
82
+ opt.separator 'Options:'
83
+ opt.on("--skip-migration",
84
+ "Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
85
+ opt.on("--self-referential",
86
+ "Allow tags to tag themselves.") { |v| options[:self_referential] = v }
87
+ end
88
+
89
+ # Useful for generating tests/fixtures
90
+ def model_one
91
+ taggable_models[0][1..-1].classify
92
+ end
93
+
94
+ def model_two
95
+ taggable_models[1][1..-1].classify rescue model_one
96
+ end
97
+ end
@@ -0,0 +1,28 @@
1
+
2
+ # A migration to add tables for Tag and Tagging. This file is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
3
+
4
+ class CreateTagsAndTaggings < ActiveRecord::Migration
5
+
6
+ # Add the new tables.
7
+ def self.up
8
+ create_table :tags do |t|
9
+ t.column :name, :string, :null => false
10
+ end
11
+ add_index :tags, :name, :unique => true
12
+
13
+ create_table :taggings do |t|
14
+ t.column :<%= parent_association_name -%>_id, :integer, :null => false
15
+ t.column :taggable_id, :integer, :null => false
16
+ t.column :taggable_type, :string, :null => false
17
+ # t.column :position, :integer # Uncomment this if you need to use <tt>acts_as_list</tt>.
18
+ end
19
+ add_index :taggings, [:<%= parent_association_name -%>_id, :taggable_id, :taggable_type], :unique => true
20
+ end
21
+
22
+ # Remove the tables.
23
+ def self.down
24
+ drop_table :tags
25
+ drop_table :taggings
26
+ end
27
+
28
+ end
@@ -0,0 +1,39 @@
1
+
2
+ # The Tag model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
3
+
4
+ class Tag < ActiveRecord::Base
5
+
6
+ DELIMITER = " " # Controls how to split and join tagnames from strings. You may need to change the <tt>validates_format_of parameters</tt> if you change this.
7
+
8
+ # If database speed becomes an issue, you could remove these validations and rescue the ActiveRecord database constraint errors instead.
9
+ validates_presence_of :name
10
+ validates_uniqueness_of :name, :case_sensitive => false
11
+
12
+ # Change this validation if you need more complex tag names.
13
+ validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, :message => "can not contain special characters"
14
+
15
+ # Set up the polymorphic relationship.
16
+ has_many_polymorphs :taggables,
17
+ :from => [<%= taggable_models.join(", ") %>],
18
+ :through => :taggings,
19
+ :dependent => :destroy,
20
+ <% if options[:self_referential] -%> :as => :<%= parent_association_name -%>,
21
+ <% end -%>
22
+ :skip_duplicates => false,
23
+ :parent_extend => proc {
24
+ # Defined on the taggable models, not on Tag itself. Return the tagnames associated with this record as a string.
25
+ def to_s
26
+ self.map(&:name).sort.join(Tag::DELIMITER)
27
+ end
28
+ }
29
+
30
+ # Callback to strip extra spaces from the tagname before saving it. If you allow tags to be renamed later, you might want to use the <tt>before_save</tt> callback instead.
31
+ def before_create
32
+ self.name = name.downcase.strip.squeeze(" ")
33
+ end
34
+
35
+ # Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if something goes wrong.
36
+ class Error < StandardError
37
+ end
38
+
39
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class TagTest < ActiveSupport::TestCase
4
+ fixtures <%= taggable_models[0..1].join(", ") -%>
5
+
6
+ def setup
7
+ @obj = <%= model_two %>.find(:first)
8
+ @obj.tag_with "pale imperial"
9
+ end
10
+
11
+ def test_to_s
12
+ assert_equal "imperial pale", <%= model_two -%>.find(:first).tags.to_s
13
+ end
14
+
15
+ end
@@ -0,0 +1,16 @@
1
+
2
+ # The Tagging join model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
3
+
4
+ class Tagging < ActiveRecord::Base
5
+
6
+ belongs_to :<%= parent_association_name -%><%= ", :foreign_key => \"#{parent_association_name}_id\", :class_name => \"Tag\"" if options[:self_referential] %>
7
+ belongs_to :taggable, :polymorphic => true
8
+
9
+ # If you also need to use <tt>acts_as_list</tt>, you will have to manage the tagging positions manually by creating decorated join records when you associate Tags with taggables.
10
+ # acts_as_list :scope => :taggable
11
+
12
+ # This callback makes sure that an orphaned <tt>Tag</tt> is deleted if it no longer tags anything.
13
+ def after_destroy
14
+ <%= parent_association_name -%>.destroy_without_callbacks if <%= parent_association_name -%> and <%= parent_association_name -%>.taggings.count == 0
15
+ end
16
+ end
@@ -0,0 +1,203 @@
1
+ class ActiveRecord::Base #:nodoc:
2
+
3
+ # These extensions make models taggable. This file is automatically generated and required by your app if you run the tagging generator included with has_many_polymorphs.
4
+ module TaggingExtensions
5
+
6
+ # Add tags to <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
7
+ #
8
+ # We need to avoid name conflicts with the built-in ActiveRecord association methods, thus the underscores.
9
+ def _add_tags incoming
10
+ taggable?(true)
11
+ tag_cast_to_string(incoming).each do |tag_name|
12
+ begin
13
+ tag = Tag.find_or_create_by_name(tag_name)
14
+ raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record?
15
+ tags << tag
16
+ rescue ActiveRecord::StatementInvalid => e
17
+ raise unless e.to_s =~ /duplicate/i
18
+ end
19
+ end
20
+ end
21
+
22
+ # Removes tags from <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
23
+ def _remove_tags outgoing
24
+ taggable?(true)
25
+ outgoing = tag_cast_to_string(outgoing)
26
+ <% if options[:self_referential] %>
27
+ # because of http://dev.rubyonrails.org/ticket/6466
28
+ taggings.destroy(*(taggings.find(:all, :include => :<%= parent_association_name -%>).select do |tagging|
29
+ outgoing.include? tagging.<%= parent_association_name -%>.name
30
+ end))
31
+ <% else -%>
32
+ <%= parent_association_name -%>s.delete(*(<%= parent_association_name -%>s.select do |tag|
33
+ outgoing.include? tag.name
34
+ end))
35
+ <% end -%>
36
+ end
37
+
38
+ # Returns the tags on <tt>self</tt> as a string.
39
+ def tag_list
40
+ # Redefined later to avoid an RDoc parse error.
41
+ end
42
+
43
+ # Replace the existing tags on <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
44
+ def tag_with list
45
+ #:stopdoc:
46
+ taggable?(true)
47
+ list = tag_cast_to_string(list)
48
+
49
+ # Transactions may not be ideal for you here; be aware.
50
+ Tag.transaction do
51
+ current = <%= parent_association_name -%>s.map(&:name)
52
+ _add_tags(list - current)
53
+ _remove_tags(current - list)
54
+ end
55
+
56
+ self
57
+ #:startdoc:
58
+ end
59
+
60
+ # Returns the tags on <tt>self</tt> as a string.
61
+ def tag_list #:nodoc:
62
+ #:stopdoc:
63
+ taggable?(true)
64
+ <%= parent_association_name -%>s.reload
65
+ <%= parent_association_name -%>s.to_s
66
+ #:startdoc:
67
+ end
68
+
69
+ def tag_list=(value)
70
+ tag_with(value)
71
+ end
72
+
73
+ private
74
+
75
+ def tag_cast_to_string obj #:nodoc:
76
+ case obj
77
+ when Array
78
+ obj.map! do |item|
79
+ case item
80
+ when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot.
81
+ when Tag then item.name
82
+ when String then item
83
+ else
84
+ raise "Invalid type"
85
+ end
86
+ end
87
+ when String
88
+ obj = obj.split(Tag::DELIMITER).map do |tag_name|
89
+ tag_name.strip.squeeze(" ")
90
+ end
91
+ else
92
+ raise "Invalid object of class #{obj.class} as tagging method parameter"
93
+ end.flatten.compact.map(&:downcase).uniq
94
+ end
95
+
96
+ # Check if a model is in the :taggables target list. The alternative to this check is to explicitly include a TaggingMethods module (which you would create) in each target model.
97
+ def taggable?(should_raise = false) #:nodoc:
98
+ unless flag = respond_to?(:<%= parent_association_name -%>s)
99
+ raise "#{self.class} is not a taggable model" if should_raise
100
+ end
101
+ flag
102
+ end
103
+
104
+ end
105
+
106
+ module TaggingFinders
107
+ # Find all the objects tagged with the supplied list of tags
108
+ #
109
+ # Usage : Model.tagged_with("ruby")
110
+ # Model.tagged_with("hello", "world")
111
+ # Model.tagged_with("hello", "world", :limit => 10)
112
+ #
113
+ # XXX This query strategy is not performant, and needs to be rewritten as an inverted join or a series of unions
114
+ #
115
+ def tagged_with(*tag_list)
116
+ options = tag_list.last.is_a?(Hash) ? tag_list.pop : {}
117
+ tag_list = parse_tags(tag_list)
118
+
119
+ scope = scope(:find)
120
+ options[:select] ||= "#{table_name}.*"
121
+ options[:from] ||= "#{table_name}, tags, taggings"
122
+
123
+ sql = "SELECT #{(scope && scope[:select]) || options[:select]} "
124
+ sql << "FROM #{(scope && scope[:from]) || options[:from]} "
125
+
126
+ add_joins!(sql, options[:joins], scope)
127
+
128
+ sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id "
129
+ sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' "
130
+ sql << "AND taggings.tag_id = tags.id "
131
+
132
+ tag_list_condition = tag_list.map {|name| "'#{name}'"}.join(", ")
133
+
134
+ sql << "AND (tags.name IN (#{sanitize_sql(tag_list_condition)})) "
135
+ sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
136
+
137
+ columns = column_names.map do |column|
138
+ "#{table_name}.#{column}"
139
+ end.join(", ")
140
+
141
+ sql << "GROUP BY #{columns} "
142
+ sql << "HAVING COUNT(taggings.tag_id) = #{tag_list.size}"
143
+
144
+ add_order!(sql, options[:order], scope)
145
+ add_limit!(sql, options, scope)
146
+ add_lock!(sql, options, scope)
147
+
148
+ find_by_sql(sql)
149
+ end
150
+
151
+ def self.tagged_with_any(*tag_list)
152
+ options = tag_list.last.is_a?(Hash) ? tag_list.pop : {}
153
+ tag_list = parse_tags(tag_list)
154
+
155
+ scope = scope(:find)
156
+ options[:select] ||= "#{table_name}.*"
157
+ options[:from] ||= "#{table_name}, tags, taggings"
158
+
159
+ sql = "SELECT #{(scope && scope[:select]) || options[:select]} "
160
+ sql << "FROM #{(scope && scope[:from]) || options[:from]} "
161
+
162
+ add_joins!(sql, options, scope)
163
+
164
+ sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id "
165
+ sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' "
166
+ sql << "AND taggings.tag_id = tags.id "
167
+
168
+ sql << "AND ("
169
+ or_options = []
170
+ tag_list.each do |name|
171
+ or_options << "tags.name = '#{name}'"
172
+ end
173
+ or_options_joined = or_options.join(" OR ")
174
+ sql << "#{or_options_joined}) "
175
+
176
+
177
+ sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
178
+
179
+ columns = column_names.map do |column|
180
+ "#{table_name}.#{column}"
181
+ end.join(", ")
182
+
183
+ sql << "GROUP BY #{columns} "
184
+
185
+ add_order!(sql, options[:order], scope)
186
+ add_limit!(sql, options, scope)
187
+ add_lock!(sql, options, scope)
188
+
189
+ find_by_sql(sql)
190
+ end
191
+
192
+ def parse_tags(tags)
193
+ return [] if tags.blank?
194
+ tags = Array(tags).first
195
+ tags = tags.respond_to?(:flatten) ? tags.flatten : tags.split(Tag::DELIMITER)
196
+ tags.map { |tag| tag.strip.squeeze(" ") }.flatten.compact.map(&:downcase).uniq
197
+ end
198
+
199
+ end
200
+
201
+ include TaggingExtensions
202
+ extend TaggingFinders
203
+ end
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class TaggingTest < ActiveSupport::TestCase
4
+ fixtures :tags, :taggings, <%= taggable_models[0..1].join(", ") -%>
5
+
6
+ def setup
7
+ @objs = <%= model_two %>.find(:all, :limit => 2)
8
+
9
+ @obj1 = @objs[0]
10
+ @obj1.tag_with("pale")
11
+ @obj1.reload
12
+
13
+ @obj2 = @objs[1]
14
+ @obj2.tag_with("pale imperial")
15
+ @obj2.reload
16
+
17
+ <% if taggable_models.size > 1 -%>
18
+ @obj3 = <%= model_one -%>.find(:first)
19
+ <% end -%>
20
+ @tag1 = Tag.find(1)
21
+ @tag2 = Tag.find(2)
22
+ @tagging1 = Tagging.find(1)
23
+ end
24
+
25
+ def test_tag_with
26
+ @obj2.tag_with "hoppy pilsner"
27
+ assert_equal "hoppy pilsner", @obj2.tag_list
28
+ end
29
+
30
+ def test_find_tagged_with
31
+ @obj1.tag_with "seasonal lager ipa"
32
+ @obj2.tag_with ["lager", "stout", "fruity", "seasonal"]
33
+
34
+ result1 = [@obj1]
35
+ assert_equal <%= model_two %>.tagged_with("ipa"), result1
36
+ assert_equal <%= model_two %>.tagged_with("ipa lager"), result1
37
+ assert_equal <%= model_two %>.tagged_with("ipa", "lager"), result1
38
+
39
+ result2 = [@obj1.id, @obj2.id].sort
40
+ assert_equal <%= model_two %>.tagged_with("seasonal").map(&:id).sort, result2
41
+ assert_equal <%= model_two %>.tagged_with("seasonal lager").map(&:id).sort, result2
42
+ assert_equal <%= model_two %>.tagged_with("seasonal", "lager").map(&:id).sort, result2
43
+ end
44
+
45
+ <% if options[:self_referential] -%>
46
+ def test_self_referential_tag_with
47
+ @tag1.tag_with [1, 2]
48
+ assert @tag1.tags.any? {|obj| obj == @tag1}
49
+ assert !@tag2.tags.any? {|obj| obj == @tag1}
50
+ end
51
+
52
+ <% end -%>
53
+ def test__add_tags
54
+ @obj1._add_tags "porter longneck"
55
+ assert Tag.find_by_name("porter").taggables.any? {|obj| obj == @obj1}
56
+ assert Tag.find_by_name("longneck").taggables.any? {|obj| obj == @obj1}
57
+ assert_equal "longneck pale porter", @obj1.tag_list
58
+
59
+ @obj1._add_tags [2]
60
+ assert_equal "imperial longneck pale porter", @obj1.tag_list
61
+ end
62
+
63
+ def test__remove_tags
64
+ @obj2._remove_tags ["2", @tag1]
65
+ assert @obj2.tags.empty?
66
+ end
67
+
68
+ def test_tag_list
69
+ assert_equal "imperial pale", @obj2.tag_list
70
+ end
71
+
72
+ def test_taggable
73
+ assert_raises(RuntimeError) do
74
+ @tagging1.send(:taggable?, true)
75
+ end
76
+ assert !@tagging1.send(:taggable?)
77
+ <% if taggable_models.size > 1 -%>
78
+ assert @obj3.send(:taggable?)
79
+ <% end -%>
80
+ <% if options[:self_referential] -%>
81
+ assert @tag1.send(:taggable?)
82
+ <% end -%>
83
+ end
84
+
85
+ end
@@ -0,0 +1,23 @@
1
+ ---
2
+ <% if taggable_models.size > 1 -%>
3
+ taggings_003:
4
+ <%= parent_association_name -%>_id: "2"
5
+ id: "3"
6
+ taggable_type: <%= model_one %>
7
+ taggable_id: "1"
8
+ <% end -%>
9
+ taggings_004:
10
+ <%= parent_association_name -%>_id: "2"
11
+ id: "4"
12
+ taggable_type: <%= model_two %>
13
+ taggable_id: "2"
14
+ taggings_001:
15
+ <%= parent_association_name -%>_id: "1"
16
+ id: "1"
17
+ taggable_type: <%= model_two %>
18
+ taggable_id: "1"
19
+ taggings_002:
20
+ <%= parent_association_name -%>_id: "1"
21
+ id: "2"
22
+ taggable_type: <%= model_two %>
23
+ taggable_id: "2"
@@ -0,0 +1,7 @@
1
+ ---
2
+ tags_001:
3
+ name: pale
4
+ id: "1"
5
+ tags_002:
6
+ name: imperial
7
+ id: "2"
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{has_many_polymorphs}
5
+ s.version = "2.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = [""]
9
+ s.cert_chain = ["/Users/eweaver/p/configuration/gem_certificates/evan_weaver-original-public_cert.pem"]
10
+ s.date = %q{2009-11-18}
11
+ s.description = %q{An ActiveRecord plugin for self-referential and double-sided polymorphic associations.}
12
+ s.email = %q{}
13
+ s.extra_rdoc_files = ["CHANGELOG", "generators/tagging/templates/migration.rb", "generators/tagging/templates/tag.rb", "generators/tagging/templates/tagging.rb", "generators/tagging/templates/tagging_extensions.rb", "lib/has_many_polymorphs/association.rb", "lib/has_many_polymorphs/autoload.rb", "lib/has_many_polymorphs/class_methods.rb", "lib/has_many_polymorphs/configuration.rb", "lib/has_many_polymorphs/reflection.rb", "LICENSE", "README", "test/integration/app/doc/README_FOR_APP", "test/integration/app/README", "TODO"]
14
+ s.files = ["CHANGELOG", "examples/hmph.rb", "generators/tagging/tagging_generator.rb", "generators/tagging/templates/migration.rb", "generators/tagging/templates/tag.rb", "generators/tagging/templates/tag_test.rb", "generators/tagging/templates/tagging.rb", "generators/tagging/templates/tagging_extensions.rb", "generators/tagging/templates/tagging_test.rb", "generators/tagging/templates/taggings.yml", "generators/tagging/templates/tags.yml", "init.rb", "lib/has_many_polymorphs/association.rb", "lib/has_many_polymorphs/autoload.rb", "lib/has_many_polymorphs/base.rb", "lib/has_many_polymorphs/class_methods.rb", "lib/has_many_polymorphs/configuration.rb", "lib/has_many_polymorphs/debugging_tools.rb", "lib/has_many_polymorphs/rake_task_redefine_task.rb", "lib/has_many_polymorphs/reflection.rb", "lib/has_many_polymorphs/support_methods.rb", "lib/has_many_polymorphs.rb", "LICENSE", "Manifest", "Rakefile", "README", "test/fixtures/bow_wows.yml", "test/fixtures/cats.yml", "test/fixtures/eaters_foodstuffs.yml", "test/fixtures/fish.yml", "test/fixtures/frogs.yml", "test/fixtures/keep_your_enemies_close.yml", "test/fixtures/little_whale_pupils.yml", "test/fixtures/people.yml", "test/fixtures/petfoods.yml", "test/fixtures/whales.yml", "test/fixtures/wild_boars.yml", "test/generator/tagging_generator_test.rb", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/bones_controller.rb", "test/integration/app/app/helpers/addresses_helper.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/bones_helper.rb", "test/integration/app/app/helpers/sellers_helper.rb", "test/integration/app/app/helpers/states_helper.rb", "test/integration/app/app/helpers/users_helper.rb", "test/integration/app/app/models/bone.rb", "test/integration/app/app/models/double_sti_parent.rb", "test/integration/app/app/models/double_sti_parent_relationship.rb", "test/integration/app/app/models/organic_substance.rb", "test/integration/app/app/models/single_sti_parent.rb", "test/integration/app/app/models/single_sti_parent_relationship.rb", "test/integration/app/app/models/stick.rb", "test/integration/app/app/models/stone.rb", "test/integration/app/app/views/addresses/edit.html.erb", "test/integration/app/app/views/addresses/index.html.erb", "test/integration/app/app/views/addresses/new.html.erb", "test/integration/app/app/views/addresses/show.html.erb", "test/integration/app/app/views/bones/index.rhtml", "test/integration/app/app/views/layouts/addresses.html.erb", "test/integration/app/app/views/layouts/sellers.html.erb", "test/integration/app/app/views/layouts/states.html.erb", "test/integration/app/app/views/layouts/users.html.erb", "test/integration/app/app/views/sellers/edit.html.erb", "test/integration/app/app/views/sellers/index.html.erb", "test/integration/app/app/views/sellers/new.html.erb", "test/integration/app/app/views/sellers/show.html.erb", "test/integration/app/app/views/states/edit.html.erb", "test/integration/app/app/views/states/index.html.erb", "test/integration/app/app/views/states/new.html.erb", "test/integration/app/app/views/states/show.html.erb", "test/integration/app/app/views/users/edit.html.erb", "test/integration/app/app/views/users/index.html.erb", "test/integration/app/app/views/users/new.html.erb", "test/integration/app/app/views/users/show.html.erb", "test/integration/app/config/boot.rb", "test/integration/app/config/database.yml", "test/integration/app/config/environment.rb", "test/integration/app/config/environment.rb.canonical", "test/integration/app/config/environments/development.rb", "test/integration/app/config/environments/production.rb", "test/integration/app/config/environments/test.rb", "test/integration/app/config/locomotive.yml", "test/integration/app/config/routes.rb", "test/integration/app/config/ultrasphinx/default.base", "test/integration/app/config/ultrasphinx/development.conf.canonical", "test/integration/app/db/migrate/001_create_sticks.rb", "test/integration/app/db/migrate/002_create_stones.rb", "test/integration/app/db/migrate/003_create_organic_substances.rb", "test/integration/app/db/migrate/004_create_bones.rb", "test/integration/app/db/migrate/005_create_single_sti_parents.rb", "test/integration/app/db/migrate/006_create_double_sti_parents.rb", "test/integration/app/db/migrate/007_create_single_sti_parent_relationships.rb", "test/integration/app/db/migrate/008_create_double_sti_parent_relationships.rb", "test/integration/app/db/migrate/009_create_library_model.rb", "test/integration/app/doc/README_FOR_APP", "test/integration/app/generators/commenting_generator_test.rb", "test/integration/app/lib/library_model.rb", "test/integration/app/public/404.html", "test/integration/app/public/500.html", "test/integration/app/public/dispatch.cgi", "test/integration/app/public/dispatch.fcgi", "test/integration/app/public/dispatch.rb", "test/integration/app/public/favicon.ico", "test/integration/app/public/images/rails.png", "test/integration/app/public/index.html", "test/integration/app/public/javascripts/application.js", "test/integration/app/public/javascripts/controls.js", "test/integration/app/public/javascripts/dragdrop.js", "test/integration/app/public/javascripts/effects.js", "test/integration/app/public/javascripts/prototype.js", "test/integration/app/public/robots.txt", "test/integration/app/public/stylesheets/scaffold.css", "test/integration/app/Rakefile", "test/integration/app/README", "test/integration/app/script/about", "test/integration/app/script/breakpointer", "test/integration/app/script/console", "test/integration/app/script/destroy", "test/integration/app/script/generate", "test/integration/app/script/performance/benchmarker", "test/integration/app/script/performance/profiler", "test/integration/app/script/plugin", "test/integration/app/script/process/inspector", "test/integration/app/script/process/reaper", "test/integration/app/script/process/spawner", "test/integration/app/script/runner", "test/integration/app/script/server", "test/integration/app/test/fixtures/double_sti_parent_relationships.yml", "test/integration/app/test/fixtures/double_sti_parents.yml", "test/integration/app/test/fixtures/organic_substances.yml", "test/integration/app/test/fixtures/single_sti_parent_relationships.yml", "test/integration/app/test/fixtures/single_sti_parents.yml", "test/integration/app/test/fixtures/sticks.yml", "test/integration/app/test/fixtures/stones.yml", "test/integration/app/test/functional/addresses_controller_test.rb", "test/integration/app/test/functional/bones_controller_test.rb", "test/integration/app/test/functional/sellers_controller_test.rb", "test/integration/app/test/functional/states_controller_test.rb", "test/integration/app/test/functional/users_controller_test.rb", "test/integration/app/test/test_helper.rb", "test/integration/app/test/unit/bone_test.rb", "test/integration/app/test/unit/double_sti_parent_relationship_test.rb", "test/integration/app/test/unit/double_sti_parent_test.rb", "test/integration/app/test/unit/organic_substance_test.rb", "test/integration/app/test/unit/single_sti_parent_relationship_test.rb", "test/integration/app/test/unit/single_sti_parent_test.rb", "test/integration/app/test/unit/stick_test.rb", "test/integration/app/test/unit/stone_test.rb", "test/integration/server_test.rb", "test/models/aquatic/fish.rb", "test/models/aquatic/pupils_whale.rb", "test/models/aquatic/whale.rb", "test/models/beautiful_fight_relationship.rb", "test/models/canine.rb", "test/models/cat.rb", "test/models/dog.rb", "test/models/eaters_foodstuff.rb", "test/models/frog.rb", "test/models/kitten.rb", "test/models/parentship.rb", "test/models/person.rb", "test/models/petfood.rb", "test/models/tabby.rb", "test/models/wild_boar.rb", "test/modules/extension_module.rb", "test/modules/other_extension_module.rb", "test/patches/symlinked_plugins_1.2.6.diff", "test/schema.rb", "test/setup.rb", "test/test_helper.rb", "test/unit/has_many_polymorphs_test.rb", "TODO", "has_many_polymorphs.gemspec"]
15
+ s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/has_many_polymorphs/}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Has_many_polymorphs", "--main", "README"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{fauna}
19
+ s.rubygems_version = %q{1.3.5}
20
+ s.signing_key = %q{/Users/eweaver/p/configuration/gem_certificates/evan_weaver-original-private_key.pem}
21
+ s.summary = %q{An ActiveRecord plugin for self-referential and double-sided polymorphic associations.}
22
+ s.test_files = ["test/generator/tagging_generator_test.rb", "test/integration/server_test.rb", "test/unit/has_many_polymorphs_test.rb"]
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 3
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<activerecord>, [">= 0"])
32
+ end
33
+ else
34
+ s.add_dependency(%q<activerecord>, [">= 0"])
35
+ end
36
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+
2
+ require 'has_many_polymorphs'