- 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>
134 lines
4.6 KiB
Python
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
|