groupify 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,249 @@
1
+ require 'mongoid'
2
+ require 'set'
3
+
4
+ # Groups and members
5
+ module Groupify
6
+ module Mongoid
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def none; where(:id => nil); end
11
+ end
12
+
13
+ module ClassMethods
14
+ def acts_as_group(opts = {})
15
+ include Groupify::Mongoid::Group
16
+
17
+ if (member_klass = opts.delete :default_members)
18
+ self.default_member_class = member_klass.to_s.classify.constantize
19
+ end
20
+
21
+ if (member_klasses = opts.delete :members)
22
+ member_klasses.each do |member_klass|
23
+ has_members(member_klass)
24
+ end
25
+ end
26
+ end
27
+
28
+ def acts_as_group_member(opts = {})
29
+ @group_class_name = opts[:class_name] || 'Group'
30
+ include Groupify::Mongoid::GroupMember
31
+ end
32
+
33
+ def acts_as_named_group_member(opts = {})
34
+ include Groupify::Mongoid::NamedGroupMember
35
+ end
36
+ end
37
+
38
+ # Usage:
39
+ # class Group
40
+ # include Mongoid::Document
41
+ #
42
+ # acts_as_group, :members => [:users]
43
+ # ...
44
+ # end
45
+ #
46
+ # group.add(member)
47
+ #
48
+ module Group
49
+ extend ActiveSupport::Concern
50
+
51
+ included do
52
+ @default_member_class = nil
53
+ @member_klasses ||= Set.new
54
+ end
55
+
56
+ def members
57
+ self.class.default_member_class.any_in(:group_ids => [self.id])
58
+ end
59
+
60
+ def member_classes
61
+ self.class.member_classes
62
+ end
63
+
64
+ def add(member)
65
+ member.groups << self
66
+ end
67
+
68
+ # Merge a source group into this group.
69
+ def merge!(source)
70
+ self.class.merge!(source, self)
71
+ end
72
+
73
+ module ClassMethods
74
+ def with_member(member)
75
+ criteria.for_ids(member.group_ids)
76
+ end
77
+
78
+ def default_member_class
79
+ @default_member_class ||= register(User)
80
+ end
81
+
82
+ def default_member_class=(klass)
83
+ @default_member_class = klass
84
+ end
85
+
86
+ # Returns the member classes defined for this class, as well as for the super classes
87
+ def member_classes
88
+ (@member_klasses ||= Set.new).merge(superclass.method_defined?(:member_classes) ? superclass.member_classes : [])
89
+ end
90
+
91
+ # Define which classes are members of this group
92
+ def has_members(name)
93
+ klass = name.to_s.classify.constantize
94
+ register(klass)
95
+
96
+ # Define specific members accessor, i.e. group.users
97
+ define_method name.to_s.pluralize.underscore do
98
+ klass.any_in(:group_ids => [self.id])
99
+ end
100
+ end
101
+
102
+ # Merge two groups. The members of the source become members of the destination, and the source is destroyed.
103
+ def merge!(source_group, destination_group)
104
+ # Ensure that all the members of the source can be members of the destination
105
+ invalid_member_classes = (source_group.member_classes - destination_group.member_classes)
106
+ invalid_member_classes.each do |klass|
107
+ if klass.any_in(:group_ids => [source_group.id]).count > 0
108
+ raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}")
109
+ end
110
+ end
111
+
112
+ source_group.member_classes.each do |klass|
113
+ klass.any_in(:group_ids => [source_group.id]).update(:$set => {:"group_ids.$" => destination_group.id})
114
+ end
115
+ source_group.destroy
116
+ end
117
+
118
+ protected
119
+
120
+ def register(member_klass)
121
+ (@member_klasses ||= Set.new) << member_klass
122
+ member_klass
123
+ end
124
+ end
125
+ end
126
+
127
+ # Usage:
128
+ # class User
129
+ # include Mongoid::Document
130
+ #
131
+ # acts_as_group_member
132
+ # ...
133
+ # end
134
+ #
135
+ # user.groups << group
136
+ #
137
+ module GroupMember
138
+ extend ActiveSupport::Concern
139
+
140
+ included do
141
+ has_and_belongs_to_many :groups, :autosave => true, :inverse_of => nil, :class_name => @group_class_name
142
+ end
143
+
144
+ def in_group?(group)
145
+ self.groups.include?(group)
146
+ end
147
+
148
+ def in_any_group?(*groups)
149
+ groups.flatten.each do |group|
150
+ return true if in_group?(group)
151
+ end
152
+ return false
153
+ end
154
+
155
+ def in_all_groups?(*groups)
156
+ Set.new(groups.flatten) == Set.new(self.named_groups)
157
+ end
158
+
159
+ def shares_any_group?(other)
160
+ in_any_group?(other.groups)
161
+ end
162
+
163
+ module ClassMethods
164
+ def group_class_name; @group_class_name ||= 'Group'; end
165
+ def group_class_name=(klass); @group_class_name = klass; end
166
+
167
+ def in_group(group)
168
+ group.present? ? where(:group_ids.in => [group.id]) : none
169
+ end
170
+
171
+ def in_any_group(*groups)
172
+ groups.present? ? where(:group_ids.in => groups.flatten.map(&:id)) : none
173
+ end
174
+
175
+ def in_all_groups(*groups)
176
+ groups.present? ? where(:group_ids => groups.flatten.map(&:id)) : none
177
+ end
178
+
179
+ def shares_any_group(other)
180
+ in_any_group(other.groups)
181
+ end
182
+
183
+ end
184
+ end
185
+
186
+ # Usage:
187
+ # class User
188
+ # include Mongoid::Document
189
+ #
190
+ # acts_as_named_group_member
191
+ # ...
192
+ # end
193
+ #
194
+ # user.named_groups << :admin
195
+ #
196
+ module NamedGroupMember
197
+ extend ActiveSupport::Concern
198
+
199
+ included do
200
+ field :named_groups, :type => Array, :default => []
201
+
202
+ before_save :uniq_named_groups
203
+ protected
204
+ def uniq_named_groups
205
+ named_groups.uniq!
206
+ end
207
+ end
208
+
209
+ def in_named_group?(group)
210
+ named_groups.include?(group)
211
+ end
212
+
213
+ def in_any_named_group?(*groups)
214
+ groups.flatten.each do |group|
215
+ return true if in_named_group?(group)
216
+ end
217
+ return false
218
+ end
219
+
220
+ def in_all_named_groups?(*groups)
221
+ Set.new(groups.flatten) == Set.new(self.named_groups)
222
+ end
223
+
224
+ def shares_any_named_group?(other)
225
+ in_any_named_group?(other.named_groups)
226
+ end
227
+
228
+ module ClassMethods
229
+ def in_named_group(named_group)
230
+ named_group.present? ? where(:named_groups.in => [named_group]) : none
231
+ end
232
+
233
+ def in_any_named_group(*named_groups)
234
+ named_groups.present? ? where(:named_groups.in => named_groups.flatten) : none
235
+ end
236
+
237
+ def in_all_named_groups(*named_groups)
238
+ named_groups.present? ? where(:named_groups => named_groups.flatten) : none
239
+ end
240
+
241
+ def shares_any_named_group(other)
242
+ in_any_named_group(other.named_groups)
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ Mongoid::Document.send :include, Groupify::Mongoid
@@ -0,0 +1,20 @@
1
+ require 'groupify'
2
+ require 'rails'
3
+
4
+ module Groupify
5
+ class Railtie < Rails::Railtie
6
+
7
+ initializer "groupify.active_record" do |app|
8
+ ActiveSupport.on_load :active_record do
9
+ require 'groupify/adapter/active_record'
10
+ end
11
+ end
12
+
13
+ initializer "groupify.mongoid" do |app|
14
+ if defined?(Mongoid)
15
+ require 'groupify/adapter/mongoid'
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Groupify
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/groupify.rb CHANGED
@@ -1,197 +1,3 @@
1
1
  require 'active_support'
