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