Files
training-software/attendance/views.py
Paperclip CTO 72e319767f
Some checks failed
CI / lint (push) Failing after 46s
CI / test (push) Failing after 11s
CI / build-container (push) Has been skipped
feat(TRA-368): add attendance app with check-in/check-out core
- AttendanceSession model with open/closed status, org_id, timestamps
- POST /api/v1/attendance/check-in/ – creates session, rejects duplicate open check-ins (409)
- POST /api/v1/attendance/check-out/ – closes open session, validates checkout > checkin
- GET /api/v1/attendance/status/ – current open session for requesting user
- GET /api/v1/attendance/history/ – paginated session history per user
- GET /api/v1/attendance/admin/sessions/ – admin view for managers/admins with filters
- Initial migration with composite indexes on (user, status) and (user, checked_in_at)
- 18 test cases covering core flows and edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-18 12:11:16 +02:00

134 lines
4.6 KiB
Python

from django.utils import timezone
from rest_framework import status
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.permissions import IsAdminOrManager
from .models import AttendanceSession
from .serializers import AttendanceSessionSerializer, CheckInSerializer, CheckOutSerializer
class CheckInView(APIView):
"""POST /api/v1/attendance/check-in/ — open a new attendance session."""
permission_classes = [IsAuthenticated]
def post(self, request):
open_session = AttendanceSession.objects.filter(
user=request.user, status=AttendanceSession.STATUS_OPEN
).first()
if open_session is not None:
return Response(
{
"detail": "Already checked in.",
"session": AttendanceSessionSerializer(open_session).data,
},
status=status.HTTP_409_CONFLICT,
)
serializer = CheckInSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
checked_in_at = data.get("checked_in_at") or timezone.now()
session = AttendanceSession.objects.create(
user=request.user,
org_id=data.get("org_id"),
checked_in_at=checked_in_at,
notes=data.get("notes", ""),
status=AttendanceSession.STATUS_OPEN,
)
return Response(AttendanceSessionSerializer(session).data, status=status.HTTP_201_CREATED)
class CheckOutView(APIView):
"""POST /api/v1/attendance/check-out/ — close the current open session."""
permission_classes = [IsAuthenticated]
def post(self, request):
open_session = AttendanceSession.objects.filter(
user=request.user, status=AttendanceSession.STATUS_OPEN
).first()
if open_session is None:
return Response(
{"detail": "No open check-in found."},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CheckOutSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
checked_out_at = data.get("checked_out_at") or timezone.now()
if checked_out_at <= open_session.checked_in_at:
return Response(
{"detail": "Check-out time must be after check-in time."},
status=status.HTTP_400_BAD_REQUEST,
)
if data.get("notes"):
open_session.notes = data["notes"]
open_session.close(checked_out_at=checked_out_at)
return Response(AttendanceSessionSerializer(open_session).data, status=status.HTTP_200_OK)
class AttendanceStatusView(APIView):
"""GET /api/v1/attendance/status/ — current check-in status for the requesting user."""
permission_classes = [IsAuthenticated]
def get(self, request):
open_session = AttendanceSession.objects.filter(
user=request.user, status=AttendanceSession.STATUS_OPEN
).first()
if open_session is None:
return Response({"checked_in": False, "session": None})
return Response(
{
"checked_in": True,
"session": AttendanceSessionSerializer(open_session).data,
}
)
class AttendanceHistoryView(ListAPIView):
"""GET /api/v1/attendance/history/ — paginated closed sessions for the requesting user."""
permission_classes = [IsAuthenticated]
serializer_class = AttendanceSessionSerializer
def get_queryset(self):
return (
AttendanceSession.objects.filter(user=self.request.user)
.select_related("user")
.order_by("-checked_in_at")
)
class AdminAttendanceListView(ListAPIView):
"""GET /api/v1/attendance/admin/sessions/ — all sessions (managers/admins only)."""
permission_classes = [IsAuthenticated, IsAdminOrManager]
serializer_class = AttendanceSessionSerializer
def get_queryset(self):
qs = AttendanceSession.objects.select_related("user").order_by("-checked_in_at")
user_id = self.request.query_params.get("user_id")
org_id = self.request.query_params.get("org_id")
session_status = self.request.query_params.get("status")
if user_id:
qs = qs.filter(user_id=user_id)
if org_id:
qs = qs.filter(org_id=org_id)
if session_status:
qs = qs.filter(status=session_status)
return qs