diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a64f03..92942be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,6 @@ name: Test on: - create: - branches: - - releases/* push: branches: - develop diff --git a/src/country_workspace/admin/batch.py b/src/country_workspace/admin/batch.py index c251e91..a75752c 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 LinkedAutoCompleteFilter from ..models import Batch from .base import BaseModelAdmin @@ -11,7 +12,10 @@ @admin.register(Batch) class BatchAdmin(BaseModelAdmin): list_display = ("name", "import_date", "imported_by") - + list_filter = ( + ("country_office", LinkedAutoCompleteFilter.factory(parent=None)), + ("program", LinkedAutoCompleteFilter.factory(parent="country_office")), + ) readonly_fields = ("country_office", "program") search_fields = ("name",) @@ -20,3 +24,10 @@ def members(self, button: LinkButton) -> None: base = reverse("admin:country_workspace_individual_changelist") obj = button.context["original"] button.href = f"{base}?household__exact={obj.pk}" + + @link(change_list=True, change_form=False) + def view_in_workspace(self, btn: "LinkButton") -> None: + if "request" in btn.context: + req = btn.context["request"] + base = reverse("workspace:workspaces_countrybatch_changelist") + btn.href = f"{base}?%s" % req.META["QUERY_STRING"] diff --git a/src/country_workspace/admin/household.py b/src/country_workspace/admin/household.py index 9aef0c4..aafb87d 100644 --- a/src/country_workspace/admin/household.py +++ b/src/country_workspace/admin/household.py @@ -23,3 +23,10 @@ def members(self, button: LinkButton) -> None: base = reverse("admin:country_workspace_individual_changelist") obj = button.context["original"] button.href = f"{base}?household__exact={obj.pk}" + + @link(change_list=True, change_form=False) + def view_in_workspace(self, btn: "LinkButton") -> None: + if "request" in btn.context: + req = btn.context["request"] + base = reverse("workspace:workspaces_countryhousehold_changelist") + btn.href = f"{base}?%s" % req.META["QUERY_STRING"] diff --git a/src/country_workspace/models/batch.py b/src/country_workspace/models/batch.py index 5808735..72a66af 100644 --- a/src/country_workspace/models/batch.py +++ b/src/country_workspace/models/batch.py @@ -13,3 +13,6 @@ class Batch(BaseModel): class Meta: unique_together = (("import_date", "name"),) + + def __str__(self): + return self.name or f"Batch self.pk ({self.country_office})" diff --git a/src/country_workspace/workspaces/admin/batch.py b/src/country_workspace/workspaces/admin/batch.py index 07fdd04..e4d2eee 100644 --- a/src/country_workspace/workspaces/admin/batch.py +++ b/src/country_workspace/workspaces/admin/batch.py @@ -2,24 +2,37 @@ from django.db.models import QuerySet from django.http import HttpRequest +from django.urls import reverse -from ...state import state +from admin_extra_buttons.buttons import LinkButton +from admin_extra_buttons.decorators import link + +from ..filters import ProgramFilter from ..options import WorkspaceModelAdmin +from .hh_ind import SelectedProgramMixin if TYPE_CHECKING: from ..models import CountryBatch -class CountryBatchAdmin(WorkspaceModelAdmin): +class CountryBatchAdmin(SelectedProgramMixin, WorkspaceModelAdmin): list_display = ["name", "program", "country_office"] search_fields = ("label",) - - change_list_template = "workspace/household/change_list.html" - change_form_template = "workspace/household/change_form.html" + list_filter = (("program", ProgramFilter),) + change_list_template = "workspace/change_list.html" + change_form_template = "workspace/change_form.html" ordering = ("name",) + readonly_fields = ("program", "country_office", "imported_by") def get_queryset(self, request: HttpRequest) -> "QuerySet[CountryBatch]": - return super().get_queryset(request).filter(country_office=state.tenant) + return super().get_queryset(request).all() + # return super().get_queryset(request).filter(country_office=state.tenant) def has_add_permission(self, request, obj=None): return False + + @link(change_list=False) + def imported_records(self, btn: LinkButton) -> None: + base = reverse("workspace:workspaces_countryhousehold_changelist") + obj = btn.context["original"] + btn.href = f"{base}?batch__exact={obj.pk}&batch__program__exact={obj.program.pk}" diff --git a/src/country_workspace/workspaces/admin/hh_ind.py b/src/country_workspace/workspaces/admin/hh_ind.py index ecca8e3..275f562 100644 --- a/src/country_workspace/workspaces/admin/hh_ind.py +++ b/src/country_workspace/workspaces/admin/hh_ind.py @@ -8,9 +8,11 @@ from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.template.response import TemplateResponse +from django.urls import reverse from django.utils.translation import gettext as _ -from admin_extra_buttons.decorators import button +from admin_extra_buttons.buttons import LinkButton +from admin_extra_buttons.decorators import button, link from adminfilters.autocomplete import LinkedAutoCompleteFilter from adminfilters.mixin import AdminAutoCompleteSearchMixin from hope_flex_fields.models import DataChecker @@ -22,10 +24,45 @@ from .program import CountryProgram -class CountryHouseholdIndividualBaseAdmin(AdminAutoCompleteSearchMixin, WorkspaceModelAdmin): +class SelectedProgramMixin(WorkspaceModelAdmin): + + def get_selected_program(self, request: HttpRequest, obj: "Optional[Validable]" = None) -> "CountryProgram | None": + from country_workspace.workspaces.models import CountryProgram + + self._selected_program = None + if obj: + self._selected_program = obj.batch.program + elif "program__exact" in request.GET: + self._selected_program = CountryProgram.objects.get(pk=request.GET["program__exact"]) + elif "batch__program__exact" in request.GET: + self._selected_program = CountryProgram.objects.get(pk=request.GET["batch__program__exact"]) + return self._selected_program + + def get_common_context(self, request: HttpRequest, pk: Optional[str] = None, **kwargs: Any) -> dict[str, Any]: + ret = super().get_common_context(request, pk, **kwargs) + + ret["selected_program"] = self.get_selected_program(request, ret.get("original")) + ret["preserved_filters"] = request.GET.get("_changelist_filters", "") + return ret + + def changelist_view(self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None) -> HttpResponse: + context = self.get_common_context(request, title="") + context.update(extra_context or {}) + return super().changelist_view(request, context) + + @link() + def import_rdi(self, btn: LinkButton) -> None: + btn.visible = False + if prg := self.get_selected_program(btn.context["request"]): + btn.href = reverse("workspace:workspaces_countryprogram_import_rdi", args=[prg.pk]) + btn.visible = True + + +class CountryHouseholdIndividualBaseAdmin(AdminAutoCompleteSearchMixin, SelectedProgramMixin, WorkspaceModelAdmin): list_filter = ( ("batch__program", LinkedAutoCompleteFilter.factory(parent=None)), ("batch", LinkedAutoCompleteFilter.factory(parent="batch__program")), + # ("batch", BatchFilter), ) actions = ["validate_queryset"] @@ -66,7 +103,7 @@ def validate_queryset(self, request: HttpRequest, queryset: QuerySet) -> HttpRes @button() def view_raw_data(self, request: HttpRequest, pk: str) -> "HttpResponse": - context = self.get_common_context(request, pk) + context = self.get_common_context(request, pk, title="Raw Data") return render(request, "workspace/raw_data.html", context) def is_valid(self, obj: "Validable") -> bool | None: @@ -80,27 +117,16 @@ def get_changelist(self, request: HttpRequest, **kwargs: Any) -> type: from ..changelist import FlexFieldsChangeList if program := self.get_selected_program(request): - return type("FlexFieldsChangeList", (FlexFieldsChangeList,), {"checker": program.household_checker}) + return type( + "FlexFieldsChangeList", + (FlexFieldsChangeList,), + {"checker": program.household_checker, "selected_program": self.get_selected_program(request)}, + ) return FlexFieldsChangeList def has_add_permission(self, request: HttpRequest) -> bool: return False - def get_selected_program(self, request: HttpRequest, obj: "Optional[Validable]" = None) -> "CountryProgram | None": - from country_workspace.workspaces.models import CountryProgram - - self._selected_program = None - if obj: - self._selected_program = obj.batch.program - elif "batch__program__exact" in request.GET: - self._selected_program = CountryProgram.objects.get(pk=request.GET["batch__program__exact"]) - return self._selected_program - - def changelist_view(self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None) -> HttpResponse: - context = self.get_common_context(request, title="") - context.update(extra_context or {}) - return super().changelist_view(request, context) - def get_checker(self, request: HttpRequest, obj: Optional[str] = None) -> "DataChecker": if obj: return obj.program.get_checker_for(obj) @@ -108,12 +134,6 @@ def get_checker(self, request: HttpRequest, obj: Optional[str] = None) -> "DataC return p.household_checker raise Http404("No Household checkers available") - def get_common_context(self, request: HttpRequest, pk: Optional[str] = None, **kwargs: Any) -> dict[str, Any]: - ret = super().get_common_context(request, pk, **kwargs) - - ret["selected_program"] = self.get_selected_program(request, ret.get("original")) - return ret - def _changeform_view( self, request: HttpRequest, object_id: str, form_url: str, extra_context: dict[str, Any] ) -> HttpResponse: diff --git a/src/country_workspace/workspaces/admin/program.py b/src/country_workspace/workspaces/admin/program.py index bac962e..a1c5b4d 100644 --- a/src/country_workspace/workspaces/admin/program.py +++ b/src/country_workspace/workspaces/admin/program.py @@ -158,7 +158,7 @@ def individual_columns(self, request: HttpResponse, pk: str) -> "HttpResponse | @button(label=_("Import File")) def import_rdi(self, request: HttpRequest, pk: str) -> "HttpResponse": - context = self.get_common_context(request, pk) + context = self.get_common_context(request, pk, title="Import RDI file") program: "CountryProgram" = context["original"] context["selected_program"] = context["original"] hh_ids = {} diff --git a/src/country_workspace/workspaces/filters.py b/src/country_workspace/workspaces/filters.py index 6e0497b..ce62dbe 100644 --- a/src/country_workspace/workspaces/filters.py +++ b/src/country_workspace/workspaces/filters.py @@ -13,21 +13,30 @@ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet: if self.lookup_val: p = state.tenant.programs.get(pk=self.lookup_val) # if request.usser.has_perm() - queryset = super().queryset(request, queryset) + queryset = super().queryset(request, queryset).filter(batch__program=p) state.program = p return queryset class BatchFilter(LinkedAutoCompleteFilter): - # def has_output(self) -> bool: - # return bool("batch__program__exact" in self.request.GET) + def has_output(self) -> bool: + return bool("batch__program__exact" in self.request.GET) + + # def get_url(self): + # url = reverse("%s:autocomplete" % self.admin_site.name) + # # if self.parent_lookup_kwarg in self.request.GET: + # # flt = self.parent_lookup_kwarg.split("__")[-2] + # if self.has_output(): + # oid = self.request.GET["batch__program__exact"] + # return f"{url}?program__exact={oid}" + # return url def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet: if self.lookup_val: - p = state.tenant.programs.get(pk=self.lookup_val) + # p = state.tenant.programs.get(pk=self.lookup_val) # if request.usser.has_perm() - queryset = super().queryset(request, queryset) - state.program = p + queryset = super().queryset(request, queryset).filter(batch=self.lookup_val) + # state.program = p return queryset diff --git a/src/country_workspace/workspaces/options.py b/src/country_workspace/workspaces/options.py index 556d3ac..bc35e5c 100644 --- a/src/country_workspace/workspaces/options.py +++ b/src/country_workspace/workspaces/options.py @@ -1,6 +1,7 @@ from urllib.parse import urlencode from django.contrib import admin +from django.contrib.admin import ShowFacets from django.http import HttpResponseRedirect from django.urls import reverse @@ -25,6 +26,9 @@ class WorkspaceModelAdmin(ExtraButtonsMixin, AdminFiltersMixin, SmartFilterMixin delete_confirmation_template = "workspace/delete_confirmation.html" preserve_filters = True default_url_filters = {} + actions_selection_counter = False + show_facets = ShowFacets.NEVER + show_full_result_count = False def __init__(self, model, admin_site): self._selected_program = None diff --git a/src/country_workspace/workspaces/sites.py b/src/country_workspace/workspaces/sites.py index d99acb8..0284702 100644 --- a/src/country_workspace/workspaces/sites.py +++ b/src/country_workspace/workspaces/sites.py @@ -26,6 +26,12 @@ class TenantAutocompleteJsonView(SmartAutocompleteJsonView): def has_perm(self, request: "HttpRequest", obj: "Model|None" = None) -> bool: return self.model_admin.has_view_permission(request, obj=obj) + def filter_queryset(self, queryset: QuerySet) -> QuerySet: + params = { + k: v for k, v in self.request.GET.items() if k not in ["app_label", "model_name", "field_name", "term"] + } + return queryset.filter(**params) + def get_queryset(self) -> QuerySet: """Return queryset based on ModelAdmin.get_search_results().""" qs = self.model_admin.get_queryset(self.request) @@ -34,7 +40,7 @@ def get_queryset(self) -> QuerySet: qs, search_use_distinct = self.model_admin.get_search_results(self.request, qs, self.term) if search_use_distinct: qs = qs.distinct() - return qs + return self.filter_queryset(qs) def process_request(self, request: HttpRequest): # noqa C901 """ diff --git a/src/country_workspace/workspaces/templates/workspace/change_list.html b/src/country_workspace/workspaces/templates/workspace/change_list.html index a8b192e..6558d0b 100644 --- a/src/country_workspace/workspaces/templates/workspace/change_list.html +++ b/src/country_workspace/workspaces/templates/workspace/change_list.html @@ -37,7 +37,10 @@ {% endif %} {% block coltype %}{% endblock %} - +{% block content_title %}{% endblock %} +{% block pretitle %} + {% include "workspace/includes/program_title.html" with program=selected_program %} +{% endblock %} {% block content %}
{% block object-tools %} diff --git a/src/country_workspace/workspaces/templates/workspace/hh_ind_change_form.html b/src/country_workspace/workspaces/templates/workspace/hh_ind_change_form.html index 31942a2..fa551de 100644 --- a/src/country_workspace/workspaces/templates/workspace/hh_ind_change_form.html +++ b/src/country_workspace/workspaces/templates/workspace/hh_ind_change_form.html @@ -6,6 +6,21 @@ {% block content_title %}{% endblock %} {% block coltype %}colMX{% endblock %} +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + + {% block content %}
{% block object-tools %} diff --git a/src/country_workspace/workspaces/templates/workspace/hh_ind_change_list.html b/src/country_workspace/workspaces/templates/workspace/hh_ind_change_list.html index d38c77a..6f150cd 100644 --- a/src/country_workspace/workspaces/templates/workspace/hh_ind_change_list.html +++ b/src/country_workspace/workspaces/templates/workspace/hh_ind_change_list.html @@ -3,9 +3,10 @@ {% block coltype %}{% endblock %} {% block pretitle %} - {{ selected_program }} {% include "workspace/includes/program_title.html" with program=selected_program %} {% endblock %} + + {% block content %}
{% block object-tools %} diff --git a/src/country_workspace/workspaces/templates/workspace/includes/program_title.html b/src/country_workspace/workspaces/templates/workspace/includes/program_title.html index 6fcb884..efe9b51 100644 --- a/src/country_workspace/workspaces/templates/workspace/includes/program_title.html +++ b/src/country_workspace/workspaces/templates/workspace/includes/program_title.html @@ -1,13 +1,13 @@

{% if program %} - {{ program }} + › {{ program }} {% if perms.country_workspace.change_program %} {% endif %} {% if household %} - {{ household }} + › {{ household }} {% if perms.country_workspace.change_household %} diff --git a/src/country_workspace/workspaces/templates/workspace/program/configure_columns.html b/src/country_workspace/workspaces/templates/workspace/program/configure_columns.html index 2f08df2..d9ff37d 100644 --- a/src/country_workspace/workspaces/templates/workspace/program/configure_columns.html +++ b/src/country_workspace/workspaces/templates/workspace/program/configure_columns.html @@ -11,6 +11,15 @@ {% block content %}

{{ checker }}

+ {% block object-tools %} +{% if change and not is_popup %} +
    + {% block object-tools-items %} + {% include "admin_extra_buttons/includes/change_form_buttons.html" %} + {% endblock %} +
+{% endif %} +{% endblock %}
{% csrf_token %} diff --git a/src/country_workspace/workspaces/templates/workspace/program/import_rdi.html b/src/country_workspace/workspaces/templates/workspace/program/import_rdi.html index d933c95..67af28a 100644 --- a/src/country_workspace/workspaces/templates/workspace/program/import_rdi.html +++ b/src/country_workspace/workspaces/templates/workspace/program/import_rdi.html @@ -19,6 +19,15 @@ {# #} {% endblock %} {% block content %} + {% block object-tools %} +{% if change and not is_popup %} +
    + {% block object-tools-items %} + {% include "admin_extra_buttons/includes/change_form_buttons.html" %} + {% endblock %} +
+{% endif %} +{% endblock %} {% csrf_token %}
diff --git a/src/country_workspace/workspaces/templates/workspace/raw_data.html b/src/country_workspace/workspaces/templates/workspace/raw_data.html index 2932a1c..a13ca2e 100644 --- a/src/country_workspace/workspaces/templates/workspace/raw_data.html +++ b/src/country_workspace/workspaces/templates/workspace/raw_data.html @@ -1,4 +1,25 @@ {% extends "workspace/admin_extra_buttons/action_page.html" %}{% load i18n workspace_urls workspace_modify %} + +{% block breadcrumbs %} + +{% endblock %} + {% block content %}
{% for name, value in original.flex_fields.items %} diff --git a/src/country_workspace/workspaces/templates/workspace/w_search_form.html b/src/country_workspace/workspaces/templates/workspace/w_search_form.html new file mode 100644 index 0000000..2a23269 --- /dev/null +++ b/src/country_workspace/workspaces/templates/workspace/w_search_form.html @@ -0,0 +1,32 @@ +{% load i18n static %} +{% if cl.search_fields %} +
+
+ + + +{% if show_result_count %} + + {% blocktranslate count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktranslate %} + ( + {% if cl.show_full_result_count %} + {% blocktranslate with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktranslate %} + {% else %} + {% translate "Show all" %} + {% endif %} + ) + +{% endif %} +{% for pair in cl.params.items %} + {% if pair.0 != search_var %}{% endif %} +{% endfor %} +
+{% if cl.search_help_text %} +
+
{{ cl.search_help_text }}
+{% endif %} +
+{% endif %} diff --git a/src/country_workspace/workspaces/templatetags/workspace_list.py b/src/country_workspace/workspaces/templatetags/workspace_list.py index 99befd1..387b517 100644 --- a/src/country_workspace/workspaces/templatetags/workspace_list.py +++ b/src/country_workspace/workspaces/templatetags/workspace_list.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from django.contrib.admin import ModelAdmin +from django.contrib.admin.templatetags import admin_list from django.contrib.admin.templatetags.admin_list import ResultList as DjangoResultList from django.contrib.admin.templatetags.admin_list import _coerce_field_name, result_hidden_fields from django.contrib.admin.utils import display_for_field, display_for_value, label_for_field, lookup_field @@ -39,6 +40,17 @@ def flex_field_lookup_field(field_name: str, result, model_admin: "ModelAdmin") return f, attr, value +@register.tag(name="search_form") +def search_form_tag(parser, token): + return WorkspaceInclusionAdminNode( + parser, + token, + func=admin_list.search_form, + template_name="w_search_form.html", + takes_context=False, + ) + + def result_headers(cl: "WorkspaceChangeList"): # noqa """ Overrides standard Django behaviour to silent error if wrong columns have been configured