Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Groupify v1.0.0: Better STI/polymorphism and various fixes #61

Open
wants to merge 206 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
206 commits
Select commit Hold shift + click to select a range
00c923c
Add the ability to specify a map of association names to class names
joelvh May 9, 2017
87fadf5
Introduce `has_member` with `:class_name` option for custom class on …
joelvh May 9, 2017
c7d3e64
Make it easier to add subclassed group associations with STI
joelvh May 10, 2017
46e6ad8
Updated docs for `has_group`
joelvh May 10, 2017
7696974
Override `<<` on collections to properly add membership_type
joelvh May 10, 2017
7dcaa1a
Add member to members collection rather than the group to groups coll…
joelvh May 10, 2017
f177d85
Don't set default, for backwards-compatibility
joelvh May 10, 2017
4bcff73
Query based group class (to support subclasses)
joelvh May 10, 2017
337f3c9
Remove unused option
joelvh May 10, 2017
1710727
Clear association cache after deleting associated models
joelvh May 10, 2017
3df2ea4
Fix query to be extensible
joelvh May 10, 2017
2a84b72
Refactored to mimic `<<` method returning `false` if an invalid argum…
joelvh May 10, 2017
c9b8d58
Add to collection with options (e.g. membership type)
joelvh May 10, 2017
ae7bec1
Specify group class name when merging in case it's a subclass
joelvh May 10, 2017
0166f5d
Remove `group_type` based on group class because we handle STI separa…
joelvh May 10, 2017
b87d1f1
Use ActiveRecord `base_class` in case member is STI
joelvh May 10, 2017
74b3044
Revert back to using association and assume STI
joelvh May 10, 2017
abd27e4
Added ability for `group.add` to throw a validation exception
joelvh May 10, 2017
76d45cc
Remove unneeded `default_members_association_name` feature
joelvh May 10, 2017
5694c15
Don't use `validate!` to ensure Rails 4.0.x compatibility
joelvh May 18, 2017
c18a2f4
Move extensions modules to separate files
joelvh May 18, 2017
ef466f2
Consolidate queries for deleting records
joelvh May 18, 2017
de70b89
Refactor to handle groups and members associations with same logic
joelvh May 18, 2017
f4ef818
Moved `delete` and `destroy` methods to shared module
joelvh May 18, 2017
16a5896
Move finder methods to appropriate modules
joelvh May 18, 2017
fcaf155
Reuse delete/destroy logic
joelvh May 18, 2017
34d659b
Require modules
joelvh May 18, 2017
3eb9a59
Mark methods protected
joelvh May 18, 2017
a5b73cf
Define `add` explicitly as raising an exception instead of using `opts`
joelvh May 18, 2017
4ec1ae1
Alias `add` and `<<` in shared module
joelvh May 18, 2017
8f64968
Simplify abstractions and fix some reference errors
joelvh May 18, 2017
3fd5074
Fixed alias methods
joelvh May 19, 2017
19f76bb
Fix `super` reference
joelvh May 19, 2017
c52d063
Added `group_type` to query to properly build membership record
joelvh May 19, 2017
539b97c
Added tests to make sure inverse associations are updated after `dele…
joelvh May 19, 2017
aa82ec4
Added tests to make sure members and groups aren't added more than once
joelvh May 19, 2017
31bcafe
Added tests to check subclassing
joelvh May 19, 2017
a3935e7
Fix whitespace
joelvh Jul 1, 2017
eea48e0
Use `merge` instead of `where`
joelvh Jul 4, 2017
adf691d
Refactor `where` with `merge`
joelvh Jul 4, 2017
3dfd19e
Let Rails generate the queries on `group_id` and `group_type`
joelvh Jul 4, 2017
341eae1
Update `group_type` for completeness
joelvh Jul 4, 2017
2e87738
Remove commented code
joelvh Jul 4, 2017
52517a3
Remove duplicated extension modules from rebase
joelvh Jul 5, 2017
972b80d
Simplify member criteria
joelvh Jul 5, 2017
f213396
helpers to query on polymorphic groups and members
joelvh Jul 5, 2017
1734420
Use merge and helpers for query
joelvh Jul 5, 2017
68e9028
clearer logic
joelvh Jul 5, 2017
04c6cf3
Add ability to call `has_group :custom_groups, "CustomGroup"`
joelvh Jul 8, 2017
7d321d3
Clarify logic and simplify some steps
joelvh Jul 8, 2017
31f2afe
Pass each record as separate parameter
joelvh Jul 8, 2017
c6bd4cd
Include type column when counting matches
joelvh Jul 8, 2017
0a05de7
Removed unused method
joelvh Jul 8, 2017
c5db985
Added helper method to make it easier to read SQL criteria
joelvh Jul 8, 2017
77c438e
DRY logic
joelvh Jul 8, 2017
efe9df9
Simplify logic when finding matching named group
joelvh Jul 8, 2017
b990b0f
Simplify variable use
joelvh Jul 8, 2017
d5af10b
Simplify membership query building
joelvh Jul 8, 2017
98fb5a9
Code spacing
joelvh Jul 8, 2017
3440327
Refer to `arel_table` consistently
joelvh Jul 9, 2017
6c5c226
Removed unused methods
joelvh Aug 1, 2017
3b0f9a3
Formatted for clarity
joelvh Aug 1, 2017
922c102
Consolidate association extensions and move helpers to `ActiveRecord`…
joelvh Aug 2, 2017
9ed5007
Fix polymorphic adding of members to groups by bypassing association …
joelvh Aug 2, 2017
1f82fda
Added `memberships_merge` helper to named groups
joelvh Aug 2, 2017
f9d3216
Add helper method to infer class and association names
joelvh Aug 3, 2017
00fe96b
Clean up `membership_type` usage and check presence better/minimally
joelvh Aug 3, 2017
19f73cb
Improve inferring parent model when model can be group and member
joelvh Aug 3, 2017
8295cbd
Consolidate `membership_merge` logic
joelvh Aug 3, 2017
f32e082
Exclude `ActiveRecord::AssociationTypeMismatch` test because it is no…
joelvh Aug 3, 2017
64fecca
Build query via chain instead of control logic
joelvh Aug 3, 2017
3dbe428
Set blank values to nil
joelvh Aug 3, 2017
4192d31
Throw "type mismatch" exception when adding to association
joelvh Aug 3, 2017
eea79b3
Update Mongoid with some similar code cleanup as ActiveRecord
joelvh Aug 3, 2017
856377f
Fixed filter fallback in tests
joelvh Aug 3, 2017
f708dba
Fix flattening arrays
joelvh Aug 3, 2017
a47b625
DRY up Mongoid similar to ActiveRecord
joelvh Aug 3, 2017
e2cf3e5
Fix Mongoid test
joelvh Aug 3, 2017
3614e2a
Rails 4.0 doesn't like merging empty hash, so we build the query step…
joelvh Aug 4, 2017
c7f09bd
Fix test to use specific IDs
joelvh Aug 4, 2017
a3ba035
Clear association cache on child after creating membership
joelvh Aug 4, 2017
33f7af2
Fix tests for Postgres which changes the order of records
joelvh Aug 4, 2017
028f88c
Fix tests to compare arrays properly
joelvh Aug 4, 2017
df9bdb8
Simplify methods by removing outdated aliasing
joelvh Aug 4, 2017
228aa6c
Add test to make sure associations are reset after deletion
joelvh Aug 4, 2017
f47a514
Add default`members` association in more intuitive spot
joelvh Aug 4, 2017
e3240ab
Fix named group support in Rails 5
dwbutler Aug 4, 2017
cfbbc8f
Clean up `has_member` and `has_group` and allow association options
joelvh Aug 4, 2017
49f592d
Fix wrong variable name reference
joelvh Aug 4, 2017
8271701
Add `has_groups` and `has_group` to Mongoid
joelvh Aug 4, 2017
ed43370
Add proper default class
joelvh Aug 4, 2017
3f051c8
Only look up base class if needed
joelvh Aug 4, 2017
5781d41
Fix Rails 4.0 - 4.1, which don't support the `required` option
dwbutler Aug 4, 2017
7dc1405
Added `Groupify.ignore_base_class_inference_errors` to swallow infere…
joelvh Aug 5, 2017
50bdc00
Use `source_type` instead of `class_name`
joelvh Aug 5, 2017
3f69372
Consolidate `has_member` and `has_group` and add error handling
joelvh Aug 5, 2017
31ed051
Use base class
joelvh Aug 5, 2017
e4b0fdd
Update tests with `autoload` to help resolve circular class dependencies
joelvh Aug 5, 2017
92f08fb
Moved test classes to separate files for autoloading
joelvh Aug 5, 2017
38bdff0
Indicate value of variable better in exception (e.g. when nil)
joelvh Aug 5, 2017
fd36319
Simplify
joelvh Aug 5, 2017
691a28d
Add option to return a string version of inferred class name rather t…
joelvh Aug 5, 2017
a18bd85
Fix variable name
joelvh Aug 6, 2017
435e294
Add `class_name` in case the default group class is a STI subclass
joelvh Aug 6, 2017
2cf9875
Use default group class when base class can't be resolved
joelvh Aug 6, 2017
e056730
Make default `members` and `groups` association names configurable
joelvh Aug 6, 2017
ae3ba76
Abstract out extension methods
joelvh Aug 6, 2017
cfd9b98
Introduce `PolymorphicChildren` collection class to
joelvh Aug 6, 2017
895b345
Disabled autoloading in tests (for now)
joelvh Aug 6, 2017
39bd88b
Remove unused argument
joelvh Aug 6, 2017
3e3acb0
Split out polymorphic classes for multiple uses
joelvh Aug 6, 2017
bae0e02
Properly merge queries
joelvh Aug 6, 2017
788ef79
Allow modifying query
joelvh Aug 6, 2017
d1ed8f6
Crate a relation
joelvh Aug 6, 2017
b6485b6
Scope properly
joelvh Aug 6, 2017
74c6b57
Fix variable name
joelvh Aug 6, 2017
900ce84
Fix query nesting
joelvh Aug 6, 2017
6196619
Fix type
joelvh Aug 6, 2017
a43048f
Add `pretty_print`
joelvh Aug 6, 2017
f76d1b3
Rename block argument for consistency
joelvh Aug 6, 2017
a895ea4
Use `instance_eval` for consistency
joelvh Aug 6, 2017
92f513b
Fix up some consistency in the code
joelvh Aug 6, 2017
9fd75bd
Remove duplicated class definitions
joelvh Aug 6, 2017
b59feaf
Fix postgres by changing from `GROUP BY` to `DISTINCT ON`
joelvh Aug 6, 2017
a8a22f7
Added test to make sure polymorphic groups are unique
joelvh Aug 6, 2017
7ca1813
Make sure that `count` is correct based on PostgreSQL DISTINCT vs. GR…
joelvh Aug 6, 2017
d1686b1
Make sure memberships are added properly based on role
joelvh Aug 6, 2017
c592f93
Moved parent/child logic into `ParentProxy` class to consolidate and …
joelvh Aug 7, 2017
09f1964
Fix for group deletion deleting group membership after merge... not s…
joelvh Aug 7, 2017
09cadcb
Simplify class
joelvh Aug 7, 2017
083509e
Move query logic to `ParentQueryBuilder` class
joelvh Aug 7, 2017
e0b7037
Clarify logic
joelvh Aug 7, 2017
57b46ea
DRY up `has_many` creation
joelvh Aug 7, 2017
3dd6891
Rename "query" to "scope"
joelvh Aug 7, 2017
679da0e
determine child type
joelvh Aug 7, 2017
96a588f
Fix Rails 4.0-5.0 tests using `extending` on model class when chainin…
joelvh Aug 10, 2017
0d57f0a
Fix DISTINCT queries in PostgreSQL for SELECT vs COUNT
joelvh Aug 10, 2017
039a94a
Drop support for Rails 4.0
joelvh Aug 10, 2017
1100d09
Use helper to build query
joelvh Aug 10, 2017
b0dbaa6
DRY up code with helpers
joelvh Aug 10, 2017
ba6015d
Make default member class configurable
joelvh Aug 10, 2017
f3b76b8
Fix `merge` to use `all` scope for Rails 4.0-5.0
joelvh Aug 10, 2017
f036617
Make public method names more user friendly
joelvh Aug 10, 2017
52c528d
Fix `children_association` parent reference
joelvh Aug 10, 2017
273b78d
Add `superclass` helper to get inherited values for STI
joelvh Aug 10, 2017
2d8004a
Use inherited class and association names
joelvh Aug 10, 2017
be3ddd4
Standardize same options on group members as on groups
joelvh Aug 10, 2017
1f1ecd7
DRY configuring defaults
joelvh Aug 10, 2017
9d65209
Return nothing if child association name not specified
joelvh Aug 10, 2017
2b82b3b
Don't delegate methods to create records
joelvh Aug 10, 2017
2310dd2
Updated README with polymorphic and STI documentation
joelvh Aug 10, 2017
566e7ae
Added tests for disabling default associations
joelvh Aug 10, 2017
7b8ee8f
Add block to extend `has_many`
joelvh Aug 10, 2017
5a3ade8
Indicate 4.1+ support
joelvh Aug 11, 2017
1475205
Updated README
joelvh Aug 11, 2017
434b6e6
Revert ActiveRecord configuration to inline for clarity
joelvh Aug 11, 2017
9ac8b4d
Replicated configuration setup from ActiveRecord to Mongoid
joelvh Aug 11, 2017
2ae566b
Renamed "merge" methods for clarity
joelvh Aug 11, 2017
bee094f
Removed redundant scope merge
joelvh Aug 11, 2017
ca534e0
Disable `appraisal` gem because it causes segfaults - loosen other ge…
joelvh Aug 11, 2017
a6d2a90
Don't add default associations by default, but provide `configure_leg…
joelvh Aug 11, 2017
003fe8f
Updated README
joelvh Aug 11, 2017
b72a143
Consistent naming of `opts`
joelvh Aug 11, 2017
8909726
Cleanup unused code
joelvh Aug 11, 2017
d9305e2
Update generator with default configuration comments
joelvh Aug 11, 2017
48ec17a
Note that groups and members need to be persisted first
joelvh Aug 16, 2017
fb89783
Add method to get membership types for a group/member combo
joelvh Aug 16, 2017
f302c0e
Add `ParentQueryBuilder` methods as extension methods to models
joelvh Aug 17, 2017
8768892
Build extensions in separate files for clarity
joelvh Aug 17, 2017
10c370a
Generate extension modules rather than using helper classes
joelvh Aug 17, 2017
7fba336
Pass extension block to `has_many`
joelvh Aug 17, 2017
d95c594
Continued de-duplication of code with dynamic module creation
joelvh Aug 17, 2017
3ef6176
Renamed `ModelMembershipExtensions` to `ModelExtensions`
joelvh Aug 17, 2017
7b6bc9c
Remove references to old classes
joelvh Aug 17, 2017
7f54082
Fix module naming
joelvh Aug 17, 2017
ec14b70
Added ability to search on multiple membership types (SQL `OR`/`IN(..…
joelvh Aug 17, 2017
e30b732
Fix tests and DSL to allow both group and member implemented on one m…
joelvh Aug 17, 2017
61612d8
Set `class_name` when inferring class from association name (STI)
joelvh Aug 18, 2017
d7ab6fd
Removed Rails 4.1 support
joelvh Aug 18, 2017
0757a3a
Removed unnecessary option
joelvh Aug 18, 2017
5f23bed
Fixes Rails 4.2 bug merging associations
joelvh Aug 19, 2017
6e668c7
Adding should throw validation exception because old version called `…
joelvh Aug 20, 2017
6a19ef0
Fix tests to use specific order of records for PostgreSQL
joelvh Aug 20, 2017
95c4ebb
Fix merging relations that don't go through group memberships
joelvh Aug 21, 2017
3c020ca
Fix test to order comparison for PostgreSQL
joelvh Aug 21, 2017
6469902
Remove db type check
joelvh Aug 21, 2017
76b70c6
Use new helper method
joelvh Aug 21, 2017
88c255a
Check for relation to generate subquery
joelvh Aug 30, 2017
cb5c741
Fixed exclusion path
joelvh Aug 30, 2017
f64a844
Require newer versions of gems
joelvh Aug 30, 2017
3f99967
Simplify generated variable names and clarify attribute references
joelvh Aug 30, 2017
3696802
Fix test that is using a class that wasn't designated as a group member
joelvh Sep 4, 2017
a46ff8e
Fix inverse association references for unsaved records
joelvh Sep 4, 2017
a1edc01
Test for persistence of new records when adding to groups
joelvh Sep 4, 2017
cde2b17
Added brief descriptions to polymorphic classes
joelvh Sep 4, 2017
284df94
Fix MySQL GROUP BY error when sql_mode=only_full_group_by
joelvh Sep 5, 2017
719430d
Add instructions on how to run tests
joelvh Sep 7, 2017
97d3db6
Made further cross-database SQL compatibility fixes for DISTINCT
joelvh Sep 7, 2017
3831368
Add line info for dynamic modules
joelvh Sep 7, 2017
3c7d529
Merge remote-tracking branch 'dwbutler/fix/rails5-named-groups' into …
joelvh Sep 7, 2017
ad62271
Clean membership handling - could add multiple memberships at once
joelvh Sep 8, 2017
ff2c0e2
Update authors
joelvh Sep 10, 2017
3959b50
Fix attempt to join and merge to do so on group model, not group memb…
joelvh Oct 4, 2017
e9b7e62
Join and merge is flawed - removed
joelvh Oct 4, 2017
05035e0
Clarify error source
joelvh Nov 1, 2017
dea8e47
Don't infer association or class name if `:class_name` option is spec…
joelvh Jan 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Set up your member models:
```ruby
class User
include Mongoid::Document

groupify :group_member
groupify :named_group_member
end
Expand Down Expand Up @@ -131,10 +131,71 @@ Example:
class Organization < Group
has_members :offices, :equipment
end

class InternationalOrganization < Organization
has_member :offices, class_name: 'CustomOfficeClass'
has_member :equipment, class_name: 'CustomEquipmentClass'
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
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.
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
Expand Down
4 changes: 4 additions & 0 deletions lib/groupify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
100 changes: 100 additions & 0 deletions lib/groupify/adapter/active_record/association_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module Groupify
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 <<(*)
super
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

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)}
end

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)

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

self
end
end
end
end
112 changes: 47 additions & 65 deletions lib/groupify/adapter/active_record/group.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'groupify/adapter/active_record/member_association_extensions'

module Groupify
module ActiveRecord

Expand All @@ -15,32 +17,25 @@ 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

has_many :group_memberships_as_group,
dependent: :destroy,
as: :group,
class_name: Groupify.group_membership_class_name
end

def member_classes
self.class.member_classes
end

def add(*args)
opts = args.extract_options!
membership_type = opts[:as]
members = args.flatten
return unless members.present?
def add(*members)
opts = members.extract_options!

__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!
end
member.__send__(:clear_association_cache)
members.flatten.each do |member|
member.groups.add(self, opts)
end

self
end

# Merge a source group into this group.
Expand All @@ -50,7 +45,8 @@ def merge!(source)

module ClassMethods
def with_member(member)
member.groups
memberships_merge(member.group_memberships_as_member).
extending(Groupify::ActiveRecord::GroupAssociationExtensions)
end

def default_member_class
Expand All @@ -69,73 +65,52 @@ 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
end
end

def has_member(name, options = {})
klass_name = options[:class_name]

if klass_name.nil?
klass = name.to_s.classify.constantize
register(klass)
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.
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.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

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

protected
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

module MemberAssociationExtensions
def as(membership_type)
merge(Groupify.group_membership_klass.as(membership_type))
end

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
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
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)
Expand All @@ -147,11 +122,18 @@ 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: MemberAssociationExtensions
->{ distinct },
through: :group_memberships_as_group,
source: :member,
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
Expand Down
26 changes: 26 additions & 0 deletions lib/groupify/adapter/active_record/group_association_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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
Loading