122 lines
4.6 KiB
Python
122 lines
4.6 KiB
Python
"""CSV import endpoints for tickets (cases).
|
|
|
|
Preview reads the uploaded file and returns row-level validation results
|
|
without touching the DB. Commit re-runs validation and writes inside a
|
|
single transaction. Both endpoints are gated by
|
|
`common.permissions.can_mass_import` (admins, superusers, sales access)
|
|
so non-privileged members cannot mass-create cases through this surface.
|
|
"""
|
|
|
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
|
from rest_framework import serializers, status
|
|
from rest_framework.parsers import MultiPartParser
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from cases.services.csv_import import commit_rows, parse_and_validate
|
|
from common.permissions import HasOrgContext, can_mass_import
|
|
|
|
MAX_UPLOAD_BYTES = 5 * 1024 * 1024 # 5 MB; matches the UI hint
|
|
|
|
|
|
def _read_upload(request):
|
|
"""Return (file_bytes, error_response). One of the two will be None.
|
|
|
|
Reads at most one byte past the cap, so an oversized file is refused
|
|
without being loaded into memory. Django has already spooled the body to
|
|
disk by now; an unbounded `read()` pulled all of it into the worker. The
|
|
cap is measured on the bytes read rather than on `upload.size`, so it does
|
|
not depend on how the upload object was built.
|
|
"""
|
|
upload = request.FILES.get("file")
|
|
if not upload:
|
|
return None, Response(
|
|
{"error": True, "message": "No file uploaded (expected field 'file')"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
name = (upload.name or "").lower()
|
|
if not name.endswith(".csv"):
|
|
return None, Response(
|
|
{"error": True, "message": "File must have a .csv extension"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
file_bytes = upload.read(MAX_UPLOAD_BYTES + 1)
|
|
if len(file_bytes) > MAX_UPLOAD_BYTES:
|
|
return None, Response(
|
|
{"error": True, "message": "File exceeds the 5 MB upload limit"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
return file_bytes, None
|
|
|
|
|
|
class CaseImportPreviewView(APIView):
|
|
permission_classes = (IsAuthenticated, HasOrgContext)
|
|
parser_classes = (MultiPartParser,)
|
|
|
|
@extend_schema(
|
|
tags=["Cases"],
|
|
request=inline_serializer(
|
|
name="CaseImportPreviewRequest",
|
|
fields={"file": serializers.FileField()},
|
|
),
|
|
responses={
|
|
200: inline_serializer(
|
|
name="CaseImportPreviewResponse",
|
|
fields={
|
|
"header_error": serializers.CharField(allow_null=True),
|
|
"valid": serializers.ListField(child=serializers.DictField()),
|
|
"errors": serializers.ListField(child=serializers.DictField()),
|
|
"summary": serializers.DictField(),
|
|
},
|
|
)
|
|
},
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
if not can_mass_import(request.profile):
|
|
return Response(
|
|
{"error": True, "message": "Permission denied"},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
file_bytes, err = _read_upload(request)
|
|
if err is not None:
|
|
return err
|
|
result = parse_and_validate(file_bytes, request.profile.org, request.profile)
|
|
return Response(result.to_dict(), status=status.HTTP_200_OK)
|
|
|
|
|
|
class CaseImportCommitView(APIView):
|
|
permission_classes = (IsAuthenticated, HasOrgContext)
|
|
parser_classes = (MultiPartParser,)
|
|
|
|
@extend_schema(
|
|
tags=["Cases"],
|
|
request=inline_serializer(
|
|
name="CaseImportCommitRequest",
|
|
fields={"file": serializers.FileField()},
|
|
),
|
|
responses={
|
|
200: inline_serializer(
|
|
name="CaseImportCommitResponse",
|
|
fields={
|
|
"error": serializers.BooleanField(),
|
|
"created": serializers.IntegerField(),
|
|
"ids": serializers.ListField(child=serializers.CharField()),
|
|
},
|
|
)
|
|
},
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
if not can_mass_import(request.profile):
|
|
return Response(
|
|
{"error": True, "message": "Permission denied"},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
file_bytes, err = _read_upload(request)
|
|
if err is not None:
|
|
return err
|
|
result = commit_rows(file_bytes, request.profile.org, request.profile)
|
|
http_status = (
|
|
status.HTTP_400_BAD_REQUEST if result.get("error") else status.HTTP_200_OK
|
|
)
|
|
return Response(result, status=http_status)
|