mirror of https://github.com/interlegis/sapl.git
Browse Source
Add AnonCachePageMixin (sapl/middleware/page_cache.py) that stores full view responses in the default Redis cache for anonymous (unauthenticated) GET requests only. Authenticated users always bypass the cache so CSRF tokens and user-specific UI controls are never served stale. Applied to: - ParlamentarCrud.ListView / DetailView — TTL 600 s (changes each term) - AudienciaCrud.ListView — TTL 120 s (hearings added infrequently) - ComissaoCrud.ListView — TTL 300 s (committees change rarely) Also: - Add PAGE_CACHE_TTL_LIST/DETAIL/STABLE settings (env-configurable) - Add bingbot + SERankingBacklinksBot to nginx UA blocklist (were already in BOT_UA_FRAGMENTS / robots.txt; nginx map was the only gap) - Remove unused ratelimit/method_decorator/RATE_LIMITER_RATE imports from audiencia/views.py that crept in during Phase 2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>rate-limiter-2026
6 changed files with 104 additions and 8 deletions
@ -0,0 +1,75 @@ |
|||
""" |
|||
AnonCachePageMixin — anonymous-only Django view response caching. |
|||
|
|||
Why anonymous-only? |
|||
- Authenticated responses include CSRF tokens and user-specific UI fragments |
|||
(edit/delete action buttons injected by SAPL's CRUD framework). Caching |
|||
those would serve stale or wrong data to other users. |
|||
- Bot traffic is entirely anonymous. A 2-minute cache converts hundreds of |
|||
identical list-view DB queries into a single one — exactly the workload |
|||
that triggers OOM in the fleet. |
|||
|
|||
How it works: |
|||
- `dispatch()` short-circuits to the normal (uncached) path for any |
|||
authenticated request. |
|||
- For anonymous GET/HEAD requests, the response is stored in the 'default' |
|||
Redis cache under a key that includes the full URL (scheme + host + path + |
|||
query string). The Django cache framework handles key construction and |
|||
TTL expiry automatically. |
|||
- HTTPS and HTTP requests are stored under separate keys (Django default). |
|||
|
|||
Usage: |
|||
|
|||
from sapl.middleware.page_cache import AnonCachePageMixin |
|||
|
|||
class MyListView(AnonCachePageMixin, ListView): |
|||
anon_cache_ttl = settings.PAGE_CACHE_TTL_LIST # 120 s |
|||
|
|||
class MyDetailView(AnonCachePageMixin, DetailView): |
|||
anon_cache_ttl = settings.PAGE_CACHE_TTL_DETAIL # 300 s |
|||
|
|||
TTL reference (see settings.PAGE_CACHE_TTL_*): |
|||
|
|||
View type Default TTL |
|||
───────────────────────────────────────────────────── |
|||
Public list (norma, materia, sessao…) 120 s (PAGE_CACHE_TTL_LIST) |
|||
Public detail (norma, materia, sessao…) 300 s (PAGE_CACHE_TTL_DETAIL) |
|||
Stable detail (parlamentar, comissão) 600 s (PAGE_CACHE_TTL_STABLE) |
|||
|
|||
Invalidation: |
|||
The cache TTL is intentionally short (≤ 10 min) so stale content expires |
|||
on its own. Explicit invalidation is not implemented — legislative data |
|||
changes infrequently and short TTLs are acceptable. |
|||
""" |
|||
|
|||
from django.conf import settings |
|||
from django.views.decorators.cache import cache_page, never_cache |
|||
from django.utils.decorators import method_decorator |
|||
|
|||
|
|||
class AnonCachePageMixin: |
|||
""" |
|||
Cache the full view response for anonymous (unauthenticated) requests. |
|||
|
|||
Set `anon_cache_ttl` on the subclass to override the default TTL. |
|||
Authenticated requests always bypass the cache. |
|||
""" |
|||
|
|||
# Override per view class. Use settings.PAGE_CACHE_TTL_* for consistency. |
|||
anon_cache_ttl = getattr(settings, 'PAGE_CACHE_TTL_LIST', 120) |
|||
|
|||
def dispatch(self, request, *args, **kwargs): |
|||
if getattr(request, 'user', None) and request.user.is_authenticated: |
|||
# Authenticated: skip cache entirely — response may contain |
|||
# user-specific controls (CSRF token, edit/delete buttons). |
|||
handler = never_cache( |
|||
lambda req, *a, **kw: super(AnonCachePageMixin, self).dispatch(req, *a, **kw) |
|||
) |
|||
return handler(request, *args, **kwargs) |
|||
|
|||
# Anonymous: wrap the parent dispatch in cache_page so Django stores |
|||
# the rendered response in the 'default' cache for anon_cache_ttl seconds. |
|||
handler = cache_page(self.anon_cache_ttl)( |
|||
lambda req, *a, **kw: super(AnonCachePageMixin, self).dispatch(req, *a, **kw) |
|||
) |
|||
return handler(request, *args, **kwargs) |
|||
Loading…
Reference in new issue