feat / AEB-65 generate reports
This commit is contained in:
@@ -17,6 +17,7 @@ requests = "*"
|
||||
pyjwt = "*"
|
||||
drf-spectacular = "*"
|
||||
pillow = "*"
|
||||
reportlab = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
||||
11
backend/Pipfile.lock
generated
11
backend/Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "9818bd0afdf9b9b9884ad0f207ea2fa3485e448d2a80cd079d604018130ae088"
|
||||
"sha256": "c74de9a446b92f720fd07b652e785a1e38dbca779487bb7ea2ae07c596c76b0f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -560,6 +560,15 @@
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.36.2"
|
||||
},
|
||||
"reportlab": {
|
||||
"hashes": [
|
||||
"sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb",
|
||||
"sha256:cb2f658b7f4a15be2cc68f7203aa67faef67213edd4f2d4bdd3eb20dab75a80d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9' and python_version < '4'",
|
||||
"version": "==4.4.4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.urls import path
|
||||
from .views.UserDataView import UserDataView
|
||||
from .views.objects_views import ObjectView
|
||||
from .views.sensors_views import SensorView
|
||||
from .views.alert_views import AlertView
|
||||
from .views.alert_views import AlertView, ReportView
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
@@ -31,4 +31,6 @@ urlpatterns = [
|
||||
|
||||
path("get-alerts/", AlertView.as_view({'get': 'get_alerts'}), name="alerts"),
|
||||
path("update-alert/<int:pk>/", AlertView.as_view({'patch': 'change_alert_status'}), name="update-alert"),
|
||||
|
||||
path("get-reports/", ReportView.as_view({'post': 'get_reports'}), name="reports"),
|
||||
]
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework import status
|
||||
from rest_framework.viewsets import ViewSet
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.http import HttpResponse
|
||||
from api.account.serializers.alert_serializers import AlertSerializer
|
||||
from sitemanagement.models import Alert
|
||||
from api.utils.decorators import handle_exceptions
|
||||
from api.utils.error_serializer import ErrorResponseSerializer
|
||||
from api.utils.report_generators import generate_csv_report, generate_pdf_report
|
||||
from django.utils import timezone
|
||||
|
||||
@extend_schema(tags=['Алерты'])
|
||||
class AlertView(ViewSet):
|
||||
@@ -79,3 +82,68 @@ class AlertView(ViewSet):
|
||||
{"error": "Алерт не найден"},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@extend_schema(tags=['Репорты'])
|
||||
class ReportView(ViewSet):
|
||||
# permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
summary="Генерация отчета",
|
||||
description="Генерирует отчет в выбранном формате (PDF или CSV)",
|
||||
request={'application/json': {'type': 'object', 'properties': {'report_format': {'type': 'string', 'enum': ['pdf', 'csv']}}}},
|
||||
responses={
|
||||
200: OpenApiResponse(response=AlertSerializer(many=True), description="Список репортов успешно получен",
|
||||
examples=[OpenApiExample(
|
||||
'Успешный ответ',
|
||||
value=[{
|
||||
"message" : "Отчет успешно сгенерирован"
|
||||
}]
|
||||
)]),
|
||||
400: OpenApiResponse(
|
||||
response=ErrorResponseSerializer,
|
||||
description="Неверный формат",
|
||||
examples=[OpenApiExample(
|
||||
'Неверный формат',
|
||||
value={"error": "Неверный формат"},
|
||||
status_codes=['400']
|
||||
)]
|
||||
)
|
||||
})
|
||||
@action(detail=False, methods=['post'])
|
||||
@handle_exceptions
|
||||
def get_reports(self, request):
|
||||
"""Генерация отчета в выбранном формате"""
|
||||
report_format = request.data.get('report_format', '').lower()
|
||||
|
||||
if not report_format:
|
||||
report_format = request.query_params.get('format', '').lower()
|
||||
|
||||
if report_format not in ["pdf", "csv"]:
|
||||
return Response(
|
||||
{"error": "Неверный формат. Допустимые значения: pdf, csv"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
alerts = Alert.objects.select_related(
|
||||
'sensor',
|
||||
'sensor__signal_format',
|
||||
'sensor_type',
|
||||
'metric'
|
||||
).prefetch_related(
|
||||
'sensor__zones',
|
||||
'sensor__zones__object'
|
||||
).all()
|
||||
|
||||
# текущая дата для имени файла
|
||||
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if report_format == "csv":
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = f'attachment; filename="alerts_report_{timestamp}.csv"'
|
||||
response.write(generate_csv_report(alerts))
|
||||
return response
|
||||
|
||||
else: # pdf
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="alerts_report_{timestamp}.pdf"'
|
||||
response.write(generate_pdf_report(alerts))
|
||||
return response
|
||||
BIN
backend/api/utils/fonts/arial_bolditalicmt.ttf
Executable file
BIN
backend/api/utils/fonts/arial_bolditalicmt.ttf
Executable file
Binary file not shown.
BIN
backend/api/utils/fonts/arialmt.ttf
Executable file
BIN
backend/api/utils/fonts/arialmt.ttf
Executable file
Binary file not shown.
143
backend/api/utils/report_generators.py
Normal file
143
backend/api/utils/report_generators.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import csv
|
||||
from io import StringIO, BytesIO
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from django.utils import timezone
|
||||
import os
|
||||
|
||||
FONT_PATH = os.path.join(os.path.dirname(__file__), 'fonts')
|
||||
ARIAL_PATH = os.path.join(FONT_PATH, 'arialmt.ttf')
|
||||
ARIAL_BOLD_PATH = os.path.join(FONT_PATH, 'arial_bolditalicmt.ttf')
|
||||
|
||||
if not os.path.exists(ARIAL_PATH) or not os.path.exists(ARIAL_BOLD_PATH):
|
||||
raise FileNotFoundError(
|
||||
f"Шрифты не найдены. Пожалуйста, убедитесь что файлы arialmt.ttf и arial_bolditalicmt.ttf "
|
||||
f"находятся в директории {FONT_PATH}"
|
||||
)
|
||||
|
||||
pdfmetrics.registerFont(TTFont('Arial', ARIAL_PATH))
|
||||
pdfmetrics.registerFont(TTFont('Arial-Bold', ARIAL_BOLD_PATH))
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
styles.add(ParagraphStyle(
|
||||
name='CustomNormal',
|
||||
fontName='Arial',
|
||||
fontSize=10,
|
||||
leading=12,
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
name='CustomHeading1',
|
||||
fontName='Arial-Bold',
|
||||
fontSize=14,
|
||||
leading=16,
|
||||
))
|
||||
|
||||
|
||||
def generate_csv_report(alerts):
|
||||
"""Генерирует CSV отчет из списка алертов"""
|
||||
output = StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# заголовки
|
||||
headers = ['ID', 'Датчик', 'Объект', 'Значение', 'Тип датчика', 'Сообщение',
|
||||
'Уровень важности', 'Дата создания', 'Обработан']
|
||||
writer.writerow(headers)
|
||||
|
||||
# данные
|
||||
for alert in alerts:
|
||||
zone = alert.sensor.zones.first()
|
||||
object_name = zone.object.title if zone else 'Не указан'
|
||||
|
||||
writer.writerow([
|
||||
alert.id,
|
||||
alert.sensor.name,
|
||||
object_name,
|
||||
f"{alert.metric.value} {alert.sensor.signal_format.unit if alert.sensor.signal_format else ''}" if alert.metric.value else alert.metric.raw_value,
|
||||
alert.sensor_type.name,
|
||||
alert.message,
|
||||
alert.severity,
|
||||
timezone.localtime(alert.created_at).strftime("%Y-%m-%d %H:%M:%S %Z"),
|
||||
'Да' if alert.resolved else 'Нет'
|
||||
])
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
def generate_pdf_report(alerts):
|
||||
"""Генерирует PDF отчет из списка алертов"""
|
||||
buffer = BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
||||
elements = []
|
||||
|
||||
# заголовок
|
||||
title = Paragraph(
|
||||
f"Отчет по алертам (сгенерирован {timezone.localtime().strftime('%Y-%m-%d %H:%M %Z')})",
|
||||
styles['CustomHeading1']
|
||||
)
|
||||
elements.append(title)
|
||||
|
||||
# данные для таблицы
|
||||
headers = ['ID', 'Датчик', 'Объект', 'Значение', 'Тип датчика', 'Сообщение',
|
||||
'Уровень', 'Дата создания', 'Обработан']
|
||||
|
||||
# оборачиваем заголовки в параграф
|
||||
header_cells = [Paragraph(str(header), styles['CustomNormal']) for header in headers]
|
||||
data = [header_cells]
|
||||
|
||||
for alert in alerts:
|
||||
zone = alert.sensor.zones.first()
|
||||
object_name = zone.object.title if zone else 'Не указан'
|
||||
|
||||
# оборачиваем каждую ячейку в параграф
|
||||
row_data = [
|
||||
Paragraph(str(alert.id), styles['CustomNormal']),
|
||||
Paragraph(str(alert.sensor.name), styles['CustomNormal']),
|
||||
Paragraph(str(object_name), styles['CustomNormal']),
|
||||
Paragraph(
|
||||
str(f"{alert.metric.value} {alert.sensor.signal_format.unit if alert.sensor.signal_format else ''}" if alert.metric.value else alert.metric.raw_value),
|
||||
styles['CustomNormal']
|
||||
),
|
||||
Paragraph(str(alert.sensor_type.name), styles['CustomNormal']),
|
||||
Paragraph(str(alert.message), styles['CustomNormal']),
|
||||
Paragraph(str(alert.severity), styles['CustomNormal']),
|
||||
Paragraph(timezone.localtime(alert.created_at).strftime("%Y-%m-%d %H:%M:%S %Z"), styles['CustomNormal']),
|
||||
Paragraph('Да' if alert.resolved else 'Нет', styles['CustomNormal'])
|
||||
]
|
||||
data.append(row_data)
|
||||
|
||||
# создаем таблицу
|
||||
table = Table(data)
|
||||
table.setStyle(TableStyle([
|
||||
# стиль заголовка
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1b8755')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 11),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('TOPPADDING', (0, 0), (-1, 0), 12),
|
||||
|
||||
# стиль данных
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
|
||||
# отступы для всех ячеек
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 8),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 8),
|
||||
|
||||
# чередующиеся цвета строк для лучшей читаемости
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')])
|
||||
]))
|
||||
|
||||
elements.append(table)
|
||||
doc.build(elements)
|
||||
|
||||
return buffer.getvalue()
|
||||
@@ -25,6 +25,7 @@ pytest-django==4.11.1
|
||||
python-dotenv==1.1.1
|
||||
PyYAML==6.0.3
|
||||
referencing==0.36.2
|
||||
reportlab==4.4.4
|
||||
requests==2.32.5
|
||||
rpds-py==0.27.1
|
||||
sqlparse==0.5.3
|
||||
|
||||
Reference in New Issue
Block a user