@@ -15,8 +15,7 @@ from authentication.exceptions.user import DomainNotFound from authentication.models import BusinessAccount, BusinessUserHost from authentication.models.user import CustomUserModel from authentication.services.email_token_service import EmailTokenService -from ml_model.services.minio_service import MinIOService -from reports.models.error_report import ErrorReport +from reports.domain import Report logger = logging.getLogger(__name__) @@ -83,23 +82,22 @@ class EmailService: user_emails=(self.user.email,), ) - def send_error_email(self, error: ErrorReport): + def send_error_email(self, report: Report) -> None: html_message = self._render_letter_template( template_name='authentication/error_email', - context={'email': error.author.email, 'report_text': error.report_text}, + context={'email': self.user.email, 'report_text': report.message}, ) mail = EmailMessage( - subject=f'Ошибка у пользователя {error.author.email}!', + subject=f'Ошибка у пользователя {self.user.email}!', body=html_message, from_email=settings.EMAIL_HOST_USER, to=settings.ERROR_EMAIL_RECIPIENTS, # reply_to=(error.author.email,), ) mail.content_subtype = 'html' - if error.additional_images is not None: - for _, img in error.additional_images.items(): - image = MinIOService().get_object('air-errors', img) - mail.attach(img, image) + for img in report.attachments: + mail.attach(img.name, img.read(), img.content_type) + img.close() mail.send(fail_silently=True) def send_copr_purchase_email(self, host: BusinessUserHost): @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from io import BytesIO +from typing import Optional + + +@dataclass +class File: + name: str + stream: BytesIO + content_type: str + size: int + + def read(self, *args, **kwargs) -> bytes: + return self.stream.read(*args, **kwargs) + + def seek(self, *args, **kwargs) -> Optional[int]: + return self.stream.seek(*args, **kwargs) + + def close(self) -> None: + return self.stream.close() @@ -1,40 +0,0 @@ -import datetime - -from django.core.management.base import BaseCommand - -from reports.admin import RequestReponseLog, RequestReponseLogAdmin - - -class Command(BaseCommand): - help = 'Collect reports and create an Excel file' - - def add_arguments(self, parser): - parser.add_argument( - 'from_datetime', - type=str, - help='Start date and time (YYYY-MM-DD HH:MM:SS)', - ) - parser.add_argument( - 'to_datetime', - type=str, - help='End date and time (YYYY-MM-DD HH:MM:SS)', - default=datetime.datetime.now().strftime('%Y-%M-%D %H:%M:%S'), - ) - - def handle(self, *args, **kwargs): - from_datetime = datetime.datetime.fromisoformat(kwargs['from_datetime']) - if 'to_datetime' in kwargs: - to_datetime = datetime.datetime.fromisoformat(kwargs['to_datetime']) - else: - to_datetime = datetime.datetime.now() - - logs = RequestReponseLog.objects.filter(created_at__range=(from_datetime, to_datetime)) - - if logs.exists(): - wb = RequestReponseLogAdmin.create_xlsx_report(logs) - file_name = f'reports_{from_datetime}_{to_datetime}.xlsx' - wb.save(file_name) - self.stdout.write(self.style.SUCCESS('Successfully created file')) - else: - self.stdout.write(self.style.WARNING('No logs found in the specified time range')) - return file_name @@ -0,0 +1,23 @@ +# Generated by Django 5.0.11 on 2025-12-13 09:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0002_alter_errorreport_options_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='requestreponselog', + name='request_user', + ), + migrations.DeleteModel( + name='ErrorReport', + ), + migrations.DeleteModel( + name='RequestReponseLog', + ), + ] @@ -1,2 +0,0 @@ -from .error_report import ErrorReport -from .log import RequestReponseLog @@ -1,15 +0,0 @@ -from django.contrib.auth import get_user_model -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from core.models import BaseModel - - -class ErrorReport(BaseModel): - author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_('Author')) - report_text = models.TextField(verbose_name=_('Text')) - additional_images = models.JSONField(null=True, verbose_name=_('Attachments')) - - class Meta: - verbose_name = _('User Report') - verbose_name_plural = _('User Reports') @@ -1,28 +0,0 @@ -from django.db import models - -from authentication.models import CustomUserModel -from core.models import BaseModel - - -class RequestReponseLog(BaseModel): - method = models.CharField(max_length=10, verbose_name='Метод запроса') - path = models.CharField(max_length=255, verbose_name='Эндпойнт запроса') - from_ip = models.GenericIPAddressField(verbose_name='От IP адреса') - request_body = models.TextField(null=True, blank=True, verbose_name='Тело запроса') - request_user = models.ForeignKey( - CustomUserModel, - verbose_name='От пользователя', - on_delete=models.CASCADE, - null=True, - blank=True, - ) - response_body = models.TextField(null=True, blank=True, verbose_name='Тело ответа') - status_code = models.IntegerField(verbose_name='Код ответа') - - def __str__(self): - return f'{self.created_at} - {self.method} {self.path} - {self.status_code}' - - class Meta: - verbose_name = 'Неудачный запрос' - verbose_name_plural = 'Неудачные запросы' - ordering = ['-created_at'] @@ -1,34 +0,0 @@ -from datetime import datetime - -from rest_framework.request import Request - -from authentication.models import CustomUserModel -from authentication.services.email_service import EmailService -from ml_model.services.minio_service import MinIOService -from reports.models.error_report import ErrorReport -from reports.serializers import NewErrorReportSerializer -from reports.utils import create_original_image - - -class ErrorReportService: - def __init__(self, user: CustomUserModel): - self.user = user - - def create(self, request: Request): - serializer = NewErrorReportSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - image_names = [] - if images := serializer.validated_data.get('images', False): - for img in images: - file, filename = create_original_image(img, self.user.email, datetime.now().timestamp()) - res_name = MinIOService().put_object(file, filename, 'air-errors') - image_names.append(res_name) - report = ErrorReport.objects.create( - author=self.user, - report_text=serializer.validated_data['report_text'], - ) - if image_names != []: - report.additional_images = {f'img_{i}': val for i, val in enumerate(image_names)} - report.save() - EmailService(self.user).send_error_email(report) @@ -1,67 +0,0 @@ -from django.contrib import admin -from django.db.models import QuerySet -from django.http import HttpResponse -from openpyxl import Workbook - -from reports.models import ErrorReport, RequestReponseLog - - -@admin.register(ErrorReport) -class ErrorReportAdmin(admin.ModelAdmin): - date_hierarchy = 'created_at' - search_fields = ['created_at'] - readonly_fields = ['created_at'] - list_display = ['author', 'report_text'] - raw_id_fields = ['author'] - - -@admin.register(RequestReponseLog) -class RequestReponseLogAdmin(admin.ModelAdmin): - search_fields = ['created_at', 'response_body'] - verbose_name = 'Запрос и ответ' - verbose_name_plural = 'Запросы и ответы' - list_display = [ - 'request_body', - 'request_user', - 'response_body', - 'status_code', - ] - raw_id_fields = ['request_user'] - actions = ['download_xlsx_logs'] - - @admin.action(description='Скачать выбранные логи в XLSX') - def download_xlsx_logs(self, request, qs: QuerySet[RequestReponseLog]): - wb = self.create_xlsx_report(qs=qs) - response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = 'attachment; filename=errors.xlsx' - wb.save(response) - return response - - @staticmethod - def create_xlsx_report(qs: QuerySet[RequestReponseLog]) -> Workbook: - wb = Workbook() - sheet = wb.active - sheet.append( - [ - 'Метод', - 'Путь', - 'IP-адрес', - 'Тело запроса', - 'Пользователь запроса', - 'Тело ответа', - 'Код статуса', - ] - ) - for s in qs: - sheet.append( - [ - s.method, - s.path, - s.from_ip, - s.request_body, - s.request_user.email if s.request_user else '', - s.response_body, - s.status_code, - ] - ) - return wb @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +from core.domain import File + + +@dataclass +class Report: + message: str + attachments: list[File] @@ -1,32 +0,0 @@ -import logging - -from rest_framework.request import Request -from rest_framework.response import Response - -from authentication.models.user import CustomUserModel -from reports.models import RequestReponseLog - -logger = logging.getLogger(__name__) - - -class RequestResponseMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request: Request): - response = self.get_response(request) - self.log_request_response(request, response) - return response - - def log_request_response(self, request: Request, response: Response): - if response.status_code in (400, 500): - log_entry = RequestReponseLog( - method=request.method, - path=request.path, - from_ip=request.META.get('REMOTE_ADDR'), - request_body=str(request.POST), - request_user=request.user if isinstance(request.user, CustomUserModel) else None, - response_body=response.content, - status_code=response.status_code, - ) - log_entry.save() @@ -1,9 +0,0 @@ -from io import BytesIO -from typing import BinaryIO, Tuple - - -def create_original_image(data: BinaryIO, user: str, date: float) -> Tuple[BytesIO, str]: - file = BytesIO(data.read()) - filename = f'{user}-{date}.png' - - return file, filename @@ -4,8 +4,10 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from authentication.services.email_service import EmailService +from reports.domain import Report +from core.domain import File from reports.serializers import NewErrorReportSerializer -from reports.services.error_report_service import ErrorReportService class SendErrorReportEmailAPIView(APIView): @@ -15,7 +17,14 @@ class SendErrorReportEmailAPIView(APIView): def post(self, request, *args, **kwargs): """Send new error report from form data.""" try: - ErrorReportService(self.request.user).create(request) + serializer = NewErrorReportSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + report = Report( + message=data['report_text'], + attachments=[File(file.name, file.file, file.content_type, file.size) for file in data['images']] + ) + EmailService(request.user).send_error_email(report) return Response({'detail': 'error report sent'}, status=status.HTTP_201_CREATED) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST)