acts-as-joinable 0.0.1.5

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