This commit is contained in:
2026-05-02 04:49:29 +03:30
parent 8159190a84
commit 8d73b1703c
26 changed files with 2534 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
from datetime import date
from django.db.models import Q
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import serializers, status
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from farm_hub.models import FarmHub
from farmer_calendar.enums import FARMER_TAG_ITEMS
from farmer_calendar.models import FarmerCalendarZone
from .models import FarmerTodoTask
from .serializers import (
FarmerTodoListQuerySerializer,
FarmerTodoTagSerializer,
FarmerTodoTaskResponseSerializer,
FarmerTodoTaskWriteSerializer,
FarmerTodoZoneSerializer,
)
class FarmerTodoBaseView(APIView):
permission_classes = [IsAuthenticated]
@staticmethod
def error_response(message, details=None, code="TASK_VALIDATION_ERROR", status_code=status.HTTP_400_BAD_REQUEST):
payload = {"code": code, "message": message}
if details is not None:
payload["details"] = details
return Response(payload, status=status_code)
def handle_exception(self, exc):
if isinstance(exc, serializers.ValidationError):
details = exc.detail
message = "Invalid farmer todo payload"
if isinstance(details, dict):
first_value = next(iter(details.values()), None)
if isinstance(first_value, list) and first_value:
message = str(first_value[0])
elif first_value:
message = str(first_value)
elif isinstance(details, list) and details:
message = str(details[0])
return self.error_response(message=message, details=details)
if isinstance(exc, NotFound):
return self.error_response(
message=str(exc.detail),
code="TASK_NOT_FOUND",
status_code=status.HTTP_404_NOT_FOUND,
)
return super().handle_exception(exc)
def _get_user_farms(self, request):
return FarmHub.objects.filter(owner=request.user).order_by("id")
def _resolve_farm(self, request, farm_uuid=None, required=False):
farms = self._get_user_farms(request)
if farm_uuid:
try:
return farms.get(farm_uuid=farm_uuid)
except FarmHub.DoesNotExist as exc:
raise serializers.ValidationError({"farm_uuid": ["Farm not found."]}) from exc
if required:
farm_count = farms.count()
if farm_count == 1:
return farms.first()
if farm_count == 0:
raise serializers.ValidationError({"farm_uuid": ["No farm found for this user."]})
raise serializers.ValidationError({"farm_uuid": ["farm_uuid is required when multiple farms exist."]})
return None
def _get_task(self, request, task_id):
queryset = FarmerTodoTask.objects.select_related("farm", "zone").prefetch_related("tags")
try:
return queryset.get(uuid=task_id, farm__owner=request.user)
except FarmerTodoTask.DoesNotExist as exc:
raise NotFound("Task not found.") from exc
class FarmerTodoListCreateView(FarmerTodoBaseView):
@extend_schema(
tags=["Farmer Todos"],
parameters=[
OpenApiParameter(name="status", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="priority", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="date", type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="from", type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="to", type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="zone", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="search", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False),
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
)
def get(self, request):
query_data = request.query_params.copy()
if "from" in query_data and "from_date" not in query_data:
query_data["from_date"] = query_data["from"]
query_serializer = FarmerTodoListQuerySerializer(data=query_data)
query_serializer.is_valid(raise_exception=True)
filters = query_serializer.validated_data
queryset = FarmerTodoTask.objects.filter(farm__owner=request.user).select_related("zone").prefetch_related("tags")
farm = self._resolve_farm(request, filters.get("farm_uuid"), required=False)
if farm is not None:
queryset = queryset.filter(farm=farm)
if filters.get("status"):
queryset = queryset.filter(status=filters["status"])
if filters.get("priority"):
queryset = queryset.filter(priority=filters["priority"])
if filters.get("date"):
queryset = queryset.filter(scheduled_date=filters["date"])
if filters.get("from"):
queryset = queryset.filter(scheduled_date__gte=filters["from"])
if filters.get("to"):
queryset = queryset.filter(scheduled_date__lte=filters["to"])
if filters.get("zone"):
queryset = queryset.filter(zone__value=filters["zone"].strip())
if filters.get("search"):
search_value = filters["search"].strip()
queryset = queryset.filter(Q(title__icontains=search_value) | Q(description__icontains=search_value))
tasks = queryset.order_by("scheduled_date", "time", "created_at")
data = FarmerTodoTaskResponseSerializer(tasks, many=True).data
return Response({"tasks": data, "meta": {"total": len(data)}}, status=status.HTTP_200_OK)
@extend_schema(tags=["Farmer Todos"])
def post(self, request):
serializer = FarmerTodoTaskWriteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
farm = self._resolve_farm(request, serializer.validated_data.get("farm_uuid"), required=True)
task = serializer.save(farm=farm)
data = FarmerTodoTaskResponseSerializer(task).data
return Response({"task": data}, status=status.HTTP_201_CREATED)
class FarmerTodoDetailView(FarmerTodoBaseView):
@extend_schema(tags=["Farmer Todos"])
def put(self, request, task_id):
task = self._get_task(request, task_id)
serializer = FarmerTodoTaskWriteSerializer(task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
requested_farm_uuid = serializer.validated_data.get("farm_uuid")
if requested_farm_uuid and str(task.farm.farm_uuid) != str(requested_farm_uuid):
return self.error_response(
message="farm_uuid cannot change an existing task",
details={"farm_uuid": ["farm_uuid cannot change an existing task"]},
)
task = serializer.save()
data = FarmerTodoTaskResponseSerializer(task).data
return Response({"task": data}, status=status.HTTP_200_OK)
@extend_schema(tags=["Farmer Todos"])
def delete(self, request, task_id):
task = self._get_task(request, task_id)
task.delete()
return Response({"success": True}, status=status.HTTP_200_OK)
@extend_schema(tags=["Farmer Todos"])
def get(self, request, task_id):
task = self._get_task(request, task_id)
data = FarmerTodoTaskResponseSerializer(task).data
return Response({"task": data}, status=status.HTTP_200_OK)
class FarmerTodoZonesView(FarmerTodoBaseView):
@extend_schema(
tags=["Farmer Todos"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
)
def get(self, request):
farm = self._resolve_farm(request, request.query_params.get("farm_uuid"), required=False)
queryset = FarmerCalendarZone.objects.filter(farm__owner=request.user, is_active=True)
if farm is not None:
queryset = queryset.filter(farm=farm)
if farm is None:
unique_zones = {}
for zone in queryset.order_by("label", "created_at"):
unique_zones.setdefault(zone.value, zone)
zones = list(unique_zones.values())
else:
zones = queryset.order_by("label")
data = FarmerTodoZoneSerializer(zones, many=True).data
return Response({"zones": data, "meta": {"total": len(data)}}, status=status.HTTP_200_OK)
class FarmerTodoTagsView(FarmerTodoBaseView):
@extend_schema(
tags=["Farmer Todos"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
)
def get(self, request):
self._resolve_farm(request, request.query_params.get("farm_uuid"), required=False)
data = FarmerTodoTagSerializer(FARMER_TAG_ITEMS, many=True).data
return Response({"tags": data, "meta": {"total": len(data)}}, status=status.HTTP_200_OK)
class FarmerTodoSummaryView(FarmerTodoBaseView):
@extend_schema(
tags=["Farmer Todos"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
)
def get(self, request):
farm = self._resolve_farm(request, request.query_params.get("farm_uuid"), required=False)
queryset = FarmerTodoTask.objects.filter(farm__owner=request.user).select_related("zone").prefetch_related("tags")
if farm is not None:
queryset = queryset.filter(farm=farm)
today = date.today()
total_count = queryset.count()
today_count = queryset.filter(scheduled_date=today).count()
completed_count = queryset.filter(status=FarmerTodoTask.STATUS_DONE).count()
urgent_count = queryset.filter(priority=FarmerTodoTask.PRIORITY_HIGH, status=FarmerTodoTask.STATUS_OPEN).count()
next_task = queryset.filter(
status=FarmerTodoTask.STATUS_OPEN,
).filter(
Q(scheduled_date__gt=today) | Q(scheduled_date=today)
).order_by("scheduled_date", "time", "created_at").first()
progress_value = int((completed_count / total_count) * 100) if total_count else 0
next_task_data = FarmerTodoTaskResponseSerializer(next_task).data if next_task else None
return Response(
{
"todayTasksCount": today_count,
"completedCount": completed_count,
"urgentCount": urgent_count,
"progressValue": progress_value,
"nextTask": next_task_data,
},
status=status.HTTP_200_OK,
)