2
- require 'mongoid'
3
- require 'set'
4
2
 
5
- # Groups and members
6
- module Groupify
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- def none; where(:id => nil); end
11
- end
12
-
13
- module ClassMethods
14
- def acts_as_group(opts = {})
15
- include Groupify::Group
16
-
17
- if (member_klass = opts.delete :default_members)
18
- self.default_member_class = member_klass.to_s.classify.constantize
19
- end
20
-
21
- if (member_klasses = opts.delete :members)
22
- member_klasses.each do |member_klass|
23
- has_members(member_klass)
24
- end
25
- end
26
- end
27
-
28
- def acts_as_group_member(opts = {})
29
- class_eval { @group_class_name = opts[:class_name] || 'Group' }
30
- include Groupify::GroupMember
31
- end
32
-
33
- def acts_as_named_group_member(opts = {})
34
- include Groupify::NamedGroupMember
35
- end
36
- end
37
-
38
- # Usage:
39
- # class Group
40
- # acts_as_group, :members => [:users]
41
- # ...
42
- # end
43
- #
44
- # group.add(member)
45
- #
46
- module Group
47
- extend ActiveSupport::Concern
48
-
49
- included do
50
- @default_member_class = nil
51
- end
52
-
53
- def members
54
- self.class.default_member_class.any_in(:group_ids => [self.id])
55
- end
56
-
57
- def add(member)
58
- member.groups << self
59
- end
60
-
61
- module ClassMethods
62
- def with_member(member)
63
- criteria.for_ids(member.group_ids)
64
- end
65
-
66
- def default_member_class; @default_member_class || User; end
67
- def default_member_class=(klass); @default_member_class = klass; end
68
-
69
- # Define which classes are members of this group
70
- def has_members(name)
71
- klass = name.to_s.classify.constantize
72
- define_method name.to_s.pluralize.underscore do
73
- klass.any_in(:group_ids => [self.id])
74
- end
75
- end
76
- end
77
- end
78
-
79
- # Usage:
80
- # class User
81
- # acts_as_group_member
82
- # ...
83
- # end
84
- #
85
- # user.groups << group
86
- #
87
- module GroupMember
88
- extend ActiveSupport::Concern
89
-
90
- included do
91
- group_class_name='Group' unless defined?(@group_class_name)
92
- has_and_belongs_to_many :groups, :autosave => true, :inverse_of => nil, :class_name => @group_class_name
93
- end
94
-
95
- def in_group?(group)
96
- self.groups.include?(group)
97
- end
98
-
99
- def in_any_group?(*groups)
100
- groups.flatten.each do |group|
101
- return true if in_group?(group)
102
- end
103
- return false
104
- end
105
-
106
- def in_all_groups?(*groups)
107
- Set.new(groups.flatten) == Set.new(self.named_groups)
108
- end
109
-
110
- def shares_any_group?(other)
111
- in_any_group?(other.groups)
112
- end
113
-
114
- module ClassMethods
115
- def group_class_name; @group_class_name || 'Group'; end
116
- def group_class_name=(klass); @group_class_name = klass; end
117
-
118
- def in_group(group)
119
- group.present? ? where(:group_ids.in => [group.id]) : none
120
- end
121
-
122
- def in_any_group(*groups)
123
- groups.present? ? where(:group_ids.in => groups.flatten.map{|g|g.id}) : none
124
- end
125
-
126
- def in_all_groups(*groups)
127
- groups.present? ? where(:group_ids => groups.flatten.map{|g|g.id}) : none
128
- end
129
-
130
- def shares_any_group(other)
131
- in_any_group(other.groups)
132
- end
133
-
134
- end
135
- end
136
-
137
- # Usage:
138
- # class User
139
- # acts_as_named_group_member
140
- # ...
141
- # end
142
- #
143
- # user.named_groups << :admin
144
- #
145
- module NamedGroupMember
146
- extend ActiveSupport::Concern
147
-
148
- included do
149
- field :named_groups, :type => Array, :default => []
150
-
151
- before_save :uniq_named_groups
152
- protected
153
- def uniq_named_groups
154
- named_groups.uniq!
155
- end
156
- end
157
-
158
- def in_named_group?(group)
159
- self.named_groups.include?(group)
160
- end
161
-
162
- def in_any_named_group?(*groups)
163
- groups.flatten.each do |group|
164
- return true if in_named_group?(group)
165
- end
166
- return false
167
- end
168
-
169
- def in_all_named_groups?(*groups)
170
- Set.new(groups.flatten) == Set.new(self.named_groups)
171
- end
172
-
173
- def shares_any_named_group?(other)
174
- in_any_named_group?(other.named_groups)
175
- end
176
-
177
- module ClassMethods
178
- def in_named_group(named_group)
179
- named_group.present? ? where(:named_groups.in => [named_group]) : none
180
- end
181
-
182
- def in_any_named_group(*named_groups)
183
- named_groups.present? ? where(:named_groups.in => named_groups.flatten) : none
184
- end
185
-
186
- def in_all_named_groups(*named_groups)
187
- named_groups.present? ? where(:named_groups => named_groups.flatten) : none
188
- end
189
-
190
- def shares_any_named_group(other)
191
- in_any_named_group(other.named_groups)
192
- end
193
- end
194
- end
195
- end
196
-
197
- Mongoid::Document.send :include, Groupify
3
+ require 'groupify/railtie' if defined?(Rails)
@@ -0,0 +1,231 @@
1
+ require 'spec_helper'
2
+ require 'active_record'
3
+
4
+ # Load database config
5
+ if JRUBY
6
+ require 'jdbc/sqlite3'
7
+ require 'active_record'
8
+ require 'active_record/connection_adapters/jdbcsqlite3_adapter'
9
+ else
10
+ require 'sqlite3'
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.before(:suite) do
15
+ DatabaseCleaner[:active_record].strategy = :transaction
16
+ end
17
+
18
+ config.before(:each) do
19
+ DatabaseCleaner[:active_record].start
20
+ end
21
+
22
+ config.after(:each) do
23
+ DatabaseCleaner[:active_record].clean
24
+ end
25
+ end
26
+
27
+ ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" )
28
+
29
+ ActiveRecord::Migration.verbose = false
30
+ ActiveRecord::Schema.define(:version => 1) do
31
+
32
+ create_table :groups do |t|
33
+ t.string :name
34
+ t.string :type
35
+ end
36
+
37
+ create_table :group_memberships do |t|
38
+ t.string :member_type
39
+ t.integer :member_id
40
+ t.integer :group_id
41
+ t.string :group_name
42
+ end
43
+
44
+ create_table :users do |t|
45
+ t.string :name
46
+ t.string :type
47
+ end
48
+
49
+ create_table :widgets do |t|
50
+ t.string :name
51
+ end
52
+
53
+ create_table :organizations do |t|
54
+ t.string :name
55
+ end
56
+ end
57
+
58
+ require 'groupify'
59
+ require 'groupify/adapter/active_record'
60
+
61
+ class User < ActiveRecord::Base
62
+ acts_as_group_member
63
+ acts_as_named_group_member
64
+ end
65
+
66
+ class Widget < ActiveRecord::Base
67
+ acts_as_group_member
68
+ end
69
+
70
+ class Group < ActiveRecord::Base
71
+ acts_as_group :members => [:users, :widgets], :default_members => :users
72
+ end
73
+
74
+ class Manager < User
75
+ end
76
+
77
+ class Organization < Group
78
+ has_members :managers
79
+ end
80
+
81
+ class GroupMembership < ActiveRecord::Base
82
+ acts_as_group_membership
83
+ end
84
+
85
+ describe Group do
86
+ it { should respond_to :members}
87
+ it { should respond_to :add }
88
+ end
89
+
90
+ describe User do
91
+ it { should respond_to :groups}
92
+ it { should respond_to :in_group?}
93
+ it { should respond_to :in_any_group?}
94
+ it { should respond_to :in_all_groups?}
95
+ it { should respond_to :shares_any_group?}
96
+ end
97
+
98
+ describe Groupify::ActiveRecord do
99
+ let(:user) { User.create! }
100
+ let(:group) { Group.create! }
101
+ let(:widget) { Widget.create! }
102
+
103
+ it "adds a group to a member" do
104
+ user.groups << group
105
+ user.groups.should include(group)
106
+ group.members.should include(user)
107
+ group.users.should include(user)
108
+ end
109
+
110
+ it "adds a member to a group" do
111
+ group.add user
112
+ user.groups.should include(group)
113
+ group.members.should include(user)
114
+ end
115
+
116
+ it 'lists which member classes can belong to this group' do
117
+ group.class.member_classes.should include(User, Widget)
118
+ group.member_classes.should include(User, Widget)
119
+
120
+ Organization.member_classes.should include(User, Widget, Manager)
121
+ end
122
+
123
+ it "finds members by group" do
124
+ group.add user
125
+
126
+ User.in_group(group).first.should eql(user)
127
+ end
128
+
129
+ it "finds the group a member belongs to" do
130
+ group.add user
131
+
132
+ Group.with_member(user).first.should == group
133
+ end
134
+
135
+ context 'when merging' do
136
+ let(:task) { Task.create! }
137
+ let(:manager) { Manager.create! }
138
+
139
+ it "moves the members from source to destination and destroys the source" do
140
+ source = Group.create!
141
+ destination = Organization.create!
142
+
143
+ source.add(user)
144
+ destination.add(manager)
145
+
146
+ destination.merge!(source)
147
+ source.destroyed?.should be_true
148
+
149
+ destination.users.should include(user, manager)
150
+ destination.managers.should include(manager)
151
+ end
152
+
153
+ it "fails to merge if the destination group cannot contain the source group's members" do
154
+ source = Organization.create!
155
+ destination = Group.create!
156
+
157
+ source.add(manager)
158
+ destination.add(user)
159
+
160
+ # Managers cannot be members of a Group
161
+ expect {destination.merge!(source)}.to raise_error(ArgumentError)
162
+ end
163
+
164
+ it "merges incompatible groups as long as all the source members can be moved to the destination" do
165
+ source = Organization.create!
166
+ destination = Group.create!
167
+
168
+ source.add(user)
169
+ destination.add(widget)
170
+
171
+ expect {destination.merge!(source)}.to_not raise_error
172
+
173
+ source.destroyed?.should be_true
174
+ destination.users.to_a.should include(user)
175
+ destination.widgets.to_a.should include(widget)
176
+ end
177
+ end
178
+
179
+ it "members can belong to many groups" do
180
+ user.groups << group
181
+ group2 = Group.create!
182
+ user.groups << group2
183
+
184
+ user.groups.should include(group)
185
+ user.groups.should include(group2)
186
+
187
+ User.in_group(group).first.should eql(user)
188
+ User.in_group(group2).first.should eql(user)
189
+
190
+ User.in_any_group(group).first.should eql(user)
191
+ User.in_all_groups(group, group2).first.should eql(user)
192
+ User.in_all_groups([group, group2]).first.should eql(user)
193
+ end
194
+
195
+ it "members can have named groups" do
196
+ user.named_groups << :admin
197
+ user.named_groups << :user
198
+ user.save
199
+ user.named_groups.should include(:admin)
200
+
201
+ user.in_named_group?(:admin).should be_true
202
+ user.in_any_named_group?(:admin, :user, :test).should be_true
203
+ user.in_all_named_groups?(:admin, :user).should be_true
204
+ user.in_all_named_groups?(:admin, :user, :test).should be_false
205
+
206
+ User.in_named_group(:admin).first.should eql(user)
207
+ User.in_any_named_group(:admin, :test).first.should eql(user)
208
+ User.in_all_named_groups(:admin, :user).first.should eql(user)
209
+
210
+ # Uniqueness
211
+ user.named_groups << :admin
212
+ user.save
213
+ user.named_groups.count{|g| g == :admin}.should == 1
214
+ end
215
+
216
+ it "members can check if groups are shared" do
217
+ user.groups << group
218
+ user2 = User.create!(:groups => [group])
219
+
220
+ user.shares_any_group?(user2).should be_true
221
+ User.shares_any_group(user).to_a.should include(user2)
222
+ end
223
+
224
+ it "members can check if named groups are shared" do
225
+ user.named_groups << :admin
226
+ user2 = User.create!(:named_groups => [:admin])
227
+
228
+ user.shares_any_named_group?(user2).should be_true
229
+ User.shares_any_named_group(user).to_a.should include(user2)
230
+ end
231
+ end
File without changes