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 CHANGED
@@ -1,82 +1,99 @@
1
1
  # ActsAsJoinable
2
2
 
3
- It extends the functionality of `has_and_belongs_to_many`, conventionalizing common use cases.
3
+ Like has_many_polymorphs but easier. Can handle double polymorphic associations with single table inheritance from the join model.
4
4
 
5
- ## Usage
6
-
7
- ### Install
5
+ ## Install
8
6
 
9
7
  sudo gem install acts-as-joinable
10
8
 
11
- ### Add Relationships
9
+ ## Usage
10
+
11
+ ### Dry Assocations with Zero Dependencies
12
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:
13
+ Here's what you would write:
14
14
 
15
- class Post < ActiveRecord::Base
16
- acts_as_joinable_on :assets, :layouts, :tags, :slugs
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 Asset < ActiveRecord::Base
20
- acts_as_joinable
21
+ class Page < Content
22
+ joins :children, :as => :parent, :source => :content
23
+ joins :parents, :as => :child, :source => :content
21
24
  end
22
25
 
23
- That is a replacement for the longer (and non-polymorphic):
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
- has_and_belongs_to_many :posts
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
- ## Why
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
- Many-to-many relationships end up requiring the same features 99% of the time:
53
+ class User < ActiveRecord::Base
54
+ joins :groups, :as => :child, :context => :membership
55
+ end
36
56
 
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)
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
- It looks like this:
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
- 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
67
+ company.members << basic_member
68
+ company.board_of_directors << board_member
51
69
 
52
- ## Alternatives
70
+ assert_equal 2, company.members.length
71
+ assert_equal 1, company.board_of_directors
72
+
73
+ ### What's it doing?
53
74
 
