groupify 0.5.1 → 0.6.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +1 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -4
- data/Appraisals +24 -0
- data/Gemfile +6 -9
- data/README.md +181 -25
- data/Rakefile +1 -0
- data/gemfiles/rails_3.2.gemfile +18 -0
- data/gemfiles/rails_4.0.gemfile +18 -0
- data/gemfiles/rails_4.1.gemfile +18 -0
- data/groupify.gemspec +4 -4
- data/lib/groupify/adapter/active_record.rb +317 -59
- data/lib/groupify/adapter/mongoid.rb +304 -49
- data/lib/groupify/version.rb +1 -1
- data/spec/groupify/active_record_spec.rb +460 -148
- data/spec/groupify/mongoid_spec.rb +510 -162
- data/spec/spec_helper.rb +14 -3
- metadata +15 -13
- data/gemfiles/activesupport3.gemfile +0 -18
- data/gemfiles/activesupport4.gemfile +0 -24
- data/spec/groupify/mongoid.yml +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 688ba7945e95997b9bd71d248d9c8d0e17da2663
|
4
|
+
data.tar.gz: 6e239d414dd7098a5d36e1517ac25827187ea55e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9e8494ba575bf48cf3b1c439aba5fbfd512e08a51d998c48a84ea10738cd43d409c8588e053391a5f4a8da50b683dba8bf9ae57907c262d71a40fb930d0377f
|
7
|
+
data.tar.gz: 2ee79e0d86e422250f1bc6859707ba439009eb5d8ef95e47998f87e492e983cce22bd7153703f8dc446f5688374e65e7a95bd364286ca3e0378febfe6829f14d
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
CHANGED
@@ -6,8 +6,11 @@ rvm:
|
|
6
6
|
- 2.1.0
|
7
7
|
- 2.1.1
|
8
8
|
- jruby-19mode
|
9
|
-
|
9
|
+
- rbx-2
|
10
10
|
gemfile:
|
11
|
-
- gemfiles/
|
12
|
-
- gemfiles/
|
13
|
-
|
11
|
+
- gemfiles/rails_3.2.gemfile
|
12
|
+
- gemfiles/rails_4.0.gemfile
|
13
|
+
- gemfiles/rails_4.1.gemfile
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: rbx-2
|
data/Appraisals
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
appraise "rails-3.2" do
|
2
|
+
gem 'activerecord', "~> 3.2"
|
3
|
+
gem "mongoid", ">= 3.0", "< 4"
|
4
|
+
end
|
5
|
+
|
6
|
+
appraise "rails-4.0" do
|
7
|
+
gem 'activerecord', "~> 4.0.0"
|
8
|
+
|
9
|
+
# TODO: Enable once Mongoid 4 is released
|
10
|
+
#gem "mongoid", ">= 4.0"
|
11
|
+
|
12
|
+
#gem "mongoid", github: "mongoid/mongoid", branch: "master"
|
13
|
+
gem "mongoid", "4.0.0.rc2"
|
14
|
+
end
|
15
|
+
|
16
|
+
appraise "rails-4.1" do
|
17
|
+
gem 'activerecord', "~> 4.1.0"
|
18
|
+
|
19
|
+
# TODO: Enable once Mongoid 4 is released
|
20
|
+
#gem "mongoid", ">= 4.0"
|
21
|
+
|
22
|
+
#gem "mongoid", :github => "mongoid/mongoid"
|
23
|
+
gem "mongoid", "4.0.0.rc2"
|
24
|
+
end
|
data/Gemfile
CHANGED
@@ -4,15 +4,12 @@ group :development do
|
|
4
4
|
gem 'pry'
|
5
5
|
end
|
6
6
|
|
7
|
+
group :test do
|
8
|
+
gem 'coveralls', require: false
|
9
|
+
end
|
10
|
+
|
7
11
|
# Specify your gem's dependencies in groupify.gemspec
|
8
12
|
gemspec
|
9
13
|
|
10
|
-
platform :jruby
|
11
|
-
|
12
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
13
|
-
|
14
|
-
if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
|
15
|
-
gem "json"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
14
|
+
gem "jdbc-sqlite3", platform: :jruby
|
15
|
+
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
|
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
-
# Groupify
|
1
|
+
# Groupify
|
2
|
+
[![Build Status](https://secure.travis-ci.org/dwbutler/groupify.png)](http://travis-ci.org/dwbutler/groupify) [![Coverage Status](https://coveralls.io/repos/dwbutler/groupify/badge.png?branch=master)](https://coveralls.io/r/dwbutler/groupify?branch=master) [![Code Climate](https://codeclimate.com/github/dwbutler/groupify.png)](https://codeclimate.com/github/dwbutler/groupify) [![Dependency Status](https://gemnasium.com/dwbutler/groupify.png)](https://gemnasium.com/dwbutler/groupify)
|
2
3
|
|
3
|
-
Adds group and membership functionality to Rails models.
|
4
|
+
Adds group and membership functionality to Rails models. Defines a polymorphic
|
5
|
+
relationship between a Group model and any member model. Don't need a Group
|
6
|
+
model? Use named groups instead to add members to named groups such as
|
7
|
+
`:admin` or `"Team Rocketpants"`.
|
4
8
|
|
5
9
|
The following ORMs are supported:
|
6
10
|
Mongoid 3.1 & 4.0, ActiveRecord 3.2 & 4.x
|
@@ -34,10 +38,11 @@ class CreateGroups < ActiveRecord::Migration
|
|
34
38
|
end
|
35
39
|
|
36
40
|
create_table :group_memberships do |t|
|
37
|
-
t.string :member_type
|
38
|
-
t.integer :member_id
|
39
|
-
t.integer :group_id
|
40
|
-
t.string :group_name
|
41
|
+
t.string :member_type # Necessary to make polymorphic members work
|
42
|
+
t.integer :member_id # The id of the member that belongs to this group
|
43
|
+
t.integer :group_id # The group to which the member belongs
|
44
|
+
t.string :group_name # The named group to which a member belongs (if using)
|
45
|
+
t.string :membership_type # The type of membership the member belongs with
|
41
46
|
end
|
42
47
|
|
43
48
|
add_index :group_memberships, [:member_id, :member_type]
|
@@ -100,7 +105,7 @@ end
|
|
100
105
|
|
101
106
|
## Basic Usage
|
102
107
|
|
103
|
-
Create groups and add members
|
108
|
+
### Create groups and add members
|
104
109
|
|
105
110
|
```ruby
|
106
111
|
group = Group.new
|
@@ -117,44 +122,129 @@ user.in_group?(group)
|
|
117
122
|
group.add(user, widget, task)
|
118
123
|
```
|
119
124
|
|
120
|
-
Add to named groups
|
125
|
+
### Add to named groups
|
121
126
|
|
122
127
|
```ruby
|
123
128
|
user.named_groups << :admin
|
124
|
-
user.in_named_group?(:admin)
|
125
|
-
|
129
|
+
user.in_named_group?(:admin) # => true
|
130
|
+
```
|
131
|
+
|
132
|
+
### Remove from groups
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
users.groups.destroy(group) # Destroys this user's group membership for this group
|
136
|
+
group.users.delete(user) # Deletes this group's group membership for this user
|
137
|
+
```
|
138
|
+
|
139
|
+
### Check if two members share any of the same groups:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
user1.shares_any_group?(user2) # Returns true if user1 and user2 are in any of the same groups
|
143
|
+
user2.shares_any_named_group?(user1) # Also works for named groups
|
126
144
|
```
|
127
145
|
|
128
|
-
|
146
|
+
### Query for groups & members:
|
129
147
|
|
130
148
|
```ruby
|
131
|
-
|
132
|
-
|
149
|
+
User.in_group(group) # Find all users in this group
|
150
|
+
User.in_named_group(:admin) # Find all users in this named group
|
151
|
+
Group.with_member(user) # Find all groups with this user
|
152
|
+
|
153
|
+
User.shares_any_group(user) # Find all users that share any groups with this user
|
154
|
+
User.shares_any_named_group(user) # Find all users that share any named groups with this user
|
133
155
|
```
|
134
156
|
|
135
|
-
|
157
|
+
### Check if member belongs to any/all groups
|
136
158
|
|
137
159
|
```ruby
|
138
|
-
User.
|
139
|
-
User.
|
140
|
-
|
160
|
+
User.in_any_group(group1, group2) # Find users that belong to any of these groups
|
161
|
+
User.in_all_groups(group1, group2) # Find users that belong to all of these groups
|
162
|
+
Widget.in_only_groups(group2, group3) # Find widgets that belong to only these groups
|
141
163
|
|
142
|
-
|
143
|
-
|
164
|
+
widget.in_any_named_group?(:foo, :bar) # Check if widget belongs to any of these named groups
|
165
|
+
user.in_all_named_groups?(:manager, :poster) # Check if user belongs to all of these named groups
|
166
|
+
user.in_only_named_groups?(:employee, :worker) # Check if user belongs to only these named groups
|
144
167
|
```
|
145
168
|
|
146
|
-
Merge one group into another:
|
169
|
+
### Merge one group into another:
|
147
170
|
|
148
171
|
```ruby
|
149
172
|
# Moves the members of source into destination, and destroys source
|
150
173
|
destination_group.merge!(source_group)
|
151
174
|
```
|
152
175
|
|
153
|
-
|
176
|
+
## Membership Types
|
177
|
+
|
178
|
+
Membership types allow a member to belong to a group in a more specific way. For example,
|
179
|
+
you can add a user to a group with membership type of "manager" to specify that this
|
180
|
+
user has the "manager role" on that group.
|
181
|
+
|
182
|
+
This can be used to implement role-based authorization combined with group authorization,
|
183
|
+
which could be used to mass-assign roles to groups of resources.
|
184
|
+
|
185
|
+
It could also be used to add users and resources to the same "sub-group" or "project"
|
186
|
+
within a larger group (say, an organization).
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
# Add user to group as a specific membership type
|
190
|
+
group.add(user, as: 'manager')
|
191
|
+
|
192
|
+
# Works with named groups too
|
193
|
+
user.named_groups.add user, as: 'manager'
|
194
|
+
|
195
|
+
# Query for the groups that a user belongs to with a certain role
|
196
|
+
user.groups.as(:manager)
|
197
|
+
user.named_groups.as('manager')
|
198
|
+
Group.with_member(user).as('manager')
|
199
|
+
|
200
|
+
# Remove a member's membership type from a group
|
201
|
+
group.users.delete(user, as: 'manager') # Deletes this group's 'manager' group membership for this user
|
202
|
+
user.groups.destroy(group, as: 'employee') # Destroys this user's 'employee' group membership for this group
|
203
|
+
user.groups.destroy(group) # Destroys any membership types this user had in this group
|
204
|
+
|
205
|
+
# Find all members that have a certain membership type in a group
|
206
|
+
User.in_group(group).as(:manager)
|
207
|
+
|
208
|
+
# Find all members of a certain membership type regardless of group
|
209
|
+
User.as(:manager) # Find users that are managers, we don't care what group
|
210
|
+
|
211
|
+
# Check if a member belongs to any/all groups with a certain membership type
|
212
|
+
user.in_all_groups?(group1, group2, as: 'manager')
|
213
|
+
|
214
|
+
# Find all members that share the same group with the same membership type
|
215
|
+
Widget.shares_any_group(user).as("Moon Launch Project")
|
216
|
+
|
217
|
+
# Check is one member belongs to the same group as another member with a certain membership type
|
218
|
+
user.shares_any_group?(widget, as: 'employee')
|
219
|
+
```
|
220
|
+
|
221
|
+
Note that adding a member to a group with a specific membership type will automatically
|
222
|
+
add them to that group without a specific membership type. This way you can still query
|
223
|
+
`groups` and find the member in that group. If you then remove that specific membership
|
224
|
+
type, they still remain in the group without a specific membership type.
|
225
|
+
|
226
|
+
Removing a member from a group will bulk remove any specific membership types as well.
|
227
|
+
|
228
|
+
```
|
229
|
+
group.add(manager, as: 'manager')
|
230
|
+
manager.groups.include?(group) # => true
|
231
|
+
|
232
|
+
manager.groups.delete(group, as: 'manager')
|
233
|
+
manager.groups.include?(group) # => true
|
234
|
+
|
235
|
+
group.add(employee, as: 'employee')
|
236
|
+
employee.groups.delete(group)
|
237
|
+
employee.in_group?(group) # => false
|
238
|
+
employee.in_group?(group, as: 'employee') # => false
|
239
|
+
```
|
240
|
+
|
241
|
+
## But wait, there's more!
|
242
|
+
|
243
|
+
Check the specs for a complete list of methods and scopes provided by Groupify.
|
154
244
|
|
155
245
|
## Using for Authorization
|
156
246
|
Groupify was originally created to help implement user authorization, although it can be used
|
157
|
-
generically for much more than that. Here
|
247
|
+
generically for much more than that. Here are some examples of how to do it.
|
158
248
|
|
159
249
|
### With CanCan
|
160
250
|
|
@@ -163,9 +253,8 @@ class Ability
|
|
163
253
|
include CanCan::Ability
|
164
254
|
|
165
255
|
def initialize(user)
|
166
|
-
|
167
|
-
#
|
168
|
-
# Users can only manage assignment which belong to the same group
|
256
|
+
# Implements group-based authorization
|
257
|
+
# Users can only manage assignment which belong to the same group.
|
169
258
|
can [:manage], Assignment, Assignment.shares_any_group(user) do |assignment|
|
170
259
|
assignment.shares_any_group?(user)
|
171
260
|
end
|
@@ -173,6 +262,73 @@ class Ability
|
|
173
262
|
end
|
174
263
|
```
|
175
264
|
|
265
|
+
### With Authority
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
# Whatever class represents a logged-in user in your app
|
269
|
+
class User
|
270
|
+
acts_as_named_group_member
|
271
|
+
include Authority::UserAbilities
|
272
|
+
end
|
273
|
+
|
274
|
+
class Widget
|
275
|
+
acts_as_named_group_member
|
276
|
+
include Authority::Abilities
|
277
|
+
end
|
278
|
+
|
279
|
+
class WidgetAuthorizer < ApplicationAuthorizer
|
280
|
+
# Implements group-based authorization using named groups.
|
281
|
+
# Users can only see widgets which belong to the same named group.
|
282
|
+
def readable_by?(user)
|
283
|
+
user.shares_any_named_group?(resource)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Implements combined role-based and group-based authorization.
|
287
|
+
# Widgets can only be updated by users that are employees of the same named group.
|
288
|
+
def updateable_by?(user)
|
289
|
+
user.shares_any_named_group?(resource, as: :employee)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Widgets can only be deleted by users that are managers of the same named group.
|
293
|
+
def deletable_by?(user)
|
294
|
+
user.shares_any_named_group?(resource, as: :manager)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
user = User.create!
|
299
|
+
user.named_groups.add(:team1, as: :employee)
|
300
|
+
|
301
|
+
widget = Widget.create!
|
302
|
+
widget.named_groups << :team1
|
303
|
+
|
304
|
+
widget.readable_by?(user) # => true
|
305
|
+
user.can_update?(widget) # => true
|
306
|
+
user.can_delete?(widget) # => false
|
307
|
+
```
|
308
|
+
|
309
|
+
### With Pundit
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
class PostPolicy < Struct.new(:user, :post)
|
313
|
+
# User can only update a published post if they are admin of the same group.
|
314
|
+
def update?
|
315
|
+
user.shares_any_group?(post, as: :admin) || !post.published?
|
316
|
+
end
|
317
|
+
|
318
|
+
class Scope < Struct.new(:user, :scope)
|
319
|
+
def resolve
|
320
|
+
if user.admin?
|
321
|
+
# An admin can see all the posts in the group(s) they are admin for
|
322
|
+
scope.shares_any_group(user).as(:admin)
|
323
|
+
else
|
324
|
+
# Normal users can only see published posts in the same group(s).
|
325
|
+
scope.shares_any_group(user).where(published: true)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
176
332
|
## Contributing
|
177
333
|
|
178
334
|
1. Fork it
|
data/Rakefile
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "jdbc-sqlite3", :platform => :jruby
|
6
|
+
gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
|
7
|
+
gem "activerecord", "~> 3.2"
|
8
|
+
gem "mongoid", ">= 3.0", "< 4"
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "pry"
|
12
|
+
end
|
13
|
+
|
14
|
+
group :test do
|
15
|
+
gem "coveralls", :require => false
|
16
|
+
end
|
17
|
+
|
18
|
+
gemspec :path => "../"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "jdbc-sqlite3", :platform => :jruby
|
6
|
+
gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
|
7
|
+
gem "activerecord", "~> 4.0.0"
|
8
|
+
gem "mongoid", "4.0.0.rc2"
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "pry"
|
12
|
+
end
|
13
|
+
|
14
|
+
group :test do
|
15
|
+
gem "coveralls", :require => false
|
16
|
+
end
|
17
|
+
|
18
|
+
gemspec :path => "../"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "jdbc-sqlite3", :platform => :jruby
|
6
|
+
gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
|
7
|
+
gem "activerecord", "~> 4.1.0"
|
8
|
+
gem "mongoid", "4.0.0.rc2"
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "pry"
|
12
|
+
end
|
13
|
+
|
14
|
+
group :test do
|
15
|
+
gem "coveralls", :require => false
|
16
|
+
end
|
17
|
+
|
18
|
+
gemspec :path => "../"
|
data/groupify.gemspec
CHANGED
@@ -24,8 +24,8 @@ Gem::Specification.new do |gem|
|
|
24
24
|
end
|
25
25
|
|
26
26
|
gem.add_development_dependency "rake"
|
27
|
-
gem.add_development_dependency "rspec"
|
28
|
-
|
29
|
-
gem.add_development_dependency
|
30
|
-
gem.add_development_dependency
|
27
|
+
gem.add_development_dependency "rspec", ">= 3"
|
28
|
+
|
29
|
+
gem.add_development_dependency "database_cleaner"
|
30
|
+
gem.add_development_dependency "appraisal"
|
31
31
|
end
|
@@ -68,11 +68,20 @@ module Groupify
|
|
68
68
|
self.class.member_classes
|
69
69
|
end
|
70
70
|
|
71
|
-
def add(*
|
71
|
+
def add(*args)
|
72
|
+
opts = args.extract_options!
|
73
|
+
membership_type = opts[:as]
|
74
|
+
members = args.flatten
|
75
|
+
return unless members.present?
|
76
|
+
|
72
77
|
clear_association_cache
|
73
78
|
|
74
|
-
members.
|
75
|
-
member.
|
79
|
+
members.each do |member|
|
80
|
+
member.group_memberships.where(group_id: self.id).first_or_create!
|
81
|
+
if membership_type
|
82
|
+
member.group_memberships.where(group_id: self, membership_type: membership_type).first_or_create!
|
83
|
+
end
|
84
|
+
member.clear_association_cache
|
76
85
|
end
|
77
86
|
end
|
78
87
|
|
@@ -132,13 +141,62 @@ module Groupify
|
|
132
141
|
member_klass
|
133
142
|
end
|
134
143
|
|
144
|
+
module MemberAssociationExtensions
|
145
|
+
def as(membership_type)
|
146
|
+
where(group_memberships: {membership_type: membership_type})
|
147
|
+
end
|
148
|
+
|
149
|
+
def delete(*args)
|
150
|
+
opts = args.extract_options!
|
151
|
+
members = args
|
152
|
+
|
153
|
+
if opts[:as]
|
154
|
+
proxy_association.owner.group_memberships.
|
155
|
+
where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]).
|
156
|
+
as(opts[:as]).
|
157
|
+
delete_all
|
158
|
+
else
|
159
|
+
super(*members)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def destroy(*args)
|
164
|
+
opts = args.extract_options!
|
165
|
+
members = args
|
166
|
+
|
167
|
+
if opts[:as]
|
168
|
+
proxy_association.owner.group_memberships.
|
169
|
+
where(member_id: members.map(&:id), member_type: proxy_association.reflection.options[:source_type]).
|
170
|
+
as(opts[:as]).
|
171
|
+
destroy_all
|
172
|
+
else
|
173
|
+
super(*members)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
135
178
|
def associate_member_class(member_klass)
|
136
179
|
association_name = member_klass.name.to_s.pluralize.underscore.to_sym
|
137
180
|
source_type = member_klass.base_class
|
138
|
-
|
181
|
+
|
182
|
+
has_many association_name, through: :group_memberships, source: :member, source_type: source_type, extend: MemberAssociationExtensions
|
183
|
+
override_member_accessor(association_name)
|
139
184
|
|
140
185
|
if member_klass == default_member_class
|
141
|
-
has_many :members, :
|
186
|
+
has_many :members, through: :group_memberships, source: :member, source_type: source_type, extend: MemberAssociationExtensions
|
187
|
+
override_member_accessor(:members)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def override_member_accessor(association_name)
|
192
|
+
define_method(association_name) do |*args|
|
193
|
+
opts = args.extract_options!
|
194
|
+
membership_type = opts[:as]
|
195
|
+
if membership_type.present?
|
196
|
+
super().as(membership_type)
|
197
|
+
else
|
198
|
+
super()
|
199
|
+
end
|
142
200
|
end
|
143
201
|
end
|
144
202
|
end
|
@@ -156,10 +214,24 @@ module Groupify
|
|
156
214
|
extend ActiveSupport::Concern
|
157
215
|
|
158
216
|
included do
|
217
|
+
attr_accessible(:member, :group, :group_name, :membership_type, :as) if ActiveSupport::VERSION::MAJOR < 4
|
218
|
+
|
159
219
|
belongs_to :member, :polymorphic => true
|
160
220
|
belongs_to :group
|
161
221
|
end
|
162
222
|
|
223
|
+
def membership_type=(membership_type)
|
224
|
+
self[:membership_type] = membership_type.to_s if membership_type.present?
|
225
|
+
end
|
226
|
+
|
227
|
+
def as=(membership_type)
|
228
|
+
self.membership_type = membership_type
|
229
|
+
end
|
230
|
+
|
231
|
+
def as
|
232
|
+
membership_type
|
233
|
+
end
|
234
|
+
|
163
235
|
module ClassMethods
|
164
236
|
def named(group_name=nil)
|
165
237
|
if group_name.present?
|
@@ -168,6 +240,10 @@ module Groupify
|
|
168
240
|
where("group_name IS NOT NULL")
|
169
241
|
end
|
170
242
|
end
|
243
|
+
|
244
|
+
def as(membership_type)
|
245
|
+
where(membership_type: membership_type)
|
246
|
+
end
|
171
247
|
end
|
172
248
|
end
|
173
249
|
|
@@ -183,53 +259,118 @@ module Groupify
|
|
183
259
|
extend ActiveSupport::Concern
|
184
260
|
|
185
261
|
included do
|
186
|
-
|
187
|
-
|
262
|
+
unless respond_to?(:group_memberships)
|
263
|
+
has_many :group_memberships, :as => :member, :autosave => true, :dependent => :destroy
|
264
|
+
end
|
265
|
+
|
266
|
+
has_many :groups, :through => :group_memberships, :class_name => @group_class_name do
|
267
|
+
def as(membership_type)
|
268
|
+
return self unless membership_type
|
269
|
+
where(group_memberships: {membership_type: membership_type})
|
270
|
+
end
|
271
|
+
|
272
|
+
def delete(*args)
|
273
|
+
opts = args.extract_options!
|
274
|
+
groups = args.flatten
|
275
|
+
|
276
|
+
if opts[:as]
|
277
|
+
proxy_association.owner.group_memberships.where(group_id: groups.map(&:id)).as(opts[:as]).delete_all
|
278
|
+
else
|
279
|
+
super(*groups)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def destroy(*args)
|
284
|
+
opts = args.extract_options!
|
285
|
+
groups = args.flatten
|
286
|
+
|
287
|
+
if opts[:as]
|
288
|
+
proxy_association.owner.group_memberships.where(group_id: groups.map(&:id)).as(opts[:as]).destroy_all
|
289
|
+
else
|
290
|
+
super(*groups)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
188
294
|
end
|
189
295
|
|
190
|
-
def in_group?(group)
|
191
|
-
|
296
|
+
def in_group?(group, opts={})
|
297
|
+
criteria = {group_id: group.id}
|
298
|
+
|
299
|
+
if opts[:as]
|
300
|
+
criteria.merge!(membership_type: opts[:as])
|
301
|
+
end
|
302
|
+
|
303
|
+
group_memberships.exists?(criteria)
|
192
304
|
end
|
193
305
|
|
194
|
-
def in_any_group?(*
|
306
|
+
def in_any_group?(*args)
|
307
|
+
opts = args.extract_options!
|
308
|
+
groups = args
|
309
|
+
|
195
310
|
groups.flatten.each do |group|
|
196
|
-
return true if in_group?(group)
|
311
|
+
return true if in_group?(group, opts)
|
197
312
|
end
|
198
313
|
return false
|
199
314
|
end
|
200
315
|
|
201
|
-
def in_all_groups?(*
|
202
|
-
|
316
|
+
def in_all_groups?(*args)
|
317
|
+
opts = args.extract_options!
|
318
|
+
groups = args.flatten
|
319
|
+
|
320
|
+
groups.to_set.subset? self.groups.as(opts[:as]).to_set
|
321
|
+
end
|
322
|
+
|
323
|
+
def in_only_groups?(*args)
|
324
|
+
opts = args.extract_options!
|
325
|
+
groups = args.flatten
|
326
|
+
|
327
|
+
groups.to_set == self.groups.as(opts[:as]).to_set
|
203
328
|
end
|
204
329
|
|
205
|
-
def shares_any_group?(other)
|
206
|
-
in_any_group?(other.groups)
|
330
|
+
def shares_any_group?(other, opts={})
|
331
|
+
in_any_group?(other.groups, opts)
|
207
332
|
end
|
208
333
|
|
209
334
|
module ClassMethods
|
210
335
|
def group_class_name; @group_class_name ||= 'Group'; end
|
211
336
|
def group_class_name=(klass); @group_class_name = klass; end
|
337
|
+
|
338
|
+
def as(membership_type)
|
339
|
+
joins(:group_memberships).where(group_memberships: { membership_type: membership_type })
|
340
|
+
end
|
212
341
|
|
213
342
|
def in_group(group)
|
214
|
-
|
343
|
+
return none unless group.present?
|
344
|
+
|
345
|
+
joins(:group_memberships).where(group_memberships: { group_id: group.id }).uniq
|
215
346
|
end
|
216
347
|
|
217
348
|
def in_any_group(*groups)
|
218
|
-
groups
|
349
|
+
groups = groups.flatten
|
350
|
+
return none unless groups.present?
|
351
|
+
|
352
|
+
joins(:group_memberships).where(group_memberships: { group_id: groups.map(&:id) }).uniq
|
219
353
|
end
|
220
354
|
|
221
355
|
def in_all_groups(*groups)
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
356
|
+
groups = groups.flatten
|
357
|
+
return none unless groups.present?
|
358
|
+
|
359
|
+
joins(:group_memberships).
|
360
|
+
group(:"group_memberships.member_id").
|
361
|
+
where(:group_memberships => {:group_id => groups.map(&:id)}).
|
362
|
+
having("COUNT(group_memberships.group_id) = #{groups.count}").
|
363
|
+
uniq
|
364
|
+
end
|
365
|
+
|
366
|
+
def in_only_groups(*groups)
|
367
|
+
groups = groups.flatten
|
368
|
+
return none unless groups.present?
|
369
|
+
|
370
|
+
joins(:group_memberships).
|
371
|
+
group(:"group_memberships.member_id").
|
372
|
+
having("COUNT(DISTINCT group_memberships.group_id) = #{groups.count}").
|
373
|
+
uniq
|
233
374
|
end
|
234
375
|
|
235
376
|
def shares_any_group(other)
|
@@ -242,16 +383,101 @@ module Groupify
|
|
242
383
|
class NamedGroupCollection < Set
|
243
384
|
def initialize(member)
|
244
385
|
@member = member
|
245
|
-
|
386
|
+
@named_group_memberships = member.group_memberships.named
|
387
|
+
@group_names = @named_group_memberships.pluck(:group_name).map(&:to_sym)
|
388
|
+
super(@group_names)
|
389
|
+
end
|
390
|
+
|
391
|
+
def add(named_group, opts={})
|
392
|
+
named_group = named_group.to_sym
|
393
|
+
membership_type = opts[:as]
|
394
|
+
|
395
|
+
if @member.new_record?
|
396
|
+
@member.group_memberships.build(group_name: named_group, membership_type: nil)
|
397
|
+
else
|
398
|
+
@member.transaction do
|
399
|
+
@member.group_memberships.where(group_name: named_group, membership_type: nil).first_or_create!
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
if membership_type
|
404
|
+
if @member.new_record?
|
405
|
+
@member.group_memberships.build(group_name: named_group, membership_type: membership_type)
|
406
|
+
else
|
407
|
+
@member.group_memberships.where(group_name: named_group, membership_type: membership_type).first_or_create!
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
super(named_group)
|
412
|
+
end
|
413
|
+
|
414
|
+
alias_method :push, :add
|
415
|
+
alias_method :<<, :add
|
416
|
+
|
417
|
+
def merge(*args)
|
418
|
+
opts = args.extract_options!
|
419
|
+
named_groups = args.flatten
|
420
|
+
named_groups.each do |named_group|
|
421
|
+
add(named_group, opts)
|
422
|
+
end
|
246
423
|
end
|
247
424
|
|
248
|
-
|
425
|
+
alias_method :concat, :merge
|
426
|
+
|
427
|
+
def include?(named_group, opts={})
|
249
428
|
named_group = named_group.to_sym
|
250
|
-
|
251
|
-
|
429
|
+
if opts[:as]
|
430
|
+
as(opts[:as]).include?(named_group)
|
431
|
+
else
|
252
432
|
super(named_group)
|
253
433
|
end
|
254
|
-
|
434
|
+
end
|
435
|
+
|
436
|
+
def delete(*args)
|
437
|
+
opts = args.extract_options!
|
438
|
+
named_groups = args.flatten.compact
|
439
|
+
|
440
|
+
remove(named_groups, :delete_all, opts)
|
441
|
+
end
|
442
|
+
|
443
|
+
def destroy(*args)
|
444
|
+
opts = args.extract_options!
|
445
|
+
named_groups = args.flatten.compact
|
446
|
+
|
447
|
+
remove(named_groups, :destroy_all, opts)
|
448
|
+
end
|
449
|
+
|
450
|
+
def clear
|
451
|
+
@named_group_memberships.delete_all
|
452
|
+
super
|
453
|
+
end
|
454
|
+
|
455
|
+
alias_method :delete_all, :clear
|
456
|
+
alias_method :destroy_all, :clear
|
457
|
+
|
458
|
+
# Criteria to filter by membership type
|
459
|
+
def as(membership_type)
|
460
|
+
@named_group_memberships.as(membership_type).pluck(:group_name).map(&:to_sym)
|
461
|
+
end
|
462
|
+
|
463
|
+
protected
|
464
|
+
|
465
|
+
def remove(named_groups, method, opts)
|
466
|
+
if named_groups.present?
|
467
|
+
scope = @named_group_memberships.where(group_name: named_groups)
|
468
|
+
|
469
|
+
if opts[:as]
|
470
|
+
scope = scope.where(membership_type: opts[:as])
|
471
|
+
end
|
472
|
+
|
473
|
+
scope.send(method)
|
474
|
+
|
475
|
+
unless opts[:as]
|
476
|
+
named_groups.each do |named_group|
|
477
|
+
@hash.delete(named_group)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
255
481
|
end
|
256
482
|
end
|
257
483
|
|
@@ -267,56 +493,88 @@ module Groupify
|
|
267
493
|
module NamedGroupMember
|
268
494
|
extend ActiveSupport::Concern
|
269
495
|
|
496
|
+
included do
|
497
|
+
unless respond_to?(:group_memberships)
|
498
|
+
has_many :group_memberships, :as => :member, :autosave => true, :dependent => :destroy
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
270
502
|
def named_groups
|
271
503
|
@named_groups ||= NamedGroupCollection.new(self)
|
272
504
|
end
|
273
505
|
|
274
|
-
def named_groups=(
|
275
|
-
|
276
|
-
self.named_groups <<
|
506
|
+
def named_groups=(groups)
|
507
|
+
groups.each do |group|
|
508
|
+
self.named_groups << group
|
277
509
|
end
|
278
510
|
end
|
279
511
|
|
280
|
-
def in_named_group?(
|
281
|
-
named_groups.include?(
|
512
|
+
def in_named_group?(named_group, opts={})
|
513
|
+
named_groups.include?(named_group, opts)
|
282
514
|
end
|
283
515
|
|
284
|
-
def in_any_named_group?(*
|
285
|
-
|
286
|
-
|
516
|
+
def in_any_named_group?(*args)
|
517
|
+
opts = args.extract_options!
|
518
|
+
named_groups = args.flatten
|
519
|
+
named_groups.each do |named_group|
|
520
|
+
return true if in_named_group?(named_group, opts)
|
287
521
|
end
|
288
522
|
return false
|
289
523
|
end
|
290
524
|
|
291
|
-
def in_all_named_groups?(*
|
292
|
-
|
525
|
+
def in_all_named_groups?(*args)
|
526
|
+
opts = args.extract_options!
|
527
|
+
named_groups = args.flatten.to_set
|
528
|
+
named_groups.subset? self.named_groups.as(opts[:as]).to_set
|
529
|
+
end
|
530
|
+
|
531
|
+
def in_only_named_groups?(*args)
|
532
|
+
opts = args.extract_options!
|
533
|
+
named_groups = args.flatten.to_set
|
534
|
+
named_groups == self.named_groups.as(opts[:as]).to_set
|
293
535
|
end
|
294
536
|
|
295
|
-
def shares_any_named_group?(other)
|
296
|
-
in_any_named_group?(other.named_groups.to_a)
|
537
|
+
def shares_any_named_group?(other, opts={})
|
538
|
+
in_any_named_group?(other.named_groups.to_a, opts)
|
297
539
|
end
|
298
540
|
|
299
541
|
module ClassMethods
|
542
|
+
def as(membership_type)
|
543
|
+
joins(:group_memberships).where(group_memberships: {membership_type: membership_type})
|
544
|
+
end
|
545
|
+
|
300
546
|
def in_named_group(named_group)
|
301
|
-
|
547
|
+
return none unless named_group.present?
|
548
|
+
|
549
|
+
joins(:group_memberships).where(group_memberships: {group_name: named_group}).uniq
|
302
550
|
end
|
303
551
|
|
304
552
|
def in_any_named_group(*named_groups)
|
305
|
-
named_groups.
|
553
|
+
named_groups.flatten!
|
554
|
+
return none unless named_groups.present?
|
555
|
+
|
556
|
+
joins(:group_memberships).where(group_memberships: {group_name: named_groups.flatten}).uniq
|
306
557
|
end
|
307
|
-
|
558
|
+
|
308
559
|
def in_all_named_groups(*named_groups)
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
560
|
+
named_groups.flatten!
|
561
|
+
return none unless named_groups.present?
|
562
|
+
|
563
|
+
joins(:group_memberships).
|
564
|
+
group(:"group_memberships.member_id").
|
565
|
+
where(:group_memberships => {:group_name => named_groups}).
|
566
|
+
having("COUNT(DISTINCT group_memberships.group_name) = #{named_groups.count}").
|
567
|
+
uniq
|
568
|
+
end
|
569
|
+
|
570
|
+
def in_only_named_groups(*named_groups)
|
571
|
+
named_groups.flatten!
|
572
|
+
return none unless named_groups.present?
|
573
|
+
|
574
|
+
joins(:group_memberships).
|
575
|
+
group("group_memberships.member_id").
|
576
|
+
having("COUNT(DISTINCT group_memberships.group_name) = #{named_groups.count}").
|
577
|
+
uniq
|
320
578
|
end
|
321
579
|
|
322
580
|
def shares_any_named_group(other)
|