acts-as-joinable 0.0.1.5 → 0.0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -51,4 +51,31 @@ It looks like this:
51
51
 
52
52
  ## Alternatives
53
53
 
54
- - [ActsAsRelationable](http://github.com/winton/acts_as_relationable)
54
+ - [ActsAsRelationable](http://github.com/winton/acts_as_relationable)
55
+
56
+ ## Examples
57
+
58
+ If you would like to define accessors that scope the relationship based on the `context` attribute of the `Relationship` model, you can do this:
59
+
60
+ class Post < ActiveRecord::Base
61
+ acts_as_joinable_on :assets, :scopes => [:featured, :thumb]
62
+ end
63
+
64
+ @post = Post.new
65
+ @post.featured_assets << Asset.first
66
+
67
+ If you need more fine-grained control over each relationship scope, you can use a block:
68
+
69
+ class Post < ActiveRecord::Base
70
+ acts_as_joinable_on :assets do
71
+ has_one :featured_image, where(:...)
72
+ has_many :thumbnails
73
+ end
74
+ acts_as_joinable_on :tags
75
+ end
76
+
77
+ @post = Post.new
78
+ @post.featured_image = Image.first
79
+ @post.thumbnails = Image.all
80
+
81
+ The goal of this is to make it so you never have to create migrations or new classes, or rewrite the same code over and over again. Instead, you can just define `scopes` for cherry-picking the habtm items you'd like.
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
5
5
  spec = Gem::Specification.new do |s|
6
6
  s.name = "acts-as-joinable"
7
7
  s.authors = ["Lance Pollard"]
8
- s.version = "0.0.1.5"
8
+ s.version = "0.0.1.6"
9
9
  s.summary = "ActsAsJoinable: DRYing up Many-to-Many Relationships in ActiveRecord"
10
10
  s.homepage = "http://github.com/viatropos/cockpit"
11
11
  s.email = "lancejpollard@gmail.com"
@@ -13,160 +13,47 @@ module ActsAsJoinable
13
13
  end
14
14
 
15
15
  module ClassMethods
16
+
17
+ def joinable?
18
+ false
19
+ end
16
20
  # the parent in the relationship, so to speak
17
21
  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
22
+ if joinable?
23
+ write_inheritable_attribute(:acts_as_joinable_config, args)
26
24
  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)
25
+ write_inheritable_attribute(:acts_as_joinable_config, args)
26
+ class_inheritable_reader(:acts_as_joinable_config)
32
27
 
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
- }
28
+ class_eval do
29
+ has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child
30
+ has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent
66
31
 
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
32
+ def self.joinable?
33
+ true
74
34
  end
75
35
 
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
36
+ include ActsAsJoinable::Core
103
37
  end
104
38
  end
105
39
  end
106
-
107
40
  # the child in the relationship, so to speak
108
41
  def acts_as_joinable(*args, &block)
109
42
  acts_as_joinable_on(*args, &block)
110
43
  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
44
+
45
+ def acts_as_relationship
46
+ belongs_to :parent, :polymorphic => true
47
+ belongs_to :child, :polymorphic => true
48
+
49
+ ActsAsJoinable.models.each do |m|
50
+ belongs_to "parent_#{m}".intern, :foreign_key => 'parent_id', :class_name => m.camelize
51
+ belongs_to "child_#{m}".intern, :foreign_key => 'child_id', :class_name => m.camelize
144
52
  end
145
53
  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
54
  end
168
55
  end
169
56
 
170
57
  ActiveRecord::Base.send(:include, ActsAsJoinable) if defined?(ActiveRecord::Base)
171
58
 
