Skip to content
Merged
Show file tree
Hide file tree
Changes from 95 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
900aeb9
account, accountability, core
Mar 12, 2026
56c9a05
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 16, 2026
11e8c78
generic import, cov
Mar 16, 2026
c44fad9
Merge branch '207255_Celery_tasks_refactor' of github.com:unicef/hope…
Mar 16, 2026
451ce51
geo
Mar 16, 2026
7a1d54d
grievance
Mar 16, 2026
a9bece9
cov
Mar 17, 2026
5671f78
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 17, 2026
223d4fe
household
Mar 17, 2026
b45077e
fix group_key
Mar 17, 2026
c27c0e6
Merge branch '207255_Celery_tasks_refactor' of github.com:unicef/hope…
Mar 17, 2026
6944208
ut
Mar 17, 2026
1e58257
ut
Mar 17, 2026
9309cf9
ut
Mar 17, 2026
4e79125
cov
Mar 17, 2026
5f4cfaa
payment app, generic task jobs refactor
Mar 19, 2026
2474787
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 19, 2026
14d575f
ut
Mar 19, 2026
7c72cd9
cov
Mar 19, 2026
aacf455
refactor
Mar 20, 2026
454e10a
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Mar 24, 2026
47d1b19
conflicts
Mar 24, 2026
4ae7c01
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 24, 2026
42f6f09
207255_Celery_tasks_refactor_2
Mar 25, 2026
c8a6b6c
Merge branch '207255_Celery_tasks_refactor' into 207255_Celery_tasks_…
MarekBiczysko Mar 25, 2026
0387204
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 25, 2026
df14bbb
ut
Mar 25, 2026
c6e0e47
Merge branch '207255_Celery_tasks_refactor' of github.com:unicef/hope…
Mar 25, 2026
8f3e8af
ut
Mar 25, 2026
21941b6
recover_missing_async_jobs_task
Mar 29, 2026
841e69c
DEFAULT_RECOVER_MISSING_ASYNC_JOBS_LIMIT
Mar 29, 2026
9846f2b
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Mar 30, 2026
7961c78
ut
Mar 30, 2026
a8aaa60
cov
Mar 30, 2026
a1fff80
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Mar 30, 2026
74f3d27
lint
Mar 30, 2026
d0b438d
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Mar 30, 2026
46194c9
remove registration_data revoke tasks, remove univeral update celey m…
Mar 30, 2026
b279496
fix universal update admin
Mar 30, 2026
a680852
fill async job program
Mar 30, 2026
b22044d
refactor pdu app
Mar 31, 2026
dd47c61
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Mar 31, 2026
f363e1f
remove pragma
Mar 31, 2026
baf0e22
Merge branch '207255_Celery_tasks_refactor' of github.com:unicef/hope…
Mar 31, 2026
9c4d77e
remove pragma
Mar 31, 2026
d688683
migration
Mar 31, 2026
f5a60ba
pdu fixes
Mar 31, 2026
182a29c
ut
Mar 31, 2026
0a210e8
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Apr 1, 2026
4a77d7f
fix timeout envs
Apr 2, 2026
5b6277a
remove outer layer celery tasks
Apr 3, 2026
2451f0b
ut fix
Apr 3, 2026
190b7fd
ut fix
Apr 7, 2026
7e1c9d5
review fixes
Apr 7, 2026
e1c2f2d
Merge branch '207255_Celery_tasks_refactor' of github.com:unicef/hope…
Apr 7, 2026
765b466
review fixes
Apr 7, 2026
3cdf63f
review fixes
Apr 7, 2026
5e3f9d0
review fixes
Apr 7, 2026
4ca8d62
review fixes
Apr 7, 2026
1ea4ebc
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Apr 7, 2026
ce9f054
ut
Apr 7, 2026
96a0cbc
ut
Apr 7, 2026
50e6cbb
ut
Apr 8, 2026
81843eb
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Apr 8, 2026
25457a0
ut
Apr 8, 2026
63e4965
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Apr 8, 2026
f01f4c7
ut
Apr 8, 2026
ac7816c
ut
Apr 8, 2026
e58004d
ut
Apr 8, 2026
9a77f46
ut
Apr 9, 2026
d3003a1
ut
Apr 9, 2026
22fb74a
ut
Apr 9, 2026
56af457
ut
Apr 9, 2026
c9e8fba
cov, e2e
Apr 9, 2026
da9f028
cov
Apr 9, 2026
cdc082d
cleanup
Apr 9, 2026
9f3cddd
cleanup
Apr 9, 2026
5678c3d
cleanup
Apr 9, 2026
8a0cffd
cleanup
Apr 9, 2026
5cc90ed
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Apr 9, 2026
e46a236
ut
Apr 9, 2026
67fe048
cov
Apr 9, 2026
c6f760d
cov
Apr 9, 2026
cb883f2
e2e
Apr 9, 2026
5b3877f
cov
Apr 10, 2026
1a1f2ba
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Apr 10, 2026
406edd3
ut, admin
Apr 10, 2026
8f59242
ut
Apr 10, 2026
baa51b5
cov
Apr 10, 2026
f188109
e2e
Apr 10, 2026
0e40658
Merge remote-tracking branch 'origin/develop' into 207255_Celery_task…
Apr 10, 2026
b0ebaad
merge fix
Apr 10, 2026
c6e03db
merge fix
Apr 10, 2026
c2b1453
mypy
Apr 10, 2026
22c3382
ut
Apr 10, 2026
ba2e28c
fix aurora schedule task names
Apr 13, 2026
57072e9
Merge branch 'develop' into 207255_Celery_tasks_refactor
MarekBiczysko Apr 13, 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
193 changes: 193 additions & 0 deletions src/hope/admin/async_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from typing import Any, cast

from adminfilters.autocomplete import AutoCompleteFilter
from django.contrib import admin
from django.db.models import QuerySet
from django.http import HttpRequest

from hope.admin.utils import HOPEModelAdminBase
from hope.models import AsyncJob


class HasErrorsListFilter(admin.SimpleListFilter):
title = "has errors"
parameter_name = "has_errors"

def lookups(
self, request: HttpRequest, model_admin: admin.ModelAdmin[Any]
) -> tuple[tuple[str, str], tuple[str, str]]:
return (
("yes", "Yes"),
("no", "No"),
)

def queryset(self, request: HttpRequest, queryset: QuerySet[AsyncJob]) -> QuerySet[AsyncJob]:
if self.value() == "yes":
return queryset.exclude(errors={})
if self.value() == "no":
return queryset.filter(errors={})
return queryset


@admin.register(AsyncJob)
class AsyncJobAdmin(HOPEModelAdminBase):
list_display = (
"id",
"job_name",
"type",
"task_status_display",
"local_status",
"program",
"gfk_display",
"curr_async_result_id",
"repeatable",
"datetime_created",
"datetime_queued",
)
list_filter = (
("program", AutoCompleteFilter),
("content_type", AutoCompleteFilter),
("job_name", AutoCompleteFilter),
HasErrorsListFilter,
"type",
"repeatable",
"local_status",
)
search_fields = (
"job_name",
"action",
"program__id",
"curr_async_result_id",
"last_async_result_id",
"object_id",
)
readonly_fields = (
"task_status_display",
"content_object_display",
"id",
"version",
"description",
"curr_async_result_id",
"last_async_result_id",
"datetime_created",
"datetime_queued",
"repeatable",
"celery_history",
"local_status",
"owner",
"group_key",
"type",
"config",
"action",
"sentry_id",
"errors",
"program",
"content_type",
"object_id",
"job_name",
)
ordering = ("-datetime_created",)

fieldsets = (
(
None,
{
"fields": (
"id",
"job_name",
"type",
"task_status_display",
"local_status",
"description",
"action",
"config",
"errors",
)
},
),
(
"Relations",
{
"fields": (
"program",
"owner",
"content_type",
"object_id",
"content_object_display",
)
},
),
(
"Execution",
{
"fields": (
"repeatable",
"group_key",
"curr_async_result_id",
"last_async_result_id",
"datetime_created",
"datetime_queued",
"sentry_id",
"celery_history",
"version",
)
},
),
)

@admin.display(description="Task status")
def task_status_display(self, obj: AsyncJob) -> str:
return obj.task_status

@admin.display(description="GFK")
def gfk_display(self, obj: AsyncJob) -> str:
if not obj.content_type_id or not obj.object_id:
return "-"
return f"{obj.content_type}:{obj.object_id}"

@admin.display(description="Content object")
def content_object_display(self, obj: AsyncJob) -> str:
return str(obj.content_object) if obj.content_object else "-"

def get_queryset(self, request: HttpRequest) -> QuerySet[AsyncJob]:
return cast(
"QuerySet[AsyncJob]",
super()
.get_queryset(request)
.select_related("program", "owner", "content_type")
.only(
"id",
"job_name",
"type",
"local_status",
"program",
"program__id",
"program__name",
"content_type",
"content_type__app_label",
"content_type__model",
"object_id",
"curr_async_result_id",
"repeatable",
"datetime_created",
"datetime_queued",
"description",
"action",
"config",
"errors",
"version",
"last_async_result_id",
"celery_history",
"owner",
"owner__id",
"owner__username",
"group_key",
"sentry_id",
),
)

def has_add_permission(self, request: HttpRequest) -> bool:
return False

