Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically update user permission groups to users when they are added as managers to orgs/facilities #425

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions organizations/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# coding=utf-8
default_app_config = 'organizations.apps.OrganizationsConfig'
2 changes: 2 additions & 0 deletions organizations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ def get_readonly_fields(self, request, obj=None):
if len(user_orgs) <= 1 and hasattr(obj, 'organization') \
and 'organization' not in readonly:
readonly += ('organization',)
if obj and hasattr(obj, "user_account"):
readonly += ('user_account',)
return readonly

def get_list_display(self, request):
Expand Down
11 changes: 11 additions & 0 deletions organizations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class OrganizationsConfig(AppConfig):
name = "organizations"
verbose_name = _("Organizations")

def ready(self):
# Connect signals
from . import signals # noqa
91 changes: 91 additions & 0 deletions organizations/migrations/0013_add_manager_group_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generated by Django 2.2.3 on 2022-03-12 09:57

from django.db import migrations

from organizations.models import Membership
from ..settings import FACILITY_MANAGER_GROUPNAME, ORGANIZATION_MANAGER_GROUPNAME

VIEW, ADD, CHANGE, DELETE = "view", "add", "change", "delete"
ALL = (VIEW, ADD, CHANGE, DELETE)

FACILITY_MANAGER_PERMISSIONS = (
("accounts", "useraccount", (VIEW,)),
("organizations", "facility", (CHANGE, VIEW)),
("organizations", "facilitymembership", ALL),
("organizations", "organization", (VIEW,)),
("organizations", "organizationmembership", (VIEW,)),
("organizations", "task", ALL),
("organizations", "workplace", ALL),
("scheduler", "shift", ALL),
("scheduler", "shifthelper", ALL),
("scheduletemplates", "scheduletemplate", ALL),
("scheduletemplates", "shifttemplate", ALL),
)

ORGANIZATION_MANAGER_PERMISSIONS = FACILITY_MANAGER_PERMISSIONS + (
("accounts", "useraccount", (VIEW,)),
("organizations", "facility", ALL),
("organizations", "facilitymembership", ALL),
("organizations", "organization", (CHANGE, VIEW)),
("organizations", "organizationmembership", ALL),
("organizations", "task", ALL),
("organizations", "workplace", ALL),
("scheduler", "shift", ALL),
("scheduler", "shifthelper", ALL),
("scheduletemplates", "scheduletemplate", ALL),
("scheduletemplates", "shifttemplate", ALL),
)

MANAGER_GROUPS = {
FACILITY_MANAGER_GROUPNAME: FACILITY_MANAGER_PERMISSIONS,
ORGANIZATION_MANAGER_GROUPNAME: ORGANIZATION_MANAGER_PERMISSIONS,
}


def forwards(apps, schema_editor):
ContentTypeModel = apps.get_model("contenttypes", "ContentType")
GroupModel = apps.get_model("auth", "Group")
PermissionModel = apps.get_model("auth", "Permission")

for group_name, permissions in MANAGER_GROUPS.items():
group, created = GroupModel.objects.get_or_create(name=group_name)
print(f" - {group.name}: created group object")
for app_label, model, actions in permissions:
content_type = ContentTypeModel.objects.get(app_label=app_label.lower(), model=model.lower())
for action in actions:
permission = PermissionModel.objects.get(
content_type=content_type, codename=f"{action}_{model}".lower()
)
group.permissions.add(permission)
print(f' - {group.name}: added permission "{permission.name}" ({permission.codename})')

OrganizationMembershipModel = apps.get_model("organizations", "OrganizationMembership")
for organization_member in OrganizationMembershipModel.objects.filter(role__lt=Membership.Roles.MEMBER):
user = organization_member.user_account.user
group = GroupModel.objects.get(name=ORGANIZATION_MANAGER_GROUPNAME)
user.groups.add(group)
print(f' - {group.name}: added user "{user.username}" (id: {user.id})')

FacilityMembershipModel = apps.get_model("organizations", "FacilityMembership")
for facility_member in FacilityMembershipModel.objects.filter(role__lt=Membership.Roles.MEMBER):
user = facility_member.user_account.user
group = GroupModel.objects.get(name=FACILITY_MANAGER_GROUPNAME)
user.groups.add(group)
print(f' - {group.name}: added user "{user.username}" (id: {user.id})')


