feat(time_entries): add time_entries app's basic structure and endpoints
This commit is contained in:
113
apps/time_entries/api/views.py
Normal file
113
apps/time_entries/api/views.py
Normal file
@@ -0,0 +1,113 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user