54
- - [ActsAsRelationable](http://github.com/winton/acts_as_relationable)
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
- ## Examples
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
- If you would like to define accessors that scope the relationship based on the `context` attribute of the `Relationship` model, you can do this:
79
+ Here's the table for the built-in `Relationship` model:
59
80
 
60
- class Post < ActiveRecord::Base
61
- acts_as_joinable_on :assets, :scopes => [:featured, :thumb]
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
- @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:
90
+ The features are:
68
91
 
69
- class Post < ActiveRecord::Base
70
- acts_as_joinable_on :tags, :assets
71
- end
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
- Has 2 key properties on the join model that are pretty common:
97
+ You can always add columns to the relationship table, but the foundation is set.
80
98
 
81
- - context: what the meaning of the join is
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.1.7"
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"
@@ -25,8 +25,9 @@ module ActsAsJoinable
25
25
  def joinable?
26
26
  false
27
27
  end
28
- # the parent in the relationship, so to speak
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
- # the child in the relationship, so to speak
49
- def acts_as_joinable(*args, &block)
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
- args = acts_as_joinable_config.dup
13
- options = args.extract_options!
14
- if args.empty?
15
- args = ActsAsJoinable.models
16
- as = :child
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
- as = options[:as] || :parent
50
+ relationships = [options[:as] || :parent].flatten.map(&:to_sym)
19
51
  end
20
52
 
21
- class_name = options[:class_name] || nil
53
+ options[:class_name] ||= options[:source].to_s.camelize if options[:source]
22
54
 
23
- contexts = options[:contexts] || []
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
- sql = options[:conditions]
57
+ scope_name = options[:named_scope]
27
58
 
28
- joins = args.inject({}) { |hash, i| hash[i.to_s.pluralize] = as; hash }
59
+ joins.map!(&:to_sym)
29
60
 
30
- fields = options[:fields] || []
31
- fields = [fields] unless fields.respond_to?(:flatten)
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
- has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child
34
- has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent
71
+ sql = options[:conditions]
35
72
 
36
- joins.each do |type, as|
37
- type = type.to_s
38
- singular_type = type.singularize
39
- relationship_type = "#{singular_type}_relationships".to_sym
40
- select = "#{type}.*, relationships.id AS relationship_id#{fields.empty? ? '' : ', '}" + fields.collect { |f| "relationships.#{f}" }.join(', ')
41
-
42
- role = opposite_for(as).to_sym
43
- joined_options = {
44
- :select => select,
45
- :conditions => sql,
46
- :through => relationship_type,
47
- :source => role,
48
- :class_name => class_name ? class_name : type.classify,
49
- :source_type => class_name ? class_name : type.classify
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
- relationship_options = {
53
- :class_name => 'ActsAsJoinable::Relationship',
54
- :as => as
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
- type = type.to_sym
58
-
59
- # so we don't override
60
- unless self.reflect_on_all_associations.detect {|association| association.name.to_s == type.to_s}
61
- has_many type, joined_options
62
- has_many relationship_type, relationship_options
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
- contexts.each do |context, context_prefix|
66
- context_type = "#{context_prefix}#{type}".to_sym
67
- relationship_type = "#{context_prefix}#{singular_type}_relationships".to_sym
68
- has_many relationship_type, relationship_options.merge(
69
- :conditions => ["#{ActsAsJoinable::Relationship.table_name}.context = ?", context.to_s]
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
- has_many context_type, joined_options.merge(
72
- :through => relationship_type,
73
- :before_add => lambda do |parent, child|
74
- parent.set_joined(role, type, context, child)
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
- # define_method context_type do
79
- # joins_for(role, type, context)
80
- # end
171
+ options.delete(:after_add) if association_type == :has_one
172
+ options.delete(:uniq) if association_type == :has_one
81
173
 
82
- # define_method "add_#{context_type.to_s.singularize}" do |value|
83
- # value = [value] unless value.is_a?(Array)
84
- # value.each do |item|
85
- # set_joined(role, type, context, item)
86
- # end
87
- # end
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
- end
104
-
105
- module InstanceMethods
106
-
107
- def relationships_for(role, type, context = nil, options = {})
108
- conditions = {
109
- "#{role}_type" => type.to_s.classify,
110
- }
111
- conditions[:context] = context.to_s unless context.blank?
112
- options.merge!(:conditions => conditions)
113
-
114
- self.send("#{type.to_s.singularize}_relationships").all(options)
115
- end
116
-
117
- def relationship_for(role, type, context, value, options = {})
118
- relationships_for(role, type, context).detect do |relationship|
119
- relationship.send(role).id == value.id
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
- def joins_for(role, type, context)
124
- relationships_for(role, type, context, :include => opposite_for(role)).map(&role)
125
- end
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
@@ -1,4 +1,4 @@
1
1
  class Asset < ActiveRecord::Base
2
- acts_as_joinable_on :tags
3
- acts_as_joinable
2
+ joins :tags
3
+ joins :groups, :source => :group, :as => :child
4
4
  end
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
@@ -1,4 +1,3 @@
1
1
  class Post < ActiveRecord::Base
2
- acts_as_joinable_on :assets, :contexts => [:featured, :thumb]
3
- acts_as_joinable_on :tags
2
+
4
3
  end
@@ -0,0 +1,5 @@
1
+ class SubGroup < Group
2
+ joins :assets, :images, :pictures, :galleries, :source => :asset
3
+ joins :parents, :source => :group, :context => :nested, :as => :child
4
+ joins_one :cover_image, :source => :asset, :through => :relationship, :nestable => true
5
+ end
data/test/lib/tag.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Tag < ActiveRecord::Base
2
- acts_as_joinable
2
+ joinable
3
3
  end
@@ -4,95 +4,72 @@ class ActsAsJoinableTest < ActiveSupport::TestCase
4
4
 
5
5
  context "ActsAsJoinable" do
6
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
- 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
- @post = Post.first
31
- @asset = Asset.first
32
- @tag = Tag.first
33
- end
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 "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
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
- 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
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
- setup do
83
- @post = Post.create!(:title => "testing")
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
- should "be able to add and remove objects" do
87
- @post.assets << Asset.all
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
- # ActiveRecord::Base.logger = Logger.new(STDOUT)
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: 69
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- - 7
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-07-02 00:00:00 -07:00
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