groupify 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -1,27 +1,11 @@
1
1
  language: ruby
2
2
  services: mongodb
3
3
  rvm:
4
- - ree
5
- - 1.8.7
6
4
  - 1.9.3
7
5
  - 2.0.0
8
- - jruby-18mode # JRuby in 1.8 mode
9
6
  - jruby-19mode # JRuby in 1.9 mode
10
- - rbx-18mode
11
7
  - rbx-19mode
12
8
  gemfile:
13
- - gemfiles/mongoid2.gemfile
14
- - gemfiles/mongoid3.gemfile
15
- matrix:
16
- exclude:
17
- - rvm: 1.8.7
18
- gemfile: gemfiles/mongoid3.gemfile
19
- - rvm: ree
20
- gemfile: gemfiles/mongoid3.gemfile
21
- - rvm: jruby-18mode
22
- gemfile: gemfiles/mongoid3.gemfile
23
- - rvm: rbx-18mode
24
- gemfile: gemfiles/mongoid3.gemfile
25
- allow_failures:
26
- - rvm: rbx-18mode
9
+ - gemfiles/activesupport3.gemfile
10
+ - gemfiles/activesupport4.gemfile
27
11
 
data/Gemfile CHANGED
@@ -1,4 +1,14 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in groupify.gemspec
4
4
  gemspec
5
+
6
+ platform :jruby do
7
+ gem "jdbc-sqlite3"
8
+ gem "activerecord-jdbcsqlite3-adapter"
9
+
10
+ if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
11
+ gem "json"
12
+ end
13
+ end
14
+
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # Groupify [![Build Status](https://secure.travis-ci.org/dwbutler/groupify.png)](http://travis-ci.org/dwbutler/groupify)
1
+ # Groupify [![Build Status](https://secure.travis-ci.org/dwbutler/groupify.png)](http://travis-ci.org/dwbutler/groupify) [![Dependency Status](https://gemnasium.com/dwbutler/groupify.png)](https://gemnasium.com/dwbutler/groupify)
2
2
  Adds group and membership functionality to Rails models.
3
3
 
4
- Currently only Mongoid 2 and 3 are supported. Tested in Ruby 1.8.7 and 1.9.3 (MRI, REE, JRuby, and Rubinius).
5
- It shouldn't be difficult to adapt to ActiveRecord.
4
+ The following ORMs are supported:
5
+ Mongoid 3.1 & 4.0, ActiveRecord 3.2 & 4.0
6
+
7
+ The following Rubies are supported:
8
+ Ruby 1.9.3, 2.0.0 (MRI, REE, JRuby, and Rubinius).
6
9
 
7
10
  ## Installation
8
11
 
@@ -19,28 +22,75 @@ Or install it yourself as:
19
22
  $ gem install groupify
20
23
 
21
24
  ## Getting Started
25
+
26
+ ### Active Record
27
+ Add a migration similar to the following:
28
+
29
+ ```ruby
30
+ class CreateGroups < ActiveRecord::Migration
31
+ def change
32
+ create_table :groups do |t|
33
+ t.string :type # Only needed if using single table inheritence
34
+ end
35
+
36
+ create_table :group_memberships do |t|
37
+ t.string :member_type # Needed to make polymorphic members work
38
+ t.integer :member_id # The member that belongs to this group
39
+ t.integer :group_id # The group to which the member belongs
40
+ t.string :group_name # Links a member to a named group (if using named groups)
41
+ end
42
+
43
+ add_index :group_memberships, [:member_id, :member_type]
44
+ add_index :group_memberships, :group_id
45
+ add_index :group_memberships, :group_name
46
+ end
47
+ end
48
+ ```
49
+
50
+ In your group model:
51
+
52
+ ```ruby
53
+ class Group < ActiveRecord::Base
54
+ acts_as_group :members => [:users, :assignments], :default_members => :users
55
+ end
56
+ ```
57
+
58
+ In your member models (i.e. `User`):
59
+
60
+ ```ruby
61
+ class User < ActiveRecord::Base
62
+ acts_as_group_member
63
+ acts_as_named_group_member
64
+ end
65
+
66
+ class Assignment < ActiveRecord::Base
67
+ acts_as_group_member
68
+ end
69
+ ```
70
+
71
+ ### Mongoid
22
72
  In your group model:
