243 lines
11 KiB
Python
243 lines
11 KiB
Python
|
|
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,
|
||
|
|
)
|