From 7c483c352571bc0795ad24fd3233e809be4e6b46 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Fri, 27 Sep 2024 10:37:21 +0530 Subject: [PATCH] Add rolespermissions module, refractor the group to use it Update how initial groups are created in the database, instead of putting them in the migration files, which is quite hard to maintain the group definitation is now maintained via the AbstractRoles and they rsync'ed using `python manage.py sync_roles`. It will never delete any existing group or permissions attached to them. This PR also updates the `hypha.apply.users.groups` module to `hypha.apply.users.roles` to better prepare of upcoming changes due to rolepermissions module --- Procfile | 2 +- docs/setup/deployment/development/docker.md | 5 +- .../deployment/development/stand-alone.md | 2 + docs/setup/deployment/production/heroku.md | 3 +- .../deployment/production/stand-alone.md | 1 + hypha/apply/activity/adapters/emails.py | 4 +- hypha/apply/activity/adapters/utils.py | 4 +- hypha/apply/api/v1/serializers.py | 2 +- .../seed_community_lab_application.py | 2 +- .../management/commands/seed_concept_note.py | 2 +- .../management/commands/seed_fellowship.py | 2 +- .../commands/seed_rapid_response.py | 2 +- hypha/apply/funds/models/submissions.py | 2 +- hypha/apply/funds/models/utils.py | 2 +- hypha/apply/funds/permissions.py | 2 +- hypha/apply/funds/reviewers/services.py | 2 +- hypha/apply/funds/tests/factories/models.py | 2 +- hypha/apply/funds/tests/test_admin_views.py | 2 +- hypha/apply/funds/views_partials.py | 2 +- hypha/apply/projects/forms/project.py | 2 +- hypha/apply/projects/service_utils.py | 2 +- hypha/apply/projects/tests/factories.py | 2 +- hypha/apply/projects/views/project.py | 2 +- hypha/apply/review/models.py | 2 +- hypha/apply/users/admin_views.py | 10 +- hypha/apply/users/forms.py | 7 +- hypha/apply/users/groups.py | 93 ------------------ .../management/commands/migrate_users.py | 2 +- .../users/migrations/0002_initial_data.py | 24 +---- .../migrations/0008_add_staff_permissions.py | 10 +- .../migrations/0009_add_partner_group.py | 28 +----- .../0010_add_community_reviewer_group.py | 28 +----- .../migrations/0011_add_applicant_group.py | 28 +----- .../migrations/0012_set_applicant_group.py | 25 +---- .../migrations/0013_add_approver_group.py | 31 +----- .../migrations/0016_add_finance_group.py | 31 +----- .../migrations/0017_rename_staff_admin.py | 25 +---- .../migrations/0018_add_contracting_group.py | 31 +----- .../apply/users/migrations/0021_groupdesc.py | 12 --- .../users/migrations/0024_update_is_staff.py | 6 +- .../users/migrations/0026_delete_groupdesc.py | 15 +++ hypha/apply/users/models.py | 25 +---- hypha/apply/users/roles.py | 98 +++++++++++++++++++ hypha/apply/users/tests/factories.py | 2 +- hypha/home/wagtail_hooks.py | 2 +- hypha/settings/django.py | 4 + requirements.txt | 1 + 47 files changed, 179 insertions(+), 414 deletions(-) delete mode 100644 hypha/apply/users/groups.py create mode 100644 hypha/apply/users/migrations/0026_delete_groupdesc.py create mode 100644 hypha/apply/users/roles.py diff --git a/Procfile b/Procfile index b073761b7d..c144f8947e 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -release: python manage.py migrate --noinput && python manage.py clear_cache --cache=default +release: python manage.py migrate --noinput && python manage.py clear_cache --cache=default && python manage.py sync_roles web: gunicorn hypha.wsgi:application --log-file - diff --git a/docs/setup/deployment/development/docker.md b/docs/setup/deployment/development/docker.md index c6549534d3..26854ec246 100644 --- a/docs/setup/deployment/development/docker.md +++ b/docs/setup/deployment/development/docker.md @@ -134,6 +134,7 @@ pg_restore --verbose --clean --if-exists --no-acl --no-owner --dbname=hypha --us After restoring the sandbox db run the migrate command inside the py container. ```shell -docker-compose exec py bash -python3 manage.py migrate +docker-compose exec py python3 manage.py migrate +docker-compose exec py python3 manage.py sync_roles + ``` diff --git a/docs/setup/deployment/development/stand-alone.md b/docs/setup/deployment/development/stand-alone.md index e625b74426..438affb2c9 100644 --- a/docs/setup/deployment/development/stand-alone.md +++ b/docs/setup/deployment/development/stand-alone.md @@ -196,6 +196,7 @@ There are two ways to about it, you can either load demo data from `/public/san ```shell python3 manage.py migrate + python3 manage.py sync_roles ``` === "From Scratch" @@ -209,6 +210,7 @@ There are two ways to about it, you can either load demo data from `/public/san ```text python3 manage.py migrate + python3 manage.py sync_roles ``` !!! tip "Tips" diff --git a/docs/setup/deployment/production/heroku.md b/docs/setup/deployment/production/heroku.md index b56324107e..28e448c972 100644 --- a/docs/setup/deployment/production/heroku.md +++ b/docs/setup/deployment/production/heroku.md @@ -51,12 +51,13 @@ python3 -c "from django.core.management.utils import get_random_secret_key; prin ```shell heroku run python3 manage.py migrate -a [name-of-app] + heroku run python3 manage.py sync_roles -a [name-of-app] heroku run python3 manage.py createcachetable -a [name-of-app] heroku run python3 manage.py createsuperuser -a [name-of-app] heroku run python3 manage.py wagtailsiteupdate [the-public-address] [the-apply-address] 443 -a [name-of-app] ``` -7. Now add the "release" step back to the "Procfile" and deploy again. +7. Now add the "release" step back to the `Procfile` and deploy again. You should now have a running site. diff --git a/docs/setup/deployment/production/stand-alone.md b/docs/setup/deployment/production/stand-alone.md index 57b97fbc9d..2f4fdcb165 100644 --- a/docs/setup/deployment/production/stand-alone.md +++ b/docs/setup/deployment/production/stand-alone.md @@ -152,6 +152,7 @@ npm run build python manage.py collectstatic --noinput python manage.py createcachetable python manage.py migrate --noinput +python manage.py sync_roles python manage.py clear_cache --cache=default python manage.py createsuperuser python manage.py wagtailsiteupdate apply.server.domain 80 diff --git a/hypha/apply/activity/adapters/emails.py b/hypha/apply/activity/adapters/emails.py index 0cd1210a24..bd4533a798 100644 --- a/hypha/apply/activity/adapters/emails.py +++ b/hypha/apply/activity/adapters/emails.py @@ -11,13 +11,13 @@ from hypha.apply.activity.models import ALL, APPLICANT_PARTNERS, PARTNER from hypha.apply.projects.models.payment import CHANGES_REQUESTED_BY_STAFF, DECLINED from hypha.apply.projects.templatetags.project_tags import display_project_status -from hypha.apply.users.groups import ( +from hypha.apply.users.models import User +from hypha.apply.users.roles import ( APPROVER_GROUP_NAME, CONTRACTING_GROUP_NAME, FINANCE_GROUP_NAME, STAFF_GROUP_NAME, ) -from hypha.apply.users.models import User from hypha.core.mail import ( language, remove_extra_empty_lines, diff --git a/hypha/apply/activity/adapters/utils.py b/hypha/apply/activity/adapters/utils.py index 0e2bd8eb2a..2e18067ae4 100644 --- a/hypha/apply/activity/adapters/utils.py +++ b/hypha/apply/activity/adapters/utils.py @@ -16,12 +16,12 @@ RESUBMITTED, SUBMITTED, ) -from hypha.apply.users.groups import ( +from hypha.apply.users.models import User +from hypha.apply.users.roles import ( CONTRACTING_GROUP_NAME, FINANCE_GROUP_NAME, STAFF_GROUP_NAME, ) -from hypha.apply.users.models import User def link_to(target, request): diff --git a/hypha/apply/api/v1/serializers.py b/hypha/apply/api/v1/serializers.py index b366e8486a..2d17a8ed26 100644 --- a/hypha/apply/api/v1/serializers.py +++ b/hypha/apply/api/v1/serializers.py @@ -16,7 +16,7 @@ ) from hypha.apply.review.models import Review, ReviewOpinion from hypha.apply.review.options import RECOMMENDATION_CHOICES -from hypha.apply.users.groups import PARTNER_GROUP_NAME, STAFF_GROUP_NAME +from hypha.apply.users.roles import PARTNER_GROUP_NAME, STAFF_GROUP_NAME from hypha.core.utils import markdown_to_html User = get_user_model() diff --git a/hypha/apply/funds/management/commands/seed_community_lab_application.py b/hypha/apply/funds/management/commands/seed_community_lab_application.py index b62d4d5cf3..f5a2a4ae9c 100644 --- a/hypha/apply/funds/management/commands/seed_community_lab_application.py +++ b/hypha/apply/funds/management/commands/seed_community_lab_application.py @@ -7,7 +7,7 @@ from hypha.apply.funds.models import ApplicationForm, LabType from hypha.apply.funds.models.forms import LabBaseForm, LabBaseReviewForm from hypha.apply.review.models import ReviewForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from hypha.home.models import ApplyHomePage CL_FUND_TITLE = "Community lab (archive fund)" diff --git a/hypha/apply/funds/management/commands/seed_concept_note.py b/hypha/apply/funds/management/commands/seed_concept_note.py index 87ede22bbb..b19b730b2d 100644 --- a/hypha/apply/funds/management/commands/seed_concept_note.py +++ b/hypha/apply/funds/management/commands/seed_concept_note.py @@ -12,7 +12,7 @@ ApplicationBaseReviewForm, ) from hypha.apply.review.models import ReviewForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from hypha.home.models import ApplyHomePage CN_ROUND_TITLE = "Internet Freedom Fund (archive round)" diff --git a/hypha/apply/funds/management/commands/seed_fellowship.py b/hypha/apply/funds/management/commands/seed_fellowship.py index 48225e9691..059ad015b6 100644 --- a/hypha/apply/funds/management/commands/seed_fellowship.py +++ b/hypha/apply/funds/management/commands/seed_fellowship.py @@ -12,7 +12,7 @@ ApplicationBaseReviewForm, ) from hypha.apply.review.models import ReviewForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from hypha.home.models import ApplyHomePage FS_ROUND_TITLE = "Fellowship (archive round)" diff --git a/hypha/apply/funds/management/commands/seed_rapid_response.py b/hypha/apply/funds/management/commands/seed_rapid_response.py index 2659a36cdb..eaeb630705 100644 --- a/hypha/apply/funds/management/commands/seed_rapid_response.py +++ b/hypha/apply/funds/management/commands/seed_rapid_response.py @@ -12,7 +12,7 @@ ApplicationBaseReviewForm, ) from hypha.apply.review.models import ReviewForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from hypha.home.models import ApplyHomePage RR_ROUND_TITLE = "Rapid Response (archive round)" diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py index 5d5838cc9a..a1279c4ca3 100644 --- a/hypha/apply/funds/models/submissions.py +++ b/hypha/apply/funds/models/submissions.py @@ -48,7 +48,7 @@ from hypha.apply.stream_forms.models import BaseStreamForm from hypha.apply.todo.options import SUBMISSION_DRAFT from hypha.apply.todo.views import remove_tasks_for_user -from hypha.apply.users.groups import APPLICANT_GROUP_NAME +from hypha.apply.users.roles import APPLICANT_GROUP_NAME from ..blocks import NAMED_BLOCKS, ApplicationCustomFormFieldsBlock from ..workflow import ( diff --git a/hypha/apply/funds/models/utils.py b/hypha/apply/funds/models/utils.py index 6d8081a11a..e304df75fc 100644 --- a/hypha/apply/funds/models/utils.py +++ b/hypha/apply/funds/models/utils.py @@ -17,7 +17,7 @@ from hypha.apply.stream_forms.models import AbstractStreamForm from hypha.apply.todo.options import SUBMISSION_DRAFT from hypha.apply.todo.views import add_task_to_user -from hypha.apply.users.groups import ( +from hypha.apply.users.roles import ( COMMUNITY_REVIEWER_GROUP_NAME, PARTNER_GROUP_NAME, REVIEWER_GROUP_NAME, diff --git a/hypha/apply/funds/permissions.py b/hypha/apply/funds/permissions.py index cd12ba1737..2c5b60b3ec 100644 --- a/hypha/apply/funds/permissions.py +++ b/hypha/apply/funds/permissions.py @@ -3,7 +3,7 @@ from hypha.apply.funds.models.submissions import DRAFT_STATE -from ..users.groups import STAFF_GROUP_NAME, SUPERADMIN, TEAMADMIN_GROUP_NAME +from ..users.roles import STAFF_GROUP_NAME, SUPERADMIN, TEAMADMIN_GROUP_NAME def has_permission(action, user, object=None, raise_exception=True): diff --git a/hypha/apply/funds/reviewers/services.py b/hypha/apply/funds/reviewers/services.py index 84ff8e0ea7..347cc57e01 100644 --- a/hypha/apply/funds/reviewers/services.py +++ b/hypha/apply/funds/reviewers/services.py @@ -2,7 +2,7 @@ from django.db.models import Q from django.db.models.query import QuerySet -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME User = get_user_model() diff --git a/hypha/apply/funds/tests/factories/models.py b/hypha/apply/funds/tests/factories/models.py index 9a740c4154..17164445da 100644 --- a/hypha/apply/funds/tests/factories/models.py +++ b/hypha/apply/funds/tests/factories/models.py @@ -28,7 +28,7 @@ ) from hypha.apply.funds.workflow import ConceptProposal, Request, RequestExternal from hypha.apply.stream_forms.testing.factories import FormDataFactory -from hypha.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME +from hypha.apply.users.roles import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME from hypha.apply.users.tests.factories import ( ApplicantFactory, GroupFactory, diff --git a/hypha/apply/funds/tests/test_admin_views.py b/hypha/apply/funds/tests/test_admin_views.py index 492f974f15..dd61ed036f 100644 --- a/hypha/apply/funds/tests/test_admin_views.py +++ b/hypha/apply/funds/tests/test_admin_views.py @@ -6,7 +6,7 @@ from wagtail.test.utils import WagtailTestUtils from hypha.apply.funds.models.forms import ApplicationForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from hypha.apply.users.tests.factories import SuperUserFactory from hypha.home.factories import ApplyHomePageFactory diff --git a/hypha/apply/funds/views_partials.py b/hypha/apply/funds/views_partials.py index 9ecb40fc6c..9e3829666c 100644 --- a/hypha/apply/funds/views_partials.py +++ b/hypha/apply/funds/views_partials.py @@ -24,7 +24,7 @@ from hypha.apply.funds.reviewers.services import get_all_reviewers from hypha.apply.funds.services import annotate_review_recommendation_and_count from hypha.apply.review.options import REVIEWER -from hypha.apply.users.groups import REVIEWER_GROUP_NAME +from hypha.apply.users.roles import REVIEWER_GROUP_NAME from . import services from .models import ApplicationSubmission, Round diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index e67b602a72..225c641091 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -8,7 +8,7 @@ from hypha.apply.funds.models import ApplicationSubmission from hypha.apply.stream_forms.fields import SingleFileField from hypha.apply.stream_forms.forms import StreamBaseForm -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from ..models.project import ( CLOSING, diff --git a/hypha/apply/projects/service_utils.py b/hypha/apply/projects/service_utils.py index 577d386638..b2ec7cb3d9 100644 --- a/hypha/apply/projects/service_utils.py +++ b/hypha/apply/projects/service_utils.py @@ -12,7 +12,7 @@ remove_tasks_for_user, remove_tasks_for_user_group, ) -from hypha.apply.users.groups import ( +from hypha.apply.users.roles import ( APPROVER_GROUP_NAME, FINANCE_GROUP_NAME, ) diff --git a/hypha/apply/projects/tests/factories.py b/hypha/apply/projects/tests/factories.py index a77b6855a9..590151e04d 100644 --- a/hypha/apply/projects/tests/factories.py +++ b/hypha/apply/projects/tests/factories.py @@ -11,7 +11,7 @@ FormFieldsBlockFactory, NonFileFormFieldsBlockFactory, ) -from hypha.apply.users.groups import APPROVER_GROUP_NAME, STAFF_GROUP_NAME +from hypha.apply.users.roles import APPROVER_GROUP_NAME, STAFF_GROUP_NAME from hypha.apply.users.tests.factories import GroupFactory, StaffFactory, UserFactory from ..models.payment import Invoice, InvoiceDeliverable, SupportingDocument diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index 2b51085e4c..43715fff61 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -61,7 +61,7 @@ staff_or_finance_or_contracting_required, staff_required, ) -from hypha.apply.users.groups import CONTRACTING_GROUP_NAME +from hypha.apply.users.roles import CONTRACTING_GROUP_NAME from hypha.apply.utils.models import PDFPageSettings from hypha.apply.utils.storage import PrivateMediaView from hypha.apply.utils.views import DelegateableView, DelegatedViewMixin, ViewDispatcher diff --git a/hypha/apply/review/models.py b/hypha/apply/review/models.py index ec1b1bd166..87d7c0b8eb 100644 --- a/hypha/apply/review/models.py +++ b/hypha/apply/review/models.py @@ -8,7 +8,7 @@ from hypha.apply.funds.models.mixins import AccessFormData from hypha.apply.stream_forms.models import BaseStreamForm -from hypha.apply.users.groups import ( +from hypha.apply.users.roles import ( PARTNER_GROUP_NAME, REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, diff --git a/hypha/apply/users/admin_views.py b/hypha/apply/users/admin_views.py index 4f0fa99313..5b48b8c45e 100644 --- a/hypha/apply/users/admin_views.py +++ b/hypha/apply/users/admin_views.py @@ -4,6 +4,7 @@ from django.db.models import CharField, Q, Value from django.db.models.functions import Coalesce, Lower, NullIf from django.template.defaultfilters import mark_safe +from rolepermissions import roles from wagtail.admin.filters import WagtailFilterSet from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME from wagtail.users.views.groups import GroupViewSet @@ -11,8 +12,6 @@ from wagtail.users.views.users import Index as UserIndexView from wagtail.users.views.users import get_users_filter_query -from .models import GroupDesc - User = get_user_model() # Typically we would check the permission 'auth.change_user' (and 'auth.add_user' / @@ -147,14 +146,17 @@ class CustomGroupIndexView(GroupIndexView): def get_queryset(self): """ - Overriding the normal queryset that would return all Group objects, this returnd an iterable of groups with custom names containing HTML help text. + Overriding the normal queryset that would return all Group objects, this returned an iterable of groups with custom names containing HTML help text. """ group_qs = super().get_queryset() custom_groups = [] for group in group_qs: - help_text = GroupDesc.get_from_group(group) + # Check if the group is a role + help_text = getattr( + roles.registered_roles.get(group.name, {}), "help_text", "" + ) if help_text: group.name = mark_safe( f"{group.name}

