acts-as-joinable 0.0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Lance Pollard (lancejpollard@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,54 @@
1
+ # ActsAsJoinable
2
+
3
+ It extends the functionality of `has_and_belongs_to_many`, conventionalizing common use cases.
4
+
5
+ ## Usage
6
+
7
+ ### Install
8
+
9
+ sudo gem install acts-as-joinable
10
+
11
+ ### Add Relationships
12
+
13
+ Say you have `Post` and `Asset` models. If each `has_many` of each other `through` some join model, you can write it as such:
14
+
15
+ class Post < ActiveRecord::Base
16
+ acts_as_joinable_on :assets, :layouts, :tags, :slugs
17
+ end
18
+
19
+ class Asset < ActiveRecord::Base
20
+ acts_as_joinable
21
+ end
22
+
23
+ That is a replacement for the longer (and non-polymorphic):
24
+
25
+ class Post < ActiveRecord::Base
26
+ has_and_belongs_to_many :assets
27
+ end
28
+
29
+ class Asset < ActiveRecord::Base
30
+ has_and_belongs_to_many :posts
31
+ end
32
+
33
+ ## Why
34
+
35
+ Many-to-many relationships end up requiring the same features 99% of the time:
36
+
37
+ 1. Join Table that keeps track of `context`
38
+ - [ActsAsTaggableOn](http://github.com/mbleigh/acts-as-taggable-on/blob/master/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb)
39
+ - ActsAsAuthorized
40
+ - [Preferences](http://github.com/pluginaweek/preferences/blob/master/generators/preferences/templates/001_create_preferences.rb)
41
+ - [FriendlyId](http://github.com/norman/friendly_id/blob/ca9821192c8c3c4e81a938603151645c7cbe1470/generators/friendly_id/templates/create_slugs.rb)
42
+
43
+ It looks like this:
44
+
45
+ create_table :relationships do |t|
46
+ t.references :parent, :polymorphic => true
47
+ t.references :child, :polymorphic => true
48
+ t.string :context
49
+ t.timestamps
50
+ end
51
+
52
+ ## Alternatives
53
+
54
+ - [ActsAsRelationable](http://github.com/winton/acts_as_relationable)
data/Rakefile ADDED
@@ -0,0 +1,78 @@
1
+ require 'rake'
2
+ require "rake/rdoctask"
3
+ require 'rake/gempackagetask'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = "acts-as-joinable"
7
+ s.authors = ["Lance Pollard"]
8
+ s.version = "0.0.1.5"
9
+ s.summary = "ActsAsJoinable: DRYing up Many-to-Many Relationships in ActiveRecord"
10
+ s.homepage = "http://github.com/viatropos/cockpit"
11
+ s.email = "lancejpollard@gmail.com"
12
+ s.description = "DRYing up Many-to-Many Relationships in ActiveRecord"
13
+ s.has_rdoc = false
14
+ s.rubyforge_project = "acts-as-joinable"
15
+ s.platform = Gem::Platform::RUBY
16
+ s.files = %w(README.markdown Rakefile init.rb MIT-LICENSE) + Dir["{lib,rails,test}/**/*"] - Dir["test/tmp"]
17
+ s.require_path = "lib"
18
+ end
19
+
20
+ Rake::GemPackageTask.new(spec) do |pkg|
21
+ pkg.gem_spec = spec
22
+ pkg.package_dir = "pkg"
23
+ end
24
+
25
+ desc 'run unit tests'
26
+ task :test do
27
+ Dir["test/**/*"].each do |file|
28
+ next unless File.basename(file) =~ /test_/
29
+ next unless File.extname(file) == ".rb"
30
+ system "ruby #{file}"
31
+ end
32
+ end
33
+
34
+ desc "Create .gemspec file (useful for github)"
35
+ task :gemspec do
36
+ File.open("pkg/#{spec.name}.gemspec", "w") do |f|
37
+ f.puts spec.to_ruby
38
+ end
39
+ end
40
+
41
+ desc "Build the gem into the current directory"
42
+ task :gem => :gemspec do
43
+ `gem build pkg/#{spec.name}.gemspec`
44
+ end
45
+
46
+ desc "Publish gem to rubygems"
47
+ task :publish => [:package] do
48
+ %x[gem push pkg/#{spec.name}-#{spec.version}.gem]
49
+ end
50
+
51
+ desc "Print a list of the files to be put into the gem"
52
+ task :manifest do
53
+ File.open("Manifest", "w") do |f|
54
+ spec.files.each do |file|
55
+ f.puts file
56
+ end
57
+ end
58
+ end
59
+
60
+ desc "Install the gem locally"
61
+ task :install => [:package] do
62
+ File.mkdir("pkg") unless File.exists?("pkg")
63
+ command = "gem install pkg/#{spec.name}-#{spec.version} --no-ri --no-rdoc"
64
+ command = "sudo #{command}" if ENV["SUDO"] == true
65
+ sh %{#{command}}
66
+ end
67
+
68
+ desc "Generate the rdoc"
69
+ Rake::RDocTask.new do |rdoc|
70
+ files = ["README.markdown", "lib/**/*.rb"]
71
+ rdoc.rdoc_files.add(files)
72
+ rdoc.main = "README.markdown"
73
+ rdoc.title = spec.summary
74
+ end
75
+
76
+ task :yank do
77
+ `gem yank #{spec.name} -v #{spec.version}`
78
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ File.dirname(__FILE__) + "/rails/init.rb"
@@ -0,0 +1,172 @@
1
+ module ActsAsJoinable
2
+
3
+ def self.models
4
+ @models ||= Dir[Rails.root + "/app/models/*.rb"].collect { |f| File.basename f, '.rb' }
5
+ end
6
+
7
+ def self.models=(value)
8
+ @models = value
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend ClassMethods
13
+ end
14
+
15
+ module ClassMethods
16
+ # the parent in the relationship, so to speak
17
+ def acts_as_joinable_on(*args, &block)
18
+ if args.empty? # Relationship model
19
+ belongs_to :parent, :polymorphic => true
20
+ belongs_to :child, :polymorphic => true
21
+
22
+ ActsAsJoinable.models.each do |m|
23
+ belongs_to "parent_#{m}".intern, :foreign_key => 'parent_id', :class_name => m.camelize
24
+ belongs_to "child_#{m}".intern, :foreign_key => 'child_id', :class_name => m.camelize
25
+ end
26
+ else
27
+ options = args.extract_options!
28
+ sql = options[:conditions]
29
+ table = options[:table]
30
+ fields = options[:fields] || []
31
+ fields = [ fields ] unless fields.respond_to?(:flatten)
32
+
33
+ has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child
34
+ has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent
35
+
36
+ args.each do |type|
37
+ type = type.to_s
38
+ table = table || type
39
+ select = "#{table}.*, relationships.id AS relationship_id#{fields.empty? ? '' : ', '}" + fields.collect { |f| "relationships.#{f}" }.join(', ')
40
+ opts = {
41
+ :select => select,
42
+ :conditions => sql,
43
+ :through => :parent_relationships,
44
+ :source => :parent,
45
+ :class_name => type.classify,
46
+ :source_type => table.classify
47
+ }
48
+
49
+ has_many "parent_#{type}", opts do
50
+ fields.each do |field|
51
+ define_method field.to_s.pluralize do |*args|
52
+ value = args[0] || 1
53
+ scoped :conditions => [ "relationships.#{field} = ?", value ]
54
+ end
55
+ end
56
+ end
57
+
58
+ opts = {
59
+ :select => select,
60
+ :conditions => sql,
61
+ :through => :child_relationships,
62
+ :source => :child,
63
+ :class_name => type.classify,
64
+ :source_type => table.classify
65
+ }
66
+
67
+ has_many "child_#{type}", opts do
68
+ fields.each do |field|
69
+ define_method field.to_s.pluralize do |*args|
70
+ value = args[0] || 1
71
+ scoped :conditions => [ "relationships.#{field} = ?", value ]
72
+ end
73
+ end
74
+ end
75
+
76
+ self.class_eval do
77
+ # Records reader
78
+ define_method type do |*args|
79
+ if (read_attribute(:type) || self.class.to_s) < (args.empty? ? type.classify : args[0].to_s)
80
+ eval "self.child_#{type}"
81
+ else
82
+ eval "self.parent_#{type}"
83
+ end
84
+ end
85
+ end
86
+
87
+ fields.each do |field|
88
+ # Relationship field writer
89
+ self.class_eval do
90
+ define_method field.to_s + '=' do |value|
91
+ modified = read_attribute(:modified_relationship_fields) || []
92
+ modified << field
93
+ write_attribute :modified_relationship_fields, modified.uniq
94
+ write_attribute field, value
95
+ end
96
+ end
97
+ end
98
+ end
99
+ unless included_modules.include?(InstanceMethods)
100
+ extend ClassMethods
101
+ include InstanceMethods
102
+ before_save :save_relationship_fields
103
+ end
104
+ end
105
+ end
106
+
107
+ # the child in the relationship, so to speak
108
+ def acts_as_joinable(*args, &block)
109
+ acts_as_joinable_on(*args, &block)
110
+ end
111
+ end
112
+
113
+ module InstanceMethods
114
+ # Before save
115
+ def save_relationship_fields
116
+ return unless read_attribute(:relationship_id) && read_attribute(:modified_relationship_fields)
117
+ r = Relationship.find self.relationship_id
118
+ read_attribute(:modified_relationship_fields).each do |field|
119
+ r[field] = self[field]
120
+ end
121
+ r.save
122
+ write_attribute :modified_relationship_fields, nil
123
+ end
124
+
125
+ def get_joining(type, context)
126
+ get_joinings(type, context).first
127
+ end
128
+
129
+ def get_joinings(type, context)
130
+ self.joinings.select do |joining|
131
+ joining.context == context.to_s
132
+ end
133
+ end
134
+
135
+ def get_joined(type, context)
136
+ get_joineds(type, context).first
137
+ end
138
+
139
+ def get_joineds(type, context)
140
+ return [] unless self.joineds and !self.joineds.empty?
141
+
142
+ get_joinings(context).collect do |joining|
143
+ joining.joined
144
+ end
145
+ end
146
+
147
+ def set_joined(type, context, value)
148
+ joining = get_joining(context) || Joining.new
149
+ clazz = get_join_class(type)
150
+ joining.joined = value.is_a?(clazz) ? value : clazz.find(value)
151
+ joining.joining = self
152
+ joining.context = context.to_s
153
+ joining.save
154
+ self.send("#{context.to_s}_#{type.to_s}")
155
+ end
156
+
157
+ private
158
+ def get_join_class(type)
159
+ if type.is_a?(String) || type.is_a?(Symbol)
160
+ type.to_s.camelize.constantize
161
+ elsif type.is_a?(Class)
162
+ type
163
+ else
164
+ type.class
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ ActiveRecord::Base.send(:include, ActsAsJoinable) if defined?(ActiveRecord::Base)
171
+
172
+ Dir["#{File.dirname(__FILE__)}/../app/models/*"].each { |c| require c if File.extname(c) == ".rb" }
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'acts-as-joinable'
2
+
3
+ ActiveRecord::Base.send :include, ActsAsJoinable
@@ -0,0 +1,33 @@
1
+ begin
2
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
3
+ rescue ArgumentError
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
5
+ end
6
+
7
+ ActiveRecord::Base.configurations = true
8
+
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+
11
+ create_table :posts, :force => true do |t|
12
+ t.string :title
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :assets, :force => true do |t|
17
+ t.string :title
18
+ t.timestamps
19
+ end
20
+
21
+ create_table :tags, :force => true do |t|
22
+ t.string :name
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :relationships do |t|
27
+ t.references :parent, :polymorphic => true
28
+ t.references :child, :polymorphic => true
29
+ t.string :context
30
+ t.timestamps
31
+ end
32
+
33
+ end
data/test/lib/asset.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Asset < ActiveRecord::Base
2
+ acts_as_joinable
3
+ end
data/test/lib/post.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Post < ActiveRecord::Base
2
+ acts_as_joinable_on :assets, :tags
3
+ end
data/test/lib/tag.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Tag < ActiveRecord::Base
2
+ acts_as_joinable
3
+ end
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ActsAsJoinableTest < ActiveSupport::TestCase
4
+
5
+ context "ActsAsJoinable" do
6
+
7
+ setup do
8
+ create_models(1, 2)
9
+ end
10
+
11
+ should "have 1 of each post, asset, tag to start" do
12
+ assert_equal 1, Post.count
13
+ assert_equal 2, Tag.count
14
+ assert_equal 2, Asset.count
15
+ end
16
+
17
+ context "correct generated methods" do
18
+ setup do
19
+ @post = Post.first
20
+ @asset = Asset.first
21
+ @tag = Tag.first
22
+ end
23
+
24
+ should "Post should respond_to?(:tags) and respond_to?(:assets)" do
25
+ puts @post.tags.inspect + " !!"
26
+ assert @post.respond_to?(:tags)
27
+ assert @post.respond_to?(:assets)
28
+ end
29
+ end
30
+
31
+ teardown do
32
+ Post.detonate
33
+ Tag.detonate
34
+ Asset.detonate
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ require "rubygems"
2
+ require "ruby-debug"
3
+ gem 'test-unit'
4
+ require "test/unit"
5
+ require 'active_support'
6
+ require 'active_support/test_case'
7
+ require 'active_record'
8
+ require 'active_record/fixtures'
9
+ require 'shoulda'
10
+ require 'shoulda/active_record'
11
+
12
+ this = File.expand_path(File.dirname(__FILE__))
13
+
14
+ require File.expand_path(File.join(this, '/../lib/acts-as-joinable'))
15
+
16
+ ActsAsJoinable.models = Dir["#{this}/../app/models/*.rb"].collect { |f| File.basename f, '.rb' }
17
+
18
+ Dir["#{this}/lib/*"].each { |c| require c if File.extname(c) == ".rb" }
19
+
20
+ ActiveRecord::Base.class_eval do
21
+ def self.detonate
22
+ all.map(&:destroy)
23
+ end
24
+ end
25
+
26
+ ActiveSupport::TestCase.class_eval do
27
+
28
+ def create_models(parent = 1, child = 2)
29
+ parent.times do |i|
30
+ post = Post.create!(:title => "title-#{i.to_s}")
31
+ child.times do |j|
32
+ position = (i + 1) * (j + 1)
33
+ asset = Asset.create!(:title => "asset-title-#{position.to_s}")
34
+ tag = Tag.create!(:name => "tag-name-#{position.to_s}")
35
+ end
36
+ end
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts-as-joinable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 65
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ - 5
11
+ version: 0.0.1.5
12
+ platform: ruby
13
+ authors:
14
+ - Lance Pollard
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-06-20 00:00:00 -07:00
20
+ default_executable:
21
+ dependencies: []
22
+
23
+ description: DRYing up Many-to-Many Relationships in ActiveRecord
24
+ email: lancejpollard@gmail.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - README.markdown
33
+ - Rakefile
34
+ - init.rb
35
+ - MIT-LICENSE
36
+ - lib/acts-as-joinable.rb
37
+ - rails/init.rb
38
+ - test/lib/_database.rb
39
+ - test/lib/asset.rb
40
+ - test/lib/post.rb
41
+ - test/lib/tag.rb
42
+ - test/test_acts_as_joinable.rb
43
+ - test/test_helper.rb
44
+ has_rdoc: true
45
+ homepage: http://github.com/viatropos/cockpit
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options: []
50
+
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ requirements: []
72
+
73
+ rubyforge_project: acts-as-joinable
74
+ rubygems_version: 1.3.7
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: "ActsAsJoinable: DRYing up Many-to-Many Relationships in ActiveRecord"
78
+ test_files: []
79
+