{"id":13631765,"url":"https://github.com/dwbutler/groupify","last_synced_at":"2025-04-04T10:06:19.929Z","repository":{"id":4409027,"uuid":"5546502","full_name":"dwbutler/groupify","owner":"dwbutler","description":"Add group and membership functionality to your Rails models","archived":false,"fork":false,"pushed_at":"2018-12-21T13:06:37.000Z","size":305,"stargazers_count":194,"open_issues_count":26,"forks_count":44,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-03-28T09:05:48.236Z","etag":null,"topics":["activerecord","authorization","group-membership","groups","mongoid","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dwbutler.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-08-24T22:34:36.000Z","updated_at":"2024-04-09T05:52:56.000Z","dependencies_parsed_at":"2022-09-21T11:11:29.293Z","dependency_job_id":null,"html_url":"https://github.com/dwbutler/groupify","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwbutler%2Fgroupify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwbutler%2Fgroupify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwbutler%2Fgroupify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwbutler%2Fgroupify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwbutler","download_url":"https://codeload.github.com/dwbutler/groupify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247157046,"owners_count":20893202,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","authorization","group-membership","groups","mongoid","ruby"],"created_at":"2024-08-01T22:02:37.399Z","updated_at":"2025-04-04T10:06:19.905Z","avatar_url":"https://github.com/dwbutler.png","language":"Ruby","readme":"# Groupify\n[![Build Status](https://travis-ci.org/dwbutler/groupify.svg?branch=master)](https://travis-ci.org/dwbutler/groupify) [![Coverage Status](https://coveralls.io/repos/dwbutler/groupify/badge.svg?branch=master\u0026service=github)](https://coveralls.io/github/dwbutler/groupify?branch=master) [![Code Climate](https://codeclimate.com/github/dwbutler/groupify/badges/gpa.svg)](https://codeclimate.com/github/dwbutler/groupify) [![Inline docs](http://inch-ci.org/github/dwbutler/groupify.svg?branch=master)](http://inch-ci.org/github/dwbutler/groupify)\n\nAdds group and membership functionality to Rails models. Defines a polymorphic\nrelationship between a Group model and any member model. Don't need a Group\nmodel? Use named groups instead to add members to named groups such as\n`:admin` or `\"Team Rocketpants\"`.\n\n## Compatibility\n\nThe following ORMs are supported:\n * ActiveRecord 4.x, 5.x\n * Mongoid 4.x, 5.x, 6.x\n\nThe following Rubies are supported:\n * MRI Ruby 2.2, 2.3, 2.4\n * JRuby 9000\n\nThe following databases are supported:\n * MySQL\n * PostgreSQL\n * SQLite\n * MongoDB\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'groupify'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install groupify\n\n### Setup\n\n#### Active Record\n\nExecute:\n\n    $ rails generate groupify:active_record:install\n\nThis will generate an initializer, `Group` model, `GroupMembership` model, and migrations.\n\nModify the models and migrations as needed, then run the migration:\n\n    $ rake db:migrate\n\nSet up your member models:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  groupify :group_member\n  groupify :named_group_member\nend\n\nclass Assignment \u003c ActiveRecord::Base\n  groupify :group_member\nend\n```\n\n#### Mongoid\n\nExecute:\n\n    $ rails generate groupify:mongoid:install\n\nSet up your member models:\n\n```ruby\nclass User\n  include Mongoid::Document\n  \n  groupify :group_member\n  groupify :named_group_member\nend\n```\n\n#### Advanced Configuration\n\n##### Groupify Model Names\n\nThe default model names for groups and group memberships are configurable. Add the following\nconfiguration in `config/initializers/groupify.rb` to change the model names for all classes:\n\n```ruby\nGroupify.configure do |config|\n  config.group_class_name = 'MyCustomGroup'\n  # ActiveRecord only\n  config.group_membership_class_name = 'MyCustomGroupMembership'\nend\n```\n\nThe group name can also be set on a model-by-model basis for each group member by passing\nthe `group_class_name` option:\n\n```ruby\nclass Member \u003c ActiveRecord::Base\n  groupify :group_member, group_class_name: 'MyOtherCustomGroup'\nend\n```\n\nNote that each member model can only belong to a single type of group (or child classes\nof that group).\n\n##### Member Associations on Group\n\nYour group class can be configured to create associations for each expected member type.\nFor example, let's say that your group class will have users and assignments as members.\nThe following configuration adds `users` and `assignments` associations on the group model:\n\n```ruby\nclass Group \u003c ActiveRecord::Base\n  groupify :group, members: [:users, :assignments], default_members: :users\nend\n```\n\nThe `default_members` option sets the model type when accessing the `members` association.\nIn the example above, `group.members` would return the users who are members of this group.\n\nIf you are using single table inheritance, child classes inherit the member associations\nof the parent. If your child class needs to add more members, use the `has_members` method.\n\nExample:\n\n```ruby\nclass Organization \u003c Group\n  has_members :offices, :equipment\nend\n```\n\nMongoid works the same way by creating Mongoid relations.\n\n## Usage\n\n### Create groups and add members\n\n```ruby\ngroup = Group.new\nuser = User.new\n\nuser.groups \u003c\u003c group\n# or\ngroup.add user\n\nuser.in_group?(group)\n# =\u003e true\n\n# Add multiple members at once\ngroup.add(user, widget, task)\n```\n\n### Remove from groups\n\n```ruby\nusers.groups.destroy(group)          # Destroys this user's group membership for this group\ngroup.users.delete(user)             # Deletes this group's group membership for this user\n```\n\n### Named groups\n\n```ruby\nuser.named_groups \u003c\u003c :admin\nuser.in_named_group?(:admin)        # =\u003e true\nuser.named_groups.destroy(:admin)\n```\n\n### Check if two members share any of the same groups:\n\n```ruby\nuser1.shares_any_group?(user2)          # Returns true if user1 and user2 are in any of the same groups\nuser2.shares_any_named_group?(user1)    # Also works for named groups\n```\n\n### Query for groups \u0026 members:\n\n```ruby\nUser.in_group(group)                # Find all users in this group\nUser.in_named_group(:admin)         # Find all users in this named group\nGroup.with_member(user)             # Find all groups with this user\n\nUser.shares_any_group(user)         # Find all users that share any groups with this user\nUser.shares_any_named_group(user)   # Find all users that share any named groups with this user\n```\n\n### Check if member belongs to any/all groups\n\n```ruby\nUser.in_any_group(group1, group2)               # Find users that belong to any of these groups\nUser.in_all_groups(group1, group2)              # Find users that belong to all of these groups\nWidget.in_only_groups(group2, group3)           # Find widgets that belong to only these groups\n\nwidget.in_any_named_group?(:foo, :bar)          # Check if widget belongs to any of these named groups\nuser.in_all_named_groups?(:manager, :poster)    # Check if user belongs to all of these named groups\nuser.in_only_named_groups?(:employee, :worker)  # Check if user belongs to only these named groups\n```\n\n### Merge one group into another:\n\n```ruby\n# Moves the members of source into destination, and destroys source\ndestination_group.merge!(source_group)\n```\n\n## Membership Types\n\nMembership types allow a member to belong to a group in a more specific way. For example,\nyou can add a user to a group with membership type of \"manager\" to specify that this\nuser has the \"manager role\" on that group.\n\nThis can be used to implement role-based authorization combined with group authorization,\nwhich could be used to mass-assign roles to groups of resources.\n\nIt could also be used to add users and resources to the same \"sub-group\" or \"project\"\nwithin a larger group (say, an organization).\n\n```ruby\n# Add user to group as a specific membership type\ngroup.add(user, as: 'manager')\n\n# Works with named groups too\nuser.named_groups.add 'Company', as: 'manager'\n\n# Query for the groups that a user belongs to with a certain role\nuser.groups.as(:manager)\nuser.named_groups.as('manager')\nGroup.with_member(user).as('manager')\n\n# Remove a member's membership type from a group\ngroup.users.delete(user, as: 'manager')         # Deletes this group's 'manager' group membership for this user\nuser.groups.destroy(group, as: 'employee')      # Destroys this user's 'employee' group membership for this group\nuser.groups.destroy(group)                      # Destroys any membership types this user had in this group\n\n# Find all members that have a certain membership type in a group\nUser.in_group(group).as(:manager)\n\n# Find all members of a certain membership type regardless of group\nUser.as(:manager)    # Find users that are managers, we don't care what group\n\n# Check if a member belongs to any/all groups with a certain membership type\nuser.in_all_groups?(group1, group2, as: 'manager')\n\n# Find all members that share the same group with the same membership type\nWidget.shares_any_group(user).as(\"Moon Launch Project\")\n\n# Check is one member belongs to the same group as another member with a certain membership type\nuser.shares_any_group?(widget, as: 'employee')\n```\n\nNote that adding a member to a group with a specific membership type will automatically\nadd them to that group without a specific membership type. This way you can still query\n`groups` and find the member in that group. If you then remove that specific membership\ntype, they still remain in the group without a specific membership type.\n\nRemoving a member from a group will bulk remove any specific membership types as well.\n\n```\ngroup.add(manager, as: 'manager')\nmanager.groups.include?(group)              # =\u003e true\n\nmanager.groups.delete(group, as: 'manager')\nmanager.groups.include?(group)              # =\u003e true\n\ngroup.add(employee, as: 'employee')\nemployee.groups.delete(group)\nemployee.in_group?(group)                   # =\u003e false\nemployee.in_group?(group, as: 'employee')   # =\u003e false\n```\n\n## Using for Authorization\nGroupify was originally created to help implement user authorization, although it can be used\ngenerically for much more than that. Here are some examples of how to do it.\n\n### With CanCan\n\n```ruby\nclass Ability\n  include CanCan::Ability\n\n  def initialize(user)\n    # Implements group-based authorization\n    # Users can only manage assignment which belong to the same group.\n    can [:manage], Assignment, Assignment.shares_any_group(user) do |assignment|\n      assignment.shares_any_group?(user)\n    end\n  end\nend\n```\n\n### With Authority\n\n```ruby\n# Whatever class represents a logged-in user in your app\nclass User\n  groupify :named_group_member\n  include Authority::UserAbilities\nend\n\nclass Widget\n  groupify :named_group_member\n  include Authority::Abilities\nend\n\nclass WidgetAuthorizer  \u003c ApplicationAuthorizer\n  # Implements group-based authorization using named groups.\n  # Users can only see widgets which belong to the same named group.\n  def readable_by?(user)\n    user.shares_any_named_group?(resource)\n  end\n\n  # Implements combined role-based and group-based authorization.\n  # Widgets can only be updated by users that are employees of the same named group.\n  def updateable_by?(user)\n    user.shares_any_named_group?(resource, as: :employee)\n  end\n\n  # Widgets can only be deleted by users that are managers of the same named group.\n  def deletable_by?(user)\n    user.shares_any_named_group?(resource, as: :manager)\n  end\nend\n\nuser = User.create!\nuser.named_groups.add(:team1, as: :employee)\n\nwidget = Widget.create!\nwidget.named_groups \u003c\u003c :team1\n\nwidget.readable_by?(user) # =\u003e true\nuser.can_update?(widget)  # =\u003e true\nuser.can_delete?(widget)  # =\u003e false\n```\n\n### With Pundit\n\n```ruby\nclass PostPolicy \u003c Struct.new(:user, :post)\n  # User can only update a published post if they are admin of the same group.\n  def update?\n    user.shares_any_group?(post, as: :admin) || !post.published?\n  end\n\n  class Scope \u003c Struct.new(:user, :scope)\n    def resolve\n      if user.admin?\n        # An admin can see all the posts in the group(s) they are admin for\n        scope.shares_any_group(user).as(:admin)\n      else\n        # Normal users can only see published posts in the same group(s).\n        scope.shares_any_group(user).where(published: true)\n      end\n    end\n  end\nend\n```\n\n## Backwards-Incompatible Releases\n\n### 0.9+ - Dropped support for Rails 3.2 and Ruby 1.9 - 2.1\n\nGroupify 0.9 added support for Rails 5.1, and dropped support for EOL'ed versions of Ruby,\nRails, ActiveRecord, and Mongoid.\n\nActiveRecord 5.1 no longer supports passing arguments to collection\nassociations. Because of this, the undocumented syntax `groups.as(:membership_type)`\nis no longer supported.\n\n### 0.8+ - Name Change for `group_memberships` Associations (ActiveRecord only)\n\nGroupify 0.8 changed the ActiveRecord adapter to support configuring the same\nmodel as both a group and a group member. To accomplish this, the internal `group_memberships`\nassociation was renamed to be different for groups and members. If you were\nusing it, please be aware that you will need to change your code. This\nassociation is considered to be an internal implementation details and not part\nof the public API, so please don't rely on it if you can avoid it.\n\n### 0.7+ - Polymorphic Groups (ActiveRecord only)\n\nGroupify \u003c 0.7 required a single `Group` model used for all group memberships.\nGroupify 0.7+ supports using multiple models as groups by implementing polymorphic associations.\nUpgrading requires adding a new `group_type` column to the `group_memberships` table and\npopulating that column with the class name of the group. Create the migration by executing:\n\n    $ rails generate groupify:active_record:upgrade\n\nAnd then run the migration:\n\n    $ rake db:migrate\n\nPlease note that this migration may block writes in MySQL if your `group_memberships`\ntable is large.\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Added some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## Contributors\n\nSee a list of contributors [here](https://github.com/dwbutler/groupify/graphs/contributors).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwbutler%2Fgroupify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdwbutler%2Fgroupify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwbutler%2Fgroupify/lists"}