from rest_framework import status from rest_framework.viewsets import ModelViewSet from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework.filters import SearchFilter, OrderingFilter from django_filters.rest_framework import DjangoFilterBackend from core.paginations.limit_offset import CustomLimitOffsetPagination from apps.time_entries.models import TimeEntry from apps.time_entries.api.serializers import ( TimeEntrySerializer, TimeEntryCreateSerializer, TimeEntryUpdateSerializer, TimeEntryStopSerializer ) from apps.time_entries.services.time_entries import ( create_time_entry, update_time_entry, stop_time_entry ) class TimeEntryViewSet(ModelViewSet): """ API endpoints for managing time entries. """ pagination_class = CustomLimitOffsetPagination permission_classes = [IsAuthenticated] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ["workspace", "project", "is_billable"] search_fields = ["description", "project__name", "tags__name"] ordering_fields = ["start_time", "end_time", "created_at", "updated_at"] ordering = ["-start_time"] def get_queryset(self): """ Users can only interact with their own time entries within workspaces where they hold an active membership. """ if getattr(self, "swagger_fake_view", False) or not self.request.user.is_authenticated: return TimeEntry.objects.none() return TimeEntry.objects.filter( user=self.request.user, workspace__memberships__user=self.request.user, workspace__memberships__is_active=True, is_deleted=False ).distinct() def get_serializer_class(self): if self.action == "create": return TimeEntryCreateSerializer elif self.action in ["update", "partial_update"]: return TimeEntryUpdateSerializer elif self.action == "stop": return TimeEntryStopSerializer return TimeEntrySerializer def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) entry = create_time_entry( user=request.user, workspace_id=serializer.validated_data.pop("workspace_id"), **serializer.validated_data ) output_serializer = TimeEntrySerializer(entry) return Response(output_serializer.data, status=status.HTTP_201_CREATED) def update(self, request, *args, **kwargs): partial = kwargs.pop("partial", False) entry = self.get_object() serializer = self.get_serializer(data=request.data, partial=partial) serializer.is_valid(raise_exception=True) updated_entry = update_time_entry( entry=entry, **serializer.validated_data ) output_serializer = TimeEntrySerializer(updated_entry) return Response(output_serializer.data, status=status.HTTP_200_OK) @action(detail=True, methods=["post"]) def stop(self, request, pk=None): """ Dedicated endpoint to stop an actively running timer. """ entry = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) end_time = serializer.validated_data.get("end_time") stopped_entry = stop_time_entry(entry, end_time=end_time) output_serializer = TimeEntrySerializer(stopped_entry) return Response(output_serializer.data, status=status.HTTP_200_OK) def destroy(self, request, *args, **kwargs): """ Soft deletes the time entry. """ entry = self.get_object() entry.is_deleted = True entry.save(update_fields=["is_deleted", "updated_at"]) return Response(status=status.HTTP_204_NO_CONTENT)