refactor: migrate user map bounds caching from session to Django cache with signal-based invalidation
This commit is contained in:
parent
73738b6757
commit
cc0facdf92
4 changed files with 110 additions and 20 deletions
|
|
@ -11,6 +11,9 @@ class CommonConfig(AppConfig):
|
|||
from .middleware import set_login_time
|
||||
user_logged_in.connect(set_login_time)
|
||||
|
||||
# Connecter les signaux de common (ex: invalidation de cache)
|
||||
import common.signals # noqa
|
||||
|
||||
# Charger les extensions conditionnelles
|
||||
try:
|
||||
from common import bm_extensions
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from .models import UserConfig, AppView
|
|||
|
||||
SESSION_ALLOWED_IDS_KEY = "account_switch_allowed_user_ids"
|
||||
SESSION_ORIGIN_ID_KEY = "account_switch_origin_user_id"
|
||||
SESSION_MAP_BOUNDS_KEY = "user_map_bounds"
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -98,7 +97,6 @@ def linked_accounts(request):
|
|||
if not user or isinstance(user, AnonymousUser) or not user.is_authenticated:
|
||||
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
|
||||
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
|
||||
request.session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
return {
|
||||
"switchable_accounts": [],
|
||||
"account_switch_origin_user_id": None,
|
||||
|
|
@ -109,7 +107,6 @@ def linked_accounts(request):
|
|||
except UserConfig.DoesNotExist:
|
||||
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
|
||||
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
|
||||
request.session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
return {
|
||||
"switchable_accounts": [],
|
||||
"account_switch_origin_user_id": None,
|
||||
|
|
@ -149,22 +146,31 @@ def linked_accounts(request):
|
|||
}
|
||||
|
||||
|
||||
def invalidate_user_map_bounds(user_id: int) -> None:
|
||||
"""
|
||||
Invalidates the cached map center/zoom/extent for a given user.
|
||||
Called when UserConfig or UserContractAccess changes.
|
||||
"""
|
||||
if user_id:
|
||||
cache.delete(f"user_map_bounds_{user_id}")
|
||||
|
||||
|
||||
def organization_info(request):
|
||||
"""
|
||||
Context processor that exposes organization details from settings.
|
||||
Dynamically adapts map_default_center, map_default_zoom and map_default_extent
|
||||
when the authenticated user has an assigned contract associated with a municipality.
|
||||
Caches calculated bounds in session to avoid heavy GIS and SQL processing on every page.
|
||||
Caches calculated bounds in Django cache to avoid heavy GIS and SQL processing on every page.
|
||||
"""
|
||||
center = [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)]
|
||||
zoom = getattr(settings, 'MAP_DEFAULT_ZOOM', 11)
|
||||
extent = getattr(settings, 'MAP_DEFAULT_EXTENT', None)
|
||||
|
||||
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||
session = getattr(request, 'session', None)
|
||||
cached_bounds = session.get(SESSION_MAP_BOUNDS_KEY) if session is not None else None
|
||||
cache_key = f"user_map_bounds_{request.user.pk}"
|
||||
cached_bounds = cache.get(cache_key)
|
||||
|
||||
if cached_bounds and cached_bounds.get('user_id') == request.user.pk:
|
||||
if cached_bounds is not None:
|
||||
center = cached_bounds.get('center', center)
|
||||
zoom = cached_bounds.get('zoom', zoom)
|
||||
extent = cached_bounds.get('extent', extent)
|
||||
|
|
@ -211,13 +217,11 @@ def organization_info(request):
|
|||
else:
|
||||
zoom = 11
|
||||
|
||||
if session is not None:
|
||||
session[SESSION_MAP_BOUNDS_KEY] = {
|
||||
'user_id': request.user.pk,
|
||||
'center': center,
|
||||
'zoom': zoom,
|
||||
'extent': extent,
|
||||
}
|
||||
cache.set(cache_key, {
|
||||
'center': center,
|
||||
'zoom': zoom,
|
||||
'extent': extent,
|
||||
}, 86400)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
37
loko/common/signals.py
Normal file
37
loko/common/signals.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import logging
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import UserConfig, UserContractAccess
|
||||
from .context_processors import invalidate_user_map_bounds
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=UserConfig)
|
||||
@receiver(post_delete, sender=UserConfig)
|
||||
def invalidate_map_bounds_on_userconfig_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Invalidates the cached map center/zoom/extent when UserConfig is modified or deleted
|
||||
(e.g., when default_contract changes).
|
||||
"""
|
||||
if instance.user_id:
|
||||
invalidate_user_map_bounds(instance.user_id)
|
||||
|
||||
|
||||
@receiver(post_save, sender=UserContractAccess)
|
||||
@receiver(post_delete, sender=UserContractAccess)
|
||||
def invalidate_map_bounds_on_contract_access_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Invalidates the cached map center/zoom/extent when user contract accesses are added or removed.
|
||||
"""
|
||||
try:
|
||||
user_id = instance.user_config.user_id if instance.user_config else None
|
||||
if user_id:
|
||||
invalidate_user_map_bounds(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to invalidate map bounds for UserContractAccess %s: %s",
|
||||
getattr(instance, 'pk', None),
|
||||
e
|
||||
)
|
||||
|
|
@ -396,7 +396,6 @@ from django.core.cache import cache
|
|||
from common.context_processors import (
|
||||
current_app_view,
|
||||
organization_info,
|
||||
SESSION_MAP_BOUNDS_KEY,
|
||||
)
|
||||
from common.models import AppView
|
||||
|
||||
|
|
@ -426,19 +425,66 @@ class ContextProcessorsCachingTests(TestCase):
|
|||
ctx2 = current_app_view(request)
|
||||
self.assertEqual(ctx1["current_view"], ctx2["current_view"])
|
||||
|
||||
def test_organization_info_caches_in_session(self):
|
||||
def test_organization_info_caches_in_django_cache(self):
|
||||
request = self.factory.get("/")
|
||||
self._add_session_to_request(request)
|
||||
request.user = self.user
|
||||
|
||||
ctx1 = organization_info(request)
|
||||
self.assertIn(SESSION_MAP_BOUNDS_KEY, request.session)
|
||||
self.assertEqual(request.session[SESSION_MAP_BOUNDS_KEY]["user_id"], self.user.pk)
|
||||
cache_key = f"user_map_bounds_{self.user.pk}"
|
||||
self.assertIsNone(cache.get(cache_key))
|
||||
|
||||
# Subsequent call with session already containing user_map_bounds does not query or recalculate
|
||||
ctx1 = organization_info(request)
|
||||
self.assertIsNotNone(cache.get(cache_key))
|
||||
|
||||
# Subsequent call with cache populated performs 0 queries
|
||||
with self.assertNumQueries(0):
|
||||
ctx2 = organization_info(request)
|
||||
self.assertEqual(ctx1["map_default_center"], ctx2["map_default_center"])
|
||||
self.assertEqual(ctx1["map_default_zoom"], ctx2["map_default_zoom"])
|
||||
self.assertEqual(ctx1["map_default_extent"], ctx2["map_default_extent"])
|
||||
|
||||
def test_user_config_save_invalidates_cache(self):
|
||||
request = self.factory.get("/")
|
||||
self._add_session_to_request(request)
|
||||
request.user = self.user
|
||||
|
||||
cache_key = f"user_map_bounds_{self.user.pk}"
|
||||
organization_info(request)
|
||||
self.assertIsNotNone(cache.get(cache_key))
|
||||
|
||||
# Modifying and saving UserConfig should invalidate the cached map bounds via signal
|
||||
self.config.save()
|
||||
self.assertIsNone(cache.get(cache_key))
|
||||
|
||||
def test_user_contract_access_invalidates_cache(self):
|
||||
from datetime import date
|
||||
from contracts.models import Contract, Company
|
||||
from common.models import UserContractAccess
|
||||
|
||||
company = Company.objects.create(name="Company Test")
|
||||
contract = Contract.objects.create(
|
||||
company=company,
|
||||
contract_number="CTR-TEST-CACHE",
|
||||
start_date=date(2026, 1, 1),
|
||||
end_date=date(2026, 12, 31),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
self._add_session_to_request(request)
|
||||
request.user = self.user
|
||||
|
||||
cache_key = f"user_map_bounds_{self.user.pk}"
|
||||
organization_info(request)
|
||||
self.assertIsNotNone(cache.get(cache_key))
|
||||
|
||||
# Creating UserContractAccess invalidates cache
|
||||
access = UserContractAccess.objects.create(user_config=self.config, contract=contract)
|
||||
self.assertIsNone(cache.get(cache_key))
|
||||
|
||||
# Re-populate cache
|
||||
organization_info(request)
|
||||
self.assertIsNotNone(cache.get(cache_key))
|
||||
|
||||
# Deleting UserContractAccess invalidates cache
|
||||
access.delete()
|
||||
self.assertIsNone(cache.get(cache_key))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue