Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
02346de
add perms for admin button
pavlo-mk Apr 3, 2026
b08a66f
add new perms
pavlo-mk Apr 7, 2026
a00a747
Merge branch 'develop' into perms_admin_button
pavlo-mk Apr 7, 2026
61a5740
migrations
pavlo-mk Apr 7, 2026
ae2b7a0
tests
pavlo-mk Apr 8, 2026
3276d85
tests again
pavlo-mk Apr 8, 2026
2227112
tests group id
pavlo-mk Apr 8, 2026
2a53fd4
tests_groups
pavlo-mk Apr 8, 2026
7da74aa
tests_groups fixtures
pavlo-mk Apr 8, 2026
cba02a0
test_groups.py
pavlo-mk Apr 8, 2026
0000334
Merge branch 'develop' into perms_admin_button
pavlo-mk Apr 8, 2026
ede269c
revert changes test_groups.py
pavlo-mk Apr 8, 2026
0af4291
fixtures
pavlo-mk Apr 8, 2026
feb531a
revert all test_groups.py changes
pavlo-mk Apr 8, 2026
e961c47
update fixtures group
pavlo-mk Apr 8, 2026
04551b8
update fixtures
pavlo-mk Apr 8, 2026
2a36aa2
revert test_groups.py
pavlo-mk Apr 8, 2026
4e20934
passed locally
pavlo-mk Apr 8, 2026
93e2dca
CI test
pavlo-mk Apr 8, 2026
eaf89da
CI test
pavlo-mk Apr 8, 2026
78d5517
use get perms
pavlo-mk Apr 8, 2026
9cfee1a
test_groups
pavlo-mk Apr 8, 2026
4181155
test_groups fixtures
pavlo-mk Apr 8, 2026
afdb51f
conflicts & review part 1
pavlo-mk Apr 14, 2026
ac0fdae
migrations
pavlo-mk Apr 14, 2026
68a54be
add migrations
pavlo-mk Apr 14, 2026
b03d215
review part 2
pavlo-mk Apr 14, 2026
ae8e7cb
new HH migration
pavlo-mk Apr 14, 2026
4adc0f7
fix lambda permission
pavlo-mk Apr 15, 2026
a2c19a4
review
pavlo-mk Apr 15, 2026
4e1f520
Merge branch 'develop' into perms_admin_button
pavlo-mk Apr 15, 2026
8cb6afa
migrations
pavlo-mk Apr 15, 2026
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
6 changes: 5 additions & 1 deletion src/hope/admin/api_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ def _send_token_email(self, request: HttpRequest, obj: Any, template: str) -> No
messages.ERROR,
)

@button(permission=is_root)
@button(
permission=lambda request, obj, handler: (
is_root(request) or request.user.has_perm("api_token.resend_token_email")
)
)
def resend_email(self, request: HttpRequest, pk: "UUID") -> None:
obj = self.get_object(request, str(pk))
self._send_token_email(request, obj, TOKEN_INFO_EMAIL)
Expand Down
4 changes: 3 additions & 1 deletion src/hope/admin/business_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,9 @@ def _test_rapidpro_connection(self, request: HttpRequest, pk: "UUID") -> Templat

return TemplateResponse(request, "core/test_rapidpro.html", context)

