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.
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