172
- Dir["#{File.dirname(__FILE__)}/../app/models/*"].each { |c| require c if File.extname(c) == ".rb" }
59
+ Dir["#{File.dirname(__FILE__)}/acts_as_joinable/*"].each { |c| require c if File.extname(c) == ".rb" }
@@ -0,0 +1,153 @@
1
+ module ActsAsJoinable
2
+ module Core
3
+ def self.included(base)
4
+ base.send :include, ActsAsJoinable::Core::InstanceMethods
5
+ base.extend ActsAsJoinable::Core::ClassMethods
6
+
7
+ base.initialize_acts_as_joinable_on_core
8
+ end
9
+
10
+ module ClassMethods
11
+ def initialize_acts_as_joinable_on_core
12
+ args = acts_as_joinable_config.dup
13
+ options = args.extract_options!
14
+ if args.empty?
15
+ args = ActsAsJoinable.models
16
+ as = :child
17
+ else
18
+ as = options[:as] || :parent
19
+ end
20
+
21
+ contexts = options[:contexts] || []
22
+ contexts = contexts.map(&:to_s).inject({}) {|hash, i| hash[i] = i.empty? ? i : "#{i}_"; hash}
23
+
24
+ sql = options[:conditions]
25
+
26
+ joins = args.inject({}) { |hash, i| hash[i.to_s.pluralize] = as; hash }
27
+
28
+ fields = options[:fields] || []
29
+ fields = [fields] unless fields.respond_to?(:flatten)
30
+
31
+ has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child
32
+ has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent
33
+
34
+ joins.each do |type, as|
35
+ type = type.to_s
36
+ singular_type = type.singularize
37
+ relationship_type = "#{singular_type}_relationships".to_sym
38
+ select = "#{type}.*, relationships.id AS relationship_id#{fields.empty? ? '' : ', '}" + fields.collect { |f| "relationships.#{f}" }.join(', ')
39
+
40
+ role = opposite_for(as).to_sym
41
+ joined_options = {
42
+ :select => select,
43
+ :conditions => sql,
44
+ :through => relationship_type,
45
+ :source => role,
46
+ :class_name => type.classify,
47
+ :source_type => type.classify
48
+ }
49
+
50
+ relationship_options = {
51
+ :class_name => 'ActsAsJoinable::Relationship',
52
+ :as => as
53
+ }
54
+
55
+ type = type.to_sym
56
+
57
+ # so we don't override
58
+ unless self.reflect_on_all_associations.detect {|association| association.name.to_s == type.to_s}
59
+ has_many type, joined_options
60
+ has_many relationship_type, relationship_options
61
+ end
62
+
63
+ contexts.each do |context, context_prefix|
64
+ context_type = "#{context_prefix}#{type}".to_sym
65
+ relationship_type = "#{context_prefix}#{singular_type}_relationships".to_sym
66
+ has_many relationship_type, relationship_options.merge(
67
+ :conditions => ["#{ActsAsJoinable::Relationship.table_name}.context = ?", context.to_s]
68
+ )
69
+
70
+ has_many context_type, joined_options.merge(
71
+ :through => relationship_type,
72
+ :before_add => lambda do |parent, child|
73
+ parent.set_joined(role, type, context, child)
74
+ end
75
+ )
76
+
77
+ # define_method context_type do
78
+ # joins_for(role, type, context)
79
+ # end
80
+
81
+ # define_method "add_#{context_type.to_s.singularize}" do |value|
82
+ # value = [value] unless value.is_a?(Array)
83
+ # value.each do |item|
84
+ # set_joined(role, type, context, item)
85
+ # end
86
+ # end
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+ def acts_as_joinable_on(*args)
93
+ super(*args)
94
+ initialize_acts_as_joinable_on_core
95
+ end
96
+
97
+ private
98
+ def opposite_for(role)
99
+ role.to_s == "parent" ? "child" : "parent"
100
+ end
101
+
102
+ end
103
+
104
+ module InstanceMethods
105
+
106
+ def relationships_for(role, type, context = nil, options = {})
107
+ conditions = {
108
+ "#{role}_type" => type.to_s.classify,
109
+ }
110
+ conditions[:context] = context.to_s unless context.blank?
111
+ options.merge!(:conditions => conditions)
112
+
113
+ self.send("#{type.to_s.singularize}_relationships").all(options)
114
+ end
115
+
116
+ def relationship_for(role, type, context, value, options = {})
117
+ relationships_for(role, type, context).detect do |relationship|
118
+ relationship.send(role).id == value.id
119
+ end
120
+ end
121
+
122
+ def joins_for(role, type, context)
123
+ relationships_for(role, type, context, :include => opposite_for(role)).map(&role)
124
+ end
125
+
126
+ def set_joined(role, type, context, value)
127
+ relationship = relationship_for(role, type, context, value) || ActsAsJoinable::Relationship.new
128
+ clazz = get_join_class(type)
129
+ relationship.send("#{role}=", value.is_a?(clazz) ? value : clazz.find(value))
130
+ relationship.send("#{opposite_for(role)}=", self)
131
+ relationship.context = context.to_s
132
+ relationship.save
133
+ self.send("#{context.to_s}_#{type.to_s.pluralize}")
134
+ end
135
+
136
+ private
137
+ def get_join_class(type)
138
+ if type.is_a?(String) || type.is_a?(Symbol)
139
+ type.to_s.singularize.camelize.constantize
140
+ elsif type.is_a?(Class)
141
+ type
142
+ else
143
+ type.class
144
+ end
145
+ end
146
+
147
+ def opposite_for(role)
148
+ role.to_s == "parent" ? "child" : "parent"
149
+ end
150
+
151
+ end
152
+ end
153
+ end
@@ -23,6 +23,11 @@ ActiveRecord::Schema.define(:version => 1) do
23
23
  t.timestamps