23
73
 
24
74
  ```ruby
25
75
  class Group
26
- include Mongoid::Document
76
+ include Mongoid::Document
27
77
 
28
- acts_as_group
78
+ acts_as_group :members => [:users], :default_members => :users
29
79
  end
30
80
  ```
31
81
 
32
- In your user model:
82
+ In your member models (i.e. `User`):
33
83
 
34
84
  ```ruby
35
85
  class User
36
- include Mongoid::Document
37
-
38
- acts_as_group_member
39
- acts_as_named_group_member
86
+ include Mongoid::Document
87
+
88
+ acts_as_group_member
89
+ acts_as_named_group_member
40
90
  end
41
91
  ```
42
92
 
43
- ## Usage
93
+ ## Basic Usage
44
94
 
45
95
  Create groups and add members:
46
96
 
@@ -49,20 +99,22 @@ group = Group.new
49
99
  user = User.new
50
100
 
51
101
  user.groups << group
52
- or
102
+ # or
53
103
  group.add user
54
104
 
55
- user.in_group?(group) => true
105
+ user.in_group?(group)
106
+ # => true
56
107
  ```
57
108
 
58
109
  Add to named groups:
59
110
 
60
111
  ```ruby
61
112
  user.named_groups << :admin
62
- user.in_named_group?(:admin) => true
113
+ user.in_named_group?(:admin)
114
+ # => true
63
115
  ```
64
116
 
65
- Check if two group members share any of the same groups:
117
+ Check if two members share any of the same groups:
66
118
 
67
119
  ```ruby
68
120
  user1.shares_any_group?(user2)
@@ -72,16 +124,44 @@ user2.shares_any_named_group?(user1)
72
124
  Query for groups & members:
73
125
 
74
126
  ```ruby
75
- User.in_group(group) # Find all users in this group
76
- User.in_named_group(:admin) # Find all users in this named group
77
- Group.with_member(user) # Find all groups with this user
127
+ User.in_group(group) # Find all users in this group
128
+ User.in_named_group(:admin) # Find all users in this named group
129
+ Group.with_member(user) # Find all groups with this user
78
130
 
79
- User.shares_any_group(user) # Find all users that share any groups with this user
80
- User.shares_any_named_group(user) # Find all users that share any named groups with this user
131
+ User.shares_any_group(user) # Find all users that share any groups with this user
132
+ User.shares_any_named_group(user) # Find all users that share any named groups with this user
133
+ ```
134
+
135
+ Merge one group into another:
136
+
137
+ ```ruby
138
+ # Moves the members of source into destination, and destroys source
139
+ destination_group.merge!(source_group)
81
140
  ```
82
141
 
83
142
  Check the specs for more details.
84
143
 
144
+ ## Using for Authorization
145
+ Groupify was originally created to help implement user authorization, although it can be used
146
+ generically for much more than that. Here is how to do it.
147
+
148
+ ### With CanCan
149
+
150
+ ```ruby
151
+ class Ability
152
+ include CanCan::Ability
153
+
154
+ def initialize(user)
155
+
156
+ # Implements group authorization
157
+ # Users can only manage assignment which belong to the same group
158
+ can [:manage], Assignment, Assignment.shares_any_group(user) do |assignment|
159
+ assignment.shares_any_group?(user)
160
+ end
161
+ end
162
+ end
163
+ ```
164
+
85
165
  ## Contributing
86
166
 
