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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e6c1b6a28b9ffcb7a8fd8cdd22c7882e68b7fc33
4
- data.tar.gz: 1b828c4275ba593ff14e4813117047faf668082b
3
+ metadata.gz: 688ba7945e95997b9bd71d248d9c8d0e17da2663
4
+ data.tar.gz: 6e239d414dd7098a5d36e1517ac25827187ea55e
5
5
  SHA512:
6
- metadata.gz: abffefbf67162095c5943df53b394b1adb4bcf846e78909b37fec2e3b26db2d21e7c0bab6ae1f5888a6d280e1061590fb5968556d5d1b05255fce7bd37531226
7
- data.tar.gz: c9db57ffa507fb16e2606fa2fd1bdb245d120e18cf66148d37a014a88b578b36ee4e1d679e35e21d9da6015c633c7e50cc778b346f4e1fd086e67e40d97eb1ea
6
+ metadata.gz: a9e8494ba575bf48cf3b1c439aba5fbfd512e08a51d998c48a84ea10738cd43d409c8588e053391a5f4a8da50b683dba8bf9ae57907c262d71a40fb930d0377f
7
+ data.tar.gz: 2ee79e0d86e422250f1bc6859707ba439009eb5d8ef95e47998f87e492e983cce22bd7153703f8dc446f5688374e65e7a95bd364286ca3e0378febfe6829f14d
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  /**/*.lock
19
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
4
+ --pattern "spec/**/*_spec.rb"
data/.travis.yml CHANGED
@@ -6,8 +6,11 @@ rvm:
6
6
  - 2.1.0
7
7
  - 2.1.1
8
8
  - jruby-19mode
9
- #- rbx
9
+ - rbx-2
10
10
  gemfile:
11
- - gemfiles/activesupport3.gemfile
12
- - gemfiles/activesupport4.gemfile
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 do
11
- gem "jdbc-sqlite3"
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 [![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) [![Code Climate](https://codeclimate.com/github/dwbutler/groupify.png)](https://codeclimate.com/github/dwbutler/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 # 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
+ 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
- # => true
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
- Check if two members share any of the same groups:
146
+ ### Query for groups & members:
129
147
 
130
148
  ```ruby
131
- user1.shares_any_group?(user2)
132
- user2.shares_any_named_group?(user1)
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
- Query for groups & members:
157
+ ### Check if member belongs to any/all groups
136
158
 
137
159
  ```ruby
138
- User.in_group(group) # Find all users in this group
139
- User.in_named_group(:admin) # Find all users in this named group
140
- Group.with_member(user) # Find all groups with this user
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
- User.shares_any_group(user) # Find all users that share any groups with this user
143
- User.shares_any_named_group(user) # Find all users that share any named groups with this user
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
- Check the specs for more details.
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 is how to do it.
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
- # Implements group authorization
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env rake
2
+ require "bundler/setup"
2
3
  require "bundler/gem_tasks"
3
4
 
4
5
  require 'rspec/core/rake_task'
@@ -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 'mongoid-rspec'
30
- gem.add_development_dependency 'database_cleaner'
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(*members)
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.flatten.each do |member|
75
- member.groups << self
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
- has_many association_name, :through => :group_memberships, :source => :member, :source_type => source_type
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, :through => :group_memberships, :source => :member, :source_type => source_type
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
- has_many :group_memberships, :as => :member, :autosave => true, :dependent => :destroy
187
- has_many :groups, :through => :group_memberships, :class_name => @group_class_name
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
- self.group_memberships.exists?(:group_id => group.id)
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?(*groups)
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?(*groups)
202
- Set.new(groups.flatten) == Set.new(self.named_groups)
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
- group.present? ? joins(:group_memberships).where(:group_memberships => {:group_id => group.id}).uniq : none
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.present? ? joins(:group_memberships).where(:group_memberships => {:group_id => groups.flatten.map(&:id)}).uniq : none
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
- if groups.present?
223
- groups = groups.flatten
224
-
225
- joins(:group_memberships).
226
- group(:"group_memberships.member_id").
227
- where(:group_memberships => {:group_id => groups.map(&:id)}).
228
- having("COUNT(group_memberships.group_id) = #{groups.count}").
229
- uniq
230
- else
231
- none
232
- end
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
- super(member.group_memberships.named.pluck(:group_name).map(&:to_sym))
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
- def <<(named_group)
425
+ alias_method :concat, :merge
426
+
427
+ def include?(named_group, opts={})
249
428
  named_group = named_group.to_sym
250
- unless include?(named_group)
251
- @member.group_memberships.build(:group_name => named_group)
429
+ if opts[:as]
430
+ as(opts[:as]).include?(named_group)
431
+ else
252
432
  super(named_group)
253
433
  end
254
- named_group
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=(named_groups)
275
- named_groups.each do |named_group|
276
- self.named_groups << named_group
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?(group)
281
- named_groups.include?(group)
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?(*groups)
285
- groups.flatten.each do |group|
286
- return true if in_named_group?(group)
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?(*groups)
292
- Set.new(groups.flatten) == Set.new(self.named_groups)
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
- named_group.present? ? joins(:group_memberships).where(:group_memberships => {:group_name => named_group}).uniq : none
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.present? ? joins(:group_memberships).where(:group_memberships => {:group_name => named_groups.flatten}).uniq : none
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
- if named_groups.present?
310
- named_groups = named_groups.flatten.map(&:to_s)
311
-
312
- joins(:group_memberships).
313
- group(:"group_memberships.member_id").
314
- where(:group_memberships => {:group_name => named_groups}).
315
- having("COUNT(group_memberships.group_name) = #{named_groups.count}").
316
- uniq
317
- else
318
- none
319
- end
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)