From 990db0f2652070e49570e0b4c41ccaad84b13b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Colangelo?= Date: Thu, 2 Apr 2026 12:35:07 +0200 Subject: [PATCH] fix public ressources not being public --- apps/sessions/views.py | 51 ++++++++++++++++++++++++++++++--------- apps/tools/serializers.py | 9 +++++-- docker-compose.yml | 4 +-- frontend/js/nav.js | 2 +- frontend/js/sessions.js | 8 ++++++ 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/apps/sessions/views.py b/apps/sessions/views.py index a21d2d5..55e6aac 100644 --- a/apps/sessions/views.py +++ b/apps/sessions/views.py @@ -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') ) diff --git a/apps/tools/serializers.py b/apps/tools/serializers.py index f5035bf..8c85119 100644 --- a/apps/tools/serializers.py +++ b/apps/tools/serializers.py @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 7881767..fd52678 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/js/nav.js b/frontend/js/nav.js index 4753609..64b67db 100644 --- a/frontend/js/nav.js +++ b/frontend/js/nav.js @@ -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']; diff --git a/frontend/js/sessions.js b/frontend/js/sessions.js index aeb9d95..6790487 100644 --- a/frontend/js/sessions.js +++ b/frontend/js/sessions.js @@ -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();