87
167
  1. Fork it
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', "~> 3.2"
4
+ gem "mongoid", ">= 3.0", "< 4"
5
+ gem 'mongoid-rspec', '>= 1.5.1'
6
+
7
+ gem 'database_cleaner'
8
+ gem "rspec"
9
+ gem "rake"
10
+
11
+ platform :ruby do
12
+ gem 'sqlite3'
13
+ end
14
+
15
+ platform :jruby do
16
+ gem "jdbc-sqlite3"
17
+ gem "activerecord-jdbcsqlite3-adapter"
18
+ end
@@ -0,0 +1,23 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', "~> 4"
4
+
5
+ # TODO: Enable once Mongoid 4 is released
6
+ #gem "mongoid", ">= 4.0"
7
+
8
+ gem "mongoid", :github => "mongoid/mongoid"
9
+ gem 'mongoid-rspec'
10
+
11
+ gem 'database_cleaner'
12
+ gem "rspec"
13
+ gem "rake"
14
+
15
+ platform :ruby do
16
+ gem 'sqlite3'
17
+ end
18
+
19
+ platform :jruby do
20
+ gem "jdbc-sqlite3"
21
+ # See https://github.com/jruby/activerecord-jdbc-adapter/issues/253
22
+ gem "activerecord-jdbcsqlite3-adapter", :github => 'jruby/activerecord-jdbc-adapter', :branch => 'v1.3.0.beta2'
23
+ end
data/groupify.gemspec CHANGED
@@ -14,8 +14,13 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "groupify"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Groupify::VERSION
17
-
18
- gem.add_dependency "mongoid", '>= 2'
17
+
18
+ gem.add_development_dependency "mongoid", ">= 3.1"
19
+ gem.add_development_dependency "activerecord", ">= 3.2"
20
+
21
+ unless defined?(JRUBY_VERSION)
22
+ gem.add_development_dependency "sqlite3"
23
+ end
19
24
 
20
25
  gem.add_development_dependency "rake"
21
26
  gem.add_development_dependency "rspec"