@button(permission=is_root)
@button(
permission=lambda request, obj, handler: is_root(request) and request.user.has_perm("core.mark_submissions")
)
def mark_submissions(self, request: HttpRequest, pk: "UUID") -> HttpResponseBase | None:
business_area = self.get_queryset(request).get(pk=pk)
if request.method == "POST":
Expand Down
2 changes: 1 addition & 1 deletion src/hope/admin/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class GroupAdmin(ImportExportModelAdmin, SyncModelAdmin, HopeModelAdminMixin, _G
resource_class = GroupResource
change_list_template = "admin/account/group/change_list.html"

@button(permission=lambda request, group: request.user.is_superuser)
@button(permission="auth.change_group")
def import_fixture(self, request: HttpRequest) -> HttpResponseBase | None:
from adminactions.helpers import import_fixture as _import_fixture

Expand Down
26 changes: 19 additions & 7 deletions src/hope/admin/household.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _toggle_withdraw_status(
return service

def has_withdrawn_permission(self, request: HttpRequest) -> bool:
return request.user.has_perm("household.can_withdrawn")
return request.user.has_perm("household.withdrawn")

def mass_withdraw(self, request: HttpRequest, qs: QuerySet) -> TemplateResponse | None:
context = self.get_common_context(request, title="Withdrawn")
Expand Down Expand Up @@ -148,7 +148,7 @@ def mass_withdraw(self, request: HttpRequest, qs: QuerySet) -> TemplateResponse
)
return TemplateResponse(request, "admin/household/household/mass_withdrawn.html", context)

mass_withdraw.allowed_permissions = ["household.can_withdrawn"]
mass_withdraw.allowed_permissions = ["household.withdrawn"]

def mass_unwithdraw(self, request: HttpRequest, qs: QuerySet) -> TemplateResponse | None:
context = self.get_common_context(request, title="Restore")
Expand Down Expand Up @@ -188,7 +188,7 @@ def mass_unwithdraw(self, request: HttpRequest, qs: QuerySet) -> TemplateRespons

mass_withdraw.allowed_permissions = ["withdrawn"]

@button(permission="household.can_withdrawn")
@button(permission="household.withdrawn")
def withdraw(self, request: HttpRequest, pk: UUID) -> HttpResponseRedirect | TemplateResponse:
from hope.apps.grievance.models import GrievanceTicket

Expand Down Expand Up @@ -531,7 +531,9 @@ def members(self, request: HttpRequest, pk: UUID) -> HttpResponseRedirect:
flt = f"&qs=household_id={obj.id}"
return HttpResponseRedirect(f"{url}?{flt}")

@button(permission=is_root)
@button(
permission=lambda request, obj, handler: is_root(request) or request.user.has_perm("household.sanity_check")
)
def sanity_check(self, request: HttpRequest, pk: UUID) -> TemplateResponse:
# NOTE: this code should be optimized in the future, and it is not intended to be used in bulk
hh = self.get_object(request, str(pk))
Expand Down Expand Up @@ -584,7 +586,12 @@ def sanity_check(self, request: HttpRequest, pk: UUID) -> TemplateResponse:
}
return TemplateResponse(request, "admin/household/household/sanity_check.html", context)

@button(permission=lambda request, obj, handler: is_root(request, obj, handler) and obj.can_be_erase())
@button(
permission=lambda request, obj, handler: (
(is_root(request, obj, handler) and obj.can_be_erase())
or (request.user.has_perm("household.gdpr_remove") and obj.can_be_erase())
)
)
def gdpr_remove(self, request: HttpRequest, pk: UUID) -> HttpResponseBase | None:
household: Household = cast("Household", self.get_queryset(request).get(pk=pk))
if request.method == "POST":
Expand All @@ -610,7 +617,12 @@ def gdpr_remove(self, request: HttpRequest, pk: UUID) -> HttpResponseBase | None
"Successfully executed",
)

@button(permission=lambda request, household, *args, **kwargs: is_root(request) and not household.is_removed)
@button(
permission=lambda request, household, *args, **kwargs: (
(is_root(request) and not household.is_removed)
or (request.user.has_perm("household.logical_delete") and not household.is_removed)
)
)
def logical_delete(self, request: HttpRequest, pk: UUID) -> HttpResponseBase | None:
household: Household = cast("Household", self.get_queryset(request).get(pk=pk))
if request.method == "POST":
Expand Down Expand Up @@ -675,7 +687,7 @@ def mass_enroll_to_another_program(self, request: HttpRequest, qs: QuerySet) ->

