acts-as-joinable 0.0.1.7 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +68 -51
- data/Rakefile +1 -1
- data/lib/acts-as-joinable.rb +28 -11
- data/lib/acts_as_joinable/active_record_patch.rb +133 -0
- data/lib/acts_as_joinable/core.rb +187 -108
- data/test/database.rb +7 -0
- data/test/lib/asset.rb +2 -2
- data/test/lib/group.rb +7 -0
- data/test/lib/post.rb +1 -2
- data/test/lib/sub_group.rb +5 -0
- data/test/lib/tag.rb +1 -1
- data/test/test_acts_as_joinable.rb +55 -78
- data/test/test_helper.rb +5 -3
- metadata +7 -5
data/README.markdown
CHANGED
@@ -1,82 +1,99 @@
|
|
1
1
|
# ActsAsJoinable
|
2
2
|
|
3
|
-
|
3
|
+
Like has_many_polymorphs but easier. Can handle double polymorphic associations with single table inheritance from the join model.
|
4
4
|
|
5
|
-
##
|
6
|
-
|
7
|
-
### Install
|
5
|
+
## Install
|
8
6
|
|
9
7
|
sudo gem install acts-as-joinable
|
10
8
|
|
11
|
-
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
### Dry Assocations with Zero Dependencies
|
12
12
|
|
13
|
-
|
13
|
+
Here's what you would write:
|
14
14
|
|
15
|
-
class
|
16
|
-
|
15
|
+
class Content < ActiveRecord::Base
|
16
|
+
joins :assets
|
17
|
+
joins :images
|
18
|
+
joins_one :cover_image, :source => :image
|
17
19
|
end
|
18
20
|
|
19
|
-
class
|
20
|
-
|
21
|
+
class Page < Content
|
22
|
+
joins :children, :as => :parent, :source => :content
|
23
|
+
joins :parents, :as => :child, :source => :content
|
21
24
|
end
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
class Post < ActiveRecord::Base
|
26
|
-
has_and_belongs_to_many :assets
|
26
|
+
class Post < Content
|
27
|
+
joins :parents, :as => :child, :source => :page
|
27
28
|
end
|
28
29
|
|
29
30
|
class Asset < ActiveRecord::Base
|
30
|
-
|
31
|
+
joinable
|
32
|
+
end
|
33
|
+
|
34
|
+
class Image < Asset
|
35
|
+
|
31
36
|
end
|
37
|
+
|
38
|
+
Here's how you'd use it:
|
32
39
|
|
33
|
-
|
40
|
+
page = Page.create!(:title => "Home Page")
|
41
|
+
post = Post.create!(:title => "My first blog post")
|
42
|
+
image = Image.create!(:src => "http://imgur.com/123123.png")
|
43
|
+
|
44
|
+
page.children << post
|
45
|
+
post.cover_image = image
|
46
|
+
|
47
|
+
assert_equal post, page.children.first
|
48
|
+
assert_equal image, post.images.first
|
49
|
+
assert_equal image, post.assets.first
|
50
|
+
|
51
|
+
You can also create a simple group membership system no problem:
|
34
52
|
|
35
|
-
|
53
|
+
class User < ActiveRecord::Base
|
54
|
+
joins :groups, :as => :child, :context => :membership
|
55
|
+
end
|
36
56
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
57
|
+
class Group < ActiveRecord::Base
|
58
|
+
joins :members, :context => :membership, :source => :user
|
59
|
+
joins_one :admin, :context => :membership, :value => :admin, :source => :user
|
60
|
+
joins :board_of_directors, :context => :membership, :value => :board, :source => :user
|
61
|
+
end
|
42
62
|
|
43
|
-
|
63
|
+
basic_member = User.create!(:position => "I'm an employee")
|
64
|
+
board_member = User.create!(:position => "I'm on the board")
|
65
|
+
company = Group.create!(:name => "A Company")
|
44
66
|
|
45
|
-
|
46
|
-
|
47
|
-
t.references :child, :polymorphic => true
|
48
|
-
t.string :context
|
49
|
-
t.timestamps
|
50
|
-
end
|
67
|
+
company.members << basic_member
|
68
|
+
company.board_of_directors << board_member
|
51
69
|
|
52
|
-
|
70
|
+
assert_equal 2, company.members.length
|
71
|
+
assert_equal 1, company.board_of_directors
|
72
|
+
|
73
|
+
### What's it doing?
|
53
74
|
|
54
|
-
|
75
|
+
First, it has a generic join model. All join models are the same in the end, so there is no need to create extra tables for each. Maybe when you get 10,000,000 records you'll need to start creating tables for specific joins, but you don't need that by default. It increases the complexity of your application unnecessarily which makes it harder to extend and manage.
|
55
76
|
|
56
|
-
|
77
|
+
Instead, this creates a single join model that will solve for most of your cases (if there is a case it doesn't solve for, I'd love to know, it solves all of mine).
|
57
78
|
|
58
|
-
|
79
|
+
Here's the table for the built-in `Relationship` model:
|
59
80
|
|
60
|
-
|
61
|
-
|
81
|
+
create_table :relationships do |t|
|
82
|
+
t.references :parent, :polymorphic => true
|
83
|
+
t.references :child, :polymorphic => true
|
84
|
+
t.string :context
|
85
|
+
t.string :value
|
86
|
+
t.integer :position
|
87
|
+
t.timestamps
|
62
88
|
end
|
63
89
|
|
64
|
-
|
65
|
-
@post.featured_assets << Asset.first
|
66
|
-
|
67
|
-
If you need more fine-grained control over each relationship scope, you can use a block:
|
90
|
+
The features are:
|
68
91
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
@post = Post.new
|
74
|
-
@post.featured_image = Image.first
|
75
|
-
@post.thumbnails = Image.all
|
76
|
-
|
77
|
-
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.
|
92
|
+
1. **Double sided polymorphic associations**. Which means you can tie any object to any other object.
|
93
|
+
2. Built-in relationship directionality, similar to a **Directed Acyclic Graph**. So you can say the `Post` is `parent` of `Image`, since you usually attach an Image to a Post (not the other way around), so `Image` is `child` of `Post`. This means you have some sort of built in _hierarchy_.
|
94
|
+
3. **Context**. You can create many-to-many relationships between the same models and call them different things. This is roughly equivalent to creating STI join models. This is useful for creating something like organizing `Users` of a `Group` into `Roles`.
|
95
|
+
4. **Position**. You can sort the objects by relationship in primitive ways.
|
78
96
|
|
79
|
-
|
97
|
+
You can always add columns to the relationship table, but the foundation is set.
|
80
98
|
|
81
|
-
|
82
|
-
- position: what position the join is in relation to the context
|
99
|
+
<cite>copyright @viatropos 2010</cite>
|
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.
|
8
|
+
s.version = "0.0.6"
|
9
9
|
s.summary = "ActsAsJoinable: DRYing up Many-to-Many Relationships in ActiveRecord"
|
10
10
|
s.homepage = "http://github.com/viatropos/acts-as-joinable"
|
11
11
|
s.email = "lancejpollard@gmail.com"
|
data/lib/acts-as-joinable.rb
CHANGED
@@ -25,8 +25,9 @@ module ActsAsJoinable
|
|
25
25
|
def joinable?
|
26
26
|
false
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def acts_as_joinable_on(*args, &block)
|
30
|
+
args << block
|
30
31
|
if joinable?
|
31
32
|
write_inheritable_attribute(:acts_as_joinable_config, args)
|
32
33
|
else
|
@@ -34,9 +35,6 @@ module ActsAsJoinable
|
|
34
35
|
class_inheritable_reader(:acts_as_joinable_config)
|
35
36
|
|
36
37
|
class_eval do
|
37
|
-
has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :dependent => :destroy
|
38
|
-
has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :dependent => :destroy
|
39
|
-
|
40
38
|
def self.joinable?
|
41
39
|
true
|
42
40
|
end
|
@@ -45,19 +43,38 @@ module ActsAsJoinable
|
|
45
43
|
end
|
46
44
|
end
|
47
45
|
end
|
48
|
-
|
49
|
-
def acts_as_joinable
|
46
|
+
|
47
|
+
def acts_as_joinable
|
48
|
+
acts_as_joinable_on
|
49
|
+
end
|
50
|
+
|
51
|
+
def joinable
|
52
|
+
acts_as_joinable_on
|
53
|
+
end
|
54
|
+
|
55
|
+
def joins_many(*args, &block)
|
50
56
|
acts_as_joinable_on(*args, &block)
|
51
57
|
end
|
52
|
-
|
58
|
+
|
59
|
+
def joins(*args, &block)
|
60
|
+
acts_as_joinable_on(*args, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def joins_one(*args, &block)
|
64
|
+
options = args.extract_options!
|
65
|
+
options[:limit] = 1
|
66
|
+
args << options
|
67
|
+
acts_as_joinable_on(*args, &block)
|
68
|
+
end
|
69
|
+
|
53
70
|
def acts_as_relationship
|
54
71
|
belongs_to :parent, :polymorphic => true
|
55
72
|
belongs_to :child, :polymorphic => true
|
56
73
|
|
57
|
-
ActsAsJoinable.models.each do |m|
|
58
|
-
belongs_to "parent_#{m}".intern, :foreign_key => 'parent_id', :class_name => m.camelize
|
59
|
-
belongs_to "child_#{m}".intern, :foreign_key => 'child_id', :class_name => m.camelize
|
60
|
-
end
|
74
|
+
# ActsAsJoinable.models.each do |m|
|
75
|
+
# belongs_to "parent_#{m}".intern, :foreign_key => 'parent_id', :class_name => m.camelize
|
76
|
+
# belongs_to "child_#{m}".intern, :foreign_key => 'child_id', :class_name => m.camelize
|
77
|
+
# end
|
61
78
|
end
|
62
79
|
end
|
63
80
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class HasManyThroughAssociation < HasManyAssociation
|
4
|
+
protected
|
5
|
+
# added support for STI with polymorphism
|
6
|
+
def construct_conditions
|
7
|
+
table_name = @reflection.through_reflection.quoted_table_name
|
8
|
+
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
|
9
|
+
if attr =~ /_type$/
|
10
|
+
construct_polymorphic_sql(table_name, attr)
|
11
|
+
else
|
12
|
+
"#{table_name}.#{attr} = #{value}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
conditions << sql_conditions if sql_conditions
|
16
|
+
|
17
|
+
"(" + conditions.join(') AND (') + ")"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Construct attributes for associate pointing to owner.
|
21
|
+
def construct_owner_attributes(reflection)
|
22
|
+
if as = reflection.options[:as]
|
23
|
+
{ "#{as}_id" => @owner.id,
|
24
|
+
"#{as}_type" => @owner.class.name.to_s }
|
25
|
+
else
|
26
|
+
{ reflection.primary_key_name => @owner.id }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Construct attributes for :through pointing to owner and associate.
|
31
|
+
def construct_join_attributes(associate)
|
32
|
+
# TODO: revist this to allow it for deletion, supposing dependent option is supported
|
33
|
+
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
34
|
+
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
|
35
|
+
|
36
|
+
if @reflection.options[:source_type]
|
37
|
+
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.name.to_s)
|
38
|
+
end
|
39
|
+
join_attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
# Associate attributes pointing to owner, quoted.
|
43
|
+
def construct_quoted_owner_attributes(reflection)
|
44
|
+
if as = reflection.options[:as]
|
45
|
+
{ "#{as}_id" => owner_quoted_id,
|
46
|
+
"#{as}_type" => reflection.klass.quote_value(
|
47
|
+
@owner.class.name.to_s,
|
48
|
+
reflection.klass.columns_hash["#{as}_type"]) }
|
49
|
+
elsif reflection.macro == :belongs_to
|
50
|
+
{ reflection.klass.primary_key => @owner[reflection.primary_key_name] }
|
51
|
+
else
|
52
|
+
{ reflection.primary_key_name => owner_quoted_id }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def construct_joins(custom_joins = nil)
|
57
|
+
polymorphic_join = nil
|
58
|
+
if @reflection.source_reflection.macro == :belongs_to
|
59
|
+
reflection_primary_key = @reflection.klass.primary_key
|
60
|
+
source_primary_key = @reflection.source_reflection.primary_key_name
|
61
|
+
if @reflection.options[:source_type]
|
62
|
+
polymorphic_join = construct_polymorphic_sql(
|
63
|
+
@reflection.through_reflection.quoted_table_name,
|
64
|
+
"#{@reflection.source_reflection.options[:foreign_type]}",
|
65
|
+
@reflection.options[:source_type]
|
66
|
+
)
|
67
|
+
polymorphic_join = "AND (#{polymorphic_join})"
|
68
|
+
end
|
69
|
+
else
|
70
|
+
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
71
|
+
source_primary_key = @reflection.through_reflection.klass.primary_key
|
72
|
+
if @reflection.source_reflection.options[:as]
|
73
|
+
polymorphic_join = construct_polymorphic_sql(
|
74
|
+
@reflection.quoted_table_name,
|
75
|
+
"#{@reflection.source_reflection.options[:as]}_type",
|
76
|
+
@reflection.through_reflection.klass
|
77
|
+
)
|
78
|
+
polymorphic_join = "AND (#{polymorphic_join})"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
|
83
|
+
@reflection.through_reflection.quoted_table_name,
|
84
|
+
@reflection.quoted_table_name, reflection_primary_key,
|
85
|
+
@reflection.through_reflection.quoted_table_name, source_primary_key,
|
86
|
+
polymorphic_join
|
87
|
+
]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class HasManyAssociation < AssociationCollection
|
92
|
+
protected
|
93
|
+
def construct_polymorphic_sql(table_name, attribute, clazz = @owner.class)
|
94
|
+
condition = []
|
95
|
+
clazz = clazz.to_s.constantize unless clazz.is_a?(Class)
|
96
|
+
ancestor_classes = (clazz.ancestors.reverse - clazz.included_modules).uniq + clazz.send(:subclasses)
|
97
|
+
while ancestor = ancestor_classes.pop
|
98
|
+
break if ancestor == clazz.base_class.superclass
|
99
|
+
condition << "#{table_name}.#{attribute} = #{clazz.quote_value(ancestor.name)}"
|
100
|
+
end
|
101
|
+
condition.join(" OR ")
|
102
|
+
end
|
103
|
+
|
104
|
+
# added support for STI with polymorphism
|
105
|
+
def construct_sql
|
106
|
+
case
|
107
|
+
when @reflection.options[:finder_sql]
|
108
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
109
|
+
|
110
|
+
when @reflection.options[:as]
|
111
|
+
@finder_sql =
|
112
|
+
"(#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id})"
|
113
|
+
polymorphic_conditions = construct_polymorphic_sql(@reflection.quoted_table_name, "#{@reflection.options[:as]}_type")
|
114
|
+
@finder_sql << " AND (#{polymorphic_conditions})"
|
115
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
116
|
+
else
|
117
|
+
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
118
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
119
|
+
end
|
120
|
+
|
121
|
+
if @reflection.options[:counter_sql]
|
122
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
123
|
+
elsif @reflection.options[:finder_sql]
|
124
|
+
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
125
|
+
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
126
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
127
|
+
else
|
128
|
+
@counter_sql = @finder_sql
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -8,85 +8,192 @@ module ActsAsJoinable
|
|
8
8
|
end
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
+
|
12
|
+
# custom parent, self, and child contexts
|
13
|
+
# Group...
|
14
|
+
# acts_as_joinable_on :groups,
|
15
|
+
# :as => [:parent, :child],
|
16
|
+
# :context => :nested_groups,
|
17
|
+
# :child_classes => %w(pod store)
|
18
|
+
# acts_as_joinable_on :pods, :as => :parent, :class_name => "Group"
|
19
|
+
# acts_as_joinable_on :users,
|
20
|
+
# :as => :parent,
|
21
|
+
# :context => :membership
|
22
|
+
# :values => %w(owner developer admin consumer)
|
23
|
+
# acts_as_joinable_on class|context|custom_alias, class, role, context, context values
|
24
|
+
# acts_as_joinable_on :members, :class_name => "User", :as => :parent, :context => :memebership
|
25
|
+
# acts_as_joinable_on :owner, :class_name => "User", :context => :memebership, :value => :owner
|
26
|
+
# joins_one :owner, :class_name => "User", :context => :memebership, :value => :owner
|
27
|
+
# joins_many :members, :class_name => "User", :context => :memebership
|
28
|
+
# joins :user, :with => :role do
|
29
|
+
# has_many :board_of_directors
|
30
|
+
# has_one :owner
|
31
|
+
# end
|
32
|
+
# has_many_parent :posts
|
33
|
+
# has_many_child :assets
|
34
|
+
# has_many_relationships :users, :through => :memberships
|
35
|
+
# joins :user, :with => :membership do
|
36
|
+
# has_many :members
|
37
|
+
# end
|
38
|
+
# Office
|
39
|
+
# acts_as_joinable_on :pods, :as => :child
|
40
|
+
# acts_as_joinable_on :tenants, :as => :parent
|
41
|
+
# User
|
11
42
|
def initialize_acts_as_joinable_on_core
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
43
|
+
joins = acts_as_joinable_config.dup
|
44
|
+
block = joins.pop
|
45
|
+
options = joins.extract_options!
|
46
|
+
if joins.empty?
|
47
|
+
joins = ActsAsJoinable.models
|
48
|
+
relationships = [:child]
|
17
49
|
else
|
18
|
-
|
50
|
+
relationships = [options[:as] || :parent].flatten.map(&:to_sym)
|
19
51
|
end
|
20
52
|
|
21
|
-
class_name
|
53
|
+
options[:class_name] ||= options[:source].to_s.camelize if options[:source]
|
22
54
|
|
23
|
-
|
24
|
-
contexts = contexts.map(&:to_s).inject({}) {|hash, i| hash[i] = i.empty? ? i : "#{i}_"; hash}
|
55
|
+
association_type = options[:limit] == 1 ? :has_one : :has_many
|
25
56
|
|
26
|
-
|
57
|
+
scope_name = options[:named_scope]
|
27
58
|
|
28
|
-
joins
|
59
|
+
joins.map!(&:to_sym)
|
29
60
|
|
30
|
-
|
31
|
-
|
61
|
+
# class name of the model we're joining to self
|
62
|
+
# otherwise it's retrieved from joins.each...
|
63
|
+
class_name = options[:class_name] || nil
|
64
|
+
# contexts defining the relationship between self and target
|
65
|
+
contexts = [options[:context] || []].flatten
|
66
|
+
context = contexts.first
|
67
|
+
# possible values of the context
|
68
|
+
values = [options[:values] || options[:value] || []].flatten.compact
|
69
|
+
value = values.first
|
32
70
|
|
33
|
-
|
34
|
-
has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent
|
71
|
+
sql = options[:conditions]
|
35
72
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
73
|
+
nestable = options[:nestable] || false
|
74
|
+
|
75
|
+
# parent, child, or contexts (both) for custom helper getters/setters
|
76
|
+
|
77
|
+
has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :dependent => :destroy, :foreign_key => "child_id", :uniq => true
|
78
|
+
has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :dependent => :destroy, :foreign_key => "parent_id", :uniq => true
|
79
|
+
|
80
|
+
related_classes = (ancestors.reverse - included_modules + send(:subclasses)).uniq
|
81
|
+
wanted_classes = []
|
82
|
+
while wanted_classes.push(related_classes.pop)
|
83
|
+
break if wanted_classes.last == base_class.superclass
|
84
|
+
end
|
85
|
+
wanted_classes.pop
|
86
|
+
|
87
|
+
joins.each do |type|
|
88
|
+
singular_type = type.to_s.singularize
|
89
|
+
if association_type == :has_one
|
90
|
+
plural_type = type.to_s.pluralize
|
91
|
+
else
|
92
|
+
plural_type = type.to_s
|
93
|
+
end
|
94
|
+
class_name = options[:class_name] || type.to_s.classify
|
51
95
|
|
52
|
-
|
53
|
-
|
54
|
-
|
96
|
+
join_context = (context || singular_type).to_s
|
97
|
+
|
98
|
+
options = {
|
99
|
+
:through => :relationships,
|
100
|
+
:class_name => class_name,
|
101
|
+
:source => :child,
|
102
|
+
:source_type => class_name,
|
103
|
+
:conditions => sql
|
55
104
|
}
|
56
105
|
|
57
|
-
|
58
|
-
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
106
|
+
has_both_relationships = relationships.length > 1
|
107
|
+
|
108
|
+
# don't know how to do this with has_many, since the join model requires 2
|
109
|
+
# different polymorphic models, and it breaks if they're the same class
|
110
|
+
if has_both_relationships
|
111
|
+
# you can only get all objects by this, method.
|
112
|
+
# to create them, use `child_(posts)`, `parent_(posts)`, etc.
|
113
|
+
# define_method type do
|
114
|
+
# [:parent, :child].map do |relationship|
|
115
|
+
# send("#{relationship.to_s}_#{type.to_s}") if respond_to?("#{relationship.to_s}_#{type.to_s}")
|
116
|
+
# end.flatten.compact.uniq
|
117
|
+
# end
|
63
118
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
119
|
+
|
120
|
+
# relationships == [:parent, :child]
|
121
|
+
relationships.each do |relationship|
|
122
|
+
relationship = opposite_for(relationship)
|
123
|
+
singular_relationship = "#{relationship.to_s}_relationship".to_sym
|
124
|
+
plural_relationship = "#{relationship.to_s}_relationships".to_sym
|
125
|
+
plural_relationship = "#{relationship.to_s}_relationships".to_sym
|
126
|
+
|
127
|
+
if association_type == :has_one
|
128
|
+
relationship_with_context = "#{relationship.to_s}_#{singular_type}_relationship".to_sym
|
129
|
+
else
|
130
|
+
relationship_with_context = "#{relationship.to_s}_#{singular_type}_relationships".to_sym
|
131
|
+
end
|
132
|
+
|
133
|
+
options = options.merge(
|
134
|
+
:through => relationship_with_context,
|
135
|
+
:source => relationship
|
70
136
|
)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
137
|
+
|
138
|
+
join_value = value
|
139
|
+
if join_context
|
140
|
+
unless join_context == class_name.underscore
|
141
|
+
condition_string = "(#{ActsAsJoinable::Relationship.table_name.to_s}.context IN (?))"
|
142
|
+
unless join_value
|
143
|
+
# join_value = singular_type.to_s
|
144
|
+
end
|
145
|
+
condition_string << " AND (#{ActsAsJoinable::Relationship.table_name.to_s}.value = ?)" if join_value
|
146
|
+
join_contexts = [join_context, class_name.underscore]
|
147
|
+
conditions = [condition_string, join_contexts]
|
148
|
+
conditions << join_value.to_s if join_value
|
75
149
|
end
|
76
|
-
|
150
|
+
|
151
|
+
through_options = {
|
152
|
+
:class_name => "ActsAsJoinable::Relationship",
|
153
|
+
:conditions => conditions,
|
154
|
+
:as => opposite_for(relationship).to_sym,
|
155
|
+
:dependent => :destroy
|
156
|
+
}
|
157
|
+
|
158
|
+
through_options[:uniq] = true unless association_type == :has_one
|
159
|
+
|
160
|
+
# has_many :member_relationships
|
161
|
+
# conditions: context == x, value == y
|
162
|
+
send(:has_many, relationship_with_context, through_options)
|
163
|
+
|
164
|
+
options = options.merge(
|
165
|
+
:through => relationship_with_context,
|
166
|
+
:source => relationship,
|
167
|
+
:uniq => true
|
168
|
+
)
|
169
|
+
end
|
77
170
|
|
78
|
-
|
79
|
-
|
80
|
-
# end
|
171
|
+
options.delete(:after_add) if association_type == :has_one
|
172
|
+
options.delete(:uniq) if association_type == :has_one
|
81
173
|
|
82
|
-
|
83
|
-
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
174
|
+
method_scope = association_type == :has_one ? :protected : :public
|
175
|
+
send(method_scope)
|
176
|
+
# has_many :child_users, :through => :child_relationships
|
177
|
+
add_association(relationship.to_s, plural_type, options, join_context, join_value, &block)
|
178
|
+
|
179
|
+
accepts_nested_attributes_for plural_type if nestable
|
180
|
+
|
181
|
+
if association_type == :has_one
|
182
|
+
define_method singular_type do
|
183
|
+
send(plural_type).first
|
184
|
+
end
|
185
|
+
define_method "#{singular_type}=" do |value|
|
186
|
+
send(relationship_with_context).destroy_all
|
187
|
+
send(plural_type) << value
|
188
|
+
end
|
189
|
+
define_method "#{singular_type}_id" do
|
190
|
+
send(singular_type).id rescue nil
|
191
|
+
end
|
192
|
+
define_method "#{singular_type}_id=" do |id|
|
193
|
+
send("#{singular_type}=", class_name.constantize.find(id))
|
194
|
+
end
|
195
|
+
end
|
88
196
|
end
|
89
|
-
|
90
197
|
end
|
91
198
|
end
|
92
199
|
|
@@ -100,58 +207,30 @@ module ActsAsJoinable
|
|
100
207
|
role.to_s == "parent" ? "child" : "parent"
|
101
208
|
end
|
102
209
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
210
|
+
def add_association(relationship, plural_type, options, join_context, join_value, &block)
|
211
|
+
eval_options = {:context => join_context}
|
212
|
+
eval_options[:value] = join_value unless join_value.blank?
|
213
|
+
send(:has_many, "#{relationship}_#{plural_type}".to_sym, options) do
|
214
|
+
class_eval <<-EOF
|
215
|
+
def construct_join_attributes(associate)
|
216
|
+
super.merge(#{eval_options.inspect})
|
217
|
+
end
|
218
|
+
EOF
|
219
|
+
end
|
220
|
+
# has_many :users, :through => :child_relationships
|
221
|
+
send(:has_many, plural_type, options) do
|
222
|
+
class_eval <<-EOF
|
223
|
+
def construct_join_attributes(associate)
|
224
|
+
super.merge(#{eval_options.inspect})
|
225
|
+
end
|
226
|
+
EOF
|
120
227
|
end
|
121
228
|
end
|
122
229
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
def join_for(role, type, context)
|
128
|
-
joins_for(role, type, context).first
|
129
|
-
end
|
130
|
-
|
131
|
-
def set_joined(role, type, context, value)
|
132
|
-
relationship = relationship_for(role, type, context, value) || ActsAsJoinable::Relationship.new
|
133
|
-
clazz = get_join_class(type)
|
134
|
-
relationship.send("#{role}=", value.is_a?(clazz) ? value : clazz.find(value))
|
135
|
-
relationship.send("#{opposite_for(role)}=", self)
|
136
|
-
relationship.context = context.to_s
|
137
|
-
relationship.save
|
138
|
-
self.send("#{context.to_s}_#{type.to_s.pluralize}")
|
139
|
-
end
|
230
|
+
end
|
231
|
+
|
232
|
+
module InstanceMethods
|
140
233
|
|
141
|
-
private
|
142
|
-
def get_join_class(type)
|
143
|
-
if type.is_a?(String) || type.is_a?(Symbol)
|
144
|
-
type.to_s.singularize.camelize.constantize
|
145
|
-
elsif type.is_a?(Class)
|
146
|
-
type
|
147
|
-
else
|
148
|
-
type.class
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def opposite_for(role)
|
153
|
-
role.to_s == "parent" ? "child" : "parent"
|
154
|
-
end
|
155
234
|
|
156
235
|
end
|
157
236
|
end
|
data/test/database.rb
CHANGED
@@ -28,10 +28,17 @@ ActiveRecord::Schema.define(:version => 1) do
|
|
28
28
|
t.timestamps
|
29
29
|
end
|
30
30
|
|
31
|
+
create_table :groups, :force => true do |t|
|
32
|
+
t.string :title
|
33
|
+
t.string :type
|
34
|
+
t.timestamps
|
35
|
+
end
|
36
|
+
|
31
37
|
create_table :relationships do |t|
|
32
38
|
t.references :parent, :polymorphic => true
|
33
39
|
t.references :child, :polymorphic => true
|
34
40
|
t.string :context
|
41
|
+
t.string :value
|
35
42
|
t.integer :position
|
36
43
|
t.timestamps
|
37
44
|
end
|
data/test/lib/asset.rb
CHANGED
data/test/lib/group.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
class Group < ActiveRecord::Base
|
2
|
+
joins :groups, :as => [:parent, :child]
|
3
|
+
joins :posts
|
4
|
+
joins :photos, :source => :asset, :context => "photos"
|
5
|
+
joins :custom_photos, :source => :asset, :context => "custom"
|
6
|
+
joins :nesteds, :source => :sub_group, :context => :nested
|
7
|
+
end
|
data/test/lib/post.rb
CHANGED
data/test/lib/tag.rb
CHANGED
@@ -4,95 +4,72 @@ class ActsAsJoinableTest < ActiveSupport::TestCase
|
|
4
4
|
|
5
5
|
context "ActsAsJoinable" do
|
6
6
|
|
7
|
-
|
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
|
-
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
|
-
|
28
|
-
context "correct generated methods" do
|
7
|
+
context "modeling groups" do
|
29
8
|
setup do
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
should "Post should respond_to?(:tags) and respond_to?(:assets)" do
|
36
|
-
assert @post.respond_to?(:tags)
|
37
|
-
assert @post.respond_to?(:assets)
|
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
|
9
|
+
@grandparent = Group.create!(:title => "Grandparent")
|
10
|
+
@parent = Group.create!(:title => "Parent")
|
11
|
+
@child = Group.create!(:title => "Child")
|
12
|
+
@child_2 = Group.create!(:title => "Child 2")
|
55
13
|
end
|
56
14
|
|
57
|
-
should "
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
15
|
+
should "allow self referential and sti associations" do
|
16
|
+
@sub_class = SubGroup.create!(:title => "Subgroup!")
|
17
|
+
@grandparent.nesteds << @sub_class
|
18
|
+
@sub_class.reload
|
19
|
+
@grandparent.reload
|
20
|
+
|
21
|
+
association = ActsAsJoinable::Relationship.first
|
22
|
+
|
23
|
+
assert @sub_class.valid?
|
24
|
+
assert_equal "SubGroup", association.child_type
|
25
|
+
assert_not_equal "Group", association.child_type
|
26
|
+
assert_equal @grandparent, SubGroup.first.parents.first
|
62
27
|
end
|
63
28
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
29
|
+
should "have one instead of many" do
|
30
|
+
parent = SubGroup.create!(:title => "subgroup with has_one :cover_image")
|
31
|
+
child = Asset.create!
|
32
|
+
another_child = Asset.create!(:title => "Not a cover_image!")
|
33
|
+
|
34
|
+
parent.cover_image = child
|
35
|
+
parent.galleries << another_child
|
36
|
+
parent.save!
|
37
|
+
parent.reload
|
38
|
+
|
39
|
+
assert parent.valid?
|
40
|
+
|
41
|
+
assert_equal child, parent.cover_image
|
42
|
+
assert_equal 1, parent.galleries.length
|
43
|
+
assert_equal 2, parent.assets.length
|
44
|
+
assert_equal 0, parent.images.length
|
45
|
+
assert_equal 0, parent.pictures.length
|
46
|
+
|
47
|
+
# add another
|
48
|
+
parent.cover_image = another_child
|
49
|
+
parent.save!
|
50
|
+
parent.reload
|
51
|
+
|
52
|
+
assert_equal another_child, parent.cover_image
|
53
|
+
|
54
|
+
another_child.reload
|
76
55
|
|
56
|
+
assert_equal parent, another_child.groups.first
|
77
57
|
end
|
78
|
-
end
|
79
|
-
|
80
|
-
context "manipulating posts" do
|
81
58
|
|
82
|
-
|
83
|
-
|
59
|
+
should "allow nested attributes" do
|
60
|
+
assert_equal 0, Asset.count
|
61
|
+
|
62
|
+
parent = SubGroup.create!(
|
63
|
+
:title => "subgroup with has_one :cover_image",
|
64
|
+
:cover_images_attributes => [{:title => "an image"}]
|
65
|
+
)
|
66
|
+
|
67
|
+
assert_equal 1, Asset.count
|
84
68
|
end
|
85
69
|
|
86
|
-
|
87
|
-
|
88
|
-
@post.featured_assets << Asset.all
|
89
|
-
@post = Post.last
|
70
|
+
teardown do
|
71
|
+
destroy_models
|
90
72
|
end
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
teardown do
|
95
|
-
destroy_models
|
96
73
|
end
|
97
74
|
|
98
75
|
end
|
data/test/test_helper.rb
CHANGED
@@ -8,10 +8,11 @@ require 'active_record'
|
|
8
8
|
require 'active_record/fixtures'
|
9
9
|
require 'shoulda'
|
10
10
|
require 'shoulda/active_record'
|
11
|
+
require 'logger'
|
11
12
|
|
12
13
|
this = File.expand_path(File.dirname(__FILE__))
|
13
14
|
|
14
|
-
#
|
15
|
+
#ActiveRecord::Base.logger = Logger.new(STDOUT)
|
15
16
|
|
16
17
|
require File.expand_path(File.join(this, '/../lib/acts-as-joinable'))
|
17
18
|
require File.join(this, "database")
|
@@ -21,6 +22,7 @@ ActsAsJoinable.models = Dir["#{this}/lib/*.rb"].collect { |f| File.basename f, '
|
|
21
22
|
Dir["#{File.dirname(__FILE__)}/../app/models/*"].each { |c| require c if File.extname(c) == ".rb" }
|
22
23
|
|
23
24
|
Dir["#{this}/lib/*"].each { |c| require c if File.extname(c) == ".rb" }
|
25
|
+
require "#{this}/lib/group"
|
24
26
|
|
25
27
|
ActiveRecord::Base.class_eval do
|
26
28
|
def self.detonate
|
@@ -29,8 +31,6 @@ ActiveRecord::Base.class_eval do
|
|
29
31
|
end
|
30
32
|
|
31
33
|
ActiveSupport::TestCase.class_eval do
|
32
|
-
|
33
|
-
|
34
34
|
def create_models(parent = 1, child = 2)
|
35
35
|
return unless Post.all.empty?
|
36
36
|
parent.times do |i|
|
@@ -52,5 +52,7 @@ ActiveSupport::TestCase.class_eval do
|
|
52
52
|
Asset.detonate
|
53
53
|
Page.detonate
|
54
54
|
ActsAsJoinable::Relationship.detonate
|
55
|
+
Group.detonate
|
56
|
+
Asset.detonate
|
55
57
|
end
|
56
58
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts-as-joinable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
|
11
|
-
version: 0.0.1.7
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
12
11
|
platform: ruby
|
13
12
|
authors:
|
14
13
|
- Lance Pollard
|
@@ -16,7 +15,7 @@ autorequire:
|
|
16
15
|
bindir: bin
|
17
16
|
cert_chain: []
|
18
17
|
|
19
|
-
date: 2010-
|
18
|
+
date: 2010-09-14 00:00:00 -05:00
|
20
19
|
default_executable:
|
21
20
|
dependencies: []
|
22
21
|
|
@@ -34,12 +33,15 @@ files:
|
|
34
33
|
- init.rb
|
35
34
|
- MIT-LICENSE
|
36
35
|
- lib/acts-as-joinable.rb
|
36
|
+
- lib/acts_as_joinable/active_record_patch.rb
|
37
37
|
- lib/acts_as_joinable/core.rb
|
38
38
|
- rails/init.rb
|
39
39
|
- test/database.rb
|
40
40
|
- test/lib/asset.rb
|
41
|
+
- test/lib/group.rb
|
41
42
|
- test/lib/page.rb
|
42
43
|
- test/lib/post.rb
|
44
|
+
- test/lib/sub_group.rb
|
43
45
|
- test/lib/tag.rb
|
44
46
|
- test/test_acts_as_joinable.rb
|
45
47
|
- test/test_helper.rb
|