Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/country_workspace/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ..cache.smart_panel import panel_cache
from .batch import BatchAdmin
from .flex_fields import CWDataCheckerAdmin, CWFieldsetAdmin, CWFlexFieldAdmin
from .beneficiary_group import BeneficiaryGroupAdmin
from .constance import ConstanceAdmin
from .household import HouseholdAdmin
Expand Down Expand Up @@ -37,6 +38,9 @@
"AsyncJobAdmin",
"BatchAdmin",
"BeneficiaryGroupAdmin",
"CWDataCheckerAdmin",
"CWFieldsetAdmin",
"CWFlexFieldAdmin",
"ConstanceAdmin",
"CountryAdmin",
"DataSerializerAdmin",
Expand Down
50 changes: 50 additions & 0 deletions src/country_workspace/admin/flex_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Any

from admin_extra_buttons.decorators import button
from django.contrib import admin
from django.http import HttpRequest, HttpResponse

from hope_flex_fields.admin import DataCheckerAdmin, FieldsetAdmin, FlexFieldAdmin
from hope_flex_fields.models import DataChecker, Fieldset, FlexField

from country_workspace.signals import collect_invalidations


class CollectInvalidationsMixin:
def changeform_view(
self,
request: HttpRequest,
object_id: str | None = None,
form_url: str = "",
extra_context: dict[str, Any] | None = None,
) -> HttpResponse:
if request.method == "POST":
with collect_invalidations():
return super().changeform_view(request, object_id, form_url, extra_context)
return super().changeform_view(request, object_id, form_url, extra_context)


admin.site.unregister(DataChecker)
admin.site.unregister(Fieldset)
admin.site.unregister(FlexField)


@admin.register(DataChecker)
class CWDataCheckerAdmin(CollectInvalidationsMixin, DataCheckerAdmin):
pass


@admin.register(Fieldset)
class CWFieldsetAdmin(CollectInvalidationsMixin, FieldsetAdmin):
@button(label="Fields")
def all_fields(self, request: HttpRequest, pk: str) -> HttpResponse:
impl = FieldsetAdmin.all_fields.func
if request.method == "POST":
with collect_invalidations():
return impl(self, request, pk)
return impl(self, request, pk)


@admin.register(FlexField)
class CWFlexFieldAdmin(CollectInvalidationsMixin, FlexFieldAdmin):
pass
26 changes: 8 additions & 18 deletions src/country_workspace/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,10 @@ def _get_dc_associated_programs(dc: DataChecker) -> Any:


def _invalidate_qs(qs: Any) -> None:
qs = qs.filter(last_checked__isnull=False)
batch_size = 500
pks = list(qs.values_list("pk", flat=True))

for start in range(0, len(pks), batch_size):
batch_pks = pks[start : start + batch_size]
qs.filter(pk__in=batch_pks).update(
errors={"data_checker": "Invalidated due to DataChecker change."},
last_checked=None,
)
qs.filter(last_checked__isnull=False).update(
errors={"data_checker": "Invalidated due to DataChecker change."},
last_checked=None,
)


def _process_program(program: Program) -> None:
Expand All @@ -49,8 +43,7 @@ def _process_program(program: Program) -> None:


def _process_datachecker_change(dc: DataChecker) -> None:
if not (programs := _get_dc_associated_programs(dc=dc)):
return
programs = _get_dc_associated_programs(dc=dc)

deferred = getattr(_invalidation_state, "deferred_program_pks", None)
if deferred is not None:
Expand All @@ -65,13 +58,12 @@ def _process_datachecker_change(dc: DataChecker) -> None:

