400 lines
13 KiB
Python

import logging
from calendar import monthrange
from datetime import date
from attendance.models import AttendanceRecord, EmployeeWorkSchedule
from attendance.services import (
build_monthly_attendance_matrix,
get_employee_work_schedule_payload,
normalize_attendance_record_manual_issues,
save_logstorm_attendance_result,
)
from communications import comments_helpers
from communications.models import Message
from common.logstorm_attendance import (
analyze_employee_attendance,
build_logstorm_attendance_payload,
)
from common.logstorm_client import LogStormClient, LogStormClientError
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, IntegerField, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce
from django.shortcuts import get_object_or_404
from django.utils import timezone
from employees.models import Employee
from rest_framework import status
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import (
AttendanceMonthlyMatrixQuerySerializer,
AttendanceRecordCommentSerializer,
AttendanceRecordSerializer,
AttendanceRecordUpdateSerializer,
EmployeeWorkScheduleSerializer,
LogStormAttendanceAnalyzeSerializer,
default_work_schedule_response,
)
logger = logging.getLogger(__name__)
def _can_access_employee(user, employee_id) -> bool:
try:
normalized_employee_id = int(employee_id)
except (TypeError, ValueError):
return False
return bool(
user
and user.is_authenticated
and (user.is_staff or normalized_employee_id == int(user.id))
)
def _check_employee_access(user, employee_id) -> None:
if not _can_access_employee(user, employee_id):
raise PermissionDenied(
"You don't have permission to access this attendance data"
)
def _records_visible_to(user):
queryset = AttendanceRecord.objects.all()
if user.is_staff:
return queryset
return queryset.filter(employee_id=user.id)
def _comments_count_annotation():
record_ct = ContentType.objects.get_for_model(AttendanceRecord)
comments_subquery = (
Message.objects.filter(
chat__type="comments",
chat__context_content_type=record_ct,
chat__context_object_id=OuterRef("pk"),
is_deleted=False,
)
.values("chat")
.annotate(count=Count("id"))
.values("count")
)
return {
"comments_count": Coalesce(
Subquery(comments_subquery, output_field=IntegerField()),
Value(0),
)
}
class LogStormAttendanceAnalyzeAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = LogStormAttendanceAnalyzeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
_check_employee_access(request.user, data["employee_id"])
try:
employee = Employee.objects.get(pk=data["employee_id"])
except Employee.DoesNotExist:
return Response(
{"detail": "Employee not found"},
status=status.HTTP_404_NOT_FOUND,
)
schedule_payload = data.get("schedule")
if schedule_payload is None:
schedule_payload = get_employee_work_schedule_payload(employee)
try:
result = analyze_employee_attendance(
employee=employee,
period_start=data["period_start"],
period_end=data["period_end"],
schedule=schedule_payload,
client=None,
)
except LogStormClientError as exc:
logger.warning("LogStorm attendance analysis failed: %s", exc)
return Response(
{"detail": str(exc), "error": "logstorm_unavailable"},
status=status.HTTP_502_BAD_GATEWAY,
)
request_payload = build_logstorm_attendance_payload(
employee=employee,
period_start=data["period_start"],
period_end=data["period_end"],
schedule=schedule_payload,
)
save_logstorm_attendance_result(
employee=employee,
period_start=data["period_start"],
period_end=data["period_end"],
schedule_payload=request_payload.get("schedule"),
request_payload=request_payload,
response_payload=result,
triggered_by=request.user,
)
return Response(result)
class EmployeeWorkScheduleAPIView(APIView):
permission_classes = [IsAuthenticated]
def get_employee(self, employee_id):
_check_employee_access(self.request.user, employee_id)
return get_object_or_404(Employee, pk=employee_id)
def get(self, request, employee_id):
employee = self.get_employee(employee_id)
try:
schedule = employee.work_schedule
except EmployeeWorkSchedule.DoesNotExist:
schedule = None
if schedule is None:
return Response(default_work_schedule_response(employee.id))
return Response(EmployeeWorkScheduleSerializer(schedule).data)
def patch(self, request, employee_id):
if not request.user.is_staff:
raise PermissionDenied("You don't have permission to edit schedules")
employee = get_object_or_404(Employee, pk=employee_id)
try:
schedule = employee.work_schedule
except EmployeeWorkSchedule.DoesNotExist:
schedule = None
serializer = EmployeeWorkScheduleSerializer(
schedule,
data=request.data,
partial=True,
)
serializer.is_valid(raise_exception=True)
instance = serializer.save(employee=employee, updated_by=request.user)
return Response(EmployeeWorkScheduleSerializer(instance).data)
class AttendanceRecordListAPIView(ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = AttendanceRecordSerializer
def get_queryset(self):
queryset = (
_records_visible_to(self.request.user)
.select_related("employee", "analysis_run")
.annotate(**_comments_count_annotation())
.all()
.order_by("-date", "-id")
)
employee_id = self.request.query_params.get("employee_id")
if employee_id:
_check_employee_access(self.request.user, employee_id)
queryset = queryset.filter(employee_id=employee_id)
date_from = self.request.query_params.get("date_from")
if date_from:
queryset = queryset.filter(date__gte=date_from)
date_to = self.request.query_params.get("date_to")
if date_to:
queryset = queryset.filter(date__lte=date_to)
return queryset
class AttendanceMonthlyMatrixAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = AttendanceMonthlyMatrixQuerySerializer(
data=request.query_params,
)
serializer.is_valid(raise_exception=True)
employee_ids = serializer.validated_data["employee_ids"]
year, month = [
int(part) for part in serializer.validated_data["month"].split("-")
]
for employee_id in employee_ids:
_check_employee_access(request.user, employee_id)
employees = list(Employee.objects.filter(id__in=employee_ids))
employees_by_id = {employee.id: employee for employee in employees}
employees = [
employees_by_id[employee_id]
for employee_id in employee_ids
if employee_id in employees_by_id
]
start_date = date(year, month, 1)
end_date = date(year, month, monthrange(year, month)[1])
records = list(
_records_visible_to(request.user)
.filter(
employee_id__in=[employee.id for employee in employees],
date__gte=start_date,
date__lte=end_date,
)
.select_related("employee")
.annotate(**_comments_count_annotation())
.order_by("employee_id", "date", "id")
)
return Response(
build_monthly_attendance_matrix(
employees=employees,
records=records,
year=year,
month=month,
)
)
class AttendanceRecordDetailAPIView(APIView):
permission_classes = [IsAuthenticated]
def patch(self, request, record_id):
if not request.user.is_staff:
raise PermissionDenied(
"You don't have permission to edit attendance records"
)
record = get_object_or_404(
_records_visible_to(request.user)
.select_related("employee", "analysis_run")
.annotate(**_comments_count_annotation()),
pk=record_id,
)
serializer = AttendanceRecordUpdateSerializer(
record,
data=request.data,
partial=True,
)
serializer.is_valid(raise_exception=True)
manual_payload = dict(serializer.validated_data)
try:
logstorm_override = LogStormClient().update_attendance_override(
employee_id=str(record.employee_id),
record_date=record.date,
payload={**manual_payload, "source": "eusrr"},
)
except LogStormClientError as exc:
logger.warning("LogStorm attendance override failed: %s", exc)
return Response(
{"detail": str(exc), "error": "logstorm_unavailable"},
status=status.HTTP_502_BAD_GATEWAY,
)
record = serializer.save(
is_manually_edited=True,
manual_edit_payload=manual_payload,
manual_edited_by=request.user,
manual_edited_at=timezone.now(),
)
normalize_attendance_record_manual_issues(record, manual_payload)
record.raw_data = {
**record.raw_data,
"manual_edited": True,
"manual_edit_payload": manual_payload,
"logstorm_override": logstorm_override,
}
record.save(
update_fields=[
"statuses",
"employee_issues",
"raw_data",
"updated_at",
]
)
record = (
_records_visible_to(request.user)
.select_related("employee", "analysis_run")
.annotate(**_comments_count_annotation())
.get(pk=record.pk)
)
return Response(AttendanceRecordSerializer(record).data)
class AttendanceRecordCommentsAPIView(APIView):
permission_classes = [IsAuthenticated]
def get_record(self, request, record_id):
return get_object_or_404(_records_visible_to(request.user), pk=record_id)
@staticmethod
def serialize_message(record, message):
payload = {
"id": message.id,
"record": record.id,
"author": message.author,
"text": message.content,
"created_at": message.created_at,
}
return AttendanceRecordCommentSerializer(payload).data
def get(self, request, record_id):
record = self.get_record(request, record_id)
messages = comments_helpers.get_comments(record)
return Response(
[self.serialize_message(record, message) for message in messages]
)
def post(self, request, record_id):
record = self.get_record(request, record_id)
serializer = AttendanceRecordCommentSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
message = comments_helpers.create_comment(
obj=record,
author=request.user,
content=serializer.validated_data["text"].strip(),
)
return Response(
self.serialize_message(record, message),
status=status.HTTP_201_CREATED,
)
class AttendanceRecordCommentDetailAPIView(APIView):
permission_classes = [IsAuthenticated]
def delete(self, request, record_id, comment_id):
record = get_object_or_404(_records_visible_to(request.user), pk=record_id)
chat = comments_helpers.get_comments_chat_if_exists(record)
if chat is None:
return Response(
{"detail": "Comment not found"},
status=status.HTTP_404_NOT_FOUND,
)
try:
message = Message.objects.get(id=comment_id, chat=chat)
except Message.DoesNotExist:
return Response(
{"detail": "Comment not found"},
status=status.HTTP_404_NOT_FOUND,
)
if message.author != request.user:
return Response(
{"detail": "You don't have permission to delete this comment"},
status=status.HTTP_403_FORBIDDEN,
)
comments_helpers.delete_comment(
message=message,
deleted_by=request.user,
soft_delete=True,
)
return Response(status=status.HTTP_204_NO_CONTENT)