groupify 0.6.0.rc1 → 0.6.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/Appraisals +2 -11
- data/README.md +1 -1
- data/gemfiles/rails_4.0.gemfile +1 -1
- data/gemfiles/rails_4.1.gemfile +1 -1
- data/lib/groupify/adapter/active_record.rb +6 -580
- data/lib/groupify/adapter/active_record/group.rb +159 -0
- data/lib/groupify/adapter/active_record/group_member.rb +137 -0
- data/lib/groupify/adapter/active_record/group_membership.rb +49 -0
- data/lib/groupify/adapter/active_record/model.rb +48 -0
- data/lib/groupify/adapter/active_record/named_group_collection.rb +105 -0
- data/lib/groupify/adapter/active_record/named_group_member.rb +105 -0
- data/lib/groupify/adapter/mongoid.rb +7 -500
- data/lib/groupify/adapter/mongoid/group.rb +150 -0
- data/lib/groupify/adapter/mongoid/group_member.rb +132 -0
- data/lib/groupify/adapter/mongoid/member_scoped_as.rb +37 -0
- data/lib/groupify/adapter/mongoid/model.rb +38 -0
- data/lib/groupify/adapter/mongoid/named_group_collection.rb +87 -0
- data/lib/groupify/adapter/mongoid/named_group_member.rb +91 -0
- data/lib/groupify/railtie.rb +1 -1
- data/lib/groupify/version.rb +1 -1
- data/spec/{groupify/active_record_spec.rb → active_record_spec.rb} +13 -0
- data/spec/{groupify/mongoid_spec.rb → mongoid_spec.rb} +13 -0
- metadata +18 -6
@@ -0,0 +1,159 @@
|
|
1
|
+
module Groupify
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
# Usage:
|
5
|
+
# class Group < ActiveRecord::Base
|
6
|
+
# acts_as_group, :members => [:users]
|
7
|
+
# ...
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# group.add(member)
|
11
|
+
#
|
12
|
+
module Group
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
@default_member_class = nil
|
17
|
+
@member_klasses ||= Set.new
|
18
|
+
has_many :group_memberships, :dependent => :destroy
|
19
|
+
end
|
20
|
+
|
21
|
+
def member_classes
|
22
|
+
self.class.member_classes
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(*args)
|
26
|
+
opts = args.extract_options!
|
27
|
+
membership_type = opts[:as]
|
28
|
+
members = args.flatten
|
29
|
+
return unless members.present?
|
30
|
+
|
31
|
+
clear_association_cache
|
32
|
+
|
33
|
+
members.each do |member|
|
34
|
+
member.group_memberships.where(group_id: self.id).first_or_create!
|
35
|
+
if membership_type
|
36
|
+
member.group_memberships.where(group_id: self.id, membership_type: membership_type).first_or_create!
|
37
|
+
end
|
38
|
+
member.clear_association_cache
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Merge a source group into this group.
|
43
|
+
def merge!(source)
|
44
|
+
self.class.merge!(source, self)
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def with_member(member)
|
49
|
+
#joins(:group_memberships).where(:group_memberships => {:member_id => member.id, :member_type => member.class.to_s})
|
50
|
+
member.groups
|
51
|
+
end
|
52
|
+
|
53
|
+
def default_member_class
|
54
|
+
@default_member_class ||= (User rescue false)
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_member_class=(klass)
|
58
|
+
@default_member_class = klass
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the member classes defined for this class, as well as for the super classes
|
62
|
+
def member_classes
|
63
|
+
(@member_klasses ||= Set.new).merge(superclass.method_defined?(:member_classes) ? superclass.member_classes : [])
|
64
|
+
end
|
65
|
+
|
66
|
+
# Define which classes are members of this group
|
67
|
+
def has_members(name)
|
68
|
+
klass = name.to_s.classify.constantize
|
69
|
+
register(klass)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Merge two groups. The members of the source become members of the destination, and the source is destroyed.
|
73
|
+
def merge!(source_group, destination_group)
|
74
|
+
# Ensure that all the members of the source can be members of the destination
|
75
|
+
invalid_member_classes = (source_group.member_classes - destination_group.member_classes)
|
76
|
+
invalid_member_classes.each do |klass|
|
77
|
+
if klass.joins(:group_memberships).where(:group_memberships => {:group_id => source_group.id}).count > 0
|
78
|
+
raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
source_group.transaction do
|
83
|
+
source_group.group_memberships.update_all(:group_id => destination_group.id)
|
84
|
+
source_group.destroy
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def register(member_klass)
|
91
|
+
(@member_klasses ||= Set.new) << member_klass
|
92
|
+
|
93
|
+
associate_member_class(member_klass)
|
94
|
+
|
95
|
+
member_klass
|
96
|
+
end
|
97
|
+
|
98
|
+
module MemberAssociationExtensions
|
99
|
+
def as(membership_type)
|
100
|
+
where(group_memberships: {membership_type: membership_type})
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete(*args)
|
104
|
+
opts = args.extract_options!
|
105
|
+
members = args
|
106
|
+
|
107
|
+
if opts[:as]
|
108
|
+
proxy_association.owner.group_memberships.
|
109
|
+
where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]).
|
110
|
+
as(opts[:as]).
|
111
|
+
delete_all
|
112
|
+
else
|
113
|
+
super(*members)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def destroy(*args)
|
118
|
+
opts = args.extract_options!
|
119
|
+
members = args
|
120
|
+
|
121
|
+
if opts[:as]
|
122
|
+
proxy_association.owner.group_memberships.
|
123
|
+
where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]).
|
124
|
+
as(opts[:as]).
|
125
|
+
destroy_all
|
126
|
+
else
|
127
|
+
super(*members)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def associate_member_class(member_klass)
|
133
|
+
association_name = member_klass.name.to_s.pluralize.underscore.to_sym
|
134
|
+
source_type = member_klass.base_class
|
135
|
+
|
136
|
+
has_many association_name, through: :group_memberships, source: :member, source_type: source_type, extend: MemberAssociationExtensions
|
137
|
+
override_member_accessor(association_name)
|
138
|
+
|
139
|
+
if member_klass == default_member_class
|
140
|
+
has_many :members, through: :group_memberships, source: :member, source_type: source_type, extend: MemberAssociationExtensions
|
141
|
+
override_member_accessor(:members)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def override_member_accessor(association_name)
|
146
|
+
define_method(association_name) do |*args|
|
147
|
+
opts = args.extract_options!
|
148
|
+
membership_type = opts[:as]
|
149
|
+
if membership_type.present?
|
150
|
+
super().as(membership_type)
|
151
|
+
else
|
152
|
+
super()
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Groupify
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
# Usage:
|
5
|
+
# class User < ActiveRecord::Base
|
6
|
+
# acts_as_group_member
|
7
|
+
# ...
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# user.groups << group
|
11
|
+
#
|
12
|
+
module GroupMember
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
unless respond_to?(:group_memberships)
|
17
|
+
has_many :group_memberships, :as => :member, :autosave => true, :dependent => :destroy
|
18
|
+
end
|
19
|
+
|
20
|
+
has_many :groups, :through => :group_memberships, :class_name => @group_class_name do
|
21
|
+
def as(membership_type)
|
22
|
+
return self unless membership_type
|
23
|
+
where(group_memberships: {membership_type: membership_type})
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(*args)
|
27
|
+
opts = args.extract_options!
|
28
|
+
groups = args.flatten
|
29
|
+
|
30
|
+
if opts[:as]
|
31
|
+
proxy_association.owner.group_memberships.where(group_id: groups.map(&:id)).as(opts[:as]).delete_all
|
32
|
+
else
|
33
|
+
super(*groups)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy(*args)
|
38
|
+
opts = args.extract_options!
|
39
|
+
groups = args.flatten
|
40
|
+
|
41
|
+
if opts[:as]
|
42
|
+
proxy_association.owner.group_memberships.where(group_id: groups.map(&:id)).as(opts[:as]).destroy_all
|
43
|
+
else
|
44
|
+
super(*groups)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def in_group?(group, opts={})
|
51
|
+
criteria = {group_id: group.id}
|
52
|
+
|
53
|
+
if opts[:as]
|
54
|
+
criteria.merge!(membership_type: opts[:as])
|
55
|
+
end
|
56
|
+
|
57
|
+
group_memberships.exists?(criteria)
|
58
|
+
end
|
59
|
+
|
60
|
+
def in_any_group?(*args)
|
61
|
+
opts = args.extract_options!
|
62
|
+
groups = args
|
63
|
+
|
64
|
+
groups.flatten.each do |group|
|
65
|
+
return true if in_group?(group, opts)
|
66
|
+
end
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
|
70
|
+
def in_all_groups?(*args)
|
71
|
+
opts = args.extract_options!
|
72
|
+
groups = args.flatten
|
73
|
+
|
74
|
+
groups.to_set.subset? self.groups.as(opts[:as]).to_set
|
75
|
+
end
|
76
|
+
|
77
|
+
def in_only_groups?(*args)
|
78
|
+
opts = args.extract_options!
|
79
|
+
groups = args.flatten
|
80
|
+
|
81
|
+
groups.to_set == self.groups.as(opts[:as]).to_set
|
82
|
+
end
|
83
|
+
|
84
|
+
def shares_any_group?(other, opts={})
|
85
|
+
in_any_group?(other.groups, opts)
|
86
|
+
end
|
87
|
+
|
88
|
+
module ClassMethods
|
89
|
+
def group_class_name; @group_class_name ||= 'Group'; end
|
90
|
+
def group_class_name=(klass); @group_class_name = klass; end
|
91
|
+
|
92
|
+
def as(membership_type)
|
93
|
+
joins(:group_memberships).where(group_memberships: { membership_type: membership_type })
|
94
|
+
end
|
95
|
+
|
96
|
+
def in_group(group)
|
97
|
+
return none unless group.present?
|
98
|
+
|
99
|
+
joins(:group_memberships).where(group_memberships: { group_id: group.id }).uniq
|
100
|
+
end
|
101
|
+
|
102
|
+
def in_any_group(*groups)
|
103
|
+
groups = groups.flatten
|
104
|
+
return none unless groups.present?
|
105
|
+
|
106
|
+
joins(:group_memberships).where(group_memberships: { group_id: groups.map(&:id) }).uniq
|
107
|
+
end
|
108
|
+
|
109
|
+
def in_all_groups(*groups)
|
110
|
+
groups = groups.flatten
|
111
|
+
return none unless groups.present?
|
112
|
+
|
113
|
+
joins(:group_memberships).
|
114
|
+
group(:"group_memberships.member_id").
|
115
|
+
where(:group_memberships => {:group_id => groups.map(&:id)}).
|
116
|
+
having("COUNT(group_memberships.group_id) = #{groups.count}").
|
117
|
+
uniq
|
118
|
+
end
|
119
|
+
|
120
|
+
def in_only_groups(*groups)
|
121
|
+
groups = groups.flatten
|
122
|
+
return none unless groups.present?
|
123
|
+
|
124
|
+
joins(:group_memberships).
|
125
|
+
group(:"group_memberships.member_id").
|
126
|
+
having("COUNT(DISTINCT group_memberships.group_id) = #{groups.count}").
|
127
|
+
uniq
|
128
|
+
end
|
129
|
+
|
130
|
+
def shares_any_group(other)
|
131
|
+
in_any_group(other.groups)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Groupify
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
# Join table that tracks which members belong to which groups
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# class GroupMembership < ActiveRecord::Base
|
8
|
+
# acts_as_group_membership
|
9
|
+
# ...
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
module GroupMembership
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
attr_accessible(:member, :group, :group_name, :membership_type, :as) if ActiveSupport::VERSION::MAJOR < 4
|
17
|
+
|
18
|
+
belongs_to :member, :polymorphic => true
|
19
|
+
belongs_to :group
|
20
|
+
end
|
21
|
+
|
22
|
+
def membership_type=(membership_type)
|
23
|
+
self[:membership_type] = membership_type.to_s if membership_type.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
def as=(membership_type)
|
27
|
+
self.membership_type = membership_type
|
28
|
+
end
|
29
|
+
|
30
|
+
def as
|
31
|
+
membership_type
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def named(group_name=nil)
|
36
|
+
if group_name.present?
|
37
|
+
where(group_name: group_name)
|
38
|
+
else
|
39
|
+
where("group_name IS NOT NULL")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def as(membership_type)
|
44
|
+
where(membership_type: membership_type)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Groupify
|
2
|
+
module ActiveRecord
|
3
|
+
module Model
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
# Define a scope that returns nothing.
|
8
|
+
# This is built into ActiveRecord 4, but not 3
|
9
|
+
unless self.class.respond_to? :none
|
10
|
+
def self.none
|
11
|
+
where(arel_table[:id].eq(nil).and(arel_table[:id].not_eq(nil)))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def acts_as_group(opts = {})
|
18
|
+
include Groupify::ActiveRecord::Group
|
19
|
+
|
20
|
+
if (member_klass = opts.delete :default_members)
|
21
|
+
self.default_member_class = member_klass.to_s.classify.constantize
|
22
|
+
end
|
23
|
+
|
24
|
+
if (member_klasses = opts.delete :members)
|
25
|
+
member_klasses.each do |member_klass|
|
26
|
+
has_members(member_klass)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def acts_as_group_member(opts = {})
|
32
|
+
@group_class_name = opts[:class_name] || 'Group'
|
33
|
+
include Groupify::ActiveRecord::GroupMember
|
34
|
+
end
|
35
|
+
|
36
|
+
def acts_as_named_group_member(opts = {})
|
37
|
+
include Groupify::ActiveRecord::NamedGroupMember
|
38
|
+
end
|
39
|
+
|
40
|
+
def acts_as_group_membership(opts = {})
|
41
|
+
include Groupify::ActiveRecord::GroupMembership
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ActiveRecord::Base.send :include, Groupify::ActiveRecord::Model
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Groupify
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
class NamedGroupCollection < Set
|
5
|
+
def initialize(member)
|
6
|
+
@member = member
|
7
|
+
@named_group_memberships = member.group_memberships.named
|
8
|
+
@group_names = @named_group_memberships.pluck(:group_name).map(&:to_sym)
|
9
|
+
super(@group_names)
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(named_group, opts={})
|
13
|
+
named_group = named_group.to_sym
|
14
|
+
membership_type = opts[:as]
|
15
|
+
|
16
|
+
if @member.new_record?
|
17
|
+
@member.group_memberships.build(group_name: named_group, membership_type: nil)
|
18
|
+
else
|
19
|
+
@member.transaction do
|
20
|
+
@member.group_memberships.where(group_name: named_group, membership_type: nil).first_or_create!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if membership_type
|
25
|
+
if @member.new_record?
|
26
|
+
@member.group_memberships.build(group_name: named_group, membership_type: membership_type)
|
27
|
+
else
|
28
|
+
@member.group_memberships.where(group_name: named_group, membership_type: membership_type).first_or_create!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
super(named_group)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :push, :add
|
36
|
+
alias_method :<<, :add
|
37
|
+
|
38
|
+
def merge(*args)
|
39
|
+
opts = args.extract_options!
|
40
|
+
named_groups = args.flatten
|
41
|
+
named_groups.each do |named_group|
|
42
|
+
add(named_group, opts)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :concat, :merge
|
47
|
+
|
48
|
+
def include?(named_group, opts={})
|
49
|
+
named_group = named_group.to_sym
|
50
|
+
if opts[:as]
|
51
|
+
as(opts[:as]).include?(named_group)
|
52
|
+
else
|
53
|
+
super(named_group)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(*args)
|
58
|
+
opts = args.extract_options!
|
59
|
+
named_groups = args.flatten.compact
|
60
|
+
|
61
|
+
remove(named_groups, :delete_all, opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy(*args)
|
65
|
+
opts = args.extract_options!
|
66
|
+
named_groups = args.flatten.compact
|
67
|
+
|
68
|
+
remove(named_groups, :destroy_all, opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear
|
72
|
+
@named_group_memberships.delete_all
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
alias_method :delete_all, :clear
|
77
|
+
alias_method :destroy_all, :clear
|
78
|
+
|
79
|
+
# Criteria to filter by membership type
|
80
|
+
def as(membership_type)
|
81
|
+
@named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym)
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def remove(named_groups, method, opts)
|
87
|
+
if named_groups.present?
|
88
|
+
scope = @named_group_memberships.where(group_name: named_groups)
|
89
|
+
|
90
|
+
if opts[:as]
|
91
|
+
scope = scope.where(membership_type: opts[:as])
|
92
|
+
end
|
93
|
+
|
94
|
+
scope.send(method)
|
95
|
+
|
96
|
+
unless opts[:as]
|
97
|
+
named_groups.each do |named_group|
|
98
|
+
@hash.delete(named_group)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|