Skip to content

Commit 27e882e

Browse files
committed
Add cache_page and simplify etag see HEA-872
1 parent c94456d commit 27e882e

2 files changed

Lines changed: 6 additions & 127 deletions

File tree

apps/baseline/viewsets.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from django.conf import settings
33
from django.db import models
44
from django.db.models import Q, Subquery
5+
from django.utils.decorators import method_decorator
56
from django.utils.translation import override
7+
from django.views.decorators.cache import cache_page
8+
from django.views.decorators.http import etag
69
from django_filters import rest_framework as filters
710
from django_filters.filters import CharFilter
811
from rest_framework.permissions import AllowAny
@@ -12,7 +15,7 @@
1215

1316
from common.fields import translation_fields
1417
from common.filters import MultiFieldFilter, UpperCaseFilter
15-
from common.utils import etag_response
18+
from common.utils import get_etag_for_cachedrequest
1619
from common.viewsets import AggregatingViewSet, BaseModelViewSet
1720

1821
from .models import (
@@ -265,7 +268,8 @@ def get_serializer_class(self):
265268
return LivelihoodZoneBaselineGeoSerializer # Use GeoFeatureModelSerializer for GeoJSON
266269
return LivelihoodZoneBaselineSerializer
267270

268-
@etag_response()
271+
@method_decorator(cache_page(60 * 60 * 24)) # Cache on server for 24 hours
272+
@method_decorator(etag(get_etag_for_cachedrequest))
269273
def list(self, request, *args, **kwargs):
270274
return super().list(request, *args, **kwargs)
271275

apps/common/utils.py

Lines changed: 0 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,19 @@
44
import importlib
55
import logging
66
import re
7-
import time
87
from copy import deepcopy
98
from datetime import datetime, timedelta
10-
from functools import wraps
119
from io import BytesIO, StringIO
1210
from pathlib import Path
1311
from urllib.parse import parse_qs, urlencode, urlparse
1412

1513
import pandas as pd
1614
from django.apps import apps
17-
from django.db import connection, reset_queries
1815
from django.db.migrations.operations.base import Operation
1916
from django.forms.models import modelform_factory
2017
from django.utils.cache import set_response_etag
2118
from django.utils.timezone import now
2219
from openpyxl.utils import get_column_letter
23-
from rest_framework.response import Response
2420
from treebeard.mp_tree import MP_Node
2521

2622
logger = logging.getLogger(__name__)
@@ -396,10 +392,6 @@ def timekeeper():
396392
def get_etag_for_cachedrequest(request, *args, **kwargs):
397393
"""
398394
Generate an ETag for the request based on path and query parameters.
399-
400-
This is a simplified version that generates ETags without requiring a CachedRequest model
401-
or permissions system. The ETag is deterministic based on the request URL.
402-
403395
If the request includes a _refresh parameter, returns None to force a cache miss.
404396
"""
405397
u = urlparse(request.get_full_path())
@@ -435,120 +427,3 @@ def set_etag_for_response(response):
435427
logger.info("Created etag header in %s seconds" % round(t.elapsed().total_seconds(), 2))
436428

437429
return response
438-
439-
440-
def etag_response(etag_func=None, enable_etag=True):
441-
"""
442-
Decorator that adds ETag support with 304 Not Modified responses for DRF viewsets.
443-
"""
444-
445-
if etag_func is None:
446-
etag_func = get_etag_for_cachedrequest
447-
448-
def decorator(view_func):
449-
@wraps(view_func)
450-
def wrapped_view(self, request, *args, **kwargs):
451-
start_time = time.time()
452-
453-
# Reset query count for accurate measurement
454-
reset_queries()
455-
initial_query_count = len(connection.queries)
456-
457-
# Generate ETag based on request using existing function
458-
etag_start = time.time()
459-
etag = etag_func(request, *args, **kwargs)
460-
etag_time = time.time() - etag_start
461-
462-
# Check if ETag caching is enabled and client sent matching ETag
463-
client_etag = request.META.get("HTTP_IF_NONE_MATCH")
464-
465-
# Normalize ETags for comparison (handle weak ETags with W/ prefix)
466-
def normalize_etag(etag_value):
467-
"""Strip W/ prefix from weak ETags for comparison."""
468-
if etag_value:
469-
return etag_value.replace("W/", "").strip()
470-
return etag_value
471-
472-
normalized_client_etag = normalize_etag(client_etag)
473-
normalized_server_etag = normalize_etag(etag)
474-
475-
# Log for debugging browser issues
476-
if client_etag:
477-
logger.debug(
478-
f"Client ETag: {client_etag} (normalized: {normalized_client_etag}), "
479-
f"Server ETag: {etag} (normalized: {normalized_server_etag})"
480-
)
481-
482-
if enable_etag and etag and normalized_client_etag == normalized_server_etag:
483-
# Cache HIT - return 304
484-
response = Response(status=304)
485-
response["ETag"] = etag
486-
# Ensure Vary header is set for browser caching
487-
response["Vary"] = "Accept, Accept-Language, Cookie"
488-
489-
elapsed_time = time.time() - start_time
490-
query_count = len(connection.queries) - initial_query_count
491-
492-
logger.info(
493-
f"[ETAG HIT] "
494-
f"path={request.path} | "
495-
f"etag={etag[:12]}... | "
496-
f"time={elapsed_time:.3f}s | "
497-
f"etag_gen={etag_time:.3f}s | "
498-
f"queries={query_count} | "
499-
f"size=0 bytes | "
500-
f"status=304"
501-
)
502-
return response
503-
elif enable_etag and client_etag and etag:
504-
# Client sent ETag but it doesn't match
505-
logger.debug(f"[ETAG MISMATCH] Client: {client_etag} vs Server: {etag}")
506-
507-
# Cache MISS or ETag disabled - render full response
508-
render_start = time.time()
509-
response = view_func(self, request, *args, **kwargs)
510-
render_time = time.time() - render_start
511-
512-
if enable_etag and etag:
513-
response["ETag"] = etag
514-
response["Cache-Control"] = "public, max-age=2592000" # 30 days
515-
existing_vary = response.get("Vary", "")
516-
if "Accept" not in existing_vary:
517-
response["Vary"] = "Accept, Accept-Language, Cookie"
518-
519-
# Calculate metrics
520-
elapsed_time = time.time() - start_time
521-
query_count = len(connection.queries) - initial_query_count
522-
523-
# Get response size safely
524-
response_size = 0
525-
try:
526-
# Try to get size from data attribute first (DRF Response)
527-
if hasattr(response, "data"):
528-
# Estimate size from data length
529-
response_size = len(str(response.data))
530-
elif hasattr(response, "content"):
531-
response_size = len(response.content)
532-
except (AssertionError, AttributeError):
533-
# If we can't determine size, just log 0
534-
response_size = 0
535-
536-
# Log with different prefix based on whether ETag is enabled
537-
log_prefix = "[ETAG MISS]" if enable_etag else "[NO ETAG]"
538-
logger.info(
539-
f"{log_prefix} "
540-
f"path={request.path} | "
541-
f"etag={etag[:12] if etag else 'None'}... | "
542-
f"time={elapsed_time:.3f}s | "
543-
f"etag_gen={etag_time:.3f}s | "
544-
f"render={render_time:.3f}s | "
545-
f"queries={query_count} | "
546-
f"size={response_size:,} bytes (estimated) | "
547-
f"status={response.status_code}"
548-
)
549-
550-
return response
551-
552-
return wrapped_view
553-
554-
return decorator

0 commit comments

Comments
 (0)