def has_delete_permission(self, request: HttpRequest, obj: Any | None = None) -> bool:
return False
4 changes: 2 additions & 2 deletions src/hope/admin/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from smart_admin.mixins import FieldsetMixin

from hope.admin.utils import HOPEModelAdminBase
from hope.apps.geo.celery_tasks import import_areas_from_csv_task
from hope.apps.geo.celery_tasks import import_areas_from_csv_async_task
from hope.models import Area, AreaType, Country

if TYPE_CHECKING:
Expand Down Expand Up @@ -255,7 +255,7 @@ def import_areas(
existing_p_codes = set(Area.objects.filter(p_code__in=all_p_codes).values_list("p_code", flat=True))
new_areas_count = len(all_p_codes - existing_p_codes)

import_areas_from_csv_task.delay(data_set, delay_mptt_updates)
import_areas_from_csv_async_task(data_set, delay_mptt_updates)

self.message_user(
request,
Expand Down
46 changes: 25 additions & 21 deletions src/hope/admin/household.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.contrib.messages import DEFAULT_TAGS
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import F, Q, QuerySet, Value
from django.db.models import Case, F, Q, QuerySet, Value, When
from django.db.transaction import atomic
from django.forms import Form
from django.http import HttpRequest, HttpResponse, HttpResponseBase, HttpResponseRedirect
Expand All @@ -37,8 +37,8 @@
from hope.apps.grievance.signals import increment_grievance_ticket_version_cache_for_ticket_ids
from hope.apps.household.api.caches import invalidate_household_and_individual_list_cache
from hope.apps.household.celery_tasks import (
enroll_households_to_program_task,
mass_withdraw_households_from_list_task,
enroll_households_to_program_async_task,
mass_withdraw_households_from_list_async_task,
)
from hope.apps.household.forms import (
MassEnrollForm,
Expand Down Expand Up @@ -244,31 +244,35 @@ def has_add_permission(self, request: HttpRequest, obj: Household | None = None)

class HouseholdWithdrawFromListMixin:
@staticmethod
def get_household_queryset_from_list(household_id_list: list, program: Program) -> QuerySet:
def get_household_queryset_from_list(household_id_list: list[str], program: Program) -> QuerySet:
return Household.objects.filter(
unicef_id__in=household_id_list,
withdrawn=False,
program=program,
)

@transaction.atomic
def mass_withdraw_households_from_list_bulk(self, household_id_list: list, tag: str, program: Program) -> None:
def mass_withdraw_households_from_list_bulk(self, household_id_list: list[str], tag: str, program: Program) -> None:
households = self.get_household_queryset_from_list(household_id_list, program)
individuals = Individual.objects.filter(household__in=households, withdrawn=False, duplicate=False)

tickets = GrievanceTicket.objects.belong_households_individuals(households, individuals)
Comment thread
MarekBiczysko marked this conversation as resolved.
ticket_ids = [str(t.ticket.id) for t in tickets]
for status, _ in GrievanceTicket.STATUS_CHOICES:
if status == GrievanceTicket.STATUS_CLOSED:
continue
GrievanceTicket.objects.filter(id__in=ticket_ids, status=status).update(
extras=JSONBSet(
F("extras"),
Value("{status_before_withdrawn}"),
Value(f'"{status}"'),
),
status=GrievanceTicket.STATUS_CLOSED,
)
ticket_ids = list({t.ticket_id for t in tickets})
previous_status = Case(
*[
When(status=status, then=Value(f'"{status}"'))
for status, _ in GrievanceTicket.STATUS_CHOICES
if status != GrievanceTicket.STATUS_CLOSED
]
)
GrievanceTicket.objects.filter(id__in=ticket_ids).exclude(status=GrievanceTicket.STATUS_CLOSED).update(
extras=JSONBSet(
F("extras"),
Value("{status_before_withdrawn}"),
previous_status,
),
status=GrievanceTicket.STATUS_CLOSED,
)
increment_grievance_ticket_version_cache_for_ticket_ids(program.business_area.slug, ticket_ids)

Document.objects.filter(individual__in=individuals).update(status=Document.STATUS_INVALID)
Expand Down Expand Up @@ -350,7 +354,7 @@ def withdraw_households_from_list(self, request: HttpRequest) -> HttpResponse |
)

if step == "3":
mass_withdraw_households_from_list_task.delay(household_id_list, tag, str(program.id))
mass_withdraw_households_from_list_async_task(household_id_list, tag, program)
self.message_user(request, f"{len(household_id_list)} Households are being withdrawn.")
return HttpResponseRedirect(reverse("admin:household_household_changelist"))
return TemplateResponse(
Expand Down Expand Up @@ -639,9 +643,9 @@ def mass_enroll_to_another_program(self, request: HttpRequest, qs: QuerySet) ->
if form.is_valid():
program_for_enroll = form.cleaned_data["program_for_enroll"]
households_ids = list(qs.distinct("unicef_id").values_list("id", flat=True))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we do no pass the unicef_id to the task? we could save a query and a cast iteration

enroll_households_to_program_task.delay(
enroll_households_to_program_async_task(
households_ids=households_ids,
program_for_enroll_id=str(program_for_enroll.id),
program_for_enroll_id=program_for_enroll,
user_id=str(request.user.id),
)
self.message_user(
Expand All @@ -663,7 +667,7 @@ def mass_enroll_to_another_program(self, request: HttpRequest, qs: QuerySet) ->
context["action"] = "mass_enroll_to_another_program"
return TemplateResponse(
request,
"admin/household/household/enroll_households_to_program.html",
"admin/household/household/enroll_households_to_program_async_task.html",
context,
)

Expand Down
9 changes: 4 additions & 5 deletions src/hope/admin/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
RdiMergeStatusAdminMixin,
SoftDeletableAdminMixin,
)
from hope.apps.household.celery_tasks import revalidate_phone_number_task
from hope.apps.household.celery_tasks import revalidate_phone_number_async_task
from hope.apps.utils.security import is_root
from hope.models import (
Account,
Expand Down Expand Up @@ -227,17 +227,16 @@ def sanity_check(self, request: HttpRequest, pk: UUID) -> TemplateResponse:

def revalidate_phone_number_sync(self, request: HttpRequest, queryset: QuerySet) -> None:
try:
ids = queryset.values_list("id", flat=True)
revalidate_phone_number_task(ids)
ids = list(queryset.values_list("id", flat=True))
revalidate_phone_number_async_task(ids)
self.message_user(request, f"Updated {len(ids)} records", messages.SUCCESS)
except Error as e:
self.message_user(request, str(e), messages.ERROR)

revalidate_phone_number_sync.short_description = "Re-validate phone number (sync)"

def revalidate_phone_number_async(self, request: HttpRequest, queryset: QuerySet) -> None:
ids = list(queryset.values_list("id", flat=True))
revalidate_phone_number_task.delay(ids)
revalidate_phone_number_async_task(list(queryset.values_list("id", flat=True)))
self.message_user(request, "Updating in progress", messages.SUCCESS)

revalidate_phone_number_async.short_description = "Re-validate phone number (async)"
Expand Down
11 changes: 4 additions & 7 deletions src/hope/admin/kobo_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.html import format_html
from openpyxl import load_workbook

from hope.admin.utils import HOPEModelAdminBase, SoftDeletableAdminMixin
from hope.apps.core.celery_tasks import (
upload_new_kobo_template_and_update_flex_fields_task,
upload_new_kobo_template_and_update_flex_fields_async_task,
)
from hope.apps.core.validators import KoboTemplateValidator
from hope.models import XLSXKoboTemplate
Expand Down Expand Up @@ -101,10 +101,7 @@ def download_last_valid_file(
permission="core.rerun_kobo_import",
)
def rerun_kobo_import(self, request: HttpRequest, pk: "UUID") -> HttpResponseBase | None:
xlsx_kobo_template_object = get_object_or_404(XLSXKoboTemplate, pk=pk)
upload_new_kobo_template_and_update_flex_fields_task.run(
xlsx_kobo_template_id=str(xlsx_kobo_template_object.id)
)
upload_new_kobo_template_and_update_flex_fields_async_task(xlsx_kobo_template_id=str(pk))
return redirect(".")

def add_view(
Expand Down Expand Up @@ -171,7 +168,7 @@ def add_view(
"Core field validation successful, running KoBo Template upload task..., "
"Import status will change after task completion",
)
upload_new_kobo_template_and_update_flex_fields_task.run(
upload_new_kobo_template_and_update_flex_fields_async_task(
xlsx_kobo_template_id=str(xlsx_kobo_template_object.id)
)
return redirect("..")
Expand Down
4 changes: 2 additions & 2 deletions src/hope/admin/payment_plan.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any

from admin_cursor_paginator import CursorPaginatorAdmin
from admin_extra_buttons.decorators import button
Expand Down Expand Up @@ -222,7 +222,7 @@ def regenerate_export_xlsx(self, request: HttpRequest, pk: "UUID") -> HttpRespon
template_obj = form.cleaned_data.get("template")
fsp_xlsx_template_id = str(template_obj.id) if template_obj else None
PaymentPlanService(payment_plan=payment_plan).export_xlsx_per_fsp(
cast("UUID", request.user.pk), fsp_xlsx_template_id
str(request.user.pk), fsp_xlsx_template_id
)
messages.success(request, "Celery task for export regenerate file successfully started.")
return redirect(reverse("admin:payment_paymentplan_change", args=[pk]))
Expand Down
Loading
Loading