@button(
label="Withdraw households from list",
permission="household.can_withdrawn",
permission="household.withdrawn",
)
def withdraw_households_from_list_button(self, request: HttpRequest) -> HttpResponse | None:
return self.withdraw_households_from_list(request)
Expand Down
7 changes: 6 additions & 1 deletion src/hope/admin/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,12 @@ def household_members(self, request: HttpRequest, pk: UUID) -> HttpResponseRedir
flt = f"&qs=household_id={obj.household.id}&qs__negate=false"
return HttpResponseRedirect(f"{url}?{flt}")

@button(html_attrs={"class": "aeb-green"}, permission=is_root)
@button(
html_attrs={"class": "aeb-green"},
permission=lambda request, obj, handler: (
is_root(request) or request.user.has_perm("household.individual_sanity_check")
),
)
def sanity_check(self, request: HttpRequest, pk: UUID) -> TemplateResponse:
context = self.get_common_context(request, pk, title="Sanity Check")
obj = context["original"]
Expand Down
8 changes: 7 additions & 1 deletion src/hope/admin/payment_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def has_delete_permission(self, request: HttpRequest, obj: Any | None = None) ->

@button(
visible=lambda btn: btn.original.status == PaymentPlan.Status.ACCEPTED,
permission="payment.can_recalculate_exchange_rate",
permission="payment.recalculate_exchange_rate",
)
def recalculate_exchange_rate(self, request: HttpRequest, pk: "UUID") -> HttpResponse:
if request.method == "POST":
Expand Down Expand Up @@ -246,6 +246,12 @@ def related_configs(self, request: HttpRequest, pk: "UUID") -> HttpResponse:
flt = f"delivery_mechanism__exact={obj.delivery_mechanism.id}&fsp__exact={obj.financial_service_provider.id}"
return HttpResponseRedirect(f"{url}?{flt}")

@button(permission="payment.view_paymentplan")
def payment_records(self, request: HttpRequest, pk: "UUID") -> HttpResponse:
url = reverse("admin:payment_payment_changelist")
filter_by_parent = f"&parent__exact={str(pk)}"
return HttpResponseRedirect(f"{url}?{filter_by_parent}")


class PaymentHouseholdSnapshotInline(admin.StackedInline):
model = PaymentHouseholdSnapshot
Expand Down
2 changes: 1 addition & 1 deletion src/hope/admin/periodic_data_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def celery_task_result_id(self, obj: PDUXlsxTemplate) -> str:

@button(
visible=lambda btn: btn.original.status == PDUXlsxTemplate.Status.FAILED,
permission=lambda request, obj, handler: request.user.is_superuser,
permission="pdu_xlsx_template.restart_export_task",
)
def restart_export_task(self, request: HttpRequest, pk: "UUID") -> HttpResponse:
if request.method == "POST":
Expand Down
9 changes: 7 additions & 2 deletions src/hope/admin/registration_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def fetch_biometric_deduplication_results_visible(rdi: RegistrationDataImport) -
@button(
label="Fetch Biometric Deduplication Results",
visible=lambda btn: RegistrationDataImportAdmin.fetch_biometric_deduplication_results_visible(btn.original),
permission="registration_data.fetch_biometric_deduplication_results",
)
def fetch_biometric_deduplication_results(self, request: HttpRequest, pk: str) -> HttpResponse | None:
rdi = self.get_object(request, pk)
Expand Down Expand Up @@ -206,7 +207,9 @@ def _delete_rdi(rdi: RegistrationDataImport) -> None:
)

@button(
permission=is_root,
permission=lambda request, obj, handler: (
is_root(request) or request.user.has_perm("registration_data.delete_rdi")
),
enabled=lambda btn: btn.original.status not in [RegistrationDataImport.MERGED, RegistrationDataImport.MERGING],
)
def delete_rdi(self, request: HttpRequest, pk: str) -> Any: # TODO: typing
Expand Down Expand Up @@ -290,7 +293,9 @@ def _delete_merged_rdi(rdi: RegistrationDataImport) -> None:
remove_elasticsearch_documents_by_matching_ids(household_ids, get_household_doc(str(rdi.program.id)))