def backwards(apps, schema_editor):
GroupModel = apps.get_model("auth", "Group")
for group_name in MANAGER_GROUPS.keys():
GroupModel.objects.get(name=group_name).delete()


class Migration(migrations.Migration):
dependencies = [
("auth", "0011_update_proxy_permissions"),
("organizations", "0012_cascade_deletion"),
]

operations = [
migrations.RunPython(forwards, backwards),
]
5 changes: 5 additions & 0 deletions organizations/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.conf import settings

# provide sane default group names, change in settings
FACILITY_MANAGER_GROUPNAME = getattr(settings, "FACILITY_MANAGER_GROUPNAME", "Facility Manager")
ORGANIZATION_MANAGER_GROUPNAME = getattr(settings, "ORGANIZATION_MANAGER_GROUPNAME", "Organization Manager")
85 changes: 85 additions & 0 deletions organizations/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import logging

from django.contrib.auth.models import Group
from django.db import transaction
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

from django.utils.translation import gettext_lazy as _

from .models import Membership, FacilityMembership, OrganizationMembership
from .settings import ORGANIZATION_MANAGER_GROUPNAME, FACILITY_MANAGER_GROUPNAME

logger = logging.getLogger(__name__)


class MembershipGroupUpdateException(Exception):
pass


@transaction.atomic
def update_group_for_user(user_account, membership_set, group_name):
"""
Check django.contrib.auth groups of user in user_account and add or remove groups for its memberships.

:param user_account: the user account of the associated user to update the groups of
:param membership_set: the membership set (reverse relation) to consider
:param group_name: Name of group to be added or removed from the associated user
:return:
"""
user = user_account.user
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
logger.error(
_(
f"User '{user}' manager status of a facility/organization was changed. "
f"We tried to automatically update facility/organization manager "
f"group '{group_name}' for them, but no such group exists. "
f"In order to auto-assign permission groups to de-/resignated facility managers or organization "
f"managers, please make sure, to configure the permission group names in your VP installation settings "
f'module ORGANIZATION_MANAGER_GROUPNAME (default: "{ORGANIZATION_MANAGER_GROUPNAME}") and '
f'FACILITY_MANAGER_GROUPNAME (default: "{FACILITY_MANAGER_GROUPNAME}") exactly as they are '
f"named in the database."
)
)
return

if membership_set.filter(role__lt=Membership.Roles.MEMBER).exists():
user.groups.add(group)

if not user.is_staff:
user.is_staff = True
user.save()
else:
user.groups.remove(group)
# Revoking the user is_staff flag here is not save, because it can not be known, if the user has this flag
# for another reason, too.


@receiver([post_save, post_delete], sender=FacilityMembership)
def handle_facility_membership_change(sender, instance, **kwargs):
"""
Update the django.contrib.auth groups of the associated user object, whenever a facility membership for it is
created, changed or deleted.
"""
try:
user_account = instance.user_account
update_group_for_user(user_account, user_account.facilitymembership_set, FACILITY_MANAGER_GROUPNAME)

except Exception as e:
raise MembershipGroupUpdateException(f'facility -> "{FACILITY_MANAGER_GROUPNAME}"') from e


@receiver((post_save, post_delete), sender=OrganizationMembership)
def handle_organization_membership_change(sender, instance, **kwargs):
"""
Update the django.contrib.auth groups of the associated user object, whenever a organization membership for it is
created, changed or deleted.
"""
try:
user_account = instance.user_account
update_group_for_user(user_account, user_account.organizationmembership_set, ORGANIZATION_MANAGER_GROUPNAME)

except Exception as e:
raise MembershipGroupUpdateException(f'organization -> "{FACILITY_MANAGER_GROUPNAME}"') from e
16 changes: 16 additions & 0 deletions scheduler/migrations/0039_delete_workdone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 2.2.3 on 2022-03-12 09:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('scheduler', '0038_protect_deletion'),
]

operations = [
migrations.DeleteModel(
name='WorkDone',
),
]
4 changes: 4 additions & 0 deletions volunteer_planner/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,7 @@
INCLUDE_REGISTER_URL = True
INCLUDE_AUTH_URLS = True
REGISTRATION_FORM = "registration.forms.RegistrationFormUniqueEmail"

FACILITY_MANAGER_GROUPNAME = "Facility Manager"
ORGANIZATION_MANAGER_GROUPNAME = "Organization Manager"