{help_text}

" diff --git a/hypha/apply/users/forms.py b/hypha/apply/users/forms.py index a72c953a1a..e52415b732 100644 --- a/hypha/apply/users/forms.py +++ b/hypha/apply/users/forms.py @@ -5,9 +5,10 @@ from django.template.defaultfilters import mark_safe from django.utils.translation import gettext_lazy as _ from django_select2.forms import Select2Widget +from rolepermissions import roles from wagtail.users.forms import UserCreationForm, UserEditForm -from .models import AuthSettings, GroupDesc +from .models import AuthSettings from .utils import strip_html_and_nerf_urls User = get_user_model() @@ -125,7 +126,9 @@ def label_from_instance(self, group_obj): """ Overwriting ModelMultipleChoiceField's label from instance to provide help_text (if it exists) """ - help_text = GroupDesc.get_from_group(group_obj) + help_text = getattr( + roles.registered_roles.get(group_obj.name, {}), "help_text", "" + ) if help_text: return mark_safe( f'{group_obj.name}

{help_text}

' diff --git a/hypha/apply/users/groups.py b/hypha/apply/users/groups.py deleted file mode 100644 index 882f02f819..0000000000 --- a/hypha/apply/users/groups.py +++ /dev/null @@ -1,93 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -SUPERADMIN = _("Administrator") -APPLICANT_GROUP_NAME = _("Applicant") -STAFF_GROUP_NAME = _("Staff") -REVIEWER_GROUP_NAME = _("Reviewer") -TEAMADMIN_GROUP_NAME = _("Staff Admin") -PARTNER_GROUP_NAME = _("Partner") -COMMUNITY_REVIEWER_GROUP_NAME = _("Community reviewer") -APPROVER_GROUP_NAME = _("Approver") -FINANCE_GROUP_NAME = _("Finance") -CONTRACTING_GROUP_NAME = _("Contracting") - -APPLICANT_HELP_TEXT = _( - "Can access their own application and communicate via the communication tab." -) -STAFF_HELP_TEXT = _( - "View and edit all submissions, submit reviews, send determinations, and set up applications." -) -REVIEWER_HELP_TEXT = _( - "Has a dashboard and can submit reviews. Advisory Council Members are typically assigned this role." -) - -TEAMADMIN_HELP_TEXT = _( - "Can view application message log. Must also be in group Staff." -) - -PARTNER_HELP_TEXT = _( - "Can view, edit, and comment on a specific application they are assigned to." -) - -COMMUNITY_REVIEWER_HELP_TEXT = _( - "An applicant with access to other applications utilizing the community/peer review workflow." -) - -APPROVER_HELP_TEXT = _( - "Can review/approve PAF, and access compliance documents. Must also be in group: Staff, Contracting, or Finance." -) -FINANCE_HELP_TEXT = _( - "Can review/approve the PAF, access documents associated with contracting, and access invoices approved by Staff." -) -CONTRACTING_HELP_TEXT = _( - "Can review/approve the PAF and access documents associated with contracting." -) - - -GROUPS = [ - { - "name": APPLICANT_GROUP_NAME, - "permissions": [], - "help_text": APPLICANT_HELP_TEXT, - }, - { - "name": STAFF_GROUP_NAME, - "permissions": [], - "help_text": STAFF_HELP_TEXT, - }, - { - "name": REVIEWER_GROUP_NAME, - "permissions": [], - "help_text": REVIEWER_HELP_TEXT, - }, - { - "name": TEAMADMIN_GROUP_NAME, - "permissions": [], - "help_text": TEAMADMIN_HELP_TEXT, - }, - { - "name": PARTNER_GROUP_NAME, - "permissions": [], - "help_text": PARTNER_HELP_TEXT, - }, - { - "name": COMMUNITY_REVIEWER_GROUP_NAME, - "permissions": [], - "help_text": COMMUNITY_REVIEWER_HELP_TEXT, - }, - { - "name": APPROVER_GROUP_NAME, - "permissions": [], - "help_text": APPROVER_HELP_TEXT, - }, - { - "name": FINANCE_GROUP_NAME, - "permissions": [], - "help_text": FINANCE_HELP_TEXT, - }, - { - "name": CONTRACTING_GROUP_NAME, - "permissions": [], - "help_text": CONTRACTING_HELP_TEXT, - }, -] diff --git a/hypha/apply/users/management/commands/migrate_users.py b/hypha/apply/users/management/commands/migrate_users.py index 2e65c20b0a..59d91389e2 100644 --- a/hypha/apply/users/management/commands/migrate_users.py +++ b/hypha/apply/users/management/commands/migrate_users.py @@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand from django.db import transaction -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME class Command(BaseCommand): diff --git a/hypha/apply/users/migrations/0002_initial_data.py b/hypha/apply/users/migrations/0002_initial_data.py index 1621f68c92..de52563a2e 100644 --- a/hypha/apply/users/migrations/0002_initial_data.py +++ b/hypha/apply/users/migrations/0002_initial_data.py @@ -2,35 +2,15 @@ # Generated by Django 1.11.7 on 2017-12-15 13:15 from __future__ import unicode_literals -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import GROUPS - def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for permission in group_data["permissions"]: - try: - group.permissions.add(Permission.objects.get(codename=permission)) - except ObjectDoesNotExist: - print("Could not find the '%s' permission" % permission) + pass def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - - groups = [group_data["name"] for group_data in GROUPS] - Group.objects.filter(name__in=groups).delete() + pass class Migration(migrations.Migration): diff --git a/hypha/apply/users/migrations/0008_add_staff_permissions.py b/hypha/apply/users/migrations/0008_add_staff_permissions.py index d6be98faac..8f91ec2ebe 100644 --- a/hypha/apply/users/migrations/0008_add_staff_permissions.py +++ b/hypha/apply/users/migrations/0008_add_staff_permissions.py @@ -1,19 +1,11 @@ # Generated by Django 2.0.9 on 2019-01-10 09:28 -from django.contrib.auth.models import Group, Permission from django.db import migrations -from hypha.apply.users.groups import STAFF_GROUP_NAME - class Migration(migrations.Migration): - def add_permissions(apps, schema_editor): - staff_group = Group.objects.get(name=STAFF_GROUP_NAME) - staff_add_perm = Permission.objects.get(name="Can change event") - staff_group.permissions.add(staff_add_perm) - dependencies = [ ("users", "0007_user_slack"), ] - operations = [migrations.RunPython(add_permissions)] + operations = [] diff --git a/hypha/apply/users/migrations/0009_add_partner_group.py b/hypha/apply/users/migrations/0009_add_partner_group.py index a471d8d8f8..eca6859675 100644 --- a/hypha/apply/users/migrations/0009_add_partner_group.py +++ b/hypha/apply/users/migrations/0009_add_partner_group.py @@ -1,38 +1,12 @@ # Generated by Django 2.0.9 on 2018-12-19 13:21 from __future__ import unicode_literals -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import GROUPS, TEAMADMIN_GROUP_NAME, PARTNER_GROUP_NAME - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for permission in group_data["permissions"]: - try: - group.permissions.add(Permission.objects.get(codename=permission)) - except ObjectDoesNotExist: - print("Could not find the '%s' permission" % permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name__in=[TEAMADMIN_GROUP_NAME, PARTNER_GROUP_NAME]).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0008_add_staff_permissions"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0010_add_community_reviewer_group.py b/hypha/apply/users/migrations/0010_add_community_reviewer_group.py index 4829cbaf5c..be39afe9c4 100644 --- a/hypha/apply/users/migrations/0010_add_community_reviewer_group.py +++ b/hypha/apply/users/migrations/0010_add_community_reviewer_group.py @@ -1,38 +1,12 @@ # Generated by Django 2.0.9 on 2018-12-19 13:21 from __future__ import unicode_literals -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import GROUPS, COMMUNITY_REVIEWER_GROUP_NAME - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for permission in group_data["permissions"]: - try: - group.permissions.add(Permission.objects.get(codename=permission)) - except ObjectDoesNotExist: - print("Could not find the '%s' permission" % permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name__in=[COMMUNITY_REVIEWER_GROUP_NAME]).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0009_add_partner_group"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0011_add_applicant_group.py b/hypha/apply/users/migrations/0011_add_applicant_group.py index e55ee34e7d..05a8304744 100644 --- a/hypha/apply/users/migrations/0011_add_applicant_group.py +++ b/hypha/apply/users/migrations/0011_add_applicant_group.py @@ -1,38 +1,12 @@ # Generated by Django 2.0.9 on 2018-12-19 13:21 from __future__ import unicode_literals -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import GROUPS, APPLICANT_GROUP_NAME - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for permission in group_data["permissions"]: - try: - group.permissions.add(Permission.objects.get(codename=permission)) - except ObjectDoesNotExist: - print("Could not find the '%s' permission" % permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name__in=[APPLICANT_GROUP_NAME]).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0010_add_community_reviewer_group"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0012_set_applicant_group.py b/hypha/apply/users/migrations/0012_set_applicant_group.py index 0358424960..38e88013db 100644 --- a/hypha/apply/users/migrations/0012_set_applicant_group.py +++ b/hypha/apply/users/migrations/0012_set_applicant_group.py @@ -1,35 +1,12 @@ # Generated by Django 2.0.9 on 2018-12-19 13:21 from __future__ import unicode_literals -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.db import migrations -from hypha.apply.users.groups import APPLICANT_GROUP_NAME - - -def set_group(apps, schema_editor): - User = get_user_model() - applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - applicants = User.objects.exclude(applicationsubmission=None) - for user in applicants: - if not user.is_apply_staff: - user.groups.add(applicant_group) - user.save() - - -def unset_group(apps, schema_editor): - User = get_user_model() - applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - applicants = User.objects.filter(groups__name=APPLICANT_GROUP_NAME).all() - for user in applicants: - user.groups.remove(applicant_group) - user.save() - class Migration(migrations.Migration): dependencies = [ ("users", "0011_add_applicant_group"), ] - operations = [migrations.RunPython(set_group, unset_group)] + operations = [] diff --git a/hypha/apply/users/migrations/0013_add_approver_group.py b/hypha/apply/users/migrations/0013_add_approver_group.py index 3638944b2c..5662ab86d4 100644 --- a/hypha/apply/users/migrations/0013_add_approver_group.py +++ b/hypha/apply/users/migrations/0013_add_approver_group.py @@ -1,40 +1,11 @@ # Generated by Django 2.0.13 on 2019-08-05 13:12 -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import APPROVER_GROUP_NAME, GROUPS - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for codename in group_data["permissions"]: - try: - permission = Permission.objects.get(codename=codename) - except ObjectDoesNotExist: - print(f"Could not find the '{permission}' permission") - continue - - group.permissions.add(permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name=APPROVER_GROUP_NAME).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0012_set_applicant_group"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0016_add_finance_group.py b/hypha/apply/users/migrations/0016_add_finance_group.py index 88525ad545..c170781980 100644 --- a/hypha/apply/users/migrations/0016_add_finance_group.py +++ b/hypha/apply/users/migrations/0016_add_finance_group.py @@ -1,40 +1,11 @@ # Generated by Django 2.0.13 on 2019-08-05 13:12 -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import FINANCE_GROUP_NAME, GROUPS - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for codename in group_data["permissions"]: - try: - permission = Permission.objects.get(codename=codename) - except ObjectDoesNotExist: - print(f"Could not find the '{permission}' permission") - continue - - group.permissions.add(permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name=FINANCE_GROUP_NAME).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0015_login_extra_text"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0017_rename_staff_admin.py b/hypha/apply/users/migrations/0017_rename_staff_admin.py index 11ae3ad0a0..ac2c859ff7 100644 --- a/hypha/apply/users/migrations/0017_rename_staff_admin.py +++ b/hypha/apply/users/migrations/0017_rename_staff_admin.py @@ -1,35 +1,12 @@ # Generated by Django 2.0.9 on 2018-12-19 13:21 from __future__ import unicode_literals -from django.contrib.auth.models import Group from django.db import migrations -from hypha.apply.users.groups import TEAMADMIN_GROUP_NAME - - -def rename_group(apps, schema_editor): - try: - team_admin_group = Group.objects.get(name="Team Admin") - except Group.DoesNotExist: - pass - else: - team_admin_group.name = TEAMADMIN_GROUP_NAME - team_admin_group.save() - - -def unrename_group(apps, schema_editor): - try: - team_admin_group = Group.objects.get(name=TEAMADMIN_GROUP_NAME) - except Group.DoesNotExist: - pass - else: - team_admin_group.name = "Team Admin" - team_admin_group.save() - class Migration(migrations.Migration): dependencies = [ ("users", "0016_add_finance_group"), ] - operations = [migrations.RunPython(rename_group, unrename_group)] + operations = [] diff --git a/hypha/apply/users/migrations/0018_add_contracting_group.py b/hypha/apply/users/migrations/0018_add_contracting_group.py index ac220bd2c4..1965ae478e 100644 --- a/hypha/apply/users/migrations/0018_add_contracting_group.py +++ b/hypha/apply/users/migrations/0018_add_contracting_group.py @@ -1,40 +1,11 @@ # Generated by Django 2.0.13 on 2019-08-05 13:12 -from django.core.exceptions import ObjectDoesNotExist -from django.core.management.sql import emit_post_migrate_signal from django.db import migrations -from hypha.apply.users.groups import CONTRACTING_GROUP_NAME, GROUPS - - -def add_groups(apps, schema_editor): - # Workaround for https://code.djangoproject.com/ticket/23422 - db_alias = schema_editor.connection.alias - emit_post_migrate_signal(2, False, db_alias) - - Group = apps.get_model("auth.Group") - Permission = apps.get_model("auth.Permission") - - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - for codename in group_data["permissions"]: - try: - permission = Permission.objects.get(codename=codename) - except ObjectDoesNotExist: - print(f"Could not find the '{permission}' permission") - continue - - group.permissions.add(permission) - - -def remove_groups(apps, schema_editor): - Group = apps.get_model("auth.Group") - Group.objects.filter(name=CONTRACTING_GROUP_NAME).delete() - class Migration(migrations.Migration): dependencies = [ ("users", "0017_rename_staff_admin"), ] - operations = [migrations.RunPython(add_groups, remove_groups)] + operations = [] diff --git a/hypha/apply/users/migrations/0021_groupdesc.py b/hypha/apply/users/migrations/0021_groupdesc.py index 44116250b4..be849fd53b 100644 --- a/hypha/apply/users/migrations/0021_groupdesc.py +++ b/hypha/apply/users/migrations/0021_groupdesc.py @@ -1,19 +1,8 @@ # Generated by Django 3.2.22 on 2023-10-31 17:26 from django.db import migrations, models -from django.contrib.auth.models import Group import django.db.models.deletion -from hypha.apply.users.groups import GROUPS -from hypha.apply.users.models import GroupDesc - - -def add_desc_groups(apps, schema_editor): - for group_data in GROUPS: - group, created = Group.objects.get_or_create(name=group_data["name"]) - if group_data.get("help_text") is not None: - GroupDesc.objects.create(group=group, help_text=group_data["help_text"]) - class Migration(migrations.Migration): dependencies = [ @@ -40,5 +29,4 @@ class Migration(migrations.Migration): ), ], ), - migrations.RunPython(add_desc_groups), ] diff --git a/hypha/apply/users/migrations/0024_update_is_staff.py b/hypha/apply/users/migrations/0024_update_is_staff.py index 09501e7a6b..1bc008cbb7 100644 --- a/hypha/apply/users/migrations/0024_update_is_staff.py +++ b/hypha/apply/users/migrations/0024_update_is_staff.py @@ -3,12 +3,14 @@ from django.db import migrations from django.contrib.auth.models import Group -from hypha.apply.users.groups import TEAMADMIN_GROUP_NAME +from hypha.apply.users.roles import TEAMADMIN_GROUP_NAME from hypha.apply.users.utils import update_is_staff def migrate_is_staff(apps, schema_editor): - group = Group.objects.get(name=TEAMADMIN_GROUP_NAME) + group = Group.objects.filter(name=TEAMADMIN_GROUP_NAME).first() + if group is None: + return users = group.user_set.all() [update_is_staff(None, user) for user in users] diff --git a/hypha/apply/users/migrations/0026_delete_groupdesc.py b/hypha/apply/users/migrations/0026_delete_groupdesc.py new file mode 100644 index 0000000000..73b8de620f --- /dev/null +++ b/hypha/apply/users/migrations/0026_delete_groupdesc.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.16 on 2024-09-27 05:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0025_remove_authsettings_register_extra_text"), + ] + + operations = [ + migrations.DeleteModel( + name="GroupDesc", + ), + ] diff --git a/hypha/apply/users/models.py b/hypha/apply/users/models.py index 17d08f29a1..03dfb257f3 100644 --- a/hypha/apply/users/models.py +++ b/hypha/apply/users/models.py @@ -1,6 +1,6 @@ from django.conf import settings from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import AbstractUser, BaseUserManager, Group +from django.contrib.auth.models import AbstractUser, BaseUserManager from django.core import exceptions from django.db import IntegrityError, models from django.db.models.constants import LOOKUP_SEP @@ -13,7 +13,7 @@ from wagtail.contrib.settings.models import BaseGenericSetting, register_setting from wagtail.fields import RichTextField -from .groups import ( +from .roles import ( APPLICANT_GROUP_NAME, APPROVER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME, @@ -381,27 +381,6 @@ class Meta: ] -class GroupDesc(models.Model): - group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True) - help_text = models.CharField(verbose_name="Help Text", max_length=255) - - @staticmethod - def get_from_group(group_obj: Group) -> str | None: - """ - Get the group description/help text string from a Group object. Returns None if group doesn't have a help text entry. - - Args: - group_obj (Group): The group to retrieve the description of. - """ - try: - return GroupDesc.objects.get(group_id=group_obj.id).help_text - except (exceptions.ObjectDoesNotExist, exceptions.FieldError): - return None - - def __str__(self): - return self.help_text - - class PendingSignup(models.Model): """This model tracks pending passwordless self-signups, and is used to generate a one-time use URLfor each signup. diff --git a/hypha/apply/users/roles.py b/hypha/apply/users/roles.py new file mode 100644 index 0000000000..d8760b0b6d --- /dev/null +++ b/hypha/apply/users/roles.py @@ -0,0 +1,98 @@ +from django.utils.translation import gettext_lazy as _ +from rolepermissions.roles import AbstractUserRole + +SUPERADMIN = _("Administrator") +APPLICANT_GROUP_NAME = _("Applicant") +STAFF_GROUP_NAME = _("Staff") +REVIEWER_GROUP_NAME = _("Reviewer") +TEAMADMIN_GROUP_NAME = _("Staff Admin") +PARTNER_GROUP_NAME = _("Partner") +COMMUNITY_REVIEWER_GROUP_NAME = _("Community reviewer") +APPROVER_GROUP_NAME = _("Approver") +FINANCE_GROUP_NAME = _("Finance") +CONTRACTING_GROUP_NAME = _("Contracting") + + +# roles for the application +# https://django-role-permissions.readthedocs.io/en/stable/roles.html +class Applicant(AbstractUserRole): + role_name = APPLICANT_GROUP_NAME + help_text = _( + "Can access their own application and communicate via " "the communication tab." + ) + + available_permissions = {} + + +class Staff(AbstractUserRole): + role_name = STAFF_GROUP_NAME + help_text = _( + "View and edit all submissions, submit reviews, send determinations, " + "and set up applications." + ) + + available_permissions = {} + + +class Reviewer(AbstractUserRole): + role_name = REVIEWER_GROUP_NAME + help_text = _( + "Has a dashboard and can submit reviews. " + "Advisory Council Members are typically assigned this role." + ) + + available_permissions = {} + + +class StaffAdmin(AbstractUserRole): + role_name = TEAMADMIN_GROUP_NAME + help_text = _("Can view application message log. Must also be in group Staff.") + + available_permissions = {} + + +class Partner(AbstractUserRole): + role_name = PARTNER_GROUP_NAME + help_text = _( + "Can view, edit, and comment on a specific application they are assigned to." + ) + + available_permissions = {} + + +class CommunityReviewer(AbstractUserRole): + role_name = COMMUNITY_REVIEWER_GROUP_NAME + help_text = _( + "An applicant with access to other applications utilizing the community/peer review workflow." + ) + + available_permissions = {} + + +class Approver(AbstractUserRole): + role_name = APPROVER_GROUP_NAME + help_text = _( + "Can review/approve PAF, and access compliance documents. " + "Must also be in group: Staff, Contracting, or Finance." + ) + + available_permissions = {} + + +class Finance(AbstractUserRole): + role_name = FINANCE_GROUP_NAME + help_text = _( + "Can review/approve the PAF, access documents associated with " + "contracting, and access invoices approved by Staff." + ) + + available_permissions = {} + + +class Contracting(AbstractUserRole): + role_name = CONTRACTING_GROUP_NAME + help_text = _( + "Can review/approve the PAF and access documents associated with contracting." + ) + + available_permissions = {} diff --git a/hypha/apply/users/tests/factories.py b/hypha/apply/users/tests/factories.py index 2068b91c25..ca17b3d84e 100644 --- a/hypha/apply/users/tests/factories.py +++ b/hypha/apply/users/tests/factories.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group, Permission from django.utils.text import slugify -from ..groups import ( +from ..roles import ( APPLICANT_GROUP_NAME, APPROVER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME, diff --git a/hypha/home/wagtail_hooks.py b/hypha/home/wagtail_hooks.py index 2987a90540..f4c7fe6771 100644 --- a/hypha/home/wagtail_hooks.py +++ b/hypha/home/wagtail_hooks.py @@ -1,6 +1,6 @@ from wagtail import hooks -from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.users.roles import STAFF_GROUP_NAME from .models import ApplyHomePage diff --git a/hypha/settings/django.py b/hypha/settings/django.py index 8682287c4d..c0703929ac 100644 --- a/hypha/settings/django.py +++ b/hypha/settings/django.py @@ -70,6 +70,7 @@ "rest_framework", "rest_framework_api_key", "django_file_form", + "rolepermissions", "hijack", "elevate", # https://django-elevate.readthedocs.io/ "pagedown", @@ -222,6 +223,9 @@ CUSTOM_AUTH_BACKEND, ) +# django-rolepermissions +ROLEPERMISSIONS_MODULE = "hypha.apply.users.roles" + # Default Auto field configuration DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/requirements.txt b/requirements.txt index b0df8fddcb..eff51290e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ django-htmx==1.17.3 django-pagedown==2.2.1 django-ratelimit==4.1.0 django-referrer-policy==1.0 +django-role-permissions==3.2.0 django-select2==8.1.2 django-slack==5.19.0 django-storages==1.14.2