fix public ressources not being public

This commit is contained in:
Gérald Colangelo
2026-04-02 12:35:07 +02:00
parent 2a86165c10
commit 990db0f265
5 changed files with 58 additions and 16 deletions

View File

@@ -1,9 +1,27 @@
from django.shortcuts import get_object_or_404
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
from rest_framework.response import Response
class IsOwnerOrReadPublic(BasePermission):
"""
Read-only access for anonymous users (public sessions only).
Authenticated users can only mutate their own sessions.
"""
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return obj.is_public or (
request.user.is_authenticated and obj.user == request.user
)
return request.user.is_authenticated and obj.user == request.user
from .ballistics import compute_corrections
from .models import FreePracticeSession, PRSSession, PRSStage, SpeedShootingSession
from .serializers import (
@@ -23,17 +41,18 @@ from .serializers import (
# ── PRS ───────────────────────────────────────────────────────────────────────
class PRSSessionViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
permission_classes = [IsOwnerOrReadPublic]
filterset_fields = ['date', 'is_public']
search_fields = ['name', 'location', 'notes', 'competition_name']
ordering_fields = ['date', 'created_at']
def get_queryset(self):
qs = (
PRSSession.objects
.filter(user=self.request.user)
.select_related('rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis')
)
user = self.request.user
if not user.is_authenticated:
qs = PRSSession.objects.filter(is_public=True)
else:
qs = PRSSession.objects.filter(user=user)
qs = qs.select_related('rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis')
if self.action != 'list':
qs = qs.prefetch_related('stages')
return qs
@@ -94,15 +113,20 @@ class PRSSessionViewSet(viewsets.ModelViewSet):
# ── Free Practice ─────────────────────────────────────────────────────────────
class FreePracticeSessionViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
permission_classes = [IsOwnerOrReadPublic]
filterset_fields = ['date', 'is_public']
search_fields = ['name', 'location', 'notes']
ordering_fields = ['date', 'created_at']
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
return FreePracticeSession.objects.filter(is_public=True).select_related(
'rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis'
)
return (
FreePracticeSession.objects
.filter(user=self.request.user)
.filter(user=user)
.select_related('rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis')
)
@@ -120,15 +144,20 @@ class FreePracticeSessionViewSet(viewsets.ModelViewSet):
# ── Speed Shooting ────────────────────────────────────────────────────────────
class SpeedShootingSessionViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
permission_classes = [IsOwnerOrReadPublic]
filterset_fields = ['date', 'is_public']
search_fields = ['name', 'location', 'notes', 'format']
ordering_fields = ['date', 'created_at']
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
return SpeedShootingSession.objects.filter(is_public=True).select_related(
'rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis'
)
return (
SpeedShootingSession.objects
.filter(user=self.request.user)
.filter(user=user)
.select_related('rig', 'ammo', 'reloaded_batch__recipe', 'reloaded_batch__powder', 'analysis')
)

View File

@@ -171,6 +171,11 @@ class ChronographAnalysisDetailSerializer(serializers.ModelSerializer):
read_only_fields = ['user', 'created_at', 'updated_at']
def validate(self, attrs):
instance = ChronographAnalysis(**attrs)
instance.clean()
# For partial updates, fall back to the existing instance's name
if self.instance is not None:
name = attrs.get('name', self.instance.name)
else:
name = attrs.get('name')
if not name or not str(name).strip():
raise serializers.ValidationError({'name': 'Name may not be blank.'})
return attrs

View File

@@ -44,8 +44,8 @@ services:
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# ports:
#- "5173:80"
ports:
- "5173:5000"
depends_on:
- app
networks:

View File

@@ -6,7 +6,7 @@
const path = window.location.pathname;
const PROTECTED = ['/dashboard.html', '/gears.html', '/reloads.html',
'/profile.html', '/sessions.html', '/admin.html',
'/profile.html', '/admin.html',
'/messages.html', '/friends.html'];
const ADMIN_ONLY = ['/admin.html'];
const AUTH_ONLY = ['/login.html', '/register.html'];

View File

@@ -131,6 +131,9 @@ function renderDetail() {
const s = currentSession;
const titleLabel = s.competition_name || s.name || '(unnamed)';
document.getElementById('detailTitle').textContent = titleLabel;
// Hide write controls for anonymous viewers
document.getElementById('togglePublicBtn').classList.toggle('d-none', !_isAuth);
document.getElementById('deleteSessionBtn').classList.toggle('d-none', !_isAuth);
document.getElementById('detailMeta').textContent =
[s.date, s.location].filter(Boolean).join(' · ');
@@ -1080,5 +1083,10 @@ function esc(s) {
// ── Init ──────────────────────────────────────────────────────────────────────
const _isAuth = !!getAccess();
if (!_isAuth) {
document.getElementById('newSessionBtn').classList.add('d-none');
}
applyTranslations();
loadSessions();