826 lines
28 KiB
Python
826 lines
28 KiB
Python
import logging
|
|
from calendar import monthrange
|
|
from datetime import date, timedelta
|
|
from urllib.parse import quote
|
|
|
|
from attendance.models import (
|
|
AttendanceRecord,
|
|
EmployeeWorkSchedule,
|
|
StandardWorkSchedule,
|
|
)
|
|
from attendance.services import (
|
|
build_attendance_matrix_export_workbook,
|
|
build_monthly_attendance_matrix,
|
|
get_attendance_auto_sync_settings,
|
|
get_employee_work_schedule_payload,
|
|
get_standard_work_schedule_payload,
|
|
normalize_attendance_record_manual_issues,
|
|
run_attendance_auto_sync,
|
|
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.http import HttpResponse
|
|
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 (
|
|
AttendanceAutoSyncSettingsSerializer,
|
|
AttendanceMonthlyMatrixExportQuerySerializer,
|
|
AttendanceMonthlyMatrixQuerySerializer,
|
|
AttendanceRecordCommentSerializer,
|
|
AttendanceRecordSerializer,
|
|
AttendanceRecordUpdateSerializer,
|
|
EmployeeWorkScheduleSerializer,
|
|
LogStormAttendanceAnalyzeSerializer,
|
|
StandardWorkScheduleSerializer,
|
|
default_work_schedule_response,
|
|
default_standard_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 user.is_superuser
|
|
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 or user.is_superuser:
|
|
return queryset
|
|
return queryset.filter(employee_id=user.id)
|
|
|
|
|
|
def _check_staff_access(user) -> None:
|
|
if not (user and user.is_authenticated and (user.is_staff or user.is_superuser)):
|
|
raise PermissionDenied("You don't have permission to manage attendance")
|
|
|
|
|
|
def _has_attendance_pass(record: AttendanceRecord) -> bool:
|
|
return bool(
|
|
record.arrival_time
|
|
or record.departure_time
|
|
or (record.work_hours is not None and record.work_hours > 0)
|
|
)
|
|
|
|
|
|
def _has_absence_marker(record: AttendanceRecord) -> bool:
|
|
values = [
|
|
record.statuses,
|
|
record.employee_issues,
|
|
]
|
|
return bool(
|
|
record.is_absent
|
|
or any(
|
|
"absent" in normalized
|
|
or "absence" in normalized
|
|
or "отсутств" in normalized
|
|
for items in values
|
|
for normalized in [str(item).strip().lower() for item in (items or [])]
|
|
)
|
|
)
|
|
|
|
|
|
def _employee_attendance_aliases(record: AttendanceRecord) -> list[str]:
|
|
return [
|
|
str(alias).strip()
|
|
for alias in (getattr(record.employee, "attendance_aliases", None) or [])
|
|
if str(alias).strip()
|
|
]
|
|
|
|
|
|
def _attendance_employee_summary(employee: Employee) -> dict[str, str | int]:
|
|
full_name = f"{employee.last_name or ''} {employee.first_name or ''}".strip()
|
|
if getattr(employee, "patronymic", ""):
|
|
full_name = f"{full_name} {employee.patronymic}".strip()
|
|
return {
|
|
"id": employee.id,
|
|
"name": full_name or employee.email or f"Сотрудник #{employee.id}",
|
|
"email": employee.email or "",
|
|
}
|
|
|
|
|
|
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),
|
|
)
|
|
}
|
|
|
|
|
|
def _comment_author_name(author) -> str:
|
|
if not author:
|
|
return "Сотрудник"
|
|
first_name = getattr(author, "first_name", "") or ""
|
|
last_name = getattr(author, "last_name", "") or ""
|
|
full_name = f"{last_name} {first_name}".strip()
|
|
return full_name or getattr(author, "email", "") or "Сотрудник"
|
|
|
|
|
|
def _attendance_record_comments_map(record_ids: list[int]) -> dict[int, list[dict[str, str]]]:
|
|
if not record_ids:
|
|
return {}
|
|
record_ct = ContentType.objects.get_for_model(AttendanceRecord)
|
|
messages = (
|
|
Message.objects.filter(
|
|
chat__type="comments",
|
|
chat__context_content_type=record_ct,
|
|
chat__context_object_id__in=record_ids,
|
|
is_deleted=False,
|
|
)
|
|
.select_related("author", "chat")
|
|
.order_by("created_at", "id")
|
|
)
|
|
result: dict[int, list[dict[str, str]]] = {}
|
|
for message in messages:
|
|
record_id = int(message.chat.context_object_id)
|
|
result.setdefault(record_id, []).append(
|
|
{
|
|
"author": _comment_author_name(message.author),
|
|
"text": message.content,
|
|
"created_at": timezone.localtime(message.created_at).strftime(
|
|
"%d.%m.%Y %H:%M"
|
|
),
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
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)
|
|
or get_standard_work_schedule_payload()
|
|
)
|
|
aliases = (
|
|
data["aliases"]
|
|
if "aliases" in data
|
|
else list(getattr(employee, "attendance_aliases", None) or [])
|
|
)
|
|
|
|
try:
|
|
result = analyze_employee_attendance(
|
|
employee=employee,
|
|
period_start=data["period_start"],
|
|
period_end=data["period_end"],
|
|
schedule=schedule_payload,
|
|
aliases=aliases,
|
|
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,
|
|
aliases=aliases,
|
|
)
|
|
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 or request.user.is_superuser):
|
|
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 StandardWorkScheduleAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
_check_staff_access(request.user)
|
|
schedule = StandardWorkSchedule.objects.order_by("id").first()
|
|
if schedule is None:
|
|
return Response(default_standard_work_schedule_response())
|
|
return Response(StandardWorkScheduleSerializer(schedule).data)
|
|
|
|
def patch(self, request):
|
|
_check_staff_access(request.user)
|
|
schedule = StandardWorkSchedule.objects.order_by("id").first()
|
|
serializer = StandardWorkScheduleSerializer(
|
|
schedule,
|
|
data=request.data,
|
|
partial=True,
|
|
)
|
|
serializer.is_valid(raise_exception=True)
|
|
instance = serializer.save(updated_by=request.user)
|
|
return Response(StandardWorkScheduleSerializer(instance).data)
|
|
|
|
|
|
class AttendanceAutoSyncSettingsAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
_check_staff_access(request.user)
|
|
return Response(
|
|
AttendanceAutoSyncSettingsSerializer(
|
|
get_attendance_auto_sync_settings()
|
|
).data
|
|
)
|
|
|
|
def patch(self, request):
|
|
_check_staff_access(request.user)
|
|
settings = get_attendance_auto_sync_settings()
|
|
was_enabled = settings.enabled
|
|
old_frequency = settings.frequency_minutes
|
|
serializer = AttendanceAutoSyncSettingsSerializer(
|
|
settings,
|
|
data=request.data,
|
|
partial=True,
|
|
)
|
|
serializer.is_valid(raise_exception=True)
|
|
instance = serializer.save(updated_by=request.user)
|
|
|
|
update_fields = ["updated_by", "updated_at"]
|
|
if not instance.enabled:
|
|
instance.next_run_at = None
|
|
update_fields.append("next_run_at")
|
|
elif not was_enabled or old_frequency != instance.frequency_minutes:
|
|
instance.next_run_at = timezone.now()
|
|
update_fields.append("next_run_at")
|
|
if update_fields != ["updated_by", "updated_at"]:
|
|
instance.save(update_fields=update_fields)
|
|
|
|
return Response(AttendanceAutoSyncSettingsSerializer(instance).data)
|
|
|
|
|
|
class AttendanceAutoSyncRunNowAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
_check_staff_access(request.user)
|
|
settings = run_attendance_auto_sync(force=True)
|
|
return Response(AttendanceAutoSyncSettingsSerializer(settings).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 AttendanceWeeklySummaryAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
_check_staff_access(request.user)
|
|
week_start = self._week_start(request.query_params.get("week_start"))
|
|
week_end = week_start + timedelta(days=6)
|
|
records = (
|
|
_records_visible_to(request.user)
|
|
.filter(
|
|
employee__is_active=True,
|
|
date__gte=week_start,
|
|
date__lte=week_end,
|
|
)
|
|
.select_related("employee")
|
|
.only(
|
|
"employee_id",
|
|
"employee__id",
|
|
"employee__first_name",
|
|
"employee__last_name",
|
|
"employee__patronymic",
|
|
"employee__email",
|
|
"date",
|
|
"arrival_time",
|
|
"departure_time",
|
|
"work_hours",
|
|
"is_absent",
|
|
"statuses",
|
|
"employee_issues",
|
|
)
|
|
.order_by("date", "employee_id")
|
|
)
|
|
|
|
by_date = {
|
|
week_start + timedelta(days=day_offset): {
|
|
"present": {},
|
|
"absent": {},
|
|
}
|
|
for day_offset in range(7)
|
|
}
|
|
|
|
for record in records:
|
|
bucket = by_date.get(record.date)
|
|
if bucket is None:
|
|
continue
|
|
employee_summary = _attendance_employee_summary(record.employee)
|
|
if _has_attendance_pass(record):
|
|
bucket["present"][record.employee_id] = employee_summary
|
|
bucket["absent"].pop(record.employee_id, None)
|
|
elif _has_absence_marker(record):
|
|
if record.employee_id not in bucket["present"]:
|
|
bucket["absent"][record.employee_id] = employee_summary
|
|
|
|
return Response(
|
|
{
|
|
"week_start": week_start,
|
|
"week_end": week_end,
|
|
"days": [
|
|
{
|
|
"date": day.isoformat(),
|
|
"weekday": day.strftime("%A"),
|
|
"present": len(counts["present"]),
|
|
"absent": len(counts["absent"]),
|
|
"present_people": list(counts["present"].values()),
|
|
"absent_people": list(counts["absent"].values()),
|
|
}
|
|
for day, counts in by_date.items()
|
|
],
|
|
}
|
|
)
|
|
|
|
def _week_start(self, value: str | None) -> date:
|
|
if value:
|
|
try:
|
|
parsed = date.fromisoformat(value)
|
|
except ValueError:
|
|
parsed = timezone.localdate()
|
|
else:
|
|
parsed = timezone.localdate()
|
|
return parsed - timedelta(days=parsed.weekday())
|
|
|
|
|
|
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 AttendanceMonthlyMatrixExportAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
_check_staff_access(request.user)
|
|
serializer = AttendanceMonthlyMatrixExportQuerySerializer(
|
|
data=request.query_params,
|
|
)
|
|
serializer.is_valid(raise_exception=True)
|
|
data = serializer.validated_data
|
|
employee_ids = data["employee_ids"]
|
|
period_start = data["period_start"]
|
|
period_end = data["period_end"]
|
|
|
|
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
|
|
]
|
|
|
|
records = list(
|
|
_records_visible_to(request.user)
|
|
.filter(
|
|
employee_id__in=[employee.id for employee in employees],
|
|
date__gte=period_start,
|
|
date__lte=period_end,
|
|
)
|
|
.select_related("employee")
|
|
.annotate(**_comments_count_annotation())
|
|
.order_by("employee_id", "date", "id")
|
|
)
|
|
|
|
content = build_attendance_matrix_export_workbook(
|
|
employees=employees,
|
|
records=records,
|
|
period_start=period_start,
|
|
period_end=period_end,
|
|
record_comments=_attendance_record_comments_map(
|
|
[record.id for record in records]
|
|
),
|
|
)
|
|
filename = f"attendance-{period_start.isoformat()}_{period_end.isoformat()}.xlsx"
|
|
response = HttpResponse(
|
|
content,
|
|
content_type=(
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
),
|
|
)
|
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
return response
|
|
|
|
|
|
class AttendanceRecordDetailAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, record_id):
|
|
record = get_object_or_404(
|
|
_records_visible_to(request.user)
|
|
.select_related("employee", "analysis_run")
|
|
.annotate(**_comments_count_annotation()),
|
|
pk=record_id,
|
|
)
|
|
return Response(AttendanceRecordSerializer(record).data)
|
|
|
|
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 AttendanceRecordDayEventsAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_record(self, request, record_id):
|
|
return get_object_or_404(
|
|
_records_visible_to(request.user).select_related("employee"),
|
|
pk=record_id,
|
|
)
|
|
|
|
def get(self, request, record_id):
|
|
record = self.get_record(request, record_id)
|
|
try:
|
|
events = LogStormClient().get_attendance_day_events(
|
|
employee_id=str(record.employee_id),
|
|
record_date=record.date,
|
|
aliases=_employee_attendance_aliases(record),
|
|
)
|
|
except LogStormClientError as exc:
|
|
logger.warning("LogStorm attendance day events failed: %s", exc)
|
|
return Response(
|
|
{"detail": str(exc), "error": "logstorm_unavailable"},
|
|
status=status.HTTP_502_BAD_GATEWAY,
|
|
)
|
|
|
|
return Response(
|
|
[
|
|
self._with_proxy_photo_url(request, record, event)
|
|
for event in events
|
|
if isinstance(event, dict)
|
|
]
|
|
)
|
|
|
|
@staticmethod
|
|
def _with_proxy_photo_url(request, record, event):
|
|
result = dict(event)
|
|
event_key = str(result.get("event_key") or "")
|
|
if result.get("has_photo") and event_key:
|
|
result["photo_url"] = (
|
|
f"/api/v1/attendance/records/{record.id}/day-events/"
|
|
f"{quote(event_key)}/photo/"
|
|
)
|
|
else:
|
|
result["photo_url"] = None
|
|
return result
|
|
|
|
|
|
class AttendanceRecordDayEventPhotoAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_record(self, request, record_id):
|
|
return get_object_or_404(
|
|
_records_visible_to(request.user).select_related("employee"),
|
|
pk=record_id,
|
|
)
|
|
|
|
def get(self, request, record_id, event_key):
|
|
record = self.get_record(request, record_id)
|
|
client = LogStormClient()
|
|
try:
|
|
events = client.get_attendance_day_events(
|
|
employee_id=str(record.employee_id),
|
|
record_date=record.date,
|
|
aliases=_employee_attendance_aliases(record),
|
|
)
|
|
event = next(
|
|
(
|
|
item
|
|
for item in events
|
|
if isinstance(item, dict)
|
|
and str(item.get("event_key")) == str(event_key)
|
|
),
|
|
None,
|
|
)
|
|
if not event or not event.get("has_photo"):
|
|
return Response(
|
|
{"detail": "Event photo not found"},
|
|
status=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
photo_response = client.get_attendance_event_photo(event_key)
|
|
except LogStormClientError as exc:
|
|
if exc.status_code == status.HTTP_404_NOT_FOUND:
|
|
return Response(
|
|
{"detail": "Event photo not found"},
|
|
status=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
logger.warning("LogStorm attendance event photo failed: %s", exc)
|
|
return Response(
|
|
{"detail": str(exc), "error": "logstorm_unavailable"},
|
|
status=status.HTTP_502_BAD_GATEWAY,
|
|
)
|
|
|
|
return HttpResponse(
|
|
photo_response.content,
|
|
content_type=photo_response.headers.get("Content-Type", "image/jpeg"),
|
|
)
|
|
|
|
|
|
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)
|