24
24
  end
25
25
 
26
+ create_table :pages, :force => true do |t|
27
+ t.string :title
28
+ t.timestamps
29
+ end
30
+
26
31
  create_table :relationships do |t|
27
32
  t.references :parent, :polymorphic => true
28
33
  t.references :child, :polymorphic => true
@@ -1,3 +1,4 @@
1
1
  class Asset < ActiveRecord::Base
2
+ acts_as_joinable_on :tags
2
3
  acts_as_joinable
3
4
  end
@@ -0,0 +1,6 @@
1
+ class Page < ActiveRecord::Base
2
+ acts_as_joinable_on :assets do
3
+ has_many :featured_images
4
+ end
5
+ acts_as_joinable_on :tags
6
+ end
@@ -1,3 +1,4 @@
1
1
  class Post < ActiveRecord::Base
2
- acts_as_joinable_on :assets, :tags
2
+ acts_as_joinable_on :assets, :contexts => [:featured, :thumb]
3
+ acts_as_joinable_on :tags
3
4
  end
@@ -14,24 +14,85 @@ class ActsAsJoinableTest < ActiveSupport::TestCase
14
14
  assert_equal 2, Asset.count
15
15
  end
16
16
 
17
+ should "have the correct number of Relationship models" do
18
+ result = [ActsAsJoinable::Relationship.find_all_by_parent_type("Post")]
19
+ result << ActsAsJoinable::Relationship.find_all_by_parent_type("Asset")
20
+ result << ActsAsJoinable::Relationship.find_all_by_parent_type("Tag")
21
+ result.each do |kind|
22
+ kind.sort {|a, b| b.child_type <=> a.child_type}.each do |relationship|
23
+
24
+ end
25
+ end
26
+ end
27
+
17
28
  context "correct generated methods" do
18
29
  setup do
19
- @post = Post.first
30
+ @post = Post.first
20
31
  @asset = Asset.first
21
- @tag = Tag.first
32
+ @tag = Tag.first
22
33
  end
23
34
 
24
35
  should "Post should respond_to?(:tags) and respond_to?(:assets)" do
25
- puts @post.tags.inspect + " !!"
26
36
  assert @post.respond_to?(:tags)
27
37
  assert @post.respond_to?(:assets)
28
38
  end