@receiver(post_save, sender=Fieldset, dispatch_uid="cw_on_fieldset_change")
@receiver(post_save, sender=FlexField, dispatch_uid="cw_on_flexfield_change")
@receiver(post_save, sender=DataChecker, dispatch_uid="cw_on_datachecker_change")
@receiver(post_save, sender=DataCheckerFieldset, dispatch_uid="cw_on_dcfieldset_change")
@receiver(pre_save, sender=Program, dispatch_uid="cw_on_program_change")
@receiver(pre_save, sender=CountryProgram, dispatch_uid="cw_on_country_program_change")
def invalidate_entities_on_datachecker_change( # noqa: C901
sender: type[Fieldset | FlexField | DataCheckerFieldset | DataChecker | Program | CountryProgram],
instance: Fieldset | FlexField | DataCheckerFieldset | DataChecker | Program | CountryProgram,
def invalidate_entities_on_datachecker_change(
sender: type[Fieldset | FlexField | DataCheckerFieldset | Program | CountryProgram],
instance: Fieldset | FlexField | DataCheckerFieldset | Program | CountryProgram,
created: bool | None = None,
**kwargs: Any,
) -> None:
Expand All @@ -83,8 +75,6 @@ def invalidate_entities_on_datachecker_change( # noqa: C901
dcs = instance.fieldset.datachecker_set.all().distinct()
for dc in dcs:
_process_datachecker_change(dc=dc)
elif isinstance(instance, DataChecker):
_process_datachecker_change(dc=instance)
elif isinstance(instance, DataCheckerFieldset):
dc = instance.checker
_process_datachecker_change(dc=dc)
Expand Down
2 changes: 1 addition & 1 deletion tests/admin/test_admin_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def extend(self, iterable: Iterable[Any]) -> None:
GLOBAL_EXCLUDED_BUTTONS = RegexList(
[
r"social.SocialProviderAdmin:test",
r"hope_flex_fields.FieldsetAdmin:detect_changes",
r"hope_flex_fields.CWFieldsetAdmin:detect_changes",
r"country_workspace.CountryHouseholdAdmin:import_file",
r".*:sync",
r".*:create_xls_importer",
Expand Down
44 changes: 42 additions & 2 deletions tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from django.utils import timezone
from hope_flex_fields.models import DataChecker
from hope_flex_fields.models import DataCheckerFieldset
from hope_flex_fields.models import Fieldset
from strategy_field.utils import fqn
from django.contrib.admin.sites import AdminSite
from country_workspace.admin.flex_fields import CWFieldsetAdmin

from country_workspace.contrib.hope.constants import (
HOUSEHOLD_CHECKER_NAME,
Expand Down Expand Up @@ -226,11 +229,11 @@ def test_dcfieldset_update_triggers_processing(ind_datachecker, ind_dcfieldset):
mocked.assert_called_once_with(dc=ind_datachecker)


def test_datachecker_update_triggers_processing(hh_datachecker):
def test_datachecker_update_does_not_trigger_processing(hh_datachecker):
with patch("country_workspace.signals._process_datachecker_change") as mocked:
hh_datachecker.description = "Updated"
hh_datachecker.save(update_fields=["description"])
mocked.assert_called_once_with(dc=hh_datachecker)
mocked.assert_not_called()


def test_flexfield_update_triggers_processing(hh_datachecker, hh_flexfield):
Expand Down Expand Up @@ -418,3 +421,40 @@ class _Unrelated:
with patch("country_workspace.signals._process_program") as mock_process:
invalidate_entities_on_datachecker_change(sender=_Unrelated, instance=_Unrelated())
mock_process.assert_not_called()


def test_datachecker_save_does_not_invalidate_entities(hh_datachecker, checked_household):
hh_datachecker.description = "Changed description"
hh_datachecker.save()

checked_household.refresh_from_db()
assert checked_household.last_checked == CHECKED
assert checked_household.errors == {}


def test_collect_invalidations_deduplicates_multiple_signal_sources(program, hh_flexfield, checked_household):
fs = hh_flexfield.fieldset

with patch("country_workspace.signals._process_program") as mock_process:
with collect_invalidations():
hh_flexfield.attrs = {"label": "New"}
hh_flexfield.save(update_fields=["attrs"])

fs.description = "Updated"
fs.save(update_fields=["description"])

mock_process.assert_called_once_with(program=program)


def test_admin_fieldset_all_fields_post_uses_collect_invalidations(rf, hh_datachecker, hh_flexfield, program):
fs = hh_flexfield.fieldset
admin_instance = CWFieldsetAdmin(Fieldset, AdminSite())

request = rf.post(f"/admin/hope_flex_fields/fieldset/{fs.pk}/all_fields/")
request.user = type("U", (), {"is_authenticated": True, "has_perm": lambda *a: True})()

with patch("country_workspace.admin.flex_fields.FieldsetAdmin.all_fields") as mock_parent:
mock_parent.func = lambda self, request, pk: None
with patch("country_workspace.admin.flex_fields.collect_invalidations") as mock_ctx:
admin_instance.all_fields.func(admin_instance, request, str(fs.pk))
mock_ctx.assert_called_once()