groupify 0.5.1 → 0.6.0.rc1
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.
- 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
|
+
[](http://travis-ci.org/dwbutler/groupify) [](https://coveralls.io/r/dwbutler/groupify?branch=master) [](https://codeclimate.com/github/dwbutler/groupify) [](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)
|