@button(
permission=is_root,
permission=lambda request, obj, handler: (
is_root(request) or request.user.has_perm("registration_data.delete_merged_rdi")
),
visible=lambda btn: RegistrationDataImportAdmin.delete_merged_rdi_visible(btn.original),
)
def delete_merged_rdi(self, request: HttpRequest, pk: str) -> HttpResponse | None:
Expand Down
4 changes: 2 additions & 2 deletions src/hope/admin/sanction_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class SanctionListAdmin(HOPEModelAdminBase):
list_display = ("name",)

@button()
@button(permission="sanction_list.refresh_sanction_list")
def refresh(self, request: HttpRequest, pk: str) -> None:
try:
sl = SanctionList.objects.get(pk=pk)
Expand All @@ -19,7 +19,7 @@ def refresh(self, request: HttpRequest, pk: str) -> None:
except KeyError:
self.message_user(request, "Configuration Problem", messages.ERROR)

@button()
@button(permission="sanction_list.empty_sanction_list")
def empty(self, request: HttpRequest, pk: str) -> HttpResponse:
sl = SanctionList.objects.get(pk=pk)

Expand Down
2 changes: 1 addition & 1 deletion src/hope/admin/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ class UserAdmin(HopeModelAdminMixin, UserAdminPlus, ADUSerMixin):
def get_inline_instances(self, request: HttpRequest, obj: Any = None) -> list:
return super().get_inline_instances(request, obj) if obj else []

@button(permissions=is_root)
@button(permission=lambda request, obj, handler: is_root(request) or request.user.has_perm("account.ad_users"))
def ad(self, request: HttpRequest, pk: "UUID") -> TemplateResponse:
obj = self.get_object(request, str(pk))
context = dict
Expand Down
23 changes: 19 additions & 4 deletions src/hope/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ def formfield_for_dbfield(self, db_field: Any, request: HttpRequest, **kwargs: A


class LastSyncDateResetMixin:
@button(permission=is_root)
@button(
permission=lambda request, obj, handler: (
is_root(request) or (obj is not None and request.user.has_perm(f"{obj._meta.app_label}.reset_sync_date"))
)
)
def reset_sync_date(self, request: HttpRequest) -> HttpResponse | None:
if request.method == "POST":
self.get_queryset(request).update(last_sync_at=None)
Expand All @@ -72,7 +76,12 @@ def reset_sync_date(self, request: HttpRequest) -> HttpResponse | None:
)
return None