@@ -0,0 +1,318 @@
1
+ require 'active_record'
2
+ require 'set'
3
+
4
+ # Groups and members
5
+ module Groupify
6
+ module ActiveRecord
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Define a scope that returns nothing.
11
+ # This is built into ActiveRecord 4, but not 3
12
+ unless self.class.respond_to? :none
13
+ def self.none
14
+ where(arel_table[:id].eq(nil).and(arel_table[:id].not_eq(nil)))
15
+ end
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def acts_as_group(opts = {})
21
+ include Groupify::ActiveRecord::Group
22
+
23
+ if (member_klass = opts.delete :default_members)
24
+ self.default_member_class = member_klass.to_s.classify.constantize
25
+ end
26
+
27
+ if (member_klasses = opts.delete :members)
28
+ member_klasses.each do |member_klass|
29
+ has_members(member_klass)
30
+ end
31
+ end
32
+ end
33
+
34
+ def acts_as_group_member(opts = {})
35
+ @group_class_name = opts[:class_name] || 'Group'
36
+ include Groupify::ActiveRecord::GroupMember
37
+ end
38
+
39
+ def acts_as_named_group_member(opts = {})
40
+ include Groupify::ActiveRecord::NamedGroupMember
41
+ end
42
+
43
+ def acts_as_group_membership(opts = {})
44
+ include Groupify::ActiveRecord::GroupMembership
45
+ end
46
+ end
47
+
48
+ # Usage:
49
+ # class Group < ActiveRecord::Base
50
+ # acts_as_group, :members => [:users]
51
+ # ...
52
+ # end
53
+ #
54
+ # group.add(member)
55
+ #
56
+ module Group
57
+ extend ActiveSupport::Concern
58
+
59
+ included do
60
+ @default_member_class = nil
61
+ @member_klasses ||= Set.new
62
+ has_many :group_memberships
63
+ end
64
+
65
+ def members
66
+ self.class.default_member_class.joins(:group_memberships).where(:group_memberships => {:member_type => self.class.default_member_class.to_s})
67
+ end
68
+
69
+ def member_classes
70
+ self.class.member_classes
71
+ end
72
+
73
+ def add(member)
74
+ member.groups << self
75
+ end
76
+
77
+ # Merge a source group into this group.
78
+ def merge!(source)
79
+ self.class.merge!(source, self)
80
+ end
81
+
82
+ module ClassMethods
83
+ def with_member(member)
84
+ #joins(:group_memberships).where(:group_memberships => {:member_id => member.id, :member_type => member.class.to_s})
85
+ member.groups
86
+ end
87
+
88
+ def default_member_class
89
+ @default_member_class ||= register(User)
90
+ end
91
+
92
+ def default_member_class=(klass)
93
+ @default_member_class = klass
94
+ end
95
+
96
+ # Returns the member classes defined for this class, as well as for the super classes
97
+ def member_classes
98
+ (@member_klasses ||= Set.new).merge(superclass.method_defined?(:member_classes) ? superclass.member_classes : [])
99
+ end
100
+
101
+ # Define which classes are members of this group
102
+ def has_members(name)
103
+ klass = name.to_s.classify.constantize
104
+ register(klass)
105
+
106
+ # Define specific members accessor, i.e. group.users
107
+ define_method name.to_s.pluralize.underscore do
108
+ klass.joins(:group_memberships).where(:group_memberships => {:group_id => self.id})
109
+ end
110
+ end
111
+
112
+ # Merge two groups. The members of the source become members of the destination, and the source is destroyed.
113
+ def merge!(source_group, destination_group)
114
+ # Ensure that all the members of the source can be members of the destination
115
+ invalid_member_classes = (source_group.member_classes - destination_group.member_classes)
116
+ invalid_member_classes.each do |klass|
117
+ if klass.joins(:group_memberships).where(:group_memberships => {:group_id => source_group.id}).count > 0
118
+ raise ArgumentError.new("#{source_group.class} has members that cannot belong to #{destination_group.class}")
119
+ end
120
+ end
121
+
122
+ source_group.transaction do
123
+ source_group.group_memberships.update_all(:group_id => destination_group.id)
124
+ source_group.destroy
125
+ end
126
+ end
127
+
128
+ protected
129
+
130
+ def register(member_klass)
131
+ (@member_klasses ||= Set.new) << member_klass
132
+ member_klass
133
+ end
134
+ end
135
+ end
136
+
137
+ # Join table that tracks which members belong to which groups
138
+ #
139
+ # Usage:
140
+ # class GroupMembership < ActiveRecord::Base
141
+ # acts_as_group_membership
142
+ # ...
143
+ # end
144
+ #
145
+ module GroupMembership
146
+ extend ActiveSupport::Concern
147
+
148
+ included do
149
+ belongs_to :member, :polymorphic => true
150
+ belongs_to :group
151
+ end
152
+
153
+ module ClassMethods
154
+ def named(group_name=nil)
155
+ if group_name.present?
156
+ where(group_name: group_name)
157
+ else
158
+ where("group_name IS NOT NULL")
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Usage:
165
+ # class User < ActiveRecord::Base
166
+ # acts_as_group_member
167
+ # ...
168
+ # end
169
+ #
170
+ # user.groups << group
171
+ #
172
+ module GroupMember
173
+ extend ActiveSupport::Concern
174
+
175
+ included do
176
+ has_many :group_memberships, :as => :member, :autosave => true
177
+ has_many :groups, :through => :group_memberships, :class_name => @group_class_name
178
+ end
179
+
180
+ def in_group?(group)
181
+ self.group_memberships.exists?(:group_id => group.id)
182
+ end
183
+
184
+ def in_any_group?(*groups)
185
+ groups.flatten.each do |group|
186
+ return true if in_group?(group)
187
+ end
188
+ return false
189
+ end
190
+
191
+ def in_all_groups?(*groups)
192
+ Set.new(groups.flatten) == Set.new(self.named_groups)
193
+ end
194
+
195
+ def shares_any_group?(other)
196
+ in_any_group?(other.groups)
197
+ end
198
+
199
+ module ClassMethods
200
+ def group_class_name; @group_class_name ||= 'Group'; end
201
+ def group_class_name=(klass); @group_class_name = klass; end
202
+
203
+ def in_group(group)
204
+ group.present? ? joins(:group_memberships).where(:group_memberships => {:group_id => group.id}) : none
205
+ end
206
+
207
+ def in_any_group(*groups)
208
+ groups.present? ? joins(:group_memberships).where(:group_memberships => {:group_id => groups.flatten.map(&:id)}) : none
209
+ end
210
+
211
+ def in_all_groups(*groups)
212
+ if groups.present?
213
+ groups = groups.flatten
214
+
215
+ joins(:group_memberships).
216
+ group(:"group_memberships.member_id").
217
+ where(:group_memberships => {:group_id => groups.map(&:id)}).
218
+ having("COUNT(group_memberships.group_id) = #{groups.count}")
219
+ else
220
+ none
221
+ end
222
+ end
223
+
224
+ def shares_any_group(other)
225
+ in_any_group(other.groups)
226
+ end
227
+
228
+ end
229
+ end
230
+
231
+ class NamedGroupCollection < Set
232
+ def initialize(member)
233
+ @member = member
234
+ super(member.group_memberships.named.pluck(:group_name).map(&:to_sym))
235
+ end
236
+
237
+ def <<(named_group)
238
+ named_group = named_group.to_sym
239
+ unless include?(named_group)
240
+ @member.group_memberships.build(:group_name => named_group)
241
+ super(named_group)
242
+ end
243
+ named_group
244
+ end
245
+ end
246
+
247
+
248
+ # Usage:
249
+ # class User < ActiveRecord::Base
250
+ # acts_as_named_group_member
251
+ # ...
252
+ # end
253
+ #
254
+ # user.named_groups << :admin
255
+ #
256
+ module NamedGroupMember
257
+ extend ActiveSupport::Concern
258
+
259
+ def named_groups
260
+ @named_groups ||= NamedGroupCollection.new(self)
261
+ end
262
+
263
+ def named_groups=(named_groups)
264
+ named_groups.each do |named_group|
265
+ self.named_groups << named_group
266
+ end
267
+ end
268
+
269
+ def in_named_group?(group)
270
+ named_groups.include?(group)
271
+ end
272
+
273
+ def in_any_named_group?(*groups)
274
+ groups.flatten.each do |group|
275
+ return true if in_named_group?(group)
276
+ end
277
+ return false
278
+ end
279
+
280
+ def in_all_named_groups?(*groups)
281
+ Set.new(groups.flatten) == Set.new(self.named_groups)
282
+ end
283
+
284
+ def shares_any_named_group?(other)
285
+ in_any_named_group?(other.named_groups.to_a)
286
+ end
287
+
288
+ module ClassMethods
289
+ def in_named_group(named_group)
290
+ named_group.present? ? joins(:group_memberships).where(:group_memberships => {:group_name => named_group}) : none
291
+ end
292
+
293
+ def in_any_named_group(*named_groups)
294
+ named_groups.present? ? joins(:group_memberships).where(:group_memberships => {:group_name => named_groups.flatten}) : none
295
+ end
296
+
297
+ def in_all_named_groups(*named_groups)
298
+ if named_groups.present?
299
+ named_groups = named_groups.flatten.map(&:to_s)
300
+
301
+ joins(:group_memberships).
302
+ group(:"group_memberships.member_id").
303
+ where(:group_memberships => {:group_name => named_groups}).
304
+ having("COUNT(group_memberships.group_name) = #{named_groups.count}")
305
+ else
306
+ none
307
+ end
308
+ end
309
+
310
+ def shares_any_named_group(other)
311
+ in_any_named_group(other.named_groups.to_a)
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ ActiveRecord::Base.send :include, Groupify::ActiveRecord