diff --git a/src/country_workspace/admin/batch.py b/src/country_workspace/admin/batch.py index c361707..54b5c8b 100644 --- a/src/country_workspace/admin/batch.py +++ b/src/country_workspace/admin/batch.py @@ -3,6 +3,7 @@ from admin_extra_buttons.buttons import LinkButton from admin_extra_buttons.decorators import link +from adminfilters.autocomplete import AutoCompleteFilter, LinkedAutoCompleteFilter from ..models import Batch from .base import BaseModelAdmin @@ -10,12 +11,14 @@ @admin.register(Batch) class BatchAdmin(BaseModelAdmin): - list_display = ("name", "import_date", "imported_by", "program") + list_display = ("name", "import_date", "imported_by", "program", "source") list_filter = ( - "country_office", - "program", - # ("country_office", LinkedAutoCompleteFilter.factory(parent=None)), - # ("program", LinkedAutoCompleteFilter.factory(parent="country_office")), + # "country_office", + # "program", + ("country_office", LinkedAutoCompleteFilter.factory(parent=None)), + ("program", LinkedAutoCompleteFilter.factory(parent="country_office")), + ("imported_by", AutoCompleteFilter), + "source", ) readonly_fields = ("country_office", "program", "imported_by") search_fields = ("name",) diff --git a/src/country_workspace/admin/filters.py b/src/country_workspace/admin/filters.py new file mode 100644 index 0000000..ca87e3a --- /dev/null +++ b/src/country_workspace/admin/filters.py @@ -0,0 +1,74 @@ +from django.contrib.admin import SimpleListFilter +from django.utils.translation import gettext as _ + + +class FailedFilter(SimpleListFilter): + title = "Status" + parameter_name = "failed" + + def lookups(self, request, model_admin): + return ( + ("f", _("Failed")), + ("s", _("Success")), + ) + + def get_title(self): + return self.title + + def queryset(self, request, queryset): + if self.value() == "s": + return queryset.filter(sentry_id__isnull=True) + elif self.value() == "f": + return queryset.filter(sentry_id__isnull=False) + return queryset + + def has_output(self): + return True + + def html_attrs(self): + classes = f"adminfilters {self.__class__.__name__.lower()}" + if self.value(): + classes += " active" + + return { + "class": classes, + "id": "_".join(self.expected_parameters()), + } + + +class IsValidFilter(SimpleListFilter): + title = "Valid" + parameter_name = "valid" + # template = "workspace/adminfilters/combobox.html" + + def lookups(self, request, model_admin): + return ( + ("v", _("Valid")), + ("i", _("Invalid")), + ("u", _("Not Verified")), + ) + + def get_title(self): + return self.title + + def queryset(self, request, queryset): + if self.value() == "v": + return queryset.filter(last_checked__isnull=False).filter(errors__iexact="{}") + elif self.value() == "i": + return queryset.filter(last_checked__isnull=False).exclude(errors__iexact="{}") + elif self.value() == "u": + return queryset.filter(last_checked__isnull=True) + return queryset + + def has_output(self): + return True + + def html_attrs(self): + classes = f"adminfilters {self.__class__.__name__.lower()}" + if self.value(): + classes += " active" + + return { + "class": classes, + "id": "_".join(self.expected_parameters()), + } diff --git a/src/country_workspace/admin/household.py b/src/country_workspace/admin/household.py index 09059af..8cb598a 100644 --- a/src/country_workspace/admin/household.py +++ b/src/country_workspace/admin/household.py @@ -7,6 +7,7 @@ from ..models import Household from .base import BaseModelAdmin +from .filters import IsValidFilter @admin.register(Household) @@ -16,6 +17,7 @@ class HouseholdAdmin(BaseModelAdmin): ("batch__country_office", LinkedAutoCompleteFilter.factory(parent=None)), ("batch__program", LinkedAutoCompleteFilter.factory(parent="batch__country_office")), ("batch", LinkedAutoCompleteFilter.factory(parent="batch__program")), + IsValidFilter, ) # readonly_fields = ("country_office", "program") search_fields = ("name",) diff --git a/src/country_workspace/admin/individual.py b/src/country_workspace/admin/individual.py index 3fca53d..d5a8c92 100644 --- a/src/country_workspace/admin/individual.py +++ b/src/country_workspace/admin/individual.py @@ -7,6 +7,7 @@ from ..models import Individual from .base import BaseModelAdmin +from .filters import IsValidFilter @admin.register(Individual) @@ -17,6 +18,8 @@ class IndividualAdmin(BaseModelAdmin): list_filter = ( ("batch__country_office", LinkedAutoCompleteFilter.factory(parent=None)), ("batch__program", LinkedAutoCompleteFilter.factory(parent="batch__country_office")), + ("batch", LinkedAutoCompleteFilter.factory(parent="batch__program")), + IsValidFilter, ) autocomplete_fields = ("batch",) diff --git a/src/country_workspace/admin/job.py b/src/country_workspace/admin/job.py index c17159d..0e389d9 100644 --- a/src/country_workspace/admin/job.py +++ b/src/country_workspace/admin/job.py @@ -1,59 +1,25 @@ from typing import Optional, Sequence from django.contrib import admin -from django.contrib.admin import SimpleListFilter from django.http import HttpRequest -from django.utils.translation import gettext as _ from adminfilters.autocomplete import AutoCompleteFilter, LinkedAutoCompleteFilter from django_celery_boost.admin import CeleryTaskModelAdmin from ..models import AsyncJob from .base import BaseModelAdmin - - -class FailedFilter(SimpleListFilter): - title = "Status" - parameter_name = "failed" - - def lookups(self, request, model_admin): - return ( - ("f", _("Failed")), - ("s", _("Success")), - ) - - def get_title(self): - return self.title - - def queryset(self, request, queryset): - if self.value() == "s": - return queryset.filter(sentry_id__isnull=True) - elif self.value() == "f": - return queryset.filter(sentry_id__isnull=False) - return queryset - - def has_output(self): - return True - - def html_attrs(self): - classes = f"adminfilters {self.__class__.__name__.lower()}" - if self.value(): - classes += " active" - - return { - "class": classes, - "id": "_".join(self.expected_parameters()), - } +from .filters import FailedFilter @admin.register(AsyncJob) class AsyncJobAdmin(CeleryTaskModelAdmin, BaseModelAdmin): - list_display = ("program", "type", "verbose_status") + list_display = ("program", "type", "verbose_status", "owner") autocomplete_fields = ("program", "owner") list_filter = ( ("program__country_office", LinkedAutoCompleteFilter.factory(parent=None)), ("program", LinkedAutoCompleteFilter.factory(parent="program__country_office")), ("owner", AutoCompleteFilter), + "type", FailedFilter, ) diff --git a/src/country_workspace/contrib/aurora/sync.py b/src/country_workspace/contrib/aurora/sync.py index 5dd6ed0..1852609 100644 --- a/src/country_workspace/contrib/aurora/sync.py +++ b/src/country_workspace/contrib/aurora/sync.py @@ -17,39 +17,29 @@ def sync_aurora_job(job: AsyncJob) -> dict[str, int]: Returns: dict[str, int]: A dictionary with counts of households and individuals created. """ + batch_name = job.config["batch_name"] + batch = Batch.objects.create( + name=batch_name, + program=job.program, + country_office=job.program.country_office, + imported_by=job.owner, + source=Batch.BatchSource.RDI, + ) total_hh = total_ind = 0 client = AuroraClient() with atomic(): for record in client.get("record"): for f_name, f_value in record["fields"].items(): if f_name == "household": - hh = _create_household(job, f_value[0]) + hh = _create_household(batch, f_value[0]) total_hh += 1 elif f_name == "individuals": - total_ind += len(_create_individuals(job, hh, f_value)) + total_ind += len(_create_individuals(hh, f_value, job.config.get("household_name_column", None))) return {"households": total_hh, "individuals": total_ind} -def _create_batch(job: AsyncJob) -> Batch: - """ - Creates a batch entity associated with the given job. - - Args: - job (AsyncJob): The job instance containing the configuration for the batch creation. - - Returns: - Batch: The newly created batch instance. - """ - return Batch.objects.create( - name=job.config.get("batch_name"), - program=job.program, - country_office=job.program.country_office, - imported_by=job.owner, - ) - - -def _create_household(job: AsyncJob, fields: dict[str, Any]) -> Household: +def _create_household(batch: Batch, fields: dict[str, Any]) -> Household: """ Creates a household entity associated with the given job and batch. @@ -60,15 +50,11 @@ def _create_household(job: AsyncJob, fields: dict[str, Any]) -> Household: Returns: Household: The newly created household instance. """ - return job.program.households.create( - batch=job.batch, flex_fields={clean_field_name(k): v for k, v in fields.items()} - ) + return batch.program.households.create(batch=batch, flex_fields={clean_field_name(k): v for k, v in fields.items()}) def _create_individuals( - job: AsyncJob, - household: Household, - fields: list[dict[str, Any]], + household: Household, data: list[dict[str, Any]], household_name_column: str ) -> list[Individual]: """ Creates individuals associated with a household and updates the household name if necessary. @@ -82,24 +68,27 @@ def _create_individuals( list[Individual]: The list of newly created individual instances. """ individuals = [] - for individual in fields: - - _update_household_name_from_individual(job, household, individual) + head_found = False + for individual in data: + if not head_found: + head_found = _update_household_name_from_individual(household, individual, household_name_column) fullname = next((k for k in individual if k.startswith("given_name")), None) individuals.append( Individual( - batch=job.batch, + batch=household.batch, household_id=household.pk, name=individual.get(fullname, ""), flex_fields={clean_field_name(k): v for k, v in individual.items()}, ) ) - return job.program.individuals.bulk_create(individuals) + return household.program.individuals.bulk_create(individuals) -def _update_household_name_from_individual(job: AsyncJob, household: Household, individual: dict[str, Any]) -> None: +def _update_household_name_from_individual( + household: Household, individual: dict[str, Any], household_name_column: str +) -> bool: """ Updates the household name based on an individual's relationship and name field. @@ -116,6 +105,8 @@ def _update_household_name_from_individual(job: AsyncJob, household: Household, """ if any(individual.get(k) == "head" for k in individual if k.startswith("relationship")): for k, v in individual.items(): - if clean_field_name(k) == job.config["household_name_column"]: - job.program.households.filter(pk=household.pk).update(name=v) - break + if clean_field_name(k) == household_name_column: + household.name = v + household.save() + # household.program.households.filter(pk=household.pk).update(name=v) + return True diff --git a/src/country_workspace/datasources/rdi.py b/src/country_workspace/datasources/rdi.py index d6b09bf..75b4eac 100644 --- a/src/country_workspace/datasources/rdi.py +++ b/src/country_workspace/datasources/rdi.py @@ -5,29 +5,30 @@ from hope_smart_import.readers import open_xls_multi -from country_workspace.models import AsyncJob, Batch, Household, Program +from country_workspace.models import AsyncJob, Batch, Household from country_workspace.utils.fields import clean_field_name RDI = Union[str, io.BytesIO] -def import_from_rdi_job(job: AsyncJob) -> dict[str, int]: - return import_from_rdi( - job.file, - program=job.program, - **job.config, - ) - - -def import_from_rdi( - rdi: RDI, batch: str, program: Program, household_pk_col: str, master_column_label: str, detail_column_label: str -) -> dict[str, int]: +def import_from_rdi(job: AsyncJob) -> dict[str, int]: ret = {"household": 0, "individual": 0} hh_ids = {} with atomic(): + batch_name = job.config["batch_name"] + household_pk_col = job.config["household_pk_col"] + master_column_label = job.config["master_column_label"] + detail_column_label = job.config["detail_column_label"] + rdi = job.file # household_pk_col = form.cleaned_data["pk_column_name"] # total_hh = total_ind = 0 - batch = Batch.objects.get(id=batch) + batch = Batch.objects.create( + name=batch_name, + program=job.program, + country_office=job.program.country_office, + imported_by=job.owner, + source=Batch.BatchSource.RDI, + ) for sheet_index, sheet_generator in open_xls_multi(rdi, sheets=[0, 1]): for line, raw_record in enumerate(sheet_generator, 1): record = {} @@ -36,13 +37,13 @@ def import_from_rdi( if record[household_pk_col]: try: if sheet_index == 0: - hh: "Household" = program.households.create( + hh: "Household" = job.program.households.create( batch=batch, name=raw_record[master_column_label], flex_fields=record ) hh_ids[record[household_pk_col]] = hh.pk ret["household"] += 1 elif sheet_index == 1: - program.individuals.create( + job.program.individuals.create( batch=batch, name=raw_record[detail_column_label], household_id=hh_ids[record[household_pk_col]], diff --git a/src/country_workspace/migrations/0001_initial.py b/src/country_workspace/migrations/0001_initial.py index 5e14af7..d41aa27 100644 --- a/src/country_workspace/migrations/0001_initial.py +++ b/src/country_workspace/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2024-11-29 04:20 +# Generated by Django 5.1.3 on 2024-11-30 09:06 import concurrency.fields import country_workspace.models.base @@ -173,6 +173,15 @@ class Migration(migrations.Migration): ("last_modified", models.DateTimeField(auto_now=True)), ("name", models.CharField(blank=True, max_length=255, null=True)), ("import_date", models.DateTimeField(auto_now=True)), + ( + "source", + models.CharField( + blank=True, + choices=[("RDI", "Rdi file"), ("AURORA", "Aurora"), ("KOBO", "Kobo")], + max_length=255, + null=True, + ), + ), ( "imported_by", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), @@ -239,6 +248,12 @@ class Migration(migrations.Migration): ), ], options={ + "permissions": ( + ("validate_beneficiary", "Can validate Beneficiary Records"), + ("mass_update_beneficiary", "Can Mass update Beneficiary Records"), + ("regex_update_beneficiary", "Can Mass update Beneficiary Records"), + ("export_beneficiary", "Can Export Beneficiary Records"), + ), "abstract": False, }, bases=(country_workspace.models.base.Cachable, models.Model), @@ -325,6 +340,7 @@ class Migration(migrations.Migration): options={ "verbose_name": "Programme", "verbose_name_plural": "Programmes", + "permissions": (("import_program_data", "Can Import beneficiaries"),), }, ), migrations.AddField( @@ -419,8 +435,7 @@ class Migration(migrations.Migration): ), ], options={ - "abstract": False, - "default_permissions": ("add", "change", "delete", "view", "queue", "terminate", "inspect", "revoke"), + "permissions": (("debug_job", "Can debug background jobs"),), }, ), migrations.CreateModel( diff --git a/src/country_workspace/models/base.py b/src/country_workspace/models/base.py index 286a202..17009cf 100644 --- a/src/country_workspace/models/base.py +++ b/src/country_workspace/models/base.py @@ -70,6 +70,12 @@ class Validable(Cachable, models.Model): class Meta: abstract = True + permissions = ( + ("validate_beneficiary", "Can validate Beneficiary Records"), + ("mass_update_beneficiary", "Can Mass update Beneficiary Records"), + ("regex_update_beneficiary", "Can Mass update Beneficiary Records"), + ("export_beneficiary", "Can Export Beneficiary Records"), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/country_workspace/models/batch.py b/src/country_workspace/models/batch.py index 87db28f..3cb89d5 100644 --- a/src/country_workspace/models/batch.py +++ b/src/country_workspace/models/batch.py @@ -5,11 +5,17 @@ class Batch(BaseModel): + class BatchSource(models.TextChoices): + RDI = "RDI", "Rdi file" + AURORA = "AURORA", "Aurora" + KOBO = "KOBO", "Kobo" + country_office = models.ForeignKey("Office", on_delete=models.CASCADE, related_name="%(class)ss") program = models.ForeignKey("Program", on_delete=models.CASCADE, related_name="%(class)ss") name = models.CharField(max_length=255, blank=True, null=True) import_date = models.DateTimeField(auto_now=True) imported_by = models.ForeignKey(User, on_delete=models.CASCADE) + source = models.CharField(max_length=255, blank=True, null=True, choices=BatchSource.choices) class Meta: unique_together = (("import_date", "name"),) diff --git a/src/country_workspace/models/jobs.py b/src/country_workspace/models/jobs.py index 4124c89..0d3c224 100644 --- a/src/country_workspace/models/jobs.py +++ b/src/country_workspace/models/jobs.py @@ -11,8 +11,6 @@ class JobType(models.TextChoices): FQN = "FQN", "Operation" ACTION = "ACTION", "Action" TASK = "TASK", "Task" - BULK_UPDATE_HH = "BULK_UPDATE_HH" - BULK_UPDATE_IND = "BULK_UPDATE_IND" type = models.CharField(max_length=50, choices=JobType.choices) program = models.ForeignKey("Program", related_name="jobs", on_delete=models.CASCADE) @@ -27,6 +25,9 @@ class JobType(models.TextChoices): def __str__(self): return self.description or f"Background Job #{self.pk}" + class Meta: + permissions = (("debug_job", "Can debug background jobs"),) + @property def queue_position(self) -> int: try: diff --git a/src/country_workspace/models/program.py b/src/country_workspace/models/program.py index ef5d6e9..89ce7c2 100644 --- a/src/country_workspace/models/program.py +++ b/src/country_workspace/models/program.py @@ -74,6 +74,7 @@ def __str__(self) -> str: class Meta: verbose_name = _("Programme") verbose_name_plural = _("Programmes") + permissions = (("import_program_data", "Can Import beneficiaries"),) @property def households(self) -> "QuerySet[Household]": diff --git a/src/country_workspace/security/backends.py b/src/country_workspace/security/backends.py index 5d90503..4bbe149 100644 --- a/src/country_workspace/security/backends.py +++ b/src/country_workspace/security/backends.py @@ -22,10 +22,10 @@ def authenticate( password: str | None = None, **kwargs: Any, ) -> "AbstractBaseUser | None": - countries = Office.objects.values_list("slug", flat=True) + offices = Office.objects.values_list("slug", flat=True) if settings.DEBUG: - if username in countries: + if username in offices: user, __ = get_user_model().objects.update_or_create( username=username, defaults=dict(is_staff=True, is_active=True, is_superuser=False), @@ -40,4 +40,12 @@ def authenticate( defaults=dict(is_staff=True, is_active=True, is_superuser=True), ) return user + elif username in [ + "staff", + ]: + user, __ = get_user_model().objects.update_or_create( + username=username, + defaults=dict(is_staff=True, is_active=True, is_superuser=False), + ) + return user return None diff --git a/src/country_workspace/workspaces/admin/batch.py b/src/country_workspace/workspaces/admin/batch.py index 06b5c91..950ab1f 100644 --- a/src/country_workspace/workspaces/admin/batch.py +++ b/src/country_workspace/workspaces/admin/batch.py @@ -12,7 +12,7 @@ from ..models import CountryBatch from ..options import WorkspaceModelAdmin from ..sites import workspace -from .filters import CWLinkedAutoCompleteFilter +from .filters import ChoiceFilter, CWLinkedAutoCompleteFilter, UserAutoCompleteFilter from .hh_ind import SelectedProgramMixin if TYPE_CHECKING: @@ -30,11 +30,12 @@ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet: @register(CountryBatch, site=workspace) class CountryBatchAdmin(SelectedProgramMixin, WorkspaceModelAdmin): - list_display = ["name", "program", "country_office"] + list_display = ["import_date", "name", "imported_by", "source"] search_fields = ("label",) change_list_template = "workspace/change_list.html" change_form_template = "workspace/change_form.html" ordering = ("name",) + list_filter = (("source", ChoiceFilter), ("imported_by", UserAutoCompleteFilter)) exclude = ("program", "country_office", "imported_by") def get_search_results(self, request, queryset, search_term): diff --git a/src/country_workspace/workspaces/admin/cleaners/actions.py b/src/country_workspace/workspaces/admin/cleaners/actions.py index 5726289..1310df5 100644 --- a/src/country_workspace/workspaces/admin/cleaners/actions.py +++ b/src/country_workspace/workspaces/admin/cleaners/actions.py @@ -22,7 +22,7 @@ from country_workspace.workspaces.admin.hh_ind import BeneficiaryBaseAdmin -@admin.action(description="Validate selected records") +@admin.action(description="Validate selected records", permissions=["validate"]) def validate_records( model_admin: "BeneficiaryBaseAdmin", request: HttpRequest, queryset: "QuerySet[Beneficiary]" ) -> None: @@ -40,7 +40,7 @@ def validate_records( return job -@admin.action(description="Mass update record fields") +@admin.action(description="Mass update record fields", permissions=["mass_update"]) def mass_update( model_admin: "BeneficiaryBaseAdmin", request: HttpRequest, queryset: "QuerySet[Beneficiary]" ) -> "HttpResponse": @@ -75,7 +75,7 @@ def mass_update( return render(request, "workspace/actions/mass_update.html", ctx) -@admin.action(description="Update fields using RegEx") +@admin.action(description="Update fields using RegEx", permissions=["regex_update"]) def regex_update( model_admin: "BeneficiaryBaseAdmin", request: "HttpRequest", queryset: "QuerySet[Beneficiary]" ) -> HttpResponse: @@ -123,7 +123,7 @@ def regex_update( return render(request, "workspace/actions/regex.html", ctx) -@admin.action(description="Create XLS template for bulk updates") +@admin.action(description="Create XLS template for bulk updates", permissions=["export"]) def bulk_update_export( model_admin: "BeneficiaryBaseAdmin", request: HttpRequest, queryset: "QuerySet[Beneficiary]" ) -> HttpResponse: diff --git a/src/country_workspace/workspaces/admin/filters.py b/src/country_workspace/workspaces/admin/filters.py index 1925872..57f8e92 100644 --- a/src/country_workspace/workspaces/admin/filters.py +++ b/src/country_workspace/workspaces/admin/filters.py @@ -1,17 +1,13 @@ from typing import TYPE_CHECKING, Any -from django.contrib.admin import SimpleListFilter from django.http import HttpRequest from django.urls import reverse -from django.utils.translation import gettext as _ from adminfilters.autocomplete import AutoCompleteFilter, LinkedAutoCompleteFilter from adminfilters.combo import ChoicesFieldComboFilter -from adminfilters.mixin import SmartFieldListFilter -from debugpy.common.util import force_str +from country_workspace.admin.filters import IsValidFilter from country_workspace.admin.job import FailedFilter -from country_workspace.models import User from country_workspace.state import state if TYPE_CHECKING: @@ -73,28 +69,6 @@ def html_attrs(self): } -# -# class ProgramFilter(CWLinkedAutoCompleteFilter): -# -# def queryset(self, request: HttpRequest, queryset: "QuerySet[Beneficiary]") -> "QuerySet[Beneficiary]": -# if self.lookup_val: -# p = state.tenant.programs.get(pk=self.lookup_val) -# # if request.usser.has_perm() -# queryset = super().queryset(request, queryset).filter(batch__program=p) -# return queryset -# -# -# class BatchFilter(CWLinkedAutoCompleteFilter): -# def has_output(self) -> bool: -# return bool("batch__program__exact" in self.request.GET) -# -# def queryset(self, request: HttpRequest, queryset: "QuerySet[Beneficiary]") -> "QuerySet[Beneficiary]": -# if self.lookup_val: -# queryset = super().queryset(request, queryset).filter(batch=self.lookup_val) -# return queryset -# - - class HouseholdFilter(CWLinkedAutoCompleteFilter): fk_name = "name" @@ -111,44 +85,9 @@ def queryset(self, request: HttpRequest, queryset: "QuerySet[Beneficiary]") -> " return qs -class IsValidFilter(SimpleListFilter): - title = "Valid" - # lookup_val = "valid" - parameter_name = "valid" +class WIsValidFilter(IsValidFilter): template = "workspace/adminfilters/combobox.html" - def lookups(self, request, model_admin): - return ( - ("v", _("Valid")), - ("i", _("Invalid")), - ("u", _("Not Verified")), - ) - - def get_title(self): - return self.title - - def queryset(self, request, queryset): - if self.value() == "v": - return queryset.filter(last_checked__isnull=False).filter(errors__iexact="{}") - elif self.value() == "i": - return queryset.filter(last_checked__isnull=False).exclude(errors__iexact="{}") - elif self.value() == "u": - return queryset.filter(last_checked__isnull=True) - return queryset - - def has_output(self): - return True - - def html_attrs(self): - classes = f"adminfilters {self.__class__.__name__.lower()}" - if self.value(): - classes += " active" - - return { - "class": classes, - "id": "_".join(self.expected_parameters()), - } - class ChoiceFilter(ChoicesFieldComboFilter): template = "workspace/adminfilters/combobox.html" @@ -158,48 +97,11 @@ class WFailedFilter(FailedFilter): template = "workspace/adminfilters/combobox.html" -class OwnerFilter(SmartFieldListFilter): - template = "workspace/adminfilters/combobox.html" - - def expected_parameters(self): - return ["owner"] - - def choices(self, changelist): - value = self.used_parameters.get(self.field.name) - yield { - "selected": value is None, - "query_string": changelist.get_query_string({}, [self.field.name]), - "display": _("All"), - } - for pk, username in User.objects.values_list("pk", "username"): - selected = value is not None and force_str(pk, "utf8") in value - yield { - "selected": selected, - "query_string": changelist.get_query_string({self.field.name: pk}, []), - "display": username, - } - - class UserAutoCompleteFilter(AutoCompleteFilter): - parent_lookup_kwarg: str template = "workspace/adminfilters/autocomplete.html" - def __init__( - self, - field: Any, - request: "HttpRequest", - params: dict[str, Any], - model: "Model", - model_admin: "ModelAdmin", - field_path: str, - ): - super().__init__(field, request, params, model, model_admin, field_path) - def get_url(self) -> str: base_url = reverse("admin:country_workspace_user_autocomplete") - # if self.parent_lookup_kwarg in self.request.GET: - # flt = self.parent_lookup_kwarg.split("__")[-2] - # oid = self.request.GET[self.parent_lookup_kwarg] url = f"{base_url}?program={state.program.pk}" return url diff --git a/src/country_workspace/workspaces/admin/hh_ind.py b/src/country_workspace/workspaces/admin/hh_ind.py index 4bf6819..61718ac 100644 --- a/src/country_workspace/workspaces/admin/hh_ind.py +++ b/src/country_workspace/workspaces/admin/hh_ind.py @@ -66,16 +66,23 @@ class BeneficiaryBaseAdmin(AdminAutoCompleteSearchMixin, SelectedProgramMixin, W actions.regex_update, actions.bulk_update_export, actions.calculate_checksum, - # find_duplicates_action, - # actions.mass_update, - # actions.calculate_checksum, - # actions.regex_update, - # actions.bulk_update_export, ] title = None title_plural = None list_per_page = 20 + def has_validate_permission(self, request: HttpRequest) -> bool: + return request.user.has_perm("country_workspace.validate_beneficiary") + + def has_export_permission(self, request: HttpRequest) -> bool: + return request.user.has_perm("country_workspace.export_beneficiary") + + def has_mass_update_permission(self, request: HttpRequest) -> bool: + return request.user.has_perm("country_workspace.mass_update_beneficiary") + + def has_regex_update_permission(self, request: HttpRequest) -> bool: + return request.user.has_perm("country_workspace.regex_update_beneficiary") + def get_queryset(self, request: HttpRequest) -> "QuerySet[Beneficiary]": qs = super().get_queryset(request) if prg := self.get_selected_program(request): @@ -95,7 +102,7 @@ def get_common_context(self, request: HttpRequest, pk: Optional[str] = None, **k def validate_single(self, request: HttpRequest, pk: str) -> "HttpResponse": obj: "Beneficiary" = self.get_object(request, pk) if obj.validate_with_checker(): - self.message_user(request, _("Validation successful!")) + self.message_user(request, _("Validation successful!"), messages.SUCCESS) else: self.message_user(request, _("Validation failed!"), messages.ERROR) @@ -179,7 +186,7 @@ def _changeform_view( form: "FlexForm" = form_class(request.POST, prefix="flex_field", initial=initials) if form.is_valid(): obj.flex_fields = form.cleaned_data - obj.save() + self.save_model(request, obj, form, True) return HttpResponseRedirect(request.META["HTTP_REFERER"]) else: self.message_user(request, "Please fixes the errors below", messages.ERROR) @@ -206,6 +213,10 @@ def change_view( response = super().change_view(request, object_id, form_url, context) return response + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + obj.validate_with_checker() + # def log_change(self, request, obj, message): # from django.contrib.admin.models import CHANGE, LogEntry # diff --git a/src/country_workspace/workspaces/admin/household.py b/src/country_workspace/workspaces/admin/household.py index dc4bfe7..2fc729b 100644 --- a/src/country_workspace/workspaces/admin/household.py +++ b/src/country_workspace/workspaces/admin/household.py @@ -9,7 +9,7 @@ from admin_extra_buttons.decorators import link from ...state import state -from .filters import CWLinkedAutoCompleteFilter, IsValidFilter +from .filters import CWLinkedAutoCompleteFilter, WIsValidFilter from .hh_ind import BeneficiaryBaseAdmin if TYPE_CHECKING: @@ -31,7 +31,7 @@ class CountryHouseholdAdmin(BeneficiaryBaseAdmin): list_per_page = 20 list_filter = ( ("batch", CWLinkedAutoCompleteFilter.factory(parent=None)), - IsValidFilter, + WIsValidFilter, ) def get_list_display(self, request: HttpRequest) -> list[str]: diff --git a/src/country_workspace/workspaces/admin/individual.py b/src/country_workspace/workspaces/admin/individual.py index f560058..9ed8b45 100644 --- a/src/country_workspace/workspaces/admin/individual.py +++ b/src/country_workspace/workspaces/admin/individual.py @@ -9,7 +9,7 @@ from ...state import state from ..models import CountryHousehold, CountryIndividual, CountryProgram from ..sites import workspace -from .filters import CWLinkedAutoCompleteFilter, HouseholdFilter, IsValidFilter +from .filters import CWLinkedAutoCompleteFilter, HouseholdFilter, WIsValidFilter from .hh_ind import BeneficiaryBaseAdmin @@ -24,7 +24,7 @@ class CountryIndividualAdmin(BeneficiaryBaseAdmin): list_filter = ( ("batch", CWLinkedAutoCompleteFilter.factory(parent=None)), ("household", HouseholdFilter), - IsValidFilter, + WIsValidFilter, ) exclude = [ "household", diff --git a/src/country_workspace/workspaces/admin/program.py b/src/country_workspace/workspaces/admin/program.py index c4060a7..7a45978 100644 --- a/src/country_workspace/workspaces/admin/program.py +++ b/src/country_workspace/workspaces/admin/program.py @@ -11,8 +11,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ -from admin_extra_buttons.api import button, link -from admin_extra_buttons.buttons import LinkButton +from admin_extra_buttons.api import button from hope_flex_fields.models import DataChecker from strategy_field.utils import fqn @@ -21,8 +20,8 @@ from country_workspace.state import state from ...contrib.aurora.forms import ImportAuroraForm -from ...datasources.rdi import import_from_rdi_job -from ...models import AsyncJob, Batch +from ...datasources.rdi import import_from_rdi +from ...models import AsyncJob from ...utils.flex_fields import get_checker_fields from ..models import CountryProgram from ..options import WorkspaceModelAdmin @@ -133,10 +132,6 @@ def changelist_view(self, request, extra_context=None): url = reverse("workspace:workspaces_countryprogram_change", args=[state.program.pk]) return HttpResponseRedirect(url) - @link(change_list=False) - def population(self, btn: LinkButton) -> None: - btn.href = reverse("workspace:workspaces_countryhousehold_changelist") - def _configure_columns( self, request: HttpResponse, @@ -169,7 +164,7 @@ def _configure_columns( return render(request, "workspace/program/configure_columns.html", context) - @button() + @button(permission="workspaces.change_countryprogram") def household_columns(self, request: HttpResponse, pk: str) -> "HttpResponse | HttpResponseRedirect": context = self.get_common_context(request, pk, title="Configure default Household columns") program: "CountryProgram" = context["original"] @@ -177,7 +172,7 @@ def household_columns(self, request: HttpResponse, pk: str) -> "HttpResponse | H context["storage_field"] = "household_columns" return self._configure_columns(request, SelectColumnsForm, context) - @button() + @button(permission="workspaces.change_countryprogram") def individual_columns(self, request: HttpResponse, pk: str) -> "HttpResponse | HttpResponseRedirect": context = self.get_common_context(request, pk, title="Configure default Individual columns") program: "CountryProgram" = context["original"] @@ -185,7 +180,7 @@ def individual_columns(self, request: HttpResponse, pk: str) -> "HttpResponse | context["storage_field"] = "individual_columns" return self._configure_columns(request, SelectIndividualColumnsForm, context) - @button(label=_("Update Records")) + @button(label=_("Update Records"), permission="workspaces.import_program_data") def import_file_updates(self, request: HttpRequest, pk: str) -> "HttpResponse": context = self.get_common_context(request, pk, title="Import updates from file") program: "CountryProgram" = context["original"] @@ -214,7 +209,7 @@ def import_file_updates(self, request: HttpRequest, pk: str) -> "HttpResponse": context["form"] = form return render(request, "workspace/actions/bulk_update_import.html", context) - @button(label=_("Import Data")) + @button(label=_("Import Data"), permission="workspaces.import_program_data") def import_data(self, request: HttpRequest, pk: str) -> "HttpResponse": context = self.get_common_context(request, pk, title="Import Data") context["selected_program"] = program = context["original"] @@ -241,21 +236,15 @@ def import_data(self, request: HttpRequest, pk: str) -> "HttpResponse": def import_rdi(self, request: HttpRequest, program: CountryProgram) -> "ImportFileForm | None": form = ImportFileForm(request.POST, request.FILES, prefix="rdi") if form.is_valid(): - batch = Batch.objects.create( - name=form.cleaned_data["batch_name"] or BATCH_NAME_DEFAULT, - program=program, - country_office=program.country_office, - imported_by=state.request.user, - ) job: AsyncJob = AsyncJob.objects.create( description="RDI importing", type=AsyncJob.JobType.TASK, - action=fqn(import_from_rdi_job), + action=fqn(import_from_rdi), file=request.FILES["rdi-file"], program=program, owner=request.user, config={ - "batch": batch.pk, + "batch_name": form.cleaned_data["batch_name"] or BATCH_NAME_DEFAULT, "household_pk_col": form.cleaned_data["pk_column_name"], "master_column_label": form.cleaned_data["master_column_label"], "detail_column_label": form.cleaned_data["detail_column_label"], @@ -269,20 +258,17 @@ def import_rdi(self, request: HttpRequest, program: CountryProgram) -> "ImportFi def import_aurora(self, request: HttpRequest, program: "CountryProgram") -> "ImportAuroraForm|None": form = ImportAuroraForm(request.POST, prefix="aurora") if form.is_valid(): - batch = Batch.objects.create( - name=form.cleaned_data["batch_name"] or BATCH_NAME_DEFAULT, - program=program, - country_office=program.country_office, - imported_by=state.request.user, - ) + job: AsyncJob = AsyncJob.objects.create( type=AsyncJob.JobType.TASK, action=fqn(sync_aurora_job), - batch=batch, file=None, program=program, owner=request.user, - config={"household_name_column": form.cleaned_data["household_name_column"]}, + config={ + "batch_name": form.cleaned_data["batch_name"] or BATCH_NAME_DEFAULT, + "household_name_column": form.cleaned_data["household_name_column"], + }, ) job.queue() self.message_user( diff --git a/src/country_workspace/workspaces/migrations/0001_initial.py b/src/country_workspace/workspaces/migrations/0001_initial.py index 29b6c2d..eaf686c 100644 --- a/src/country_workspace/workspaces/migrations/0001_initial.py +++ b/src/country_workspace/workspaces/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2024-11-29 04:20 +# Generated by Django 5.1.3 on 2024-11-30 09:06 import django.db.models.deletion from django.db import migrations, models diff --git a/src/country_workspace/workspaces/templates/workspace/_base.html b/src/country_workspace/workspaces/templates/workspace/_base.html index 0ae9488..b9f9661 100755 --- a/src/country_workspace/workspaces/templates/workspace/_base.html +++ b/src/country_workspace/workspaces/templates/workspace/_base.html @@ -25,7 +25,7 @@ {% if active_program %} -