feat / AEB-64 create alert routes
This commit is contained in:
8
backend/Pipfile.lock
generated
8
backend/Pipfile.lock
generated
@@ -26,11 +26,11 @@
|
|||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
|
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
|
||||||
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
|
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==25.3.0"
|
"version": "==25.4.0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
35
backend/api/account/serializers/alert_serializers.py
Normal file
35
backend/api/account/serializers/alert_serializers.py
Normal file
@@ -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
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.conf import settings
|
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
|
from sitemanagement.models import Object, Zone, Sensor
|
||||||
|
|
||||||
class SensorBasicSerializer(serializers.ModelSerializer):
|
class SensorBasicSerializer(serializers.ModelSerializer):
|
||||||
@@ -22,6 +25,7 @@ class ObjectSerializer(serializers.ModelSerializer):
|
|||||||
model = Object
|
model = Object
|
||||||
fields = ('id', 'title', 'description', 'image', 'address', 'floors', 'area', 'zones')
|
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 изображения объекта"""
|
"""Возвращает URL изображения объекта"""
|
||||||
return f"{settings.BASE_URL}{obj.image.url}" if obj.image else None
|
return f"{settings.BASE_URL}{obj.image.url}" if obj.image else None
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
from rest_framework import serializers
|
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
|
from sitemanagement.models import Sensor, Alert
|
||||||
|
|
||||||
class NotificationSerializer(serializers.ModelSerializer):
|
class NotificationSerializer(serializers.ModelSerializer):
|
||||||
@@ -76,7 +79,8 @@ class DetectorSerializer(serializers.ModelSerializer):
|
|||||||
class DetectorsResponseSerializer(serializers.Serializer):
|
class DetectorsResponseSerializer(serializers.Serializer):
|
||||||
detectors = serializers.SerializerMethodField()
|
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)
|
detector_serializer = DetectorSerializer(sensors, many=True)
|
||||||
return {
|
return {
|
||||||
sensor['detector_id']: sensor
|
sensor['detector_id']: sensor
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.urls import path
|
|||||||
from .views.UserDataView import UserDataView
|
from .views.UserDataView import UserDataView
|
||||||
from .views.objects_views import ObjectView
|
from .views.objects_views import ObjectView
|
||||||
from .views.sensors_views import SensorView
|
from .views.sensors_views import SensorView
|
||||||
|
from .views.alert_views import AlertView
|
||||||
from drf_spectacular.views import (
|
from drf_spectacular.views import (
|
||||||
SpectacularAPIView,
|
SpectacularAPIView,
|
||||||
SpectacularSwaggerView,
|
SpectacularSwaggerView,
|
||||||
@@ -23,6 +24,11 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
|
|
||||||
path("user/", UserDataView.as_view(), name="user-data"),
|
path("user/", UserDataView.as_view(), name="user-data"),
|
||||||
|
|
||||||
path("get-objects/", ObjectView.as_view(), name="objects"),
|
path("get-objects/", ObjectView.as_view(), name="objects"),
|
||||||
|
|
||||||
path("get-detectors/", SensorView.as_view(), name="detectors"),
|
path("get-detectors/", SensorView.as_view(), name="detectors"),
|
||||||
|
|
||||||
|
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"),
|
||||||
]
|
]
|
||||||
|
|||||||
81
backend/api/account/views/alert_views.py
Normal file
81
backend/api/account/views/alert_views.py
Normal file
@@ -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)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class ObjectView(APIView):
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "Объект 1",
|
"title": "Объект 1",
|
||||||
"description": "Описание объекта 1",
|
"description": "Описание объекта 1",
|
||||||
"image": "https://example.com/image.jpg",
|
"image": "https://aerbim.org/media/image.jpg",
|
||||||
"address": "Адрес объекта 1",
|
"address": "Адрес объекта 1",
|
||||||
"floors": 1,
|
"floors": 1,
|
||||||
"area": 100.0,
|
"area": 100.0,
|
||||||
|
|||||||
5
backend/api/utils/error_serializer.py
Normal file
5
backend/api/utils/error_serializer.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class ErrorResponseSerializer(serializers.Serializer):
|
||||||
|
"""Сериализатор для ответа с ошибкой"""
|
||||||
|
error = serializers.CharField(help_text="Текст ошибки")
|
||||||
@@ -100,6 +100,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
{'name': 'Профиль', 'description': 'Методы для получения данных профиля пользователя'},
|
{'name': 'Профиль', 'description': 'Методы для получения данных профиля пользователя'},
|
||||||
{'name': 'Объекты', 'description': 'Метод для получения данных об объектах'},
|
{'name': 'Объекты', 'description': 'Метод для получения данных об объектах'},
|
||||||
{'name': 'Датчики', 'description': 'Методы для работы с датчиками'},
|
{'name': 'Датчики', 'description': 'Методы для работы с датчиками'},
|
||||||
|
{'name': 'Алерты', 'description': 'Методы для работы с алертами'},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
asgiref==3.10.0
|
asgiref==3.10.0
|
||||||
attrs==25.3.0
|
attrs==25.4.0
|
||||||
certifi==2025.10.5
|
certifi==2025.10.5
|
||||||
charset-normalizer==3.4.3
|
charset-normalizer==3.4.3
|
||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ class MultiplexorAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('name', 'ip', 'subnet', 'gateway', 'sd_path')
|
search_fields = ('name', 'ip', 'subnet', 'gateway', 'sd_path')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('ip', 'subnet', 'gateway', 'sd_path')
|
|
||||||
list_display_links = ('name',)
|
list_display_links = ('name',)
|
||||||
|
|
||||||
@admin.register(Channel)
|
@admin.register(Channel)
|
||||||
@@ -18,7 +17,6 @@ class ChannelAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('multiplexor', 'number', 'position')
|
search_fields = ('multiplexor', 'number', 'position')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('number', 'position')
|
|
||||||
list_display_links = ('multiplexor',)
|
list_display_links = ('multiplexor',)
|
||||||
|
|
||||||
@admin.register(SensorType)
|
@admin.register(SensorType)
|
||||||
@@ -28,7 +26,6 @@ class SensorTypeAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('code', 'name', 'description')
|
search_fields = ('code', 'name', 'description')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('name', 'description')
|
|
||||||
list_display_links = ('code',)
|
list_display_links = ('code',)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,17 +36,15 @@ class MetricAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('timestamp', 'sensor', 'status')
|
search_fields = ('timestamp', 'sensor', 'status')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('value', 'status')
|
|
||||||
list_display_links = ('timestamp',)
|
list_display_links = ('timestamp',)
|
||||||
|
|
||||||
@admin.register(Alert)
|
@admin.register(Alert)
|
||||||
class AlertAdmin(admin.ModelAdmin):
|
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')
|
list_filter = ('sensor', 'metric', 'sensor_type', 'severity')
|
||||||
search_fields = ('sensor', 'metric', 'sensor_type', 'severity')
|
search_fields = ('sensor', 'metric', 'sensor_type', 'severity')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('message', 'severity', 'resolved')
|
|
||||||
list_display_links = ('sensor',)
|
list_display_links = ('sensor',)
|
||||||
|
|
||||||
@admin.register(SignalFormat)
|
@admin.register(SignalFormat)
|
||||||
@@ -59,7 +54,6 @@ class SignalFormatAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('sensor_type', 'code', 'unit', 'conversion_rule')
|
search_fields = ('sensor_type', 'code', 'unit', 'conversion_rule')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('unit', 'conversion_rule')
|
|
||||||
list_display_links = ('sensor_type',)
|
list_display_links = ('sensor_type',)
|
||||||
|
|
||||||
@admin.register(Sensor)
|
@admin.register(Sensor)
|
||||||
@@ -69,7 +63,6 @@ class SensorAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
search_fields = ('channel', 'sensor_type', 'signal_format', 'serial_number', 'name', 'math_formula')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('signal_format', 'serial_number', 'name', 'math_formula')
|
|
||||||
list_display_links = ('channel',)
|
list_display_links = ('channel',)
|
||||||
|
|
||||||
@admin.register(Object)
|
@admin.register(Object)
|
||||||
@@ -88,5 +81,4 @@ class ZoneAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('object__title', 'name')
|
search_fields = ('object__title', 'name')
|
||||||
list_per_page = 10
|
list_per_page = 10
|
||||||
list_max_show_all = 100
|
list_max_show_all = 100
|
||||||
list_editable = ('floor', 'name')
|
|
||||||
list_display_links = ('object',)
|
list_display_links = ('object',)
|
||||||
Reference in New Issue
Block a user