@button(label="reset sync date", permission=is_root)
@button(
label="reset sync date",
permission=lambda request, obj, handler: (
is_root(request) or (obj is not None and request.user.has_perm(f"{obj._meta.app_label}.reset_sync_date"))
),
)
def reset_sync_date_single(self, request: HttpRequest, pk: UUID) -> HttpResponse | None:
if request.method == "POST":
self.get_queryset(request).filter(id=pk).update(last_sync_at=None)
Expand Down Expand Up @@ -219,7 +228,9 @@ def _terminate_active_payment_plan_jobs(self, payment_plan: PaymentPlan, task_na
@button(
visible=is_preparing_payment_plan,
enabled=is_enabled,
permission=is_root,
permission=lambda request, obj, handler: (
is_root(request) or request.user.has_perm("payment.restart_preparing_payment_plan")
),
)
def restart_preparing_payment_plan(self, request: HttpRequest, pk: str) -> HttpResponse | None:
"""Prepare Payment Plan."""
Expand Down Expand Up @@ -267,6 +278,7 @@ def restart_preparing_payment_plan(self, request: HttpRequest, pk: str) -> HttpR
@button(
visible=lambda btn: is_exporting_xlsx_file(btn) and is_locked_payment_plan(btn),
enabled=is_enabled,
permission="payment.restart_exporting_template_for_entitlement",
)
def restart_exporting_template_for_entitlement(self, request: HttpRequest, pk: str) -> HttpResponse | None:
"""Export template for entitlement."""
Expand Down Expand Up @@ -296,6 +308,7 @@ def restart_exporting_template_for_entitlement(self, request: HttpRequest, pk: s
@button(
visible=lambda btn: is_importing_entitlements_xlsx_file(btn) and is_locked_payment_plan(btn),
enabled=is_enabled,
permission="payment.restart_importing_entitlements_xlsx_file",
)
def restart_importing_entitlements_xlsx_file(self, request: HttpRequest, pk: str) -> HttpResponse | None:
"""Import entitlement file."""
Expand Down Expand Up @@ -328,6 +341,7 @@ def restart_importing_entitlements_xlsx_file(self, request: HttpRequest, pk: str
@button(
visible=lambda btn: is_exporting_xlsx_file(btn) and is_accepted_payment_plan(btn),
enabled=is_enabled,
permission="payment.restart_exporting_payment_plan_list",
)
def restart_exporting_payment_plan_list(self, request: HttpRequest, pk: str) -> HttpResponse | None:
"""Export payment plan list."""
Expand Down Expand Up @@ -359,6 +373,7 @@ def restart_exporting_payment_plan_list(self, request: HttpRequest, pk: str) ->
@button(
visible=lambda btn: is_importing_reconciliation_xlsx_file(btn) and is_accepted_payment_plan(btn),
enabled=is_enabled,
permission="payment.restart_importing_reconciliation_xlsx_file",
)
def restart_importing_reconciliation_xlsx_file(self, request: HttpRequest, pk: str) -> HttpResponse | None:
"""Import payment plan list (from xlsx)."""
Expand Down Expand Up @@ -408,7 +423,7 @@ class LinkedObjectsManagerMixin:
def get_ignored_linked_objects(self, request: HttpRequest) -> list[str]:
return self.linked_objects_ignore

@button()
@button(permission=lambda obj: f"{obj._meta.app_label}.see_linked_objects")
def linked_objects(self, request: HttpRequest, pk: int) -> TemplateResponse:
ignored = self.get_ignored_linked_objects(request)
opts = self.model._meta
Expand Down
19 changes: 19 additions & 0 deletions src/hope/api/migrations/0006_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-04-15 12:20

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("api", "0005_migration"),
]

operations = [
migrations.AlterModelOptions(
name="apitoken",
options={
"ordering": ("id",),
"permissions": (("resend_token_email", "Can resend an email with token"),),
},
),
]
32 changes: 32 additions & 0 deletions src/hope/apps/account/migrations/0029_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.2.12 on 2026-04-14 21:18

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("account", "0028_migration"),
]

operations = [
migrations.AlterModelOptions(
name="user",
options={
"ordering": ("id",),
"permissions": (
("can_load_from_ad", "Can load users from ActiveDirectory"),
("can_sync_with_ad", "Can synchronise user with ActiveDirectory"),
("can_debug", "Can access debug information"),
("can_inspect", "Can inspect objects"),
("quick_links", "Can see quick links in admin"),
("restrict_help_desk", "Limit fields to be editable for help desk"),
("can_reindex_programs", "Can reindex programs"),
("can_add_business_area_to_partner", "Can add business area to partner"),
("can_change_allowed_partners", "Can change allowed partners"),
("can_change_area_limits", "Can change area limits"),
("can_import_fixture", "Can import fixture"),
("ad_users", "Can import AD users"),
),
},
),
]
25 changes: 25 additions & 0 deletions src/hope/apps/core/migrations/0022_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.2.12 on 2026-04-15 12:20

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("core", "0021_migration"),
]

operations = [
migrations.AlterModelOptions(
name="businessarea",
options={
"ordering": ["name"],
"permissions": (
("can_split", "Can split BusinessArea"),
("ping_rapidpro", "Can test RapidPRO connection"),
("execute_sync_rapid_pro", "Can execute RapidPRO sync"),
("mark_submissions", "Can mark submissions"),
("reset_sync_date", "Can reset sync date"),
),
},
),
]
Loading
Loading