From 00c923c3c46daae902d0b43ec0ca79ef250f0939 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 9 May 2017 02:01:35 -0400 Subject: [PATCH 001/205] Add the ability to specify a map of association names to class names --- README.md | 6 +++++- lib/groupify/adapter/active_record/group.rb | 24 +++++++++++++++------ lib/groupify/adapter/mongoid/group.rb | 24 +++++++++++++++------ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d8f28c4..7cdb7f6 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Set up your member models: ```ruby class User include Mongoid::Document - + groupify :group_member groupify :named_group_member end @@ -131,6 +131,10 @@ Example: class Organization < Group has_members :offices, :equipment end + +class Organization < Group + has_members users: 'CustomUserClass', teams: 'CustomTeamClass' +end ``` Mongoid works the same way by creating Mongoid relations. diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 46b9396..1307d6e 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -68,9 +68,19 @@ def member_classes # Define which classes are members of this group def has_members(*names) - Array.wrap(names.flatten).each do |name| - klass = name.to_s.classify.constantize - register(klass) + klass_map = names.last.is_a?(Hash) ? names.pop : {} + klass_map = names.inject({}){ |hash, name| hash.merge(name => nil) }.merge(klass_map) + + klass_map.each do |name, klass_name| + if klass_name.nil? + klass = name.to_s.classify.constantize + association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + else + klass = klass_name.to_s.classify.constantize + association_name = name.to_sym + end + + register(klass, association_name) end end @@ -92,10 +102,10 @@ def merge!(source_group, destination_group) protected - def register(member_klass) + def register(member_klass, association_name = nil) (@member_klasses ||= Set.new) << member_klass - associate_member_class(member_klass) + associate_member_class(member_klass, association_name) member_klass end @@ -134,8 +144,8 @@ def destroy(*args) end end - def associate_member_class(member_klass) - define_member_association(member_klass) + def associate_member_class(member_klass, association_name = nil) + define_member_association(member_klass, association_name) if member_klass == default_member_class define_member_association(member_klass, :members) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 5ea992a..699e204 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -66,9 +66,19 @@ def member_classes # Define which classes are members of this group def has_members(*names) - Array.wrap(names.flatten).each do |name| - klass = name.to_s.classify.constantize - register(klass) + klass_map = names.last.is_a?(Hash) ? names.pop : {} + klass_map = names.inject({}){ |hash, name| hash.merge(name => nil) }.merge(klass_map) + + klass_map.each do |name, klass_name| + if klass_name.nil? + klass = name.to_s.classify.constantize + association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + else + klass = klass_name.to_s.classify.constantize + association_name = name.to_sym + end + + register(klass, association_name) end end @@ -101,9 +111,11 @@ def merge!(source_group, destination_group) protected - def register(member_klass) + def register(member_klass, association_name = nil) (@member_klasses ||= Set.new) << member_klass - associate_member_class(member_klass) + + associate_member_class(member_klass, association_name) + member_klass end @@ -137,7 +149,7 @@ def delete(*args) end end - def associate_member_class(member_klass) + def associate_member_class(member_klass, association_name = nil) association_name ||= member_klass.model_name.plural.to_sym has_many association_name, class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions From 87fadf50c6e125134c6d5ccde8d417f802d6b079 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 9 May 2017 16:42:32 -0400 Subject: [PATCH 002/205] Introduce `has_member` with `:class_name` option for custom class on association --- README.md | 5 ++-- lib/groupify/adapter/active_record/group.rb | 25 ++++++++++--------- lib/groupify/adapter/mongoid/group.rb | 27 ++++++++++++--------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 7cdb7f6..0963148 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,9 @@ class Organization < Group has_members :offices, :equipment end -class Organization < Group - has_members users: 'CustomUserClass', teams: 'CustomTeamClass' +class Organization2 < Group + has_member :offices, class_name: 'CustomOfficeClass' + has_member :equipment, class_name: 'CustomEquipmentClass' end ``` diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 1307d6e..9a85589 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -68,20 +68,23 @@ def member_classes # Define which classes are members of this group def has_members(*names) - klass_map = names.last.is_a?(Hash) ? names.pop : {} - klass_map = names.inject({}){ |hash, name| hash.merge(name => nil) }.merge(klass_map) + Array.wrap(names.flatten).each do |name| + has_member name + end + end - klass_map.each do |name, klass_name| - if klass_name.nil? - klass = name.to_s.classify.constantize - association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym - else - klass = klass_name.to_s.classify.constantize - association_name = name.to_sym - end + def has_member(name, options = {}) + klass_name = options[:class_name] - register(klass, association_name) + if klass_name.nil? + klass = name.to_s.classify.constantize + association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + else + klass = klass_name.to_s.classify.constantize + association_name = name.to_sym end + + register(klass, association_name) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 699e204..aa8eddf 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -66,20 +66,23 @@ def member_classes # Define which classes are members of this group def has_members(*names) - klass_map = names.last.is_a?(Hash) ? names.pop : {} - klass_map = names.inject({}){ |hash, name| hash.merge(name => nil) }.merge(klass_map) + Array.wrap(names.flatten).each do |name| + has_member name + end + end - klass_map.each do |name, klass_name| - if klass_name.nil? - klass = name.to_s.classify.constantize - association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym - else - klass = klass_name.to_s.classify.constantize - association_name = name.to_sym - end + def has_member(name, options = {}) + klass_name = options[:class_name] - register(klass, association_name) + if klass_name.nil? + klass = name.to_s.classify.constantize + association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + else + klass = klass_name.to_s.classify.constantize + association_name = name.to_sym end + + register(klass, association_name) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -115,7 +118,7 @@ def register(member_klass, association_name = nil) (@member_klasses ||= Set.new) << member_klass associate_member_class(member_klass, association_name) - + member_klass end From c7d3e64a0b568e8277caee1887d5a304d615e7c7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 17:12:49 -0400 Subject: [PATCH 003/205] Make it easier to add subclassed group associations with STI --- .../adapter/active_record/group_member.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 9d4e0c9..64ec2e6 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -21,11 +21,7 @@ module GroupMember class_name: Groupify.group_membership_class_name end - has_many :groups, ->{ distinct }, - through: :group_memberships_as_member, - as: :group, - source_type: @group_class_name, - extend: GroupAssociationExtensions + has_group :groups end module GroupAssociationExtensions @@ -121,10 +117,10 @@ def in_all_groups(*groups) return none unless groups.present? joins(:group_memberships_as_member). - group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). + group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). merge(Groupify.group_membership_klass.where(group_id: groups)). having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}) = ?", groups.count). - distinct + distinct end def in_only_groups(*groups) @@ -144,6 +140,15 @@ def in_other_groups(*groups) def shares_any_group(other) in_any_group(other.groups) end + + def has_group(name, options = {}) + has_many name.to_sym, ->{ distinct }, { + through: :group_memberships_as_member, + source: :group, + source_type: @group_class_name, + extend: GroupAssociationExtensions + }.merge(options.slice :class_name) + end end end end From 46e6ad8480b9fe73f7b71ca980b19d1aabee606b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 17:51:08 -0400 Subject: [PATCH 004/205] Updated docs for `has_group` --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0963148..a178afd 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ class Organization < Group has_members :offices, :equipment end -class Organization2 < Group +class InternationalOrganization < Organization has_member :offices, class_name: 'CustomOfficeClass' has_member :equipment, class_name: 'CustomEquipmentClass' end @@ -140,6 +140,35 @@ end Mongoid works the same way by creating Mongoid relations. +##### Group Associations on Member (ActiveRecord only) + +Your member class can be configured to create associations for each expected group type. +For example, let's say that your member class will have multiple types of organizations as groups. +The following configuration adds `organizations` and `international_organizations` associations +on the member model: + +```ruby +class Group < ActiveRecord::Base + groupify :group, members: [:users, :assignments], default_members: :users +end + +class Organization < Group + has_members :offices, :equipment +end + +class InternationalOrganization < Organization +end + +class Member < ActiveRecord::Base + groupify :group_member + + has_group :organizations, class_name: 'Organization' + has_group :international_organizations, class_name: 'InternationalOrganization' +end +``` + +Mongoid does not support the `has_group` helper method. + ## Usage ### Create groups and add members From 769697448ff80611dc61714e6f02a11898b58f9c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 01:21:25 -0400 Subject: [PATCH 005/205] Override `<<` on collections to properly add membership_type --- lib/groupify/adapter/active_record/group.rb | 22 +++++++++++++++++++ .../adapter/active_record/group_member.rb | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 9a85589..3d4ae07 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -118,6 +118,28 @@ def as(membership_type) merge(Groupify.group_membership_klass.as(membership_type)) end + def <<(*args) + opts = args.extract_options! + membership_type = opts[:as] + members = args.flatten + return self unless members.present? + + group = proxy_association.owner + group.__send__(:clear_association_cache) + + members.each do |member| + super(member) unless include?(member) + if membership_type + membership = member.group_memberships_as_member.where(group_id: group.id, group_type: group.class.model_name.to_s, membership_type: membership_type).first_or_initialize + member.group_memberships_as_member << membership unless membership.persisted? + end + member.__send__(:clear_association_cache) + end + + self + end + alias_method :add, :<< + def delete(*args) opts = args.extract_options! members = args diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 64ec2e6..7eb4799 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -30,6 +30,28 @@ def as(membership_type) merge(Groupify.group_membership_klass.as(membership_type)) end + def <<(*args) + opts = args.extract_options! + membership_type = opts[:as] + groups = args.flatten + return self unless groups.present? + + member = proxy_association.owner + member.__send__(:clear_association_cache) + + groups.each do |group| + super(group) unless include?(group) + if membership_type + membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.model_name.to_s, membership_type: membership_type).first_or_initialize + group.group_memberships_as_group << membership unless membership.persisted? + end + group.__send__(:clear_association_cache) + end + + self + end + alias_method :add, :<< + def delete(*args) opts = args.extract_options! groups = args.flatten From 7dcaa1a4b344267b9b7978b45b94d969f535ab14 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 01:22:48 -0400 Subject: [PATCH 006/205] Add member to members collection rather than the group to groups collection for polymorphic groups compatibility --- README.md | 27 +++++++++++++++++++++ lib/groupify/adapter/active_record/group.rb | 27 ++++++++++++++------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a178afd..479d626 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,33 @@ end Mongoid works the same way by creating Mongoid relations. +With polymorphic groups, the `default_members` option specifies the association +on the group to which members should be added. When specifying individual `has_member` +options, `default_members: true` indicates the association is the one to add new +members to. (If the `default_members` is not specified and the `members` association +does not exist, adding users to subclasses of a group can cause a +`ActiveRecord::AssociationTypeMismatch` exception.) + +Example: + +```ruby +class GroupBase < ActiveRecord::Base + self.table_name = "groups" + self.abstract_class = true +end + +class Organization < GroupBase + acts_as_group default_member_association: :users + has_member :users, class_name: 'CustomUserClass', default_members: true +end + +org = Organization.create! +user = CustomUserClass.create! + +# adds the user to the `ord.users` association +org.add user, as: 'admin' +``` + ##### Group Associations on Member (ActiveRecord only) Your member class can be configured to create associations for each expected group type. diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 3d4ae07..51edfa6 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -28,19 +28,20 @@ def member_classes def add(*args) opts = args.extract_options! - membership_type = opts[:as] members = args.flatten - return unless members.present? - __send__(:clear_association_cache) - - members.each do |member| - member.groups << self unless member.groups.include?(self) - if membership_type - member.group_memberships_as_member.where(group_id: id, group_type: self.class.model_name.to_s, membership_type: membership_type).first_or_create! + if members.present? + if self.class.default_members_association_name.present? && respond_to?(self.class.default_members_association_name) + association = __send__(self.class.default_members_association_name) + association.add members, opts + else + members.each do |member| + member.groups << self + end end - member.__send__(:clear_association_cache) end + + self end # Merge a source group into this group. @@ -84,9 +85,17 @@ def has_member(name, options = {}) association_name = name.to_sym end + if options[:default_members] + @default_members_association_name = association_name + end + register(klass, association_name) end + def default_members_association_name + @default_members_association_name ||= :members + end + # Merge two groups. The members of the source become members of the destination, and the source is destroyed. def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination From f177d8513d259109866a29005ecf26aa4cefada8 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 02:47:00 -0400 Subject: [PATCH 007/205] Don't set default, for backwards-compatibility --- lib/groupify/adapter/active_record/group.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 51edfa6..3cca2e0 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -31,7 +31,7 @@ def add(*args) members = args.flatten if members.present? - if self.class.default_members_association_name.present? && respond_to?(self.class.default_members_association_name) + if self.class.default_members_association_name && respond_to?(self.class.default_members_association_name) association = __send__(self.class.default_members_association_name) association.add members, opts else @@ -93,7 +93,7 @@ def has_member(name, options = {}) end def default_members_association_name - @default_members_association_name ||= :members + @default_members_association_name end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -101,13 +101,13 @@ def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = (source_group.member_classes - destination_group.member_classes) invalid_member_classes.each do |klass| - if klass.joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: source_group)).count > 0 + if klass.joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: source_group.id)).count > 0 raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") end end source_group.transaction do - source_group.group_memberships_as_group.update_all(:group_id => destination_group.id) + source_group.group_memberships_as_group.update_all(group_id: destination_group.id) source_group.destroy end end From 4bcff733feb52466104af9d7f8401983e5a6a177 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 02:47:21 -0400 Subject: [PATCH 008/205] Query based group class (to support subclasses) --- lib/groupify/adapter/active_record/group.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 3cca2e0..98c8c2a 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -51,7 +51,8 @@ def merge!(source) module ClassMethods def with_member(member) - member.groups + #member.groups + where(id: member.group_memberships_as_member.where(group_type: self.model_name.to_s).select(:group_id)) end def default_member_class From 337f3c903ffbec53318bc120131746b1cd1126d0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 02:57:11 -0400 Subject: [PATCH 009/205] Remove unused option --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 479d626..89e3423 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ class GroupBase < ActiveRecord::Base end class Organization < GroupBase - acts_as_group default_member_association: :users + acts_as_group has_member :users, class_name: 'CustomUserClass', default_members: true end From 1710727fa3cbe57c0666d425671ed1891d725ebf Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 03:46:46 -0400 Subject: [PATCH 010/205] Clear association cache after deleting associated models --- lib/groupify/adapter/active_record/group.rb | 4 ++++ lib/groupify/adapter/active_record/group_member.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 98c8c2a..bb3820e 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -162,6 +162,8 @@ def delete(*args) else super(*members) end + + members.each{|member| member.__send__(:clear_association_cache)} end def destroy(*args) @@ -176,6 +178,8 @@ def destroy(*args) else super(*members) end + + members.each{|member| member.__send__(:clear_association_cache)} end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 7eb4799..4666acf 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -61,6 +61,8 @@ def delete(*args) else super(*groups) end + + groups.each{|group| group.__send__(:clear_association_cache)} end def destroy(*args) @@ -72,6 +74,8 @@ def destroy(*args) else super(*groups) end + + groups.each{|group| group.__send__(:clear_association_cache)} end end From 3df2ea4eed51977aea46ad5a0c33a9dce1d43a4d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 03:47:00 -0400 Subject: [PATCH 011/205] Fix query to be extensible --- lib/groupify/adapter/active_record/group.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index bb3820e..bb5b7be 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -52,7 +52,9 @@ def merge!(source) module ClassMethods def with_member(member) #member.groups - where(id: member.group_memberships_as_member.where(group_type: self.model_name.to_s).select(:group_id)) + joins(:group_memberships_as_group). + where(group_memberships: {member_id: member.id, member_type: member.class.model_name.to_s}). + extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) end def default_member_class From 2a84b72b6d44d92b1f47fd7c6cab2d942a3d6000 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 15:30:53 -0400 Subject: [PATCH 012/205] Refactored to mimic `<<` method returning `false` if an invalid argument is given otherwise returns collection --- lib/groupify/adapter/active_record/group.rb | 22 +++++++++++++++++-- .../adapter/active_record/group_member.rb | 22 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index bb5b7be..c594e56 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -139,15 +139,33 @@ def <<(*args) group = proxy_association.owner group.__send__(:clear_association_cache) + to_add_directly = [] + to_add_with_membership_type = [] + + # first prepare changes members.each do |member| - super(member) unless include?(member) + # add to collection without membership type + to_add_directly << member unless include?(member) + # add a second entry for the given membership type if membership_type membership = member.group_memberships_as_member.where(group_id: group.id, group_type: group.class.model_name.to_s, membership_type: membership_type).first_or_initialize - member.group_memberships_as_member << membership unless membership.persisted? + to_add_with_membership_type << membership unless membership.persisted? end member.__send__(:clear_association_cache) end + # then validate changes + return false unless to_add_directly.all?(&:valid?) + return false unless to_add_with_membership_type.all?(&:valid?) + + # then persist changes + super(to_add_directly) + + to_add_with_membership_type.each do |membership| + membership.member.group_memberships_as_member << membership + membership.member.__send__(:clear_association_cache) + end + self end alias_method :add, :<< diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 4666acf..b089851 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -39,15 +39,33 @@ def <<(*args) member = proxy_association.owner member.__send__(:clear_association_cache) + to_add_directly = [] + to_add_with_membership_type = [] + + # first prepare changes groups.each do |group| - super(group) unless include?(group) + # add to collection without membership type + to_add_directly << group unless include?(group) + # add a second entry for the given membership type if membership_type membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.model_name.to_s, membership_type: membership_type).first_or_initialize - group.group_memberships_as_group << membership unless membership.persisted? + to_add_with_membership_type << membership unless membership.persisted? end group.__send__(:clear_association_cache) end + # then validate changes + return false unless to_add_directly.all?(&:valid?) + return false unless to_add_with_membership_type.all?(&:valid?) + + # then persist changes + super(to_add_directly) + + to_add_with_membership_type.each do |membership| + membership.group.group_memberships_as_group << membership + membership.group.__send__(:clear_association_cache) + end + self end alias_method :add, :<< From c9b8d58c172bafdd36fb12048bac2e7680e546be Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 15:31:10 -0400 Subject: [PATCH 013/205] Add to collection with options (e.g. membership type) --- lib/groupify/adapter/active_record/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index c594e56..f00c2e6 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -36,7 +36,7 @@ def add(*args) association.add members, opts else members.each do |member| - member.groups << self + member.groups.add self, opts end end end From ae7bec1370e8b395a4e9271eb35cf7d634928fa5 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 15:31:37 -0400 Subject: [PATCH 014/205] Specify group class name when merging in case it's a subclass --- lib/groupify/adapter/active_record/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index f00c2e6..d798ead 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -110,7 +110,7 @@ def merge!(source_group, destination_group) end source_group.transaction do - source_group.group_memberships_as_group.update_all(group_id: destination_group.id) + source_group.group_memberships_as_group.update_all(group_id: destination_group.id, group_type: destination_group.class.model_name.to_s) source_group.destroy end end From 0166f5d39a8277fa9e459ab37f7a266e9237cac3 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 17:12:21 -0400 Subject: [PATCH 015/205] Remove `group_type` based on group class because we handle STI separately --- lib/groupify/adapter/active_record/group.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index d798ead..edb58e4 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -110,7 +110,7 @@ def merge!(source_group, destination_group) end source_group.transaction do - source_group.group_memberships_as_group.update_all(group_id: destination_group.id, group_type: destination_group.class.model_name.to_s) + source_group.group_memberships_as_group.update_all(group_id: destination_group.id) source_group.destroy end end @@ -148,7 +148,7 @@ def <<(*args) to_add_directly << member unless include?(member) # add a second entry for the given membership type if membership_type - membership = member.group_memberships_as_member.where(group_id: group.id, group_type: group.class.model_name.to_s, membership_type: membership_type).first_or_initialize + membership = member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end member.__send__(:clear_association_cache) From b87d1f162be65dca54c89e7d3939745322cc7264 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 17:35:56 -0400 Subject: [PATCH 016/205] Use ActiveRecord `base_class` in case member is STI --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index edb58e4..452b37d 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -53,7 +53,7 @@ module ClassMethods def with_member(member) #member.groups joins(:group_memberships_as_group). - where(group_memberships: {member_id: member.id, member_type: member.class.model_name.to_s}). + where(group_memberships: {member_id: member.id, member_type: member.class.base_class.to_s}). extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index b089851..0456673 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -48,7 +48,7 @@ def <<(*args) to_add_directly << group unless include?(group) # add a second entry for the given membership type if membership_type - membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.model_name.to_s, membership_type: membership_type).first_or_initialize + membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end group.__send__(:clear_association_cache) From 74b3044ade43d407e236db3eb121aa230a5be3d6 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 18:00:23 -0400 Subject: [PATCH 017/205] Revert back to using association and assume STI --- lib/groupify/adapter/active_record/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 452b37d..65050be 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -53,7 +53,7 @@ module ClassMethods def with_member(member) #member.groups joins(:group_memberships_as_group). - where(group_memberships: {member_id: member.id, member_type: member.class.base_class.to_s}). + merge(Groupify.group_membership_klass.where(member_id: member.id, member_type: member.class.base_class.to_s)). extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) end From abd27e429f825f9a9ebfa810a5b8aff227c74f82 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 18:20:24 -0400 Subject: [PATCH 018/205] Added ability for `group.add` to throw a validation exception --- lib/groupify/adapter/active_record/group.rb | 9 +++++---- lib/groupify/adapter/active_record/group_member.rb | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 65050be..5d8bf19 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -27,7 +27,7 @@ def member_classes end def add(*args) - opts = args.extract_options! + opts = {silent: false}.merge args.extract_options! members = args.flatten if members.present? @@ -131,7 +131,7 @@ def as(membership_type) end def <<(*args) - opts = args.extract_options! + opts = {silent: true}.merge args.extract_options! membership_type = opts[:as] members = args.flatten return self unless members.present? @@ -155,8 +155,9 @@ def <<(*args) end # then validate changes - return false unless to_add_directly.all?(&:valid?) - return false unless to_add_with_membership_type.all?(&:valid?) + validation_method = opts[:silent] ? :valid? : :validate! + return false unless to_add_directly.all?(&validation_method) + return false unless to_add_with_membership_type.all?(&validation_method) # then persist changes super(to_add_directly) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 0456673..7eaaa91 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -31,7 +31,7 @@ def as(membership_type) end def <<(*args) - opts = args.extract_options! + opts = {silent: true}.merge args.extract_options! membership_type = opts[:as] groups = args.flatten return self unless groups.present? @@ -55,8 +55,9 @@ def <<(*args) end # then validate changes - return false unless to_add_directly.all?(&:valid?) - return false unless to_add_with_membership_type.all?(&:valid?) + validation_method = opts[:silent] ? :valid? : :validate! + return false unless to_add_directly.all?(&validation_method) + return false unless to_add_with_membership_type.all?(&validation_method) # then persist changes super(to_add_directly) From 76d45ccd2727d0dd849ea7372bc15a04eaadda43 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 10 May 2017 18:20:51 -0400 Subject: [PATCH 019/205] Remove unneeded `default_members_association_name` feature --- lib/groupify/adapter/active_record/group.rb | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 5d8bf19..467e190 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -30,15 +30,8 @@ def add(*args) opts = {silent: false}.merge args.extract_options! members = args.flatten - if members.present? - if self.class.default_members_association_name && respond_to?(self.class.default_members_association_name) - association = __send__(self.class.default_members_association_name) - association.add members, opts - else - members.each do |member| - member.groups.add self, opts - end - end + members.each do |member| + member.groups.add(self, opts) end self @@ -88,17 +81,9 @@ def has_member(name, options = {}) association_name = name.to_sym end - if options[:default_members] - @default_members_association_name = association_name - end - register(klass, association_name) end - def default_members_association_name - @default_members_association_name - end - # Merge two groups. The members of the source become members of the destination, and the source is destroyed. def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination From 5694c1567398ce374638dcfdb15a6e7a663c074a Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 16:39:13 -0400 Subject: [PATCH 020/205] Don't use `validate!` to ensure Rails 4.0.x compatibility --- lib/groupify/adapter/active_record/group.rb | 14 +++++++++++--- lib/groupify/adapter/active_record/group_member.rb | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 467e190..7ae8d71 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -140,9 +140,17 @@ def <<(*args) end # then validate changes - validation_method = opts[:silent] ? :valid? : :validate! - return false unless to_add_directly.all?(&validation_method) - return false unless to_add_with_membership_type.all?(&validation_method) + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |item| + next if item.valid? + + if opts[:silent] + return false + else + raise RecordInvalid.new(item) + end + end # then persist changes super(to_add_directly) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 7eaaa91..be08938 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -55,9 +55,17 @@ def <<(*args) end # then validate changes - validation_method = opts[:silent] ? :valid? : :validate! - return false unless to_add_directly.all?(&validation_method) - return false unless to_add_with_membership_type.all?(&validation_method) + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |item| + next if item.valid? + + if opts[:silent] + return false + else + raise RecordInvalid.new(item) + end + end # then persist changes super(to_add_directly) From c18a2f42d9cc946e796a5ed83f2d5092e541224b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:08:18 -0400 Subject: [PATCH 021/205] Move extensions modules to separate files --- lib/groupify/adapter/active_record/group.rb | 2 +- .../group_association_extensions.rb | 87 ++++++++++++++++++ .../adapter/active_record/group_member.rb | 4 +- .../member_association_extensions.rb | 92 +++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 lib/groupify/adapter/active_record/group_association_extensions.rb create mode 100644 lib/groupify/adapter/active_record/member_association_extensions.rb diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 7ae8d71..72aeef6 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -214,7 +214,7 @@ def define_member_association(member_klass, association_name = nil) through: :group_memberships_as_group, source: :member, source_type: source_type.to_s, - extend: MemberAssociationExtensions + extend: Groupify::ActiveRecord::MemberAssociationExtensions end end end diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb new file mode 100644 index 0000000..ef25b19 --- /dev/null +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -0,0 +1,87 @@ +module Groupify + module ActiveRecord + module GroupAssociationExtensions + include AssociationExtensions + + def as(membership_type) + return self unless membership_type + where(group_memberships: {membership_type: membership_type}) + end + + def <<(*args) + opts = {silent: true}.merge args.extract_options! + membership_type = opts[:as] + groups = args.flatten + return self unless groups.present? + + member = proxy_association.owner + member.__send__(:clear_association_cache) + + to_add_directly = [] + to_add_with_membership_type = [] + + # first prepare changes + groups.each do |group| + # add to collection without membership type + to_add_directly << group unless include?(group) + # add a second entry for the given membership type + if membership_type + membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type).first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + group.__send__(:clear_association_cache) + end + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |item| + next if item.valid? + + if opts[:silent] + return false + else + raise RecordInvalid.new(item) + end + end + + # then persist changes + super(to_add_directly) + + to_add_with_membership_type.each do |membership| + membership.group.group_memberships_as_group << membership + membership.group.__send__(:clear_association_cache) + end + + self + end + alias_method :add, :<< + + def delete(*args) + opts = args.extract_options! + groups = args.flatten + + if opts[:as] + proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).delete_all + else + super(*groups) + end + + groups.each{|group| group.__send__(:clear_association_cache)} + end + + def destroy(*args) + opts = args.extract_options! + groups = args.flatten + + if opts[:as] + proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).destroy_all + else + super(*groups) + end + + groups.each{|group| group.__send__(:clear_association_cache)} + end + end + end +end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index be08938..7f3784f 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -153,7 +153,7 @@ def as(membership_type) def in_group(group) return none unless group.present? - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: group)).distinct + joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: group.id)).distinct end def in_any_group(*groups) @@ -199,7 +199,7 @@ def has_group(name, options = {}) through: :group_memberships_as_member, source: :group, source_type: @group_class_name, - extend: GroupAssociationExtensions + extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb new file mode 100644 index 0000000..479ee5e --- /dev/null +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -0,0 +1,92 @@ +module Groupify + module ActiveRecord + module MemberAssociationExtensions + include AssociationExtensions + + def as(membership_type) + where(group_memberships: {membership_type: membership_type}) + end + + def <<(*args) + opts = {silent: true}.merge args.extract_options! + membership_type = opts[:as] + members = args.flatten + return self unless members.present? + + group = proxy_association.owner + group.__send__(:clear_association_cache) + + to_add_directly = [] + to_add_with_membership_type = [] + + # first prepare changes + members.each do |member| + # add to collection without membership type + to_add_directly << member unless include?(member) + # add a second entry for the given membership type + if membership_type + membership = member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type).first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + member.__send__(:clear_association_cache) + end + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |item| + next if item.valid? + + if opts[:silent] + return false + else + raise RecordInvalid.new(item) + end + end + + # then persist changes + super(to_add_directly) + + to_add_with_membership_type.each do |membership| + membership.member.group_memberships_as_member << membership + membership.member.__send__(:clear_association_cache) + end + + self + end + alias_method :add, :<< + + def delete(*args) + opts = args.extract_options! + members = args + + if opts[:as] + proxy_association.owner.group_memberships_as_group. + where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). + as(opts[:as]). + delete_all + else + super(*members) + end + + members.each{|member| member.__send__(:clear_association_cache)} + end + + def destroy(*args) + opts = args.extract_options! + members = args + + if opts[:as] + proxy_association.owner.group_memberships_as_group. + where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). + as(opts[:as]). + destroy_all + else + super(*members) + end + + members.each{|member| member.__send__(:clear_association_cache)} + end + end + end +end From ef466f24b0837795c2ca753457126e40d8a952ca Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:33:43 -0400 Subject: [PATCH 022/205] Consolidate queries for deleting records --- .../group_association_extensions.rb | 20 +++++++------ .../member_association_extensions.rb | 28 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index ef25b19..2cb8eb6 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -57,12 +57,11 @@ def <<(*args) end alias_method :add, :<< - def delete(*args) - opts = args.extract_options! - groups = args.flatten + def delete(*groups) + opts = groups.extract_options! if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).delete_all + find_for_destruction(opts[:as], *groups).delete_all else super(*groups) end @@ -70,18 +69,23 @@ def delete(*args) groups.each{|group| group.__send__(:clear_association_cache)} end - def destroy(*args) - opts = args.extract_options! - groups = args.flatten + def destroy(*groups) + opts = groups.extract_options! if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).destroy_all + find_for_destruction(opts[:as], *groups).destroy_all else super(*groups) end groups.each{|group| group.__send__(:clear_association_cache)} end + + protected + + def find_for_destruction(membership_type, *groups) + proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(membership_type) + end end end end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 479ee5e..0bb9b5a 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -56,15 +56,11 @@ def <<(*args) end alias_method :add, :<< - def delete(*args) - opts = args.extract_options! - members = args + def delete(*members) + opts = members.extract_options! if opts[:as] - proxy_association.owner.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). - as(opts[:as]). - delete_all + find_for_destruction(opts[:as], *members).delete_all else super(*members) end @@ -72,21 +68,25 @@ def delete(*args) members.each{|member| member.__send__(:clear_association_cache)} end - def destroy(*args) - opts = args.extract_options! - members = args + def destroy(*members) + opts = members.extract_options! if opts[:as] - proxy_association.owner.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). - as(opts[:as]). - destroy_all + find_for_destruction(opts[:as], *members).destroy_all else super(*members) end members.each{|member| member.__send__(:clear_association_cache)} end + + protected + + def find_for_destruction(membership_type, *members) + proxy_association.owner.group_memberships_as_group. + where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). + as(membership_type) + end end end end From de70b89a325847c4f0f4b43a8d7cccae5e6b757c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:34:02 -0400 Subject: [PATCH 023/205] Refactor to handle groups and members associations with same logic --- .../active_record/association_extensions.rb | 82 +++++++++++++++++++ .../group_association_extensions.rb | 53 +----------- .../member_association_extensions.rb | 52 +----------- 3 files changed, 86 insertions(+), 101 deletions(-) create mode 100644 lib/groupify/adapter/active_record/association_extensions.rb diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb new file mode 100644 index 0000000..3b92820 --- /dev/null +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -0,0 +1,82 @@ +module Groupify + module ActiveRecord + module AssociationExtensions + + def as(membership_type) + return self unless membership_type + where(group_memberships: {membership_type: membership_type}) + end + + private + + def add_children_to_parent(parent_type, *args) + opts = {silent: true}.merge args.extract_options! + membership_type = opts[:as] + children = args.flatten + return self unless children.present? + + parent = proxy_association.owner + parent.__send__(:clear_association_cache) + + finder_method = :"find_memberships_for_#{parent_type == :group ? :member : :group}" + + to_add_directly = [] + to_add_with_membership_type = [] + + # first prepare changes + children.each do |child| + # add to collection without membership type + to_add_directly << item unless association.include?(item) + # add a second entry for the given membership type + if membership_type + membership = item.__send__(finder_method, parent, child).first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + parent.__send__(:clear_association_cache) + end + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |item| + next if item.valid? + + if opts[:silent] + return false + else + raise RecordInvalid.new(item) + end + end + + # then persist changes + super(to_add_directly) + + memberships_association = :"group_memberships_as_#{parent_type}" + + to_add_with_membership_type.each do |membership| + membership_parent = membership.__send__(parent_type) + membership_parent.__send__(memberships_association) << membership + membership_parent.__send__(:clear_association_cache) + end + + self + end + alias_method :add, :<< + + def find_memberships_for_member(group, member, membership_type) + group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) + end + + def find_memberships_for_group(member, group, membership_type) + member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) + end + + def find_members_for_destruction(group, member_type) + group.group_memberships_as_group. + where(member_id: members.map(&:id), member_type: member_type). + as(opts[:as]) + end + + end + end +end diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 2cb8eb6..3fb6908 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -3,57 +3,8 @@ module ActiveRecord module GroupAssociationExtensions include AssociationExtensions - def as(membership_type) - return self unless membership_type - where(group_memberships: {membership_type: membership_type}) - end - - def <<(*args) - opts = {silent: true}.merge args.extract_options! - membership_type = opts[:as] - groups = args.flatten - return self unless groups.present? - - member = proxy_association.owner - member.__send__(:clear_association_cache) - - to_add_directly = [] - to_add_with_membership_type = [] - - # first prepare changes - groups.each do |group| - # add to collection without membership type - to_add_directly << group unless include?(group) - # add a second entry for the given membership type - if membership_type - membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type).first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - group.__send__(:clear_association_cache) - end - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |item| - next if item.valid? - - if opts[:silent] - return false - else - raise RecordInvalid.new(item) - end - end - - # then persist changes - super(to_add_directly) - - to_add_with_membership_type.each do |membership| - membership.group.group_memberships_as_group << membership - membership.group.__send__(:clear_association_cache) - end - - self + def <<(*children) + add_children_to_parent(:group, *children) end alias_method :add, :<< diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 0bb9b5a..07574ed 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -2,57 +2,9 @@ module Groupify module ActiveRecord module MemberAssociationExtensions include AssociationExtensions - - def as(membership_type) - where(group_memberships: {membership_type: membership_type}) - end - - def <<(*args) - opts = {silent: true}.merge args.extract_options! - membership_type = opts[:as] - members = args.flatten - return self unless members.present? - - group = proxy_association.owner - group.__send__(:clear_association_cache) - - to_add_directly = [] - to_add_with_membership_type = [] - - # first prepare changes - members.each do |member| - # add to collection without membership type - to_add_directly << member unless include?(member) - # add a second entry for the given membership type - if membership_type - membership = member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type).first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - member.__send__(:clear_association_cache) - end - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |item| - next if item.valid? - - if opts[:silent] - return false - else - raise RecordInvalid.new(item) - end - end - - # then persist changes - super(to_add_directly) - - to_add_with_membership_type.each do |membership| - membership.member.group_memberships_as_member << membership - membership.member.__send__(:clear_association_cache) - end - self + def <<(*children) + add_children_to_parent(:member, *children) end alias_method :add, :<< From f4ef818878db9e1f5d64b9fcf8edb0e1f1d86d80 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:37:00 -0400 Subject: [PATCH 024/205] Moved `delete` and `destroy` methods to shared module --- .../active_record/association_extensions.rb | 24 +++++++++++++++++++ .../group_association_extensions.rb | 24 ------------------- .../member_association_extensions.rb | 24 ------------------- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 3b92820..05f6d5c 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -7,6 +7,30 @@ def as(membership_type) where(group_memberships: {membership_type: membership_type}) end + def delete(*records) + opts = records.extract_options! + + if opts[:as] + find_for_destruction(opts[:as], *records).delete_all + else + super(*records) + end + + records.each{|record| record.__send__(:clear_association_cache)} + end + + def destroy(*records) + opts = records.extract_options! + + if opts[:as] + find_for_destruction(opts[:as], *records).destroy_all + else + super(*records) + end + + records.each{|record| record.__send__(:clear_association_cache)} + end + private def add_children_to_parent(parent_type, *args) diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 3fb6908..9a1d291 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -8,30 +8,6 @@ def <<(*children) end alias_method :add, :<< - def delete(*groups) - opts = groups.extract_options! - - if opts[:as] - find_for_destruction(opts[:as], *groups).delete_all - else - super(*groups) - end - - groups.each{|group| group.__send__(:clear_association_cache)} - end - - def destroy(*groups) - opts = groups.extract_options! - - if opts[:as] - find_for_destruction(opts[:as], *groups).destroy_all - else - super(*groups) - end - - groups.each{|group| group.__send__(:clear_association_cache)} - end - protected def find_for_destruction(membership_type, *groups) diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 07574ed..b8f7e0e 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -8,30 +8,6 @@ def <<(*children) end alias_method :add, :<< - def delete(*members) - opts = members.extract_options! - - if opts[:as] - find_for_destruction(opts[:as], *members).delete_all - else - super(*members) - end - - members.each{|member| member.__send__(:clear_association_cache)} - end - - def destroy(*members) - opts = members.extract_options! - - if opts[:as] - find_for_destruction(opts[:as], *members).destroy_all - else - super(*members) - end - - members.each{|member| member.__send__(:clear_association_cache)} - end - protected def find_for_destruction(membership_type, *members) From 16a5896ab0a3111c4cd89e0f1a6347d067f50718 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:52:24 -0400 Subject: [PATCH 025/205] Move finder methods to appropriate modules --- .../active_record/association_extensions.rb | 24 +++---------------- .../group_association_extensions.rb | 6 ++++- .../member_association_extensions.rb | 6 ++++- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 05f6d5c..c30b3a3 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -31,9 +31,7 @@ def destroy(*records) records.each{|record| record.__send__(:clear_association_cache)} end - private - - def add_children_to_parent(parent_type, *args) + def add_children_to_parent(parent_type, *args, &_super) opts = {silent: true}.merge args.extract_options! membership_type = opts[:as] children = args.flatten @@ -42,8 +40,6 @@ def add_children_to_parent(parent_type, *args) parent = proxy_association.owner parent.__send__(:clear_association_cache) - finder_method = :"find_memberships_for_#{parent_type == :group ? :member : :group}" - to_add_directly = [] to_add_with_membership_type = [] @@ -53,7 +49,7 @@ def add_children_to_parent(parent_type, *args) to_add_directly << item unless association.include?(item) # add a second entry for the given membership type if membership_type - membership = item.__send__(finder_method, parent, child).first_or_initialize + membership = find_memberships_for_adding_children(parent, child).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end parent.__send__(:clear_association_cache) @@ -73,7 +69,7 @@ def add_children_to_parent(parent_type, *args) end # then persist changes - super(to_add_directly) + _super.call(to_add_directly) memberships_association = :"group_memberships_as_#{parent_type}" @@ -87,20 +83,6 @@ def add_children_to_parent(parent_type, *args) end alias_method :add, :<< - def find_memberships_for_member(group, member, membership_type) - group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) - end - - def find_memberships_for_group(member, group, membership_type) - member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) - end - - def find_members_for_destruction(group, member_type) - group.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: member_type). - as(opts[:as]) - end - end end end diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 9a1d291..d7b2545 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -4,12 +4,16 @@ module GroupAssociationExtensions include AssociationExtensions def <<(*children) - add_children_to_parent(:group, *children) + add_children_to_parent(:group, *children, &super) end alias_method :add, :<< protected + def find_memberships_for_adding_children(group, member, membership_type) + group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) + end + def find_for_destruction(membership_type, *groups) proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(membership_type) end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index b8f7e0e..e4c55d6 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -4,12 +4,16 @@ module MemberAssociationExtensions include AssociationExtensions def <<(*children) - add_children_to_parent(:member, *children) + add_children_to_parent(:member, *children, &super) end alias_method :add, :<< protected + def find_memberships_for_adding_children(member, group, membership_type) + member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) + end + def find_for_destruction(membership_type, *members) proxy_association.owner.group_memberships_as_group. where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). From fcaf155777ddbbfd7adbe677d72fde483c53b54e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:52:55 -0400 Subject: [PATCH 026/205] Reuse delete/destroy logic --- .../active_record/association_extensions.rb | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index c30b3a3..e727495 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -8,22 +8,20 @@ def as(membership_type) end def delete(*records) - opts = records.extract_options! - - if opts[:as] - find_for_destruction(opts[:as], *records).delete_all - else - super(*records) - end - - records.each{|record| record.__send__(:clear_association_cache)} + remove_children_from_parent(:delete, *records, &super) end def destroy(*records) - opts = records.extract_options! + remove_children_from_parent(:destroy, *records, &super) + end + + private + + def remove_children_from_parent(destruction_type, *records) + membership_type = records.extract_options![:as] - if opts[:as] - find_for_destruction(opts[:as], *records).destroy_all + if membership_type + find_for_destruction(membership_type, *records).__send__(:"#{destruction_type}_all") else super(*records) end From 34d659b288b95297fb73b8de8e62c5a7dfde6367 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:54:32 -0400 Subject: [PATCH 027/205] Require modules --- lib/groupify/adapter/active_record/group.rb | 2 ++ .../adapter/active_record/group_association_extensions.rb | 2 ++ lib/groupify/adapter/active_record/group_member.rb | 2 ++ .../adapter/active_record/member_association_extensions.rb | 2 ++ 4 files changed, 8 insertions(+) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 72aeef6..139392e 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -1,3 +1,5 @@ +require 'groupify/active_record/member_association_extensions' + module Groupify module ActiveRecord diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index d7b2545..a3e727a 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -1,3 +1,5 @@ +require 'groupify/active_record/association_extensions' + module Groupify module ActiveRecord module GroupAssociationExtensions diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 7f3784f..dd5cc23 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -1,3 +1,5 @@ +require 'groupify/active_record/group_association_extensions' + module Groupify module ActiveRecord diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index e4c55d6..9d93e58 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -1,3 +1,5 @@ +require 'groupify/active_record/association_extensions' + module Groupify module ActiveRecord module MemberAssociationExtensions From 3eb9a599dc13e2ebc8d3122efea7cce18864961e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 17:55:27 -0400 Subject: [PATCH 028/205] Mark methods protected --- lib/groupify/adapter/active_record/association_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index e727495..7d52820 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -15,7 +15,7 @@ def destroy(*records) remove_children_from_parent(:destroy, *records, &super) end - private + protected def remove_children_from_parent(destruction_type, *records) membership_type = records.extract_options![:as] From a5b73cf9d39a0b6d29530c48f8d2e4d0c194f0da Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 18:02:52 -0400 Subject: [PATCH 029/205] Define `add` explicitly as raising an exception instead of using `opts` --- .../adapter/active_record/association_extensions.rb | 10 ++++------ lib/groupify/adapter/active_record/group.rb | 9 +++------ .../active_record/group_association_extensions.rb | 7 +++++-- .../active_record/member_association_extensions.rb | 7 +++++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 7d52820..0f4fe69 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -29,9 +29,7 @@ def remove_children_from_parent(destruction_type, *records) records.each{|record| record.__send__(:clear_association_cache)} end - def add_children_to_parent(parent_type, *args, &_super) - opts = {silent: true}.merge args.extract_options! - membership_type = opts[:as] + def add_children_to_parent(parent_type, should_raise_exception, *args, &_super) children = args.flatten return self unless children.present? @@ -59,10 +57,10 @@ def add_children_to_parent(parent_type, *args, &_super) list_to_validate.each do |item| next if item.valid? - if opts[:silent] - return false - else + if should_raise_exception raise RecordInvalid.new(item) + else + return false end end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 139392e..6489b3b 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -28,12 +28,9 @@ def member_classes self.class.member_classes end - def add(*args) - opts = {silent: false}.merge args.extract_options! - members = args.flatten - - members.each do |member| - member.groups.add(self, opts) + def add(*members) + members.flatten.each do |member| + member.groups.add(self) end self diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index a3e727a..33bf672 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -6,9 +6,12 @@ module GroupAssociationExtensions include AssociationExtensions def <<(*children) - add_children_to_parent(:group, *children, &super) + add_children_to_parent(:group, false, *children, &super) + end + + def add(*children) + add_children_to_parent(:group, true, *children, &super) end - alias_method :add, :<< protected diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 9d93e58..92c6030 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -6,9 +6,12 @@ module MemberAssociationExtensions include AssociationExtensions def <<(*children) - add_children_to_parent(:member, *children, &super) + add_children_to_parent(:member, false, *children, &super) + end + + def add(*children) + add_children_to_parent(:member, true, *children, &super) end - alias_method :add, :<< protected From 4ec1ae154e0e7b7614f5560bf3ff31b82ce89c97 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 19:55:40 -0400 Subject: [PATCH 030/205] Alias `add` and `<<` in shared module --- .../active_record/association_extensions.rb | 19 +++++++++++++++++-- lib/groupify/adapter/active_record/group.rb | 1 - .../group_association_extensions.rb | 9 +++------ .../member_association_extensions.rb | 9 +++------ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 0f4fe69..5f96475 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -1,6 +1,23 @@ module Groupify module ActiveRecord module AssociationExtensions + extend ActiveSupport::Concern + + module ClassMethods + def setup_alias_methods! + alias_method :add_as_usual, :<< + alias_method :<<, :add_without_exception + alias_method :add, :add_with_exception + end + end + + def add_without_exception(*children) + add_children_to_parent(children.flatten, false) + end + + def add_with_exception(*children) + add_children_to_parent(children.flatten, true) + end def as(membership_type) return self unless membership_type @@ -77,8 +94,6 @@ def add_children_to_parent(parent_type, should_raise_exception, *args, &_super) self end - alias_method :add, :<< - end end end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 6489b3b..b45b751 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -21,7 +21,6 @@ module Group dependent: :destroy, as: :group, class_name: Groupify.group_membership_class_name - end def member_classes diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 33bf672..b5a43e1 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -3,14 +3,11 @@ module Groupify module ActiveRecord module GroupAssociationExtensions + extend ActiveSupport::Concern include AssociationExtensions - def <<(*children) - add_children_to_parent(:group, false, *children, &super) - end - - def add(*children) - add_children_to_parent(:group, true, *children, &super) + included do + setup_alias_methods! end protected diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 92c6030..a523c27 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -3,14 +3,11 @@ module Groupify module ActiveRecord module MemberAssociationExtensions + extend ActiveSupport::Concern include AssociationExtensions - def <<(*children) - add_children_to_parent(:member, false, *children, &super) - end - - def add(*children) - add_children_to_parent(:member, true, *children, &super) + included do + setup_alias_methods! end protected From 8f64968edc9a97e586ba8a7563fc103d7b565474 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 19:57:18 -0400 Subject: [PATCH 031/205] Simplify abstractions and fix some reference errors --- .../active_record/association_extensions.rb | 33 ++++++++++--------- lib/groupify/adapter/active_record/group.rb | 6 ++-- .../group_association_extensions.rb | 10 ++++-- .../adapter/active_record/group_member.rb | 2 +- .../member_association_extensions.rb | 10 ++++-- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 5f96475..87317ac 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -25,30 +25,31 @@ def as(membership_type) end def delete(*records) - remove_children_from_parent(:delete, *records, &super) + remove_children_from_parent(records.flatten, :delete, &method(:super)) end def destroy(*records) - remove_children_from_parent(:destroy, *records, &super) + remove_children_from_parent(records.flatten, :destroy, &method(:super)) end protected - def remove_children_from_parent(destruction_type, *records) + def remove_children_from_parent(records, destruction_type, &fallback) membership_type = records.extract_options![:as] if membership_type find_for_destruction(membership_type, *records).__send__(:"#{destruction_type}_all") else - super(*records) + fallback.call(*records) end records.each{|record| record.__send__(:clear_association_cache)} end - def add_children_to_parent(parent_type, should_raise_exception, *args, &_super) - children = args.flatten - return self unless children.present? + def add_children_to_parent(children, exception_on_invalidation) + membership_type = children.extract_options![:as] + + return self if children.none? parent = proxy_association.owner parent.__send__(:clear_association_cache) @@ -59,10 +60,10 @@ def add_children_to_parent(parent_type, should_raise_exception, *args, &_super) # first prepare changes children.each do |child| # add to collection without membership type - to_add_directly << item unless association.include?(item) + to_add_directly << child unless self.include?(child) # add a second entry for the given membership type if membership_type - membership = find_memberships_for_adding_children(parent, child).first_or_initialize + membership = find_memberships_for(child, membership_type).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end parent.__send__(:clear_association_cache) @@ -71,23 +72,23 @@ def add_children_to_parent(parent_type, should_raise_exception, *args, &_super) # then validate changes list_to_validate = to_add_directly + to_add_with_membership_type - list_to_validate.each do |item| - next if item.valid? + list_to_validate.each do |child| + next if child.valid? - if should_raise_exception - raise RecordInvalid.new(item) + if exception_on_invalidation + raise RecordInvalid.new(child) else return false end end # then persist changes - _super.call(to_add_directly) + add_as_usual(to_add_directly) - memberships_association = :"group_memberships_as_#{parent_type}" + memberships_association = :"group_memberships_as_#{association_parent_type}" to_add_with_membership_type.each do |membership| - membership_parent = membership.__send__(parent_type) + membership_parent = membership.__send__(association_parent_type) membership_parent.__send__(memberships_association) << membership membership_parent.__send__(:clear_association_cache) end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index b45b751..e7daa51 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -1,4 +1,4 @@ -require 'groupify/active_record/member_association_extensions' +require 'groupify/adapter/active_record/member_association_extensions' module Groupify module ActiveRecord @@ -28,8 +28,10 @@ def member_classes end def add(*members) + opts = members.extract_options! + members.flatten.each do |member| - member.groups.add(self) + member.groups.add(self, opts) end self diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index b5a43e1..fab81a1 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -1,4 +1,4 @@ -require 'groupify/active_record/association_extensions' +require 'groupify/adapter/active_record/association_extensions' module Groupify module ActiveRecord @@ -12,8 +12,12 @@ module GroupAssociationExtensions protected - def find_memberships_for_adding_children(group, member, membership_type) - group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) + def association_parent_type + :member + end + + def find_memberships_for(group, membership_type) + proxy_association.owner.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) end def find_for_destruction(membership_type, *groups) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index dd5cc23..1ec96cc 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -1,4 +1,4 @@ -require 'groupify/active_record/group_association_extensions' +require 'groupify/adapter/active_record/group_association_extensions' module Groupify module ActiveRecord diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index a523c27..e7c5235 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -1,4 +1,4 @@ -require 'groupify/active_record/association_extensions' +require 'groupify/adapter/active_record/association_extensions' module Groupify module ActiveRecord @@ -12,8 +12,12 @@ module MemberAssociationExtensions protected - def find_memberships_for_adding_children(member, group, membership_type) - member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) + def association_parent_type + :group + end + + def find_memberships_for(member, membership_type) + proxy_association.owner.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) end def find_for_destruction(membership_type, *members) From 3fd50749078048d4c6b2f4652672310ff0942422 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 21:34:18 -0400 Subject: [PATCH 032/205] Fixed alias methods --- .../active_record/association_extensions.rb | 14 ++++++++------ .../active_record/group_association_extensions.rb | 5 ----- .../active_record/member_association_extensions.rb | 5 ----- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 87317ac..e7d2be4 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -3,12 +3,10 @@ module ActiveRecord module AssociationExtensions extend ActiveSupport::Concern - module ClassMethods - def setup_alias_methods! - alias_method :add_as_usual, :<< - alias_method :<<, :add_without_exception - alias_method :add, :add_with_exception - end + # Defined to create alias methods before + # the association is extended with this module + def <<(*) + super end def add_without_exception(*children) @@ -19,6 +17,10 @@ def add_with_exception(*children) add_children_to_parent(children.flatten, true) end + alias_method :add_as_usual, :<< + alias_method :<<, :add_without_exception + alias_method :add, :add_with_exception + def as(membership_type) return self unless membership_type where(group_memberships: {membership_type: membership_type}) diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index fab81a1..64a321b 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -3,13 +3,8 @@ module Groupify module ActiveRecord module GroupAssociationExtensions - extend ActiveSupport::Concern include AssociationExtensions - included do - setup_alias_methods! - end - protected def association_parent_type diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index e7c5235..626307e 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -3,13 +3,8 @@ module Groupify module ActiveRecord module MemberAssociationExtensions - extend ActiveSupport::Concern include AssociationExtensions - included do - setup_alias_methods! - end - protected def association_parent_type From 19f76bb263ee5f959a9dc5e55e79e45d023ca827 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 21:34:46 -0400 Subject: [PATCH 033/205] Fix `super` reference --- .../adapter/active_record/association_extensions.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index e7d2be4..e2782d7 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -27,11 +27,11 @@ def as(membership_type) end def delete(*records) - remove_children_from_parent(records.flatten, :delete, &method(:super)) + remove_children_from_parent(records.flatten, :delete){ |*args| super(*args) } end def destroy(*records) - remove_children_from_parent(records.flatten, :destroy, &method(:super)) + remove_children_from_parent(records.flatten, :destroy){ |*args| super(*args) } end protected @@ -87,11 +87,9 @@ def add_children_to_parent(children, exception_on_invalidation) # then persist changes add_as_usual(to_add_directly) - memberships_association = :"group_memberships_as_#{association_parent_type}" - to_add_with_membership_type.each do |membership| membership_parent = membership.__send__(association_parent_type) - membership_parent.__send__(memberships_association) << membership + membership_parent.__send__(:"group_memberships_as_#{association_parent_type}") << membership membership_parent.__send__(:clear_association_cache) end From c52d063dd90a3ce90d00afc0f0fc880b19d0bbc5 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 21:35:37 -0400 Subject: [PATCH 034/205] Added `group_type` to query to properly build membership record --- lib/groupify/adapter/active_record/group.rb | 2 +- .../adapter/active_record/group_association_extensions.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index e7daa51..e3406bd 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -214,7 +214,7 @@ def define_member_association(member_klass, association_name = nil) through: :group_memberships_as_group, source: :member, source_type: source_type.to_s, - extend: Groupify::ActiveRecord::MemberAssociationExtensions + extend: MemberAssociationExtensions end end end diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 64a321b..447cd85 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -12,7 +12,7 @@ def association_parent_type end def find_memberships_for(group, membership_type) - proxy_association.owner.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type) + proxy_association.owner.group_memberships_as_member.where(group_id: group.id, group_type: group.class.base_class.to_s, membership_type: membership_type) end def find_for_destruction(membership_type, *groups) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 1ec96cc..d3c2cae 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -201,7 +201,7 @@ def has_group(name, options = {}) through: :group_memberships_as_member, source: :group, source_type: @group_class_name, - extend: Groupify::ActiveRecord::GroupAssociationExtensions + extend: GroupAssociationExtensions }.merge(options.slice :class_name) end end From 539b97cc6393b89babf56e78b9ccb8ad45054a7a Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 21:51:56 -0400 Subject: [PATCH 035/205] Added tests to make sure inverse associations are updated after `delete` and `destroy` --- spec/active_record_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index e5005d9..5a8b3f1 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -250,6 +250,9 @@ class ProjectMember < ActiveRecord::Base group.users.delete(user) group.widgets.destroy(widget) + expect(user.groups).to_not include(group) + expect(widget.groups).to_not include(group) + expect(group.widgets).to_not include(widget) expect(group.users).to_not include(user) @@ -264,6 +267,9 @@ class ProjectMember < ActiveRecord::Base user.groups.delete(group) widget.groups.destroy(group) + expect(group.users).to_not include(user) + expect(group.widgets).to_not include(widget) + expect(group.widgets).to_not include(widget) expect(group.users).to_not include(user) From aa82ec4b81969a4cb448bd809d9727faa51a0317 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 21:53:37 -0400 Subject: [PATCH 036/205] Added tests to make sure members and groups aren't added more than once --- spec/active_record_spec.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 5a8b3f1..ea37181 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -209,6 +209,28 @@ class ProjectMember < ActiveRecord::Base expect(group.users).to include(*users) end + it "only adds group to member.groups once when added directly to association" do + user.groups << group + user.groups << group + + expect(user.groups.count).to eq(1) + + user.groups.reload + + expect(user.groups.count).to eq(1) + end + + it "only adds member to group.members once when added directly to association" do + group.members << user + group.members << user + + expect(group.members.count).to eq(1) + + group.members.reload + + expect(group.members.count).to eq(1) + end + it "only allows members to be added to their configured group type" do classroom = Classroom.create! expect { classroom.add(user) }.to raise_error(ActiveRecord::AssociationTypeMismatch) From 31bcafe6068d45784197cbd07c0b64bc91be3b9b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 18 May 2017 22:06:23 -0400 Subject: [PATCH 037/205] Added tests to check subclassing --- spec/active_record_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index ea37181..26443d2 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -26,6 +26,8 @@ class User < ActiveRecord::Base groupify :group_member groupify :named_group_member + + has_group :organizations, class_name: "Organization" end class Manager < User @@ -243,6 +245,29 @@ class ProjectMember < ActiveRecord::Base parent_org.add(child_org) expect(parent_org.organizations).to include(child_org) end + + it "can have subclassed associations for groups of a specific kind" do + org = Organization.create! + + user.groups << group + user.groups << org + + expect(user.groups).to include(group) + expect(user.groups).to include(org) + + expect(user.organizations).to_not include(group) + expect(user.organizations).to include(org) + + expect(org.members).to include(user) + expect(group.members).to include(user) + + expect(user.organizations.count).to eq(1) + expect(user.organizations.first).to be_a(Organization) + + expect(user.groups.count).to eq(2) + expect(user.groups.first).to be_a(Organization) + expect(user.groups.second).to be_a(Group) + end end it "lists which member classes can belong to this group" do From a3935e7d66dcee26d461e93b7c599c8eae344da4 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 30 Jun 2017 20:55:17 -0400 Subject: [PATCH 038/205] Fix whitespace --- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index d3c2cae..4b2608a 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -172,10 +172,10 @@ def in_all_groups(*groups) return none unless groups.present? joins(:group_memberships_as_member). - group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). + group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). merge(Groupify.group_membership_klass.where(group_id: groups)). having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}) = ?", groups.count). - distinct + distinct end def in_only_groups(*groups) From eea48e08cdc2f10dc0e38693ae49c0577cb83f2f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 4 Jul 2017 13:10:52 -0400 Subject: [PATCH 039/205] Use `merge` instead of `where` --- lib/groupify/adapter/active_record/association_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index e2782d7..1c1a4be 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -23,7 +23,7 @@ def add_with_exception(*children) def as(membership_type) return self unless membership_type - where(group_memberships: {membership_type: membership_type}) + merge(Groupify.group_membership_klass.as(membership_type)) end def delete(*records) From adf691d99d38c59c15c092f45192f1e96256b117 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 4 Jul 2017 15:11:06 -0400 Subject: [PATCH 040/205] Refactor `where` with `merge` --- lib/groupify/adapter/active_record/group.rb | 6 +++--- .../adapter/active_record/group_association_extensions.rb | 7 +++++-- lib/groupify/adapter/active_record/group_member.rb | 6 +++--- .../adapter/active_record/member_association_extensions.rb | 6 ++++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index e3406bd..13cc655 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -46,7 +46,7 @@ module ClassMethods def with_member(member) #member.groups joins(:group_memberships_as_group). - merge(Groupify.group_membership_klass.where(member_id: member.id, member_type: member.class.base_class.to_s)). + merge(member.group_memberships_as_member). extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) end @@ -133,7 +133,7 @@ def <<(*args) to_add_directly << member unless include?(member) # add a second entry for the given membership type if membership_type - membership = member.group_memberships_as_member.where(group_id: group.id, membership_type: membership_type).first_or_initialize + membership = member.group_memberships_as_member.merge(group.group_memberships_as_group).as(membership_type).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end member.__send__(:clear_association_cache) @@ -170,7 +170,7 @@ def delete(*args) if opts[:as] proxy_association.owner.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). + where(member_id: members, member_type: proxy_association.reflection.options[:source_type]). as(opts[:as]). delete_all else diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 447cd85..bc1c117 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -12,11 +12,14 @@ def association_parent_type end def find_memberships_for(group, membership_type) - proxy_association.owner.group_memberships_as_member.where(group_id: group.id, group_type: group.class.base_class.to_s, membership_type: membership_type) + proxy_association.owner.group_memberships_as_member. + merge(group.group_memberships_as_group). + as(membership_type) end def find_for_destruction(membership_type, *groups) - proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(membership_type) + proxy_association.owner.group_memberships_as_member. + where(group_id: groups).as(membership_type) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 4b2608a..2dfe431 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -50,7 +50,7 @@ def <<(*args) to_add_directly << group unless include?(group) # add a second entry for the given membership type if membership_type - membership = group.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type).first_or_initialize + membership = group.group_memberships_as_group.merge(member.group_memberships_as_member).as(membership_type).first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end group.__send__(:clear_association_cache) @@ -86,7 +86,7 @@ def delete(*args) groups = args.flatten if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).delete_all + proxy_association.owner.group_memberships_as_member.where(group_id: groups).as(opts[:as]).delete_all else super(*groups) end @@ -99,7 +99,7 @@ def destroy(*args) groups = args.flatten if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups.map(&:id)).as(opts[:as]).destroy_all + proxy_association.owner.group_memberships_as_member.where(group_id: groups).as(opts[:as]).destroy_all else super(*groups) end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 626307e..cec6d7c 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -12,12 +12,14 @@ def association_parent_type end def find_memberships_for(member, membership_type) - proxy_association.owner.group_memberships_as_group.where(member_id: member.id, member_type: member.class.base_class.to_s, membership_type: membership_type) + proxy_association.owner.group_memberships_as_group. + merge(member.group_memberships_as_member). + as(membership_type) end def find_for_destruction(membership_type, *members) proxy_association.owner.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). + where(member_id: members, member_type: proxy_association.reflection.options[:source_type]). as(membership_type) end end From 3dfd19e4bf33ac8422548bedf3b1dbbe0d33be7b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 4 Jul 2017 16:01:33 -0400 Subject: [PATCH 041/205] Let Rails generate the queries on `group_id` and `group_type` --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 13cc655..8b0f97c 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -89,7 +89,7 @@ def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = (source_group.member_classes - destination_group.member_classes) invalid_member_classes.each do |klass| - if klass.joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: source_group.id)).count > 0 + if klass.joins(:group_memberships_as_member).merge(source_group.group_memberships_as_group).count > 0 raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 2dfe431..7641b40 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -86,7 +86,7 @@ def delete(*args) groups = args.flatten if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups).as(opts[:as]).delete_all + proxy_association.owner.group_memberships_as_member.where(group: groups).as(opts[:as]).delete_all else super(*groups) end @@ -99,7 +99,7 @@ def destroy(*args) groups = args.flatten if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group_id: groups).as(opts[:as]).destroy_all + proxy_association.owner.group_memberships_as_member.where(group: groups).as(opts[:as]).destroy_all else super(*groups) end @@ -155,7 +155,7 @@ def as(membership_type) def in_group(group) return none unless group.present? - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_id: group.id)).distinct + joins(:group_memberships_as_member).merge(group.group_memberships_as_group).distinct end def in_any_group(*groups) @@ -163,7 +163,7 @@ def in_any_group(*groups) return none unless groups.present? joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.where(group_id: groups)). + merge(Groupify.group_membership_klass.where(group: groups)). distinct end @@ -173,7 +173,7 @@ def in_all_groups(*groups) joins(:group_memberships_as_member). group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). - merge(Groupify.group_membership_klass.where(group_id: groups)). + merge(Groupify.group_membership_klass.where(group: groups)). having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}) = ?", groups.count). distinct end @@ -189,7 +189,7 @@ def in_only_groups(*groups) def in_other_groups(*groups) joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.where.not(group_id: groups)) + merge(Groupify.group_membership_klass.where.not(group: groups)) end def shares_any_group(other) From 341eae1747836bef1d259566fcee38681f620cdd Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 4 Jul 2017 16:01:46 -0400 Subject: [PATCH 042/205] Update `group_type` for completeness --- lib/groupify/adapter/active_record/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 8b0f97c..ab9628d 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -95,7 +95,7 @@ def merge!(source_group, destination_group) end source_group.transaction do - source_group.group_memberships_as_group.update_all(group_id: destination_group.id) + source_group.group_memberships_as_group.update_all(group_id: destination_group.id, group_type: destination_group.class.base_class.name) source_group.destroy end end From 2e877380af92bcbf3b74a2df90bffa38b9cdf5d4 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 4 Jul 2017 18:11:54 -0400 Subject: [PATCH 043/205] Remove commented code --- lib/groupify/adapter/active_record/group.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index ab9628d..9af3feb 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -44,7 +44,6 @@ def merge!(source) module ClassMethods def with_member(member) - #member.groups joins(:group_memberships_as_group). merge(member.group_memberships_as_member). extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) From 52517a3b1f0b002983455c479d5339e677320f2e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 5 Jul 2017 02:15:42 -0400 Subject: [PATCH 044/205] Remove duplicated extension modules from rebase --- .../active_record/association_extensions.rb | 18 ++-- lib/groupify/adapter/active_record/group.rb | 98 ++----------------- .../group_association_extensions.rb | 4 +- .../adapter/active_record/group_member.rb | 98 ++----------------- .../member_association_extensions.rb | 2 +- 5 files changed, 25 insertions(+), 195 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 1c1a4be..60c1ced 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -3,6 +3,11 @@ module ActiveRecord module AssociationExtensions extend ActiveSupport::Concern + def as(membership_type) + return self unless membership_type + merge(Groupify.group_membership_klass.as(membership_type)) + end + # Defined to create alias methods before # the association is extended with this module def <<(*) @@ -21,17 +26,12 @@ def add_with_exception(*children) alias_method :<<, :add_without_exception alias_method :add, :add_with_exception - def as(membership_type) - return self unless membership_type - merge(Groupify.group_membership_klass.as(membership_type)) - end - def delete(*records) - remove_children_from_parent(records.flatten, :delete){ |*args| super(*args) } + remove_children_from_parent(records.flatten, :delete){ |records| super(records) } end def destroy(*records) - remove_children_from_parent(records.flatten, :destroy){ |*args| super(*args) } + remove_children_from_parent(records.flatten, :destroy){ |records| super(records) } end protected @@ -40,9 +40,9 @@ def remove_children_from_parent(records, destruction_type, &fallback) membership_type = records.extract_options![:as] if membership_type - find_for_destruction(membership_type, *records).__send__(:"#{destruction_type}_all") + find_for_destruction(membership_type, records).__send__(:"#{destruction_type}_all") else - fallback.call(*records) + fallback.call(records) end records.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 9af3feb..5aa8bbe 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -17,10 +17,11 @@ module Group included do @default_member_class = nil @member_klasses ||= Set.new + has_many :group_memberships_as_group, - dependent: :destroy, - as: :group, - class_name: Groupify.group_membership_class_name + dependent: :destroy, + as: :group, + class_name: Groupify.group_membership_class_name end def member_classes @@ -46,7 +47,7 @@ module ClassMethods def with_member(member) joins(:group_memberships_as_group). merge(member.group_memberships_as_member). - extending(Groupify::ActiveRecord::GroupMember::GroupAssociationExtensions) + extending(Groupify::ActiveRecord::GroupAssociationExtensions) end def default_member_class @@ -109,93 +110,6 @@ def register(member_klass, association_name = nil) member_klass end - module MemberAssociationExtensions - def as(membership_type) - merge(Groupify.group_membership_klass.as(membership_type)) - end - - def <<(*args) - opts = {silent: true}.merge args.extract_options! - membership_type = opts[:as] - members = args.flatten - return self unless members.present? - - group = proxy_association.owner - group.__send__(:clear_association_cache) - - to_add_directly = [] - to_add_with_membership_type = [] - - # first prepare changes - members.each do |member| - # add to collection without membership type - to_add_directly << member unless include?(member) - # add a second entry for the given membership type - if membership_type - membership = member.group_memberships_as_member.merge(group.group_memberships_as_group).as(membership_type).first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - member.__send__(:clear_association_cache) - end - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |item| - next if item.valid? - - if opts[:silent] - return false - else - raise RecordInvalid.new(item) - end - end - - # then persist changes - super(to_add_directly) - - to_add_with_membership_type.each do |membership| - membership.member.group_memberships_as_member << membership - membership.member.__send__(:clear_association_cache) - end - - self - end - alias_method :add, :<< - - def delete(*args) - opts = args.extract_options! - members = args - - if opts[:as] - proxy_association.owner.group_memberships_as_group. - where(member_id: members, member_type: proxy_association.reflection.options[:source_type]). - as(opts[:as]). - delete_all - else - super(*members) - end - - members.each{|member| member.__send__(:clear_association_cache)} - end - - def destroy(*args) - opts = args.extract_options! - members = args - - if opts[:as] - proxy_association.owner.group_memberships_as_group. - where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]). - as(opts[:as]). - destroy_all - else - super(*members) - end - - members.each{|member| member.__send__(:clear_association_cache)} - end - end - def associate_member_class(member_klass, association_name = nil) define_member_association(member_klass, association_name) @@ -213,7 +127,7 @@ def define_member_association(member_klass, association_name = nil) through: :group_memberships_as_group, source: :member, source_type: source_type.to_s, - extend: MemberAssociationExtensions + extend: Groupify::ActiveRecord::MemberAssociationExtensions end end end diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index bc1c117..326ccdc 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -17,9 +17,9 @@ def find_memberships_for(group, membership_type) as(membership_type) end - def find_for_destruction(membership_type, *groups) + def find_for_destruction(membership_type, groups) proxy_association.owner.group_memberships_as_member. - where(group_id: groups).as(membership_type) + where(group: groups).as(membership_type) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 7641b40..e5da6f4 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -15,102 +15,18 @@ module GroupMember extend ActiveSupport::Concern included do - unless respond_to?(:group_memberships_as_member) - has_many :group_memberships_as_member, - as: :member, - autosave: true, - dependent: :destroy, - class_name: Groupify.group_membership_class_name - end + has_many :group_memberships_as_member, + as: :member, + autosave: true, + dependent: :destroy, + class_name: Groupify.group_membership_class_name has_group :groups end - module GroupAssociationExtensions - def as(membership_type) - return self unless membership_type - merge(Groupify.group_membership_klass.as(membership_type)) - end - - def <<(*args) - opts = {silent: true}.merge args.extract_options! - membership_type = opts[:as] - groups = args.flatten - return self unless groups.present? - - member = proxy_association.owner - member.__send__(:clear_association_cache) - - to_add_directly = [] - to_add_with_membership_type = [] - - # first prepare changes - groups.each do |group| - # add to collection without membership type - to_add_directly << group unless include?(group) - # add a second entry for the given membership type - if membership_type - membership = group.group_memberships_as_group.merge(member.group_memberships_as_member).as(membership_type).first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - group.__send__(:clear_association_cache) - end - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |item| - next if item.valid? - - if opts[:silent] - return false - else - raise RecordInvalid.new(item) - end - end - - # then persist changes - super(to_add_directly) - - to_add_with_membership_type.each do |membership| - membership.group.group_memberships_as_group << membership - membership.group.__send__(:clear_association_cache) - end - - self - end - alias_method :add, :<< - - def delete(*args) - opts = args.extract_options! - groups = args.flatten - - if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group: groups).as(opts[:as]).delete_all - else - super(*groups) - end - - groups.each{|group| group.__send__(:clear_association_cache)} - end - - def destroy(*args) - opts = args.extract_options! - groups = args.flatten - - if opts[:as] - proxy_association.owner.group_memberships_as_member.where(group: groups).as(opts[:as]).destroy_all - else - super(*groups) - end - - groups.each{|group| group.__send__(:clear_association_cache)} - end - end - def in_group?(group, opts={}) return false unless group.present? - criteria = {group_id: group.id} + criteria = {group: group} if opts[:as] criteria.merge!(membership_type: opts[:as]) @@ -201,7 +117,7 @@ def has_group(name, options = {}) through: :group_memberships_as_member, source: :group, source_type: @group_class_name, - extend: GroupAssociationExtensions + extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index cec6d7c..8ae183b 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -17,7 +17,7 @@ def find_memberships_for(member, membership_type) as(membership_type) end - def find_for_destruction(membership_type, *members) + def find_for_destruction(membership_type, members) proxy_association.owner.group_memberships_as_group. where(member_id: members, member_type: proxy_association.reflection.options[:source_type]). as(membership_type) From 972b80db3bb3410b63552dc51adc14df8b480f4c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 5 Jul 2017 02:39:45 -0400 Subject: [PATCH 045/205] Simplify member criteria --- .../adapter/active_record/member_association_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 8ae183b..609df40 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -19,7 +19,7 @@ def find_memberships_for(member, membership_type) def find_for_destruction(membership_type, members) proxy_association.owner.group_memberships_as_group. - where(member_id: members, member_type: proxy_association.reflection.options[:source_type]). + where(member: members). as(membership_type) end end From f21339602a69ea1499a36b2029124d419cdde25a Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 5 Jul 2017 03:05:06 -0400 Subject: [PATCH 046/205] helpers to query on polymorphic groups and members --- .../group_association_extensions.rb | 2 +- .../adapter/active_record/group_member.rb | 6 +-- .../adapter/active_record/group_membership.rb | 50 +++++++++++++++++++ .../member_association_extensions.rb | 3 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb index 326ccdc..e33b4ce 100644 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ b/lib/groupify/adapter/active_record/group_association_extensions.rb @@ -19,7 +19,7 @@ def find_memberships_for(group, membership_type) def find_for_destruction(membership_type, groups) proxy_association.owner.group_memberships_as_member. - where(group: groups).as(membership_type) + for_groups(groups).as(membership_type) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index e5da6f4..fea4fd6 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -79,7 +79,7 @@ def in_any_group(*groups) return none unless groups.present? joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.where(group: groups)). + merge(Groupify.group_membership_klass.for_groups(groups)). distinct end @@ -89,7 +89,7 @@ def in_all_groups(*groups) joins(:group_memberships_as_member). group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). - merge(Groupify.group_membership_klass.where(group: groups)). + merge(Groupify.group_membership_klass.for_groups(groups)). having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}) = ?", groups.count). distinct end @@ -105,7 +105,7 @@ def in_only_groups(*groups) def in_other_groups(*groups) joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.where.not(group: groups)) + merge(Groupify.group_membership_klass.not_for_groups(groups)) end def shares_any_group(other) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 99837b3..c7e6e1e 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -41,6 +41,56 @@ def named(group_name=nil) def as(membership_type) where(membership_type: membership_type) end + + def for_groups(groups) + for_polymorphic(:group, groups) + end + + def not_for_groups(groups) + for_polymorphic(:group, groups, not: true) + end + + def for_members(members) + for_polymorphic(:member, members) + end + + def criteria_for_groups(groups) + criteria_for_polymorphic(:group, groups) + end + + def criteria_for_members(members) + criteria_for_polymorphic(:member, members) + end + + def for_polymorphic(name, records, options = {}) + if records.is_a?(Array) + if options[:not] + where.not(criteria_for_polymorphic(name, records)) + else + where(criteria_for_polymorphic(name, records)) + end + elsif records.is_a?(::ActiveRecord::Relation) + merge(records) + elsif records + merge(records.__send__(:"group_memberships_as_#{name}")) + else + self + end + end + + def criteria_for_polymorphic(prefix, records) + records_by_base_class = records.group_by{ |record| record.class.base_class } + klass = respond_to?(:proxy_association) ? proxy_association.klass : self + + criteria_values = records_by_base_class.map do |base_class, records| + klass.arel_table.grouping([ + klass.arel_table[:"#{prefix}_type"].eq(base_class.name), + klass.arel_table[:"#{prefix}_id"].in(records.map(&:id)) + ].reduce(:and)) + end + + criteria_values.reduce(:or) + end end end end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb index 609df40..6d08063 100644 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ b/lib/groupify/adapter/active_record/member_association_extensions.rb @@ -19,8 +19,7 @@ def find_memberships_for(member, membership_type) def find_for_destruction(membership_type, members) proxy_association.owner.group_memberships_as_group. - where(member: members). - as(membership_type) + for_members(members).as(membership_type) end end end From 1734420cbbf007aee91b6425f473113f1c5e56d1 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 5 Jul 2017 03:08:30 -0400 Subject: [PATCH 047/205] Use merge and helpers for query --- lib/groupify/adapter/active_record/group_member.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index fea4fd6..1481e32 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,13 +26,10 @@ module GroupMember def in_group?(group, opts={}) return false unless group.present? - criteria = {group: group} - - if opts[:as] - criteria.merge!(membership_type: opts[:as]) - end - - group_memberships_as_member.exists?(criteria) + + criteria = group_memberships_as_member.merge(group.group_memberships_as_group) + criteria = criteria.as(opts[:as]) if opts[:as] + criteria.exists? end def in_any_group?(*args) From 68e902873919181210b0be358d13bdf6e5d38431 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 5 Jul 2017 03:14:09 -0400 Subject: [PATCH 048/205] clearer logic --- lib/groupify/adapter/active_record/group_member.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 1481e32..eaf68c5 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,7 +26,7 @@ module GroupMember def in_group?(group, opts={}) return false unless group.present? - + criteria = group_memberships_as_member.merge(group.group_memberships_as_group) criteria = criteria.as(opts[:as]) if opts[:as] criteria.exists? @@ -34,12 +34,9 @@ def in_group?(group, opts={}) def in_any_group?(*args) opts = args.extract_options! - groups = args + groups = args.flatten - groups.flatten.each do |group| - return true if in_group?(group, opts) - end - return false + groups.any?{ |group| in_group?(group, opts) } end def in_all_groups?(*args) From 04c6cf32a2ee9d7be3c8526c197fcb8ddefdda92 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 14:15:14 -0400 Subject: [PATCH 049/205] Add ability to call `has_group :custom_groups, "CustomGroup"` --- lib/groupify/adapter/active_record/group_member.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index eaf68c5..4f228af 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -106,11 +106,15 @@ def shares_any_group(other) in_any_group(other.groups) end - def has_group(name, options = {}) + def has_group(name, source_type = nil, options = {}) + if source_type.is_a?(Hash) + options, source_type = source_type, nil + end + has_many name.to_sym, ->{ distinct }, { through: :group_memberships_as_member, source: :group, - source_type: @group_class_name, + source_type: source_type || @group_class_name, extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end From 7d321d3dfe36f9ca1dc08043e42fd7660bb3ae9b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 14:15:40 -0400 Subject: [PATCH 050/205] Clarify logic and simplify some steps --- .../adapter/active_record/group_membership.rb | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index c7e6e1e..e86e6a1 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -30,7 +30,7 @@ def as end module ClassMethods - def named(group_name=nil) + def named(group_name = nil) if group_name.present? where(group_name: group_name) else @@ -47,49 +47,47 @@ def for_groups(groups) end def not_for_groups(groups) - for_polymorphic(:group, groups, not: true) + where.not(build_polymorphic_criteria_for(:group, groups)) end def for_members(members) for_polymorphic(:member, members) end + def not_for_members(groups) + where.not(build_polymorphic_criteria_for(:member, members)) + end + def criteria_for_groups(groups) - criteria_for_polymorphic(:group, groups) + build_polymorphic_criteria_for(:group, groups) end def criteria_for_members(members) - criteria_for_polymorphic(:member, members) + build_polymorphic_criteria_for(:member, members) end - def for_polymorphic(name, records, options = {}) - if records.is_a?(Array) - if options[:not] - where.not(criteria_for_polymorphic(name, records)) - else - where(criteria_for_polymorphic(name, records)) - end - elsif records.is_a?(::ActiveRecord::Relation) + def for_polymorphic(source, records, options = {}) + case records + when Array + where(build_polymorphic_criteria_for(source, records)) + when ::ActiveRecord::Relation merge(records) - elsif records - merge(records.__send__(:"group_memberships_as_#{name}")) + when ::ActiveRecord::Base + merge(records.__send__(:"group_memberships_as_#{source}")) else self end end - def criteria_for_polymorphic(prefix, records) - records_by_base_class = records.group_by{ |record| record.class.base_class } - klass = respond_to?(:proxy_association) ? proxy_association.klass : self - - criteria_values = records_by_base_class.map do |base_class, records| - klass.arel_table.grouping([ - klass.arel_table[:"#{prefix}_type"].eq(base_class.name), - klass.arel_table[:"#{prefix}_id"].in(records.map(&:id)) - ].reduce(:and)) - end + # Build criteria to search on ID grouped by base class type. + # This is for polymorphic associations where the ID may be from + # different tables. + def build_polymorphic_criteria_for(source, records) + records_by_base_class = records.group_by{ |record| record.class.base_class.name } + table = respond_to?(:proxy_association) ? proxy_association.klass.arel_table : self.arel_table + id_column, type_column = table[:"#{source}_id"], table[:"#{source}_type"] - criteria_values.reduce(:or) + records_by_base_class.map{ |type, records| table.grouping(type_column.eq(type).and(id_column.in(records.map(&:id)))) }.reduce(:or) end end end From 31f2afe910c43338dd497e329328cff8fd27016d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 14:35:11 -0400 Subject: [PATCH 051/205] Pass each record as separate parameter --- lib/groupify/adapter/active_record/association_extensions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 60c1ced..e977547 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -27,11 +27,11 @@ def add_with_exception(*children) alias_method :add, :add_with_exception def delete(*records) - remove_children_from_parent(records.flatten, :delete){ |records| super(records) } + remove_children_from_parent(records.flatten, :delete){ |records| super(*records) } end def destroy(*records) - remove_children_from_parent(records.flatten, :destroy){ |records| super(records) } + remove_children_from_parent(records.flatten, :destroy){ |records| super(*records) } end protected From c6bd4cd092076b9e87be4900cd9ee0ad8a21bd0f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 15:22:40 -0400 Subject: [PATCH 052/205] Include type column when counting matches --- .../adapter/active_record/group_member.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 4f228af..8e84c31 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -81,10 +81,20 @@ def in_all_groups(*groups) groups = groups.flatten return none unless groups.present? + group_id_column = "#{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}" + group_type_column = "#{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_type')}" + # Count distinct on ID and type combo + concatenated_columns = case connection.adapter_name.downcase + when /sqlite/ + "#{group_id_column} || #{group_type_column}" + else #when /mysql/, /postgres/, /pg/ + "CONCAT(#{group_id_column}, #{group_type_column})" + end + joins(:group_memberships_as_member). group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). merge(Groupify.group_membership_klass.for_groups(groups)). - having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}) = ?", groups.count). + having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct end @@ -118,6 +128,11 @@ def has_group(name, source_type = nil, options = {}) extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end + + private + + def build_having_count_distinct_concatenated_criteria + end end end end From 0a05de719062ea232ff4f6cb84d6dc9ce961640c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 15:23:29 -0400 Subject: [PATCH 053/205] Removed unused method --- lib/groupify/adapter/active_record/group_member.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 8e84c31..363b712 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -128,11 +128,6 @@ def has_group(name, source_type = nil, options = {}) extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end - - private - - def build_having_count_distinct_concatenated_criteria - end end end end From c5db985a6a17668a28b72eabd1eccdd8d060c58b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 15:38:47 -0400 Subject: [PATCH 054/205] Added helper method to make it easier to read SQL criteria --- lib/groupify.rb | 4 ++++ lib/groupify/adapter/active_record/group_member.rb | 8 ++++---- lib/groupify/adapter/active_record/named_group_member.rb | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index c870314..04537d8 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -14,6 +14,10 @@ def self.configure def self.group_membership_klass group_membership_class_name.constantize end + + def self.quoted_column_name_for(model_class, column_name) + "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" + end end require 'groupify/railtie' if defined?(Rails) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 363b712..afeacb6 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -81,8 +81,8 @@ def in_all_groups(*groups) groups = groups.flatten return none unless groups.present? - group_id_column = "#{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_id')}" - group_type_column = "#{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_type')}" + group_id_column = Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_id') + group_type_column = Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_type') # Count distinct on ID and type combo concatenated_columns = case connection.adapter_name.downcase when /sqlite/ @@ -92,7 +92,7 @@ def in_all_groups(*groups) end joins(:group_memberships_as_member). - group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). + group(Groupify.quoted_column_name_for(self, 'id')). merge(Groupify.group_membership_klass.for_groups(groups)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct @@ -103,7 +103,7 @@ def in_only_groups(*groups) return none unless groups.present? in_all_groups(*groups). - where.not(id: in_other_groups(*groups).select("#{quoted_table_name}.#{connection.quote_column_name('id')}")). + where.not(id: in_other_groups(*groups).select(Groupify.quoted_column_name_for(self, 'id'))). distinct end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index b7d22f7..e19e480 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -84,9 +84,9 @@ def in_all_named_groups(*named_groups) return none unless named_groups.present? joins(:group_memberships_as_member). - group("#{quoted_table_name}.#{connection.quote_column_name('id')}"). + group(Groupify.quoted_column_name_for(self, 'id')). merge(Groupify.group_membership_klass.where(group_name: named_groups)). - having("COUNT(DISTINCT #{Groupify.group_membership_klass.quoted_table_name}.#{connection.quote_column_name('group_name')}) = ?", named_groups.count). + having("COUNT(DISTINCT #{Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). distinct end @@ -95,7 +95,7 @@ def in_only_named_groups(*named_groups) return none unless named_groups.present? in_all_named_groups(*named_groups). - where.not(id: in_other_named_groups(*named_groups).select("#{quoted_table_name}.#{connection.quote_column_name('id')}")). + where.not(id: in_other_named_groups(*named_groups).select(Groupify.quoted_column_name_for(self, 'id'))). distinct end From 77c438e961667632faeb85e35e3ea77eca9ff008 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 15:46:15 -0400 Subject: [PATCH 055/205] DRY logic --- .../active_record/named_group_collection.rb | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index 05ff688..323169b 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -11,21 +11,16 @@ def initialize(member) def add(named_group, opts={}) named_group = named_group.to_sym - membership_type = opts[:as] - - if @member.new_record? - @member.group_memberships_as_member.build(group_name: named_group, membership_type: nil) - else - @member.transaction do - @member.group_memberships_as_member.where(group_name: named_group, membership_type: nil).first_or_create! - end - end - - if membership_type - if @member.new_record? - @member.group_memberships_as_member.build(group_name: named_group, membership_type: membership_type) - else - @member.group_memberships_as_member.where(group_name: named_group, membership_type: membership_type).first_or_create! + # always add a nil membership type and then a specific one (if specified) + membership_types = [nil, opts[:as]].uniq + + @member.transaction do + membership_types.each do |membership_type| + if @member.new_record? + @member.group_memberships_as_member.build(group_name: named_group, membership_type: membership_type) + else + @member.group_memberships_as_member.where(group_name: named_group, membership_type: membership_type).first_or_create! + end end end From efe9df9ae4a97f9d3c7ac6d7561fc6f3f695d2f4 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 15:51:02 -0400 Subject: [PATCH 056/205] Simplify logic when finding matching named group --- lib/groupify/adapter/active_record/named_group_member.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index e19e480..c5a0edf 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -39,10 +39,7 @@ def in_named_group?(named_group, opts={}) def in_any_named_group?(*args) opts = args.extract_options! named_groups = args.flatten - named_groups.each do |named_group| - return true if in_named_group?(named_group, opts) - end - return false + named_groups.any?{ |named_group| in_named_group?(named_group, opts) } end def in_all_named_groups?(*args) From b990b0f13f1d48da3409c68e9b00d8845f7fe2bf Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 17:12:25 -0400 Subject: [PATCH 057/205] Simplify variable use --- lib/groupify/adapter/active_record/group.rb | 13 +++++----- .../adapter/active_record/group_member.rb | 21 +++++++-------- .../active_record/named_group_collection.rb | 26 +++++++++---------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 5aa8bbe..805bee8 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -100,7 +100,7 @@ def merge!(source_group, destination_group) end end - protected + protected def register(member_klass, association_name = nil) (@member_klasses ||= Set.new) << member_klass @@ -123,11 +123,12 @@ def define_member_association(member_klass, association_name = nil) source_type = member_klass.base_class has_many association_name, - ->{ distinct }, - through: :group_memberships_as_group, - source: :member, - source_type: source_type.to_s, - extend: Groupify::ActiveRecord::MemberAssociationExtensions + ->{ distinct }, + through: :group_memberships_as_group, + source: :member, + source_type: source_type.to_s, + extend: Groupify::ActiveRecord::MemberAssociationExtensions + end end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index afeacb6..6ba8924 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -32,25 +32,22 @@ def in_group?(group, opts={}) criteria.exists? end - def in_any_group?(*args) - opts = args.extract_options! - groups = args.flatten + def in_any_group?(*groups) + opts = groups.extract_options! - groups.any?{ |group| in_group?(group, opts) } + groups.flatten.any?{ |group| in_group?(group, opts) } end - def in_all_groups?(*args) - opts = args.extract_options! - groups = args.flatten + def in_all_groups?(*groups) + opts = groups.extract_options! - groups.to_set.subset? self.groups.as(opts[:as]).to_set + groups.flatten.to_set.subset? self.groups.as(opts[:as]).to_set end - def in_only_groups?(*args) - opts = args.extract_options! - groups = args.flatten + def in_only_groups?(*groups) + opts = groups.extract_options! - groups.to_set == self.groups.as(opts[:as]).to_set + groups.flatten.to_set == self.groups.as(opts[:as]).to_set end def shares_any_group?(other, opts={}) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index 323169b..c8e8cd7 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -6,6 +6,7 @@ def initialize(member) @member = member @named_group_memberships = member.group_memberships_as_member.named @group_names = @named_group_memberships.pluck(:group_name).map(&:to_sym) + super(@group_names) end @@ -30,10 +31,10 @@ def add(named_group, opts={}) alias_method :push, :add alias_method :<<, :add - def merge(*args) - opts = args.extract_options! - named_groups = args.flatten - named_groups.each do |named_group| + def merge(*named_groups) + opts = named_groups.extract_options! + + named_groups.flatten.each do |named_group| add(named_group, opts) end end @@ -42,6 +43,7 @@ def merge(*args) def include?(named_group, opts={}) named_group = named_group.to_sym + if opts[:as] as(opts[:as]).include?(named_group) else @@ -49,18 +51,16 @@ def include?(named_group, opts={}) end end - def delete(*args) - opts = args.extract_options! - named_groups = args.flatten.compact + def delete(*named_groups) + opts = named_groups.extract_options! - remove(named_groups, :delete_all, opts) + remove(named_groups.flatten.compact, :delete_all, opts) end - def destroy(*args) - opts = args.extract_options! - named_groups = args.flatten.compact + def destroy(*named_groups) + opts = named_groups.extract_options! - remove(named_groups, :destroy_all, opts) + remove(named_groups.flatten.compact, :destroy_all, opts) end def clear @@ -76,7 +76,7 @@ def as(membership_type) @named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym) end - protected + protected def remove(named_groups, method, opts) if named_groups.present? From d5af10bb743c56e96eeffa43f577130e38e3f910 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 17:27:54 -0400 Subject: [PATCH 058/205] Simplify membership query building --- lib/groupify/adapter/active_record/group.rb | 11 +++++++--- .../adapter/active_record/group_member.rb | 21 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 805bee8..8ce9ed5 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -45,8 +45,7 @@ def merge!(source) module ClassMethods def with_member(member) - joins(:group_memberships_as_group). - merge(member.group_memberships_as_member). + memberships_merge(member.group_memberships_as_member). extending(Groupify::ActiveRecord::GroupAssociationExtensions) end @@ -89,7 +88,7 @@ def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = (source_group.member_classes - destination_group.member_classes) invalid_member_classes.each do |klass| - if klass.joins(:group_memberships_as_member).merge(source_group.group_memberships_as_group).count > 0 + if klass.memberships_merge(source_group.group_memberships_as_group).count > 0 raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") end end @@ -129,6 +128,12 @@ def define_member_association(member_klass, association_name = nil) source_type: source_type.to_s, extend: Groupify::ActiveRecord::MemberAssociationExtensions end + + def memberships_merge(merge_criteria, &group_membership_filter) + query = joins(:group_memberships_as_group) + query = query.merge(merge_criteria) if merge_criteria + query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? + query end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 6ba8924..2fcc4e3 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -56,22 +56,20 @@ def shares_any_group?(other, opts={}) module ClassMethods def as(membership_type) - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.as(membership_type)) + memberships_merge{as(membership_type)} end def in_group(group) return none unless group.present? - joins(:group_memberships_as_member).merge(group.group_memberships_as_group).distinct + memberships_merge(group.group_memberships_as_group).distinct end def in_any_group(*groups) groups = groups.flatten return none unless groups.present? - joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.for_groups(groups)). - distinct + memberships_merge{for_groups(groups)}.distinct end def in_all_groups(*groups) @@ -88,9 +86,8 @@ def in_all_groups(*groups) "CONCAT(#{group_id_column}, #{group_type_column})" end - joins(:group_memberships_as_member). + memberships_merge{for_groups(groups)}. group(Groupify.quoted_column_name_for(self, 'id')). - merge(Groupify.group_membership_klass.for_groups(groups)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct end @@ -105,8 +102,7 @@ def in_only_groups(*groups) end def in_other_groups(*groups) - joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.not_for_groups(groups)) + memberships_merge{not_for_groups(groups)} end def shares_any_group(other) @@ -125,6 +121,13 @@ def has_group(name, source_type = nil, options = {}) extend: Groupify::ActiveRecord::GroupAssociationExtensions }.merge(options.slice :class_name) end + + def memberships_merge(merge_criteria = nil, &group_membership_filter) + query = joins(:group_memberships_as_member) + query = query.merge(merge_criteria) if merge_criteria + query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? + query + end end end end From 98fb5a97c7b85f32ee83999d3f1277a96d978727 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 17:35:38 -0400 Subject: [PATCH 059/205] Code spacing --- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- .../adapter/active_record/named_group_collection.rb | 4 ++-- lib/groupify/adapter/active_record/named_group_member.rb | 4 ++-- lib/groupify/adapter/mongoid/group_member.rb | 4 ++-- lib/groupify/adapter/mongoid/named_group_collection.rb | 2 +- lib/groupify/adapter/mongoid/named_group_member.rb | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 2fcc4e3..871a051 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -24,7 +24,7 @@ module GroupMember has_group :groups end - def in_group?(group, opts={}) + def in_group?(group, opts = {}) return false unless group.present? criteria = group_memberships_as_member.merge(group.group_memberships_as_group) @@ -50,7 +50,7 @@ def in_only_groups?(*groups) groups.flatten.to_set == self.groups.as(opts[:as]).to_set end - def shares_any_group?(other, opts={}) + def shares_any_group?(other, opts = {}) in_any_group?(other.groups, opts) end diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index c8e8cd7..1af683c 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -10,7 +10,7 @@ def initialize(member) super(@group_names) end - def add(named_group, opts={}) + def add(named_group, opts = {}) named_group = named_group.to_sym # always add a nil membership type and then a specific one (if specified) membership_types = [nil, opts[:as]].uniq @@ -41,7 +41,7 @@ def merge(*named_groups) alias_method :concat, :merge - def include?(named_group, opts={}) + def include?(named_group, opts = {}) named_group = named_group.to_sym if opts[:as] diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index c5a0edf..fcd4019 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -32,7 +32,7 @@ def named_groups=(named_groups) end end - def in_named_group?(named_group, opts={}) + def in_named_group?(named_group, opts = {}) named_groups.include?(named_group, opts) end @@ -54,7 +54,7 @@ def in_only_named_groups?(*args) named_groups == self.named_groups.as(opts[:as]).to_set end - def shares_any_named_group?(other, opts={}) + def shares_any_named_group?(other, opts = {}) in_any_named_group?(other.named_groups.to_a, opts) end diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index d65098c..d42ff23 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -70,7 +70,7 @@ def as(membership_type) end end - def in_group?(group, opts={}) + def in_group?(group, opts = {}) return false unless group.present? groups.as(opts[:as]).include?(group) end @@ -99,7 +99,7 @@ def in_only_groups?(*args) groups.to_set == self.groups.as(opts[:as]).to_set end - def shares_any_group?(other, opts={}) + def shares_any_group?(other, opts = {}) in_any_group?(other.groups.to_a, opts) end diff --git a/lib/groupify/adapter/mongoid/named_group_collection.rb b/lib/groupify/adapter/mongoid/named_group_collection.rb index b0bb5fe..497e7b4 100644 --- a/lib/groupify/adapter/mongoid/named_group_collection.rb +++ b/lib/groupify/adapter/mongoid/named_group_collection.rb @@ -14,7 +14,7 @@ def as(membership_type) end end - def <<(named_group, opts={}) + def <<(named_group, opts = {}) named_group = named_group.to_sym super(named_group) uniq! diff --git a/lib/groupify/adapter/mongoid/named_group_member.rb b/lib/groupify/adapter/mongoid/named_group_member.rb index 615f2d5..4453022 100644 --- a/lib/groupify/adapter/mongoid/named_group_member.rb +++ b/lib/groupify/adapter/mongoid/named_group_member.rb @@ -24,7 +24,7 @@ module NamedGroupMember end end - def in_named_group?(named_group, opts={}) + def in_named_group?(named_group, opts = {}) named_groups.as(opts[:as]).include?(named_group) end @@ -52,12 +52,12 @@ def in_only_named_groups?(*args) named_groups == self.named_groups.as(opts[:as]).to_set end - def shares_any_named_group?(other, opts={}) + def shares_any_named_group?(other, opts = {}) in_any_named_group?(other.named_groups, opts) end module ClassMethods - def in_named_group(named_group, opts={}) + def in_named_group(named_group, opts = {}) in_any_named_group(named_group, opts) end @@ -82,7 +82,7 @@ def in_only_named_groups(*named_groups) where(named_groups: named_groups.flatten) end - def shares_any_named_group(other, opts={}) + def shares_any_named_group(other, opts = {}) in_any_named_group(other.named_groups, opts) end end From 3440327232c5de6f072142647333cb34c519668b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 8 Jul 2017 20:30:38 -0400 Subject: [PATCH 060/205] Refer to `arel_table` consistently --- lib/groupify/adapter/active_record/group_membership.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index e86e6a1..9c13cf1 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -84,10 +84,9 @@ def for_polymorphic(source, records, options = {}) # different tables. def build_polymorphic_criteria_for(source, records) records_by_base_class = records.group_by{ |record| record.class.base_class.name } - table = respond_to?(:proxy_association) ? proxy_association.klass.arel_table : self.arel_table - id_column, type_column = table[:"#{source}_id"], table[:"#{source}_type"] + id_column, type_column = arel_table[:"#{source}_id"], arel_table[:"#{source}_type"] - records_by_base_class.map{ |type, records| table.grouping(type_column.eq(type).and(id_column.in(records.map(&:id)))) }.reduce(:or) + records_by_base_class.map{ |type, records| arel_table.grouping(type_column.eq(type).and(id_column.in(records.map(&:id)))) }.reduce(:or) end end end From 6c5c2262d3b81c6d66beb7ed14f45c7a414b1f8d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 1 Aug 2017 11:55:22 -0400 Subject: [PATCH 061/205] Removed unused methods --- lib/groupify/adapter/active_record/group_membership.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 9c13cf1..da906e0 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -58,14 +58,6 @@ def not_for_members(groups) where.not(build_polymorphic_criteria_for(:member, members)) end - def criteria_for_groups(groups) - build_polymorphic_criteria_for(:group, groups) - end - - def criteria_for_members(members) - build_polymorphic_criteria_for(:member, members) - end - def for_polymorphic(source, records, options = {}) case records when Array From 3b0f9a34e2c5baa164bf3f9561fb3acdb8c99f5d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 1 Aug 2017 11:55:47 -0400 Subject: [PATCH 062/205] Formatted for clarity --- .../adapter/active_record/group_membership.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index da906e0..d2a8d91 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -75,10 +75,21 @@ def for_polymorphic(source, records, options = {}) # This is for polymorphic associations where the ID may be from # different tables. def build_polymorphic_criteria_for(source, records) - records_by_base_class = records.group_by{ |record| record.class.base_class.name } + records_by_base_class = records.group_by{ |record| record.class.base_class.name } id_column, type_column = arel_table[:"#{source}_id"], arel_table[:"#{source}_type"] - records_by_base_class.map{ |type, records| arel_table.grouping(type_column.eq(type).and(id_column.in(records.map(&:id)))) }.reduce(:or) + criteria = records_by_base_class.map do |type, grouped_records| + arel_table.grouping( + type_column.eq(type). + and( + id_column.in(grouped_records.map(&:id)) + ) + ) + end + + # Generates something like: + # (group_type = `Group` AND group_id IN (?)) OR (group_type = `Team` AND group_id IN(?)) + criteria.reduce(:or) end end end From 922c102f0cf33cc3ffba92ac268fbcdb1126bbbc Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 2 Aug 2017 19:49:24 -0400 Subject: [PATCH 063/205] Consolidate association extensions and move helpers to `ActiveRecord` module --- lib/groupify.rb | 4 - lib/groupify/adapter/active_record.rb | 82 ++++++++++++++++ .../active_record/association_extensions.rb | 93 ++++++------------- lib/groupify/adapter/active_record/group.rb | 10 +- .../group_association_extensions.rb | 26 ------ .../adapter/active_record/group_member.rb | 16 ++-- .../member_association_extensions.rb | 26 ------ 7 files changed, 124 insertions(+), 133 deletions(-) delete mode 100644 lib/groupify/adapter/active_record/group_association_extensions.rb delete mode 100644 lib/groupify/adapter/active_record/member_association_extensions.rb diff --git a/lib/groupify.rb b/lib/groupify.rb index 04537d8..c870314 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -14,10 +14,6 @@ def self.configure def self.group_membership_klass group_membership_class_name.constantize end - - def self.quoted_column_name_for(model_class, column_name) - "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" - end end require 'groupify/railtie' if defined?(Rails) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index e8d9da1..5ed4188 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -10,5 +10,87 @@ module ActiveRecord autoload :GroupMembership, 'groupify/adapter/active_record/group_membership' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' + + def self.quote(model_class, column_name) + "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" + end + + def self.find_memberships_for(parent, children, membership_type = nil) + parent_type, child_type = detect_types_from_parent(parent) + + query = parent.__send__(:"group_memberships_as_#{parent_type}").__send__(:"for_#{child_type}s", children) + query = query.as(membership_type) if membership_type + query + end + + def self.add_children_to_parent(parent, children, options = {}) + parent_type, child_type = detect_types_from_parent(parent) + + membership_type = options[:as] + exception_on_invalidation = options[:exception_on_invalidation] + + return parent if children.none? + + parent.__send__(:clear_association_cache) + + memberships_association = parent.__send__(:"group_memberships_as_#{parent_type}") + + to_add_directly = [] + to_add_with_membership_type = [] + + already_children = find_memberships_for(parent, children).includes(child_type).map(&child_type).uniq + children -= already_children + + # first prepare changes + children.each do |child| + # add to collection without membership type + to_add_directly << memberships_association.build(child_type => child) + # add a second entry for the given membership type + if membership_type + membership = memberships_association. + merge(child.__send__(:"group_memberships_as_#{child_type}")). + as(membership_type). + first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + parent.__send__(:clear_association_cache) + end + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |child| + next if child.valid? + + if exception_on_invalidation + raise ::ActiveRecord::RecordInvalid.new(child) + else + return false + end + end + + # create memberships without membership type + memberships_association << to_add_directly + + # create memberships with membership type + to_add_with_membership_type. + group_by{ |membership| membership.__send__(parent_type) }. + each do |membership_parent, memberships| + membership_parent.__send__(:"group_memberships_as_#{parent_type}") << memberships + membership_parent.__send__(:clear_association_cache) + end + + parent + end + + protected + + def self.detect_types_from_parent(parent) + if parent.is_a?(Groupify::ActiveRecord::Group) + [:group, :member] + else + [:member, :group] + end + end end end diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index e977547..2257e01 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -4,8 +4,19 @@ module AssociationExtensions extend ActiveSupport::Concern def as(membership_type) - return self unless membership_type - merge(Groupify.group_membership_klass.as(membership_type)) + if membership_type + merge(Groupify.group_membership_klass.as(membership_type)) + else + self + end + end + + def delete(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + def destroy(*records) + remove_children(records, :destroy, records.extract_options![:as]) end # Defined to create alias methods before @@ -15,83 +26,35 @@ def <<(*) end def add_without_exception(*children) - add_children_to_parent(children.flatten, false) + add_children(children, children.extract_options!.merge(exception_on_invalidation: false)) end def add_with_exception(*children) - add_children_to_parent(children.flatten, true) + add_children(children, children.extract_options!.merge(exception_on_invalidation: true)) end alias_method :add_as_usual, :<< alias_method :<<, :add_without_exception alias_method :add, :add_with_exception - def delete(*records) - remove_children_from_parent(records.flatten, :delete){ |records| super(*records) } - end - - def destroy(*records) - remove_children_from_parent(records.flatten, :destroy){ |records| super(*records) } - end - protected - def remove_children_from_parent(records, destruction_type, &fallback) - membership_type = records.extract_options![:as] - - if membership_type - find_for_destruction(membership_type, records).__send__(:"#{destruction_type}_all") - else - fallback.call(records) - end - - records.each{|record| record.__send__(:clear_association_cache)} + def add_children(children, options = {}) + ActiveRecord.add_children_to_parent( + proxy_association.owner, + children, + options + ) end - def add_children_to_parent(children, exception_on_invalidation) - membership_type = children.extract_options![:as] + def remove_children(children, destruction_type, membership_type = nil) + ActiveRecord.find_memberships_for( + proxy_association.owner, + children, + membership_type + ).__send__(:"#{destruction_type}_all") - return self if children.none? - - parent = proxy_association.owner - parent.__send__(:clear_association_cache) - - to_add_directly = [] - to_add_with_membership_type = [] - - # first prepare changes - children.each do |child| - # add to collection without membership type - to_add_directly << child unless self.include?(child) - # add a second entry for the given membership type - if membership_type - membership = find_memberships_for(child, membership_type).first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - parent.__send__(:clear_association_cache) - end - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |child| - next if child.valid? - - if exception_on_invalidation - raise RecordInvalid.new(child) - else - return false - end - end - - # then persist changes - add_as_usual(to_add_directly) - - to_add_with_membership_type.each do |membership| - membership_parent = membership.__send__(association_parent_type) - membership_parent.__send__(:"group_memberships_as_#{association_parent_type}") << membership - membership_parent.__send__(:clear_association_cache) - end + children.each{|record| record.__send__(:clear_association_cache)} self end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 8ce9ed5..684a2c9 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -1,4 +1,4 @@ -require 'groupify/adapter/active_record/member_association_extensions' +require 'groupify/adapter/active_record/association_extensions' module Groupify module ActiveRecord @@ -46,7 +46,7 @@ def merge!(source) module ClassMethods def with_member(member) memberships_merge(member.group_memberships_as_member). - extending(Groupify::ActiveRecord::GroupAssociationExtensions) + extending(Groupify::ActiveRecord::AssociationExtensions) end def default_member_class @@ -119,14 +119,14 @@ def associate_member_class(member_klass, association_name = nil) def define_member_association(member_klass, association_name = nil) association_name ||= member_klass.model_name.plural.to_sym - source_type = member_klass.base_class + source_type = member_klass.base_class.to_s has_many association_name, ->{ distinct }, through: :group_memberships_as_group, source: :member, - source_type: source_type.to_s, - extend: Groupify::ActiveRecord::MemberAssociationExtensions + source_type: source_type, + extend: Groupify::ActiveRecord::AssociationExtensions end def memberships_merge(merge_criteria, &group_membership_filter) diff --git a/lib/groupify/adapter/active_record/group_association_extensions.rb b/lib/groupify/adapter/active_record/group_association_extensions.rb deleted file mode 100644 index e33b4ce..0000000 --- a/lib/groupify/adapter/active_record/group_association_extensions.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'groupify/adapter/active_record/association_extensions' - -module Groupify - module ActiveRecord - module GroupAssociationExtensions - include AssociationExtensions - - protected - - def association_parent_type - :member - end - - def find_memberships_for(group, membership_type) - proxy_association.owner.group_memberships_as_member. - merge(group.group_memberships_as_group). - as(membership_type) - end - - def find_for_destruction(membership_type, groups) - proxy_association.owner.group_memberships_as_member. - for_groups(groups).as(membership_type) - end - end - end -end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 871a051..5918b38 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -1,4 +1,4 @@ -require 'groupify/adapter/active_record/group_association_extensions' +require 'groupify/adapter/active_record/association_extensions' module Groupify module ActiveRecord @@ -76,8 +76,8 @@ def in_all_groups(*groups) groups = groups.flatten return none unless groups.present? - group_id_column = Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_id') - group_type_column = Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_type') + group_id_column = ActiveRecord.quote(Groupify.group_membership_klass, 'group_id') + group_type_column = ActiveRecord.quote(Groupify.group_membership_klass, 'group_type') # Count distinct on ID and type combo concatenated_columns = case connection.adapter_name.downcase when /sqlite/ @@ -87,7 +87,7 @@ def in_all_groups(*groups) end memberships_merge{for_groups(groups)}. - group(Groupify.quoted_column_name_for(self, 'id')). + group(ActiveRecord.quote(self, 'id')). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct end @@ -97,7 +97,7 @@ def in_only_groups(*groups) return none unless groups.present? in_all_groups(*groups). - where.not(id: in_other_groups(*groups).select(Groupify.quoted_column_name_for(self, 'id'))). + where.not(id: in_other_groups(*groups).select(ActiveRecord.quote(self, 'id'))). distinct end @@ -114,11 +114,13 @@ def has_group(name, source_type = nil, options = {}) options, source_type = source_type, nil end + source_type ||= @group_class_name + has_many name.to_sym, ->{ distinct }, { through: :group_memberships_as_member, source: :group, - source_type: source_type || @group_class_name, - extend: Groupify::ActiveRecord::GroupAssociationExtensions + source_type: source_type, + extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options.slice :class_name) end diff --git a/lib/groupify/adapter/active_record/member_association_extensions.rb b/lib/groupify/adapter/active_record/member_association_extensions.rb deleted file mode 100644 index 6d08063..0000000 --- a/lib/groupify/adapter/active_record/member_association_extensions.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'groupify/adapter/active_record/association_extensions' - -module Groupify - module ActiveRecord - module MemberAssociationExtensions - include AssociationExtensions - - protected - - def association_parent_type - :group - end - - def find_memberships_for(member, membership_type) - proxy_association.owner.group_memberships_as_group. - merge(member.group_memberships_as_member). - as(membership_type) - end - - def find_for_destruction(membership_type, members) - proxy_association.owner.group_memberships_as_group. - for_members(members).as(membership_type) - end - end - end -end From 9ed500773e312a7f0f377da15b3a9546b5b49c1c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 2 Aug 2017 19:50:40 -0400 Subject: [PATCH 064/205] Fix polymorphic adding of members to groups by bypassing association reference --- lib/groupify/adapter/active_record/group.rb | 4 +- spec/active_record_spec.rb | 55 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 684a2c9..9378238 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -31,9 +31,7 @@ def member_classes def add(*members) opts = members.extract_options! - members.flatten.each do |member| - member.groups.add(self, opts) - end + ActiveRecord.add_children_to_parent(self, members.flatten, opts) self end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 26443d2..dc103f3 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -28,6 +28,7 @@ class User < ActiveRecord::Base groupify :named_group_member has_group :organizations, class_name: "Organization" + has_group :classrooms, class_name: "Classroom" end class Manager < User @@ -78,6 +79,60 @@ class Classroom < ActiveRecord::Base it { should respond_to :shares_any_group?} end +describe Groupify::ActiveRecord do + let(:user) { User.create! } + let(:group) { Group.create! } + let(:classroom) { Classroom.create! } + let(:organization) { Organization.create! } + + describe "polymorphic groups" do + context "memberships" do + # before do + # Groupify.configure do |config| + # config.group_class_name = 'CustomGroup' + # config.group_membership_class_name = 'CustomGroupMembership' + # end + # + # class CustomGroupMembership < ActiveRecord::Base + # groupify :group_membership + # end + # + # class CustomUser < ActiveRecord::Base + # groupify :group_member + # groupify :named_group_member + # end + # + # class CustomGroup < ActiveRecord::Base + # groupify :group, members: [:custom_users] + # end + # end + # + # after do + # Groupify.configure do |config| + # config.group_class_name = 'Group' + # config.group_membership_class_name = 'GroupMembership' + # end + # end + + it "finds multiple records for different models with same ID" do + group.add user + classroom.add user + organization.add user + + expect(group.id).to eq(1) + expect(classroom.id).to eq(1) + expect(organization.id).to eq(2) + expect(user.group_memberships_as_member.map(&:group)).to eq([group, classroom, organization]) + expect(GroupMembership.for_groups([group, classroom]).count).to eq(2) + expect(GroupMembership.for_groups([group, classroom, organization]).count).to eq(3) + expect(GroupMembership.for_groups([group, classroom]).distinct.count).to eq(2) + expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.size).to eq(1) + expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.first).to eq(user) + end + end + end +end + describe Groupify::ActiveRecord do let(:user) { User.create! } let(:group) { Group.create! } From 1f82fdab6d2a59669a500d1f2ee366db119da2a9 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 2 Aug 2017 19:51:33 -0400 Subject: [PATCH 065/205] Added `memberships_merge` helper to named groups --- .../active_record/named_group_member.rb | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index fcd4019..577fb2e 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -60,30 +60,29 @@ def shares_any_named_group?(other, opts = {}) module ClassMethods def as(membership_type) - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.as(membership_type)) + memberships_merge(Groupify.group_membership_klass.as(membership_type)) end def in_named_group(named_group) return none unless named_group.present? - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_name: named_group)).distinct + memberships_merge{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - joins(:group_memberships_as_member).merge(Groupify.group_membership_klass.where(group_name: named_groups.flatten)).distinct + memberships_merge{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - joins(:group_memberships_as_member). - group(Groupify.quoted_column_name_for(self, 'id')). - merge(Groupify.group_membership_klass.where(group_name: named_groups)). - having("COUNT(DISTINCT #{Groupify.quoted_column_name_for(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). + memberships_merge{where(group_name: named_groups)}. + group(ActiveRecord.quote(self, 'id')). + having("COUNT(DISTINCT #{ActiveRecord.quote(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). distinct end @@ -92,18 +91,24 @@ def in_only_named_groups(*named_groups) return none unless named_groups.present? in_all_named_groups(*named_groups). - where.not(id: in_other_named_groups(*named_groups).select(Groupify.quoted_column_name_for(self, 'id'))). + where.not(id: in_other_named_groups(*named_groups).select(ActiveRecord.quote(self, 'id'))). distinct end def in_other_named_groups(*named_groups) - joins(:group_memberships_as_member). - merge(Groupify.group_membership_klass.where.not(group_name: named_groups)) + memberships_merge{where.not(group_name: named_groups)} end def shares_any_named_group(other) in_any_named_group(other.named_groups.to_a) end + + def memberships_merge(merge_criteria = nil, &group_membership_filter) + query = joins(:group_memberships_as_member) + query = query.merge(merge_criteria) if merge_criteria + query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? + query + end end end end From f9d3216e37930dd30163b80b59bbb8cddfea6a49 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:02:21 -0400 Subject: [PATCH 066/205] Add helper method to infer class and association names --- lib/groupify.rb | 14 ++++++++++++++ lib/groupify/adapter/active_record/group.rb | 19 +++++++------------ .../adapter/active_record/group_member.rb | 6 ++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index c870314..a7875bb 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -14,6 +14,20 @@ def self.configure def self.group_membership_klass group_membership_class_name.constantize end + + def self.infer_class_and_association_name(name) + klass = name.to_s.classify.constantize rescue nil + + association_name = if name.is_a?(Symbol) + name + elsif klass + klass.model_name.plural.to_sym + else + name.plural.to_sym + end + + [klass, association_name] + end end require 'groupify/railtie' if defined?(Rails) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 9378238..d0e5c96 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -62,8 +62,8 @@ def member_classes # Define which classes are members of this group def has_members(*names) - Array.wrap(names.flatten).each do |name| - has_member name + names.flatten.each do |name| + has_member(name) end end @@ -71,14 +71,13 @@ def has_member(name, options = {}) klass_name = options[:class_name] if klass_name.nil? - klass = name.to_s.classify.constantize - association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + klass, association_name = Groupify.infer_class_and_association_name(name) else klass = klass_name.to_s.classify.constantize association_name = name.to_sym end - register(klass, association_name) + associate_member_class(klass, association_name) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -99,20 +98,16 @@ def merge!(source_group, destination_group) protected - def register(member_klass, association_name = nil) + def associate_member_class(member_klass, association_name = nil) (@member_klasses ||= Set.new) << member_klass - associate_member_class(member_klass, association_name) - - member_klass - end - - def associate_member_class(member_klass, association_name = nil) define_member_association(member_klass, association_name) if member_klass == default_member_class define_member_association(member_klass, :members) end + + member_klass end def define_member_association(member_klass, association_name = nil) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 5918b38..908eb08 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -109,6 +109,12 @@ def shares_any_group(other) in_any_group(other.groups) end + def has_groups(*names) + names.flatten.each do |name| + has_group(name) + end + end + def has_group(name, source_type = nil, options = {}) if source_type.is_a?(Hash) options, source_type = source_type, nil From 00fe96b6545cbd8f8d9b0cc5c9c59803e0918172 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:13:36 -0400 Subject: [PATCH 067/205] Clean up `membership_type` usage and check presence better/minimally --- lib/groupify/adapter/active_record.rb | 2 +- .../active_record/association_extensions.rb | 6 +-- .../adapter/active_record/group_member.rb | 7 ++-- .../adapter/active_record/group_membership.rb | 6 ++- .../active_record/named_group_collection.rb | 29 ++++++++------- .../active_record/named_group_member.rb | 37 +++++++++---------- lib/groupify/adapter/mongoid/group.rb | 2 +- lib/groupify/adapter/mongoid/group_member.rb | 4 +- .../adapter/mongoid/named_group_collection.rb | 4 +- 9 files changed, 49 insertions(+), 48 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 5ed4188..0d104b9 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -46,7 +46,7 @@ def self.add_children_to_parent(parent, children, options = {}) # add to collection without membership type to_add_directly << memberships_association.build(child_type => child) # add a second entry for the given membership type - if membership_type + if membership_type.present? membership = memberships_association. merge(child.__send__(:"group_memberships_as_#{child_type}")). as(membership_type). diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 2257e01..bc418e1 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -4,11 +4,7 @@ module AssociationExtensions extend ActiveSupport::Concern def as(membership_type) - if membership_type - merge(Groupify.group_membership_klass.as(membership_type)) - else - self - end + merge(Groupify.group_membership_klass.as(membership_type)) end def delete(*records) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 908eb08..968c963 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -27,9 +27,10 @@ module GroupMember def in_group?(group, opts = {}) return false unless group.present? - criteria = group_memberships_as_member.merge(group.group_memberships_as_group) - criteria = criteria.as(opts[:as]) if opts[:as] - criteria.exists? + group_memberships_as_member. + merge(group.group_memberships_as_group). + as(opts[:as]). + exists? end def in_any_group?(*groups) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index d2a8d91..a63f2ab 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -39,7 +39,11 @@ def named(group_name = nil) end def as(membership_type) - where(membership_type: membership_type) + if membership_type.present? + where(membership_type: membership_type.to_s) + else + all + end end def for_groups(groups) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index 1af683c..c5d713c 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -12,8 +12,10 @@ def initialize(member) def add(named_group, opts = {}) named_group = named_group.to_sym + membership_type = opts[:as] + membership_type = membership_type.to_s if membership_type.is_a?(Symbol) # always add a nil membership type and then a specific one (if specified) - membership_types = [nil, opts[:as]].uniq + membership_types = [nil, membership_type].uniq @member.transaction do membership_types.each do |membership_type| @@ -54,13 +56,13 @@ def include?(named_group, opts = {}) def delete(*named_groups) opts = named_groups.extract_options! - remove(named_groups.flatten.compact, :delete_all, opts) + remove(named_groups.flatten.compact, :delete_all, opts[:as]) end def destroy(*named_groups) opts = named_groups.extract_options! - remove(named_groups.flatten.compact, :destroy_all, opts) + remove(named_groups.flatten.compact, :destroy_all, opts[:as]) end def clear @@ -73,22 +75,23 @@ def clear # Criteria to filter by membership type def as(membership_type) - @named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym) + if membership_type.present? + @named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym) + else + to_a + end end protected - def remove(named_groups, method, opts) + def remove(named_groups, destruction_type, membership_type = nil) if named_groups.present? - scope = @named_group_memberships.where(group_name: named_groups) - - if opts[:as] - scope = scope.where(membership_type: opts[:as]) - end - - scope.send(method) + @named_group_memberships. + where(group_name: named_groups). + as(membership_type). + __send__(destruction_type) - unless opts[:as] + unless membership_type.present? named_groups.each do |named_group| @hash.delete(named_group) end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 577fb2e..952b425 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -15,10 +15,10 @@ module NamedGroupMember included do unless respond_to?(:group_memberships_as_member) has_many :group_memberships_as_member, - as: :member, - autosave: true, - dependent: :destroy, - class_name: Groupify.group_membership_class_name + as: :member, + autosave: true, + dependent: :destroy, + class_name: Groupify.group_membership_class_name end end @@ -36,22 +36,19 @@ def in_named_group?(named_group, opts = {}) named_groups.include?(named_group, opts) end - def in_any_named_group?(*args) - opts = args.extract_options! - named_groups = args.flatten - named_groups.any?{ |named_group| in_named_group?(named_group, opts) } + def in_any_named_group?(*named_groups) + opts = named_groups.extract_options! + named_groups.flatten.any?{ |named_group| in_named_group?(named_group, opts) } end - def in_all_named_groups?(*args) - opts = args.extract_options! - named_groups = args.flatten.to_set - named_groups.subset? self.named_groups.as(opts[:as]).to_set + def in_all_named_groups?(*named_groups) + membership_type = named_groups.extract_options![:as] + named_groups.flatten.to_set.subset? self.named_groups.as(membership_type).to_set end - def in_only_named_groups?(*args) - opts = args.extract_options! - named_groups = args.flatten.to_set - named_groups == self.named_groups.as(opts[:as]).to_set + def in_only_named_groups?(*named_groups) + membership_type = named_groups.extract_options![:as] + named_groups.flatten.to_set == self.named_groups.as(membership_type).to_set end def shares_any_named_group?(other, opts = {}) @@ -60,7 +57,7 @@ def shares_any_named_group?(other, opts = {}) module ClassMethods def as(membership_type) - memberships_merge(Groupify.group_membership_klass.as(membership_type)) + memberships_merge{as(membership_type)} end def in_named_group(named_group) @@ -81,9 +78,9 @@ def in_all_named_groups(*named_groups) return none unless named_groups.present? memberships_merge{where(group_name: named_groups)}. - group(ActiveRecord.quote(self, 'id')). - having("COUNT(DISTINCT #{ActiveRecord.quote(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). - distinct + group(ActiveRecord.quote(self, 'id')). + having("COUNT(DISTINCT #{ActiveRecord.quote(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). + distinct end def in_only_named_groups(*named_groups) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index aa8eddf..a855691 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -136,7 +136,7 @@ def delete(*args) opts = args.extract_options! members = args - if opts[:as] + if opts[:as].present? members.each do |member| member.group_memberships.as(opts[:as]).first.groups.delete(base) end diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index d42ff23..d1d3792 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -18,7 +18,7 @@ module GroupMember included do has_and_belongs_to_many :groups, autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name do def as(membership_type) - return self unless membership_type + return self unless membership_type.present? group_ids = base.group_memberships.as(membership_type).first.group_ids if group_ids.present? @@ -37,7 +37,7 @@ def delete(*args) groups = args.flatten - if opts[:as] + if opts[:as].present? base.group_memberships.as(opts[:as]).each do |membership| membership.groups.delete(*groups) end diff --git a/lib/groupify/adapter/mongoid/named_group_collection.rb b/lib/groupify/adapter/mongoid/named_group_collection.rb index 497e7b4..7f401ed 100644 --- a/lib/groupify/adapter/mongoid/named_group_collection.rb +++ b/lib/groupify/adapter/mongoid/named_group_collection.rb @@ -19,7 +19,7 @@ def <<(named_group, opts = {}) super(named_group) uniq! - if @member && opts[:as] + if @member && opts[:as].present? membership = @member.group_memberships.find_or_initialize_by(as: opts[:as]) membership.named_groups << named_group membership.save! @@ -42,7 +42,7 @@ def delete(*args) named_groups = args.flatten if @member - if opts[:as] + if opts[:as].present? membership = @member.group_memberships.as(opts[:as]).first if membership if ::Mongoid::VERSION > "4" From 19f73cb2d8ece5e45f7e3f2041da392ec975dd7b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:14:56 -0400 Subject: [PATCH 068/205] Improve inferring parent model when model can be group and member --- lib/groupify/adapter/active_record.rb | 56 +++++++++++++++---- .../active_record/association_extensions.rb | 6 +- lib/groupify/adapter/active_record/group.rb | 2 +- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 0d104b9..3f0ca6e 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -15,16 +15,27 @@ def self.quote(model_class, column_name) "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" end - def self.find_memberships_for(parent, children, membership_type = nil) - parent_type, child_type = detect_types_from_parent(parent) + def self.memberships_merge(scope, options = {}, &group_membership_filter) + parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) + merge_criteria = options[:merge_criteria] - query = parent.__send__(:"group_memberships_as_#{parent_type}").__send__(:"for_#{child_type}s", children) - query = query.as(membership_type) if membership_type + query = parent.joins(:"group_memberships_as_#{parent_type}") + query = query.merge(merge_criteria) if merge_criteria + query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? query end + def self.find_memberships_for(parent, children, options = {}) + parent, parent_type, child_type = infer_parent_and_types(parent, options[:parent_type]) + + parent. + __send__(:"group_memberships_as_#{parent_type}"). + __send__(:"for_#{child_type}s", children). + as(options[:as]) + end + def self.add_children_to_parent(parent, children, options = {}) - parent_type, child_type = detect_types_from_parent(parent) + parent, parent_type, child_type = infer_parent_and_types(parent, options[:parent_type]) membership_type = options[:as] exception_on_invalidation = options[:exception_on_invalidation] @@ -38,7 +49,7 @@ def self.add_children_to_parent(parent, children, options = {}) to_add_directly = [] to_add_with_membership_type = [] - already_children = find_memberships_for(parent, children).includes(child_type).map(&child_type).uniq + already_children = find_memberships_for(parent, children, parent_type: parent_type).includes(child_type).map(&child_type).uniq children -= already_children # first prepare changes @@ -85,11 +96,36 @@ def self.add_children_to_parent(parent, children, options = {}) protected - def self.detect_types_from_parent(parent) - if parent.is_a?(Groupify::ActiveRecord::Group) - [:group, :member] + # Takes an association or model as the parent. If a model + # is passed in, the `default_parent_type` option needs + # to be passed in if the model is both a group and group member. + # + # Can't detect based on included `Group` or `GroupMember` + # modules because a model can be both a group and a gorup member. + def self.infer_parent_and_types(parent, default_parent_type = nil) + parent_is_group = true + + # Association assumed to be a `has_many through` + if parent.respond_to?(:through_reflection) + parent_is_group = (parent.through_reflection.name == :group_memberships_as_group) + parent = parent.owner + elsif default_parent_type + parent_is_group = (default_parent_type == :group) + else + parent_is_group = parent.class.include?(Groupify::ActiveRecord::Group) + detected_modules = [detected_group, parent.class.include?(Groupify::ActiveRecord::GroupMember)].count{ |bool| bool == true } + + if detected_modules == 0 + raise "The specified record is neither group nor group member." + elsif detected_modules == 2 + raise "Can't infer whether record should be treated as group or group member because it is configured as both. Pass the `default_parent_type` option to specify which it should be treated as." + end + end + + if parent_is_group + [parent, :group, :member] else - [:member, :group] + [parent, :member, :group] end end end diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index bc418e1..c3714dc 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -37,7 +37,7 @@ def add_with_exception(*children) def add_children(children, options = {}) ActiveRecord.add_children_to_parent( - proxy_association.owner, + proxy_association, children, options ) @@ -45,9 +45,9 @@ def add_children(children, options = {}) def remove_children(children, destruction_type, membership_type = nil) ActiveRecord.find_memberships_for( - proxy_association.owner, + proxy_association, children, - membership_type + as: membership_type ).__send__(:"#{destruction_type}_all") children.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index d0e5c96..e0b4b14 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -29,7 +29,7 @@ def member_classes end def add(*members) - opts = members.extract_options! + opts = members.extract_options!.merge(parent_type: :group) ActiveRecord.add_children_to_parent(self, members.flatten, opts) From 8295cbd822ff06889864afce6ad935e34986f268 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:15:17 -0400 Subject: [PATCH 069/205] Consolidate `membership_merge` logic --- lib/groupify/adapter/active_record/group.rb | 5 +---- lib/groupify/adapter/active_record/group_member.rb | 6 ++---- lib/groupify/adapter/active_record/named_group_member.rb | 5 +---- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index e0b4b14..cabb7ff 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -123,10 +123,7 @@ def define_member_association(member_klass, association_name = nil) end def memberships_merge(merge_criteria, &group_membership_filter) - query = joins(:group_memberships_as_group) - query = query.merge(merge_criteria) if merge_criteria - query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? - query + ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :group, &group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 968c963..ae9ebf8 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -121,6 +121,7 @@ def has_group(name, source_type = nil, options = {}) options, source_type = source_type, nil end + #source_type ||= Groupify.infer_class_and_association_name(name).first || @group_class_name source_type ||= @group_class_name has_many name.to_sym, ->{ distinct }, { @@ -132,10 +133,7 @@ def has_group(name, source_type = nil, options = {}) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - query = joins(:group_memberships_as_member) - query = query.merge(merge_criteria) if merge_criteria - query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? - query + ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :member, &group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 952b425..cb9d7d0 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -101,10 +101,7 @@ def shares_any_named_group(other) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - query = joins(:group_memberships_as_member) - query = query.merge(merge_criteria) if merge_criteria - query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? - query + ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :member, &group_membership_filter) end end end From f32e08221afbc7e64ff62eb470cea19519773a30 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:16:33 -0400 Subject: [PATCH 070/205] Exclude `ActiveRecord::AssociationTypeMismatch` test because it is not raised with polymorphic logic --- spec/active_record_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index dc103f3..dde14e0 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -288,7 +288,7 @@ class ProjectMember < ActiveRecord::Base expect(group.members.count).to eq(1) end - it "only allows members to be added to their configured group type" do + xit "only allows members to be added to their configured group type" do classroom = Classroom.create! expect { classroom.add(user) }.to raise_error(ActiveRecord::AssociationTypeMismatch) expect { user.groups << classroom }.to raise_error(ActiveRecord::AssociationTypeMismatch) From 64feccae64bf94c5e7b9ff3ed280e596c515ebf1 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 00:29:30 -0400 Subject: [PATCH 071/205] Build query via chain instead of control logic --- lib/groupify/adapter/active_record.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 3f0ca6e..2e8c2b0 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -17,12 +17,12 @@ def self.quote(model_class, column_name) def self.memberships_merge(scope, options = {}, &group_membership_filter) parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) - merge_criteria = options[:merge_criteria] + group_membership_filter ||= ->{ all } - query = parent.joins(:"group_memberships_as_#{parent_type}") - query = query.merge(merge_criteria) if merge_criteria - query = query.merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) if block_given? - query + parent. + joins(:"group_memberships_as_#{parent_type}"). + merge(options[:merge_criteria] || {}). + merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) end def self.find_memberships_for(parent, children, options = {}) From 3dbe4282ec4477ccd3d4271ed794029888ba1e26 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 01:31:42 -0400 Subject: [PATCH 072/205] Set blank values to nil --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/named_group_collection.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index cabb7ff..14a938d 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -44,7 +44,7 @@ def merge!(source) module ClassMethods def with_member(member) memberships_merge(member.group_memberships_as_member). - extending(Groupify::ActiveRecord::AssociationExtensions) + extending(Groupify::ActiveRecord::AssociationExtensions) end def default_member_class diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index c5d713c..123d991 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -12,8 +12,8 @@ def initialize(member) def add(named_group, opts = {}) named_group = named_group.to_sym - membership_type = opts[:as] - membership_type = membership_type.to_s if membership_type.is_a?(Symbol) + membership_type = opts[:as].present? ? opts[:as].to_s : nil + # always add a nil membership type and then a specific one (if specified) membership_types = [nil, membership_type].uniq From 4192d316962ddb97c692efe16074841699ed3d2c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 01:32:11 -0400 Subject: [PATCH 073/205] Throw "type mismatch" exception when adding to association --- .../adapter/active_record/association_extensions.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index c3714dc..b4c68ef 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -36,6 +36,13 @@ def add_with_exception(*children) protected def add_children(children, options = {}) + # Throw an exception here when adding direction to an association + # because when adding the children to the parent this won't + # happen because the group membership is polymorphic. + children.each do |child| + proxy_association.__send__(:raise_on_type_mismatch!, child) + end + ActiveRecord.add_children_to_parent( proxy_association, children, From eea79b33fe2683e7935e3039422e73a8d0584457 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 01:32:36 -0400 Subject: [PATCH 074/205] Update Mongoid with some similar code cleanup as ActiveRecord --- lib/groupify/adapter/mongoid/group.rb | 28 +++++++++++---------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index a855691..606cf7c 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -66,8 +66,8 @@ def member_classes # Define which classes are members of this group def has_members(*names) - Array.wrap(names.flatten).each do |name| - has_member name + names.flatten.each do |name| + has_member(name) end end @@ -75,14 +75,13 @@ def has_member(name, options = {}) klass_name = options[:class_name] if klass_name.nil? - klass = name.to_s.classify.constantize - association_name = name.is_a?(Symbol) ? name : klass.model_name.plural.to_sym + klass, association_name = Groupify.infer_class_and_association_name(name) else klass = klass_name.to_s.classify.constantize association_name = name.to_sym end - register(klass, association_name) + associate_member_class(klass, association_name) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -114,17 +113,9 @@ def merge!(source_group, destination_group) protected - def register(member_klass, association_name = nil) - (@member_klasses ||= Set.new) << member_klass - - associate_member_class(member_klass, association_name) - - member_klass - end - module MemberAssociationExtensions def as(membership_type) - return self unless membership_type + return self unless membership_type.present? where(:group_memberships.elem_match => { as: membership_type.to_s, group_ids: [base.id] }) end @@ -132,9 +123,8 @@ def destroy(*args) delete(*args) end - def delete(*args) - opts = args.extract_options! - members = args + def delete(*members) + opts = members.extract_options! if opts[:as].present? members.each do |member| @@ -153,6 +143,8 @@ def delete(*args) end def associate_member_class(member_klass, association_name = nil) + (@member_klasses ||= Set.new) << member_klass + association_name ||= member_klass.model_name.plural.to_sym has_many association_name, class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions @@ -160,6 +152,8 @@ def associate_member_class(member_klass, association_name = nil) if member_klass == default_member_class has_many :members, class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions end + + member_klass end end end From 856377fc7ad2f47d8e7f5f45e7729543a5ceafd0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 15:14:40 -0400 Subject: [PATCH 075/205] Fixed filter fallback in tests --- lib/groupify/adapter/active_record.rb | 6 +++--- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- lib/groupify/adapter/active_record/named_group_member.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 2e8c2b0..f73d6c2 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -15,13 +15,13 @@ def self.quote(model_class, column_name) "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" end - def self.memberships_merge(scope, options = {}, &group_membership_filter) + def self.memberships_merge(scope, options = {}) parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) - group_membership_filter ||= ->{ all } + group_membership_filter = options[:filter] || :all parent. joins(:"group_memberships_as_#{parent_type}"). - merge(options[:merge_criteria] || {}). + merge(options[:criteria] || {}). merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 14a938d..2d1c652 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -123,7 +123,7 @@ def define_member_association(member_klass, association_name = nil) end def memberships_merge(merge_criteria, &group_membership_filter) - ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :group, &group_membership_filter) + ActiveRecord.memberships_merge(self, parent_type: :group, criteria: merge_criteria, filter: group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index ae9ebf8..3ddcd23 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -133,7 +133,7 @@ def has_group(name, source_type = nil, options = {}) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :member, &group_membership_filter) + ActiveRecord.memberships_merge(self, parent_type: :member, criteria: merge_criteria, filter: group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index cb9d7d0..7c4bd98 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -101,7 +101,7 @@ def shares_any_named_group(other) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, merge_criteria: merge_criteria, parent_type: :member, &group_membership_filter) + ActiveRecord.memberships_merge(self, parent_type: :member, criteria: merge_criteria, filter: group_membership_filter) end end end From f708dba96de1f54a8ea80d68806fae4623bf6bef Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 16:41:30 -0400 Subject: [PATCH 076/205] Fix flattening arrays --- lib/groupify/adapter/active_record/group.rb | 2 +- .../adapter/active_record/group_member.rb | 27 ++++++++----------- .../adapter/active_record/group_membership.rb | 6 +---- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 2d1c652..dccf74f 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -122,7 +122,7 @@ def define_member_association(member_klass, association_name = nil) extend: Groupify::ActiveRecord::AssociationExtensions end - def memberships_merge(merge_criteria, &group_membership_filter) + def memberships_merge(merge_criteria = nil, &group_membership_filter) ActiveRecord.memberships_merge(self, parent_type: :group, criteria: merge_criteria, filter: group_membership_filter) end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 3ddcd23..c7faaed 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -35,20 +35,17 @@ def in_group?(group, opts = {}) def in_any_group?(*groups) opts = groups.extract_options! - groups.flatten.any?{ |group| in_group?(group, opts) } end def in_all_groups?(*groups) - opts = groups.extract_options! - - groups.flatten.to_set.subset? self.groups.as(opts[:as]).to_set + membership_type = groups.extract_options![:as] + groups.flatten.to_set.subset? self.groups.as(membership_type).to_set end def in_only_groups?(*groups) - opts = groups.extract_options! - - groups.flatten.to_set == self.groups.as(opts[:as]).to_set + membership_type = groups.extract_options![:as] + groups.flatten.to_set == self.groups.as(membership_type).to_set end def shares_any_group?(other, opts = {}) @@ -61,20 +58,17 @@ def as(membership_type) end def in_group(group) - return none unless group.present? - - memberships_merge(group.group_memberships_as_group).distinct + group.present? ? memberships_merge(group.group_memberships_as_group).distinct : none end def in_any_group(*groups) - groups = groups.flatten - return none unless groups.present? - - memberships_merge{for_groups(groups)}.distinct + groups.flatten! + groups.present? ? memberships_merge{for_groups(groups)}.distinct : none end def in_all_groups(*groups) - groups = groups.flatten + groups.flatten! + return none unless groups.present? group_id_column = ActiveRecord.quote(Groupify.group_membership_klass, 'group_id') @@ -94,7 +88,8 @@ def in_all_groups(*groups) end def in_only_groups(*groups) - groups = groups.flatten + groups.flatten! + return none unless groups.present? in_all_groups(*groups). diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index a63f2ab..8b8aa72 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -39,11 +39,7 @@ def named(group_name = nil) end def as(membership_type) - if membership_type.present? - where(membership_type: membership_type.to_s) - else - all - end + membership_type.present? ? where(membership_type: membership_type.to_s) : all end def for_groups(groups) From a47b62542f38b4a347c00d7763f79e0882757a1b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 16:56:39 -0400 Subject: [PATCH 077/205] DRY up Mongoid similar to ActiveRecord --- lib/groupify/adapter/mongoid/group.rb | 44 +++++++++------ lib/groupify/adapter/mongoid/group_member.rb | 49 +++++++---------- .../adapter/mongoid/member_scoped_as.rb | 15 ++--- .../adapter/mongoid/named_group_collection.rb | 55 ++++++++----------- .../adapter/mongoid/named_group_member.rb | 39 ++++--------- 5 files changed, 87 insertions(+), 115 deletions(-) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 606cf7c..b60bd66 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -27,10 +27,10 @@ def member_classes self.class.member_classes end - def add(*args) - opts = args.extract_options! - membership_type = opts[:as] - members = args.flatten + def add(*members) + membership_type = members.extract_options![:as] + members.flatten! + return unless members.present? members.each do |member| @@ -98,12 +98,16 @@ def merge!(source_group, destination_group) klass.in(group_ids: [source_group.id]).update_all(:$set => {:"group_ids.$" => destination_group.id}) if klass.relations['group_memberships'] + scope = klass.in(:"group_memberships.group_ids" => [source_group.id]) + criteria_for_add_to_set = {:"group_memberships.$.group_ids" => destination_group.id} + criteria_for_pull = {:"group_memberships.$.group_ids" => source_group.id} + if ::Mongoid::VERSION > "4" - klass.in(:"group_memberships.group_ids" => [source_group.id]).add_to_set(:"group_memberships.$.group_ids" => destination_group.id) - klass.in(:"group_memberships.group_ids" => [source_group.id]).pull(:"group_memberships.$.group_ids" => source_group.id) + scope.add_to_set(criteria_for_add_to_set) + scope.pull(criteria_for_pull) else - klass.in(:"group_memberships.group_ids" => [source_group.id]).add_to_set(:"group_memberships.$.group_ids", destination_group.id) - klass.in(:"group_memberships.group_ids" => [source_group.id]).pull(:"group_memberships.$.group_ids", source_group.id) + scope.add_to_set(*criteria_for_add_to_set.to_a.flatten) + scope.pull(*criteria_for_pull.to_a.flatten) end end end @@ -115,20 +119,19 @@ def merge!(source_group, destination_group) module MemberAssociationExtensions def as(membership_type) - return self unless membership_type.present? - where(:group_memberships.elem_match => { as: membership_type.to_s, group_ids: [base.id] }) + membership_type.present? ? where(:group_memberships.elem_match => {as: membership_type, group_ids: [base.id]}) : self end - def destroy(*args) - delete(*args) + def destroy(*members) + delete(*members) end def delete(*members) - opts = members.extract_options! + membership_type = members.extract_options![:as] - if opts[:as].present? + if membership_type.present? members.each do |member| - member.group_memberships.as(opts[:as]).first.groups.delete(base) + member.group_memberships.as(membership_type).first.groups.delete(base) end else members.each do |member| @@ -147,10 +150,17 @@ def associate_member_class(member_klass, association_name = nil) association_name ||= member_klass.model_name.plural.to_sym - has_many association_name, class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions + options = { + class_name: member_klass.to_s, + dependent: :nullify, + foreign_key: 'group_ids', + extend: MemberAssociationExtensions + } + + has_many association_name, options if member_klass == default_member_class - has_many :members, class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions + has_many :members, options end member_klass diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index d1d3792..43f7023 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -19,6 +19,7 @@ module GroupMember has_and_belongs_to_many :groups, autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name do def as(membership_type) return self unless membership_type.present? + group_ids = base.group_memberships.as(membership_type).first.group_ids if group_ids.present? @@ -28,17 +29,16 @@ def as(membership_type) end end - def destroy(*args) - delete(*args) + def destroy(*groups) + delete(*groups) end - def delete(*args) - opts = args.extract_options! - groups = args.flatten - + def delete(*groups) + membership_type = groups.extract_options![:as] + groups.flatten! - if opts[:as].present? - base.group_memberships.as(opts[:as]).each do |membership| + if membership_type.present? + base.group_memberships.as(membership_type).each do |membership| membership.groups.delete(*groups) end else @@ -65,38 +65,28 @@ class GroupMembership embeds_many :group_memberships, class_name: GroupMembership.to_s, as: :member do def as(membership_type) - where(membership_type: membership_type.to_s) + where(membership_type: membership_type) end end end def in_group?(group, opts = {}) - return false unless group.present? - groups.as(opts[:as]).include?(group) + group.present? ? groups.as(opts[:as]).include?(group) : false end - def in_any_group?(*args) - opts = args.extract_options! - groups = args - - groups.flatten.each do |group| - return true if in_group?(group, opts) - end - return false + def in_any_group?(*groups) + opts = groups.extract_options! + groups.flatten.any?{ |group| in_group?(group, opts) } end - def in_all_groups?(*args) - opts = args.extract_options! - groups = args - - groups.flatten.to_set.subset? self.groups.as(opts[:as]).to_set + def in_all_groups?(*groups) + membership_type = groups.extract_options![:as] + groups.flatten.to_set.subset? self.groups.as(membership_type).to_set end - def in_only_groups?(*args) - opts = args.extract_options! - groups = args.flatten - - groups.to_set == self.groups.as(opts[:as]).to_set + def in_only_groups?(*groups) + membership_type = groups.extract_options![:as] + groups.to_set == self.groups.as(membership_type).to_set end def shares_any_group?(other, opts = {}) @@ -123,7 +113,6 @@ def in_only_groups(*groups) def shares_any_group(other) in_any_group(other.groups.to_a) end - end end end diff --git a/lib/groupify/adapter/mongoid/member_scoped_as.rb b/lib/groupify/adapter/mongoid/member_scoped_as.rb index 4c37c55..1efaabd 100644 --- a/lib/groupify/adapter/mongoid/member_scoped_as.rb +++ b/lib/groupify/adapter/mongoid/member_scoped_as.rb @@ -6,21 +6,18 @@ module MemberScopedAs module ClassMethods def as(membership_type) + criteria = self.criteria + + return criteria unless membership_type.present? + group_ids = criteria.selector["group_ids"] named_groups = criteria.selector["named_groups"] - criteria = self.criteria # If filtering by groups or named groups, merge into the group membership criteria if group_ids || named_groups elem_match = {as: membership_type} - - if group_ids - elem_match.merge!(group_ids: group_ids) - end - - if named_groups - elem_match.merge!(named_groups: named_groups) - end + elem_match.merge!(group_ids: group_ids) if group_ids + elem_match.merge!(named_groups: named_groups) if named_groups criteria = where(:group_memberships.elem_match => elem_match) criteria.selector.delete("group_ids") diff --git a/lib/groupify/adapter/mongoid/named_group_collection.rb b/lib/groupify/adapter/mongoid/named_group_collection.rb index 7f401ed..599c777 100644 --- a/lib/groupify/adapter/mongoid/named_group_collection.rb +++ b/lib/groupify/adapter/mongoid/named_group_collection.rb @@ -4,14 +4,11 @@ module Mongoid module NamedGroupCollection # Criteria to filter by membership type def as(membership_type) - return self unless membership_type + return self unless membership_type.present? membership = @member.group_memberships.as(membership_type).first - if membership - membership.named_groups - else - self.class.new - end + + membership ? membership.named_groups : self.class.new end def <<(named_group, opts = {}) @@ -28,45 +25,39 @@ def <<(named_group, opts = {}) self end - def merge(*args) - opts = args.extract_options! - named_groups = args.flatten + def merge(*named_groups) + opts = named_groups.extract_options! - named_groups.each do |named_group| + named_groups.flatten.each do |named_group| add(named_group, opts) end end - def delete(*args) - opts = args.extract_options! - named_groups = args.flatten + def delete(*named_groups) + membership_type = named_groups.extract_options![:as] + named_groups.flatten! if @member - if opts[:as].present? - membership = @member.group_memberships.as(opts[:as]).first - if membership - if ::Mongoid::VERSION > "4" - membership.pull_all(named_groups: named_groups) - else - membership.pull_all(:named_groups, named_groups) - end - end - - return + if membership_type.present? + skip_default = true + memberships = [@member.group_memberships.as(membership_type).first] else memberships = @member.group_memberships.where(:named_groups.in => named_groups) - memberships.each do |membership| - if ::Mongoid::VERSION > "4" - membership.pull_all(named_groups: named_groups) - else - membership.pull_all(:named_groups, named_groups) - end + end + + memberships.each do |membership| + if ::Mongoid::VERSION > "4" + membership.pull_all(named_groups: named_groups) + else + membership.pull_all(:named_groups, named_groups) end end end - named_groups.each do |named_group| - super(named_group) + unless skip_default + named_groups.each do |named_group| + super(named_group) + end end end diff --git a/lib/groupify/adapter/mongoid/named_group_member.rb b/lib/groupify/adapter/mongoid/named_group_member.rb index 4453022..6033d85 100644 --- a/lib/groupify/adapter/mongoid/named_group_member.rb +++ b/lib/groupify/adapter/mongoid/named_group_member.rb @@ -28,28 +28,19 @@ def in_named_group?(named_group, opts = {}) named_groups.as(opts[:as]).include?(named_group) end - def in_any_named_group?(*args) - opts = args.extract_options! - group_names = args.flatten - - group_names.each do |named_group| - return true if in_named_group?(named_group) - end - - return false + def in_any_named_group?(*group_names) + opts = group_names.extract_options! + group_names.flatten.any?{ |named_group| in_named_group?(named_group, opts) } end - def in_all_named_groups?(*args) - opts = args.extract_options! - named_groups = args.flatten.to_set - - named_groups.subset? self.named_groups.as(opts[:as]).to_set + def in_all_named_groups?(*named_groups) + membership_type = named_groups.extract_options![:as] + named_groups.flatten.to_set.subset? self.named_groups.as(membership_type).to_set end - def in_only_named_groups?(*args) - opts = args.extract_options! - named_groups = args.flatten.to_set - named_groups == self.named_groups.as(opts[:as]).to_set + def in_only_named_groups?(*named_groups) + membership_type = named_groups.extract_options![:as] + named_groups.flatten.to_set == self.named_groups.as(membership_type).to_set end def shares_any_named_group?(other, opts = {}) @@ -63,23 +54,17 @@ def in_named_group(named_group, opts = {}) def in_any_named_group(*named_groups) named_groups.flatten! - return none unless named_groups.present? - - self.in(named_groups: named_groups.flatten) + named_groups.present? ? self.in(named_groups: named_groups) : none end def in_all_named_groups(*named_groups) named_groups.flatten! - return none unless named_groups.present? - - where(:named_groups.all => named_groups.flatten) + named_groups.present? ? where(:named_groups.all => named_groups) : none end def in_only_named_groups(*named_groups) named_groups.flatten! - return none unless named_groups.present? - - where(named_groups: named_groups.flatten) + named_groups.present? ? where(named_groups: named_groups) : none end def shares_any_named_group(other, opts = {}) From e2cf3e5deddee1dbfedab00a7cf05f68d4f07136 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 3 Aug 2017 17:16:05 -0400 Subject: [PATCH 078/205] Fix Mongoid test --- lib/groupify/adapter/mongoid/group_member.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index 43f7023..c4bbfc3 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -18,7 +18,8 @@ module GroupMember included do has_and_belongs_to_many :groups, autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name do def as(membership_type) - return self unless membership_type.present? + # `membership_type.present?` causes tests to fail for `MongoidManager` class.... + return self unless membership_type group_ids = base.group_memberships.as(membership_type).first.group_ids From 3614e2a08eb4310508c0e73381de2435396bc242 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 02:33:32 -0400 Subject: [PATCH 079/205] Rails 4.0 doesn't like merging empty hash, so we build the query step by step --- lib/groupify/adapter/active_record.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index f73d6c2..bf20d08 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -17,12 +17,13 @@ def self.quote(model_class, column_name) def self.memberships_merge(scope, options = {}) parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) - group_membership_filter = options[:filter] || :all - parent. - joins(:"group_memberships_as_#{parent_type}"). - merge(options[:criteria] || {}). - merge(Groupify.group_membership_klass.instance_eval(&group_membership_filter)) + criteria = [parent.joins(:"group_memberships_as_#{parent_type}")] + criteria << options[:criteria] if options[:criteria] + criteria << Groupify.group_membership_klass.instance_eval(&options[:filter]) if options[:filter] + + # merge all criteria together + criteria.compact.reduce(:merge) end def self.find_memberships_for(parent, children, options = {}) From c7f09bdb8bf2686a92d1f0356cd4353dfe2540c7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 13:38:57 -0400 Subject: [PATCH 080/205] Fix test to use specific IDs --- spec/active_record_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index dde14e0..644c0ea 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -81,9 +81,9 @@ class Classroom < ActiveRecord::Base describe Groupify::ActiveRecord do let(:user) { User.create! } - let(:group) { Group.create! } - let(:classroom) { Classroom.create! } - let(:organization) { Organization.create! } + let(:group) { Group.create!(id: 10) } + let(:classroom) { Classroom.create!(id: 10) } + let(:organization) { Organization.create!(id: 11) } describe "polymorphic groups" do context "memberships" do @@ -119,13 +119,13 @@ class Classroom < ActiveRecord::Base classroom.add user organization.add user - expect(group.id).to eq(1) - expect(classroom.id).to eq(1) - expect(organization.id).to eq(2) + expect(group.id).to eq(10) + expect(classroom.id).to eq(10) + expect(organization.id).to eq(11) expect(user.group_memberships_as_member.map(&:group)).to eq([group, classroom, organization]) expect(GroupMembership.for_groups([group, classroom]).count).to eq(2) - expect(GroupMembership.for_groups([group, classroom, organization]).count).to eq(3) expect(GroupMembership.for_groups([group, classroom]).distinct.count).to eq(2) + expect(GroupMembership.for_groups([group, classroom, organization]).count).to eq(3) expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.size).to eq(1) expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.first).to eq(user) end From a3ba035880ecb02bdc4a3fce2246594e3913cbe3 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 13:39:46 -0400 Subject: [PATCH 081/205] Clear association cache on child after creating membership --- lib/groupify/adapter/active_record.rb | 5 ++++- spec/active_record_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index bf20d08..c0858ca 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -65,9 +65,12 @@ def self.add_children_to_parent(parent, children, options = {}) first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end - parent.__send__(:clear_association_cache) + + child.__send__(:clear_association_cache) end + parent.__send__(:clear_association_cache) + # then validate changes list_to_validate = to_add_directly + to_add_with_membership_type diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 644c0ea..e6032ca 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -129,6 +129,16 @@ class Classroom < ActiveRecord::Base expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.size).to eq(1) expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.first).to eq(user) end + + it "member has groups in has_many through associations after adding member to groups" do + + expect(user.groups.size).to eq(0) + + group.add user + organization.add user + + expect(user.groups.size).to eq(2) + end end end end From 33f7af23930ad60197e26548af29fd47fd971542 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 13:41:26 -0400 Subject: [PATCH 082/205] Fix tests for Postgres which changes the order of records --- spec/active_record_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index e6032ca..f224b11 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -122,7 +122,7 @@ class Classroom < ActiveRecord::Base expect(group.id).to eq(10) expect(classroom.id).to eq(10) expect(organization.id).to eq(11) - expect(user.group_memberships_as_member.map(&:group)).to eq([group, classroom, organization]) + expect(user.group_memberships_as_member.map(&:group).sort).to eq([group, classroom, organization].sort) expect(GroupMembership.for_groups([group, classroom]).count).to eq(2) expect(GroupMembership.for_groups([group, classroom]).distinct.count).to eq(2) expect(GroupMembership.for_groups([group, classroom, organization]).count).to eq(3) @@ -133,7 +133,7 @@ class Classroom < ActiveRecord::Base it "member has groups in has_many through associations after adding member to groups" do expect(user.groups.size).to eq(0) - + group.add user organization.add user From 028f88c2bc030a1edd74b6f1e6ed136eb73b7173 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 14:26:21 -0400 Subject: [PATCH 083/205] Fix tests to compare arrays properly --- spec/active_record_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index f224b11..6db8d7f 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -122,10 +122,15 @@ class Classroom < ActiveRecord::Base expect(group.id).to eq(10) expect(classroom.id).to eq(10) expect(organization.id).to eq(11) - expect(user.group_memberships_as_member.map(&:group).sort).to eq([group, classroom, organization].sort) + + membership_groups = user.group_memberships_as_member.map(&:group) + + expect(membership_groups).to include(group, classroom, organization) expect(GroupMembership.for_groups([group, classroom]).count).to eq(2) + expect(GroupMembership.for_groups([group, classroom]).map(&:group)).to include(group, classroom) expect(GroupMembership.for_groups([group, classroom]).distinct.count).to eq(2) expect(GroupMembership.for_groups([group, classroom, organization]).count).to eq(3) + expect(GroupMembership.for_groups([group, classroom, organization]).map(&:group)).to include(group, classroom, organization) expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.size).to eq(1) expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.first).to eq(user) end From df9bdb8a05b51b647cee391fbba281e64ac4abf1 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 14:26:43 -0400 Subject: [PATCH 084/205] Simplify methods by removing outdated aliasing --- .../active_record/association_extensions.rb | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index b4c68ef..5898485 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -17,22 +17,16 @@ def destroy(*records) # Defined to create alias methods before # the association is extended with this module - def <<(*) - super + def <<(*children) + opts = children.extract_options!.merge(exception_on_invalidation: false) + add_children(children.flatten, opts) end - def add_without_exception(*children) - add_children(children, children.extract_options!.merge(exception_on_invalidation: false)) + def add(*children) + opts = children.extract_options!.merge(exception_on_invalidation: true) + add_children(children.flatten, opts) end - def add_with_exception(*children) - add_children(children, children.extract_options!.merge(exception_on_invalidation: true)) - end - - alias_method :add_as_usual, :<< - alias_method :<<, :add_without_exception - alias_method :add, :add_with_exception - protected def add_children(children, options = {}) From 228aa6c79e111feed7439beec981dbc478f010b0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 14:35:49 -0400 Subject: [PATCH 085/205] Add test to make sure associations are reset after deletion --- .../adapter/active_record/association_extensions.rb | 2 ++ spec/active_record_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 5898485..d344d88 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -51,6 +51,8 @@ def remove_children(children, destruction_type, membership_type = nil) as: membership_type ).__send__(:"#{destruction_type}_all") + proxy_association.owner.__send__(:clear_association_cache) + children.each{|record| record.__send__(:clear_association_cache)} self diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 6db8d7f..86468ac 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -144,6 +144,16 @@ class Classroom < ActiveRecord::Base expect(user.groups.size).to eq(2) end + + it "member doesn't have groups in has_many through associations after deleting member from group" do + group.add user + + expect(user.groups.size).to eq(1) + + user.groups.delete group + + expect(user.groups.size).to eq(0) + end end end end From f47a514e4df43ca364803aa9b8581feccbf58871 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 14:54:16 -0400 Subject: [PATCH 086/205] Add default`members` association in more intuitive spot --- lib/groupify/adapter/active_record/group.rb | 4 ---- lib/groupify/adapter/active_record/model.rb | 2 ++ lib/groupify/adapter/mongoid/group.rb | 4 ---- lib/groupify/adapter/mongoid/model.rb | 2 ++ 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index dccf74f..fa3d7a3 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -103,10 +103,6 @@ def associate_member_class(member_klass, association_name = nil) define_member_association(member_klass, association_name) - if member_klass == default_member_class - define_member_association(member_klass, :members) - end - member_klass end diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 27e6ff9..45f179f 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -23,6 +23,8 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize + + has_member(:members, class_name: member_klass) end if (member_klasses = opts.delete :members) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index b60bd66..394d065 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -159,10 +159,6 @@ def associate_member_class(member_klass, association_name = nil) has_many association_name, options - if member_klass == default_member_class - has_many :members, options - end - member_klass end end diff --git a/lib/groupify/adapter/mongoid/model.rb b/lib/groupify/adapter/mongoid/model.rb index 7621fb4..dd69372 100644 --- a/lib/groupify/adapter/mongoid/model.rb +++ b/lib/groupify/adapter/mongoid/model.rb @@ -17,6 +17,8 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize + + has_member(:members, class_name: member_klass) end if (member_klasses = opts.delete :members) From e3240abf69ba84c9a44ff55482a98659afdcf58c Mon Sep 17 00:00:00 2001 From: David Butler Date: Fri, 4 Aug 2017 13:42:18 -0700 Subject: [PATCH 087/205] Fix named group support in Rails 5 Rails 5 made `belongs_to` associations required by default. When using named groups, the group association is expected to be empty. This fixes the issue in a Rails 4 compatible way by adding `required: false` Fixes https://github.com/dwbutler/groupify/issues/65 --- lib/groupify/adapter/active_record/group_membership.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 99837b3..a184ae5 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -14,7 +14,7 @@ module GroupMembership included do belongs_to :member, polymorphic: true - belongs_to :group, polymorphic: true + belongs_to :group, polymorphic: true, required: false end def membership_type=(membership_type) From cfbbc8fe4ccc41f3dafa61dc9dfaf6172775e924 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 16:42:34 -0400 Subject: [PATCH 088/205] Clean up `has_member` and `has_group` and allow association options --- lib/groupify.rb | 10 ++-- lib/groupify/adapter/active_record.rb | 13 +++++ lib/groupify/adapter/active_record/group.rb | 56 ++++++++----------- .../adapter/active_record/group_member.rb | 20 +++---- .../adapter/active_record/group_membership.rb | 2 +- lib/groupify/adapter/active_record/model.rb | 2 +- lib/groupify/adapter/mongoid/group.rb | 40 +++++-------- lib/groupify/adapter/mongoid/model.rb | 2 +- spec/spec_helper.rb | 4 +- 9 files changed, 69 insertions(+), 80 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index a7875bb..3d7203b 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -15,15 +15,15 @@ def self.group_membership_klass group_membership_class_name.constantize end - def self.infer_class_and_association_name(name) - klass = name.to_s.classify.constantize rescue nil + def self.infer_class_and_association_name(association_name) + klass = association_name.to_s.classify.constantize rescue nil - association_name = if name.is_a?(Symbol) - name + association_name = if association_name.is_a?(Symbol) + association_name elsif klass klass.model_name.plural.to_sym else - name.plural.to_sym + association_name.to_sym end [klass, association_name] diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index c0858ca..07f7ba0 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -15,6 +15,19 @@ def self.quote(model_class, column_name) "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" end + # Pass in record, class, or string + def self.base_class_name(target) + return if target.nil? + + if target.is_a?(::ActiveRecord::Base) + target.class.base_class.name + elsif target.is_a?(Class) && target < ::ActiveRecord::Base + target.base_class.name + else + target.to_s.constantize.base_class.name + end + end + def self.memberships_merge(scope, options = {}) parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index fa3d7a3..924d335 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -61,63 +61,53 @@ def member_classes end # Define which classes are members of this group - def has_members(*names) - names.flatten.each do |name| - has_member(name) + def has_members(*association_names) + association_names.flatten.each do |association_name| + has_member(association_name) end end - def has_member(name, options = {}) - klass_name = options[:class_name] + def has_member(association_name, options = {}) + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + model_klass = options[:class_name] || association_class - if klass_name.nil? - klass, association_name = Groupify.infer_class_and_association_name(name) - else - klass = klass_name.to_s.classify.constantize - association_name = name.to_sym - end - - associate_member_class(klass, association_name) + define_member_association(model_klass.to_s.constantize, association_name, options) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination - invalid_member_classes = (source_group.member_classes - destination_group.member_classes) - invalid_member_classes.each do |klass| - if klass.memberships_merge(source_group.group_memberships_as_group).count > 0 - raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") - end + invalid_member_classes = source_group.member_classes - destination_group.member_classes + invalid_found = invalid_member_classes.any?{ |klass| klass.memberships_merge(source_group.group_memberships_as_group).count > 0 } + + if invalid_found + raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") end source_group.transaction do - source_group.group_memberships_as_group.update_all(group_id: destination_group.id, group_type: destination_group.class.base_class.name) + source_group.group_memberships_as_group.update_all( + group_id: destination_group.id, + group_type: ActiveRecord.base_class_name(destination_group) + ) source_group.destroy end end protected - def associate_member_class(member_klass, association_name = nil) + def define_member_association(member_klass, association_name, options = {}) (@member_klasses ||= Set.new) << member_klass - define_member_association(member_klass, association_name) + has_many association_name, ->{ distinct }, { + through: :group_memberships_as_group, + source: :member, + source_type: ActiveRecord.base_class_name(member_klass), + extend: Groupify::ActiveRecord::AssociationExtensions + }.merge(options) member_klass end - def define_member_association(member_klass, association_name = nil) - association_name ||= member_klass.model_name.plural.to_sym - source_type = member_klass.base_class.to_s - - has_many association_name, - ->{ distinct }, - through: :group_memberships_as_group, - source: :member, - source_type: source_type, - extend: Groupify::ActiveRecord::AssociationExtensions - end - def memberships_merge(merge_criteria = nil, &group_membership_filter) ActiveRecord.memberships_merge(self, parent_type: :group, criteria: merge_criteria, filter: group_membership_filter) end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index c7faaed..ea4f955 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -105,26 +105,22 @@ def shares_any_group(other) in_any_group(other.groups) end - def has_groups(*names) - names.flatten.each do |name| - has_group(name) + def has_groups(*association_names) + association_names.flatten.each do |association_name| + has_group(association_name) end end - def has_group(name, source_type = nil, options = {}) - if source_type.is_a?(Hash) - options, source_type = source_type, nil - end - - #source_type ||= Groupify.infer_class_and_association_name(name).first || @group_class_name - source_type ||= @group_class_name + def has_group(association_name, options = {}) + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + model_klass = options[:class_name] || association_class || @group_class_name has_many name.to_sym, ->{ distinct }, { through: :group_memberships_as_member, source: :group, - source_type: source_type, + source_type: ActiveRecord.base_class_name(model_klass), extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(options.slice :class_name) + }.merge(options) end def memberships_merge(merge_criteria = nil, &group_membership_filter) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 8b8aa72..1499919 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -75,7 +75,7 @@ def for_polymorphic(source, records, options = {}) # This is for polymorphic associations where the ID may be from # different tables. def build_polymorphic_criteria_for(source, records) - records_by_base_class = records.group_by{ |record| record.class.base_class.name } + records_by_base_class = records.group_by{ |record| ActiveRecord.base_class_name(record) } id_column, type_column = arel_table[:"#{source}_id"], arel_table[:"#{source}_type"] criteria = records_by_base_class.map do |type, grouped_records| diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 45f179f..a9e5a9d 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -24,7 +24,7 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize - has_member(:members, class_name: member_klass) + has_member(:members, class_name: default_member_class.to_s) end if (member_klasses = opts.delete :members) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 394d065..9b3b589 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -65,33 +65,27 @@ def member_classes end # Define which classes are members of this group - def has_members(*names) - names.flatten.each do |name| - has_member(name) + def has_members(*association_names) + association_names.flatten.each do |association_name| + has_member(association_name) end end - def has_member(name, options = {}) - klass_name = options[:class_name] + def has_member(association_name, options = {}) + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + model_klass = options[:class_name] || association_class - if klass_name.nil? - klass, association_name = Groupify.infer_class_and_association_name(name) - else - klass = klass_name.to_s.classify.constantize - association_name = name.to_sym - end - - associate_member_class(klass, association_name) + define_member_association(model_klass.to_s.constantize, association_name, options) end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination - invalid_member_classes = (source_group.member_classes - destination_group.member_classes) - invalid_member_classes.each do |klass| - if klass.in(group_ids: [source_group.id]).count > 0 - raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") - end + invalid_member_classes = source_group.member_classes - destination_group.member_classes + invalid_found = invalid_member_classes.any?{ |klass| klass.in(group_ids: [source_group.id]).count > 0 } + + if invalid_found + raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") end source_group.member_classes.each do |klass| @@ -145,19 +139,15 @@ def delete(*members) end end - def associate_member_class(member_klass, association_name = nil) + def define_member_association(member_klass, association_name, options = {}) (@member_klasses ||= Set.new) << member_klass - association_name ||= member_klass.model_name.plural.to_sym - - options = { + has_many association_name, { class_name: member_klass.to_s, dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions - } - - has_many association_name, options + }.merge(options) member_klass end diff --git a/lib/groupify/adapter/mongoid/model.rb b/lib/groupify/adapter/mongoid/model.rb index dd69372..427a9f4 100644 --- a/lib/groupify/adapter/mongoid/model.rb +++ b/lib/groupify/adapter/mongoid/model.rb @@ -18,7 +18,7 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize - has_member(:members, class_name: member_klass) + has_member(:members, class_name: default_member_class.to_s) end if (member_klasses = opts.delete :members) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 20188d8..7a8fb52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,8 +4,8 @@ require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [ - SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter + SimpleCov::Formatter::HTMLFormatter, + Coveralls::SimpleCov::Formatter ] SimpleCov.start From 49f592d58a36e65c1cd437be08db55d750da5387 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 17:53:51 -0400 Subject: [PATCH 089/205] Fix wrong variable name reference --- lib/groupify/adapter/active_record/group_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index ea4f955..9de31c9 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -115,7 +115,7 @@ def has_group(association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) model_klass = options[:class_name] || association_class || @group_class_name - has_many name.to_sym, ->{ distinct }, { + has_many association_name.to_sym, ->{ distinct }, { through: :group_memberships_as_member, source: :group, source_type: ActiveRecord.base_class_name(model_klass), From 8271701197832003e919fc5ea339c2427210dc44 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 17:54:19 -0400 Subject: [PATCH 090/205] Add `has_groups` and `has_group` to Mongoid --- lib/groupify/adapter/mongoid/group_member.rb | 74 ++++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index c4bbfc3..a9e31f8 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -16,37 +16,7 @@ module GroupMember include MemberScopedAs included do - has_and_belongs_to_many :groups, autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name do - def as(membership_type) - # `membership_type.present?` causes tests to fail for `MongoidManager` class.... - return self unless membership_type - - group_ids = base.group_memberships.as(membership_type).first.group_ids - - if group_ids.present? - self.and(:id.in => group_ids) - else - self.and(:id => nil) - end - end - - def destroy(*groups) - delete(*groups) - end - - def delete(*groups) - membership_type = groups.extract_options![:as] - groups.flatten! - - if membership_type.present? - base.group_memberships.as(membership_type).each do |membership| - membership.groups.delete(*groups) - end - else - super(*groups) - end - end - end + has_group :groups class GroupMembership include ::Mongoid::Document @@ -114,6 +84,48 @@ def in_only_groups(*groups) def shares_any_group(other) in_any_group(other.groups.to_a) end + + def has_groups(*association_names) + association_names.flatten.each do |association_name| + has_group(association_name) + end + end + + def has_group(association_name, options = {}) + options = {autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name}.merge(options) + + has_and_belongs_to_many association_name, options do + def as(membership_type) + # `membership_type.present?` causes tests to fail for `MongoidManager` class.... + return self unless membership_type + + group_ids = base.group_memberships.as(membership_type).first.group_ids + + if group_ids.present? + self.and(:id.in => group_ids) + else + self.and(:id => nil) + end + end + + def destroy(*groups) + delete(*groups) + end + + def delete(*groups) + membership_type = groups.extract_options![:as] + groups.flatten! + + if membership_type.present? + base.group_memberships.as(membership_type).each do |membership| + membership.groups.delete(*groups) + end + else + super(*groups) + end + end + end + end end end end From ed4337029aae3cbbd5998747d115226753c4bc84 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 18:47:19 -0400 Subject: [PATCH 091/205] Add proper default class --- lib/groupify/adapter/active_record/group_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 9de31c9..b99c9f0 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -21,7 +21,7 @@ module GroupMember dependent: :destroy, class_name: Groupify.group_membership_class_name - has_group :groups + has_group :groups, class_name: @group_class_name end def in_group?(group, opts = {}) From 3f051c89126a3e595e6c853a27eb521d1b41ec40 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 18:47:41 -0400 Subject: [PATCH 092/205] Only look up base class if needed --- lib/groupify/adapter/active_record/group.rb | 7 ++++++- lib/groupify/adapter/active_record/group_member.rb | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 924d335..5405954 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -98,10 +98,15 @@ def merge!(source_group, destination_group) def define_member_association(member_klass, association_name, options = {}) (@member_klasses ||= Set.new) << member_klass + unless options[:source_type] + # only try to look up base class if needed - can cause circular dependency issue + source_type = ActiveRecord.base_class_name(member_klass) || member_klass || default_member_class + end + has_many association_name, ->{ distinct }, { through: :group_memberships_as_group, source: :member, - source_type: ActiveRecord.base_class_name(member_klass), + source_type: source_type, extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index b99c9f0..16c0ab4 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -115,10 +115,15 @@ def has_group(association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) model_klass = options[:class_name] || association_class || @group_class_name + unless options[:source_type] + # only try to look up base class if needed - can cause circular dependency issue + source_type = ActiveRecord.base_class_name(model_klass) || model_klass + end + has_many association_name.to_sym, ->{ distinct }, { through: :group_memberships_as_member, source: :group, - source_type: ActiveRecord.base_class_name(model_klass), + source_type: source_type, extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options) end From 5781d413b7bdb4588f943832c14f3ed74d001985 Mon Sep 17 00:00:00 2001 From: David Butler Date: Fri, 4 Aug 2017 16:38:40 -0700 Subject: [PATCH 093/205] Fix Rails 4.0 - 4.1, which don't support the `required` option Also, specs for models that are only named group members, and don't use groups --- lib/groupify/adapter/active_record/group_membership.rb | 6 +++++- spec/active_record_spec.rb | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index a184ae5..5ff9445 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -14,7 +14,11 @@ module GroupMembership included do belongs_to :member, polymorphic: true - belongs_to :group, polymorphic: true, required: false + if ActiveSupport::VERSION::STRING > '4.1' + belongs_to :group, polymorphic: true, required: false + else + belongs_to :group, polymorphic: true + end end def membership_type=(membership_type) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index e5005d9..8611d5f 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -578,6 +578,14 @@ class ProjectMember < ActiveRecord::Base expect(user.named_groups).to be_empty end + it "works when using only named groups and not groups" do + project = Project.create! + project.named_groups.add(:accounting) + expect(project.named_groups).to include(:accounting) + project.named_groups.delete_all + expect(project.named_groups).to be_empty + end + it "checks if a member belongs to one named group" do expect(user.in_named_group?(:admin)).to be true expect(User.in_named_group(:admin).first).to eql(user) From 7dc14051d95dc23a766a06a23ab1f8748276380b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:13:24 -0400 Subject: [PATCH 094/205] Added `Groupify.ignore_base_class_inference_errors` to swallow inference errors with `base_class` --- lib/groupify.rb | 4 +++- lib/groupify/adapter/active_record.rb | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 3d7203b..82e16c6 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -2,10 +2,12 @@ module Groupify mattr_accessor :group_membership_class_name, - :group_class_name + :group_class_name, + :ignore_base_class_inference_errors self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' + self.ignore_base_class_inference_errors = true def self.configure yield self diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 07f7ba0..f993b4c 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -16,16 +16,20 @@ def self.quote(model_class, column_name) end # Pass in record, class, or string - def self.base_class_name(target) - return if target.nil? + def self.base_class_name(model_class) + return if model_class.nil? - if target.is_a?(::ActiveRecord::Base) - target.class.base_class.name - elsif target.is_a?(Class) && target < ::ActiveRecord::Base - target.base_class.name - else - target.to_s.constantize.base_class.name + if model_class.is_a?(::ActiveRecord::Base) + model_class = model_class.class + elsif !(model_class.is_a?(Class) && model_class < ::ActiveRecord::Base) + model_class = model_class.to_s.constantize end + + model_class.base_class.name + rescue NameError + raise unless Groupify.ignore_base_class_inference_errors + + model_class.to_s end def self.memberships_merge(scope, options = {}) From 50bdc00fe12751766322bdff42de5c8a35c04708 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:13:44 -0400 Subject: [PATCH 095/205] Use `source_type` instead of `class_name` --- lib/groupify/adapter/active_record/group_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 16c0ab4..701e22b 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -21,7 +21,7 @@ module GroupMember dependent: :destroy, class_name: Groupify.group_membership_class_name - has_group :groups, class_name: @group_class_name + has_group :groups, source_type: ActiveRecord.base_class_name(@group_class_name) end def in_group?(group, opts = {}) From 3f69372e54e3a3c3511a6259482533e7ea191a95 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:14:11 -0400 Subject: [PATCH 096/205] Consolidate `has_member` and `has_group` and add error handling --- lib/groupify/adapter/active_record/group.rb | 38 +++++++++---------- .../adapter/active_record/group_member.rb | 3 ++ lib/groupify/adapter/mongoid/group.rb | 25 ++++++------ 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 5405954..1872673 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -70,8 +70,26 @@ def has_members(*association_names) def has_member(association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) model_klass = options[:class_name] || association_class + member_klass = model_klass.to_s.constantize - define_member_association(model_klass.to_s.constantize, association_name, options) + (@member_klasses ||= Set.new) << member_klass + + unless options[:source_type] + # only try to look up base class if needed - can cause circular dependency issue + source_type = ActiveRecord.base_class_name(member_klass) || member_klass || default_member_class + end + + has_many association_name, ->{ distinct }, { + through: :group_memberships_as_group, + source: :member, + source_type: source_type, + extend: Groupify::ActiveRecord::AssociationExtensions + }.merge(options) + + member_klass + + rescue NameError => ex + raise "Can't infer base class for #{member_klass}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -95,24 +113,6 @@ def merge!(source_group, destination_group) protected - def define_member_association(member_klass, association_name, options = {}) - (@member_klasses ||= Set.new) << member_klass - - unless options[:source_type] - # only try to look up base class if needed - can cause circular dependency issue - source_type = ActiveRecord.base_class_name(member_klass) || member_klass || default_member_class - end - - has_many association_name, ->{ distinct }, { - through: :group_memberships_as_group, - source: :member, - source_type: source_type, - extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(options) - - member_klass - end - def memberships_merge(merge_criteria = nil, &group_membership_filter) ActiveRecord.memberships_merge(self, parent_type: :group, criteria: merge_criteria, filter: group_membership_filter) end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 701e22b..89a9b4c 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -126,6 +126,9 @@ def has_group(association_name, options = {}) source_type: source_type, extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options) + + rescue NameError => ex + raise "Can't infer base class for #{model_klass}: #{ex.message}. Try specifying the `:source_type` option such as `has_group(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end def memberships_merge(merge_criteria = nil, &group_membership_filter) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 9b3b589..916d2fa 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -74,8 +74,18 @@ def has_members(*association_names) def has_member(association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) model_klass = options[:class_name] || association_class + member_klass = model_klass.to_s.constantize - define_member_association(model_klass.to_s.constantize, association_name, options) + (@member_klasses ||= Set.new) << member_klass + + has_many association_name, { + class_name: member_klass.to_s, + dependent: :nullify, + foreign_key: 'group_ids', + extend: MemberAssociationExtensions + }.merge(options) + + member_klass end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. @@ -138,19 +148,6 @@ def delete(*members) end end end - - def define_member_association(member_klass, association_name, options = {}) - (@member_klasses ||= Set.new) << member_klass - - has_many association_name, { - class_name: member_klass.to_s, - dependent: :nullify, - foreign_key: 'group_ids', - extend: MemberAssociationExtensions - }.merge(options) - - member_klass - end end end end From 31ed05168ca11ea118721bc9a048727ad3272129 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:14:23 -0400 Subject: [PATCH 097/205] Use base class --- lib/groupify/adapter/active_record/model.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index a9e5a9d..f3168c8 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -24,7 +24,10 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize - has_member(:members, class_name: default_member_class.to_s) + has_member(:members, + source_type: ActiveRecord.base_class_name(default_member_class), + class_name: default_member_class.to_s + ) end if (member_klasses = opts.delete :members) From e4b0fddf62e3e3e3fd01994367c6df1a446bbf99 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:15:58 -0400 Subject: [PATCH 098/205] Update tests with `autoload` to help resolve circular class dependencies --- spec/active_record_spec.rb | 157 ++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 81 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 86468ac..06b9121 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -23,48 +23,70 @@ require 'groupify/adapter/active_record' -class User < ActiveRecord::Base - groupify :group_member - groupify :named_group_member - - has_group :organizations, class_name: "Organization" - has_group :classrooms, class_name: "Classroom" -end - -class Manager < User -end - -class Widget < ActiveRecord::Base - groupify :group_member -end - -module Namespaced - class Member < ActiveRecord::Base - groupify :group_member - end -end - -class Project < ActiveRecord::Base - groupify :named_group_member -end - -class Group < ActiveRecord::Base - groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users -end - -class Organization < Group - groupify :group_member - - has_members :managers, :organizations -end - -class GroupMembership < ActiveRecord::Base - groupify :group_membership -end - -class Classroom < ActiveRecord::Base - groupify :group -end +autoload :Organization, 'active_record/organization' +autoload :Group, 'active_record/group' +autoload :User, 'active_record/user' +autoload :Manager, 'active_record/manager' +autoload :Classroom, 'active_record/classroom' +autoload :GroupMembership, 'active_record/group_membership' +autoload :Namespaced, 'active_record/namespaced' +autoload :Project, 'active_record/project' +autoload :Widget, 'active_record/widget' +autoload :CustomGroupMembership, 'active_record/custom_group_membership' +autoload :CustomUser, 'active_record/custom_user' +autoload :CustomGroup, 'active_record/custom_group' + +# class Organization < Group +# groupify :group_member +# +# has_members :managers, :organizations +# end +# +# class Manager < User +# end +# +# class User < ActiveRecord::Base +# groupify :group_member +# groupify :named_group_member +# +# has_group :organizations, class_name: "Organization" +# has_group :classrooms, class_name: "Classroom" +# end +# +# class Manager < User +# end +# +# class Widget < ActiveRecord::Base +# groupify :group_member +# end +# +# module Namespaced +# class Member < ActiveRecord::Base +# groupify :group_member +# end +# end +# +# class Project < ActiveRecord::Base +# groupify :named_group_member +# end +# +# class Group < ActiveRecord::Base +# groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users +# end +# +# class Organization < Group +# groupify :group_member +# +# has_members :managers, :organizations +# end +# +# class GroupMembership < ActiveRecord::Base +# groupify :group_membership +# end +# +# class Classroom < ActiveRecord::Base +# groupify :group +# end describe Group do it { should respond_to :members} @@ -87,33 +109,6 @@ class Classroom < ActiveRecord::Base describe "polymorphic groups" do context "memberships" do - # before do - # Groupify.configure do |config| - # config.group_class_name = 'CustomGroup' - # config.group_membership_class_name = 'CustomGroupMembership' - # end - # - # class CustomGroupMembership < ActiveRecord::Base - # groupify :group_membership - # end - # - # class CustomUser < ActiveRecord::Base - # groupify :group_member - # groupify :named_group_member - # end - # - # class CustomGroup < ActiveRecord::Base - # groupify :group, members: [:custom_users] - # end - # end - # - # after do - # Groupify.configure do |config| - # config.group_class_name = 'Group' - # config.group_membership_class_name = 'GroupMembership' - # end - # end - it "finds multiple records for different models with same ID" do group.add user classroom.add user @@ -172,18 +167,18 @@ class Classroom < ActiveRecord::Base config.group_membership_class_name = 'CustomGroupMembership' end - class CustomGroupMembership < ActiveRecord::Base - groupify :group_membership - end - - class CustomUser < ActiveRecord::Base - groupify :group_member - groupify :named_group_member - end - - class CustomGroup < ActiveRecord::Base - groupify :group, members: [:custom_users] - end + # class CustomGroupMembership < ActiveRecord::Base + # groupify :group_membership + # end + # + # class CustomUser < ActiveRecord::Base + # groupify :group_member + # groupify :named_group_member + # end + # + # class CustomGroup < ActiveRecord::Base + # groupify :group, members: [:custom_users] + # end end after do From 92f08fbb6136ca61a9f33ba5feef38ded0fb05c9 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 4 Aug 2017 23:28:10 -0400 Subject: [PATCH 099/205] Moved test classes to separate files for autoloading --- spec/active_record/classroom.rb | 3 +++ spec/active_record/custom_group.rb | 3 +++ spec/active_record/custom_group_membership.rb | 3 +++ spec/active_record/custom_user.rb | 4 ++++ spec/active_record/group.rb | 3 +++ spec/active_record/group_membership.rb | 3 +++ spec/active_record/manager.rb | 2 ++ spec/active_record/namespaced.rb | 5 +++++ spec/active_record/namespaced/member.rb | 5 +++++ spec/active_record/organization.rb | 5 +++++ spec/active_record/project.rb | 3 +++ spec/active_record/user.rb | 7 +++++++ spec/active_record/widget.rb | 3 +++ 13 files changed, 49 insertions(+) create mode 100644 spec/active_record/classroom.rb create mode 100644 spec/active_record/custom_group.rb create mode 100644 spec/active_record/custom_group_membership.rb create mode 100644 spec/active_record/custom_user.rb create mode 100644 spec/active_record/group.rb create mode 100644 spec/active_record/group_membership.rb create mode 100644 spec/active_record/manager.rb create mode 100644 spec/active_record/namespaced.rb create mode 100644 spec/active_record/namespaced/member.rb create mode 100644 spec/active_record/organization.rb create mode 100644 spec/active_record/project.rb create mode 100644 spec/active_record/user.rb create mode 100644 spec/active_record/widget.rb diff --git a/spec/active_record/classroom.rb b/spec/active_record/classroom.rb new file mode 100644 index 0000000..ceb8845 --- /dev/null +++ b/spec/active_record/classroom.rb @@ -0,0 +1,3 @@ +class Classroom < ActiveRecord::Base + groupify :group +end diff --git a/spec/active_record/custom_group.rb b/spec/active_record/custom_group.rb new file mode 100644 index 0000000..4f5d8f3 --- /dev/null +++ b/spec/active_record/custom_group.rb @@ -0,0 +1,3 @@ +class CustomGroup < ActiveRecord::Base + groupify :group, members: [:custom_users] +end diff --git a/spec/active_record/custom_group_membership.rb b/spec/active_record/custom_group_membership.rb new file mode 100644 index 0000000..ce78b5d --- /dev/null +++ b/spec/active_record/custom_group_membership.rb @@ -0,0 +1,3 @@ +class CustomGroupMembership < ActiveRecord::Base + groupify :group_membership +end diff --git a/spec/active_record/custom_user.rb b/spec/active_record/custom_user.rb new file mode 100644 index 0000000..fe77740 --- /dev/null +++ b/spec/active_record/custom_user.rb @@ -0,0 +1,4 @@ +class CustomUser < ActiveRecord::Base + groupify :group_member + groupify :named_group_member +end diff --git a/spec/active_record/group.rb b/spec/active_record/group.rb new file mode 100644 index 0000000..a34c188 --- /dev/null +++ b/spec/active_record/group.rb @@ -0,0 +1,3 @@ +class Group < ActiveRecord::Base + groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users +end diff --git a/spec/active_record/group_membership.rb b/spec/active_record/group_membership.rb new file mode 100644 index 0000000..600076b --- /dev/null +++ b/spec/active_record/group_membership.rb @@ -0,0 +1,3 @@ +class GroupMembership < ActiveRecord::Base + groupify :group_membership +end diff --git a/spec/active_record/manager.rb b/spec/active_record/manager.rb new file mode 100644 index 0000000..89a5f0c --- /dev/null +++ b/spec/active_record/manager.rb @@ -0,0 +1,2 @@ +class Manager < User +end diff --git a/spec/active_record/namespaced.rb b/spec/active_record/namespaced.rb new file mode 100644 index 0000000..d0a2ec3 --- /dev/null +++ b/spec/active_record/namespaced.rb @@ -0,0 +1,5 @@ +module Namespaced + class Member < ActiveRecord::Base + groupify :group_member + end +end diff --git a/spec/active_record/namespaced/member.rb b/spec/active_record/namespaced/member.rb new file mode 100644 index 0000000..d0a2ec3 --- /dev/null +++ b/spec/active_record/namespaced/member.rb @@ -0,0 +1,5 @@ +module Namespaced + class Member < ActiveRecord::Base + groupify :group_member + end +end diff --git a/spec/active_record/organization.rb b/spec/active_record/organization.rb new file mode 100644 index 0000000..bbc2c68 --- /dev/null +++ b/spec/active_record/organization.rb @@ -0,0 +1,5 @@ +class Organization < Group + groupify :group_member + + has_members :managers, :organizations +end diff --git a/spec/active_record/project.rb b/spec/active_record/project.rb new file mode 100644 index 0000000..b4e922f --- /dev/null +++ b/spec/active_record/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + groupify :named_group_member +end diff --git a/spec/active_record/user.rb b/spec/active_record/user.rb new file mode 100644 index 0000000..164f618 --- /dev/null +++ b/spec/active_record/user.rb @@ -0,0 +1,7 @@ +class User < ActiveRecord::Base + groupify :group_member + groupify :named_group_member + + has_group :organizations, class_name: "Organization" + has_group :classrooms, class_name: "Classroom" +end diff --git a/spec/active_record/widget.rb b/spec/active_record/widget.rb new file mode 100644 index 0000000..81d4b97 --- /dev/null +++ b/spec/active_record/widget.rb @@ -0,0 +1,3 @@ +class Widget < ActiveRecord::Base + groupify :group_member +end From 38bdff07cdee13f79bf68036462b8605577dfd46 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 5 Aug 2017 00:48:27 -0400 Subject: [PATCH 100/205] Indicate value of variable better in exception (e.g. when nil) --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- spec/active_record/manager.rb | 2 ++ spec/active_record/organization.rb | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 1872673..d5a84be 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -89,7 +89,7 @@ def has_member(association_name, options = {}) member_klass rescue NameError => ex - raise "Can't infer base class for #{member_klass}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." + raise "Can't infer base class for #{member_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 89a9b4c..610d038 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -128,7 +128,7 @@ def has_group(association_name, options = {}) }.merge(options) rescue NameError => ex - raise "Can't infer base class for #{model_klass}: #{ex.message}. Try specifying the `:source_type` option such as `has_group(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." + raise "Can't infer base class for #{model_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_group(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end def memberships_merge(merge_criteria = nil, &group_membership_filter) diff --git a/spec/active_record/manager.rb b/spec/active_record/manager.rb index 89a5f0c..cd7f582 100644 --- a/spec/active_record/manager.rb +++ b/spec/active_record/manager.rb @@ -1,2 +1,4 @@ +require_relative 'user' + class Manager < User end diff --git a/spec/active_record/organization.rb b/spec/active_record/organization.rb index bbc2c68..3f4e642 100644 --- a/spec/active_record/organization.rb +++ b/spec/active_record/organization.rb @@ -1,3 +1,5 @@ +require_relative 'group' + class Organization < Group groupify :group_member From fd36319a3e51c3dba0f4838cb1ecebd6c4495fc3 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 5 Aug 2017 01:33:59 -0400 Subject: [PATCH 101/205] Simplify --- lib/groupify.rb | 23 +++++++++++---------- lib/groupify/adapter/active_record/group.rb | 4 +--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 82e16c6..5c29f3a 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -18,17 +18,18 @@ def self.group_membership_klass end def self.infer_class_and_association_name(association_name) - klass = association_name.to_s.classify.constantize rescue nil - - association_name = if association_name.is_a?(Symbol) - association_name - elsif klass - klass.model_name.plural.to_sym - else - association_name.to_sym - end - - [klass, association_name] + begin + klass = association_name.to_s.classify.constantize + rescue StandardError => ex + puts "Error: #{ex.inspect}" + #puts ex.backtrace + end + + if !association_name.is_a?(Symbol) && klass + association_name = klass.model_name.plural + end + + [klass, association_name.to_sym] end end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index d5a84be..e0f5ac3 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -85,9 +85,7 @@ def has_member(association_name, options = {}) source_type: source_type, extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options) - - member_klass - + rescue NameError => ex raise "Can't infer base class for #{member_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end From 691a28d26390cec37f77902176ee9d4ea930b337 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 5 Aug 2017 02:05:35 -0400 Subject: [PATCH 102/205] Add option to return a string version of inferred class name rather than trying to constantize --- lib/groupify.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 5c29f3a..26f2715 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -3,11 +3,13 @@ module Groupify mattr_accessor :group_membership_class_name, :group_class_name, - :ignore_base_class_inference_errors + :ignore_base_class_inference_errors, + :ignore_association_class_inference_errors self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' self.ignore_base_class_inference_errors = true + self.ignore_association_class_inference_errors = true def self.configure yield self @@ -20,12 +22,16 @@ def self.group_membership_klass def self.infer_class_and_association_name(association_name) begin klass = association_name.to_s.classify.constantize - rescue StandardError => ex + rescue NameError => ex puts "Error: #{ex.inspect}" #puts ex.backtrace + + if Groupify.ignore_association_class_inference_errors + klass = association_name.to_s.classify + end end - if !association_name.is_a?(Symbol) && klass + if !association_name.is_a?(Symbol) && klass.is_a?(Class) association_name = klass.model_name.plural end From a18bd85b3541490c3ff59a476737eac6f7d57b78 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 5 Aug 2017 21:56:52 -0400 Subject: [PATCH 103/205] Fix variable name --- lib/groupify/adapter/active_record.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index f993b4c..42e11e1 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -134,7 +134,7 @@ def self.infer_parent_and_types(parent, default_parent_type = nil) parent_is_group = (default_parent_type == :group) else parent_is_group = parent.class.include?(Groupify::ActiveRecord::Group) - detected_modules = [detected_group, parent.class.include?(Groupify::ActiveRecord::GroupMember)].count{ |bool| bool == true } + detected_modules = [parent_is_group, parent.class.include?(Groupify::ActiveRecord::GroupMember)].count{ |bool| bool == true } if detected_modules == 0 raise "The specified record is neither group nor group member." From 435e29434d45a8a7f5d876c21b6d44c5ede38756 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 5 Aug 2017 21:57:51 -0400 Subject: [PATCH 104/205] Add `class_name` in case the default group class is a STI subclass --- lib/groupify/adapter/active_record/group_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 610d038..c07d182 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -21,7 +21,7 @@ module GroupMember dependent: :destroy, class_name: Groupify.group_membership_class_name - has_group :groups, source_type: ActiveRecord.base_class_name(@group_class_name) + has_group :groups, source_type: ActiveRecord.base_class_name(@group_class_name), class_name: @group_class_name end def in_group?(group, opts = {}) From 2cf9875563173e947119c126028716733320bef7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 01:34:06 -0400 Subject: [PATCH 105/205] Use default group class when base class can't be resolved --- lib/groupify/adapter/active_record.rb | 7 ++++--- lib/groupify/adapter/active_record/group_member.rb | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 42e11e1..edc7f7e 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -16,7 +16,7 @@ def self.quote(model_class, column_name) end # Pass in record, class, or string - def self.base_class_name(model_class) + def self.base_class_name(model_class, &default_base_class) return if model_class.nil? if model_class.is_a?(::ActiveRecord::Base) @@ -27,9 +27,10 @@ def self.base_class_name(model_class) model_class.base_class.name rescue NameError - raise unless Groupify.ignore_base_class_inference_errors + return base_class_name(yield) if block_given? + return model_class.to_s if Groupify.ignore_base_class_inference_errors - model_class.to_s + raise end def self.memberships_merge(scope, options = {}) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index c07d182..3f21517 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -117,7 +117,7 @@ def has_group(association_name, options = {}) unless options[:source_type] # only try to look up base class if needed - can cause circular dependency issue - source_type = ActiveRecord.base_class_name(model_klass) || model_klass + source_type = ActiveRecord.base_class_name(model_klass){ @group_class_name } end has_many association_name.to_sym, ->{ distinct }, { From e0567307f225d99b9e73faec6a7c9c72602e9ab0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 01:55:59 -0400 Subject: [PATCH 106/205] Make default `members` and `groups` association names configurable --- lib/groupify.rb | 4 ++++ lib/groupify/adapter/active_record/group_member.rb | 4 +++- lib/groupify/adapter/active_record/model.rb | 2 +- lib/groupify/adapter/mongoid/group_member.rb | 2 +- lib/groupify/adapter/mongoid/model.rb | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 26f2715..2fe3a05 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -3,11 +3,15 @@ module Groupify mattr_accessor :group_membership_class_name, :group_class_name, + :members_association_name, + :groups_association_name, :ignore_base_class_inference_errors, :ignore_association_class_inference_errors self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' + self.members_association_name = :members + self.groups_association_name = :groups self.ignore_base_class_inference_errors = true self.ignore_association_class_inference_errors = true diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 3f21517..f3c8f8d 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -21,7 +21,9 @@ module GroupMember dependent: :destroy, class_name: Groupify.group_membership_class_name - has_group :groups, source_type: ActiveRecord.base_class_name(@group_class_name), class_name: @group_class_name + has_group Groupify.groups_association_name.to_sym, + source_type: ActiveRecord.base_class_name(@group_class_name), + class_name: @group_class_name end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index f3168c8..b49df23 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -24,7 +24,7 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize - has_member(:members, + has_member(Groupify.members_association_name.to_sym, source_type: ActiveRecord.base_class_name(default_member_class), class_name: default_member_class.to_s ) diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index a9e31f8..a6a46fb 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -16,7 +16,7 @@ module GroupMember include MemberScopedAs included do - has_group :groups + has_group Groupify.groups_association_name.to_sym class GroupMembership include ::Mongoid::Document diff --git a/lib/groupify/adapter/mongoid/model.rb b/lib/groupify/adapter/mongoid/model.rb index 427a9f4..462a4cd 100644 --- a/lib/groupify/adapter/mongoid/model.rb +++ b/lib/groupify/adapter/mongoid/model.rb @@ -18,7 +18,7 @@ def acts_as_group(opts = {}) if (member_klass = opts.delete :default_members) self.default_member_class = member_klass.to_s.classify.constantize - has_member(:members, class_name: default_member_class.to_s) + has_member(Groupify.members_association_name.to_sym, class_name: default_member_class.to_s) end if (member_klasses = opts.delete :members) From ae3ba76a77e2c7a96ee735a6e3f97a31e98ba776 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 01:56:36 -0400 Subject: [PATCH 107/205] Abstract out extension methods --- .../active_record/association_extensions.rb | 48 ++++------------- .../active_record/collection_extensions.rb | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 lib/groupify/adapter/active_record/collection_extensions.rb diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index d344d88..1990993 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -1,30 +1,20 @@ +require 'groupify/adapter/active_record/collection_extensions' + module Groupify module ActiveRecord module AssociationExtensions - extend ActiveSupport::Concern - - def as(membership_type) - merge(Groupify.group_membership_klass.as(membership_type)) - end + include CollectionExtensions - def delete(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - def destroy(*records) - remove_children(records, :destroy, records.extract_options![:as]) + def collection + self end - # Defined to create alias methods before - # the association is extended with this module - def <<(*children) - opts = children.extract_options!.merge(exception_on_invalidation: false) - add_children(children.flatten, opts) + def collection_parent + proxy_association.owner end - def add(*children) - opts = children.extract_options!.merge(exception_on_invalidation: true) - add_children(children.flatten, opts) + def collection_parent_type + ActiveRecord.infer_parent_and_types(proxy_association)[1] end protected @@ -37,25 +27,7 @@ def add_children(children, options = {}) proxy_association.__send__(:raise_on_type_mismatch!, child) end - ActiveRecord.add_children_to_parent( - proxy_association, - children, - options - ) - end - - def remove_children(children, destruction_type, membership_type = nil) - ActiveRecord.find_memberships_for( - proxy_association, - children, - as: membership_type - ).__send__(:"#{destruction_type}_all") - - proxy_association.owner.__send__(:clear_association_cache) - - children.each{|record| record.__send__(:clear_association_cache)} - - self + super end end end diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb new file mode 100644 index 0000000..269816c --- /dev/null +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -0,0 +1,54 @@ +module Groupify + module ActiveRecord + module CollectionExtensions + def as(membership_type) + collection.merge(Groupify.group_membership_klass.as(membership_type)) + end + + def delete(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + def destroy(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + # Defined to create alias methods before + # the association is extended with this module + def <<(*children) + opts = children.extract_options!.merge(exception_on_invalidation: false) + add_children(children.flatten, opts) + end + + def add(*children) + opts = children.extract_options!.merge(exception_on_invalidation: true) + add_children(children.flatten, opts) + end + + protected + + def add_children(children, options = {}) + ActiveRecord.add_children_to_parent( + collection_parent, + children, + options.merge(parent_type: collection_parent_type) + ) + end + + def remove_children(children, destruction_type, membership_type = nil) + ActiveRecord.find_memberships_for( + collection_parent, + children, + parent_type: collection_parent_type, + as: membership_type + ).__send__(:"#{destruction_type}_all") + + collection_parent.__send__(:clear_association_cache) + + children.each{|record| record.__send__(:clear_association_cache)} + + self + end + end + end +end From cfd9b98eec0bd6796e448435b8b8aeb3a3d5b696 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 01:57:40 -0400 Subject: [PATCH 108/205] Introduce `PolymorphicChildren` collection class to --- lib/groupify/adapter/active_record.rb | 1 + lib/groupify/adapter/active_record/group.rb | 4 ++ .../adapter/active_record/group_member.rb | 12 ++-- .../active_record/polymorphic_children.rb | 66 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 lib/groupify/adapter/active_record/polymorphic_children.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index edc7f7e..0362e25 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -8,6 +8,7 @@ module ActiveRecord autoload :Group, 'groupify/adapter/active_record/group' autoload :GroupMember, 'groupify/adapter/active_record/group_member' autoload :GroupMembership, 'groupify/adapter/active_record/group_membership' + autoload :PolymorphicChildren, 'groupify/adapter/active_record/polymorphic_children' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index e0f5ac3..9ffd23b 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -24,6 +24,10 @@ module Group class_name: Groupify.group_membership_class_name end + def polymorphic_members + PolymorphicChildren.new(self, :group, &query_filter) + end + def member_classes self.class.member_classes end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index f3c8f8d..5d0a8c0 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,6 +26,10 @@ module GroupMember class_name: @group_class_name end + def polymorphic_groups(&query_filter) + PolymorphicChildren.new(self, :member, &query_filter) + end + def in_group?(group, opts = {}) return false unless group.present? @@ -42,16 +46,16 @@ def in_any_group?(*groups) def in_all_groups?(*groups) membership_type = groups.extract_options![:as] - groups.flatten.to_set.subset? self.groups.as(membership_type).to_set + groups.flatten.to_set.subset? self.polymorphic_groups.as(membership_type).to_set end def in_only_groups?(*groups) membership_type = groups.extract_options![:as] - groups.flatten.to_set == self.groups.as(membership_type).to_set + groups.flatten.to_set == self.polymorphic_groups.as(membership_type).to_set end def shares_any_group?(other, opts = {}) - in_any_group?(other.groups, opts) + in_any_group?(other.polymorphic_groups, opts) end module ClassMethods @@ -104,7 +108,7 @@ def in_other_groups(*groups) end def shares_any_group(other) - in_any_group(other.groups) + in_any_group(other.polymorphic_groups) end def has_groups(*association_names) diff --git a/lib/groupify/adapter/active_record/polymorphic_children.rb b/lib/groupify/adapter/active_record/polymorphic_children.rb new file mode 100644 index 0000000..353cf48 --- /dev/null +++ b/lib/groupify/adapter/active_record/polymorphic_children.rb @@ -0,0 +1,66 @@ +module Groupify + module ActiveRecord + class PolymorphicChildren + include Enumerable + extend Forwardable + include CollectionExtensions + + def initialize(parent, parent_type, child_class_for_builder = nil, &query_filter) + @collection_parent, @collection_parent_type = parent, parent_type + @child_type = parent_type == :group ? :member : :group + @collection = build_query(&query_filter) + end + + def each(&block) + @collection.map do |group_membership| + group_membership.__send__(@child_type).tap(&block) + end + end + + def inspect + "#<#{self.class}:0x#{self.__id__.to_s(16)} @collection_parent=#{@collection_parent.inspect} @collection_parent_type=#{@collection_parent_type.inspect} #{to_a.inspect}>" + end + + def_delegators :collection, :reload + + def as(membership_type) + @collection = super + @collection.reset + + self + end + + def count + @collection.loaded? ? @collection.size : @collection.count.keys.size + end + + alias_method :size, :count + + # When trying to create a new record for this collection, + # create it on the `member.default_groups` or `group.default_members` + # association. + def_delegators :default_association, :build, :create, :create! + def_delegators :to_a, :[] + + alias_method :to_ary, :to_a + alias_method :[], :take + alias_method :empty?, :none? + alias_method :blank?, :none? + + protected + + attr_reader :collection, :collection_parent, :collection_parent_type + + def default_association + @collection_parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + end + + def build_query(&query_filter) + query = @collection_parent.__send__(:"group_memberships_as_#{@collection_parent_type}").where.not(group_id: nil) + query = query.instance_eval(&query_filter) if block_given? + query = query.group(["#{@child_type}_id", "#{@child_type}_type"]).includes(@child_type) + query + end + end + end +end From 895b345632da6c3d4d07e12d6e9ed3b1589f55dd Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 01:58:58 -0400 Subject: [PATCH 109/205] Disabled autoloading in tests (for now) --- spec/active_record_spec.rb | 151 ++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 77 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 06b9121..2a7d9c0 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -23,70 +23,67 @@ require 'groupify/adapter/active_record' -autoload :Organization, 'active_record/organization' -autoload :Group, 'active_record/group' -autoload :User, 'active_record/user' -autoload :Manager, 'active_record/manager' -autoload :Classroom, 'active_record/classroom' -autoload :GroupMembership, 'active_record/group_membership' -autoload :Namespaced, 'active_record/namespaced' -autoload :Project, 'active_record/project' -autoload :Widget, 'active_record/widget' -autoload :CustomGroupMembership, 'active_record/custom_group_membership' -autoload :CustomUser, 'active_record/custom_user' -autoload :CustomGroup, 'active_record/custom_group' - -# class Organization < Group -# groupify :group_member -# -# has_members :managers, :organizations -# end -# -# class Manager < User -# end -# -# class User < ActiveRecord::Base -# groupify :group_member -# groupify :named_group_member -# -# has_group :organizations, class_name: "Organization" -# has_group :classrooms, class_name: "Classroom" -# end -# -# class Manager < User -# end -# -# class Widget < ActiveRecord::Base -# groupify :group_member -# end -# -# module Namespaced -# class Member < ActiveRecord::Base -# groupify :group_member -# end -# end -# -# class Project < ActiveRecord::Base -# groupify :named_group_member -# end -# -# class Group < ActiveRecord::Base -# groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users -# end -# -# class Organization < Group -# groupify :group_member -# -# has_members :managers, :organizations -# end -# -# class GroupMembership < ActiveRecord::Base -# groupify :group_membership -# end -# -# class Classroom < ActiveRecord::Base -# groupify :group -# end +def debug_sql + logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, Logger.new(STDOUT) + yield + ActiveRecord::Base.logger = logger +end + +# autoload :User, 'active_record/user' +# autoload :Manager, 'active_record/manager' +# autoload :Widget, 'active_record/widget' +# autoload :Namespaced, 'active_record/namespaced' +# autoload :Project, 'active_record/project' +# autoload :Group, 'active_record/group' +# autoload :Organization, 'active_record/organization' +# autoload :GroupMembership, 'active_record/group_membership' +# autoload :Classroom, 'active_record/classroom' +# autoload :CustomGroupMembership, 'active_record/custom_group_membership' +# autoload :CustomUser, 'active_record/custom_user' +# autoload :CustomGroup, 'active_record/custom_group' + +class User < ActiveRecord::Base + groupify :group_member + groupify :named_group_member + + has_group :organizations, class_name: "Organization" + has_group :classrooms, class_name: "Classroom" +end + +class Manager < User +end + +class Widget < ActiveRecord::Base + groupify :group_member +end + +module Namespaced + class Member < ActiveRecord::Base + groupify :group_member + end +end + +class Project < ActiveRecord::Base + groupify :named_group_member +end + +class Group < ActiveRecord::Base + groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users +end + +class Organization < Group + groupify :group_member + + has_members :managers, :organizations +end + +class GroupMembership < ActiveRecord::Base + groupify :group_membership +end + +class Classroom < ActiveRecord::Base + groupify :group +end describe Group do it { should respond_to :members} @@ -167,18 +164,18 @@ config.group_membership_class_name = 'CustomGroupMembership' end - # class CustomGroupMembership < ActiveRecord::Base - # groupify :group_membership - # end - # - # class CustomUser < ActiveRecord::Base - # groupify :group_member - # groupify :named_group_member - # end - # - # class CustomGroup < ActiveRecord::Base - # groupify :group, members: [:custom_users] - # end + class CustomGroupMembership < ActiveRecord::Base + groupify :group_membership + end + + class CustomUser < ActiveRecord::Base + groupify :group_member + groupify :named_group_member + end + + class CustomGroup < ActiveRecord::Base + groupify :group, members: [:custom_users] + end end after do @@ -341,7 +338,7 @@ class ProjectMember < ActiveRecord::Base expect(user.groups.count).to eq(2) expect(user.groups.first).to be_a(Organization) - expect(user.groups.second).to be_a(Group) + expect(user.groups[1]).to be_a(Group) end end From 39bd88b54ed4a88ef41275df7570b5b082d2e2c8 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:04:04 -0400 Subject: [PATCH 110/205] Remove unused argument --- lib/groupify/adapter/active_record/polymorphic_children.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_children.rb b/lib/groupify/adapter/active_record/polymorphic_children.rb index 353cf48..90e7db5 100644 --- a/lib/groupify/adapter/active_record/polymorphic_children.rb +++ b/lib/groupify/adapter/active_record/polymorphic_children.rb @@ -5,7 +5,7 @@ class PolymorphicChildren extend Forwardable include CollectionExtensions - def initialize(parent, parent_type, child_class_for_builder = nil, &query_filter) + def initialize(parent, parent_type, &query_filter) @collection_parent, @collection_parent_type = parent, parent_type @child_type = parent_type == :group ? :member : :group @collection = build_query(&query_filter) From 3e3acb08de99597701a6beaef3d77cb1800c84d4 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:27:38 -0400 Subject: [PATCH 111/205] Split out polymorphic classes for multiple uses --- lib/groupify/adapter/active_record.rb | 3 +- lib/groupify/adapter/active_record/group.rb | 2 +- .../adapter/active_record/group_member.rb | 2 +- .../adapter/active_record/group_membership.rb | 8 +++ .../active_record/polymorphic_children.rb | 66 ------------------- .../active_record/polymorphic_collection.rb | 47 +++++++++++++ .../active_record/polymorphic_relation.rb | 37 +++++++++++ 7 files changed, 96 insertions(+), 69 deletions(-) delete mode 100644 lib/groupify/adapter/active_record/polymorphic_children.rb create mode 100644 lib/groupify/adapter/active_record/polymorphic_collection.rb create mode 100644 lib/groupify/adapter/active_record/polymorphic_relation.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 0362e25..8f0d950 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -8,7 +8,8 @@ module ActiveRecord autoload :Group, 'groupify/adapter/active_record/group' autoload :GroupMember, 'groupify/adapter/active_record/group_member' autoload :GroupMembership, 'groupify/adapter/active_record/group_membership' - autoload :PolymorphicChildren, 'groupify/adapter/active_record/polymorphic_children' + autoload :PolymorphicCollection, 'groupify/adapter/active_record/polymorphic_collection' + autoload :PolymorphicRelation, 'groupify/adapter/active_record/polymorphic_relation' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 9ffd23b..ad65d7c 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -25,7 +25,7 @@ module Group end def polymorphic_members - PolymorphicChildren.new(self, :group, &query_filter) + PolymorphicRelation.new(self, :group, &query_filter) end def member_classes diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 5d0a8c0..a682b46 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -27,7 +27,7 @@ module GroupMember end def polymorphic_groups(&query_filter) - PolymorphicChildren.new(self, :member, &query_filter) + PolymorphicRelation.new(self, :member, &query_filter) end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 1499919..9a54e75 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -42,6 +42,14 @@ def as(membership_type) membership_type.present? ? where(membership_type: membership_type.to_s) : all end + def polymorphic_groups(&query_filter) + PolymorphicCollection.new(:group){ merge(self).instance_eval(&query_filter) } + end + + def polymorphic_members(&query_filter) + PolymorphicCollection.new(:member){ merge(self).instance_eval(&query_filter) } + end + def for_groups(groups) for_polymorphic(:group, groups) end diff --git a/lib/groupify/adapter/active_record/polymorphic_children.rb b/lib/groupify/adapter/active_record/polymorphic_children.rb deleted file mode 100644 index 90e7db5..0000000 --- a/lib/groupify/adapter/active_record/polymorphic_children.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Groupify - module ActiveRecord - class PolymorphicChildren - include Enumerable - extend Forwardable - include CollectionExtensions - - def initialize(parent, parent_type, &query_filter) - @collection_parent, @collection_parent_type = parent, parent_type - @child_type = parent_type == :group ? :member : :group - @collection = build_query(&query_filter) - end - - def each(&block) - @collection.map do |group_membership| - group_membership.__send__(@child_type).tap(&block) - end - end - - def inspect - "#<#{self.class}:0x#{self.__id__.to_s(16)} @collection_parent=#{@collection_parent.inspect} @collection_parent_type=#{@collection_parent_type.inspect} #{to_a.inspect}>" - end - - def_delegators :collection, :reload - - def as(membership_type) - @collection = super - @collection.reset - - self - end - - def count - @collection.loaded? ? @collection.size : @collection.count.keys.size - end - - alias_method :size, :count - - # When trying to create a new record for this collection, - # create it on the `member.default_groups` or `group.default_members` - # association. - def_delegators :default_association, :build, :create, :create! - def_delegators :to_a, :[] - - alias_method :to_ary, :to_a - alias_method :[], :take - alias_method :empty?, :none? - alias_method :blank?, :none? - - protected - - attr_reader :collection, :collection_parent, :collection_parent_type - - def default_association - @collection_parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) - end - - def build_query(&query_filter) - query = @collection_parent.__send__(:"group_memberships_as_#{@collection_parent_type}").where.not(group_id: nil) - query = query.instance_eval(&query_filter) if block_given? - query = query.group(["#{@child_type}_id", "#{@child_type}_type"]).includes(@child_type) - query - end - end - end -end diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb new file mode 100644 index 0000000..3c73594 --- /dev/null +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -0,0 +1,47 @@ +module Groupify + module ActiveRecord + class PolymorphicCollection + include Enumerable + extend Forwardable + + def initialize(source, &query_filter) + @source = source + @query = build_query(&query_filter) + end + + def each(&block) + @query.map do |group_membership| + group_membership.__send__(@source).tap(&block) + end + end + + def inspect + "#<#{self.class}:0x#{self.__id__.to_s(16)} #{to_a.inspect}>" + end + + def_delegators :@query, :reload + + def count + @query.loaded? ? @query.size : @query.count.keys.size + end + + alias_method :size, :count + + def_delegators :to_a, :[] + + alias_method :to_ary, :to_a + alias_method :[], :to_a + alias_method :empty?, :none? + alias_method :blank?, :none? + + protected + + def build_query(&query_filter) + query = Groupify.group_membership_klass.where.not(:"#{@child_type}_id" => nil) + query = query.instance_eval(&query_filter) if block_given? + query = query.group(["#{@child_type}_id", "#{@child_type}_type"]).includes(@child_type) + query + end + end + end +end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb new file mode 100644 index 0000000..3aea9e3 --- /dev/null +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -0,0 +1,37 @@ +module Groupify + module ActiveRecord + class PolymorphicRelation < PolymorphicCollection + include CollectionExtensions + + def initialize(parent, parent_type, &query_filter) + @collection_parent, @collection_parent_type = parent, parent_type + @child_type = parent_type == :group ? :member : :group + + super(@child_type) + end + + def as(membership_type) + @query = super + + self + end + + # When trying to create a new record for this collection, + # create it on the `member.default_groups` or `group.default_members` + # association. + def_delegators :default_association, :build, :create, :create! + + protected + + attr_reader :collection_parent, :collection_parent_type + + def collection + @query + end + + def default_association + @collection_parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + end + end + end +end From bae0e024803ea22dc4167826b288e0de289f1e33 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:36:24 -0400 Subject: [PATCH 112/205] Properly merge queries --- lib/groupify/adapter/active_record/group.rb | 4 ++-- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- lib/groupify/adapter/active_record/group_membership.rb | 8 ++++---- .../adapter/active_record/polymorphic_collection.rb | 2 +- .../adapter/active_record/polymorphic_relation.rb | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index ad65d7c..0207c41 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -25,7 +25,7 @@ module Group end def polymorphic_members - PolymorphicRelation.new(self, :group, &query_filter) + PolymorphicRelation.new(self, :group){ |query| query.merge(group_memberships_as_group) } end def member_classes @@ -89,7 +89,7 @@ def has_member(association_name, options = {}) source_type: source_type, extend: Groupify::ActiveRecord::AssociationExtensions }.merge(options) - + rescue NameError => ex raise "Can't infer base class for #{member_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index a682b46..a2410dd 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,8 +26,8 @@ module GroupMember class_name: @group_class_name end - def polymorphic_groups(&query_filter) - PolymorphicRelation.new(self, :member, &query_filter) + def polymorphic_groups + PolymorphicRelation.new(self, :member){ |query| query.merge(group_memberships_as_member) } end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 9a54e75..26aae96 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -42,12 +42,12 @@ def as(membership_type) membership_type.present? ? where(membership_type: membership_type.to_s) : all end - def polymorphic_groups(&query_filter) - PolymorphicCollection.new(:group){ merge(self).instance_eval(&query_filter) } + def polymorphic_groups + PolymorphicCollection.new(:group){ |query| query.merge(self) } end - def polymorphic_members(&query_filter) - PolymorphicCollection.new(:member){ merge(self).instance_eval(&query_filter) } + def polymorphic_members + PolymorphicCollection.new(:member){ |query| query.merge(self) } end def for_groups(groups) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 3c73594..a1776f3 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -38,7 +38,7 @@ def count def build_query(&query_filter) query = Groupify.group_membership_klass.where.not(:"#{@child_type}_id" => nil) - query = query.instance_eval(&query_filter) if block_given? + query = yield(query) if block_given? query = query.group(["#{@child_type}_id", "#{@child_type}_type"]).includes(@child_type) query end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 3aea9e3..1ac7525 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -7,7 +7,7 @@ def initialize(parent, parent_type, &query_filter) @collection_parent, @collection_parent_type = parent, parent_type @child_type = parent_type == :group ? :member : :group - super(@child_type) + super(@child_type, &query_filter) end def as(membership_type) From 788ef79829679e2ca9b3a1362933fefddf2e761c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:40:37 -0400 Subject: [PATCH 113/205] Allow modifying query --- lib/groupify/adapter/active_record/group.rb | 4 ++-- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 0207c41..7063e12 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -24,8 +24,8 @@ module Group class_name: Groupify.group_membership_class_name end - def polymorphic_members - PolymorphicRelation.new(self, :group){ |query| query.merge(group_memberships_as_group) } + def polymorphic_members(&query_filter) + PolymorphicRelation.new(self, :group){ |query| query_filter.call(query.merge(group_memberships_as_group)) } end def member_classes diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index a2410dd..7c71250 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,8 +26,8 @@ module GroupMember class_name: @group_class_name end - def polymorphic_groups - PolymorphicRelation.new(self, :member){ |query| query.merge(group_memberships_as_member) } + def polymorphic_groups(&query_filter) + PolymorphicRelation.new(self, :member){ |query| query_filter.call(query.merge(group_memberships_as_member)) } end def in_group?(group, opts = {}) From d1ed8f639bb39b1d7d0e86ce793807b387b7373c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:43:51 -0400 Subject: [PATCH 114/205] Crate a relation --- lib/groupify/adapter/active_record/group_membership.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 26aae96..da83e21 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -43,11 +43,11 @@ def as(membership_type) end def polymorphic_groups - PolymorphicCollection.new(:group){ |query| query.merge(self) } + PolymorphicCollection.new(:group){ |query| query.merge(all) } end def polymorphic_members - PolymorphicCollection.new(:member){ |query| query.merge(self) } + PolymorphicCollection.new(:member){ |query| query.merge(all) } end def for_groups(groups) From b6485b6c41b5bed6d00066e24ae662aae44568c5 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:44:04 -0400 Subject: [PATCH 115/205] Scope properly --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 7063e12..dbfe87a 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -25,7 +25,7 @@ module Group end def polymorphic_members(&query_filter) - PolymorphicRelation.new(self, :group){ |query| query_filter.call(query.merge(group_memberships_as_group)) } + PolymorphicRelation.new(self, :group){ |query| query.merge(group_memberships_as_group).instance_eval(&query_filter) } end def member_classes diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 7c71250..8dc2409 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -27,7 +27,7 @@ module GroupMember end def polymorphic_groups(&query_filter) - PolymorphicRelation.new(self, :member){ |query| query_filter.call(query.merge(group_memberships_as_member)) } + PolymorphicRelation.new(self, :member){ |query| query.merge(group_memberships_as_member).instance_eval(&query_filter) } end def in_group?(group, opts = {}) From 74c6b579ea7ceb0dee3481990ba23da689b4b268 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:45:30 -0400 Subject: [PATCH 116/205] Fix variable name --- lib/groupify/adapter/active_record/polymorphic_collection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index a1776f3..61c1077 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -37,9 +37,9 @@ def count protected def build_query(&query_filter) - query = Groupify.group_membership_klass.where.not(:"#{@child_type}_id" => nil) + query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) query = yield(query) if block_given? - query = query.group(["#{@child_type}_id", "#{@child_type}_type"]).includes(@child_type) + query = query.group(["#{@source}_id", "#{@source}_type"]).includes(@source) query end end From 900ce840588b87e7beec462209a964d2e15bbd84 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:54:52 -0400 Subject: [PATCH 117/205] Fix query nesting --- lib/groupify/adapter/active_record/group.rb | 2 +- lib/groupify/adapter/active_record/group_member.rb | 2 +- lib/groupify/adapter/active_record/polymorphic_relation.rb | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index dbfe87a..3d40f91 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -25,7 +25,7 @@ module Group end def polymorphic_members(&query_filter) - PolymorphicRelation.new(self, :group){ |query| query.merge(group_memberships_as_group).instance_eval(&query_filter) } + PolymorphicRelation.new(self, :group, &query_filter) end def member_classes diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 8dc2409..a682b46 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -27,7 +27,7 @@ module GroupMember end def polymorphic_groups(&query_filter) - PolymorphicRelation.new(self, :member){ |query| query.merge(group_memberships_as_member).instance_eval(&query_filter) } + PolymorphicRelation.new(self, :member, &query_filter) end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 1ac7525..81b79f7 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -7,7 +7,11 @@ def initialize(parent, parent_type, &query_filter) @collection_parent, @collection_parent_type = parent, parent_type @child_type = parent_type == :group ? :member : :group - super(@child_type, &query_filter) + super(@child_type) do |query| + query = query.merge(parent.__send__(:"group_memberships_as_#{@child_type}")) + query = query.instance_eval(&query_filter) if block_given? + query + end end def as(membership_type) From 619661938b7525459762261b5269bc24bff05b84 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 02:56:57 -0400 Subject: [PATCH 118/205] Fix type --- lib/groupify/adapter/active_record/polymorphic_relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 81b79f7..aae9c0f 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -8,7 +8,7 @@ def initialize(parent, parent_type, &query_filter) @child_type = parent_type == :group ? :member : :group super(@child_type) do |query| - query = query.merge(parent.__send__(:"group_memberships_as_#{@child_type}")) + query = query.merge(parent.__send__(:"group_memberships_as_#{parent_type}")) query = query.instance_eval(&query_filter) if block_given? query end From a43048f2e5da0feea8518e88f19d36d467a347e2 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 03:04:53 -0400 Subject: [PATCH 119/205] Add `pretty_print` --- lib/groupify/adapter/active_record/polymorphic_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 61c1077..75e1bd8 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -27,7 +27,7 @@ def count alias_method :size, :count - def_delegators :to_a, :[] + def_delegators :to_a, :[], :pretty_print alias_method :to_ary, :to_a alias_method :[], :to_a From f76d1b316bd41da5ccab28e49bf106f0694d4fe7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 03:23:01 -0400 Subject: [PATCH 120/205] Rename block argument for consistency --- lib/groupify/adapter/active_record/group.rb | 4 ++-- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- .../adapter/active_record/polymorphic_collection.rb | 6 +++--- lib/groupify/adapter/active_record/polymorphic_relation.rb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 3d40f91..f911df2 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -24,8 +24,8 @@ module Group class_name: Groupify.group_membership_class_name end - def polymorphic_members(&query_filter) - PolymorphicRelation.new(self, :group, &query_filter) + def polymorphic_members(&group_membership_filter) + PolymorphicRelation.new(self, :group, &group_membership_filter) end def member_classes diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index a682b46..6e58ecf 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,8 +26,8 @@ module GroupMember class_name: @group_class_name end - def polymorphic_groups(&query_filter) - PolymorphicRelation.new(self, :member, &query_filter) + def polymorphic_groups(&group_membership_filter) + PolymorphicRelation.new(self, :member, &group_membership_filter) end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 75e1bd8..9aec97d 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -4,9 +4,9 @@ class PolymorphicCollection include Enumerable extend Forwardable - def initialize(source, &query_filter) + def initialize(source, &group_membership_filter) @source = source - @query = build_query(&query_filter) + @query = build_query(&group_membership_filter) end def each(&block) @@ -36,7 +36,7 @@ def count protected - def build_query(&query_filter) + def build_query(&group_membership_filter) query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) query = yield(query) if block_given? query = query.group(["#{@source}_id", "#{@source}_type"]).includes(@source) diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index aae9c0f..ed70253 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -3,13 +3,13 @@ module ActiveRecord class PolymorphicRelation < PolymorphicCollection include CollectionExtensions - def initialize(parent, parent_type, &query_filter) + def initialize(parent, parent_type, &group_membership_filter) @collection_parent, @collection_parent_type = parent, parent_type @child_type = parent_type == :group ? :member : :group super(@child_type) do |query| query = query.merge(parent.__send__(:"group_memberships_as_#{parent_type}")) - query = query.instance_eval(&query_filter) if block_given? + query = query.instance_eval(&group_membership_filter) if block_given? query end end From a895ea4658a24e4bab769bb63553d690bddd46e7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 03:25:37 -0400 Subject: [PATCH 121/205] Use `instance_eval` for consistency --- lib/groupify/adapter/active_record/group_membership.rb | 4 ++-- lib/groupify/adapter/active_record/polymorphic_collection.rb | 2 +- lib/groupify/adapter/active_record/polymorphic_relation.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index da83e21..99b8205 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -43,11 +43,11 @@ def as(membership_type) end def polymorphic_groups - PolymorphicCollection.new(:group){ |query| query.merge(all) } + PolymorphicCollection.new(:group){ merge(all) } end def polymorphic_members - PolymorphicCollection.new(:member){ |query| query.merge(all) } + PolymorphicCollection.new(:member){ merge(all) } end def for_groups(groups) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 9aec97d..9253717 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -38,7 +38,7 @@ def count def build_query(&group_membership_filter) query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) - query = yield(query) if block_given? + query = query.instance_eval(&group_membership_filter) if block_given? query = query.group(["#{@source}_id", "#{@source}_type"]).includes(@source) query end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index ed70253..9ed195f 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -7,8 +7,8 @@ def initialize(parent, parent_type, &group_membership_filter) @collection_parent, @collection_parent_type = parent, parent_type @child_type = parent_type == :group ? :member : :group - super(@child_type) do |query| - query = query.merge(parent.__send__(:"group_memberships_as_#{parent_type}")) + super(@child_type) do + query = merge(parent.__send__(:"group_memberships_as_#{parent_type}")) query = query.instance_eval(&group_membership_filter) if block_given? query end From 92f513bc466e91fd30501c287ef160a7cb0bb88d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 03:32:48 -0400 Subject: [PATCH 122/205] Fix up some consistency in the code --- .../adapter/active_record/named_group_collection.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index 123d991..b9d2ccf 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -12,7 +12,7 @@ def initialize(member) def add(named_group, opts = {}) named_group = named_group.to_sym - membership_type = opts[:as].present? ? opts[:as].to_s : nil + membership_type = opts[:as].to_s if opts[:as].present? # always add a nil membership type and then a specific one (if specified) membership_types = [nil, membership_type].uniq @@ -54,15 +54,15 @@ def include?(named_group, opts = {}) end def delete(*named_groups) - opts = named_groups.extract_options! + membership_type = named_groups.extract_options![:as] - remove(named_groups.flatten.compact, :delete_all, opts[:as]) + remove(named_groups.flatten.compact, :delete_all, membership_type) end def destroy(*named_groups) - opts = named_groups.extract_options! + membership_type = named_groups.extract_options![:as] - remove(named_groups.flatten.compact, :destroy_all, opts[:as]) + remove(named_groups.flatten.compact, :destroy_all, membership_type) end def clear @@ -78,7 +78,7 @@ def as(membership_type) if membership_type.present? @named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym) else - to_a + to_a.map(&:to_sym) end end From 9fd75bd88308ed10ff63325d85439bf1e8df0fbe Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 03:38:58 -0400 Subject: [PATCH 123/205] Remove duplicated class definitions --- spec/active_record_spec.rb | 66 +++++++------------------------------- 1 file changed, 12 insertions(+), 54 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 2a7d9c0..8221843 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -42,48 +42,15 @@ def debug_sql # autoload :CustomUser, 'active_record/custom_user' # autoload :CustomGroup, 'active_record/custom_group' -class User < ActiveRecord::Base - groupify :group_member - groupify :named_group_member - - has_group :organizations, class_name: "Organization" - has_group :classrooms, class_name: "Classroom" -end - -class Manager < User -end - -class Widget < ActiveRecord::Base - groupify :group_member -end - -module Namespaced - class Member < ActiveRecord::Base - groupify :group_member - end -end - -class Project < ActiveRecord::Base - groupify :named_group_member -end - -class Group < ActiveRecord::Base - groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users -end - -class Organization < Group - groupify :group_member - - has_members :managers, :organizations -end - -class GroupMembership < ActiveRecord::Base - groupify :group_membership -end - -class Classroom < ActiveRecord::Base - groupify :group -end +require_relative './active_record/user' +require_relative './active_record/manager' +require_relative './active_record/widget' +require_relative './active_record/namespaced/member' +require_relative './active_record/project' +require_relative './active_record/group' +require_relative './active_record/organization' +require_relative './active_record/group_membership' +require_relative './active_record/classroom' describe Group do it { should respond_to :members} @@ -164,18 +131,9 @@ class Classroom < ActiveRecord::Base config.group_membership_class_name = 'CustomGroupMembership' end - class CustomGroupMembership < ActiveRecord::Base - groupify :group_membership - end - - class CustomUser < ActiveRecord::Base - groupify :group_member - groupify :named_group_member - end - - class CustomGroup < ActiveRecord::Base - groupify :group, members: [:custom_users] - end + require_relative './active_record/custom_group_membership' + require_relative './active_record/custom_user' + require_relative './active_record/custom_group' end after do From b59feaf54a2e514a8a55fe3d4cf3543cfc2ec651 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 04:09:07 -0400 Subject: [PATCH 124/205] Fix postgres by changing from `GROUP BY` to `DISTINCT ON` --- .../adapter/active_record/polymorphic_collection.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 9253717..667b456 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -39,7 +39,16 @@ def count def build_query(&group_membership_filter) query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) query = query.instance_eval(&group_membership_filter) if block_given? - query = query.group(["#{@source}_id", "#{@source}_type"]).includes(@source) + query = query.includes(@source) + query = case ::ActiveRecord::Base.connection.adapter_name.downcase + when /postgres/, /pg/ + id_column = ActiveRecord.quote(Groupify.group_membership_klass, "#{@source}_id") + type_column = ActiveRecord.quote(Groupify.group_membership_klass, "#{@source}_type") + query.select("DISTINCT ON (#{id_column}, #{type_column}) *") + else #when /mysql/, /sqlite/ + query.group(["#{@source}_id", "#{@source}_type"]) + end + query end end From a8a22f710dd5cda67b4338c1c7b6d148e2f7bb1d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 04:20:03 -0400 Subject: [PATCH 125/205] Added test to make sure polymorphic groups are unique --- spec/active_record_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 8221843..df5aced 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -113,6 +113,16 @@ def debug_sql expect(user.groups.size).to eq(0) end + + it "doesn't select duplicate groups" do + group.add user, as: 'manager' + group.add user, as: 'user' + classroom.add user + + expect(user.polymorphic_groups.count).to eq(2) + expect(user.polymorphic_groups.to_a.size).to eq(2) + expect(user.groups.count).to eq(1) + end end end end From 7ca1813bb282bbc04af2a47ad0f04f43dd1b3e9d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 04:20:34 -0400 Subject: [PATCH 126/205] Make sure that `count` is correct based on PostgreSQL DISTINCT vs. GROUP BY --- .../adapter/active_record/polymorphic_collection.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 667b456..d1d8b97 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -22,7 +22,13 @@ def inspect def_delegators :@query, :reload def count - @query.loaded? ? @query.size : @query.count.keys.size + return @query.size if @query.loaded? + + queried_count = @query.count + # The `count` is a Hash when GROUP BY is used + # PostgreSQL uses DISTINCT ON, which may be different + queried_count = queried_count.keys.size if queried_count.is_a?(Hash) + queried_count end alias_method :size, :count @@ -48,7 +54,7 @@ def build_query(&group_membership_filter) else #when /mysql/, /sqlite/ query.group(["#{@source}_id", "#{@source}_type"]) end - + query end end From d1686b16faac4830851cfacbf0ae35c325f7cbdd Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 10:44:13 -0400 Subject: [PATCH 127/205] Make sure memberships are added properly based on role --- lib/groupify/adapter/active_record.rb | 7 +++---- spec/active_record_spec.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 8f0d950..26f4a89 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -70,13 +70,12 @@ def self.add_children_to_parent(parent, children, options = {}) to_add_directly = [] to_add_with_membership_type = [] - already_children = find_memberships_for(parent, children, parent_type: parent_type).includes(child_type).map(&child_type).uniq - children -= already_children - + already_children = find_memberships_for(parent, children, parent_type: parent_type).includes(child_type).group_by{ |membership| membership.__send__(child_type) } + # first prepare changes children.each do |child| # add to collection without membership type - to_add_directly << memberships_association.build(child_type => child) + to_add_directly << memberships_association.build(child_type => child) unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } # add a second entry for the given membership type if membership_type.present? membership = memberships_association. diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index df5aced..4610beb 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -123,6 +123,16 @@ def debug_sql expect(user.polymorphic_groups.to_a.size).to eq(2) expect(user.groups.count).to eq(1) end + + it "adds based on membership_type" do + group.add user + group.add user, as: 'manager' + organization.add user + organization.add user, as: 'owner' + + expect(user.polymorphic_groups.count).to eq(2) + expect(user.group_memberships_as_member.count).to eq(4) + end end end end From c592f9380cec03af62809dbe064d437082e8740b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 22:21:59 -0400 Subject: [PATCH 128/205] Moved parent/child logic into `ParentProxy` class to consolidate and simplify --- lib/groupify/adapter/active_record.rb | 111 +----------------- .../active_record/association_extensions.rb | 12 +- .../active_record/collection_extensions.rb | 24 ++-- lib/groupify/adapter/active_record/group.rb | 12 +- .../adapter/active_record/group_member.rb | 8 +- .../active_record/named_group_member.rb | 2 +- .../adapter/active_record/parent_proxy.rb | 90 ++++++++++++++ .../active_record/polymorphic_relation.rb | 17 ++- 8 files changed, 132 insertions(+), 144 deletions(-) create mode 100644 lib/groupify/adapter/active_record/parent_proxy.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 26f4a89..c1f07ef 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -10,6 +10,7 @@ module ActiveRecord autoload :GroupMembership, 'groupify/adapter/active_record/group_membership' autoload :PolymorphicCollection, 'groupify/adapter/active_record/polymorphic_collection' autoload :PolymorphicRelation, 'groupify/adapter/active_record/polymorphic_relation' + autoload :ParentProxy, 'groupify/adapter/active_record/parent_proxy' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' @@ -35,9 +36,7 @@ def self.base_class_name(model_class, &default_base_class) raise end - def self.memberships_merge(scope, options = {}) - parent, parent_type, _ = infer_parent_and_types(scope, options[:parent_type]) - + def self.memberships_merge(parent, parent_type, options = {}) criteria = [parent.joins(:"group_memberships_as_#{parent_type}")] criteria << options[:criteria] if options[:criteria] criteria << Groupify.group_membership_klass.instance_eval(&options[:filter]) if options[:filter] @@ -45,111 +44,5 @@ def self.memberships_merge(scope, options = {}) # merge all criteria together criteria.compact.reduce(:merge) end - - def self.find_memberships_for(parent, children, options = {}) - parent, parent_type, child_type = infer_parent_and_types(parent, options[:parent_type]) - - parent. - __send__(:"group_memberships_as_#{parent_type}"). - __send__(:"for_#{child_type}s", children). - as(options[:as]) - end - - def self.add_children_to_parent(parent, children, options = {}) - parent, parent_type, child_type = infer_parent_and_types(parent, options[:parent_type]) - - membership_type = options[:as] - exception_on_invalidation = options[:exception_on_invalidation] - - return parent if children.none? - - parent.__send__(:clear_association_cache) - - memberships_association = parent.__send__(:"group_memberships_as_#{parent_type}") - - to_add_directly = [] - to_add_with_membership_type = [] - - already_children = find_memberships_for(parent, children, parent_type: parent_type).includes(child_type).group_by{ |membership| membership.__send__(child_type) } - - # first prepare changes - children.each do |child| - # add to collection without membership type - to_add_directly << memberships_association.build(child_type => child) unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } - # add a second entry for the given membership type - if membership_type.present? - membership = memberships_association. - merge(child.__send__(:"group_memberships_as_#{child_type}")). - as(membership_type). - first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - - child.__send__(:clear_association_cache) - end - - parent.__send__(:clear_association_cache) - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |child| - next if child.valid? - - if exception_on_invalidation - raise ::ActiveRecord::RecordInvalid.new(child) - else - return false - end - end - - # create memberships without membership type - memberships_association << to_add_directly - - # create memberships with membership type - to_add_with_membership_type. - group_by{ |membership| membership.__send__(parent_type) }. - each do |membership_parent, memberships| - membership_parent.__send__(:"group_memberships_as_#{parent_type}") << memberships - membership_parent.__send__(:clear_association_cache) - end - - parent - end - - protected - - # Takes an association or model as the parent. If a model - # is passed in, the `default_parent_type` option needs - # to be passed in if the model is both a group and group member. - # - # Can't detect based on included `Group` or `GroupMember` - # modules because a model can be both a group and a gorup member. - def self.infer_parent_and_types(parent, default_parent_type = nil) - parent_is_group = true - - # Association assumed to be a `has_many through` - if parent.respond_to?(:through_reflection) - parent_is_group = (parent.through_reflection.name == :group_memberships_as_group) - parent = parent.owner - elsif default_parent_type - parent_is_group = (default_parent_type == :group) - else - parent_is_group = parent.class.include?(Groupify::ActiveRecord::Group) - detected_modules = [parent_is_group, parent.class.include?(Groupify::ActiveRecord::GroupMember)].count{ |bool| bool == true } - - if detected_modules == 0 - raise "The specified record is neither group nor group member." - elsif detected_modules == 2 - raise "Can't infer whether record should be treated as group or group member because it is configured as both. Pass the `default_parent_type` option to specify which it should be treated as." - end - end - - if parent_is_group - [parent, :group, :member] - else - [parent, :member, :group] - end - end end end diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 1990993..b08ea3b 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -9,12 +9,8 @@ def collection self end - def collection_parent - proxy_association.owner - end - - def collection_parent_type - ActiveRecord.infer_parent_and_types(proxy_association)[1] + def parent_proxy + @parent_proxy ||= ParentProxy.new(proxy_association.owner, parent_type) end protected @@ -29,6 +25,10 @@ def add_children(children, options = {}) super end + + def parent_type + @parent_type ||= proxy_association.through_reflection.name == :group_memberships_as_group ? :group : :member + end end end end diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index 269816c..eeed053 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -25,25 +25,23 @@ def add(*children) add_children(children.flatten, opts) end + def collection + raise "Not implemented" + end + + def parent_proxy + raise "Not implemented" + end + protected def add_children(children, options = {}) - ActiveRecord.add_children_to_parent( - collection_parent, - children, - options.merge(parent_type: collection_parent_type) - ) + parent_proxy.add_children(children, options) end def remove_children(children, destruction_type, membership_type = nil) - ActiveRecord.find_memberships_for( - collection_parent, - children, - parent_type: collection_parent_type, - as: membership_type - ).__send__(:"#{destruction_type}_all") - - collection_parent.__send__(:clear_association_cache) + parent_proxy.find_memberships_for(children, as: membership_type).__send__(:"#{destruction_type}_all") + parent_proxy.clear_association_cache children.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index f911df2..513bfda 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -24,8 +24,12 @@ module Group class_name: Groupify.group_membership_class_name end + def group_proxy + @group_proxy ||= ParentProxy.new(self, :group) + end + def polymorphic_members(&group_membership_filter) - PolymorphicRelation.new(self, :group, &group_membership_filter) + PolymorphicRelation.new(group_proxy, &group_membership_filter) end def member_classes @@ -33,9 +37,9 @@ def member_classes end def add(*members) - opts = members.extract_options!.merge(parent_type: :group) + opts = members.extract_options! - ActiveRecord.add_children_to_parent(self, members.flatten, opts) + group_proxy.add_children(members.flatten, opts) self end @@ -116,7 +120,7 @@ def merge!(source_group, destination_group) protected def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, parent_type: :group, criteria: merge_criteria, filter: group_membership_filter) + ActiveRecord.memberships_merge(self, :group, criteria: merge_criteria, filter: group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 6e58ecf..8e0bbda 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,8 +26,12 @@ module GroupMember class_name: @group_class_name end + def member_proxy + @member_proxy ||= ParentProxy.new(self, :member) + end + def polymorphic_groups(&group_membership_filter) - PolymorphicRelation.new(self, :member, &group_membership_filter) + PolymorphicRelation.new(member_proxy, &group_membership_filter) end def in_group?(group, opts = {}) @@ -138,7 +142,7 @@ def has_group(association_name, options = {}) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, parent_type: :member, criteria: merge_criteria, filter: group_membership_filter) + ActiveRecord.memberships_merge(self, :member, criteria: merge_criteria, filter: group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 7c4bd98..998f7ff 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -101,7 +101,7 @@ def shares_any_named_group(other) end def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, parent_type: :member, criteria: merge_criteria, filter: group_membership_filter) + ActiveRecord.memberships_merge(self, :member, criteria: merge_criteria, filter: group_membership_filter) end end end diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb new file mode 100644 index 0000000..f059305 --- /dev/null +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -0,0 +1,90 @@ +module Groupify + module ActiveRecord + class ParentProxy + + attr_reader :parent_type, :child_type + + def initialize(parent, parent_type) + @parent, @parent_type = parent, parent_type + @child_type = parent_type == :group ? :member : :group + end + + def find_memberships_for(children, options = {}) + memberships_association.__send__(:"for_#{@child_type}s", children).as(options[:as]) + end + + def add_children(children, options = {}) + return @parent if children.none? + + clear_association_cache + + membership_type = options[:as] + exception_on_invalidation = options[:exception_on_invalidation] + + to_add_directly = [] + to_add_with_membership_type = [] + + already_children = find_memberships_for(children).includes(@child_type).group_by{ |membership| membership.__send__(@child_type) } + + # first prepare changes + children.each do |child| + # add to collection without membership type + unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } + to_add_directly << memberships_association.build(@child_type => child) + end + + # add a second entry for the given membership type + if membership_type.present? + membership = memberships_association. + merge(child.__send__(:"group_memberships_as_#{@child_type}")). + as(membership_type). + first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + + child.__send__(:clear_association_cache) + end + + clear_association_cache + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |child| + next if child.valid? + + if exception_on_invalidation + raise ::ActiveRecord::RecordInvalid.new(child) + else + return false + end + end + + # create memberships without membership type + memberships_association << to_add_directly + + # create memberships with membership type + to_add_with_membership_type. + group_by{ |membership| membership.__send__(@parent_type) }. + each do |membership_parent, memberships| + membership_parent.__send__(:"group_memberships_as_#{@parent_type}") << memberships + membership_parent.__send__(:clear_association_cache) + end + + @parent + end + + def children_association + @parent_proxy.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + end + + def memberships_association + @parent.__send__(:"group_memberships_as_#{@parent_type}") + end + + def clear_association_cache + @parent.__send__(:clear_association_cache) + end + end + end +end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 9ed195f..4b71a15 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -3,12 +3,11 @@ module ActiveRecord class PolymorphicRelation < PolymorphicCollection include CollectionExtensions - def initialize(parent, parent_type, &group_membership_filter) - @collection_parent, @collection_parent_type = parent, parent_type - @child_type = parent_type == :group ? :member : :group + def initialize(parent_proxy, &group_membership_filter) + @parent_proxy = parent_proxy - super(@child_type) do - query = merge(parent.__send__(:"group_memberships_as_#{parent_type}")) + super(parent_proxy.child_type) do + query = merge(parent_proxy.memberships_association) query = query.instance_eval(&group_membership_filter) if block_given? query end @@ -25,16 +24,16 @@ def as(membership_type) # association. def_delegators :default_association, :build, :create, :create! - protected - - attr_reader :collection_parent, :collection_parent_type + attr_reader :parent_proxy def collection @query end + protected + def default_association - @collection_parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + @parent_proxy.children_association end end end From 09f19645945d5e3d91088f36fe7a4a32ad2a735c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 6 Aug 2017 22:22:39 -0400 Subject: [PATCH 129/205] Fix for group deletion deleting group membership after merge... not sure why this is happening now - no logic changes. --- lib/groupify/adapter/active_record/group.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 513bfda..cbdccba 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -113,6 +113,9 @@ def merge!(source_group, destination_group) group_id: destination_group.id, group_type: ActiveRecord.base_class_name(destination_group) ) + + destination_group.__send__(:clear_association_cache) + source_group.__send__(:clear_association_cache) source_group.destroy end end From 09cadcb2cfd2c5b8a5225e49ba8e5670fc3bbdd9 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 00:02:37 -0400 Subject: [PATCH 130/205] Simplify class --- .../adapter/active_record/association_extensions.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index b08ea3b..2ac8a62 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -10,25 +10,21 @@ def collection end def parent_proxy - @parent_proxy ||= ParentProxy.new(proxy_association.owner, parent_type) + @parent_proxy ||= ParentProxy.new(proxy_association.owner, proxy_association.through_reflection.name.match(/_(group|member)$/)[1].to_sym) end protected + # Throw an exception here when adding direction to an association + # because when adding the children to the parent this won't + # happen because the group membership is polymorphic. def add_children(children, options = {}) - # Throw an exception here when adding direction to an association - # because when adding the children to the parent this won't - # happen because the group membership is polymorphic. children.each do |child| proxy_association.__send__(:raise_on_type_mismatch!, child) end super end - - def parent_type - @parent_type ||= proxy_association.through_reflection.name == :group_memberships_as_group ? :group : :member - end end end end From 083509eb13194e4b1c7a370a10f2de607305fa14 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 00:40:56 -0400 Subject: [PATCH 131/205] Move query logic to `ParentQueryBuilder` class --- lib/groupify/adapter/active_record.rb | 16 +++--- lib/groupify/adapter/active_record/group.rb | 11 ++-- .../adapter/active_record/group_member.rb | 31 +++++------ .../adapter/active_record/group_membership.rb | 4 +- .../active_record/named_group_member.rb | 20 +++---- .../adapter/active_record/parent_proxy.rb | 5 +- .../active_record/parent_query_builder.rb | 53 +++++++++++++++++++ .../active_record/polymorphic_collection.rb | 14 ++--- 8 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 lib/groupify/adapter/active_record/parent_query_builder.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index c1f07ef..0943fcb 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -11,10 +11,15 @@ module ActiveRecord autoload :PolymorphicCollection, 'groupify/adapter/active_record/polymorphic_collection' autoload :PolymorphicRelation, 'groupify/adapter/active_record/polymorphic_relation' autoload :ParentProxy, 'groupify/adapter/active_record/parent_proxy' + autoload :ParentQueryBuilder, 'groupify/adapter/active_record/parent_query_builder' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' - def self.quote(model_class, column_name) + def self.is_db?(*strings) + strings.any?{ |string| ::ActiveRecord::Base.connection.adapter_name.downcase.include?(string) } + end + + def self.quote(column_name, model_class = Groupify.group_membership_klass) "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" end @@ -35,14 +40,5 @@ def self.base_class_name(model_class, &default_base_class) raise end - - def self.memberships_merge(parent, parent_type, options = {}) - criteria = [parent.joins(:"group_memberships_as_#{parent_type}")] - criteria << options[:criteria] if options[:criteria] - criteria << Groupify.group_membership_klass.instance_eval(&options[:filter]) if options[:filter] - - # merge all criteria together - criteria.compact.reduce(:merge) - end end end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index cbdccba..aed8f59 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -51,8 +51,7 @@ def merge!(source) module ClassMethods def with_member(member) - memberships_merge(member.group_memberships_as_member). - extending(Groupify::ActiveRecord::AssociationExtensions) + group_query.merge_children(member) end def default_member_class @@ -102,7 +101,7 @@ def has_member(association_name, options = {}) def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = source_group.member_classes - destination_group.member_classes - invalid_found = invalid_member_classes.any?{ |klass| klass.memberships_merge(source_group.group_memberships_as_group).count > 0 } + invalid_found = invalid_member_classes.any?{ |klass| klass.member_query.merge_children(source_group).count > 0 } if invalid_found raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") @@ -120,10 +119,8 @@ def merge!(source_group, destination_group) end end - protected - - def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, :group, criteria: merge_criteria, filter: group_membership_filter) + def group_query + @group_query ||= ParentQueryBuilder.new(self, :group) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 8e0bbda..0a9401b 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -64,16 +64,16 @@ def shares_any_group?(other, opts = {}) module ClassMethods def as(membership_type) - memberships_merge{as(membership_type)} + member_query.as(membership_type) end def in_group(group) - group.present? ? memberships_merge(group.group_memberships_as_group).distinct : none + group.present? ? member_query.merge_children(group).distinct : none end def in_any_group(*groups) groups.flatten! - groups.present? ? memberships_merge{for_groups(groups)}.distinct : none + groups.present? ? member_query.merge_children(groups).distinct : none end def in_all_groups(*groups) @@ -81,18 +81,13 @@ def in_all_groups(*groups) return none unless groups.present? - group_id_column = ActiveRecord.quote(Groupify.group_membership_klass, 'group_id') - group_type_column = ActiveRecord.quote(Groupify.group_membership_klass, 'group_type') + id = ActiveRecord.quote('group_id') + type = ActiveRecord.quote('group_type') # Count distinct on ID and type combo - concatenated_columns = case connection.adapter_name.downcase - when /sqlite/ - "#{group_id_column} || #{group_type_column}" - else #when /mysql/, /postgres/, /pg/ - "CONCAT(#{group_id_column}, #{group_type_column})" - end - - memberships_merge{for_groups(groups)}. - group(ActiveRecord.quote(self, 'id')). + concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" + + member_query.merge_children(groups). + group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct end @@ -103,12 +98,12 @@ def in_only_groups(*groups) return none unless groups.present? in_all_groups(*groups). - where.not(id: in_other_groups(*groups).select(ActiveRecord.quote(self, 'id'))). + where.not(id: in_other_groups(*groups).select(ActiveRecord.quote('id', self))). distinct end def in_other_groups(*groups) - memberships_merge{not_for_groups(groups)} + member_query.merge_children_without(groups) end def shares_any_group(other) @@ -141,8 +136,8 @@ def has_group(association_name, options = {}) raise "Can't infer base class for #{model_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_group(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." end - def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, :member, criteria: merge_criteria, filter: group_membership_filter) + def member_query + @member_query ||= ParentQueryBuilder.new(self, :member) end end end diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 99b8205..ad69bbd 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -43,11 +43,11 @@ def as(membership_type) end def polymorphic_groups - PolymorphicCollection.new(:group){ merge(all) } + PolymorphicCollection.new(:group){merge(all)} end def polymorphic_members - PolymorphicCollection.new(:member){ merge(all) } + PolymorphicCollection.new(:member){merge(all)} end def for_groups(groups) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 998f7ff..91e390f 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -57,29 +57,29 @@ def shares_any_named_group?(other, opts = {}) module ClassMethods def as(membership_type) - memberships_merge{as(membership_type)} + named_member_query.as(membership_type) end def in_named_group(named_group) return none unless named_group.present? - memberships_merge{where(group_name: named_group)}.distinct + named_member_query.merge_memberships{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - memberships_merge{where(group_name: named_groups.flatten)}.distinct + named_member_query.merge_memberships{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - memberships_merge{where(group_name: named_groups)}. - group(ActiveRecord.quote(self, 'id')). - having("COUNT(DISTINCT #{ActiveRecord.quote(Groupify.group_membership_klass, 'group_name')}) = ?", named_groups.count). + named_member_query.merge_memberships{where(group_name: named_groups)}. + group(ActiveRecord.quote('id', self)). + having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct end @@ -88,20 +88,20 @@ def in_only_named_groups(*named_groups) return none unless named_groups.present? in_all_named_groups(*named_groups). - where.not(id: in_other_named_groups(*named_groups).select(ActiveRecord.quote(self, 'id'))). + where.not(id: in_other_named_groups(*named_groups).select(ActiveRecord.quote('id', self))). distinct end def in_other_named_groups(*named_groups) - memberships_merge{where.not(group_name: named_groups)} + named_member_query.merge_memberships{where.not(group_name: named_groups)} end def shares_any_named_group(other) in_any_named_group(other.named_groups.to_a) end - def memberships_merge(merge_criteria = nil, &group_membership_filter) - ActiveRecord.memberships_merge(self, :member, criteria: merge_criteria, filter: group_membership_filter) + def named_member_query + @named_member_query ||= ParentQueryBuilder.new(self, :member) end end end diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index f059305..31f8883 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -19,7 +19,6 @@ def add_children(children, options = {}) clear_association_cache membership_type = options[:as] - exception_on_invalidation = options[:exception_on_invalidation] to_add_directly = [] to_add_with_membership_type = [] @@ -32,7 +31,7 @@ def add_children(children, options = {}) unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } to_add_directly << memberships_association.build(@child_type => child) end - + # add a second entry for the given membership type if membership_type.present? membership = memberships_association. @@ -53,7 +52,7 @@ def add_children(children, options = {}) list_to_validate.each do |child| next if child.valid? - if exception_on_invalidation + if options[:exception_on_invalidation] raise ::ActiveRecord::RecordInvalid.new(child) else return false diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb new file mode 100644 index 0000000..65774a3 --- /dev/null +++ b/lib/groupify/adapter/active_record/parent_query_builder.rb @@ -0,0 +1,53 @@ +module Groupify + module ActiveRecord + class ParentQueryBuilder < SimpleDelegator + def initialize(scope, parent_type) + @scope = scope.extending(Groupify::ActiveRecord::AssociationExtensions) + @parent_type = parent_type + @child_type = parent_type == :group ? :member : :group + + super(@scope) + end + + def as(membership_type) + merge_memberships{as(membership_type)} + end + + def merge_children(child_or_children) + scope = if child_or_children.is_a?(::ActiveRecord::Base) + # single child + merge_memberships(criteria: child_or_children.__send__(:"group_memberships_as_#{@child_type}")) + else + method_name = :"for_#{@child_type}s" + merge_memberships{__send__(method_name, child_or_children)} + end + + if block_given? + scope = scope.merge_memberships(&group_membership_filter) + end + + scope + end + + def merge_children_without(children) + method_name = :"not_for_#{@child_type}s" + merge_memberships{__send__(method_name, children)} + end + + def merge_memberships(options = {}, &group_membership_filter) + criteria = [@scope.joins(:"group_memberships_as_#{@parent_type}")] + criteria << options[:criteria] if options[:criteria] + criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? + + # merge all criteria together + wrap criteria.compact.reduce(:merge) + end + + protected + + def wrap(scope) + self.class.new(scope, @parent_type) + end + end + end +end diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index d1d8b97..8e594e5 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -46,13 +46,13 @@ def build_query(&group_membership_filter) query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) query = query.instance_eval(&group_membership_filter) if block_given? query = query.includes(@source) - query = case ::ActiveRecord::Base.connection.adapter_name.downcase - when /postgres/, /pg/ - id_column = ActiveRecord.quote(Groupify.group_membership_klass, "#{@source}_id") - type_column = ActiveRecord.quote(Groupify.group_membership_klass, "#{@source}_type") - query.select("DISTINCT ON (#{id_column}, #{type_column}) *") - else #when /mysql/, /sqlite/ - query.group(["#{@source}_id", "#{@source}_type"]) + + id, type = "#{@source}_id", "#{@source}_type" + + query = if ActiveRecord.is_db?('postgres', 'pg') + query.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") + else + query.group([id, type]) end query From e0b7037d901326a733c5c61ee138d4f8b673e795 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 00:47:56 -0400 Subject: [PATCH 132/205] Clarify logic --- .../active_record/association_extensions.rb | 4 ---- .../active_record/collection_extensions.rb | 2 +- .../active_record/polymorphic_collection.rb | 16 +++++++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 2ac8a62..3b72ec8 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -5,10 +5,6 @@ module ActiveRecord module AssociationExtensions include CollectionExtensions - def collection - self - end - def parent_proxy @parent_proxy ||= ParentProxy.new(proxy_association.owner, proxy_association.through_reflection.name.match(/_(group|member)$/)[1].to_sym) end diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index eeed053..c4d7044 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -26,7 +26,7 @@ def add(*children) end def collection - raise "Not implemented" + self end def parent_proxy diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 8e594e5..e5604ee 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -47,15 +47,17 @@ def build_query(&group_membership_filter) query = query.instance_eval(&group_membership_filter) if block_given? query = query.includes(@source) - id, type = "#{@source}_id", "#{@source}_type" + distinct(query) + end - query = if ActiveRecord.is_db?('postgres', 'pg') - query.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") - else - query.group([id, type]) - end + def distinct(query) + id, type = "#{@source}_id", "#{@source}_type" - query + if ActiveRecord.is_db?('postgres', 'pg') + query.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") + else + query.group([id, type]) + end end end end From 57b46ea579c603c1f0b9e0336cfe662e8bea0e2d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 01:31:19 -0400 Subject: [PATCH 133/205] DRY up `has_many` creation --- lib/groupify/adapter/active_record.rb | 29 +++++++++++++++++-- lib/groupify/adapter/active_record/group.rb | 27 ++++++----------- .../adapter/active_record/group_member.rb | 26 ++++++----------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 0943fcb..471d5f5 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -20,11 +20,11 @@ def self.is_db?(*strings) end def self.quote(column_name, model_class = Groupify.group_membership_klass) - "#{model_class.quoted_table_name}.#{::ActiveRecord::Base.connection.quote_column_name(column_name)}" + "#{model_class.quoted_table_name}.#{model_class.connection.quote_column_name(column_name)}" end # Pass in record, class, or string - def self.base_class_name(model_class, &default_base_class) + def self.base_class_name(model_class, default_base_class = nil) return if model_class.nil? if model_class.is_a?(::ActiveRecord::Base) @@ -35,10 +35,33 @@ def self.base_class_name(model_class, &default_base_class) model_class.base_class.name rescue NameError - return base_class_name(yield) if block_given? + return base_class_name(default_base_class) if default_base_class return model_class.to_s if Groupify.ignore_base_class_inference_errors raise end + + def self.create_association(klass, association_name, options = {}) + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + default_base_class = options.delete(:default_base_class) + + model_klass = options[:class_name] || association_class || default_base_class + + # only try to look up base class if needed - can cause circular dependency issue + options[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) + + klass.has_many association_name, ->{ distinct }, {extend: Groupify::ActiveRecord::AssociationExtensions}.merge(options) + + model_klass + rescue NameError => ex + re = /has_(group|member)/ + line = ex.backtrace.find{ |i| i =~ re } + + message = ["Can't infer base class for #{parent_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option"] + message << "such as `#{line.match(re)[0]}(#{association_name.inspect}, source_type: 'BaseClass')`" if line + message << "in case there is a circular dependency." + + raise message.join(' ') + end end end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index aed8f59..b563191 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -55,7 +55,7 @@ def with_member(member) end def default_member_class - @default_member_class ||= (User rescue false) + @default_member_class ||= (User rescue nil) end def default_member_class=(klass) @@ -75,26 +75,17 @@ def has_members(*association_names) end def has_member(association_name, options = {}) - association_class, association_name = Groupify.infer_class_and_association_name(association_name) - model_klass = options[:class_name] || association_class - member_klass = model_klass.to_s.constantize - - (@member_klasses ||= Set.new) << member_klass - - unless options[:source_type] - # only try to look up base class if needed - can cause circular dependency issue - source_type = ActiveRecord.base_class_name(member_klass) || member_klass || default_member_class - end - - has_many association_name, ->{ distinct }, { + member_klass = ActiveRecord.create_association(self, association_name, + options.merge( through: :group_memberships_as_group, source: :member, - source_type: source_type, - extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(options) + default_base_class: default_member_class + ) + ) + + (@member_klasses ||= Set.new) << member_klass.to_s.constantize - rescue NameError => ex - raise "Can't infer base class for #{member_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_member(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." + self end # Merge two groups. The members of the source become members of the destination, and the source is destroyed. diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 0a9401b..8cf38a0 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -117,23 +117,15 @@ def has_groups(*association_names) end def has_group(association_name, options = {}) - association_class, association_name = Groupify.infer_class_and_association_name(association_name) - model_klass = options[:class_name] || association_class || @group_class_name - - unless options[:source_type] - # only try to look up base class if needed - can cause circular dependency issue - source_type = ActiveRecord.base_class_name(model_klass){ @group_class_name } - end - - has_many association_name.to_sym, ->{ distinct }, { - through: :group_memberships_as_member, - source: :group, - source_type: source_type, - extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(options) - - rescue NameError => ex - raise "Can't infer base class for #{model_klass.inspect}: #{ex.message}. Try specifying the `:source_type` option such as `has_group(#{association_name.inspect}, source_type: 'BaseClass')` in case there is a circular dependency." + ActiveRecord.create_association(self, association_name, + options.merge( + through: :group_memberships_as_member, + source: :group, + default_base_class: @group_class_name + ) + ) + + self end def member_query From 3dd6891ec301fa2c92547cd24b18d00f3a5a5695 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 02:01:06 -0400 Subject: [PATCH 134/205] Rename "query" to "scope" --- lib/groupify/adapter/active_record.rb | 10 +++--- .../active_record/association_extensions.rb | 5 ++- lib/groupify/adapter/active_record/group.rb | 10 +++--- .../adapter/active_record/group_member.rb | 19 +++++------ .../active_record/named_group_member.rb | 14 ++++---- .../active_record/polymorphic_collection.rb | 34 +++++++++---------- .../active_record/polymorphic_relation.rb | 10 ++---- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 471d5f5..76805a2 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -41,18 +41,20 @@ def self.base_class_name(model_class, default_base_class = nil) raise end - def self.create_association(klass, association_name, options = {}) + def self.create_children_association(klass, association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) default_base_class = options.delete(:default_base_class) - - model_klass = options[:class_name] || association_class || default_base_class + model_klass = options[:class_name] || association_class || default_base_class # only try to look up base class if needed - can cause circular dependency issue options[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) - klass.has_many association_name, ->{ distinct }, {extend: Groupify::ActiveRecord::AssociationExtensions}.merge(options) + klass.has_many association_name, ->{ distinct }, { + extend: Groupify::ActiveRecord::AssociationExtensions + }.merge(options) model_klass + rescue NameError => ex re = /has_(group|member)/ line = ex.backtrace.find{ |i| i =~ re } diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 3b72ec8..a280229 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -6,7 +6,10 @@ module AssociationExtensions include CollectionExtensions def parent_proxy - @parent_proxy ||= ParentProxy.new(proxy_association.owner, proxy_association.through_reflection.name.match(/_(group|member)$/)[1].to_sym) + @parent_proxy ||= ParentProxy.new( + proxy_association.owner, + proxy_association.through_reflection.name.match(/_(group|member)$/)[1].to_sym + ) end protected diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index b563191..8460012 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -51,7 +51,7 @@ def merge!(source) module ClassMethods def with_member(member) - group_query.merge_children(member) + group_scope.merge_children(member) end def default_member_class @@ -75,7 +75,7 @@ def has_members(*association_names) end def has_member(association_name, options = {}) - member_klass = ActiveRecord.create_association(self, association_name, + member_klass = ActiveRecord.create_children_association(self, association_name, options.merge( through: :group_memberships_as_group, source: :member, @@ -92,7 +92,7 @@ def has_member(association_name, options = {}) def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = source_group.member_classes - destination_group.member_classes - invalid_found = invalid_member_classes.any?{ |klass| klass.member_query.merge_children(source_group).count > 0 } + invalid_found = invalid_member_classes.any?{ |klass| klass.member_scope.merge_children(source_group).count > 0 } if invalid_found raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") @@ -110,8 +110,8 @@ def merge!(source_group, destination_group) end end - def group_query - @group_query ||= ParentQueryBuilder.new(self, :group) + def group_scope + @group_scope ||= ParentQueryBuilder.new(self, :group) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 8cf38a0..a45c43e 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -64,16 +64,16 @@ def shares_any_group?(other, opts = {}) module ClassMethods def as(membership_type) - member_query.as(membership_type) + member_scope.as(membership_type) end def in_group(group) - group.present? ? member_query.merge_children(group).distinct : none + group.present? ? member_scope.merge_children(group).distinct : none end def in_any_group(*groups) groups.flatten! - groups.present? ? member_query.merge_children(groups).distinct : none + groups.present? ? member_scope.merge_children(groups).distinct : none end def in_all_groups(*groups) @@ -81,12 +81,11 @@ def in_all_groups(*groups) return none unless groups.present? - id = ActiveRecord.quote('group_id') - type = ActiveRecord.quote('group_type') + id, type = ActiveRecord.quote('group_id'), ActiveRecord.quote('group_type') # Count distinct on ID and type combo concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" - member_query.merge_children(groups). + member_scope.merge_children(groups). group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct @@ -103,7 +102,7 @@ def in_only_groups(*groups) end def in_other_groups(*groups) - member_query.merge_children_without(groups) + member_scope.merge_children_without(groups) end def shares_any_group(other) @@ -117,7 +116,7 @@ def has_groups(*association_names) end def has_group(association_name, options = {}) - ActiveRecord.create_association(self, association_name, + ActiveRecord.create_children_association(self, association_name, options.merge( through: :group_memberships_as_member, source: :group, @@ -128,8 +127,8 @@ def has_group(association_name, options = {}) self end - def member_query - @member_query ||= ParentQueryBuilder.new(self, :member) + def member_scope + @member_scope ||= ParentQueryBuilder.new(self, :member) end end end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 91e390f..c12978f 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -57,27 +57,27 @@ def shares_any_named_group?(other, opts = {}) module ClassMethods def as(membership_type) - named_member_query.as(membership_type) + named_member_scope.as(membership_type) end def in_named_group(named_group) return none unless named_group.present? - named_member_query.merge_memberships{where(group_name: named_group)}.distinct + named_member_scope.merge_memberships{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_query.merge_memberships{where(group_name: named_groups.flatten)}.distinct + named_member_scope.merge_memberships{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_query.merge_memberships{where(group_name: named_groups)}. + named_member_scope.merge_memberships{where(group_name: named_groups)}. group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct @@ -93,15 +93,15 @@ def in_only_named_groups(*named_groups) end def in_other_named_groups(*named_groups) - named_member_query.merge_memberships{where.not(group_name: named_groups)} + named_member_scope.merge_memberships{where.not(group_name: named_groups)} end def shares_any_named_group(other) in_any_named_group(other.named_groups.to_a) end - def named_member_query - @named_member_query ||= ParentQueryBuilder.new(self, :member) + def named_member_scope + @named_member_scope ||= ParentQueryBuilder.new(self, :member) end end end diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index e5604ee..2eb2003 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -6,25 +6,21 @@ class PolymorphicCollection def initialize(source, &group_membership_filter) @source = source - @query = build_query(&group_membership_filter) + @collection = build_collection(&group_membership_filter) end def each(&block) - @query.map do |group_membership| + @collection.map do |group_membership| group_membership.__send__(@source).tap(&block) end end - def inspect - "#<#{self.class}:0x#{self.__id__.to_s(16)} #{to_a.inspect}>" - end - - def_delegators :@query, :reload + def_delegators :@collection, :reload def count - return @query.size if @query.loaded? + return @collection.size if @collection.loaded? - queried_count = @query.count + queried_count = @collection.count # The `count` is a Hash when GROUP BY is used # PostgreSQL uses DISTINCT ON, which may be different queried_count = queried_count.keys.size if queried_count.is_a?(Hash) @@ -40,23 +36,27 @@ def count alias_method :empty?, :none? alias_method :blank?, :none? + def inspect + "#<#{self.class}:0x#{self.__id__.to_s(16)} #{to_a.inspect}>" + end + protected - def build_query(&group_membership_filter) - query = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) - query = query.instance_eval(&group_membership_filter) if block_given? - query = query.includes(@source) + def build_collection(&group_membership_filter) + collection = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) + collection = collection.instance_eval(&group_membership_filter) if block_given? + collection = collection.includes(@source) - distinct(query) + distinct(collection) end - def distinct(query) + def distinct(collection) id, type = "#{@source}_id", "#{@source}_type" if ActiveRecord.is_db?('postgres', 'pg') - query.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") + collection.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") else - query.group([id, type]) + collection.group([id, type]) end end end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 4b71a15..1794b3a 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -5,7 +5,7 @@ class PolymorphicRelation < PolymorphicCollection def initialize(parent_proxy, &group_membership_filter) @parent_proxy = parent_proxy - + super(parent_proxy.child_type) do query = merge(parent_proxy.memberships_association) query = query.instance_eval(&group_membership_filter) if block_given? @@ -14,7 +14,7 @@ def initialize(parent_proxy, &group_membership_filter) end def as(membership_type) - @query = super + @collection = super self end @@ -24,11 +24,7 @@ def as(membership_type) # association. def_delegators :default_association, :build, :create, :create! - attr_reader :parent_proxy - - def collection - @query - end + attr_reader :collection, :parent_proxy protected From 679da0ef35b703591addce1bb48739f8f53e56ed Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 7 Aug 2017 02:07:18 -0400 Subject: [PATCH 135/205] determine child type --- lib/groupify/adapter/active_record/association_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index a280229..1cdebe9 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -8,7 +8,7 @@ module AssociationExtensions def parent_proxy @parent_proxy ||= ParentProxy.new( proxy_association.owner, - proxy_association.through_reflection.name.match(/_(group|member)$/)[1].to_sym + proxy_association.through_reflection.name == :group_memberships_as_group ? :group : :member ) end From 96a588f1e256206f7426829dc79e597175750353 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 21:25:45 -0400 Subject: [PATCH 136/205] Fix Rails 4.0-5.0 tests using `extending` on model class when chaining scopes --- lib/groupify/adapter/active_record/parent_query_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb index 65774a3..02887c3 100644 --- a/lib/groupify/adapter/active_record/parent_query_builder.rb +++ b/lib/groupify/adapter/active_record/parent_query_builder.rb @@ -2,7 +2,7 @@ module Groupify module ActiveRecord class ParentQueryBuilder < SimpleDelegator def initialize(scope, parent_type) - @scope = scope.extending(Groupify::ActiveRecord::AssociationExtensions) + @scope = scope.all.extending(Groupify::ActiveRecord::AssociationExtensions) @parent_type = parent_type @child_type = parent_type == :group ? :member : :group From 0d57f0a8853099183166d6704ae1819194956a29 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 22:21:50 -0400 Subject: [PATCH 137/205] Fix DISTINCT queries in PostgreSQL for SELECT vs COUNT --- .../active_record/polymorphic_collection.rb | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 2eb2003..07ebfb0 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -10,7 +10,7 @@ def initialize(source, &group_membership_filter) end def each(&block) - @collection.map do |group_membership| + distinct_compat.map do |group_membership| group_membership.__send__(@source).tap(&block) end end @@ -18,13 +18,7 @@ def each(&block) def_delegators :@collection, :reload def count - return @collection.size if @collection.loaded? - - queried_count = @collection.count - # The `count` is a Hash when GROUP BY is used - # PostgreSQL uses DISTINCT ON, which may be different - queried_count = queried_count.keys.size if queried_count.is_a?(Hash) - queried_count + @collection.loaded? ? @collection.size : count_compat end alias_method :size, :count @@ -46,18 +40,33 @@ def build_collection(&group_membership_filter) collection = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) collection = collection.instance_eval(&group_membership_filter) if block_given? collection = collection.includes(@source) + end + + def distinct_compat + id, type = ActiveRecord.quote("#{@source}_id"), ActiveRecord.quote("#{@source}_type") - distinct(collection) + # Workaround to "group by" multiple columns in PostgreSQL + if ActiveRecord.is_db?('postgres') + @collection.select("DISTINCT ON (#{id}, #{type}) *") + else + @collection.group([id, type]) + end end - def distinct(collection) - id, type = "#{@source}_id", "#{@source}_type" + def count_compat + # Workaround to "count distinct" on multiple columns in PostgreSQL + # (uses different syntax when aggregating distinct) + if ActiveRecord.is_db?('postgres') + id, type = ActiveRecord.quote("#{@source}_id"), ActiveRecord.quote("#{@source}_type") - if ActiveRecord.is_db?('postgres', 'pg') - collection.select("DISTINCT ON (#{ActiveRecord.quote(id)}, #{ActiveRecord.quote(type)}) *") + queried_count = @collection.select("DISTINCT (#{id}, #{type})").count else - collection.group([id, type]) + queried_count = distinct_compat.count + # The `count` is a Hash when GROUP BY is used + queried_count = queried_count.keys.size if queried_count.is_a?(Hash) end + + queried_count end end end From 039a94ad805159c10ee6c3018db4c951ee0355cd Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 22:31:06 -0400 Subject: [PATCH 138/205] Drop support for Rails 4.0 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1d6ad88..84ba2ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ rvm: - jruby-9.1.9.0 #- rubinius-3 gemfile: - - gemfiles/rails_4.0.gemfile + #- gemfiles/rails_4.0.gemfile - gemfiles/rails_4.1.gemfile - gemfiles/rails_4.2.gemfile - gemfiles/rails_5.0.gemfile From 1100d09dea092af7ab66042f2c930935f0b33ec8 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 23:28:22 -0400 Subject: [PATCH 139/205] Use helper to build query --- lib/groupify/adapter/active_record/group_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index a45c43e..9582dbe 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -38,7 +38,7 @@ def in_group?(group, opts = {}) return false unless group.present? group_memberships_as_member. - merge(group.group_memberships_as_group). + for_groups(group). as(opts[:as]). exists? end From b0dbaa637fab202b80d145184dd2e412a2487209 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 23:28:49 -0400 Subject: [PATCH 140/205] DRY up code with helpers --- .../adapter/active_record/parent_proxy.rb | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 31f8883..fcd1f0c 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -35,13 +35,13 @@ def add_children(children, options = {}) # add a second entry for the given membership type if membership_type.present? membership = memberships_association. - merge(child.__send__(:"group_memberships_as_#{@child_type}")). + merge(memberships_association_for(child, @child_type)). as(membership_type). first_or_initialize to_add_with_membership_type << membership unless membership.persisted? end - child.__send__(:clear_association_cache) + clear_association_cache_for(child) end clear_association_cache @@ -66,8 +66,8 @@ def add_children(children, options = {}) to_add_with_membership_type. group_by{ |membership| membership.__send__(@parent_type) }. each do |membership_parent, memberships| - membership_parent.__send__(:"group_memberships_as_#{@parent_type}") << memberships - membership_parent.__send__(:clear_association_cache) + memberships_association_for(membership_parent, @parent_type) << memberships + clear_association_cache_for(membership_parent) end @parent @@ -78,11 +78,21 @@ def children_association end def memberships_association - @parent.__send__(:"group_memberships_as_#{@parent_type}") + memberships_association_for(@parent, @parent_type) end def clear_association_cache - @parent.__send__(:clear_association_cache) + clear_association_cache_for(@parent) + end + + private + + def memberships_association_for(record, source) + record.__send__(:"group_memberships_as_#{source}") + end + + def clear_association_cache_for(record) + record.__send__(:clear_association_cache) end end end From ba6015d468313ba76eae6e78a8dd720cb4db4f3d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 9 Aug 2017 23:29:08 -0400 Subject: [PATCH 141/205] Make default member class configurable --- lib/groupify.rb | 2 ++ lib/groupify/adapter/active_record/group.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 2fe3a05..5b96e86 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -3,6 +3,7 @@ module Groupify mattr_accessor :group_membership_class_name, :group_class_name, + :member_class_name, :members_association_name, :groups_association_name, :ignore_base_class_inference_errors, @@ -10,6 +11,7 @@ module Groupify self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' + self.member_class_name = 'User' self.members_association_name = :members self.groups_association_name = :groups self.ignore_base_class_inference_errors = true diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 8460012..6f1b99c 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -55,7 +55,7 @@ def with_member(member) end def default_member_class - @default_member_class ||= (User rescue nil) + @default_member_class ||= (Groupify.member_class_name.constantize rescue nil) end def default_member_class=(klass) From f3b76b8102b5333f9d60d29402ea3b45a8468490 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 00:08:53 -0400 Subject: [PATCH 142/205] Fix `merge` to use `all` scope for Rails 4.0-5.0 --- lib/groupify/adapter/active_record/collection_extensions.rb | 5 ++++- lib/groupify/adapter/active_record/group_membership.rb | 6 +++--- lib/groupify/adapter/active_record/parent_query_builder.rb | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index c4d7044..a14e892 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -40,7 +40,10 @@ def add_children(children, options = {}) end def remove_children(children, destruction_type, membership_type = nil) - parent_proxy.find_memberships_for(children, as: membership_type).__send__(:"#{destruction_type}_all") + parent_proxy. + find_memberships_for(children, as: membership_type). + __send__(:"#{destruction_type}_all") + parent_proxy.clear_association_cache children.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index ad69bbd..f26646c 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -71,11 +71,11 @@ def for_polymorphic(source, records, options = {}) when Array where(build_polymorphic_criteria_for(source, records)) when ::ActiveRecord::Relation - merge(records) + all.merge(records) when ::ActiveRecord::Base - merge(records.__send__(:"group_memberships_as_#{source}")) + all.merge(records.__send__(:"group_memberships_as_#{source}")) else - self + all end end diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb index 02887c3..9293217 100644 --- a/lib/groupify/adapter/active_record/parent_query_builder.rb +++ b/lib/groupify/adapter/active_record/parent_query_builder.rb @@ -35,7 +35,8 @@ def merge_children_without(children) end def merge_memberships(options = {}, &group_membership_filter) - criteria = [@scope.joins(:"group_memberships_as_#{@parent_type}")] + criteria = [] + criteria << @scope.joins(:"group_memberships_as_#{@parent_type}") criteria << options[:criteria] if options[:criteria] criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? From f03661755c9d0ea60552effbc8603fdd2d0e5431 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 00:25:23 -0400 Subject: [PATCH 143/205] Make public method names more user friendly --- lib/groupify/adapter/active_record/group.rb | 16 +++++++-------- .../adapter/active_record/group_member.rb | 20 +++++++++---------- .../active_record/named_group_member.rb | 14 ++++++------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 6f1b99c..0dfa307 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -24,12 +24,12 @@ module Group class_name: Groupify.group_membership_class_name end - def group_proxy - @group_proxy ||= ParentProxy.new(self, :group) + def as_group + @as_group ||= ParentProxy.new(self, :group) end def polymorphic_members(&group_membership_filter) - PolymorphicRelation.new(group_proxy, &group_membership_filter) + PolymorphicRelation.new(as_group, &group_membership_filter) end def member_classes @@ -39,7 +39,7 @@ def member_classes def add(*members) opts = members.extract_options! - group_proxy.add_children(members.flatten, opts) + as_group.add_children(members.flatten, opts) self end @@ -51,7 +51,7 @@ def merge!(source) module ClassMethods def with_member(member) - group_scope.merge_children(member) + group_finder.merge_children(member) end def default_member_class @@ -92,7 +92,7 @@ def has_member(association_name, options = {}) def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = source_group.member_classes - destination_group.member_classes - invalid_found = invalid_member_classes.any?{ |klass| klass.member_scope.merge_children(source_group).count > 0 } + invalid_found = invalid_member_classes.any?{ |klass| klass.member_finder.merge_children(source_group).count > 0 } if invalid_found raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") @@ -110,8 +110,8 @@ def merge!(source_group, destination_group) end end - def group_scope - @group_scope ||= ParentQueryBuilder.new(self, :group) + def group_finder + @group_finder ||= ParentQueryBuilder.new(self, :group) end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 9582dbe..5b8a70d 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -26,12 +26,12 @@ module GroupMember class_name: @group_class_name end - def member_proxy - @member_proxy ||= ParentProxy.new(self, :member) + def as_member + @as_member ||= ParentProxy.new(self, :member) end def polymorphic_groups(&group_membership_filter) - PolymorphicRelation.new(member_proxy, &group_membership_filter) + PolymorphicRelation.new(as_member, &group_membership_filter) end def in_group?(group, opts = {}) @@ -64,16 +64,16 @@ def shares_any_group?(other, opts = {}) module ClassMethods def as(membership_type) - member_scope.as(membership_type) + member_finder.as(membership_type) end def in_group(group) - group.present? ? member_scope.merge_children(group).distinct : none + group.present? ? member_finder.merge_children(group).distinct : none end def in_any_group(*groups) groups.flatten! - groups.present? ? member_scope.merge_children(groups).distinct : none + groups.present? ? member_finder.merge_children(groups).distinct : none end def in_all_groups(*groups) @@ -85,7 +85,7 @@ def in_all_groups(*groups) # Count distinct on ID and type combo concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" - member_scope.merge_children(groups). + member_finder.merge_children(groups). group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct @@ -102,7 +102,7 @@ def in_only_groups(*groups) end def in_other_groups(*groups) - member_scope.merge_children_without(groups) + member_finder.merge_children_without(groups) end def shares_any_group(other) @@ -127,8 +127,8 @@ def has_group(association_name, options = {}) self end - def member_scope - @member_scope ||= ParentQueryBuilder.new(self, :member) + def member_finder + @member_finder ||= ParentQueryBuilder.new(self, :member) end end end diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index c12978f..6b303ed 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -57,27 +57,27 @@ def shares_any_named_group?(other, opts = {}) module ClassMethods def as(membership_type) - named_member_scope.as(membership_type) + named_member_finder.as(membership_type) end def in_named_group(named_group) return none unless named_group.present? - named_member_scope.merge_memberships{where(group_name: named_group)}.distinct + named_member_finder.merge_memberships{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_scope.merge_memberships{where(group_name: named_groups.flatten)}.distinct + named_member_finder.merge_memberships{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_scope.merge_memberships{where(group_name: named_groups)}. + named_member_finder.merge_memberships{where(group_name: named_groups)}. group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct @@ -93,15 +93,15 @@ def in_only_named_groups(*named_groups) end def in_other_named_groups(*named_groups) - named_member_scope.merge_memberships{where.not(group_name: named_groups)} + named_member_finder.merge_memberships{where.not(group_name: named_groups)} end def shares_any_named_group(other) in_any_named_group(other.named_groups.to_a) end - def named_member_scope - @named_member_scope ||= ParentQueryBuilder.new(self, :member) + def named_member_finder + @named_member_finder ||= ParentQueryBuilder.new(self, :member) end end end From 52c528dee4eea87997bbabf81770a4a77070beb0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 00:38:01 -0400 Subject: [PATCH 144/205] Fix `children_association` parent reference --- lib/groupify/adapter/active_record/collection_extensions.rb | 4 ++-- lib/groupify/adapter/active_record/parent_proxy.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index a14e892..8cac7ad 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -41,9 +41,9 @@ def add_children(children, options = {}) def remove_children(children, destruction_type, membership_type = nil) parent_proxy. - find_memberships_for(children, as: membership_type). + find_memberships_for_children(children, as: membership_type). __send__(:"#{destruction_type}_all") - + parent_proxy.clear_association_cache children.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index fcd1f0c..353f4a7 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -9,7 +9,7 @@ def initialize(parent, parent_type) @child_type = parent_type == :group ? :member : :group end - def find_memberships_for(children, options = {}) + def find_memberships_for_children(children, options = {}) memberships_association.__send__(:"for_#{@child_type}s", children).as(options[:as]) end @@ -23,7 +23,7 @@ def add_children(children, options = {}) to_add_directly = [] to_add_with_membership_type = [] - already_children = find_memberships_for(children).includes(@child_type).group_by{ |membership| membership.__send__(@child_type) } + already_children = find_memberships_for_children(children).includes(@child_type).group_by{ |membership| membership.__send__(@child_type) } # first prepare changes children.each do |child| @@ -74,7 +74,7 @@ def add_children(children, options = {}) end def children_association - @parent_proxy.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + @parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) end def memberships_association From 273b78d47404525088c7722ec35941de2712dcd2 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 14:25:45 -0400 Subject: [PATCH 145/205] Add `superclass` helper to get inherited values for STI --- lib/groupify.rb | 15 +++++++++++++++ lib/groupify/adapter/active_record/group.rb | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 5b96e86..04f9836 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -12,7 +12,9 @@ module Groupify self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' self.member_class_name = 'User' + # Set to `false` if default association should not be created self.members_association_name = :members + # Set to `false` if default association should not be created self.groups_association_name = :groups self.ignore_base_class_inference_errors = true self.ignore_association_class_inference_errors = true @@ -25,6 +27,19 @@ def self.group_membership_klass group_membership_class_name.constantize end + # Get the value of the superclass method. + # Return a default value if the result is `nil`. + def self.superclass_fetch(klass, method_name, default_value = nil, &default_value_builder) + # recursively try to get a non-nil value + while (klass = klass.superclass).method_defined?(method_name) + superclass_value = klass.__send__(method_name) + + return superclass_value unless superclass_value.nil? + end + + block_given? ? yield : default_value + end + def self.infer_class_and_association_name(association_name) begin klass = association_name.to_s.classify.constantize diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 0dfa307..348e839 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -64,7 +64,7 @@ def default_member_class=(klass) # Returns the member classes defined for this class, as well as for the super classes def member_classes - (@member_klasses ||= Set.new).merge(superclass.method_defined?(:member_classes) ? superclass.member_classes : []) + (@member_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :member_classes, [])) end # Define which classes are members of this group From 2d8004a22f7b3ce64f2429b5df4ac1b12a82afcc Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 14:28:05 -0400 Subject: [PATCH 146/205] Use inherited class and association names --- lib/groupify/adapter/active_record/group.rb | 27 ++++++++++++++------- lib/groupify/adapter/active_record/model.rb | 24 ++++++++++++------ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 348e839..d10ae99 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -15,7 +15,8 @@ module Group extend ActiveSupport::Concern included do - @default_member_class = nil + @default_member_class_name = nil + @default_members_association_name = nil @member_klasses ||= Set.new has_many :group_memberships_as_group, @@ -54,17 +55,25 @@ def with_member(member) group_finder.merge_children(member) end - def default_member_class - @default_member_class ||= (Groupify.member_class_name.constantize rescue nil) + # Returns the member classes defined for this class, as well as for the super classes + def member_classes + (@member_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :member_classes, [])) end - def default_member_class=(klass) - @default_member_class = klass + def default_member_class_name + @default_member_class_name ||= Groupify.member_class_name end - # Returns the member classes defined for this class, as well as for the super classes - def member_classes - (@member_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :member_classes, [])) + def default_member_class_name=(klass) + @default_member_class_name = klass + end + + def default_members_association_name + @default_members_association_name ||= Groupify.members_association_name + end + + def default_members_association_name=(name) + @default_members_association_name = name && name.to_sym end # Define which classes are members of this group @@ -79,7 +88,7 @@ def has_member(association_name, options = {}) options.merge( through: :group_memberships_as_group, source: :member, - default_base_class: default_member_class + default_base_class: default_member_class_name ) ) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index b49df23..e7b99d9 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -21,17 +21,25 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group - if (member_klass = opts.delete :default_members) - self.default_member_class = member_klass.to_s.classify.constantize + # Get defaults from parent class for STI + self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) + self.default_members_association_name = Groupify.superclass_fetch(self, :default_members_association_name, Groupify.members_association_name) - has_member(Groupify.members_association_name.to_sym, - source_type: ActiveRecord.base_class_name(default_member_class), - class_name: default_member_class.to_s - ) + if (member_association_names = opts.delete :members) + has_members(member_association_names) + end + + if (default_members = opts.delete :default_members) + self.default_member_class_name = default_members.to_s.classify + # Only use as the association name if none specified (backwards-compatibility) + self.default_members_association_name ||= default_members end - if (member_klasses = opts.delete :members) - has_members(member_klasses) + if default_members_association_name + has_member(default_members_association_name, + source_type: ActiveRecord.base_class_name(default_member_class_name), + class_name: default_member_class_name + ) end end From be3ddd4d11382333f5134b84103022a84447f352 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 14:28:52 -0400 Subject: [PATCH 147/205] Standardize same options on group members as on groups --- .../adapter/active_record/group_member.rb | 25 +++++++++++++++---- lib/groupify/adapter/active_record/model.rb | 25 ++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 5b8a70d..9bdc5ab 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -15,15 +15,14 @@ module GroupMember extend ActiveSupport::Concern included do + @default_group_class_name = nil + @default_groups_association_name = nil + has_many :group_memberships_as_member, as: :member, autosave: true, dependent: :destroy, class_name: Groupify.group_membership_class_name - - has_group Groupify.groups_association_name.to_sym, - source_type: ActiveRecord.base_class_name(@group_class_name), - class_name: @group_class_name end def as_member @@ -115,12 +114,28 @@ def has_groups(*association_names) end end + def default_group_class_name + @default_group_class_name ||= Groupify.group_class_name + end + + def default_group_class_name=(klass) + @default_group_class_name = klass + end + + def default_groups_association_name + @default_groups_association_name ||= Groupify.groups_association_name + end + + def default_groups_association_name=(name) + @default_groups_association_name = name && name.to_sym + end + def has_group(association_name, options = {}) ActiveRecord.create_children_association(self, association_name, options.merge( through: :group_memberships_as_member, source: :group, - default_base_class: @group_class_name + default_base_class: default_group_class_name ) ) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index e7b99d9..cdad093 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -44,8 +44,31 @@ def acts_as_group(opts = {}) end def acts_as_group_member(opts = {}) - @group_class_name = opts[:group_class_name] || Groupify.group_class_name include Groupify::ActiveRecord::GroupMember + + # Get defaults from parent class for STI + self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) + self.default_groups_association_name = Groupify.superclass_fetch(self, :default_groups_association_name, Groupify.groups_association_name) + + if (group_association_names = opts.delete :groups) + has_groups(group_association_names) + end + + if (default_groups = opts.delete :default_groups) + self.default_group_class_name = default_groups.to_s.classify + self.default_groups_association_name ||= default_groups + end + + # Deprecated: for backwards-compatibility + if (group_class_name = opts.delete :group_class_name) + self.default_group_class_name = group_class_name + end + + if default_groups_association_name + has_group default_groups_association_name, + source_type: ActiveRecord.base_class_name(default_group_class_name), + class_name: default_group_class_name + end end def acts_as_named_group_member(opts = {}) From 1f1ecd7e6e33b5b45e514a102602dfeb20f5ef74 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 15:17:32 -0400 Subject: [PATCH 148/205] DRY configuring defaults --- lib/groupify/adapter/active_record/model.rb | 45 +----------- .../adapter/active_record/parent_proxy.rb | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index cdad093..55cd9ee 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -21,54 +21,13 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group - # Get defaults from parent class for STI - self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) - self.default_members_association_name = Groupify.superclass_fetch(self, :default_members_association_name, Groupify.members_association_name) - - if (member_association_names = opts.delete :members) - has_members(member_association_names) - end - - if (default_members = opts.delete :default_members) - self.default_member_class_name = default_members.to_s.classify - # Only use as the association name if none specified (backwards-compatibility) - self.default_members_association_name ||= default_members - end - - if default_members_association_name - has_member(default_members_association_name, - source_type: ActiveRecord.base_class_name(default_member_class_name), - class_name: default_member_class_name - ) - end + ParentProxy.configure(self, :group, opts) end def acts_as_group_member(opts = {}) include Groupify::ActiveRecord::GroupMember - # Get defaults from parent class for STI - self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) - self.default_groups_association_name = Groupify.superclass_fetch(self, :default_groups_association_name, Groupify.groups_association_name) - - if (group_association_names = opts.delete :groups) - has_groups(group_association_names) - end - - if (default_groups = opts.delete :default_groups) - self.default_group_class_name = default_groups.to_s.classify - self.default_groups_association_name ||= default_groups - end - - # Deprecated: for backwards-compatibility - if (group_class_name = opts.delete :group_class_name) - self.default_group_class_name = group_class_name - end - - if default_groups_association_name - has_group default_groups_association_name, - source_type: ActiveRecord.base_class_name(default_group_class_name), - class_name: default_group_class_name - end + ParentProxy.configure(self, :member, opts) end def acts_as_named_group_member(opts = {}) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 353f4a7..193c346 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -2,6 +2,79 @@ module Groupify module ActiveRecord class ParentProxy + def self.configure(klass, parent_type, opts = {}) + child_type = parent_type == :group ? :member : :group + + if parent_type == :member + member_backwards_compatibility = %[ + # Deprecated: for backwards-compatibility + if (group_class_name = opts.delete :group_class_name) + self.default_group_class_name = group_class_name + end + ] + end + + klass.class_eval %[ + opts = #{opts.inspect} + # Get defaults from parent class for STI + self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) + self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) + + if (#{child_type}_association_names = opts.delete :#{child_type}s) + has_#{child_type}s(#{child_type}_association_names) + end + + #{member_backwards_compatibility} + + if (default_#{child_type}s = opts.delete :default_#{child_type}s) + self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify + # Only use as the association name if none specified (backwards-compatibility) + self.default_#{child_type}s_association_name ||= default_#{child_type}s + end + + if default_#{child_type}s_association_name + has_#{child_type}(default_#{child_type}s_association_name, + source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), + class_name: default_#{child_type}_class_name + ) + end + ] + + # children_name = :"#{child_type}s" + # default_children_type = :"default_#{children_name}" + # default_association_method = :"#{default_children_type}_association_name" + # has_child_method = :"has_#{children_name}" + # + # # Get defaults from parent class for STI + # [:class_name, :association_name].each do |setting| + # default_setting = Groupify.__send__(:"#{child_type}_#{setting}") + # superclass_setting = Groupify.superclass_fetch(klass, :"#{default_children_type}_#{setting}", default_setting) + # + # klass.__send__(:"#{default_children_type}_#{setting}=", superclass_setting) + # end + # + # if (association_names = opts.delete children_name) + # klass.__send__(has_child_method, association_names) + # end + # + # if (association_name = opts.delete default_children_type) + # klass.__send__(:"default_#{child_type}_class_name=", association_name.to_s.classify) + # # Only use as the association name if none specified (backwards-compatibility) + # unless klass.__send__(default_association_method) + # klass.__send__(:"#{default_association_method}=", association_name) + # end + # end + # + # if (default_association_name = klass.__send__(default_association_method)) + # default_class_name = klass.default_member_class_name + # + # klass.__send__(has_child_method, default_association_name, + # source_type: ActiveRecord.base_class_name(default_class_name), + # class_name: default_class_name + # ) + # end + end + attr_reader :parent_type, :child_type def initialize(parent, parent_type) From 9d652091b456b80ed889db25ff85a957e71e21f7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 16:37:32 -0400 Subject: [PATCH 149/205] Return nothing if child association name not specified --- lib/groupify/adapter/active_record/parent_proxy.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 193c346..06e61bc 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -147,7 +147,9 @@ def add_children(children, options = {}) end def children_association - @parent.__send__(Groupify.__send__(:"#{@child_type}s_association_name")) + association_name = @parent.class.__send__(:"default_#{@child_type}s_association_name") + + @parent.__send__(association_name) if association_name end def memberships_association From 2b82b3b5aafdec28dd9e0c9832b3059f801eaf0a Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 16:37:51 -0400 Subject: [PATCH 150/205] Don't delegate methods to create records --- lib/groupify/adapter/active_record/polymorphic_relation.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 1794b3a..a3b4ce0 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -5,7 +5,7 @@ class PolymorphicRelation < PolymorphicCollection def initialize(parent_proxy, &group_membership_filter) @parent_proxy = parent_proxy - + super(parent_proxy.child_type) do query = merge(parent_proxy.memberships_association) query = query.instance_eval(&group_membership_filter) if block_given? @@ -19,11 +19,6 @@ def as(membership_type) self end - # When trying to create a new record for this collection, - # create it on the `member.default_groups` or `group.default_members` - # association. - def_delegators :default_association, :build, :create, :create! - attr_reader :collection, :parent_proxy protected From 2310dd2e0ca1eec8affa06c7f493010997657f95 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 16:38:08 -0400 Subject: [PATCH 151/205] Updated README with polymorphic and STI documentation --- README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 89e3423..b70537c 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ Or install it yourself as: $ gem install groupify -### Setup +## Setup -#### Active Record +### Active Record Execute: @@ -63,7 +63,7 @@ class Assignment < ActiveRecord::Base end ``` -#### Mongoid +### Mongoid Execute: @@ -80,16 +80,21 @@ class User end ``` -#### Advanced Configuration +## Advanced Configuration -##### Groupify Model Names +### Groupify Model Names -The default model names for groups and group memberships are configurable. Add the following -configuration in `config/initializers/groupify.rb` to change the model names for all classes: +The default model names for groups, group members and group memberships are configurable. +The default association name for groups and members is also configurable. +Add the following configuration in `config/initializers/groupify.rb` to change the model names for all classes: ```ruby Groupify.configure do |config| config.group_class_name = 'MyCustomGroup' + config.member_class_name = 'MyCustomMember' + # Set to `false` if you don't want default associations + config.default_groups_association_name = :default_groups + config.default_members_association_name = :default_members # ActiveRecord only config.group_membership_class_name = 'MyCustomGroupMembership' end @@ -104,10 +109,7 @@ class Member < ActiveRecord::Base end ``` -Note that each member model can only belong to a single type of group (or child classes -of that group). - -##### Member Associations on Group +### Member Associations on Group Your group class can be configured to create associations for each expected member type. For example, let's say that your group class will have users and assignments as members. @@ -119,11 +121,21 @@ class Group < ActiveRecord::Base end ``` -The `default_members` option sets the model type when accessing the `members` association. -In the example above, `group.members` would return the users who are members of this group. +In addition to your configuration, Groupify will create a default `members` association. +The default association name can be customized with the `Groupify.default_members_association_name` +setting. + +The `default_members` option specified in the example above sets the model type when accessing the +default members association (e.g. `members`). Based on the example, `group.members` would return the +users who are members of this group. Note: if `Groupify.default_members_association_name` is set to `false` +then the `default_members` name will be used as the default members association name for this class +(e.g. `group.users` in this case). -If you are using single table inheritance, child classes inherit the member associations -of the parent. If your child class needs to add more members, use the `has_members` method. +If you are using single table inheritance (STI), child classes inherit the member associations +of the parent. If your child class needs to add more members, use the `has_members` method. You can specify +the same options that `has_many through` accepts to customize the association as you please. Note: when using inheritance, +it is recommended to specify the `source_type` option with the base class when you run into circular dependency +issues with your groups and members. Example: @@ -135,6 +147,8 @@ end class InternationalOrganization < Organization has_member :offices, class_name: 'CustomOfficeClass' has_member :equipment, class_name: 'CustomEquipmentClass' + # mitigate issues with inheritance and circular dependencies with groups and members + has_member :specific_equipment, class_name: 'SpecificEquipment', source_type: 'CustomEquipmentClass' end ``` @@ -167,7 +181,7 @@ user = CustomUserClass.create! org.add user, as: 'admin' ``` -##### Group Associations on Member (ActiveRecord only) +### Group Associations on Member Your member class can be configured to create associations for each expected group type. For example, let's say that your member class will have multiple types of organizations as groups. @@ -186,15 +200,96 @@ end class InternationalOrganization < Organization end +class Member < ActiveRecord::Base + groupify :group_member, groups: [:organizations, :international_organizations] +end +``` + +In addition to your configuration, Groupify will create a default `groups` association. +The default association name can be customized with the `Groupify.default_groups_association_name` +setting. + +If you are using single table inheritance (STI), child classes inherit the group associations +of the parent. If your child class needs to add more members, use the `has_groups` method. You can specify +the same options that `has_many through` accepts to customize the association as you please. Note: when using inheritance, +it is recommended to specify the `source_type` option with the base class when you run into circular dependency +issues with your groups and members. + +Example: + +```ruby +class Group < ActiveRecord::Base + groupify :group, members: [:users, :assignments], default_members: :users +end + +class Organization < Group + has_members :offices, :equipment +end + +class InternationalOrganization < Organization +end + class Member < ActiveRecord::Base groupify :group_member - has_group :organizations, class_name: 'Organization' - has_group :international_organizations, class_name: 'InternationalOrganization' + has_group :owned_organizations, class_name: 'Organization' end ``` -Mongoid does not support the `has_group` helper method. +### Polymoprhic Groups and Members (Active Record Only) + +When you configure multiple models as group or member, you may need to retrieve all groups or members, +particularly if they are not single-table inheritance models. When your models are distributed across +multiple tables, Groupify provides the ability to access all groups or users with the `group.polymorphic_members` +and `member.polymorphic_groups` helper methods. This returns an `Enumerable` collection of groups or members. + +Note: this collection effectively retrieves the group memberships and includes the `group_membership.group` or +`group_membership.member` to minimize N+1 queries, then returns only the groups or members from the group memberships +results. + +You can filter on membership type: + +``` +# member example +user.polymorphic_groups.as(:manager) + +# group example +group.polymorphic_members.as(:manager) +``` + +If you want to treat the collection like a scope, you can pass in a block which modifies the +criteria for retrieving the group memberships. + +``` +# member example +user.polymorphic_groups{where(group_type: 'CustomGroup')} + +# group example +group.polymorphic_members{where(member_type: 'CustomMember')} +``` + +If you want to treat the collection like an association, you can add groups to the collection and +group memberships will be created. + +``` +# member example +group = Group.new +user.polymorphic_groups << group +user.in_group?(group) # true +# equivalent to: +user.groups << group +user.in_group?(group) # true + +# group example +user = User.new +group.polymorphic_members << user +user.in_group?(group) # true +# equivalent to: +group.members << user +user.in_group?(group) # true +``` + +See _Usage_ below for additional functionality, such as how to specify membership type ## Usage From 566e7ae8f97e8878858c166318cc12e2de471dfb Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 16:38:21 -0400 Subject: [PATCH 152/205] Added tests for disabling default associations --- spec/active_record_spec.rb | 39 +++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 4610beb..5327cb6 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -129,10 +129,47 @@ def debug_sql group.add user, as: 'manager' organization.add user organization.add user, as: 'owner' - + expect(user.polymorphic_groups.count).to eq(2) expect(user.group_memberships_as_member.count).to eq(4) end + + context "default group association disabled" do + let(:user) { TestUser.create! } + let(:group) { TestGroup.create! } + + before do + @previous_groups_association_name = Groupify.groups_association_name + @previous_members_association_name = Groupify.members_association_name + Groupify.groups_association_name = false + Groupify.members_association_name = false + + class TestUser < ActiveRecord::Base + self.table_name = 'users' + groupify :group_member + end + + class TestGroup < ActiveRecord::Base + self.table_name = 'groups' + groupify :group + end + end + + after do + Groupify.groups_association_name = @previous_groups_association_name + end + + it "there is no groups or members association" do + expect(Groupify.groups_association_name).to eq(false) + expect(user.class.default_groups_association_name).to eq(false) + expect(user.as_member.children_association).to eq(nil) + + expect(Groupify.members_association_name).to eq(false) + expect(group.class.default_members_association_name).to eq(false) + expect(group.as_group.children_association).to eq(nil) + end + end + end end end From 7b8ee8f49b35c3d2d0310d3d597d39b9ef22e137 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 16:41:21 -0400 Subject: [PATCH 153/205] Add block to extend `has_many` --- lib/groupify/adapter/active_record/group.rb | 9 +++++---- lib/groupify/adapter/active_record/group_member.rb | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index d10ae99..e831874 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -77,19 +77,20 @@ def default_members_association_name=(name) end # Define which classes are members of this group - def has_members(*association_names) + def has_members(*association_names, &extension) association_names.flatten.each do |association_name| - has_member(association_name) + has_member(association_name, &extension) end end - def has_member(association_name, options = {}) + def has_member(association_name, options = {}, &extension) member_klass = ActiveRecord.create_children_association(self, association_name, options.merge( through: :group_memberships_as_group, source: :member, default_base_class: default_member_class_name - ) + ), + &extension ) (@member_klasses ||= Set.new) << member_klass.to_s.constantize diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 9bdc5ab..20cb4ff 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -108,9 +108,9 @@ def shares_any_group(other) in_any_group(other.polymorphic_groups) end - def has_groups(*association_names) + def has_groups(*association_names, &extension) association_names.flatten.each do |association_name| - has_group(association_name) + has_group(association_name, &extension) end end @@ -130,13 +130,14 @@ def default_groups_association_name=(name) @default_groups_association_name = name && name.to_sym end - def has_group(association_name, options = {}) + def has_group(association_name, options = {}, &extension) ActiveRecord.create_children_association(self, association_name, options.merge( through: :group_memberships_as_member, source: :group, default_base_class: default_group_class_name - ) + ), + &extension ) self From 5a3ade84d9becf2358f931e250a1d04b00a65d98 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 21:46:39 -0400 Subject: [PATCH 154/205] Indicate 4.1+ support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b70537c..bcbe7dc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ model? Use named groups instead to add members to named groups such as ## Compatibility The following ORMs are supported: - * ActiveRecord 4.x, 5.x + * ActiveRecord 4.1+, 5.x * Mongoid 4.x, 5.x, 6.x The following Rubies are supported: From 147520503d79c8ee901eb0efc0fbf4b8e3e55fd7 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 22:19:15 -0400 Subject: [PATCH 155/205] Updated README --- README.md | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bcbe7dc..5bd4415 100644 --- a/README.md +++ b/README.md @@ -92,24 +92,17 @@ Add the following configuration in `config/initializers/groupify.rb` to change t Groupify.configure do |config| config.group_class_name = 'MyCustomGroup' config.member_class_name = 'MyCustomMember' - # Set to `false` if you don't want default associations + + # Set to `false` if you don't want default associations automatically created config.default_groups_association_name = :default_groups config.default_members_association_name = :default_members + # ActiveRecord only config.group_membership_class_name = 'MyCustomGroupMembership' end ``` -The group name can also be set on a model-by-model basis for each group member by passing -the `group_class_name` option: - -```ruby -class Member < ActiveRecord::Base - groupify :group_member, group_class_name: 'MyOtherCustomGroup' -end -``` - -### Member Associations on Group +### Groups: Configuring Group Members Your group class can be configured to create associations for each expected member type. For example, let's say that your group class will have users and assignments as members. @@ -147,6 +140,7 @@ end class InternationalOrganization < Organization has_member :offices, class_name: 'CustomOfficeClass' has_member :equipment, class_name: 'CustomEquipmentClass' + # mitigate issues with inheritance and circular dependencies with groups and members has_member :specific_equipment, class_name: 'SpecificEquipment', source_type: 'CustomEquipmentClass' end @@ -181,7 +175,7 @@ user = CustomUserClass.create! org.add user, as: 'admin' ``` -### Group Associations on Member +### Group Members: Configuring Groups Your member class can be configured to create associations for each expected group type. For example, let's say that your member class will have multiple types of organizations as groups. @@ -201,7 +195,7 @@ class InternationalOrganization < Organization end class Member < ActiveRecord::Base - groupify :group_member, groups: [:organizations, :international_organizations] + groupify :group_member, groups: [:groups, :organizations, :international_organizations], default_groups: :groups end ``` @@ -209,6 +203,22 @@ In addition to your configuration, Groupify will create a default `groups` assoc The default association name can be customized with the `Groupify.default_groups_association_name` setting. +The `default_groups` option specified in the example above sets the model type when accessing the +default groups association (e.g. `groups`). Based on the example, `member.groups` would return the +groups the member has a membership to. Note: if `Groupify.default_groups_association_name` is set to `false` +then the `default_groups` name will be used as the default members association name for this class +(e.g. `member.groups` in this case). + +Note: the `group_class_name` option can be specified as the default group class for backwards-compatibility. However, +unlike the `default_groups` option, a default association will not be created if `Groupify.default_groups_association_name` +is set to `false`. + +```ruby +class Member < ActiveRecord::Base + groupify :group_member, group_class_name: 'MyOtherCustomGroup' +end +``` + If you are using single table inheritance (STI), child classes inherit the group associations of the parent. If your child class needs to add more members, use the `has_groups` method. You can specify the same options that `has_many through` accepts to customize the association as you please. Note: when using inheritance, @@ -236,7 +246,7 @@ class Member < ActiveRecord::Base end ``` -### Polymoprhic Groups and Members (Active Record Only) +### Polymorphic Groups and Members (Active Record Only) When you configure multiple models as group or member, you may need to retrieve all groups or members, particularly if they are not single-table inheritance models. When your models are distributed across @@ -249,7 +259,7 @@ results. You can filter on membership type: -``` +```ruby # member example user.polymorphic_groups.as(:manager) @@ -260,7 +270,7 @@ group.polymorphic_members.as(:manager) If you want to treat the collection like a scope, you can pass in a block which modifies the criteria for retrieving the group memberships. -``` +```ruby # member example user.polymorphic_groups{where(group_type: 'CustomGroup')} @@ -271,7 +281,7 @@ group.polymorphic_members{where(member_type: 'CustomMember')} If you want to treat the collection like an association, you can add groups to the collection and group memberships will be created. -``` +```ruby # member example group = Group.new user.polymorphic_groups << group From 434b6e6f317128156007209ee326c744aef690d4 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 22:19:30 -0400 Subject: [PATCH 156/205] Revert ActiveRecord configuration to inline for clarity --- lib/groupify/adapter/active_record/model.rb | 45 +++++- .../adapter/active_record/parent_proxy.rb | 144 +++++++++--------- 2 files changed, 115 insertions(+), 74 deletions(-) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 55cd9ee..cdad093 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -21,13 +21,54 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group - ParentProxy.configure(self, :group, opts) + # Get defaults from parent class for STI + self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) + self.default_members_association_name = Groupify.superclass_fetch(self, :default_members_association_name, Groupify.members_association_name) + + if (member_association_names = opts.delete :members) + has_members(member_association_names) + end + + if (default_members = opts.delete :default_members) + self.default_member_class_name = default_members.to_s.classify + # Only use as the association name if none specified (backwards-compatibility) + self.default_members_association_name ||= default_members + end + + if default_members_association_name + has_member(default_members_association_name, + source_type: ActiveRecord.base_class_name(default_member_class_name), + class_name: default_member_class_name + ) + end end def acts_as_group_member(opts = {}) include Groupify::ActiveRecord::GroupMember - ParentProxy.configure(self, :member, opts) + # Get defaults from parent class for STI + self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) + self.default_groups_association_name = Groupify.superclass_fetch(self, :default_groups_association_name, Groupify.groups_association_name) + + if (group_association_names = opts.delete :groups) + has_groups(group_association_names) + end + + if (default_groups = opts.delete :default_groups) + self.default_group_class_name = default_groups.to_s.classify + self.default_groups_association_name ||= default_groups + end + + # Deprecated: for backwards-compatibility + if (group_class_name = opts.delete :group_class_name) + self.default_group_class_name = group_class_name + end + + if default_groups_association_name + has_group default_groups_association_name, + source_type: ActiveRecord.base_class_name(default_group_class_name), + class_name: default_group_class_name + end end def acts_as_named_group_member(opts = {}) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 06e61bc..95eab7a 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -2,78 +2,78 @@ module Groupify module ActiveRecord class ParentProxy - def self.configure(klass, parent_type, opts = {}) - child_type = parent_type == :group ? :member : :group - - if parent_type == :member - member_backwards_compatibility = %[ - # Deprecated: for backwards-compatibility - if (group_class_name = opts.delete :group_class_name) - self.default_group_class_name = group_class_name - end - ] - end - - klass.class_eval %[ - opts = #{opts.inspect} - # Get defaults from parent class for STI - self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) - self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) - - if (#{child_type}_association_names = opts.delete :#{child_type}s) - has_#{child_type}s(#{child_type}_association_names) - end - - #{member_backwards_compatibility} - - if (default_#{child_type}s = opts.delete :default_#{child_type}s) - self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify - # Only use as the association name if none specified (backwards-compatibility) - self.default_#{child_type}s_association_name ||= default_#{child_type}s - end - - if default_#{child_type}s_association_name - has_#{child_type}(default_#{child_type}s_association_name, - source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), - class_name: default_#{child_type}_class_name - ) - end - ] - - # children_name = :"#{child_type}s" - # default_children_type = :"default_#{children_name}" - # default_association_method = :"#{default_children_type}_association_name" - # has_child_method = :"has_#{children_name}" - # - # # Get defaults from parent class for STI - # [:class_name, :association_name].each do |setting| - # default_setting = Groupify.__send__(:"#{child_type}_#{setting}") - # superclass_setting = Groupify.superclass_fetch(klass, :"#{default_children_type}_#{setting}", default_setting) - # - # klass.__send__(:"#{default_children_type}_#{setting}=", superclass_setting) - # end - # - # if (association_names = opts.delete children_name) - # klass.__send__(has_child_method, association_names) - # end - # - # if (association_name = opts.delete default_children_type) - # klass.__send__(:"default_#{child_type}_class_name=", association_name.to_s.classify) - # # Only use as the association name if none specified (backwards-compatibility) - # unless klass.__send__(default_association_method) - # klass.__send__(:"#{default_association_method}=", association_name) - # end - # end - # - # if (default_association_name = klass.__send__(default_association_method)) - # default_class_name = klass.default_member_class_name - # - # klass.__send__(has_child_method, default_association_name, - # source_type: ActiveRecord.base_class_name(default_class_name), - # class_name: default_class_name - # ) - # end - end + # def self.configure(klass, parent_type, opts = {}) + # child_type = parent_type == :group ? :member : :group + # + # if parent_type == :member + # member_backwards_compatibility = %[ + # # Deprecated: for backwards-compatibility + # if (group_class_name = opts.delete :group_class_name) + # self.default_group_class_name = group_class_name + # end + # ] + # end + # + # klass.class_eval %[ + # opts = #{opts.inspect} + # # Get defaults from parent class for STI + # self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) + # self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) + # + # if (#{child_type}_association_names = opts.delete :#{child_type}s) + # has_#{child_type}s(#{child_type}_association_names) + # end + # + # #{member_backwards_compatibility} + # + # if (default_#{child_type}s = opts.delete :default_#{child_type}s) + # self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify + # # Only use as the association name if none specified (backwards-compatibility) + # self.default_#{child_type}s_association_name ||= default_#{child_type}s + # end + # + # if default_#{child_type}s_association_name + # has_#{child_type}(default_#{child_type}s_association_name, + # source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), + # class_name: default_#{child_type}_class_name + # ) + # end + # ] + # + # # children_name = :"#{child_type}s" + # # default_children_type = :"default_#{children_name}" + # # default_association_method = :"#{default_children_type}_association_name" + # # has_child_method = :"has_#{children_name}" + # # + # # # Get defaults from parent class for STI + # # [:class_name, :association_name].each do |setting| + # # default_setting = Groupify.__send__(:"#{child_type}_#{setting}") + # # superclass_setting = Groupify.superclass_fetch(klass, :"#{default_children_type}_#{setting}", default_setting) + # # + # # klass.__send__(:"#{default_children_type}_#{setting}=", superclass_setting) + # # end + # # + # # if (association_names = opts.delete children_name) + # # klass.__send__(has_child_method, association_names) + # # end + # # + # # if (association_name = opts.delete default_children_type) + # # klass.__send__(:"default_#{child_type}_class_name=", association_name.to_s.classify) + # # # Only use as the association name if none specified (backwards-compatibility) + # # unless klass.__send__(default_association_method) + # # klass.__send__(:"#{default_association_method}=", association_name) + # # end + # # end + # # + # # if (default_association_name = klass.__send__(default_association_method)) + # # default_class_name = klass.default_member_class_name + # # + # # klass.__send__(has_child_method, default_association_name, + # # source_type: ActiveRecord.base_class_name(default_class_name), + # # class_name: default_class_name + # # ) + # # end + # end attr_reader :parent_type, :child_type From 9ac8b4d5abbd9a10ed9b3150289eb9f90fb5197e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 22:19:54 -0400 Subject: [PATCH 157/205] Replicated configuration setup from ActiveRecord to Mongoid --- lib/groupify/adapter/mongoid/group.rb | 23 ++++++++-- lib/groupify/adapter/mongoid/group_member.rb | 32 ++++++++++++-- lib/groupify/adapter/mongoid/model.rb | 44 +++++++++++++++++--- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index 916d2fa..d35b589 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -15,7 +15,8 @@ module Group extend ActiveSupport::Concern included do - @default_member_class = nil + @default_member_class_name = nil + @default_members_association_name = nil @member_klasses ||= Set.new end @@ -61,7 +62,23 @@ def default_member_class=(klass) # Returns the member classes defined for this class, as well as for the super classes def member_classes - (@member_klasses ||= Set.new).merge(superclass.method_defined?(:member_classes) ? superclass.member_classes : []) + (@member_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :member_classes, [])) + end + + def default_member_class_name + @default_member_class_name ||= Groupify.member_class_name + end + + def default_member_class_name=(klass) + @default_member_class_name = klass + end + + def default_members_association_name + @default_members_association_name ||= Groupify.members_association_name + end + + def default_members_association_name=(name) + @default_members_association_name = name && name.to_sym end # Define which classes are members of this group @@ -73,7 +90,7 @@ def has_members(*association_names) def has_member(association_name, options = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) - model_klass = options[:class_name] || association_class + model_klass = options[:class_name] || association_class || default_member_class_name member_klass = model_klass.to_s.constantize (@member_klasses ||= Set.new) << member_klass diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index a6a46fb..73a8fbc 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -16,7 +16,8 @@ module GroupMember include MemberScopedAs included do - has_group Groupify.groups_association_name.to_sym + @default_group_class_name = nil + @default_groups_association_name = nil class GroupMembership include ::Mongoid::Document @@ -32,8 +33,6 @@ class GroupMembership field :as, as: :membership_type, type: String end - GroupMembership.send :has_and_belongs_to_many, :groups, class_name: @group_class_name, inverse_of: nil - embeds_many :group_memberships, class_name: GroupMembership.to_s, as: :member do def as(membership_type) where(membership_type: membership_type) @@ -85,6 +84,22 @@ def shares_any_group(other) in_any_group(other.groups.to_a) end + def default_group_class_name + @default_group_class_name ||= Groupify.group_class_name + end + + def default_group_class_name=(klass) + @default_group_class_name = klass + end + + def default_groups_association_name + @default_groups_association_name ||= Groupify.groups_association_name + end + + def default_groups_association_name=(name) + @default_groups_association_name = name && name.to_sym + end + def has_groups(*association_names) association_names.flatten.each do |association_name| has_group(association_name) @@ -92,7 +107,9 @@ def has_groups(*association_names) end def has_group(association_name, options = {}) - options = {autosave: true, dependent: :nullify, inverse_of: nil, class_name: @group_class_name}.merge(options) + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + options = {autosave: true, dependent: :nullify, inverse_of: nil}.merge(options) + model_klass = options[:class_name] || association_class || default_base_class has_and_belongs_to_many association_name, options do def as(membership_type) @@ -125,6 +142,13 @@ def delete(*groups) end end end + + GroupMembership.send(:has_and_belongs_to_many, + association_name, { + class_name: model_klass, + inverse_of: nil}. + merge(options.slice(:class_name)) + ) end end end diff --git a/lib/groupify/adapter/mongoid/model.rb b/lib/groupify/adapter/mongoid/model.rb index 462a4cd..be4ead4 100644 --- a/lib/groupify/adapter/mongoid/model.rb +++ b/lib/groupify/adapter/mongoid/model.rb @@ -15,20 +15,52 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::Mongoid::Group - if (member_klass = opts.delete :default_members) - self.default_member_class = member_klass.to_s.classify.constantize + # Get defaults from parent class for STI + self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) + self.default_members_association_name = Groupify.superclass_fetch(self, :default_members_association_name, Groupify.members_association_name) - has_member(Groupify.members_association_name.to_sym, class_name: default_member_class.to_s) + if (member_association_names = opts.delete :members) + has_members(member_association_names) end - if (member_klasses = opts.delete :members) - has_members(member_klasses) + if (default_members = opts.delete :default_members) + self.default_member_class_name = default_members.to_s.classify + # Only use as the association name if none specified (backwards-compatibility) + self.default_members_association_name ||= default_members + end + + if default_members_association_name + has_member(default_members_association_name, + class_name: default_member_class_name + ) end end def acts_as_group_member(opts = {}) - @group_class_name = opts[:group_class_name] || Groupify.group_class_name include Groupify::Mongoid::GroupMember + + # Get defaults from parent class for STI + self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) + self.default_groups_association_name = Groupify.superclass_fetch(self, :default_groups_association_name, Groupify.groups_association_name) + + if (group_association_names = opts.delete :groups) + has_groups(group_association_names) + end + + if (default_groups = opts.delete :default_groups) + self.default_group_class_name = default_groups.to_s.classify + self.default_groups_association_name ||= default_groups + end + + # Deprecated: for backwards-compatibility + if (group_class_name = opts.delete :group_class_name) + self.default_group_class_name = group_class_name + end + + if default_groups_association_name + has_group default_groups_association_name, + class_name: default_group_class_name + end end def acts_as_named_group_member(opts = {}) From 2ae566b7e90054403f90356ef33b494a1ca9449c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 23:50:40 -0400 Subject: [PATCH 158/205] Renamed "merge" methods for clarity --- lib/groupify/adapter/active_record/group.rb | 4 ++-- .../adapter/active_record/group_member.rb | 8 ++++---- .../adapter/active_record/named_group_member.rb | 8 ++++---- .../active_record/parent_query_builder.rb | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index e831874..a9a3d25 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -52,7 +52,7 @@ def merge!(source) module ClassMethods def with_member(member) - group_finder.merge_children(member) + group_finder.with_children(member) end # Returns the member classes defined for this class, as well as for the super classes @@ -102,7 +102,7 @@ def has_member(association_name, options = {}, &extension) def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = source_group.member_classes - destination_group.member_classes - invalid_found = invalid_member_classes.any?{ |klass| klass.member_finder.merge_children(source_group).count > 0 } + invalid_found = invalid_member_classes.any?{ |klass| klass.member_finder.with_children(source_group).count > 0 } if invalid_found raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 20cb4ff..d696dc2 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -67,12 +67,12 @@ def as(membership_type) end def in_group(group) - group.present? ? member_finder.merge_children(group).distinct : none + group.present? ? member_finder.with_children(group).distinct : none end def in_any_group(*groups) groups.flatten! - groups.present? ? member_finder.merge_children(groups).distinct : none + groups.present? ? member_finder.with_children(groups).distinct : none end def in_all_groups(*groups) @@ -84,7 +84,7 @@ def in_all_groups(*groups) # Count distinct on ID and type combo concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" - member_finder.merge_children(groups). + member_finder.with_children(groups). group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct @@ -101,7 +101,7 @@ def in_only_groups(*groups) end def in_other_groups(*groups) - member_finder.merge_children_without(groups) + member_finder.without_children(groups) end def shares_any_group(other) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 6b303ed..0395abc 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -63,21 +63,21 @@ def as(membership_type) def in_named_group(named_group) return none unless named_group.present? - named_member_finder.merge_memberships{where(group_name: named_group)}.distinct + named_member_finder.with_memberships{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_finder.merge_memberships{where(group_name: named_groups.flatten)}.distinct + named_member_finder.with_memberships{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_finder.merge_memberships{where(group_name: named_groups)}. + named_member_finder.with_memberships{where(group_name: named_groups)}. group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct @@ -93,7 +93,7 @@ def in_only_named_groups(*named_groups) end def in_other_named_groups(*named_groups) - named_member_finder.merge_memberships{where.not(group_name: named_groups)} + named_member_finder.with_memberships{where.not(group_name: named_groups)} end def shares_any_named_group(other) diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb index 9293217..7b8b1a5 100644 --- a/lib/groupify/adapter/active_record/parent_query_builder.rb +++ b/lib/groupify/adapter/active_record/parent_query_builder.rb @@ -10,31 +10,31 @@ def initialize(scope, parent_type) end def as(membership_type) - merge_memberships{as(membership_type)} + with_memberships{as(membership_type)} end - def merge_children(child_or_children) + def with_children(child_or_children) scope = if child_or_children.is_a?(::ActiveRecord::Base) # single child - merge_memberships(criteria: child_or_children.__send__(:"group_memberships_as_#{@child_type}")) + with_memberships(criteria: child_or_children.__send__(:"group_memberships_as_#{@child_type}")) else method_name = :"for_#{@child_type}s" - merge_memberships{__send__(method_name, child_or_children)} + with_memberships{__send__(method_name, child_or_children)} end if block_given? - scope = scope.merge_memberships(&group_membership_filter) + scope = scope.with_memberships(&group_membership_filter) end scope end - def merge_children_without(children) + def without_children(children) method_name = :"not_for_#{@child_type}s" - merge_memberships{__send__(method_name, children)} + with_memberships{__send__(method_name, children)} end - def merge_memberships(options = {}, &group_membership_filter) + def with_memberships(options = {}, &group_membership_filter) criteria = [] criteria << @scope.joins(:"group_memberships_as_#{@parent_type}") criteria << options[:criteria] if options[:criteria] From bee094f705d80a5feef663de65c90cf609ab6b2f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 10 Aug 2017 23:57:04 -0400 Subject: [PATCH 159/205] Removed redundant scope merge --- lib/groupify/adapter/active_record/group_membership.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index f26646c..3c3a50b 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -43,11 +43,11 @@ def as(membership_type) end def polymorphic_groups - PolymorphicCollection.new(:group){merge(all)} + PolymorphicCollection.new(:group) end def polymorphic_members - PolymorphicCollection.new(:member){merge(all)} + PolymorphicCollection.new(:member) end def for_groups(groups) From ca534e036e42201b201e6271fbd763037dac8c5e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 00:11:25 -0400 Subject: [PATCH 160/205] Disable `appraisal` gem because it causes segfaults - loosen other gem versions --- Gemfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 778e2a6..81f854d 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,9 @@ end group :test do gem "rspec", ">= 3" - gem "database_cleaner", "~> 1.5.3" - gem "combustion", "0.5.5" - gem "appraisal" + gem "database_cleaner", ">= 1.5.3" + gem "combustion", ">= 0.5.5" + #gem "appraisal" gem 'coveralls', require: false gem "codeclimate-test-reporter", require: nil end @@ -27,6 +27,6 @@ end platforms :ruby do gem "sqlite3" - gem "mysql2", "~> 0.3.11" + gem "mysql2", ">= 0.3.11" gem "pg" end From a6d2a90f00fa4a5d598dd8ac7153814d5976c03b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 00:34:08 -0400 Subject: [PATCH 161/205] Don't add default associations by default, but provide `configure_legacy_defaults!` for backwards-compatibility --- lib/groupify.rb | 18 ++++++++++++++---- spec/active_record_spec.rb | 6 ++---- spec/mongoid_spec.rb | 32 ++++++++++++++++++-------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 04f9836..4a2da40 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -9,13 +9,13 @@ module Groupify :ignore_base_class_inference_errors, :ignore_association_class_inference_errors - self.group_class_name = 'Group' self.group_membership_class_name = 'GroupMembership' - self.member_class_name = 'User' + self.group_class_name = nil # 'Group' + self.member_class_name = nil # 'User' # Set to `false` if default association should not be created - self.members_association_name = :members + self.members_association_name = false # :members # Set to `false` if default association should not be created - self.groups_association_name = :groups + self.groups_association_name = false # :groups self.ignore_base_class_inference_errors = true self.ignore_association_class_inference_errors = true @@ -23,6 +23,16 @@ def self.configure yield self end + def self.configure_legacy_defaults! + configure do |config| + config.group_class_name = 'Group' + config.member_class_name = 'User' + + config.groups_association_name = :groups + config.members_association_name = :members + end + end + def self.group_membership_klass group_membership_class_name.constantize end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 5327cb6..952c333 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -23,10 +23,8 @@ require 'groupify/adapter/active_record' -def debug_sql - logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, Logger.new(STDOUT) - yield - ActiveRecord::Base.logger = logger +Groupify.configure do |config| + config.configure_legacy_defaults! end # autoload :User, 'active_record/user' diff --git a/spec/mongoid_spec.rb b/spec/mongoid_spec.rb index af34fba..349aa22 100644 --- a/spec/mongoid_spec.rb +++ b/spec/mongoid_spec.rb @@ -1,6 +1,6 @@ RSpec.configure do |config| config.order = "random" - + config.before(:suite) do DatabaseCleaner[:mongoid].strategy = :truncation end @@ -41,9 +41,13 @@ require 'groupify/adapter/mongoid' +Groupify.configure do |config| + config.configure_legacy_defaults! +end + class MongoidUser include Mongoid::Document - + groupify :group_member, group_class_name: 'MongoidGroup' groupify :named_group_member end @@ -58,7 +62,7 @@ class MongoidWidget class MongoidTask include Mongoid::Document - + groupify :group_member, group_class_name: 'MongoidGroup' end @@ -70,7 +74,7 @@ class MongoidIssue class MongoidGroup include Mongoid::Document - + groupify :group, members: [:mongoid_users, :mongoid_tasks, :mongoid_widgets, :mongoid_groups], default_members: :mongoid_users groupify :group_member, group_class_name: "MongoidGroup" @@ -104,7 +108,7 @@ class MongoidProject < MongoidGroup expect(MongoidGroup.new.members).to be_empty expect(group.members).to be_empty end - + context "when adding" do it "adds a group to a member" do user.groups << group @@ -112,7 +116,7 @@ class MongoidProject < MongoidGroup expect(group.members).to include(user) expect(group.users).to include(user) end - + it "adds a member to a group" do expect(user.groups).to be_empty group.add user @@ -144,16 +148,16 @@ class MongoidProject < MongoidGroup expect(MongoidProject.member_classes).to include(MongoidUser, MongoidTask, MongoidIssue) end - + it "finds members by group" do group.add user - + expect(MongoidUser.in_group(group).first).to eql(user) end it "finds the groups a member belongs to" do group.add user - + expect(MongoidGroup.with_member(user).first).to eq(group) end @@ -260,7 +264,7 @@ class MongoidProject < MongoidGroup destination.merge!(source) expect(source.destroyed?).to be true - + expect(destination.users.to_a).to include(user) expect(destination.managers.to_a).to include(manager) expect(destination.tasks.to_a).to include(task) @@ -315,14 +319,14 @@ class MongoidProject < MongoidGroup user.save! group4 = MongoidGroup.create! - + expect(user.groups).to include(group, group2, group3) expect(MongoidUser.in_group(group).first).to eql(user) expect(MongoidUser.in_group(group2).first).to eql(user) expect(user.in_group?(group)).to be true expect(user.in_group?(group4)).to be false - + expect(MongoidUser.in_any_group(group, group4).first).to eql(user) expect(MongoidUser.in_any_group(group4)).to be_empty expect(user.in_any_group?(group2, group4)).to be true @@ -346,7 +350,7 @@ class MongoidProject < MongoidGroup context "when using membership types with groups" do it 'adds groups to a member with a specific membership type' do group.add(user, as: :admin) - + expect(user.groups).to include(group) expect(group.members).to include(user) expect(group.users).to include(user) @@ -558,7 +562,7 @@ class MongoidProject < MongoidGroup it "checks if named groups are shared" do user2 = MongoidUser.create!(:named_groups => [:admin]) - + expect(user.shares_any_named_group?(user2)).to be true expect(MongoidUser.shares_any_named_group(user).to_a).to include(user2) end From 003fe8fd8a05f679823784711253b18a50f19ab0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 00:43:39 -0400 Subject: [PATCH 162/205] Updated README --- README.md | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5bd4415..221f971 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ end ### Groupify Model Names -The default model names for groups, group members and group memberships are configurable. +The default classes for groups, group members and group memberships are configurable. The default association name for groups and members is also configurable. Add the following configuration in `config/initializers/groupify.rb` to change the model names for all classes: @@ -93,15 +93,34 @@ Groupify.configure do |config| config.group_class_name = 'MyCustomGroup' config.member_class_name = 'MyCustomMember' - # Set to `false` if you don't want default associations automatically created - config.default_groups_association_name = :default_groups - config.default_members_association_name = :default_members + # Default to `false` so default associations are not automatically created + config.default_groups_association_name = :groups + config.default_members_association_name = :members # ActiveRecord only config.group_membership_class_name = 'MyCustomGroupMembership' end ``` +#### Backwards-compatible Configuration Defaults + +The new default configuration does *not* create default associations or make assumptions about your +group and group member class names. If you would like to retain the *legacy* defaults, you can +utilize the `configure_legacy_defaults!` convenience method. + +```ruby +Groupify.configure do |config| + config.configure_legacy_defaults! + + # These are the legacy defaults configured for you: + # config.group_class_name = 'Group' + # config.member_class_name = 'User' + # + # config.groups_association_name = :groups + # config.members_association_name = :members +end +``` + ### Groups: Configuring Group Members Your group class can be configured to create associations for each expected member type. @@ -116,13 +135,14 @@ end In addition to your configuration, Groupify will create a default `members` association. The default association name can be customized with the `Groupify.default_members_association_name` -setting. +setting. If the association name is set to `false`, no default association is created. -The `default_members` option specified in the example above sets the model type when accessing the +The `default_members` option specified in the example above is used to infer the model class when accessing the default members association (e.g. `members`). Based on the example, `group.members` would return the users who are members of this group. Note: if `Groupify.default_members_association_name` is set to `false` -then the `default_members` name will be used as the default members association name for this class -(e.g. `group.users` in this case). +then the name specified for `default_members` will be used as the default members association name for this class +(e.g. `group.users` in this case). If that were the case, you would not need to specify `members: [:users]` because +it would be overwritten with a new default association. If you are using single table inheritance (STI), child classes inherit the member associations of the parent. If your child class needs to add more members, use the `has_members` method. You can specify From b72a1437d61572259a0e8b3c3571c600252a0dfc Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 00:47:30 -0400 Subject: [PATCH 163/205] Consistent naming of `opts` --- lib/groupify/adapter/active_record.rb | 10 +++++----- .../adapter/active_record/association_extensions.rb | 2 +- .../adapter/active_record/collection_extensions.rb | 4 ++-- lib/groupify/adapter/active_record/group.rb | 4 ++-- lib/groupify/adapter/active_record/group_member.rb | 4 ++-- lib/groupify/adapter/active_record/group_membership.rb | 2 +- lib/groupify/adapter/active_record/parent_proxy.rb | 10 +++++----- .../adapter/active_record/parent_query_builder.rb | 4 ++-- lib/groupify/adapter/mongoid/group.rb | 6 +++--- lib/groupify/adapter/mongoid/group_member.rb | 10 +++++----- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 76805a2..ec37be9 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -41,17 +41,17 @@ def self.base_class_name(model_class, default_base_class = nil) raise end - def self.create_children_association(klass, association_name, options = {}) + def self.create_children_association(klass, association_name, opts = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) - default_base_class = options.delete(:default_base_class) - model_klass = options[:class_name] || association_class || default_base_class + default_base_class = opts.delete(:default_base_class) + model_klass = opts[:class_name] || association_class || default_base_class # only try to look up base class if needed - can cause circular dependency issue - options[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) + opts[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) klass.has_many association_name, ->{ distinct }, { extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(options) + }.merge(opts) model_klass diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 1cdebe9..23cc71c 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -17,7 +17,7 @@ def parent_proxy # Throw an exception here when adding direction to an association # because when adding the children to the parent this won't # happen because the group membership is polymorphic. - def add_children(children, options = {}) + def add_children(children, opts = {}) children.each do |child| proxy_association.__send__(:raise_on_type_mismatch!, child) end diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index 8cac7ad..eea9dc4 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -35,8 +35,8 @@ def parent_proxy protected - def add_children(children, options = {}) - parent_proxy.add_children(children, options) + def add_children(children, opts = {}) + parent_proxy.add_children(children, opts) end def remove_children(children, destruction_type, membership_type = nil) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index a9a3d25..97457b2 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -83,9 +83,9 @@ def has_members(*association_names, &extension) end end - def has_member(association_name, options = {}, &extension) + def has_member(association_name, opts = {}, &extension) member_klass = ActiveRecord.create_children_association(self, association_name, - options.merge( + opts.merge( through: :group_memberships_as_group, source: :member, default_base_class: default_member_class_name diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index d696dc2..79ea2da 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -130,9 +130,9 @@ def default_groups_association_name=(name) @default_groups_association_name = name && name.to_sym end - def has_group(association_name, options = {}, &extension) + def has_group(association_name, opts = {}, &extension) ActiveRecord.create_children_association(self, association_name, - options.merge( + opts.merge( through: :group_memberships_as_member, source: :group, default_base_class: default_group_class_name diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 3c3a50b..2441c50 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -66,7 +66,7 @@ def not_for_members(groups) where.not(build_polymorphic_criteria_for(:member, members)) end - def for_polymorphic(source, records, options = {}) + def for_polymorphic(source, records, opts = {}) case records when Array where(build_polymorphic_criteria_for(source, records)) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 95eab7a..63eaf18 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -82,16 +82,16 @@ def initialize(parent, parent_type) @child_type = parent_type == :group ? :member : :group end - def find_memberships_for_children(children, options = {}) - memberships_association.__send__(:"for_#{@child_type}s", children).as(options[:as]) + def find_memberships_for_children(children, opts = {}) + memberships_association.__send__(:"for_#{@child_type}s", children).as(opts[:as]) end - def add_children(children, options = {}) + def add_children(children, opts = {}) return @parent if children.none? clear_association_cache - membership_type = options[:as] + membership_type = opts[:as] to_add_directly = [] to_add_with_membership_type = [] @@ -125,7 +125,7 @@ def add_children(children, options = {}) list_to_validate.each do |child| next if child.valid? - if options[:exception_on_invalidation] + if opts[:exception_on_invalidation] raise ::ActiveRecord::RecordInvalid.new(child) else return false diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb index 7b8b1a5..d89c889 100644 --- a/lib/groupify/adapter/active_record/parent_query_builder.rb +++ b/lib/groupify/adapter/active_record/parent_query_builder.rb @@ -34,10 +34,10 @@ def without_children(children) with_memberships{__send__(method_name, children)} end - def with_memberships(options = {}, &group_membership_filter) + def with_memberships(opts = {}, &group_membership_filter) criteria = [] criteria << @scope.joins(:"group_memberships_as_#{@parent_type}") - criteria << options[:criteria] if options[:criteria] + criteria << opts[:criteria] if opts[:criteria] criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? # merge all criteria together diff --git a/lib/groupify/adapter/mongoid/group.rb b/lib/groupify/adapter/mongoid/group.rb index d35b589..5883315 100644 --- a/lib/groupify/adapter/mongoid/group.rb +++ b/lib/groupify/adapter/mongoid/group.rb @@ -88,9 +88,9 @@ def has_members(*association_names) end end - def has_member(association_name, options = {}) + def has_member(association_name, opts = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) - model_klass = options[:class_name] || association_class || default_member_class_name + model_klass = opts[:class_name] || association_class || default_member_class_name member_klass = model_klass.to_s.constantize (@member_klasses ||= Set.new) << member_klass @@ -100,7 +100,7 @@ def has_member(association_name, options = {}) dependent: :nullify, foreign_key: 'group_ids', extend: MemberAssociationExtensions - }.merge(options) + }.merge(opts) member_klass end diff --git a/lib/groupify/adapter/mongoid/group_member.rb b/lib/groupify/adapter/mongoid/group_member.rb index 73a8fbc..29897d1 100644 --- a/lib/groupify/adapter/mongoid/group_member.rb +++ b/lib/groupify/adapter/mongoid/group_member.rb @@ -106,12 +106,12 @@ def has_groups(*association_names) end end - def has_group(association_name, options = {}) + def has_group(association_name, opts = {}) association_class, association_name = Groupify.infer_class_and_association_name(association_name) - options = {autosave: true, dependent: :nullify, inverse_of: nil}.merge(options) - model_klass = options[:class_name] || association_class || default_base_class + opts = {autosave: true, dependent: :nullify, inverse_of: nil}.merge(opts) + model_klass = opts[:class_name] || association_class || default_base_class - has_and_belongs_to_many association_name, options do + has_and_belongs_to_many association_name, opts do def as(membership_type) # `membership_type.present?` causes tests to fail for `MongoidManager` class.... return self unless membership_type @@ -147,7 +147,7 @@ def delete(*groups) association_name, { class_name: model_klass, inverse_of: nil}. - merge(options.slice(:class_name)) + merge(opts.slice(:class_name)) ) end end From 8909726a113ff222075c5c1bacd8d1ec46b1aedb Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 00:53:34 -0400 Subject: [PATCH 164/205] Cleanup unused code --- .../adapter/active_record/parent_proxy.rb | 89 ++----------------- .../active_record/polymorphic_relation.rb | 12 +-- spec/active_record_spec.rb | 37 -------- 3 files changed, 10 insertions(+), 128 deletions(-) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb index 63eaf18..6080083 100644 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ b/lib/groupify/adapter/active_record/parent_proxy.rb @@ -2,79 +2,6 @@ module Groupify module ActiveRecord class ParentProxy - # def self.configure(klass, parent_type, opts = {}) - # child_type = parent_type == :group ? :member : :group - # - # if parent_type == :member - # member_backwards_compatibility = %[ - # # Deprecated: for backwards-compatibility - # if (group_class_name = opts.delete :group_class_name) - # self.default_group_class_name = group_class_name - # end - # ] - # end - # - # klass.class_eval %[ - # opts = #{opts.inspect} - # # Get defaults from parent class for STI - # self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) - # self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) - # - # if (#{child_type}_association_names = opts.delete :#{child_type}s) - # has_#{child_type}s(#{child_type}_association_names) - # end - # - # #{member_backwards_compatibility} - # - # if (default_#{child_type}s = opts.delete :default_#{child_type}s) - # self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify - # # Only use as the association name if none specified (backwards-compatibility) - # self.default_#{child_type}s_association_name ||= default_#{child_type}s - # end - # - # if default_#{child_type}s_association_name - # has_#{child_type}(default_#{child_type}s_association_name, - # source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), - # class_name: default_#{child_type}_class_name - # ) - # end - # ] - # - # # children_name = :"#{child_type}s" - # # default_children_type = :"default_#{children_name}" - # # default_association_method = :"#{default_children_type}_association_name" - # # has_child_method = :"has_#{children_name}" - # # - # # # Get defaults from parent class for STI - # # [:class_name, :association_name].each do |setting| - # # default_setting = Groupify.__send__(:"#{child_type}_#{setting}") - # # superclass_setting = Groupify.superclass_fetch(klass, :"#{default_children_type}_#{setting}", default_setting) - # # - # # klass.__send__(:"#{default_children_type}_#{setting}=", superclass_setting) - # # end - # # - # # if (association_names = opts.delete children_name) - # # klass.__send__(has_child_method, association_names) - # # end - # # - # # if (association_name = opts.delete default_children_type) - # # klass.__send__(:"default_#{child_type}_class_name=", association_name.to_s.classify) - # # # Only use as the association name if none specified (backwards-compatibility) - # # unless klass.__send__(default_association_method) - # # klass.__send__(:"#{default_association_method}=", association_name) - # # end - # # end - # # - # # if (default_association_name = klass.__send__(default_association_method)) - # # default_class_name = klass.default_member_class_name - # # - # # klass.__send__(has_child_method, default_association_name, - # # source_type: ActiveRecord.base_class_name(default_class_name), - # # class_name: default_class_name - # # ) - # # end - # end - attr_reader :parent_type, :child_type def initialize(parent, parent_type) @@ -83,7 +10,7 @@ def initialize(parent, parent_type) end def find_memberships_for_children(children, opts = {}) - memberships_association.__send__(:"for_#{@child_type}s", children).as(opts[:as]) + memberships.__send__(:"for_#{@child_type}s", children).as(opts[:as]) end def add_children(children, opts = {}) @@ -102,12 +29,12 @@ def add_children(children, opts = {}) children.each do |child| # add to collection without membership type unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } - to_add_directly << memberships_association.build(@child_type => child) + to_add_directly << memberships.build(@child_type => child) end # add a second entry for the given membership type if membership_type.present? - membership = memberships_association. + membership = memberships. merge(memberships_association_for(child, @child_type)). as(membership_type). first_or_initialize @@ -133,7 +60,7 @@ def add_children(children, opts = {}) end # create memberships without membership type - memberships_association << to_add_directly + memberships << to_add_directly # create memberships with membership type to_add_with_membership_type. @@ -146,13 +73,11 @@ def add_children(children, opts = {}) @parent end - def children_association - association_name = @parent.class.__send__(:"default_#{@child_type}s_association_name") - - @parent.__send__(association_name) if association_name + def children + @parent.class.__send__(:"polymorphic_#{@child_type}s") end - def memberships_association + def memberships memberships_association_for(@parent, @parent_type) end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index a3b4ce0..b5ecf6b 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -3,11 +3,13 @@ module ActiveRecord class PolymorphicRelation < PolymorphicCollection include CollectionExtensions + attr_reader :collection, :parent_proxy + def initialize(parent_proxy, &group_membership_filter) @parent_proxy = parent_proxy super(parent_proxy.child_type) do - query = merge(parent_proxy.memberships_association) + query = merge(parent_proxy.memberships) query = query.instance_eval(&group_membership_filter) if block_given? query end @@ -18,14 +20,6 @@ def as(membership_type) self end - - attr_reader :collection, :parent_proxy - - protected - - def default_association - @parent_proxy.children_association - end end end end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 952c333..644d623 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -131,43 +131,6 @@ expect(user.polymorphic_groups.count).to eq(2) expect(user.group_memberships_as_member.count).to eq(4) end - - context "default group association disabled" do - let(:user) { TestUser.create! } - let(:group) { TestGroup.create! } - - before do - @previous_groups_association_name = Groupify.groups_association_name - @previous_members_association_name = Groupify.members_association_name - Groupify.groups_association_name = false - Groupify.members_association_name = false - - class TestUser < ActiveRecord::Base - self.table_name = 'users' - groupify :group_member - end - - class TestGroup < ActiveRecord::Base - self.table_name = 'groups' - groupify :group - end - end - - after do - Groupify.groups_association_name = @previous_groups_association_name - end - - it "there is no groups or members association" do - expect(Groupify.groups_association_name).to eq(false) - expect(user.class.default_groups_association_name).to eq(false) - expect(user.as_member.children_association).to eq(nil) - - expect(Groupify.members_association_name).to eq(false) - expect(group.class.default_members_association_name).to eq(false) - expect(group.as_group.children_association).to eq(nil) - end - end - end end end From d9305e26622146def672874e9ed30c8f1f4477c9 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 11 Aug 2017 01:11:11 -0400 Subject: [PATCH 165/205] Update generator with default configuration comments --- .../initializer/templates/initializer.rb | 10 ++++++++-- .../mongoid/initializer/templates/initializer.rb | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/generators/groupify/active_record/initializer/templates/initializer.rb b/lib/generators/groupify/active_record/initializer/templates/initializer.rb index c722f5e..760a4da 100644 --- a/lib/generators/groupify/active_record/initializer/templates/initializer.rb +++ b/lib/generators/groupify/active_record/initializer/templates/initializer.rb @@ -1,9 +1,15 @@ Groupify.configure do |config| - # Configure the default group class name. - # Defaults to 'Group' + # Configure the default group and member class names. + # Default to `nil` # config.group_class_name = 'Group' + # config.member_class_name = 'User' # Configure the default group membership class name. # Defaults to 'GroupMembership' # config.group_membership_class_name = 'GroupMembership' + + # Configure the default group and member association names. + # Default to `false` - specify names to auto-create them + # config.groups_association_name = :groups + # config.members_association_name = :members end diff --git a/lib/generators/groupify/mongoid/initializer/templates/initializer.rb b/lib/generators/groupify/mongoid/initializer/templates/initializer.rb index 3632e71..760a4da 100644 --- a/lib/generators/groupify/mongoid/initializer/templates/initializer.rb +++ b/lib/generators/groupify/mongoid/initializer/templates/initializer.rb @@ -1,5 +1,15 @@ Groupify.configure do |config| - # Configure the default group class name. - # Defaults to 'Group' + # Configure the default group and member class names. + # Default to `nil` # config.group_class_name = 'Group' + # config.member_class_name = 'User' + + # Configure the default group membership class name. + # Defaults to 'GroupMembership' + # config.group_membership_class_name = 'GroupMembership' + + # Configure the default group and member association names. + # Default to `false` - specify names to auto-create them + # config.groups_association_name = :groups + # config.members_association_name = :members end From 48ec17a757069e763d6e0c4f60161c66e420d9e5 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 14:58:02 -0400 Subject: [PATCH 166/205] Note that groups and members need to be persisted first --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 221f971..636f6f7 100644 --- a/README.md +++ b/README.md @@ -326,8 +326,9 @@ See _Usage_ below for additional functionality, such as how to specify membershi ### Create groups and add members ```ruby -group = Group.new -user = User.new +# NOTE: ActiveRecord groups and members must be persisted before creating memberships. +group = Group.create! +user = User.create! user.groups << group # or From fb89783855a7557e4de71b801c378daf43a360b2 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 15:39:58 -0400 Subject: [PATCH 167/205] Add method to get membership types for a group/member combo --- lib/groupify/adapter/active_record/group.rb | 10 ++++++++ .../adapter/active_record/group_member.rb | 10 ++++++++ .../adapter/active_record/group_membership.rb | 2 +- .../active_record/named_group_member.rb | 10 ++++++++ spec/active_record_spec.rb | 25 +++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 97457b2..1d8b6f4 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -33,6 +33,16 @@ def polymorphic_members(&group_membership_filter) PolymorphicRelation.new(as_group, &group_membership_filter) end + # returns `nil` membership type with results + def membership_types_for_member(member) + group_memberships_as_group. + for_members([member]). + select(:membership_type). + distinct. + pluck(:membership_type). + sort_by(&:to_s) + end + def member_classes self.class.member_classes end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 79ea2da..a7ac90b 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -33,6 +33,16 @@ def polymorphic_groups(&group_membership_filter) PolymorphicRelation.new(as_member, &group_membership_filter) end + # returns `nil` membership type with results + def membership_types_for_group(group) + group_memberships_as_member. + for_groups([group]). + select(:membership_type). + distinct. + pluck(:membership_type). + sort_by(&:to_s) + end + def in_group?(group, opts = {}) return false unless group.present? diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 2441c50..48a481d 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -18,7 +18,7 @@ module GroupMembership end def membership_type=(membership_type) - self[:membership_type] = membership_type.to_s if membership_type.present? + super(membership_type.to_s) if membership_type.present? end def as=(membership_type) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 0395abc..4449d2d 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -32,6 +32,16 @@ def named_groups=(named_groups) end end + # returns `nil` membership type with results + def membership_types_for_named_group(named_group) + group_memberships_as_member. + where(group_name: named_group). + select(:membership_type). + distinct. + pluck(:membership_type). + sort_by(&:to_s) + end + def in_named_group?(named_group, opts = {}) named_groups.include?(named_group, opts) end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 644d623..85424a2 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -436,6 +436,31 @@ class ProjectMember < ActiveRecord::Base end end + context "when retrieving membership types" do + it "gets a list of membership types for a group" do + group.add user, as: :owner + group.add user, as: :admin + + expect(user.membership_types_for_group(group)).to include(nil, 'owner', 'admin') + end + + it "gets a list of membership types for a named group" do + project = Project.create! + + project.named_groups.add :workgroup, as: :owner + project.named_groups.add :workgroup, as: :admin + + expect(project.membership_types_for_named_group(:workgroup)).to include(nil, 'owner', 'admin') + end + + it "gets a list of membership types for a member" do + group.add user, as: :owner + group.add user, as: :admin + + expect(group.membership_types_for_member(user)).to include(nil, 'owner', 'admin') + end + end + context 'when merging groups' do let(:task) { Task.create! } let(:manager) { Manager.create! } From f302c0edd25212a5930e0e030c19d772110f1475 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 20:11:15 -0400 Subject: [PATCH 168/205] Add `ParentQueryBuilder` methods as extension methods to models --- .../active_record/association_extensions.rb | 72 +++++++++++++++++++ .../active_record/collection_extensions.rb | 4 -- lib/groupify/adapter/active_record/group.rb | 8 +-- .../adapter/active_record/group_member.rb | 16 ++--- lib/groupify/adapter/active_record/model.rb | 3 + .../active_record/named_group_member.rb | 16 ++--- .../active_record/parent_query_builder.rb | 54 -------------- .../active_record/polymorphic_collection.rb | 2 + .../active_record/polymorphic_relation.rb | 2 +- 9 files changed, 88 insertions(+), 89 deletions(-) delete mode 100644 lib/groupify/adapter/active_record/parent_query_builder.rb diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 23cc71c..b410fc2 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -27,3 +27,75 @@ def add_children(children, opts = {}) end end end + +def build_extension_module(module_name, parent_type, options = {}) + child_type = parent_type == :group ? :member : :group + + new_module = Module.new do + class_eval %Q( + def as(membership_type) + with_memberships{as(membership_type)} + end + + def with_memberships(opts = {}, &group_membership_filter) + criteria = [] + criteria << joins(:group_memberships_as_#{parent_type}) + criteria << opts[:criteria] if opts[:criteria] + criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? + + # merge all criteria together + criteria.compact.reduce(:merge) + end + ) + + if options[:child_methods] + class_eval %Q( + def with_#{child_type}s(child_or_children) + scope = if child_or_children.is_a?(::ActiveRecord::Base) + # single child + with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) + else + with_memberships{for_#{child_type}s(child_or_children)} + end + + if block_given? + scope = scope.with_memberships(&group_membership_filter) + end + + scope + end + + def without_#{child_type}s(children) + with_memberships{not_for_#{child_type}s(children)} + end + + def delete(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + def destroy(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + # Defined to create alias methods before + # the association is extended with this module + def <<(*children) + opts = children.extract_options!.merge(exception_on_invalidation: false) + add_children(children.flatten, opts) + end + + def add(*children) + opts = children.extract_options!.merge(exception_on_invalidation: true) + add_children(children.flatten, opts) + end + + ) + end + end + + Groupify::ActiveRecord.const_set(module_name, new_module) +end + +build_extension_module("GroupScopeExtensions", :group, child_methods: true) +build_extension_module("GroupMemberScopeExtensions", :member, child_methods: true) +build_extension_module("NamedGroupMemberScopeExtensions", :member) diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index eea9dc4..ce7aafe 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -1,10 +1,6 @@ module Groupify module ActiveRecord module CollectionExtensions - def as(membership_type) - collection.merge(Groupify.group_membership_klass.as(membership_type)) - end - def delete(*records) remove_children(records, :destroy, records.extract_options![:as]) end diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 1d8b6f4..32a7a82 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -62,7 +62,7 @@ def merge!(source) module ClassMethods def with_member(member) - group_finder.with_children(member) + with_members(member) end # Returns the member classes defined for this class, as well as for the super classes @@ -112,7 +112,7 @@ def has_member(association_name, opts = {}, &extension) def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination invalid_member_classes = source_group.member_classes - destination_group.member_classes - invalid_found = invalid_member_classes.any?{ |klass| klass.member_finder.with_children(source_group).count > 0 } + invalid_found = invalid_member_classes.any?{ |klass| klass.with_groups(source_group).count > 0 } if invalid_found raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}") @@ -129,10 +129,6 @@ def merge!(source_group, destination_group) source_group.destroy end end - - def group_finder - @group_finder ||= ParentQueryBuilder.new(self, :group) - end end end end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index a7ac90b..31a1b6b 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -72,17 +72,13 @@ def shares_any_group?(other, opts = {}) end module ClassMethods - def as(membership_type) - member_finder.as(membership_type) - end - def in_group(group) - group.present? ? member_finder.with_children(group).distinct : none + group.present? ? with_groups(group).distinct : none end def in_any_group(*groups) groups.flatten! - groups.present? ? member_finder.with_children(groups).distinct : none + groups.present? ? with_groups(groups).distinct : none end def in_all_groups(*groups) @@ -94,7 +90,7 @@ def in_all_groups(*groups) # Count distinct on ID and type combo concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" - member_finder.with_children(groups). + with_groups(groups). group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{concatenated_columns}) = ?", groups.count). distinct @@ -111,7 +107,7 @@ def in_only_groups(*groups) end def in_other_groups(*groups) - member_finder.without_children(groups) + without_groups(groups) end def shares_any_group(other) @@ -152,10 +148,6 @@ def has_group(association_name, opts = {}, &extension) self end - - def member_finder - @member_finder ||= ParentQueryBuilder.new(self, :member) - end end end end diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index cdad093..32041fd 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -20,6 +20,7 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group + extend Groupify::ActiveRecord::GroupScopeExtensions # Get defaults from parent class for STI self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) @@ -45,6 +46,7 @@ def acts_as_group(opts = {}) def acts_as_group_member(opts = {}) include Groupify::ActiveRecord::GroupMember + extend Groupify::ActiveRecord::GroupMemberScopeExtensions # Get defaults from parent class for STI self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) @@ -73,6 +75,7 @@ def acts_as_group_member(opts = {}) def acts_as_named_group_member(opts = {}) include Groupify::ActiveRecord::NamedGroupMember + extend Groupify::ActiveRecord::NamedGroupMemberScopeExtensions end def acts_as_group_membership(opts = {}) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 4449d2d..f8cbe37 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -66,28 +66,24 @@ def shares_any_named_group?(other, opts = {}) end module ClassMethods - def as(membership_type) - named_member_finder.as(membership_type) - end - def in_named_group(named_group) return none unless named_group.present? - named_member_finder.with_memberships{where(group_name: named_group)}.distinct + with_memberships{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_finder.with_memberships{where(group_name: named_groups.flatten)}.distinct + with_memberships{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - named_member_finder.with_memberships{where(group_name: named_groups)}. + with_memberships{where(group_name: named_groups)}. group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct @@ -103,16 +99,12 @@ def in_only_named_groups(*named_groups) end def in_other_named_groups(*named_groups) - named_member_finder.with_memberships{where.not(group_name: named_groups)} + with_memberships{where.not(group_name: named_groups)} end def shares_any_named_group(other) in_any_named_group(other.named_groups.to_a) end - - def named_member_finder - @named_member_finder ||= ParentQueryBuilder.new(self, :member) - end end end end diff --git a/lib/groupify/adapter/active_record/parent_query_builder.rb b/lib/groupify/adapter/active_record/parent_query_builder.rb deleted file mode 100644 index d89c889..0000000 --- a/lib/groupify/adapter/active_record/parent_query_builder.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Groupify - module ActiveRecord - class ParentQueryBuilder < SimpleDelegator - def initialize(scope, parent_type) - @scope = scope.all.extending(Groupify::ActiveRecord::AssociationExtensions) - @parent_type = parent_type - @child_type = parent_type == :group ? :member : :group - - super(@scope) - end - - def as(membership_type) - with_memberships{as(membership_type)} - end - - def with_children(child_or_children) - scope = if child_or_children.is_a?(::ActiveRecord::Base) - # single child - with_memberships(criteria: child_or_children.__send__(:"group_memberships_as_#{@child_type}")) - else - method_name = :"for_#{@child_type}s" - with_memberships{__send__(method_name, child_or_children)} - end - - if block_given? - scope = scope.with_memberships(&group_membership_filter) - end - - scope - end - - def without_children(children) - method_name = :"not_for_#{@child_type}s" - with_memberships{__send__(method_name, children)} - end - - def with_memberships(opts = {}, &group_membership_filter) - criteria = [] - criteria << @scope.joins(:"group_memberships_as_#{@parent_type}") - criteria << opts[:criteria] if opts[:criteria] - criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? - - # merge all criteria together - wrap criteria.compact.reduce(:merge) - end - - protected - - def wrap(scope) - self.class.new(scope, @parent_type) - end - end - end -end diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 07ebfb0..de319ac 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -40,6 +40,8 @@ def build_collection(&group_membership_filter) collection = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) collection = collection.instance_eval(&group_membership_filter) if block_given? collection = collection.includes(@source) + + collection end def distinct_compat diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index b5ecf6b..1909486 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -16,7 +16,7 @@ def initialize(parent_proxy, &group_membership_filter) end def as(membership_type) - @collection = super + @collection = @collection.as(membership_type) self end From 8768892efb3a0891bc1dce9536d166cef9ec39d0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 21:26:15 -0400 Subject: [PATCH 169/205] Build extensions in separate files for clarity --- lib/groupify/adapter/active_record.rb | 71 +++++++++++++++++- .../active_record/association_extensions.rb | 72 ------------------- .../group_member_scope_extensions.rb | 1 + .../active_record/group_scope_extensions.rb | 1 + lib/groupify/adapter/active_record/model.rb | 6 ++ .../named_group_member_scope_extensions.rb | 1 + 6 files changed, 79 insertions(+), 73 deletions(-) create mode 100644 lib/groupify/adapter/active_record/group_member_scope_extensions.rb create mode 100644 lib/groupify/adapter/active_record/group_scope_extensions.rb create mode 100644 lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index ec37be9..7379548 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -54,7 +54,7 @@ def self.create_children_association(klass, association_name, opts = {}) }.merge(opts) model_klass - + rescue NameError => ex re = /has_(group|member)/ line = ex.backtrace.find{ |i| i =~ re } @@ -65,5 +65,74 @@ def self.create_children_association(klass, association_name, opts = {}) raise message.join(' ') end + + def self.build_scope_module(module_name, parent_type, options = {}) + child_type = parent_type == :group ? :member : :group + + new_module = Module.new do + class_eval %Q( + def as(membership_type) + with_memberships{as(membership_type)} + end + + def with_memberships(opts = {}, &group_membership_filter) + criteria = [] + criteria << joins(:group_memberships_as_#{parent_type}) + criteria << opts[:criteria] if opts[:criteria] + criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? + + # merge all criteria together + criteria.compact.reduce(:merge) + end + ) + + if options[:child_methods] + class_eval %Q( + def with_#{child_type}s(child_or_children) + scope = if child_or_children.is_a?(::ActiveRecord::Base) + # single child + with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) + else + with_memberships{for_#{child_type}s(child_or_children)} + end + + if block_given? + scope = scope.with_memberships(&group_membership_filter) + end + + scope + end + + def without_#{child_type}s(children) + with_memberships{not_for_#{child_type}s(children)} + end + + def delete(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + def destroy(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + # Defined to create alias methods before + # the association is extended with this module + def <<(*children) + opts = children.extract_options!.merge(exception_on_invalidation: false) + add_children(children.flatten, opts) + end + + def add(*children) + opts = children.extract_options!.merge(exception_on_invalidation: true) + add_children(children.flatten, opts) + end + + ) + end + end + + Groupify::ActiveRecord.const_set(module_name, new_module) + end + end end diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index b410fc2..23cc71c 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -27,75 +27,3 @@ def add_children(children, opts = {}) end end end - -def build_extension_module(module_name, parent_type, options = {}) - child_type = parent_type == :group ? :member : :group - - new_module = Module.new do - class_eval %Q( - def as(membership_type) - with_memberships{as(membership_type)} - end - - def with_memberships(opts = {}, &group_membership_filter) - criteria = [] - criteria << joins(:group_memberships_as_#{parent_type}) - criteria << opts[:criteria] if opts[:criteria] - criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? - - # merge all criteria together - criteria.compact.reduce(:merge) - end - ) - - if options[:child_methods] - class_eval %Q( - def with_#{child_type}s(child_or_children) - scope = if child_or_children.is_a?(::ActiveRecord::Base) - # single child - with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) - else - with_memberships{for_#{child_type}s(child_or_children)} - end - - if block_given? - scope = scope.with_memberships(&group_membership_filter) - end - - scope - end - - def without_#{child_type}s(children) - with_memberships{not_for_#{child_type}s(children)} - end - - def delete(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - def destroy(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - # Defined to create alias methods before - # the association is extended with this module - def <<(*children) - opts = children.extract_options!.merge(exception_on_invalidation: false) - add_children(children.flatten, opts) - end - - def add(*children) - opts = children.extract_options!.merge(exception_on_invalidation: true) - add_children(children.flatten, opts) - end - - ) - end - end - - Groupify::ActiveRecord.const_set(module_name, new_module) -end - -build_extension_module("GroupScopeExtensions", :group, child_methods: true) -build_extension_module("GroupMemberScopeExtensions", :member, child_methods: true) -build_extension_module("NamedGroupMemberScopeExtensions", :member) diff --git a/lib/groupify/adapter/active_record/group_member_scope_extensions.rb b/lib/groupify/adapter/active_record/group_member_scope_extensions.rb new file mode 100644 index 0000000..eeadae5 --- /dev/null +++ b/lib/groupify/adapter/active_record/group_member_scope_extensions.rb @@ -0,0 +1 @@ +Groupify::ActiveRecord.build_scope_module("GroupMemberScopeExtensions", :member, child_methods: true) diff --git a/lib/groupify/adapter/active_record/group_scope_extensions.rb b/lib/groupify/adapter/active_record/group_scope_extensions.rb new file mode 100644 index 0000000..1ad36e8 --- /dev/null +++ b/lib/groupify/adapter/active_record/group_scope_extensions.rb @@ -0,0 +1 @@ +Groupify::ActiveRecord.build_scope_module("GroupScopeExtensions", :group, child_methods: true) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 32041fd..01fee25 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -19,6 +19,8 @@ def groupify(type, opts = {}) end def acts_as_group(opts = {}) + require 'groupify/adapter/active_record/group_scope_extensions' + include Groupify::ActiveRecord::Group extend Groupify::ActiveRecord::GroupScopeExtensions @@ -45,6 +47,8 @@ def acts_as_group(opts = {}) end def acts_as_group_member(opts = {}) + require 'groupify/adapter/active_record/group_member_scope_extensions' + include Groupify::ActiveRecord::GroupMember extend Groupify::ActiveRecord::GroupMemberScopeExtensions @@ -74,6 +78,8 @@ def acts_as_group_member(opts = {}) end def acts_as_named_group_member(opts = {}) + require 'groupify/adapter/active_record/named_group_member_scope_extensions' + include Groupify::ActiveRecord::NamedGroupMember extend Groupify::ActiveRecord::NamedGroupMemberScopeExtensions end diff --git a/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb b/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb new file mode 100644 index 0000000..ff07633 --- /dev/null +++ b/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb @@ -0,0 +1 @@ +Groupify::ActiveRecord.build_scope_module("NamedGroupMemberScopeExtensions", :member) From 10c370adf1bef040b91d2c3ee6793d356e9c6405 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 22:32:59 -0400 Subject: [PATCH 170/205] Generate extension modules rather than using helper classes --- lib/groupify/adapter/active_record.rb | 69 ------------- .../active_record/association_extensions.rb | 11 ++- .../active_record/collection_extensions.rb | 14 ++- lib/groupify/adapter/active_record/group.rb | 8 +- .../adapter/active_record/group_member.rb | 6 +- .../group_member_scope_extensions.rb | 1 - .../active_record/group_scope_extensions.rb | 1 - lib/groupify/adapter/active_record/model.rb | 17 ++-- .../model_membership_extensions.rb | 91 +++++++++++++++++ .../active_record/model_scope_extensions.rb | 80 +++++++++++++++ .../named_group_member_scope_extensions.rb | 1 - .../adapter/active_record/parent_proxy.rb | 99 ------------------- .../active_record/polymorphic_collection.rb | 16 +-- .../active_record/polymorphic_relation.rb | 11 ++- 14 files changed, 212 insertions(+), 213 deletions(-) delete mode 100644 lib/groupify/adapter/active_record/group_member_scope_extensions.rb delete mode 100644 lib/groupify/adapter/active_record/group_scope_extensions.rb create mode 100644 lib/groupify/adapter/active_record/model_membership_extensions.rb create mode 100644 lib/groupify/adapter/active_record/model_scope_extensions.rb delete mode 100644 lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb delete mode 100644 lib/groupify/adapter/active_record/parent_proxy.rb diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 7379548..bb99af9 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -65,74 +65,5 @@ def self.create_children_association(klass, association_name, opts = {}) raise message.join(' ') end - - def self.build_scope_module(module_name, parent_type, options = {}) - child_type = parent_type == :group ? :member : :group - - new_module = Module.new do - class_eval %Q( - def as(membership_type) - with_memberships{as(membership_type)} - end - - def with_memberships(opts = {}, &group_membership_filter) - criteria = [] - criteria << joins(:group_memberships_as_#{parent_type}) - criteria << opts[:criteria] if opts[:criteria] - criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? - - # merge all criteria together - criteria.compact.reduce(:merge) - end - ) - - if options[:child_methods] - class_eval %Q( - def with_#{child_type}s(child_or_children) - scope = if child_or_children.is_a?(::ActiveRecord::Base) - # single child - with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) - else - with_memberships{for_#{child_type}s(child_or_children)} - end - - if block_given? - scope = scope.with_memberships(&group_membership_filter) - end - - scope - end - - def without_#{child_type}s(children) - with_memberships{not_for_#{child_type}s(children)} - end - - def delete(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - def destroy(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - # Defined to create alias methods before - # the association is extended with this module - def <<(*children) - opts = children.extract_options!.merge(exception_on_invalidation: false) - add_children(children.flatten, opts) - end - - def add(*children) - opts = children.extract_options!.merge(exception_on_invalidation: true) - add_children(children.flatten, opts) - end - - ) - end - end - - Groupify::ActiveRecord.const_set(module_name, new_module) - end - end end diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 23cc71c..932ca2b 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -5,11 +5,12 @@ module ActiveRecord module AssociationExtensions include CollectionExtensions - def parent_proxy - @parent_proxy ||= ParentProxy.new( - proxy_association.owner, - proxy_association.through_reflection.name == :group_memberships_as_group ? :group : :member - ) + def owner + proxy_association.owner + end + + def source_name + proxy_association.through_reflection.name == :group_memberships_as_group ? :member : :group end protected diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index ce7aafe..b31c6b5 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -25,22 +25,26 @@ def collection self end - def parent_proxy + def owner + raise "Not implemented" + end + + def source_name raise "Not implemented" end protected def add_children(children, opts = {}) - parent_proxy.add_children(children, opts) + owner.__send__(:"add_#{source_name}s", children, opts) end def remove_children(children, destruction_type, membership_type = nil) - parent_proxy. - find_memberships_for_children(children, as: membership_type). + owner. + __send__(:"find_memberships_for_#{source_name}s", children, as: membership_type). __send__(:"#{destruction_type}_all") - parent_proxy.clear_association_cache + owner.__send__(:clear_association_cache) children.each{|record| record.__send__(:clear_association_cache)} diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 32a7a82..2ae5aff 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -25,12 +25,8 @@ module Group class_name: Groupify.group_membership_class_name end - def as_group - @as_group ||= ParentProxy.new(self, :group) - end - def polymorphic_members(&group_membership_filter) - PolymorphicRelation.new(as_group, &group_membership_filter) + PolymorphicRelation.new(self, :member, &group_membership_filter) end # returns `nil` membership type with results @@ -50,7 +46,7 @@ def member_classes def add(*members) opts = members.extract_options! - as_group.add_children(members.flatten, opts) + add_members(members.flatten, opts) self end diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 31a1b6b..38536a3 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -25,12 +25,8 @@ module GroupMember class_name: Groupify.group_membership_class_name end - def as_member - @as_member ||= ParentProxy.new(self, :member) - end - def polymorphic_groups(&group_membership_filter) - PolymorphicRelation.new(as_member, &group_membership_filter) + PolymorphicRelation.new(self, :group, &group_membership_filter) end # returns `nil` membership type with results diff --git a/lib/groupify/adapter/active_record/group_member_scope_extensions.rb b/lib/groupify/adapter/active_record/group_member_scope_extensions.rb deleted file mode 100644 index eeadae5..0000000 --- a/lib/groupify/adapter/active_record/group_member_scope_extensions.rb +++ /dev/null @@ -1 +0,0 @@ -Groupify::ActiveRecord.build_scope_module("GroupMemberScopeExtensions", :member, child_methods: true) diff --git a/lib/groupify/adapter/active_record/group_scope_extensions.rb b/lib/groupify/adapter/active_record/group_scope_extensions.rb deleted file mode 100644 index 1ad36e8..0000000 --- a/lib/groupify/adapter/active_record/group_scope_extensions.rb +++ /dev/null @@ -1 +0,0 @@ -Groupify::ActiveRecord.build_scope_module("GroupScopeExtensions", :group, child_methods: true) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 01fee25..8819b65 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -4,6 +4,9 @@ module Model extend ActiveSupport::Concern included do + require 'groupify/adapter/active_record/model_scope_extensions' + require 'groupify/adapter/active_record/model_membership_extensions' + # Define a scope that returns nothing. # This is built into ActiveRecord 4, but not 3 unless self.class.respond_to? :none @@ -19,10 +22,9 @@ def groupify(type, opts = {}) end def acts_as_group(opts = {}) - require 'groupify/adapter/active_record/group_scope_extensions' - include Groupify::ActiveRecord::Group - extend Groupify::ActiveRecord::GroupScopeExtensions + include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group) + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group, child_methods: true) # Get defaults from parent class for STI self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) @@ -47,10 +49,9 @@ def acts_as_group(opts = {}) end def acts_as_group_member(opts = {}) - require 'groupify/adapter/active_record/group_member_scope_extensions' - include Groupify::ActiveRecord::GroupMember - extend Groupify::ActiveRecord::GroupMemberScopeExtensions + include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group_member) + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group_member, child_methods: true) # Get defaults from parent class for STI self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) @@ -78,10 +79,8 @@ def acts_as_group_member(opts = {}) end def acts_as_named_group_member(opts = {}) - require 'groupify/adapter/active_record/named_group_member_scope_extensions' - include Groupify::ActiveRecord::NamedGroupMember - extend Groupify::ActiveRecord::NamedGroupMemberScopeExtensions + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:named_group_member) end def acts_as_group_membership(opts = {}) diff --git a/lib/groupify/adapter/active_record/model_membership_extensions.rb b/lib/groupify/adapter/active_record/model_membership_extensions.rb new file mode 100644 index 0000000..a45c978 --- /dev/null +++ b/lib/groupify/adapter/active_record/model_membership_extensions.rb @@ -0,0 +1,91 @@ +module Groupify + module ActiveRecord + module ModelMembershipExtensions + def self.build_for(parent_type, options = {}) + module_name = "#{parent_type.to_s.classify}MembershipExtensions" + + const_get(module_name.to_sym) + rescue NameError + # convert :group_member and :named_group_member + parent_type = :member unless parent_type == :group + child_type = parent_type == :group ? :member : :group + + new_module = Module.new do + class_eval %Q( + def find_memberships_for_#{child_type}s(children, opts = {}) + group_memberships_as_#{parent_type}.for_#{child_type}s(children).as(opts[:as]) + end + + def add_#{child_type}s(children, opts = {}) + return self if children.none? + + clear_association_cache_for(self) + + membership_type = opts[:as] + + to_add_directly = [] + to_add_with_membership_type = [] + + already_children = find_memberships_for_#{child_type}s(children).includes(@child_type).group_by{ |membership| membership.#{child_type} } + + # first prepare changes + children.each do |child| + # add to collection without membership type + unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } + to_add_directly << group_memberships_as_#{parent_type}.build(#{child_type}: child) + end + + # add a second entry for the given membership type + if membership_type.present? + membership = group_memberships_as_#{parent_type}. + merge(child.group_memberships_as_#{child_type}). + as(membership_type). + first_or_initialize + to_add_with_membership_type << membership unless membership.persisted? + end + + clear_association_cache_for(child) + end + + clear_association_cache_for(self) + + # then validate changes + list_to_validate = to_add_directly + to_add_with_membership_type + + list_to_validate.each do |child| + next if child.valid? + + if opts[:exception_on_invalidation] + raise ::ActiveRecord::RecordInvalid.new(child) + else + return false + end + end + + # create memberships without membership type + group_memberships_as_#{parent_type} << to_add_directly + + # create memberships with membership type + to_add_with_membership_type. + group_by{ |membership| membership.#{parent_type} }. + each do |membership_parent, memberships| + membership_parent.group_memberships_as_#{parent_type} << memberships + clear_association_cache_for(membership_parent) + end + + self + end + + protected + + def clear_association_cache_for(record) + record.__send__(:clear_association_cache) + end + ) + end + + self.const_set(module_name, new_module) + end + end + end +end diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb new file mode 100644 index 0000000..8b8d14b --- /dev/null +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -0,0 +1,80 @@ +module Groupify + module ActiveRecord + module ModelScopeExtensions + def self.build_for(parent_type, options = {}) + module_name = "#{parent_type.to_s.classify}ScopeExtensions" + + const_get(module_name.to_sym) + rescue NameError + # convert :group_member and :named_group_member + parent_type = :member unless parent_type == :group + child_type = parent_type == :group ? :member : :group + + new_module = Module.new do + base_methods = %Q( + def as(membership_type) + with_memberships{as(membership_type)} + end + + def with_memberships(opts = {}, &group_membership_filter) + criteria = [] + criteria << joins(:group_memberships_as_#{parent_type}) + criteria << opts[:criteria] if opts[:criteria] + criteria << Groupify.group_membership_klass.instance_eval(&group_membership_filter) if block_given? + + # merge all criteria together + criteria.compact.reduce(:merge) + end + ) + + child_methods = %Q( + def with_#{child_type}s(child_or_children) + scope = if child_or_children.is_a?(::ActiveRecord::Base) + # single child + with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) + else + with_memberships{for_#{child_type}s(child_or_children)} + end + + if block_given? + scope = scope.with_memberships(&group_membership_filter) + end + + scope + end + + def without_#{child_type}s(children) + with_memberships{not_for_#{child_type}s(children)} + end + + def delete(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + def destroy(*records) + remove_children(records, :destroy, records.extract_options![:as]) + end + + # Defined to create alias methods before + # the association is extended with this module + def <<(*children) + opts = children.extract_options!.merge(exception_on_invalidation: false) + add_children(children.flatten, opts) + end + + def add(*children) + opts = children.extract_options!.merge(exception_on_invalidation: true) + add_children(children.flatten, opts) + end + + ) + + class_eval(base_methods) + class_eval(child_methods) if options[:child_methods] + end + + const_set(module_name, new_module) + end + end + end +end diff --git a/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb b/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb deleted file mode 100644 index ff07633..0000000 --- a/lib/groupify/adapter/active_record/named_group_member_scope_extensions.rb +++ /dev/null @@ -1 +0,0 @@ -Groupify::ActiveRecord.build_scope_module("NamedGroupMemberScopeExtensions", :member) diff --git a/lib/groupify/adapter/active_record/parent_proxy.rb b/lib/groupify/adapter/active_record/parent_proxy.rb deleted file mode 100644 index 6080083..0000000 --- a/lib/groupify/adapter/active_record/parent_proxy.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Groupify - module ActiveRecord - class ParentProxy - - attr_reader :parent_type, :child_type - - def initialize(parent, parent_type) - @parent, @parent_type = parent, parent_type - @child_type = parent_type == :group ? :member : :group - end - - def find_memberships_for_children(children, opts = {}) - memberships.__send__(:"for_#{@child_type}s", children).as(opts[:as]) - end - - def add_children(children, opts = {}) - return @parent if children.none? - - clear_association_cache - - membership_type = opts[:as] - - to_add_directly = [] - to_add_with_membership_type = [] - - already_children = find_memberships_for_children(children).includes(@child_type).group_by{ |membership| membership.__send__(@child_type) } - - # first prepare changes - children.each do |child| - # add to collection without membership type - unless already_children[child] && already_children[child].find{ |m| m.membership_type.nil? } - to_add_directly << memberships.build(@child_type => child) - end - - # add a second entry for the given membership type - if membership_type.present? - membership = memberships. - merge(memberships_association_for(child, @child_type)). - as(membership_type). - first_or_initialize - to_add_with_membership_type << membership unless membership.persisted? - end - - clear_association_cache_for(child) - end - - clear_association_cache - - # then validate changes - list_to_validate = to_add_directly + to_add_with_membership_type - - list_to_validate.each do |child| - next if child.valid? - - if opts[:exception_on_invalidation] - raise ::ActiveRecord::RecordInvalid.new(child) - else - return false - end - end - - # create memberships without membership type - memberships << to_add_directly - - # create memberships with membership type - to_add_with_membership_type. - group_by{ |membership| membership.__send__(@parent_type) }. - each do |membership_parent, memberships| - memberships_association_for(membership_parent, @parent_type) << memberships - clear_association_cache_for(membership_parent) - end - - @parent - end - - def children - @parent.class.__send__(:"polymorphic_#{@child_type}s") - end - - def memberships - memberships_association_for(@parent, @parent_type) - end - - def clear_association_cache - clear_association_cache_for(@parent) - end - - private - - def memberships_association_for(record, source) - record.__send__(:"group_memberships_as_#{source}") - end - - def clear_association_cache_for(record) - record.__send__(:clear_association_cache) - end - end - end -end diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index de319ac..7abe329 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -4,14 +4,16 @@ class PolymorphicCollection include Enumerable extend Forwardable - def initialize(source, &group_membership_filter) - @source = source + attr_reader :source + + def initialize(source_name, &group_membership_filter) + @source_name = source_name @collection = build_collection(&group_membership_filter) end def each(&block) distinct_compat.map do |group_membership| - group_membership.__send__(@source).tap(&block) + group_membership.__send__(@source_name).tap(&block) end end @@ -37,15 +39,15 @@ def inspect protected def build_collection(&group_membership_filter) - collection = Groupify.group_membership_klass.where.not(:"#{@source}_id" => nil) + collection = Groupify.group_membership_klass.where.not(:"#{@source_name}_id" => nil) collection = collection.instance_eval(&group_membership_filter) if block_given? - collection = collection.includes(@source) + collection = collection.includes(@source_name) collection end def distinct_compat - id, type = ActiveRecord.quote("#{@source}_id"), ActiveRecord.quote("#{@source}_type") + id, type = ActiveRecord.quote("#{@source_name}_id"), ActiveRecord.quote("#{@source_name}_type") # Workaround to "group by" multiple columns in PostgreSQL if ActiveRecord.is_db?('postgres') @@ -59,7 +61,7 @@ def count_compat # Workaround to "count distinct" on multiple columns in PostgreSQL # (uses different syntax when aggregating distinct) if ActiveRecord.is_db?('postgres') - id, type = ActiveRecord.quote("#{@source}_id"), ActiveRecord.quote("#{@source}_type") + id, type = ActiveRecord.quote("#{@source_name}_id"), ActiveRecord.quote("#{@source_name}_type") queried_count = @collection.select("DISTINCT (#{id}, #{type})").count else diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 1909486..5e2583b 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -3,13 +3,14 @@ module ActiveRecord class PolymorphicRelation < PolymorphicCollection include CollectionExtensions - attr_reader :collection, :parent_proxy + attr_reader :collection - def initialize(parent_proxy, &group_membership_filter) - @parent_proxy = parent_proxy + def initialize(owner, source_name, &group_membership_filter) + @owner = owner + parent_type = source_name == :group ? :member : :group - super(parent_proxy.child_type) do - query = merge(parent_proxy.memberships) + super(source_name) do + query = merge(owner.__send__(:"group_memberships_as_#{parent_type}")) query = query.instance_eval(&group_membership_filter) if block_given? query end From 7fba3364b9d0ce0514ee7e1b34c6854026e2936e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 16 Aug 2017 23:53:11 -0400 Subject: [PATCH 171/205] Pass extension block to `has_many` --- lib/groupify/adapter/active_record.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index bb99af9..9e25d7b 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -41,7 +41,7 @@ def self.base_class_name(model_class, default_base_class = nil) raise end - def self.create_children_association(klass, association_name, opts = {}) + def self.create_children_association(klass, association_name, opts = {}, &extension) association_class, association_name = Groupify.infer_class_and_association_name(association_name) default_base_class = opts.delete(:default_base_class) model_klass = opts[:class_name] || association_class || default_base_class @@ -51,7 +51,7 @@ def self.create_children_association(klass, association_name, opts = {}) klass.has_many association_name, ->{ distinct }, { extend: Groupify::ActiveRecord::AssociationExtensions - }.merge(opts) + }.merge(opts), &extension model_klass From d95c5944f16f83f6cf0fa9798ea9745b3f520126 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 00:22:14 -0400 Subject: [PATCH 172/205] Continued de-duplication of code with dynamic module creation --- lib/groupify/adapter/active_record/group.rb | 72 +---------- .../adapter/active_record/group_member.rb | 58 +-------- lib/groupify/adapter/active_record/model.rb | 47 +------- .../model_membership_extensions.rb | 112 +++++++++++++++++- .../active_record/model_scope_extensions.rb | 21 ---- 5 files changed, 113 insertions(+), 197 deletions(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 2ae5aff..5bed74a 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -15,40 +15,13 @@ module Group extend ActiveSupport::Concern included do - @default_member_class_name = nil - @default_members_association_name = nil - @member_klasses ||= Set.new - - has_many :group_memberships_as_group, - dependent: :destroy, - as: :group, - class_name: Groupify.group_membership_class_name - end - - def polymorphic_members(&group_membership_filter) - PolymorphicRelation.new(self, :member, &group_membership_filter) - end - - # returns `nil` membership type with results - def membership_types_for_member(member) - group_memberships_as_group. - for_members([member]). - select(:membership_type). - distinct. - pluck(:membership_type). - sort_by(&:to_s) - end - - def member_classes - self.class.member_classes + include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group) end def add(*members) opts = members.extract_options! add_members(members.flatten, opts) - - self end # Merge a source group into this group. @@ -61,49 +34,6 @@ def with_member(member) with_members(member) end - # Returns the member classes defined for this class, as well as for the super classes - def member_classes - (@member_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :member_classes, [])) - end - - def default_member_class_name - @default_member_class_name ||= Groupify.member_class_name - end - - def default_member_class_name=(klass) - @default_member_class_name = klass - end - - def default_members_association_name - @default_members_association_name ||= Groupify.members_association_name - end - - def default_members_association_name=(name) - @default_members_association_name = name && name.to_sym - end - - # Define which classes are members of this group - def has_members(*association_names, &extension) - association_names.flatten.each do |association_name| - has_member(association_name, &extension) - end - end - - def has_member(association_name, opts = {}, &extension) - member_klass = ActiveRecord.create_children_association(self, association_name, - opts.merge( - through: :group_memberships_as_group, - source: :member, - default_base_class: default_member_class_name - ), - &extension - ) - - (@member_klasses ||= Set.new) << member_klass.to_s.constantize - - self - end - # Merge two groups. The members of the source become members of the destination, and the source is destroyed. def merge!(source_group, destination_group) # Ensure that all the members of the source can be members of the destination diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 38536a3..2367f73 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -15,28 +15,7 @@ module GroupMember extend ActiveSupport::Concern included do - @default_group_class_name = nil - @default_groups_association_name = nil - - has_many :group_memberships_as_member, - as: :member, - autosave: true, - dependent: :destroy, - class_name: Groupify.group_membership_class_name - end - - def polymorphic_groups(&group_membership_filter) - PolymorphicRelation.new(self, :group, &group_membership_filter) - end - - # returns `nil` membership type with results - def membership_types_for_group(group) - group_memberships_as_member. - for_groups([group]). - select(:membership_type). - distinct. - pluck(:membership_type). - sort_by(&:to_s) + include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group_member) end def in_group?(group, opts = {}) @@ -109,41 +88,6 @@ def in_other_groups(*groups) def shares_any_group(other) in_any_group(other.polymorphic_groups) end - - def has_groups(*association_names, &extension) - association_names.flatten.each do |association_name| - has_group(association_name, &extension) - end - end - - def default_group_class_name - @default_group_class_name ||= Groupify.group_class_name - end - - def default_group_class_name=(klass) - @default_group_class_name = klass - end - - def default_groups_association_name - @default_groups_association_name ||= Groupify.groups_association_name - end - - def default_groups_association_name=(name) - @default_groups_association_name = name && name.to_sym - end - - def has_group(association_name, opts = {}, &extension) - ActiveRecord.create_children_association(self, association_name, - opts.merge( - through: :group_memberships_as_member, - source: :group, - default_base_class: default_group_class_name - ), - &extension - ) - - self - end end end end diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 8819b65..66d8168 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -23,59 +23,16 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group - include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group) extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group, child_methods: true) - # Get defaults from parent class for STI - self.default_member_class_name = Groupify.superclass_fetch(self, :default_member_class_name, Groupify.member_class_name) - self.default_members_association_name = Groupify.superclass_fetch(self, :default_members_association_name, Groupify.members_association_name) - - if (member_association_names = opts.delete :members) - has_members(member_association_names) - end - - if (default_members = opts.delete :default_members) - self.default_member_class_name = default_members.to_s.classify - # Only use as the association name if none specified (backwards-compatibility) - self.default_members_association_name ||= default_members - end - - if default_members_association_name - has_member(default_members_association_name, - source_type: ActiveRecord.base_class_name(default_member_class_name), - class_name: default_member_class_name - ) - end + configure_group!(opts) end def acts_as_group_member(opts = {}) include Groupify::ActiveRecord::GroupMember - include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group_member) extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group_member, child_methods: true) - # Get defaults from parent class for STI - self.default_group_class_name = Groupify.superclass_fetch(self, :default_group_class_name, Groupify.group_class_name) - self.default_groups_association_name = Groupify.superclass_fetch(self, :default_groups_association_name, Groupify.groups_association_name) - - if (group_association_names = opts.delete :groups) - has_groups(group_association_names) - end - - if (default_groups = opts.delete :default_groups) - self.default_group_class_name = default_groups.to_s.classify - self.default_groups_association_name ||= default_groups - end - - # Deprecated: for backwards-compatibility - if (group_class_name = opts.delete :group_class_name) - self.default_group_class_name = group_class_name - end - - if default_groups_association_name - has_group default_groups_association_name, - source_type: ActiveRecord.base_class_name(default_group_class_name), - class_name: default_group_class_name - end + configure_group_member!(opts) end def acts_as_named_group_member(opts = {}) diff --git a/lib/groupify/adapter/active_record/model_membership_extensions.rb b/lib/groupify/adapter/active_record/model_membership_extensions.rb index a45c978..b188ce9 100644 --- a/lib/groupify/adapter/active_record/model_membership_extensions.rb +++ b/lib/groupify/adapter/active_record/model_membership_extensions.rb @@ -1,17 +1,123 @@ module Groupify module ActiveRecord module ModelMembershipExtensions - def self.build_for(parent_type, options = {}) - module_name = "#{parent_type.to_s.classify}MembershipExtensions" + def self.build_for(official_parent_type, options = {}) + module_name = "#{official_parent_type.to_s.classify}MembershipExtensions" const_get(module_name.to_sym) rescue NameError # convert :group_member and :named_group_member - parent_type = :member unless parent_type == :group + parent_type = official_parent_type == :group ? :group : :member child_type = parent_type == :group ? :member : :group new_module = Module.new do class_eval %Q( + extend ActiveSupport::Concern + + included do + @default_#{child_type}_class_name = nil + @default_#{child_type}s_association_name = nil + @#{child_type}_klasses ||= Set.new + + has_many :group_memberships_as_#{parent_type}, + as: :#{parent_type}, + autosave: true, + dependent: :destroy, + class_name: Groupify.group_membership_class_name + end + + module ClassMethods + def configure_#{official_parent_type}!(opts = {}) + # Get defaults from parent class for STI + self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) + self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) + + if (#{child_type}_association_names = opts.delete :#{child_type}s) + has_#{child_type}s(#{child_type}_association_names) + end + + if (default_#{child_type}s = opts.delete :default_#{child_type}s) + self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify + # Only use as the association name if none specified (backwards-compatibility) + self.default_#{child_type}s_association_name ||= default_#{child_type}s + end + + # Deprecated: for backwards-compatibility + if (#{child_type}_class_name = opts.delete :#{child_type}_class_name) + self.default_#{child_type}_class_name = #{child_type}_class_name + end + + if default_#{child_type}s_association_name + has_#{child_type}(default_#{child_type}s_association_name, + source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), + class_name: default_#{child_type}_class_name + ) + end + end + + def default_#{child_type}_class_name + @default_#{child_type}_class_name ||= Groupify.#{child_type}_class_name + end + + def default_#{child_type}_class_name=(klass) + @default_#{child_type}_class_name = klass + end + + def default_#{child_type}s_association_name + @default_#{child_type}s_association_name ||= Groupify.#{child_type}s_association_name + end + + def default_#{child_type}s_association_name=(name) + @default_#{child_type}s_association_name = name && name.to_sym + end + + # Returns the #{child_type} classes defined for this class, as well as for the super classes + def #{child_type}_classes + (@#{child_type}_klasses ||= Set.new).merge(Groupify.superclass_fetch(self, :#{child_type}_classes, [])) + end + + def has_#{child_type}s(*association_names, &extension) + association_names.flatten.each do |association_name| + has_#{child_type}(association_name, &extension) + end + end + + def has_#{child_type}(association_name, opts = {}, &extension) + #{child_type}_klass = ActiveRecord.create_children_association(self, association_name, + opts.merge( + through: :group_memberships_as_#{parent_type}, + source: :#{child_type}, + default_base_class: default_#{child_type}_class_name + ), + &extension + ) + + (@#{child_type}_klasses ||= Set.new) << #{child_type}_klass.to_s.constantize + rescue NameError + puts "Unable to add \#{#{child_type}_klass} to @#{child_type}_klasses" + ensure + self + end + end + + def polymorphic_#{child_type}s(&group_membership_filter) + PolymorphicRelation.new(self, :#{child_type}, &group_membership_filter) + end + + def #{child_type}_classes + self.class.#{child_type}_classes + end + + # returns `nil` membership type with results + def membership_types_for_#{child_type}(#{child_type}) + group_memberships_as_#{parent_type}. + for_#{child_type}s([#{child_type}]). + select(:membership_type). + distinct. + pluck(:membership_type). + sort_by(&:to_s) + end + def find_memberships_for_#{child_type}s(children, opts = {}) group_memberships_as_#{parent_type}.for_#{child_type}s(children).as(opts[:as]) end diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index 8b8d14b..d6fd007 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -46,27 +46,6 @@ def with_#{child_type}s(child_or_children) def without_#{child_type}s(children) with_memberships{not_for_#{child_type}s(children)} end - - def delete(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - def destroy(*records) - remove_children(records, :destroy, records.extract_options![:as]) - end - - # Defined to create alias methods before - # the association is extended with this module - def <<(*children) - opts = children.extract_options!.merge(exception_on_invalidation: false) - add_children(children.flatten, opts) - end - - def add(*children) - opts = children.extract_options!.merge(exception_on_invalidation: true) - add_children(children.flatten, opts) - end - ) class_eval(base_methods) From 3ef6176303a58370bd25d5200e2a27beb4d23102 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 00:24:47 -0400 Subject: [PATCH 173/205] Renamed `ModelMembershipExtensions` to `ModelExtensions` --- lib/groupify/adapter/active_record.rb | 2 ++ lib/groupify/adapter/active_record/group.rb | 4 +--- lib/groupify/adapter/active_record/group_member.rb | 4 +--- lib/groupify/adapter/active_record/model.rb | 2 +- .../{model_membership_extensions.rb => model_extensions.rb} | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) rename lib/groupify/adapter/active_record/{model_membership_extensions.rb => model_extensions.rb} (99%) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 9e25d7b..db4c3d6 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -49,6 +49,8 @@ def self.create_children_association(klass, association_name, opts = {}, &extens # only try to look up base class if needed - can cause circular dependency issue opts[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) + require 'groupify/adapter/active_record/association_extensions' + klass.has_many association_name, ->{ distinct }, { extend: Groupify::ActiveRecord::AssociationExtensions }.merge(opts), &extension diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 5bed74a..7046c88 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -1,5 +1,3 @@ -require 'groupify/adapter/active_record/association_extensions' - module Groupify module ActiveRecord @@ -15,7 +13,7 @@ module Group extend ActiveSupport::Concern included do - include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group) + include Groupify::ActiveRecord::ModelExtensions.build_for(:group) end def add(*members) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 2367f73..785ff6b 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -1,5 +1,3 @@ -require 'groupify/adapter/active_record/association_extensions' - module Groupify module ActiveRecord @@ -15,7 +13,7 @@ module GroupMember extend ActiveSupport::Concern included do - include Groupify::ActiveRecord::ModelMembershipExtensions.build_for(:group_member) + include Groupify::ActiveRecord::ModelExtensions.build_for(:group_member) end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index 66d8168..f91cbc2 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -5,7 +5,7 @@ module Model included do require 'groupify/adapter/active_record/model_scope_extensions' - require 'groupify/adapter/active_record/model_membership_extensions' + require 'groupify/adapter/active_record/model_extensions' # Define a scope that returns nothing. # This is built into ActiveRecord 4, but not 3 diff --git a/lib/groupify/adapter/active_record/model_membership_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb similarity index 99% rename from lib/groupify/adapter/active_record/model_membership_extensions.rb rename to lib/groupify/adapter/active_record/model_extensions.rb index b188ce9..fed4c21 100644 --- a/lib/groupify/adapter/active_record/model_membership_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -1,6 +1,6 @@ module Groupify module ActiveRecord - module ModelMembershipExtensions + module ModelExtensions def self.build_for(official_parent_type, options = {}) module_name = "#{official_parent_type.to_s.classify}MembershipExtensions" From 7b6bc9c3f8652888de6f1b2b861825df3e4c41c9 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 00:25:58 -0400 Subject: [PATCH 174/205] Remove references to old classes --- lib/groupify/adapter/active_record.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index db4c3d6..1bde1a9 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -10,8 +10,6 @@ module ActiveRecord autoload :GroupMembership, 'groupify/adapter/active_record/group_membership' autoload :PolymorphicCollection, 'groupify/adapter/active_record/polymorphic_collection' autoload :PolymorphicRelation, 'groupify/adapter/active_record/polymorphic_relation' - autoload :ParentProxy, 'groupify/adapter/active_record/parent_proxy' - autoload :ParentQueryBuilder, 'groupify/adapter/active_record/parent_query_builder' autoload :NamedGroupCollection, 'groupify/adapter/active_record/named_group_collection' autoload :NamedGroupMember, 'groupify/adapter/active_record/named_group_member' From 7f54082816aacc0473f83ad3c502a7aede0cef4e Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 14:17:35 -0400 Subject: [PATCH 175/205] Fix module naming --- .../adapter/active_record/model_extensions.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index fed4c21..3821e53 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -2,7 +2,7 @@ module Groupify module ActiveRecord module ModelExtensions def self.build_for(official_parent_type, options = {}) - module_name = "#{official_parent_type.to_s.classify}MembershipExtensions" + module_name = "#{official_parent_type.to_s.classify}ModelExtensions" const_get(module_name.to_sym) rescue NameError @@ -11,9 +11,9 @@ def self.build_for(official_parent_type, options = {}) child_type = parent_type == :group ? :member : :group new_module = Module.new do - class_eval %Q( - extend ActiveSupport::Concern + extend ActiveSupport::Concern + class_eval %Q( included do @default_#{child_type}_class_name = nil @default_#{child_type}s_association_name = nil @@ -181,13 +181,13 @@ def add_#{child_type}s(children, opts = {}) self end + ) - protected + protected - def clear_association_cache_for(record) - record.__send__(:clear_association_cache) - end - ) + def clear_association_cache_for(record) + record.__send__(:clear_association_cache) + end end self.const_set(module_name, new_module) From ec14b70ff3c6b38561307121f56ce2f83b2c513f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 14:21:14 -0400 Subject: [PATCH 176/205] Added ability to search on multiple membership types (SQL `OR`/`IN(...)`) query --- lib/groupify.rb | 4 +++ .../adapter/active_record/group_membership.rb | 6 ++-- .../active_record/model_scope_extensions.rb | 4 +-- .../active_record/named_group_collection.rb | 32 +++++++++++-------- .../active_record/polymorphic_relation.rb | 4 +-- spec/active_record_spec.rb | 21 ++++++++++++ 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index 4a2da40..2909abd 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -68,6 +68,10 @@ def self.infer_class_and_association_name(association_name) [klass, association_name.to_sym] end + + def self.clean_membership_types(*membership_types) + membership_types.flatten.compact.map(&:to_s).reject(&:empty?) + end end require 'groupify/railtie' if defined?(Rails) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 48a481d..e1891a4 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -38,8 +38,10 @@ def named(group_name = nil) end end - def as(membership_type) - membership_type.present? ? where(membership_type: membership_type.to_s) : all + def as(*membership_types) + membership_types = Groupify.clean_membership_types(membership_types) + + membership_types.any? ? where(membership_type: membership_types) : all end def polymorphic_groups diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index d6fd007..781dbaf 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -12,8 +12,8 @@ def self.build_for(parent_type, options = {}) new_module = Module.new do base_methods = %Q( - def as(membership_type) - with_memberships{as(membership_type)} + def as(*membership_types) + with_memberships{as(membership_types)} end def with_memberships(opts = {}, &group_membership_filter) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index b9d2ccf..ac6d293 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -74,9 +74,11 @@ def clear alias_method :destroy_all, :clear # Criteria to filter by membership type - def as(membership_type) - if membership_type.present? - @named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym) + def as(*membership_types) + membership_types = Groupify.clean_membership_types(membership_types) + + if membership_types.any? + @named_group_memberships.as(membership_types).pluck(:group_name).map(&:to_sym) else to_a.map(&:to_sym) end @@ -85,17 +87,19 @@ def as(membership_type) protected def remove(named_groups, destruction_type, membership_type = nil) - if named_groups.present? - @named_group_memberships. - where(group_name: named_groups). - as(membership_type). - __send__(destruction_type) - - unless membership_type.present? - named_groups.each do |named_group| - @hash.delete(named_group) - end - end + return unless named_groups.present? + + membership_types = Groupify.clean_membership_types(membership_type) + + (@named_group_memberships. + where(group_name: named_groups). + as(membership_types). + __send__(destruction_type)) + + return if membership_types.any? + + named_groups.each do |named_group| + @hash.delete(named_group) end end end diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 5e2583b..99b561a 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -16,8 +16,8 @@ def initialize(owner, source_name, &group_membership_filter) end end - def as(membership_type) - @collection = @collection.as(membership_type) + def as(*membership_types) + @collection = @collection.as(membership_types) self end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 85424a2..777f1dd 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -556,6 +556,22 @@ class ProjectMember < ActiveRecord::Base expect(User.as(:manager)).to include(user) end + it "finds members by multiple membership types" do + organization = Organization.create! + classroom = Classroom.create! + + organization.add user, as: 'manager' + organization.add user, as: 'employee' + classroom.add user, as: 'teacher' + group.add user, as: 'manager' + + expect(User.as(:teacher, :manager)).to include(user) + expect(User.as(:teacher, :employee)).to include(user) + expect(user.polymorphic_groups.as(:manager, :employee)).to include(organization, group) + expect(user.polymorphic_groups.as(:manager, :employee)).to_not include(classroom) + expect(user.polymorphic_groups.as(:teacher, :manager)).to include(classroom, organization, group) + end + it "finds members by group with membership type" do group.add user, as: 'employee' @@ -818,6 +834,11 @@ class ProjectMember < ActiveRecord::Base expect(user.named_groups.as(:developer)).to_not include(:team1) expect(user.named_groups.as(:employee)).to include(:team2) end + + it "finds all named group memberships for multiple membership types" do + expect(user.named_groups.as(:manager, :developer)).to include(:team3, :team1) + expect(user.named_groups.as(:manager, :developer)).to_not include(:team2) + end end end end From e30b7320b8f0460121e11ae3f8262329e4fadd5f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 19:22:11 -0400 Subject: [PATCH 177/205] Fix tests and DSL to allow both group and member implemented on one model --- README.md | 23 ++++++++ lib/groupify/adapter/active_record/group.rb | 1 + .../adapter/active_record/group_member.rb | 1 + lib/groupify/adapter/active_record/model.rb | 3 -- .../adapter/active_record/model_extensions.rb | 5 +- .../active_record/model_scope_extensions.rb | 53 ++++++++++++++++--- .../active_record/named_group_member.rb | 10 ++-- spec/active_record/ambiguous.rb | 6 +++ spec/active_record/classroom.rb | 1 + spec/active_record/custom_group.rb | 1 + spec/active_record/group.rb | 1 + spec/active_record_spec.rb | 26 +++++++++ 12 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 spec/active_record/ambiguous.rb diff --git a/README.md b/README.md index 636f6f7..a067c2d 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,29 @@ class Member < ActiveRecord::Base end ``` +### Implementing Group and Group Member on a Single Model (Active Record only) + +When a model is designated both as a group and a group member, some things can become ambiguous internally +to Groupify. Usually the context can be inferred. However, when it can't, Groupify assumes that your model +is a member. + +For example, if a `Group` can be a member and a group, the following will return groups: + +```ruby +class Group < ActiveRecord::Base + groupify :group + groupify :group_member +end + +member = Group.create! +group = Group.create! + +group.add member, as: :owner + +# This will return members who are in groups with the given membership type +Group.as(:owner) # [member] +``` + ### Polymorphic Groups and Members (Active Record Only) When you configure multiple models as group or member, you may need to retrieve all groups or members, diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index 7046c88..f6c2a09 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -14,6 +14,7 @@ module Group included do include Groupify::ActiveRecord::ModelExtensions.build_for(:group) + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group, child_methods: true) end def add(*members) diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index 785ff6b..b3f49eb 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -14,6 +14,7 @@ module GroupMember included do include Groupify::ActiveRecord::ModelExtensions.build_for(:group_member) + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group_member, child_methods: true) end def in_group?(group, opts = {}) diff --git a/lib/groupify/adapter/active_record/model.rb b/lib/groupify/adapter/active_record/model.rb index f91cbc2..664d4c2 100644 --- a/lib/groupify/adapter/active_record/model.rb +++ b/lib/groupify/adapter/active_record/model.rb @@ -23,21 +23,18 @@ def groupify(type, opts = {}) def acts_as_group(opts = {}) include Groupify::ActiveRecord::Group - extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group, child_methods: true) configure_group!(opts) end def acts_as_group_member(opts = {}) include Groupify::ActiveRecord::GroupMember - extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:group_member, child_methods: true) configure_group_member!(opts) end def acts_as_named_group_member(opts = {}) include Groupify::ActiveRecord::NamedGroupMember - extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:named_group_member) end def acts_as_group_membership(opts = {}) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index 3821e53..4ef0623 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -42,7 +42,6 @@ def configure_#{official_parent_type}!(opts = {}) self.default_#{child_type}s_association_name ||= default_#{child_type}s end - # Deprecated: for backwards-compatibility if (#{child_type}_class_name = opts.delete :#{child_type}_class_name) self.default_#{child_type}_class_name = #{child_type}_class_name end @@ -94,7 +93,7 @@ def has_#{child_type}(association_name, opts = {}, &extension) (@#{child_type}_klasses ||= Set.new) << #{child_type}_klass.to_s.constantize rescue NameError - puts "Unable to add \#{#{child_type}_klass} to @#{child_type}_klasses" + puts "Error: Unable to add \#{#{child_type}_klass} to @#{child_type}_klasses" ensure self end @@ -132,7 +131,7 @@ def add_#{child_type}s(children, opts = {}) to_add_directly = [] to_add_with_membership_type = [] - already_children = find_memberships_for_#{child_type}s(children).includes(@child_type).group_by{ |membership| membership.#{child_type} } + already_children = find_memberships_for_#{child_type}s(children).includes(:#{child_type}).group_by{ |membership| membership.#{child_type} } # first prepare changes children.each do |child| diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index 781dbaf..8cf0009 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -11,12 +11,19 @@ def self.build_for(parent_type, options = {}) child_type = parent_type == :group ? :member : :group new_module = Module.new do - base_methods = %Q( - def as(*membership_types) - with_memberships{as(membership_types)} + # This is an ambiguous call when a class implements both group and + # member. We make a guess, but default to assuming it's a member. + # See `detect_result_type_for` for more details. + def as(*membership_types) + if detect_result_type_for(current_scope || self) == :member + with_memberships_for_member{as(membership_types)} + else + with_memberships_for_group{as(membership_types)} end + end - def with_memberships(opts = {}, &group_membership_filter) + base_methods = %Q( + def with_memberships_for_#{parent_type}(opts = {}, &group_membership_filter) criteria = [] criteria << joins(:group_memberships_as_#{parent_type}) criteria << opts[:criteria] if opts[:criteria] @@ -31,25 +38,55 @@ def with_memberships(opts = {}, &group_membership_filter) def with_#{child_type}s(child_or_children) scope = if child_or_children.is_a?(::ActiveRecord::Base) # single child - with_memberships(criteria: child_or_children.group_memberships_as_#{child_type}) + with_memberships_for_#{parent_type}(criteria: child_or_children.group_memberships_as_#{child_type}) else - with_memberships{for_#{child_type}s(child_or_children)} + with_memberships_for_#{parent_type}{for_#{child_type}s(child_or_children)} end if block_given? - scope = scope.with_memberships(&group_membership_filter) + scope = scope.with_memberships_for_#{parent_type}(&group_membership_filter) end scope end def without_#{child_type}s(children) - with_memberships{not_for_#{child_type}s(children)} + with_memberships_for_#{parent_type}{not_for_#{child_type}s(children)} end ) class_eval(base_methods) class_eval(child_methods) if options[:child_methods] + + protected + + # Determines what the result type is for the scope (group or member). + # If it implements both, then we see if we can infer things from joins. + # Defaults to assume it's a group. + def detect_result_type_for(scope) + case scope + when ::ActiveRecord::Associations::CollectionProxy, ::ActiveRecord::AssociationRelation + return scope.source_name.to_sym + when Class # assume inherits ::ActiveRecord::Base + klass = scope + when ::ActiveRecord::Base + klass = scope.class + when ::ActiveRecord::Relation + klass = scope.klass + end + + types = [] + types << :group if klass < Group + types << :member if klass < GroupMember || klass < NamedGroupMember + + return types.first if types.one? + + if scope.is_a?(::ActiveRecord::Relation) && scope.joins_values.first == :group_memberships_as_group + :group + else + :member + end + end end const_set(module_name, new_module) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index f8cbe37..1754cab 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -13,6 +13,8 @@ module NamedGroupMember extend ActiveSupport::Concern included do + extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:named_group_member) + unless respond_to?(:group_memberships_as_member) has_many :group_memberships_as_member, as: :member, @@ -69,21 +71,21 @@ module ClassMethods def in_named_group(named_group) return none unless named_group.present? - with_memberships{where(group_name: named_group)}.distinct + with_memberships_for_member{where(group_name: named_group)}.distinct end def in_any_named_group(*named_groups) named_groups.flatten! return none unless named_groups.present? - with_memberships{where(group_name: named_groups.flatten)}.distinct + with_memberships_for_member{where(group_name: named_groups.flatten)}.distinct end def in_all_named_groups(*named_groups) named_groups.flatten! return none unless named_groups.present? - with_memberships{where(group_name: named_groups)}. + with_memberships_for_member{where(group_name: named_groups)}. group(ActiveRecord.quote('id', self)). having("COUNT(DISTINCT #{ActiveRecord.quote('group_name')}) = ?", named_groups.count). distinct @@ -99,7 +101,7 @@ def in_only_named_groups(*named_groups) end def in_other_named_groups(*named_groups) - with_memberships{where.not(group_name: named_groups)} + with_memberships_for_member{where.not(group_name: named_groups)} end def shares_any_named_group(other) diff --git a/spec/active_record/ambiguous.rb b/spec/active_record/ambiguous.rb new file mode 100644 index 0000000..8d193a0 --- /dev/null +++ b/spec/active_record/ambiguous.rb @@ -0,0 +1,6 @@ +class Ambiguous < ActiveRecord::Base + self.table_name = 'groups' + + groupify :group, member_class_name: 'Ambiguous' + groupify :group_member, group_class_name: 'Ambiguous' +end diff --git a/spec/active_record/classroom.rb b/spec/active_record/classroom.rb index ceb8845..7d2a425 100644 --- a/spec/active_record/classroom.rb +++ b/spec/active_record/classroom.rb @@ -1,3 +1,4 @@ class Classroom < ActiveRecord::Base groupify :group + groupify :group_member end diff --git a/spec/active_record/custom_group.rb b/spec/active_record/custom_group.rb index 4f5d8f3..96bb45a 100644 --- a/spec/active_record/custom_group.rb +++ b/spec/active_record/custom_group.rb @@ -1,3 +1,4 @@ class CustomGroup < ActiveRecord::Base groupify :group, members: [:custom_users] + groupify :group_member end diff --git a/spec/active_record/group.rb b/spec/active_record/group.rb index a34c188..481f58f 100644 --- a/spec/active_record/group.rb +++ b/spec/active_record/group.rb @@ -1,3 +1,4 @@ class Group < ActiveRecord::Base groupify :group, members: [:users, :widgets, "namespaced/members"], default_members: :users + groupify :group_member end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 777f1dd..fa8d7b6 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -40,6 +40,7 @@ # autoload :CustomUser, 'active_record/custom_user' # autoload :CustomGroup, 'active_record/custom_group' +require_relative './active_record/ambiguous' require_relative './active_record/user' require_relative './active_record/manager' require_relative './active_record/widget' @@ -392,6 +393,31 @@ class ProjectMember < ActiveRecord::Base end end + context "when designating a model as a group and member" do + it "finds members" do + member1 = Ambiguous.create!(name: "member1") + member2 = Ambiguous.create!(name: "member2") + group1 = Ambiguous.create!(name: "group1") + group2 = Ambiguous.create!(name: "group2") + + group1.add member1 + group2.add member2, as: 'member' + + expect(group1.members).to include(member1) + expect(group2.members).to include(member2) + + expect(group2.members.as(:member)).to include(member2) + expect(member2.groups.as(:member)).to include(group2) + + expect(Ambiguous.as(:member)).to include(member2) + expect(Ambiguous.as(:member)).to_not include(group2) + + expect(Ambiguous.with_member(member1)).to include(group1) + expect(Ambiguous.with_member(member1)).to_not include(member1) + expect(Ambiguous.with_member(member1)).to_not include(member2) + end + end + context 'when checking group membership' do it "members can check if they belong to any/all groups" do user.groups << group From 61612d82f165408e89153184006e8a3a79e6e478 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 17 Aug 2017 23:34:18 -0400 Subject: [PATCH 178/205] Set `class_name` when inferring class from association name (STI) --- lib/groupify/adapter/active_record.rb | 1 + spec/active_record_spec.rb | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 1bde1a9..355499f 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -46,6 +46,7 @@ def self.create_children_association(klass, association_name, opts = {}, &extens # only try to look up base class if needed - can cause circular dependency issue opts[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class) + opts[:class_name] ||= model_klass.to_s unless opts[:source_type].to_s == model_klass.to_s require 'groupify/adapter/active_record/association_extensions' diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index fa8d7b6..404a618 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -93,6 +93,22 @@ expect(GroupMembership.for_groups([group, classroom]).map(&:member).uniq.first).to eq(user) end + it "infers class name for association based on association name" do + organization = Organization.create! + organization1 = Organization.create! + organization2 = Organization.create! + group = Group.create! + manager = Manager.create! + + organization.add organization1 + organization.add organization2 + organization.add group + organization.add manager + + expect(organization.organizations.count).to eq(2) + expect(organization.organizations).to include(organization1, organization2) + end + it "member has groups in has_many through associations after adding member to groups" do expect(user.groups.size).to eq(0) @@ -405,7 +421,7 @@ class ProjectMember < ActiveRecord::Base expect(group1.members).to include(member1) expect(group2.members).to include(member2) - + expect(group2.members.as(:member)).to include(member2) expect(member2.groups.as(:member)).to include(group2) From d7ab6fd462e827234ef426cd97f430029250c356 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 18 Aug 2017 03:24:08 -0400 Subject: [PATCH 179/205] Removed Rails 4.1 support --- .travis.yml | 2 -- Appraisals | 11 ----------- README.md | 2 +- gemfiles/rails_4.0.gemfile | 35 ----------------------------------- gemfiles/rails_4.1.gemfile | 35 ----------------------------------- groupify.gemspec | 2 +- 6 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 gemfiles/rails_4.0.gemfile delete mode 100644 gemfiles/rails_4.1.gemfile diff --git a/.travis.yml b/.travis.yml index 84ba2ad..cf14233 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,8 +23,6 @@ rvm: - jruby-9.1.9.0 #- rubinius-3 gemfile: - #- gemfiles/rails_4.0.gemfile - - gemfiles/rails_4.1.gemfile - gemfiles/rails_4.2.gemfile - gemfiles/rails_5.0.gemfile - gemfiles/rails_5.1.gemfile diff --git a/Appraisals b/Appraisals index af4bead..4677e2c 100644 --- a/Appraisals +++ b/Appraisals @@ -1,14 +1,3 @@ -appraise "rails-4.0" do - gem 'activerecord', "~> 4.0.0" - gem "mongoid", "~> 4.0" -end - -appraise "rails-4.1" do - gem 'activerecord', "~> 4.1.0" - - gem "mongoid", "~> 4.0" -end - appraise "rails-4.2" do gem 'activerecord', "~> 4.2.0" diff --git a/README.md b/README.md index a067c2d..4476a45 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ model? Use named groups instead to add members to named groups such as ## Compatibility The following ORMs are supported: - * ActiveRecord 4.1+, 5.x + * ActiveRecord 4.2+, 5.x * Mongoid 4.x, 5.x, 6.x The following Rubies are supported: diff --git a/gemfiles/rails_4.0.gemfile b/gemfiles/rails_4.0.gemfile deleted file mode 100644 index b008ee6..0000000 --- a/gemfiles/rails_4.0.gemfile +++ /dev/null @@ -1,35 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 4.0.0" -gem "mongoid", "~> 4.0" - -group :development do - gem "pry" - gem "github_changelog_generator" -end - -group :test do - gem "rspec", ">= 3" - gem "database_cleaner", "~> 1.5.3" - gem "combustion", "0.5.5" - gem "appraisal" - gem "coveralls", require: false - gem "codeclimate-test-reporter", require: nil -end - -platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "activerecord-jdbcmysql-adapter" - gem "jdbc-mysql" - gem "activerecord-jdbcpostgresql-adapter" -end - -platforms :ruby do - gem "sqlite3" - gem "mysql2", "~> 0.3.11" - gem "pg" -end - -gemspec path: "../" diff --git a/gemfiles/rails_4.1.gemfile b/gemfiles/rails_4.1.gemfile deleted file mode 100644 index b75db37..0000000 --- a/gemfiles/rails_4.1.gemfile +++ /dev/null @@ -1,35 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 4.1.0" -gem "mongoid", "~> 4.0" - -group :development do - gem "pry" - gem "github_changelog_generator" -end - -group :test do - gem "rspec", ">= 3" - gem "database_cleaner", "~> 1.5.3" - gem "combustion", "0.5.5" - gem "appraisal" - gem "coveralls", require: false - gem "codeclimate-test-reporter", require: nil -end - -platforms :jruby do - gem "activerecord-jdbcsqlite3-adapter" - gem "activerecord-jdbcmysql-adapter" - gem "jdbc-mysql" - gem "activerecord-jdbcpostgresql-adapter" -end - -platforms :ruby do - gem "sqlite3" - gem "mysql2", "~> 0.3.11" - gem "pg" -end - -gemspec path: "../" diff --git a/groupify.gemspec b/groupify.gemspec index e72cc40..217358e 100644 --- a/groupify.gemspec +++ b/groupify.gemspec @@ -19,5 +19,5 @@ Gem::Specification.new do |gem| gem.required_ruby_version = ">= 2.2" gem.add_development_dependency "mongoid", ">= 4" - gem.add_development_dependency "activerecord", ">= 4", "< 5.2" + gem.add_development_dependency "activerecord", ">= 4.2", "< 5.2" end From 0757a3accbd5fc21d58424b23a72d1388f303e2b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 18 Aug 2017 19:59:21 -0400 Subject: [PATCH 180/205] Removed unnecessary option --- lib/groupify/adapter/active_record/collection_extensions.rb | 3 ++- lib/groupify/adapter/active_record/model_extensions.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/collection_extensions.rb b/lib/groupify/adapter/active_record/collection_extensions.rb index b31c6b5..a7e4221 100644 --- a/lib/groupify/adapter/active_record/collection_extensions.rb +++ b/lib/groupify/adapter/active_record/collection_extensions.rb @@ -41,7 +41,8 @@ def add_children(children, opts = {}) def remove_children(children, destruction_type, membership_type = nil) owner. - __send__(:"find_memberships_for_#{source_name}s", children, as: membership_type). + __send__(:"find_memberships_for_#{source_name}s", children). + as(membership_type). __send__(:"#{destruction_type}_all") owner.__send__(:clear_association_cache) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index 4ef0623..554f820 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -117,8 +117,8 @@ def membership_types_for_#{child_type}(#{child_type}) sort_by(&:to_s) end - def find_memberships_for_#{child_type}s(children, opts = {}) - group_memberships_as_#{parent_type}.for_#{child_type}s(children).as(opts[:as]) + def find_memberships_for_#{child_type}s(children) + group_memberships_as_#{parent_type}.for_#{child_type}s(children) end def add_#{child_type}s(children, opts = {}) From 5f23bed4f410a5d492b3f4f1dc049142877bca70 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sat, 19 Aug 2017 19:21:25 -0400 Subject: [PATCH 181/205] Fixes Rails 4.2 bug merging associations --- .../adapter/active_record/group_membership.rb | 2 + spec/active_record_spec.rb | 77 +++++++++++++++++++ spec/internal/db/schema.rb | 14 ++++ 3 files changed, 93 insertions(+) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index e1891a4..00857fe 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -75,6 +75,8 @@ def for_polymorphic(source, records, opts = {}) when ::ActiveRecord::Relation all.merge(records) when ::ActiveRecord::Base + # Nasty bug causes wrong results in Rails 4.2 + records = records.reload if ::ActiveRecord.version < Gem::Version.new("5.0.0") all.merge(records.__send__(:"group_memberships_as_#{source}")) else all diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 404a618..f57681b 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -148,6 +148,83 @@ expect(user.polymorphic_groups.count).to eq(2) expect(user.group_memberships_as_member.count).to eq(4) end + + it "properly checks group inclusion with complex relationships (Rails 4.2 bug)" do + class Parent < ActiveRecord::Base + groupify :group_member + groupify :named_group_member + has_group :personas + + has_many :enrollments, inverse_of: :some_user + has_many :enrolled_students, ->{ distinct }, through: :enrollments, source: :student + end + class Student < ActiveRecord::Base + groupify :group + groupify :group_member + has_group :universities + + has_many :enrollments, inverse_of: :student + end + class University < Group + has_member :students + + has_many :enrollments, inverse_of: :university, autosave: true + end + class Enrollment < ActiveRecord::Base + belongs_to :parent, inverse_of: :enrollments + belongs_to :student, inverse_of: :enrollments, autosave: true + belongs_to :university, inverse_of: :enrollments + end + + parent = Parent.create! + student1 = Student.create!(id: 1) + student2 = Student.create!(id: 2) + + student1.add parent + student2.add parent + + university1 = University.new(id: 11) + university1.enrollments.build(parent: parent, student: student1) + university1.save! + + university1.add student1 + + university2 = University.new(id: 22) + university2.enrollments.build(parent: parent, student: student2) + university2.save! + + university2.add student2 + + # Initially, things are as expected + + expect(student1.in_group?(university1)).to eq(true) + expect(student1.in_group?(university2)).to eq(false) + expect(student2.in_group?(university1)).to eq(false) + expect(student2.in_group?(university2)).to eq(true) + + expect(parent.enrolled_students[0].id).to eq(1) + expect(parent.enrolled_students[0].in_group?(university1)).to eq(true) + expect(parent.enrolled_students[0].in_group?(university2)).to eq(false) + + expect(parent.enrolled_students[1].id).to eq(2) + expect(parent.enrolled_students[1].in_group?(university1)).to eq(false) + expect(parent.enrolled_students[1].in_group?(university2)).to eq(true) + + # After getting records fresh from the database, a bug in Rails 4 + # returns the same `exists?` result (inside `in_group?`) for each record. + # + # This seems to be a result of some internal cache that retrieves the + # wrong internal records or values when merging or querying. + + parent = Parent.first + university2 = University.find(22) + + student2 = Student.find(2) + + results = parent.enrolled_students.map{ |s| [s.id, s.in_group?(university2)]} + + expect(results).to eq([[1, false], [2, true]]) + end end end end diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb index b648652..23a9897 100644 --- a/spec/internal/db/schema.rb +++ b/spec/internal/db/schema.rb @@ -56,4 +56,18 @@ create_table :project_members do |t| t.string :name end + + create_table :parents do |t| + t.string :name + end + + create_table :students do |t| + t.string :name + end + + create_table :enrollments do |t| + t.references :parent, index: true + t.references :student, index: true + t.references :university, index: true + end end From 6e668c74751170d43ea4797ba9f344eb18271dbd Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 20 Aug 2017 00:52:24 -0400 Subject: [PATCH 182/205] Adding should throw validation exception because old version called `create!` --- lib/groupify/adapter/active_record/group.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/group.rb b/lib/groupify/adapter/active_record/group.rb index f6c2a09..3eacdc3 100644 --- a/lib/groupify/adapter/active_record/group.rb +++ b/lib/groupify/adapter/active_record/group.rb @@ -18,7 +18,7 @@ module Group end def add(*members) - opts = members.extract_options! + opts = members.extract_options!.merge(exception_on_invalidation: true) add_members(members.flatten, opts) end From 6a19ef01ff3501a371723db35e645434e69373f5 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 20 Aug 2017 00:59:51 -0400 Subject: [PATCH 183/205] Fix tests to use specific order of records for PostgreSQL --- spec/active_record_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index f57681b..29b2277 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -202,13 +202,15 @@ class Enrollment < ActiveRecord::Base expect(student2.in_group?(university1)).to eq(false) expect(student2.in_group?(university2)).to eq(true) - expect(parent.enrolled_students[0].id).to eq(1) - expect(parent.enrolled_students[0].in_group?(university1)).to eq(true) - expect(parent.enrolled_students[0].in_group?(university2)).to eq(false) + enrolled_students = parent.enrolled_students.to_a.sort_by(&:id) - expect(parent.enrolled_students[1].id).to eq(2) - expect(parent.enrolled_students[1].in_group?(university1)).to eq(false) - expect(parent.enrolled_students[1].in_group?(university2)).to eq(true) + expect(enrolled_students[0].id).to eq(1) + expect(enrolled_students[0].in_group?(university1)).to eq(true) + expect(enrolled_students[0].in_group?(university2)).to eq(false) + + expect(enrolled_students[1].id).to eq(2) + expect(enrolled_students[1].in_group?(university1)).to eq(false) + expect(enrolled_students[1].in_group?(university2)).to eq(true) # After getting records fresh from the database, a bug in Rails 4 # returns the same `exists?` result (inside `in_group?`) for each record. From 95c4ebbeb2e9987f28210bda0a6769403bbd417b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 21 Aug 2017 04:33:33 -0400 Subject: [PATCH 184/205] Fix merging relations that don't go through group memberships --- lib/groupify.rb | 3 +- lib/groupify/adapter/active_record.rb | 35 ++++++++ .../adapter/active_record/group_membership.rb | 14 +++- .../adapter/active_record/model_extensions.rb | 6 +- .../active_record/model_scope_extensions.rb | 14 ++-- spec/active_record/enrollment.rb | 5 ++ spec/active_record/parent.rb | 8 ++ spec/active_record/student.rb | 7 ++ spec/active_record/university.rb | 5 ++ spec/active_record_spec.rb | 79 ++++++++++--------- 10 files changed, 126 insertions(+), 50 deletions(-) create mode 100644 spec/active_record/enrollment.rb create mode 100644 spec/active_record/parent.rb create mode 100644 spec/active_record/student.rb create mode 100644 spec/active_record/university.rb diff --git a/lib/groupify.rb b/lib/groupify.rb index 2909abd..d1b204b 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -54,8 +54,7 @@ def self.infer_class_and_association_name(association_name) begin klass = association_name.to_s.classify.constantize rescue NameError => ex - puts "Error: #{ex.inspect}" - #puts ex.backtrace + Rails.logger.warn "Error: #{ex.inspect}" if Groupify.ignore_association_class_inference_errors klass = association_name.to_s.classify diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 355499f..1208983 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -66,5 +66,40 @@ def self.create_children_association(klass, association_name, opts = {}, &extens raise message.join(' ') end + + # Returns `false` if this is not an association + def self.group_memberships_association_name_for_association(scope) + case scope + when ::ActiveRecord::Associations::CollectionProxy, ::ActiveRecord::AssociationRelation + scope_reflection = scope.proxy_association.reflection + + loop do + break if scope_reflection.nil? + + case scope_reflection.name + when :group_memberships_as_group, :group_memberships_as_member + break + end + + scope_reflection = scope_reflection.through_reflection + end + + scope_reflection && scope_reflection.name + else + false + end + end + + class InvalidAssociationError < StandardError + end + + def self.check_group_memberships_for_association!(scope) + association_name = group_memberships_association_name_for_association(scope) + + return association_name unless association_name.nil? + + association_example = "#{scope.proxy_association.owner.class}##{scope.proxy_association.reflection.name}" + raise InvalidAssociationError, "You can't use the #{association_example} association because it does not go through the group memberships association." + end end end diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 00857fe..e551efa 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -73,7 +73,19 @@ def for_polymorphic(source, records, opts = {}) when Array where(build_polymorphic_criteria_for(source, records)) when ::ActiveRecord::Relation - all.merge(records) + join_name = ActiveRecord.group_memberships_association_name_for_association(records) + + if join_name + all.joins(join_name).merge(records) + else + if ActiveRecord.is_db?('mysql') + all.where(source => records) + else + # PSQL/SQLite cause SQL syntax error: bind message supplies 3 parameters, but prepared statement "a211" requires 2 + # all.where(source => records) + return for_polymorphic(source, records.to_a, opts) + end + end when ::ActiveRecord::Base # Nasty bug causes wrong results in Rails 4.2 records = records.reload if ::ActiveRecord.version < Gem::Version.new("5.0.0") diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index 554f820..eee694e 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -93,7 +93,7 @@ def has_#{child_type}(association_name, opts = {}, &extension) (@#{child_type}_klasses ||= Set.new) << #{child_type}_klass.to_s.constantize rescue NameError - puts "Error: Unable to add \#{#{child_type}_klass} to @#{child_type}_klasses" + Rails.logger.warn "Error: Unable to add \#{#{child_type}_klass} to @#{child_type}_klasses" ensure self end @@ -108,9 +108,9 @@ def #{child_type}_classes end # returns `nil` membership type with results - def membership_types_for_#{child_type}(#{child_type}) + def membership_types_for_#{child_type}(record) group_memberships_as_#{parent_type}. - for_#{child_type}s([#{child_type}]). + for_#{child_type}s([record]). select(:membership_type). distinct. pluck(:membership_type). diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index 8cf0009..441aa24 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -16,9 +16,9 @@ def self.build_for(parent_type, options = {}) # See `detect_result_type_for` for more details. def as(*membership_types) if detect_result_type_for(current_scope || self) == :member - with_memberships_for_member{as(membership_types)} + with_memberships_for_member{as(*membership_types)} else - with_memberships_for_group{as(membership_types)} + with_memberships_for_group{as(*membership_types)} end end @@ -47,7 +47,7 @@ def with_#{child_type}s(child_or_children) scope = scope.with_memberships_for_#{parent_type}(&group_membership_filter) end - scope + scope.distinct end def without_#{child_type}s(children) @@ -64,9 +64,13 @@ def without_#{child_type}s(children) # If it implements both, then we see if we can infer things from joins. # Defaults to assume it's a group. def detect_result_type_for(scope) + group_memberships_association_name = ActiveRecord.check_group_memberships_for_association!(scope) + + if group_memberships_association_name + return group_memberships_association_name == :group_memberships_as_group ? :member : :group + end + case scope - when ::ActiveRecord::Associations::CollectionProxy, ::ActiveRecord::AssociationRelation - return scope.source_name.to_sym when Class # assume inherits ::ActiveRecord::Base klass = scope when ::ActiveRecord::Base diff --git a/spec/active_record/enrollment.rb b/spec/active_record/enrollment.rb new file mode 100644 index 0000000..a4afcc2 --- /dev/null +++ b/spec/active_record/enrollment.rb @@ -0,0 +1,5 @@ +class Enrollment < ActiveRecord::Base + belongs_to :parent, inverse_of: :enrollments + belongs_to :student, inverse_of: :enrollments, autosave: true + belongs_to :university, inverse_of: :enrollments +end diff --git a/spec/active_record/parent.rb b/spec/active_record/parent.rb new file mode 100644 index 0000000..171c2ca --- /dev/null +++ b/spec/active_record/parent.rb @@ -0,0 +1,8 @@ +class Parent < ActiveRecord::Base + groupify :group_member + groupify :named_group_member + has_group :personas + + has_many :enrollments, inverse_of: :some_user + has_many :enrolled_students, ->{ distinct }, through: :enrollments, source: :student +end diff --git a/spec/active_record/student.rb b/spec/active_record/student.rb new file mode 100644 index 0000000..da04d94 --- /dev/null +++ b/spec/active_record/student.rb @@ -0,0 +1,7 @@ +class Student < ActiveRecord::Base + groupify :group + groupify :group_member + has_group :universities + + has_many :enrollments, inverse_of: :student +end diff --git a/spec/active_record/university.rb b/spec/active_record/university.rb new file mode 100644 index 0000000..74e8ca8 --- /dev/null +++ b/spec/active_record/university.rb @@ -0,0 +1,5 @@ +class University < Group + has_member :students + + has_many :enrollments, inverse_of: :university, autosave: true +end diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 29b2277..c8b349b 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -27,19 +27,6 @@ config.configure_legacy_defaults! end -# autoload :User, 'active_record/user' -# autoload :Manager, 'active_record/manager' -# autoload :Widget, 'active_record/widget' -# autoload :Namespaced, 'active_record/namespaced' -# autoload :Project, 'active_record/project' -# autoload :Group, 'active_record/group' -# autoload :Organization, 'active_record/organization' -# autoload :GroupMembership, 'active_record/group_membership' -# autoload :Classroom, 'active_record/classroom' -# autoload :CustomGroupMembership, 'active_record/custom_group_membership' -# autoload :CustomUser, 'active_record/custom_user' -# autoload :CustomGroup, 'active_record/custom_group' - require_relative './active_record/ambiguous' require_relative './active_record/user' require_relative './active_record/manager' @@ -50,6 +37,10 @@ require_relative './active_record/organization' require_relative './active_record/group_membership' require_relative './active_record/classroom' +require_relative './active_record/parent' +require_relative './active_record/student' +require_relative './active_record/university' +require_relative './active_record/enrollment' describe Group do it { should respond_to :members} @@ -150,32 +141,6 @@ end it "properly checks group inclusion with complex relationships (Rails 4.2 bug)" do - class Parent < ActiveRecord::Base - groupify :group_member - groupify :named_group_member - has_group :personas - - has_many :enrollments, inverse_of: :some_user - has_many :enrolled_students, ->{ distinct }, through: :enrollments, source: :student - end - class Student < ActiveRecord::Base - groupify :group - groupify :group_member - has_group :universities - - has_many :enrollments, inverse_of: :student - end - class University < Group - has_member :students - - has_many :enrollments, inverse_of: :university, autosave: true - end - class Enrollment < ActiveRecord::Base - belongs_to :parent, inverse_of: :enrollments - belongs_to :student, inverse_of: :enrollments, autosave: true - belongs_to :university, inverse_of: :enrollments - end - parent = Parent.create! student1 = Student.create!(id: 1) student2 = Student.create!(id: 2) @@ -227,6 +192,42 @@ class Enrollment < ActiveRecord::Base expect(results).to eq([[1, false], [2, true]]) end + + it "properly joins on group memberships table when chaining" do + parent1 = Parent.create! + student1 = Student.create! + + student1.add parent1 + + parent2 = Parent.create! + student2 = Student.create! + + student2.add parent2 + + university1 = University.new + university1.enrollments.build(parent: parent1, student: student1) + university1.save! + + university1.add student1, as: :athlete + + university2 = University.new + university2.enrollments.build(parent: parent1, student: student1) + university2.enrollments.build(parent: parent2, student: student2) + university2.save! + + university2.add student1 + university2.add student2, as: :athlete + + expect(University.with_member(parent1.enrolled_students)).to include(university1, university2) + + expect(University.with_members(parent1.enrolled_students).as(:athlete)).to include(university1) + expect(University.with_members(parent1.enrolled_students).as(:athlete)).to_not include(university2) + end + + xit "doesn't allow merging associations that don't go through group memberships" do + expect{ University.with_member(Parent.new.enrolled_students) }.to raise_error(Groupify::ActiveRecord::InvalidAssociationError) + expect{ University.with_memberships_for_group(criteria: Parent.new.enrolled_students) }.to raise_error(Groupify::ActiveRecord::InvalidAssociationError) + end end end end From 3c020ca5d0f88175c1d8d9cc7fddfb2e5dd7ce08 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 21 Aug 2017 04:43:15 -0400 Subject: [PATCH 185/205] Fix test to order comparison for PostgreSQL --- spec/active_record_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index c8b349b..beb84af 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -190,7 +190,7 @@ results = parent.enrolled_students.map{ |s| [s.id, s.in_group?(university2)]} - expect(results).to eq([[1, false], [2, true]]) + expect(results.sort_by(&:first)).to eq([[1, false], [2, true]]) end it "properly joins on group memberships table when chaining" do From 6469902cd851d7978448bdffda0afb4436e51e8b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 21 Aug 2017 05:23:16 -0400 Subject: [PATCH 186/205] Remove db type check --- lib/groupify/adapter/active_record/group_membership.rb | 8 +------- .../adapter/active_record/polymorphic_collection.rb | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index e551efa..b442ff1 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -78,13 +78,7 @@ def for_polymorphic(source, records, opts = {}) if join_name all.joins(join_name).merge(records) else - if ActiveRecord.is_db?('mysql') - all.where(source => records) - else - # PSQL/SQLite cause SQL syntax error: bind message supplies 3 parameters, but prepared statement "a211" requires 2 - # all.where(source => records) - return for_polymorphic(source, records.to_a, opts) - end + all.where(source => records) end when ::ActiveRecord::Base # Nasty bug causes wrong results in Rails 4.2 diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 7abe329..c29e706 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -28,7 +28,6 @@ def count def_delegators :to_a, :[], :pretty_print alias_method :to_ary, :to_a - alias_method :[], :to_a alias_method :empty?, :none? alias_method :blank?, :none? From 76b70c6ecb1c7750accd97182690b2975e0e2da6 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 21 Aug 2017 05:45:42 -0400 Subject: [PATCH 187/205] Use new helper method --- lib/groupify/adapter/active_record/association_extensions.rb | 2 +- lib/groupify/adapter/active_record/model_extensions.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/groupify/adapter/active_record/association_extensions.rb b/lib/groupify/adapter/active_record/association_extensions.rb index 932ca2b..c902a0a 100644 --- a/lib/groupify/adapter/active_record/association_extensions.rb +++ b/lib/groupify/adapter/active_record/association_extensions.rb @@ -10,7 +10,7 @@ def owner end def source_name - proxy_association.through_reflection.name == :group_memberships_as_group ? :member : :group + ActiveRecord.check_group_memberships_for_association!(self) == :group_memberships_as_group ? :member : :group end protected diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index eee694e..d98acf9 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -7,8 +7,7 @@ def self.build_for(official_parent_type, options = {}) const_get(module_name.to_sym) rescue NameError # convert :group_member and :named_group_member - parent_type = official_parent_type == :group ? :group : :member - child_type = parent_type == :group ? :member : :group + parent_type, child_type = official_parent_type == :group ? [:group, :member] : [:member, :group] new_module = Module.new do extend ActiveSupport::Concern From 88c255a13f2f030dd778af870fa120bf8f807676 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 30 Aug 2017 12:35:28 -0400 Subject: [PATCH 188/205] Check for relation to generate subquery --- .gitignore | 1 + Appraisals | 1 - Gemfile | 2 +- .../adapter/active_record/group_membership.rb | 34 ++++++++++++------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 1cea326..231a819 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .bundle .config .yardoc +gemfiles Gemfile.lock InstalledFiles _yardoc diff --git a/Appraisals b/Appraisals index 4677e2c..14d88fe 100644 --- a/Appraisals +++ b/Appraisals @@ -1,6 +1,5 @@ appraise "rails-4.2" do gem 'activerecord', "~> 4.2.0" - gem "mongoid", "~> 4.0" end diff --git a/Gemfile b/Gemfile index 81f854d..2a63f7f 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ group :test do gem "database_cleaner", ">= 1.5.3" gem "combustion", ">= 0.5.5" - #gem "appraisal" + gem "appraisal" gem 'coveralls', require: false gem "codeclimate-test-reporter", require: nil end diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index b442ff1..8c5d8b4 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -93,21 +93,29 @@ def for_polymorphic(source, records, opts = {}) # This is for polymorphic associations where the ID may be from # different tables. def build_polymorphic_criteria_for(source, records) - records_by_base_class = records.group_by{ |record| ActiveRecord.base_class_name(record) } - id_column, type_column = arel_table[:"#{source}_id"], arel_table[:"#{source}_type"] - - criteria = records_by_base_class.map do |type, grouped_records| - arel_table.grouping( - type_column.eq(type). - and( - id_column.in(grouped_records.map(&:id)) + case records + when ::ActiveRecord::Relation + { + :"#{source}_type" => ActiveRecord.base_class_name(records.klass), + :"#{source}_id" => records.select(:id) + } + else + id_column, type_column = arel_table[:"#{source}_id"], arel_table[:"#{source}_type"] + records_by_base_class = records.group_by{ |record| ActiveRecord.base_class_name(record) } + + criteria = records_by_base_class.map do |type, grouped_records| + arel_table.grouping( + type_column.eq(type). + and( + id_column.in(grouped_records.map(&:id)) + ) ) - ) - end + end - # Generates something like: - # (group_type = `Group` AND group_id IN (?)) OR (group_type = `Team` AND group_id IN(?)) - criteria.reduce(:or) + # Generates something like: + # (group_type = `Group` AND group_id IN (?)) OR (group_type = `Team` AND group_id IN(?)) + criteria.reduce(:or) + end end end end From cb5c7416ec74c9fe330d0cac5d299c66991e8997 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 30 Aug 2017 12:39:08 -0400 Subject: [PATCH 189/205] Fixed exclusion path --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 231a819..898ecf4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .bundle .config .yardoc -gemfiles +gemfiles/vendor Gemfile.lock InstalledFiles _yardoc From f64a844c7802fafbf436c653698b9e5dc96a5117 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 30 Aug 2017 12:42:35 -0400 Subject: [PATCH 190/205] Require newer versions of gems --- Gemfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 2a63f7f..a5027f7 100644 --- a/Gemfile +++ b/Gemfile @@ -8,8 +8,8 @@ end group :test do gem "rspec", ">= 3" - gem "database_cleaner", ">= 1.5.3" - gem "combustion", ">= 0.5.5" + gem "database_cleaner", ">= 1.6.1" + gem "combustion", ">= 0.7.0" gem "appraisal" gem 'coveralls', require: false gem "codeclimate-test-reporter", require: nil From 3f9996766f84de36c3209ba855617ac9059ca21c Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Wed, 30 Aug 2017 12:53:53 -0400 Subject: [PATCH 191/205] Simplify generated variable names and clarify attribute references --- .../adapter/active_record/model_extensions.rb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index d98acf9..54e64f6 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -31,24 +31,24 @@ def configure_#{official_parent_type}!(opts = {}) self.default_#{child_type}_class_name = Groupify.superclass_fetch(self, :default_#{child_type}_class_name, Groupify.#{child_type}_class_name) self.default_#{child_type}s_association_name = Groupify.superclass_fetch(self, :default_#{child_type}s_association_name, Groupify.#{child_type}s_association_name) - if (#{child_type}_association_names = opts.delete :#{child_type}s) - has_#{child_type}s(#{child_type}_association_names) + if (association_names = opts.delete :#{child_type}s) + has_#{child_type}s(association_names) end - if (default_#{child_type}s = opts.delete :default_#{child_type}s) - self.default_#{child_type}_class_name = default_#{child_type}s.to_s.classify + if (default_association_name = opts.delete :default_#{child_type}s) + self.default_#{child_type}_class_name = default_association_name.to_s.classify # Only use as the association name if none specified (backwards-compatibility) - self.default_#{child_type}s_association_name ||= default_#{child_type}s + self.default_#{child_type}s_association_name ||= default_association_name end - if (#{child_type}_class_name = opts.delete :#{child_type}_class_name) - self.default_#{child_type}_class_name = #{child_type}_class_name + if (default_class_name = opts.delete :#{child_type}_class_name) + self.default_#{child_type}_class_name = default_class_name end - if default_#{child_type}s_association_name - has_#{child_type}(default_#{child_type}s_association_name, - source_type: ActiveRecord.base_class_name(default_#{child_type}_class_name), - class_name: default_#{child_type}_class_name + if self.default_#{child_type}s_association_name + has_#{child_type}(self.default_#{child_type}s_association_name, + source_type: ActiveRecord.base_class_name(self.default_#{child_type}_class_name), + class_name: self.default_#{child_type}_class_name ) end end @@ -85,7 +85,7 @@ def has_#{child_type}(association_name, opts = {}, &extension) opts.merge( through: :group_memberships_as_#{parent_type}, source: :#{child_type}, - default_base_class: default_#{child_type}_class_name + default_base_class: self.default_#{child_type}_class_name ), &extension ) From 36968026dddd4a3508aa72466c15467325b8e1d0 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 3 Sep 2017 21:17:29 -0400 Subject: [PATCH 192/205] Fix test that is using a class that wasn't designated as a group member --- spec/active_record_spec.rb | 8 ++++++-- spec/internal/db/schema.rb | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index beb84af..798a610 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -298,15 +298,19 @@ context "member with custom group model" do before do + class CustomProject < ActiveRecord::Base + groupify :group + end + class ProjectMember < ActiveRecord::Base - groupify :group_member, group_class_name: 'Project' + groupify :group_member, group_class_name: 'CustomProject' end end it "overrides the default group name on a per-model basis" do member = ProjectMember.create! member.groups.create! - expect(member.groups.first).to be_a Project + expect(member.groups.first).to be_a CustomProject end end end diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb index 23a9897..36a4708 100644 --- a/spec/internal/db/schema.rb +++ b/spec/internal/db/schema.rb @@ -24,6 +24,11 @@ t.string :name end + create_table :custom_projects do |t| + t.string :name + t.string :type + end + create_table :organizations do |t| t.string :name end From a46ff8ecf3b6ae36dcac48054b833e4dd2477890 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 3 Sep 2017 21:18:07 -0400 Subject: [PATCH 193/205] Fix inverse association references for unsaved records --- .../adapter/active_record/group_membership.rb | 4 ++-- .../adapter/active_record/model_extensions.rb | 21 +++++++++++++++++-- spec/active_record_spec.rb | 20 ++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 8c5d8b4..dded2e5 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -13,8 +13,8 @@ module GroupMembership extend ActiveSupport::Concern included do - belongs_to :member, polymorphic: true - belongs_to :group, polymorphic: true + belongs_to :member, polymorphic: true, inverse_of: :group_memberships_as_member + belongs_to :group, polymorphic: true, inverse_of: :group_memberships_as_group end def membership_type=(membership_type) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index 54e64f6..9416196 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -20,6 +20,7 @@ def self.build_for(official_parent_type, options = {}) has_many :group_memberships_as_#{parent_type}, as: :#{parent_type}, + inverse_of: :#{parent_type}, autosave: true, dependent: :destroy, class_name: Groupify.group_membership_class_name @@ -167,18 +168,34 @@ def add_#{child_type}s(children, opts = {}) end # create memberships without membership type - group_memberships_as_#{parent_type} << to_add_directly + assign_memberships_to_#{parent_type}(self, *to_add_directly) # create memberships with membership type to_add_with_membership_type. group_by{ |membership| membership.#{parent_type} }. each do |membership_parent, memberships| - membership_parent.group_memberships_as_#{parent_type} << memberships + assign_memberships_to_#{parent_type}(membership_parent, *memberships) clear_association_cache_for(membership_parent) end self end + + protected + + def assign_memberships_to_#{parent_type}(target, *memberships) + memberships.flatten! + + target.group_memberships_as_#{parent_type} << memberships + + return if target.persisted? + + memberships.each do |membership| + unless membership.#{child_type}.group_memberships_as_#{child_type}.include?(membership) + membership.#{child_type}.group_memberships_as_#{child_type} << membership + end + end + end ) protected diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 798a610..96f35b8 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -63,6 +63,26 @@ describe "polymorphic groups" do context "memberships" do + it "builds associations before auto-saving" do + group = Group.new + group.add user + + expect(group.persisted?).to eq true + expect(user.persisted?).to eq true + expect(group.users.size).to eq 1 + expect(user.groups.size).to eq 1 + expect(group.users).to include(user) + expect(user.groups).to include(group) + + user.reload + + expect(user.groups.first).to eq group + + group.reload + + expect(group.users.first).to eq user + end + it "finds multiple records for different models with same ID" do group.add user classroom.add user From a1edc014608015fed9b547ea6c893a599f61523b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 4 Sep 2017 12:52:10 -0400 Subject: [PATCH 194/205] Test for persistence of new records when adding to groups --- .../active_record/named_group_member.rb | 12 ++++------ spec/active_record_spec.rb | 23 ++++++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_member.rb b/lib/groupify/adapter/active_record/named_group_member.rb index 1754cab..266dc3d 100644 --- a/lib/groupify/adapter/active_record/named_group_member.rb +++ b/lib/groupify/adapter/active_record/named_group_member.rb @@ -15,13 +15,11 @@ module NamedGroupMember included do extend Groupify::ActiveRecord::ModelScopeExtensions.build_for(:named_group_member) - unless respond_to?(:group_memberships_as_member) - has_many :group_memberships_as_member, - as: :member, - autosave: true, - dependent: :destroy, - class_name: Groupify.group_membership_class_name - end + has_many :group_memberships_as_member, + as: :member, + autosave: true, + dependent: :destroy, + class_name: Groupify.group_membership_class_name end def named_groups diff --git a/spec/active_record_spec.rb b/spec/active_record_spec.rb index 96f35b8..df2e2f6 100644 --- a/spec/active_record_spec.rb +++ b/spec/active_record_spec.rb @@ -63,7 +63,7 @@ describe "polymorphic groups" do context "memberships" do - it "builds associations before auto-saving" do + it "auto-saves new group record when adding a member" do group = Group.new group.add user @@ -83,6 +83,27 @@ expect(group.users.first).to eq user end + it "auto-saves new member record when adding to a group" do + group = Group.create! + user = User.new + group.add user + + expect(group.persisted?).to eq true + expect(user.persisted?).to eq true + expect(group.users.size).to eq 1 + expect(user.groups.size).to eq 1 + expect(group.users).to include(user) + expect(user.groups).to include(group) + + user.reload + + expect(user.groups.first).to eq group + + group.reload + + expect(group.users.first).to eq user + end + it "finds multiple records for different models with same ID" do group.add user classroom.add user From cde2b176829c760fc43e253dba1dbbfaf0f83bf1 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 4 Sep 2017 13:02:12 -0400 Subject: [PATCH 195/205] Added brief descriptions to polymorphic classes --- .../adapter/active_record/polymorphic_collection.rb | 7 +++++++ lib/groupify/adapter/active_record/polymorphic_relation.rb | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index c29e706..8a76dac 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -1,5 +1,12 @@ module Groupify module ActiveRecord + # This `PolymorphicCollection` class acts as a facade to mimic the querying + # capabilities of an ActiveRecord::Relation while internally returning results + # which are actually retrieved from a method or association on the actual + # results. In other words, this class queries on the "join record" + # and returns records from one of the associations that would have + # to otherwise query across multiple tables. To avoid N+1, `includes` + # is added to the query chain to make things more efficient. class PolymorphicCollection include Enumerable extend Forwardable diff --git a/lib/groupify/adapter/active_record/polymorphic_relation.rb b/lib/groupify/adapter/active_record/polymorphic_relation.rb index 99b561a..a5c14e9 100644 --- a/lib/groupify/adapter/active_record/polymorphic_relation.rb +++ b/lib/groupify/adapter/active_record/polymorphic_relation.rb @@ -1,5 +1,9 @@ module Groupify module ActiveRecord + # This class acts as an association facade building on `PolymorphicCollection` + # by implementing the Groupify helper methods on this collection. This class + # also mimics an association by tracking the parent record that owns the + # association. class PolymorphicRelation < PolymorphicCollection include CollectionExtensions From 284df9411a43c1e24e27ef396882b560194ee32d Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Mon, 4 Sep 2017 22:06:56 -0400 Subject: [PATCH 196/205] Fix MySQL GROUP BY error when sql_mode=only_full_group_by MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed error by removing GROUP BY all together because selecting the ID column without being listed in the GROUP BY clause is not allowed in MySQL when in “strict” mode. --- lib/groupify/adapter/active_record/polymorphic_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 8a76dac..5232790 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -59,7 +59,7 @@ def distinct_compat if ActiveRecord.is_db?('postgres') @collection.select("DISTINCT ON (#{id}, #{type}) *") else - @collection.group([id, type]) + @collection.select([id, type]).distinct end end From 719430d3e60106b25020612d2d61fab87810ae55 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 7 Sep 2017 12:55:01 -0400 Subject: [PATCH 197/205] Add instructions on how to run tests --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 4476a45..bd0f05f 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,17 @@ class User end ``` +## Test Suite + +Run the RSpec test suite by installing the `appraisal` gem and dependencies: + + $ gem install appraisal + $ appraisal install + +And then running tests using `appraisal`: + + $ appraisal rake + ## Advanced Configuration ### Groupify Model Names From 97d3db6d5e2b110da54170c92efc4b1cc7a2e483 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 7 Sep 2017 15:35:41 -0400 Subject: [PATCH 198/205] Made further cross-database SQL compatibility fixes for DISTINCT --- lib/groupify/adapter/active_record.rb | 26 ++++++++++++++++++- .../adapter/active_record/group_member.rb | 3 +-- .../active_record/polymorphic_collection.rb | 25 ++++-------------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 1208983..7c03536 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -17,10 +17,34 @@ def self.is_db?(*strings) strings.any?{ |string| ::ActiveRecord::Base.connection.adapter_name.downcase.include?(string) } end - def self.quote(column_name, model_class = Groupify.group_membership_klass) + def self.quote(column_name, model_class = nil) + model_class = Groupify.group_membership_klass unless model_class.is_a?(Class) "#{model_class.quoted_table_name}.#{model_class.connection.quote_column_name(column_name)}" end + def self.prepare_concat(*columns) + options = columns.extract_options! + columns.flatten! + + if options[:quote] + columns = columns.map{ |column| quote(column, options[:quote]) } + end + + is_db?('sqlite') ? columns.join(' || ') : "CONCAT(#{columns.join(', ')})" + end + + def self.prepare_distinct(*columns) + options = columns.extract_options! + columns.flatten! + + if options[:quote] + columns = columns.map{ |column| quote(column, options[:quote]) } + end + + # Workaround to "group by" multiple columns in PostgreSQL + is_db?('postgres') ? "ON (#{columns.join(', ')}) *" : columns + end + # Pass in record, class, or string def self.base_class_name(model_class, default_base_class = nil) return if model_class.nil? diff --git a/lib/groupify/adapter/active_record/group_member.rb b/lib/groupify/adapter/active_record/group_member.rb index b3f49eb..547bacb 100644 --- a/lib/groupify/adapter/active_record/group_member.rb +++ b/lib/groupify/adapter/active_record/group_member.rb @@ -60,9 +60,8 @@ def in_all_groups(*groups) return none unless groups.present? - id, type = ActiveRecord.quote('group_id'), ActiveRecord.quote('group_type') # Count distinct on ID and type combo - concatenated_columns = ActiveRecord.is_db?('sqlite') ? "#{id} || #{type}" : "CONCAT(#{id}, #{type})" + concatenated_columns = ActiveRecord.prepare_concat('group_id', 'group_type', quote: true) with_groups(groups). group(ActiveRecord.quote('id', self)). diff --git a/lib/groupify/adapter/active_record/polymorphic_collection.rb b/lib/groupify/adapter/active_record/polymorphic_collection.rb index 5232790..fc4f697 100644 --- a/lib/groupify/adapter/active_record/polymorphic_collection.rb +++ b/lib/groupify/adapter/active_record/polymorphic_collection.rb @@ -53,30 +53,15 @@ def build_collection(&group_membership_filter) end def distinct_compat - id, type = ActiveRecord.quote("#{@source_name}_id"), ActiveRecord.quote("#{@source_name}_type") - - # Workaround to "group by" multiple columns in PostgreSQL - if ActiveRecord.is_db?('postgres') - @collection.select("DISTINCT ON (#{id}, #{type}) *") - else - @collection.select([id, type]).distinct - end + @collection.select(ActiveRecord.prepare_distinct(*distinct_columns)).distinct end def count_compat - # Workaround to "count distinct" on multiple columns in PostgreSQL - # (uses different syntax when aggregating distinct) - if ActiveRecord.is_db?('postgres') - id, type = ActiveRecord.quote("#{@source_name}_id"), ActiveRecord.quote("#{@source_name}_type") - - queried_count = @collection.select("DISTINCT (#{id}, #{type})").count - else - queried_count = distinct_compat.count - # The `count` is a Hash when GROUP BY is used - queried_count = queried_count.keys.size if queried_count.is_a?(Hash) - end + @collection.select(ActiveRecord.prepare_concat(*distinct_columns)).distinct.count + end - queried_count + def distinct_columns + [ActiveRecord.quote("#{@source_name}_id"), ActiveRecord.quote("#{@source_name}_type")] end end end From 3831368ddf3d5c7940f99c6bdb7c0585ec5bb7cc Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Thu, 7 Sep 2017 15:35:56 -0400 Subject: [PATCH 199/205] Add line info for dynamic modules --- lib/groupify/adapter/active_record/model_extensions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record/model_extensions.rb b/lib/groupify/adapter/active_record/model_extensions.rb index 9416196..97547dc 100644 --- a/lib/groupify/adapter/active_record/model_extensions.rb +++ b/lib/groupify/adapter/active_record/model_extensions.rb @@ -12,7 +12,7 @@ def self.build_for(official_parent_type, options = {}) new_module = Module.new do extend ActiveSupport::Concern - class_eval %Q( + class_eval <<-CODE, __FILE__ , __LINE__ + 1 included do @default_#{child_type}_class_name = nil @default_#{child_type}s_association_name = nil @@ -196,7 +196,7 @@ def assign_memberships_to_#{parent_type}(target, *memberships) end end end - ) + CODE protected From ad62271ef1fc79e94ea23055ef47cd65b6d4665b Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 8 Sep 2017 15:23:18 -0400 Subject: [PATCH 200/205] Clean membership handling - could add multiple memberships at once --- .../adapter/active_record/named_group_collection.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/groupify/adapter/active_record/named_group_collection.rb b/lib/groupify/adapter/active_record/named_group_collection.rb index ac6d293..6d1afcf 100644 --- a/lib/groupify/adapter/active_record/named_group_collection.rb +++ b/lib/groupify/adapter/active_record/named_group_collection.rb @@ -12,11 +12,9 @@ def initialize(member) def add(named_group, opts = {}) named_group = named_group.to_sym - membership_type = opts[:as].to_s if opts[:as].present? - - # always add a nil membership type and then a specific one (if specified) - membership_types = [nil, membership_type].uniq - + membership_types = Groupify.clean_membership_types(opts[:as]) + membership_types << nil # add default membership + @member.transaction do membership_types.each do |membership_type| if @member.new_record? From ff2c0e2aedfc708867ad7edba5a699c970a37bb2 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Sun, 10 Sep 2017 01:26:40 -0400 Subject: [PATCH 201/205] Update authors --- groupify.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/groupify.gemspec b/groupify.gemspec index 217358e..09aaad0 100644 --- a/groupify.gemspec +++ b/groupify.gemspec @@ -2,8 +2,8 @@ require File.expand_path('../lib/groupify/version', __FILE__) Gem::Specification.new do |gem| - gem.authors = ["dwbutler"] - gem.email = ["dwbutler@ucla.edu"] + gem.authors = ["dwbutler", "Joel Van Horn"] + gem.email = ["dwbutler@ucla.edu", "joel@joelvanhorn.com"] gem.description = %q{Adds group and membership functionality to Rails models} gem.summary = %q{Group functionality for Rails} gem.homepage = "https://github.com/dwbutler/groupify" From 3959b50a5722f9ff384ebb76211c35d579a1b0e6 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 3 Oct 2017 23:57:39 -0400 Subject: [PATCH 202/205] Fix attempt to join and merge to do so on group model, not group membership --- lib/groupify/adapter/active_record.rb | 24 +++++++++++++++---- .../adapter/active_record/group_membership.rb | 8 +------ .../active_record/model_scope_extensions.rb | 14 ++++++++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 7c03536..64d17b2 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -93,24 +93,38 @@ def self.create_children_association(klass, association_name, opts = {}, &extens # Returns `false` if this is not an association def self.group_memberships_association_name_for_association(scope) + find_association_name_through_group_memberships(scope).last + rescue ArgumentError + false + end + + def self.association_name_to_join_for(scope) + find_association_name_through_group_memberships(scope).first rescue nil + end + + # Finds the association name that goes through group memberships. + # e.g. [:members, :group_memberships_as_group] + def self.find_association_name_through_group_memberships(scope) case scope when ::ActiveRecord::Associations::CollectionProxy, ::ActiveRecord::AssociationRelation - scope_reflection = scope.proxy_association.reflection + scope_reflection = scope.proxy_association.reflection + previous_reflection = nil loop do break if scope_reflection.nil? - + case scope_reflection.name when :group_memberships_as_group, :group_memberships_as_member break end - scope_reflection = scope_reflection.through_reflection + previous_reflection = scope_reflection + scope_reflection = scope_reflection.through_reflection end - scope_reflection && scope_reflection.name + [previous_reflection && previous_reflection.name, scope_reflection && scope_reflection.name] else - false + raise ArgumentError, "The specified `scope` is not valid" end end diff --git a/lib/groupify/adapter/active_record/group_membership.rb b/lib/groupify/adapter/active_record/group_membership.rb index 865f839..c4d1053 100644 --- a/lib/groupify/adapter/active_record/group_membership.rb +++ b/lib/groupify/adapter/active_record/group_membership.rb @@ -73,13 +73,7 @@ def for_polymorphic(source, records, opts = {}) when Array where(build_polymorphic_criteria_for(source, records)) when ::ActiveRecord::Relation - join_name = ActiveRecord.group_memberships_association_name_for_association(records) - - if join_name - all.joins(join_name).merge(records) - else - all.where(source => records) - end + all.where(source => records) when ::ActiveRecord::Base # Nasty bug causes wrong results in Rails 4.2 records = records.reload if ::ActiveRecord.version < Gem::Version.new("5.0.0") diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index 441aa24..2e10dd9 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -36,13 +36,21 @@ def with_memberships_for_#{parent_type}(opts = {}, &group_membership_filter) child_methods = %Q( def with_#{child_type}s(child_or_children) - scope = if child_or_children.is_a?(::ActiveRecord::Base) + scope = case child_or_children + when ::ActiveRecord::Base # single child with_memberships_for_#{parent_type}(criteria: child_or_children.group_memberships_as_#{child_type}) - else - with_memberships_for_#{parent_type}{for_#{child_type}s(child_or_children)} + when ::ActiveRecord::Relation + join_name = ActiveRecord.association_name_to_join_for(child_or_children) + + if join_name + all.joins(join_name).merge(child_or_children) + end end + # Fallback + scope ||= with_memberships_for_#{parent_type}{for_#{child_type}s(child_or_children)} + if block_given? scope = scope.with_memberships_for_#{parent_type}(&group_membership_filter) end From e9b7e6214f8afed303fcb5184430bffd4e9a0fef Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 3 Oct 2017 23:58:50 -0400 Subject: [PATCH 203/205] Join and merge is flawed - removed --- lib/groupify/adapter/active_record.rb | 6 +----- .../adapter/active_record/model_scope_extensions.rb | 11 ++--------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 64d17b2..0293b4f 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -98,10 +98,6 @@ def self.group_memberships_association_name_for_association(scope) false end - def self.association_name_to_join_for(scope) - find_association_name_through_group_memberships(scope).first rescue nil - end - # Finds the association name that goes through group memberships. # e.g. [:members, :group_memberships_as_group] def self.find_association_name_through_group_memberships(scope) @@ -112,7 +108,7 @@ def self.find_association_name_through_group_memberships(scope) loop do break if scope_reflection.nil? - + case scope_reflection.name when :group_memberships_as_group, :group_memberships_as_member break diff --git a/lib/groupify/adapter/active_record/model_scope_extensions.rb b/lib/groupify/adapter/active_record/model_scope_extensions.rb index 2e10dd9..3f19c75 100644 --- a/lib/groupify/adapter/active_record/model_scope_extensions.rb +++ b/lib/groupify/adapter/active_record/model_scope_extensions.rb @@ -40,17 +40,10 @@ def with_#{child_type}s(child_or_children) when ::ActiveRecord::Base # single child with_memberships_for_#{parent_type}(criteria: child_or_children.group_memberships_as_#{child_type}) - when ::ActiveRecord::Relation - join_name = ActiveRecord.association_name_to_join_for(child_or_children) - - if join_name - all.joins(join_name).merge(child_or_children) - end + else + with_memberships_for_#{parent_type}{for_#{child_type}s(child_or_children)} end - # Fallback - scope ||= with_memberships_for_#{parent_type}{for_#{child_type}s(child_or_children)} - if block_given? scope = scope.with_memberships_for_#{parent_type}(&group_membership_filter) end From 05035e0abb41b509d33cc088cdb8a2ae8d63cf4f Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Tue, 31 Oct 2017 20:28:10 -0400 Subject: [PATCH 204/205] Clarify error source --- lib/groupify.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/groupify.rb b/lib/groupify.rb index d1b204b..ed169d5 100644 --- a/lib/groupify.rb +++ b/lib/groupify.rb @@ -54,7 +54,7 @@ def self.infer_class_and_association_name(association_name) begin klass = association_name.to_s.classify.constantize rescue NameError => ex - Rails.logger.warn "Error: #{ex.inspect}" + Rails.logger.warn "Groupify infer class error: #{ex.message}" if Groupify.ignore_association_class_inference_errors klass = association_name.to_s.classify From dea8e4738b62d78ddb975e2cfe7822bfb73233ff Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 12 Jan 2018 01:09:42 -0500 Subject: [PATCH 205/205] Don't infer association or class name if `:class_name` option is specified --- lib/groupify/adapter/active_record.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/groupify/adapter/active_record.rb b/lib/groupify/adapter/active_record.rb index 0293b4f..0a357e9 100644 --- a/lib/groupify/adapter/active_record.rb +++ b/lib/groupify/adapter/active_record.rb @@ -64,9 +64,14 @@ def self.base_class_name(model_class, default_base_class = nil) end def self.create_children_association(klass, association_name, opts = {}, &extension) - association_class, association_name = Groupify.infer_class_and_association_name(association_name) + association_class = opts[:class_name] + + unless association_class + association_class, association_name = Groupify.infer_class_and_association_name(association_name) + end + default_base_class = opts.delete(:default_base_class) - model_klass = opts[:class_name] || association_class || default_base_class + model_klass = association_class || default_base_class # only try to look up base class if needed - can cause circular dependency issue opts[:source_type] ||= ActiveRecord.base_class_name(model_klass, default_base_class)