From 497cd7e2921aeafe6ab16514368f438bfb217bd7 Mon Sep 17 00:00:00 2001 From: Timofey Date: Tue, 7 Oct 2025 12:42:01 +0300 Subject: [PATCH] feat / AEB-64 create alert routes --- backend/Pipfile.lock | 8 +- .../account/serializers/alert_serializers.py | 35 ++++++++ .../serializers/objects_serializers.py | 6 +- .../account/serializers/sensor_serializers.py | 6 +- backend/api/account/urls.py | 6 ++ backend/api/account/views/alert_views.py | 81 +++++++++++++++++++ backend/api/account/views/objects_views.py | 2 +- backend/api/utils/error_serializer.py | 5 ++ backend/base/settings.py | 1 + backend/requirements.txt | 2 +- backend/sitemanagement/admin.py | 10 +-- 11 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 backend/api/account/serializers/alert_serializers.py create mode 100644 backend/api/account/views/alert_views.py create mode 100644 backend/api/utils/error_serializer.py diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 37d7c37..a70f31e 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -26,11 +26,11 @@ }, "attrs": { "hashes": [ - "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", - "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", + "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" + "markers": "python_version >= '3.9'", + "version": "==25.4.0" }, "certifi": { "hashes": [ diff --git a/backend/api/account/serializers/alert_serializers.py b/backend/api/account/serializers/alert_serializers.py new file mode 100644 index 0000000..1104bef --- /dev/null +++ b/backend/api/account/serializers/alert_serializers.py @@ -0,0 +1,35 @@ +from rest_framework import serializers +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes +from typing import Optional +from sitemanagement.models import Alert + +class AlertSerializer(serializers.ModelSerializer): + name = serializers.SerializerMethodField() + object = serializers.SerializerMethodField() + metric_value = serializers.SerializerMethodField() + sensor_type_name = serializers.SerializerMethodField() + + class Meta: + model = Alert + fields = ('id', 'name', 'object', 'metric_value', 'sensor_type_name', 'message', 'severity', 'created_at', 'resolved') + + @extend_schema_field(OpenApiTypes.STR) + def get_name(self, obj) -> str: + return obj.sensor.name + + @extend_schema_field(OpenApiTypes.STR) + def get_object(self, obj) -> Optional[str]: + zone = obj.sensor.zones.first() + return zone.object.title if zone else None + + @extend_schema_field(OpenApiTypes.STR) + def get_metric_value(self, obj) -> str: + if obj.metric.value is not None: + unit = obj.sensor.signal_format.unit if obj.sensor.signal_format else '' + return f"{obj.metric.value} {unit}".strip() + return obj.metric.raw_value + + @extend_schema_field(OpenApiTypes.STR) + def get_sensor_type_name(self, obj) -> str: + return obj.sensor_type.name \ No newline at end of file diff --git a/backend/api/account/serializers/objects_serializers.py b/backend/api/account/serializers/objects_serializers.py index 0fcdb5c..1bcdc12 100644 --- a/backend/api/account/serializers/objects_serializers.py +++ b/backend/api/account/serializers/objects_serializers.py @@ -1,5 +1,8 @@ from rest_framework import serializers from django.conf import settings +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes +from typing import Optional from sitemanagement.models import Object, Zone, Sensor class SensorBasicSerializer(serializers.ModelSerializer): @@ -22,6 +25,7 @@ class ObjectSerializer(serializers.ModelSerializer): model = Object fields = ('id', 'title', 'description', 'image', 'address', 'floors', 'area', 'zones') - def get_image(self, obj): + @extend_schema_field(OpenApiTypes.URI) + def get_image(self, obj) -> Optional[str]: """Возвращает URL изображения объекта""" return f"{settings.BASE_URL}{obj.image.url}" if obj.image else None \ No newline at end of file diff --git a/backend/api/account/serializers/sensor_serializers.py b/backend/api/account/serializers/sensor_serializers.py index 460fac2..a179912 100644 --- a/backend/api/account/serializers/sensor_serializers.py +++ b/backend/api/account/serializers/sensor_serializers.py @@ -1,4 +1,7 @@ from rest_framework import serializers +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes +from typing import Dict, Any from sitemanagement.models import Sensor, Alert class NotificationSerializer(serializers.ModelSerializer): @@ -76,7 +79,8 @@ class DetectorSerializer(serializers.ModelSerializer): class DetectorsResponseSerializer(serializers.Serializer): detectors = serializers.SerializerMethodField() - def get_detectors(self, sensors): + @extend_schema_field(OpenApiTypes.OBJECT) + def get_detectors(self, sensors) -> Dict[str, Any]: detector_serializer = DetectorSerializer(sensors, many=True) return { sensor['detector_id']: sensor diff --git a/backend/api/account/urls.py b/backend/api/account/urls.py index af5c63a..d34c932 100644 --- a/backend/api/account/urls.py +++ b/backend/api/account/urls.py @@ -2,6 +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 drf_spectacular.views import ( SpectacularAPIView, SpectacularSwaggerView, @@ -23,6 +24,11 @@ urlpatterns = [ ), path("user/", UserDataView.as_view(), name="user-data"), + path("get-objects/", ObjectView.as_view(), name="objects"), + path("get-detectors/", SensorView.as_view(), name="detectors"), + + path("get-alerts/", AlertView.as_view({'get': 'get_alerts'}), name="alerts"), + path("update-alert//", AlertView.as_view({'patch': 'change_alert_status'}), name="update-alert"), ] diff --git a/backend/api/account/views/alert_views.py b/backend/api/account/views/alert_views.py new file mode 100644 index 0000000..c29fa81 --- /dev/null +++ b/backend/api/account/views/alert_views.py @@ -0,0 +1,81 @@ +from rest_framework import status, serializers +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 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 + +@extend_schema(tags=['Алерты']) +class AlertView(ViewSet): + permission_classes = [IsAuthenticated] + + @extend_schema( + summary="Получение списка алертов", + description="Возвращает список всех алертов в системе", + responses={ + 200: OpenApiResponse(response=AlertSerializer(many=True), description="Список алертов успешно получен", + examples=[OpenApiExample( + 'Успешный ответ', + value=[{ + "id": 1, + "name": "Датчик 1", + "object": "Объект 1", + "metric_value": "12.5 °C", + "sensor_type_name": "Инклинометр", + "message": "alert message", + "severity": "warning", + "created_at": "2025-10-06T15:53:11.759725+03:00", + "resolved": False + }] + )]) + }) + + @action(detail=False, methods=['get']) + @handle_exceptions + def get_alerts(self, request): + alerts = Alert.objects.all() + serializer = AlertSerializer(alerts, many=True) + return Response(serializer.data) + + @extend_schema( + summary="Изменение статуса алерта", + description="Изменяет статус обработки алерта на противоположный", + responses={ + 200: OpenApiResponse(response=AlertSerializer, description="Статус алерта успешно изменен", + examples=[OpenApiExample( + 'Успешный ответ', + value={ + "message": "Статус алерта успешно изменен" + } + )]), + 404: OpenApiResponse( + response=ErrorResponseSerializer, + description="Алерт не найден", + examples=[ + OpenApiExample( + 'Алерт не найден', + value={"error": "Алерт не найден"}, + status_codes=['404'] + ) + ] + ) + }) + @action(detail=True, methods=['patch']) + @handle_exceptions + def change_alert_status(self, request, pk=None): + try: + alert = Alert.objects.get(pk=pk) + alert.resolved = not alert.resolved + alert.save() + + return Response({"message": "Статус алерта успешно изменен"}, status=status.HTTP_200_OK) + + except Alert.DoesNotExist: + return Response( + {"error": "Алерт не найден"}, + status=status.HTTP_404_NOT_FOUND) + diff --git a/backend/api/account/views/objects_views.py b/backend/api/account/views/objects_views.py index d7da2ab..f4095a5 100644 --- a/backend/api/account/views/objects_views.py +++ b/backend/api/account/views/objects_views.py @@ -24,7 +24,7 @@ class ObjectView(APIView): "id": 1, "title": "Объект 1", "description": "Описание объекта 1", - "image": "https://example.com/image.jpg", + "image": "https://aerbim.org/media/image.jpg", "address": "Адрес объекта 1", "floors": 1, "area": 100.0, diff --git a/backend/api/utils/error_serializer.py b/backend/api/utils/error_serializer.py new file mode 100644 index 0000000..fcbdc2d --- /dev/null +++ b/backend/api/utils/error_serializer.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + +class ErrorResponseSerializer(serializers.Serializer): + """Сериализатор для ответа с ошибкой""" + error = serializers.CharField(help_text="Текст ошибки") \ No newline at end of file diff --git a/backend/base/settings.py b/backend/base/settings.py index 4a86d3d..4d26a24 100644 --- a/backend/base/settings.py +++ b/backend/base/settings.py @@ -100,6 +100,7 @@ SPECTACULAR_SETTINGS = { {'name': 'Профиль', 'description': 'Методы для получения данных профиля пользователя'}, {'name': 'Объекты', 'description': 'Метод для получения данных об объектах'}, {'name': 'Датчики', 'description': 'Методы для работы с датчиками'}, + {'name': 'Алерты', 'description': 'Методы для работы с алертами'}, ], } diff --git a/backend/requirements.txt b/backend/requirements.txt index cc03a3f..a2b276e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ asgiref==3.10.0 -attrs==25.3.0 +attrs==25.4.0 certifi==2025.10.5 charset-normalizer==3.4.3 Django==5.2.7 diff --git a/backend/sitemanagement/admin.py b/backend/sitemanagement/admin.py index ffe2be8..22b2ac6 100644 --- a/backend/sitemanagement/admin.py +++ b/backend/sitemanagement/admin.py @@ -8,7 +8,6 @@ class MultiplexorAdmin(admin.ModelAdmin): search_fields = ('name', 'ip', 'subnet', 'gateway', 'sd_path') list_per_page = 10 list_max_show_all = 100 - list_editable = ('ip', 'subnet', 'gateway', 'sd_path') list_display_links = ('name',) @admin.register(Channel) @@ -18,7 +17,6 @@ class ChannelAdmin(admin.ModelAdmin): search_fields = ('multiplexor', 'number', 'position') list_per_page = 10 list_max_show_all = 100 - list_editable = ('number', 'position') list_display_links = ('multiplexor',) @admin.register(SensorType) @@ -28,7 +26,6 @@ class SensorTypeAdmin(admin.ModelAdmin): search_fields = ('code', 'name', 'description') list_per_page = 10 list_max_show_all = 100 - list_editable = ('name', 'description') list_display_links = ('code',) @@ -39,17 +36,15 @@ class MetricAdmin(admin.ModelAdmin): search_fields = ('timestamp', 'sensor', 'status') list_per_page = 10 list_max_show_all = 100 - list_editable = ('value', 'status') list_display_links = ('timestamp',) @admin.register(Alert) class AlertAdmin(admin.ModelAdmin): - list_display = ('sensor', 'metric', 'sensor_type', 'message', 'severity', 'created_at', 'resolved') + list_display = ('sensor', 'sensor_type', 'message', 'severity', 'created_at', 'resolved') list_filter = ('sensor', 'metric', 'sensor_type', 'severity') search_fields = ('sensor', 'metric', 'sensor_type', 'severity') list_per_page = 10 list_max_show_all = 100 - list_editable = ('message', 'severity', 'resolved') list_display_links = ('sensor',) @admin.register(SignalFormat) @@ -59,7 +54,6 @@ class SignalFormatAdmin(admin.ModelAdmin): search_fields = ('sensor_type', 'code', 'unit', 'conversion_rule') list_per_page = 10 list_max_show_all = 100 - list_editable = ('unit', 'conversion_rule') list_display_links = ('sensor_type',) @admin.register(Sensor) @@ -69,7 +63,6 @@ class SensorAdmin(admin.ModelAdmin): search_fields = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula') list_per_page = 10 list_max_show_all = 100 - list_editable = ('signal_format', 'serial_number', 'name', 'math_formula') list_display_links = ('channel',) @admin.register(Object) @@ -88,5 +81,4 @@ class ZoneAdmin(admin.ModelAdmin): search_fields = ('object__title', 'name') list_per_page = 10 list_max_show_all = 100 - list_editable = ('floor', 'name') list_display_links = ('object',) \ No newline at end of file