feat: optimize context processing
This commit is contained in:
parent
d73a9e482d
commit
e68e6ccefe
3 changed files with 138 additions and 9 deletions
|
|
@ -4,6 +4,7 @@ import os
|
|||
from typing import Iterable, List
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
from django.urls import resolve
|
||||
|
||||
from .models import UserConfig, AppView
|
||||
|
|
@ -11,6 +12,7 @@ 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
|
||||
|
|
@ -55,6 +57,7 @@ def current_app_view(request):
|
|||
|
||||
Only 'sign' and 'documents' have their own dedicated views.
|
||||
All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view.
|
||||
Results are cached to avoid hitting the database on every page render.
|
||||
"""
|
||||
current_view = None
|
||||
|
||||
|
|
@ -67,6 +70,11 @@ def current_app_view(request):
|
|||
namespace = resolved.namespace # e.g., 'sign', 'home', 'assets'
|
||||
|
||||
if namespace:
|
||||
cache_key = f"appview_for_{namespace}"
|
||||
cached_view = cache.get(cache_key)
|
||||
if cached_view is not None:
|
||||
current_view = None if cached_view is False else cached_view
|
||||
else:
|
||||
if namespace in DEDICATED_VIEW_APPS:
|
||||
# These apps have their own dedicated view
|
||||
current_view = AppView.objects.filter(url_name__startswith=f"{namespace}:").first()
|
||||
|
|
@ -74,6 +82,8 @@ def current_app_view(request):
|
|||
# All other apps (home, assets, interventions, controls, contracts, etc.)
|
||||
# should show 'asset_management' view
|
||||
current_view = AppView.objects.filter(code='asset_management').first()
|
||||
|
||||
cache.set(cache_key, current_view if current_view is not None else False, 3600)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -88,6 +98,7 @@ 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,
|
||||
|
|
@ -98,6 +109,7 @@ 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,
|
||||
|
|
@ -140,15 +152,83 @@ def linked_accounts(request):
|
|||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
if cached_bounds and cached_bounds.get('user_id') == request.user.pk:
|
||||
center = cached_bounds.get('center', center)
|
||||
zoom = cached_bounds.get('zoom', zoom)
|
||||
extent = cached_bounds.get('extent', extent)
|
||||
else:
|
||||
try:
|
||||
user_config = getattr(request.user, 'config', None)
|
||||
if user_config:
|
||||
# Récupérer les contrats accessibles ayant des communes associées
|
||||
contracts_to_check = []
|
||||
if user_config.default_contract and user_config.default_contract.municipalities.exists():
|
||||
contracts_to_check.append(user_config.default_contract)
|
||||
else:
|
||||
contracts_to_check = list(user_config.contracts.prefetch_related('municipalities').filter(municipalities__isnull=False).distinct())
|
||||
|
||||
geoms_4326 = []
|
||||
for c in contracts_to_check:
|
||||
for m in c.municipalities.exclude(geom=None):
|
||||
g = m.geom.transform(4326, clone=True) if m.geom.srid != 4326 else m.geom
|
||||
geoms_4326.append(g)
|
||||
|
||||
if geoms_4326:
|
||||
min_lon = min(g.extent[0] for g in geoms_4326)
|
||||
min_lat = min(g.extent[1] for g in geoms_4326)
|
||||
max_lon = max(g.extent[2] for g in geoms_4326)
|
||||
max_lat = max(g.extent[3] for g in geoms_4326)
|
||||
|
||||
# Léger buffer (5% d'expansion de chaque côté pour aérer le cadre)
|
||||
delta_lon = (max_lon - min_lon) * 0.05
|
||||
delta_lat = (max_lat - min_lat) * 0.05
|
||||
|
||||
center = [round((min_lon + max_lon) / 2.0, 5), round((min_lat + max_lat) / 2.0, 5)]
|
||||
extent = [
|
||||
[round(min_lon - delta_lon, 5), round(min_lat - delta_lat, 5)],
|
||||
[round(max_lon + delta_lon, 5), round(max_lat + delta_lat, 5)]
|
||||
]
|
||||
|
||||
span = max(max_lon - min_lon, max_lat - min_lat)
|
||||
if span < 0.08:
|
||||
zoom = 14
|
||||
elif span < 0.2:
|
||||
zoom = 13
|
||||
elif span < 0.5:
|
||||
zoom = 12
|
||||
else:
|
||||
zoom = 11
|
||||
|
||||
if session is not None:
|
||||
session[SESSION_MAP_BOUNDS_KEY] = {
|
||||
'user_id': request.user.pk,
|
||||
'center': center,
|
||||
'zoom': zoom,
|
||||
'extent': extent,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
'product_name': getattr(settings, 'PRODUCT_NAME', 'Loko'),
|
||||
'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'Loko'),
|
||||
'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'),
|
||||
'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'),
|
||||
'map_default_center': [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)],
|
||||
'map_default_zoom': getattr(settings, 'MAP_DEFAULT_ZOOM', 11),
|
||||
'map_default_extent': getattr(settings, 'MAP_DEFAULT_EXTENT', None),
|
||||
'map_default_center': center,
|
||||
'map_default_zoom': zoom,
|
||||
'map_default_extent': extent,
|
||||
'map_overlay_wms_url': getattr(settings, 'MAP_OVERLAY_WMS_URL', ''),
|
||||
'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''),
|
||||
'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''),
|
||||
|
|
|
|||
|
|
@ -390,8 +390,55 @@ class ScheduledCommandResolveOutputFileTests(TestCase):
|
|||
|
||||
mock_attach.assert_called_once_with(fake_path)
|
||||
mock_send.assert_called_once()
|
||||
from django.test import RequestFactory
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
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
|
||||
|
||||
|
||||
class ContextProcessorsCachingTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.factory = RequestFactory()
|
||||
self.user = User.objects.create_user(username="context_user", password="password123")
|
||||
self.config = UserConfig.objects.create(user=self.user)
|
||||
|
||||
def _add_session_to_request(self, request):
|
||||
middleware = SessionMiddleware(lambda req: None)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
def test_current_app_view_caches_result(self):
|
||||
AppView.objects.create(code="asset_management", name_fr="Gestion des actifs", name_nl="Activa beheer", url_name="home:index")
|
||||
request = self.factory.get("/")
|
||||
|
||||
with self.assertNumQueries(1):
|
||||
ctx1 = current_app_view(request)
|
||||
self.assertIsNotNone(ctx1["current_view"])
|
||||
|
||||
# Second call should use cache and perform 0 database queries
|
||||
with self.assertNumQueries(0):
|
||||
ctx2 = current_app_view(request)
|
||||
self.assertEqual(ctx1["current_view"], ctx2["current_view"])
|
||||
|
||||
def test_organization_info_caches_in_session(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)
|
||||
|
||||
# Subsequent call with session already containing user_map_bounds does not query or recalculate
|
||||
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"])
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from urllib.parse import urlencode
|
|||
from common.context_processors import (
|
||||
SESSION_ALLOWED_IDS_KEY,
|
||||
SESSION_ORIGIN_ID_KEY,
|
||||
SESSION_MAP_BOUNDS_KEY,
|
||||
compute_initial_allowed_ids,
|
||||
)
|
||||
|
||||
|
|
@ -287,6 +288,7 @@ def switch_account(request):
|
|||
|
||||
session[SESSION_ALLOWED_IDS_KEY] = allowed_ids
|
||||
session[SESSION_ORIGIN_ID_KEY] = origin_user_id
|
||||
session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
|
||||
return redirect(origin_next or reverse("home:index"))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue