groupify 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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