Skip to content
Open
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
18 changes: 17 additions & 1 deletion src/country_workspace/cache/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from country_workspace import VERSION
from country_workspace.state import state

from .signals import cache_get, cache_invalidate, cache_set

if TYPE_CHECKING:
Expand Down Expand Up @@ -128,5 +127,22 @@ def build_key_from_request(self, request: HttpRequest, prefix: str = "view", *ar
*[slugify(request.path), slugify(str(sorted(request.GET.items()))), *[str(e) for e in args]],
)

def invalidate_pattern(self, pattern: str) -> int:
client = self.get_redis_client()
deleted_count = 0

cursor = 0
while True:
Copy link
Member

Choose a reason for hiding this comment

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

do not delete cache keys - user version pattern

Copy link
Collaborator

Choose a reason for hiding this comment

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

@patsatsia changed requested

cursor, keys = client.scan(cursor=cursor, match=pattern, count=100)
if keys:
deleted_count += client.delete(*keys)
if cursor == 0:
break

return deleted_count

def invalidate_containing(self, substring: str) -> int:
return self.invalidate_pattern(f"*{substring}*")


cache_manager = CacheManager()
16 changes: 15 additions & 1 deletion src/country_workspace/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from django.db.models import Q
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from hope_flex_fields.models import Fieldset, DataCheckerFieldset, DataChecker
from hope_flex_fields.models import Fieldset, DataCheckerFieldset, DataChecker, FlexField

from country_workspace.cache.manager import cache_manager
from country_workspace.models import Program
from country_workspace.workspaces.models import CountryProgram

Expand Down Expand Up @@ -75,3 +76,16 @@ def invalidate_entities_on_datachecker_change(
bv_type(instance),
):
_process_program(program=instance)


@receiver(post_save, sender=FlexField, dispatch_uid="cw_on_flex_field_change")
def invalidate_fieldset_fields_admin_cache(
sender: type[FlexField],
instance: FlexField,
created: bool | None = None,
**kwargs: Any,
) -> None:
if not (fieldset := getattr(instance, "fieldset", None)):
return

cache_manager.invalidate_containing(f"adminhope_flex_fieldsfieldset{fieldset.pk}")
39 changes: 39 additions & 0 deletions tests/cache/test_cache_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,42 @@ def test_invalidate(manager, program):
manager.incr_cache_version(program=program)
v = manager.get_cache_version(program=program)
assert v == 3


def test_invalidate_pattern_no_matches(manager):
manager.store("test:key", "value")

deleted_count = manager.invalidate_pattern("nonexistent:*")

assert deleted_count == 0
assert manager.retrieve("test:key") == "value"


def test_invalidate_containing(manager):
manager.store("prefix:user:123:data", "value1")
manager.store("prefix:user:123:settings", "value2")
manager.store("prefix:user:456:data", "value3")
manager.store("prefix:order:789", "value4")

assert manager.retrieve("prefix:user:123:data") == "value1"
assert manager.retrieve("prefix:user:123:settings") == "value2"
assert manager.retrieve("prefix:user:456:data") == "value3"
assert manager.retrieve("prefix:order:789") == "value4"

deleted_count = manager.invalidate_containing("user:123")

assert deleted_count == 2
assert manager.retrieve("prefix:user:123:data") is None
assert manager.retrieve("prefix:user:123:settings") is None

assert manager.retrieve("prefix:user:456:data") == "value3"
assert manager.retrieve("prefix:order:789") == "value4"


def test_invalidate_containing_no_matches(manager):
manager.store("test:key", "value")

deleted_count = manager.invalidate_containing("nonexistent")

assert deleted_count == 0
assert manager.retrieve("test:key") == "value"
1 change: 1 addition & 0 deletions tests/functional/test_f_household.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ def _test_update_with_regex(
import time

time.sleep(0.5) # for DOM
browser.wait_for_element("//table//tr[position()>1]/td[text()]", by=By.XPATH, timeout=10)

headers = browser.find_elements(By.XPATH, "//table//tr[1]/th")
header_texts = [h.text.strip().lower() for h in headers]
Expand Down
32 changes: 30 additions & 2 deletions tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import patch, MagicMock

import pytest
from hope_flex_fields.models import DataChecker
Expand All @@ -13,6 +13,7 @@
from country_workspace.contrib.hope.validators import FullHouseholdValidator
from country_workspace.signals import (
_process_datachecker_change,
invalidate_fieldset_fields_admin_cache,
)
from country_workspace.validators.registry import NoopValidator
from tests.extras.testutils.factories import (
Expand All @@ -23,7 +24,7 @@
FieldsetFactory,
)
from tests.extras.testutils.factories.program import BeneficiaryGroupFactory
from tests.extras.testutils.factories.smart_fields import DataCheckerFactory
from tests.extras.testutils.factories.smart_fields import DataCheckerFactory, FlexFieldFactory


@pytest.fixture
Expand Down Expand Up @@ -207,3 +208,30 @@ def test_no_invalidation_on_other_field_change(program):
for hh in HouseholdFactory._meta.model.objects.all():
hh.refresh_from_db()
assert hh.errors == {"x": "1"}


def test_invalidate_fieldset_fields_admin_cache_on_flexfield_save():
fieldset = FieldsetFactory.create()

with patch("country_workspace.signals.cache_manager.invalidate_containing") as mocked:
FlexFieldFactory.create(fieldset=fieldset, name="test_field")
mocked.assert_called_once_with(f"adminhope_flex_fieldsfieldset{fieldset.pk}")


def test_invalidate_fieldset_fields_admin_cache_on_flexfield_update():
fieldset = FieldsetFactory.create()
flex_field = FlexFieldFactory.create(fieldset=fieldset, name="test_field")

with patch("country_workspace.signals.cache_manager.invalidate_containing") as mocked:
flex_field.name = "updated_name"
flex_field.save()
mocked.assert_called_once_with(f"adminhope_flex_fieldsfieldset{fieldset.pk}")


def test_invalidate_fieldset_fields_admin_cache_no_fieldset():
instance = MagicMock()
instance.fieldset = None

with patch("country_workspace.signals.cache_manager.invalidate_containing") as mocked:
invalidate_fieldset_fields_admin_cache(sender=MagicMock, instance=instance)
mocked.assert_not_called()
Loading