39
+
40
+ should "Tag should respond_to?(:posts) and respond_to?(:assets)" do
41
+ assert @tag.respond_to?(:posts)
42
+ assert @tag.respond_to?(:assets)
43
+ end
44
+
45
+ should "Post have tags and assets" do
46
+ assert_equal 2, @post.tags.count
47
+ assert_equal 2, @post.assets.count
48
+ end
49
+
50
+ should "Tag have Post and Asset" do
51
+ assert_equal 1, @tag.posts.count
52
+ assert_equal 1, @tag.assets.count
53
+ assert_equal 1, Tag.first.posts.count
54
+ assert_equal 1, Tag.first.assets.count
55
+ end
56
+
57
+ should "Asset have Post and Tags" do
58
+ assert_equal 1, @asset.tags.count
59
+ assert_equal 1, @asset.posts.count
60
+ assert_equal 1, Asset.first.tags.count
61
+ assert_equal 1, Asset.first.posts.count
62
+ end
63
+
64
+ context "custom scopes" do
65
+
66
+ should "have a 'featured_assets' scope on Post" do
67
+ @post = Post.first
68
+ assert @post.respond_to?(:featured_assets)
69
+ @featured = Asset.first
70
+ @post.featured_assets << @featured
71
+ #@post.add_featured_asset(@featured)
72
+ @post = Post.first
73
+ assert_equal @post.featured_assets.first, @featured
74
+ assert @post.assets.include?(@featured)
75
+ end
76
+
77
+ end
78
+ end
79
+
80
+ context "manipulating posts" do
81
+
82
+ setup do
83
+ @post = Post.create!(:title => "testing")
84
+ end
85
+
86
+ should "be able to add and remove objects" do
87
+ @post.assets << Asset.all
88
+ @post.featured_assets << Asset.all
89
+ @post = Post.last
90
+ end
91
+
29
92
  end
30
93
 
31
94
  teardown do
32
- Post.detonate
33
- Tag.detonate
34
- Asset.detonate
95
+ destroy_models
35
96
  end
36
97
 
37
98
  end
@@ -11,9 +11,14 @@ require 'shoulda/active_record'
11
11
 
12
12
  this = File.expand_path(File.dirname(__FILE__))
13
13
 
14
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
15
+
14
16
  require File.expand_path(File.join(this, '/../lib/acts-as-joinable'))
17
+ require File.join(this, "database")
18
+
19
+ ActsAsJoinable.models = Dir["#{this}/lib/*.rb"].collect { |f| File.basename f, '.rb' }
15
20
 
16
- ActsAsJoinable.models = Dir["#{this}/../app/models/*.rb"].collect { |f| File.basename f, '.rb' }
21
+ Dir["#{File.dirname(__FILE__)}/../app/models/*"].each { |c| require c if File.extname(c) == ".rb" }
17
22
 
18
23
  Dir["#{this}/lib/*"].each { |c| require c if File.extname(c) == ".rb" }
19
24
 
@@ -25,14 +30,27 @@ end
25
30
 
26
31
  ActiveSupport::TestCase.class_eval do
27
32
 
33
+
28
34
  def create_models(parent = 1, child = 2)
35
+ return unless Post.all.empty?
29
36
  parent.times do |i|
30
37
  post = Post.create!(:title => "title-#{i.to_s}")
31
38
  child.times do |j|
32
39
  position = (i + 1) * (j + 1)
33
40
  asset = Asset.create!(:title => "asset-title-#{position.to_s}")
34
41
  tag = Tag.create!(:name => "tag-name-#{position.to_s}")
42
+ asset.tags << tag
43
+ post.tags << tag
44
+ post.assets << asset
35
45
  end
36
46
  end
37
47
  end
38
- end
48
+
49
+ def destroy_models
50
+ Post.detonate
51
+ Tag.detonate
52
+ Asset.detonate
53
+ Page.detonate
54
+ ActsAsJoinable::Relationship.detonate
55
+ end
56
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts-as-joinable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 65
4
+ hash: 71
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
9
  - 1
10
- - 5
11
- version: 0.0.1.5
10
+ - 6
11
+ version: 0.0.1.6
12
12
  platform: ruby
13
13
  authors:
14
14
  - Lance Pollard
@@ -34,9 +34,11 @@ files:
34
34
  - init.rb
35
35
  - MIT-LICENSE
36
36
  - lib/acts-as-joinable.rb
37
+ - lib/acts_as_joinable/core.rb
37
38
  - rails/init.rb
38
- - test/lib/_database.rb
39
+ - test/database.rb
39
40
  - test/lib/asset.rb
41
+ - test/lib/page.rb
40
42
  - test/lib/post.rb
41
43
  - test/lib/tag.rb
42
44
  - test/test_acts_as_joinable.rb