fix(api): align contracts with frontend md
Some checks failed
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 3m20s
CI/CD Pipeline / Run Tests (pull_request) Successful in 13m45s
CI/CD Pipeline / Run API Inventory E2E Tests (pull_request) Successful in 22s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped

This commit is contained in:
2026-03-23 13:12:10 +01:00
parent d030c705ac
commit 4ca6b75393
12 changed files with 215 additions and 215 deletions

View File

@@ -1,6 +1,7 @@
"""Сериализаторы приложения обмена данными."""
import json
import uuid
from typing import Any
from apps.exchange.models import ExchangeConnection
@@ -51,6 +52,8 @@ def get_periodic_task_payload(task: PeriodicTask) -> dict[str, Any]:
class ExchangeConnectionSerializer(serializers.ModelSerializer):
"""Сериализатор подключения без выдачи пароля в ответах."""
id = serializers.CharField(source="pk", read_only=True)
class Meta:
model = ExchangeConnection
fields = [
@@ -65,6 +68,19 @@ class ExchangeConnectionSerializer(serializers.ModelSerializer):
read_only_fields = fields
class ExchangeConnectionListResponseSerializer(serializers.Serializer):
"""Frontend-friendly wrapper for exchange connections list."""
results = ExchangeConnectionSerializer(many=True, read_only=True)
class ExchangeConnectionTestResponseSerializer(serializers.Serializer):
"""Ответ проверки подключения в формате frontend."""
success = serializers.BooleanField(read_only=True)
message = serializers.CharField(read_only=True)
class ExchangeConnectionCreateSerializer(serializers.Serializer):
"""Входные данные для создания активного подключения."""
@@ -107,14 +123,12 @@ class ExchangeCopyRequestSerializer(serializers.Serializer):
class ExchangePeriodicTaskSerializer(serializers.ModelSerializer):
"""Сериализатор периодической задачи обмена."""
id = serializers.CharField(source="pk", read_only=True)
schedule_type = serializers.SerializerMethodField()
interval_every = serializers.SerializerMethodField()
interval_period = serializers.SerializerMethodField()
crontab_minute = serializers.SerializerMethodField()
crontab_hour = serializers.SerializerMethodField()
crontab_day_of_week = serializers.SerializerMethodField()
crontab_day_of_month = serializers.SerializerMethodField()
crontab_month_of_year = serializers.SerializerMethodField()
notify_on_error = serializers.SerializerMethodField()
class Meta:
@@ -126,9 +140,6 @@ class ExchangePeriodicTaskSerializer(serializers.ModelSerializer):
"interval_period",
"crontab_minute",
"crontab_hour",
"crontab_day_of_week",
"crontab_day_of_month",
"crontab_month_of_year",
"notify_on_error",
]
read_only_fields = fields
@@ -137,7 +148,7 @@ class ExchangePeriodicTaskSerializer(serializers.ModelSerializer):
if obj.interval_id:
return "interval"
if obj.crontab_id:
return "crontab"
return "daily"
return None
def get_interval_every(self, obj: PeriodicTask) -> int | None:
@@ -146,64 +157,46 @@ class ExchangePeriodicTaskSerializer(serializers.ModelSerializer):
def get_interval_period(self, obj: PeriodicTask) -> str | None:
return obj.interval.period if obj.interval_id else None
def get_crontab_minute(self, obj: PeriodicTask) -> str | None:
return obj.crontab.minute if obj.crontab_id else None
@staticmethod
def _coerce_crontab_number(value: str | None) -> int | None:
if value is None:
return None
return int(value) if str(value).isdigit() else None
def get_crontab_hour(self, obj: PeriodicTask) -> str | None:
return obj.crontab.hour if obj.crontab_id else None
def get_crontab_minute(self, obj: PeriodicTask) -> int | None:
return self._coerce_crontab_number(obj.crontab.minute) if obj.crontab_id else None
def get_crontab_day_of_week(self, obj: PeriodicTask) -> str | None:
return obj.crontab.day_of_week if obj.crontab_id else None
def get_crontab_day_of_month(self, obj: PeriodicTask) -> str | None:
return obj.crontab.day_of_month if obj.crontab_id else None
def get_crontab_month_of_year(self, obj: PeriodicTask) -> str | None:
return obj.crontab.month_of_year if obj.crontab_id else None
def get_crontab_hour(self, obj: PeriodicTask) -> int | None:
return self._coerce_crontab_number(obj.crontab.hour) if obj.crontab_id else None
def get_notify_on_error(self, obj: PeriodicTask) -> bool:
return bool(get_periodic_task_payload(obj).get("notify_on_error", False))
def to_representation(self, instance):
data = super().to_representation(instance)
return {key: value for key, value in data.items() if value is not None}
class ExchangePeriodicTaskListResponseSerializer(serializers.Serializer):
"""Frontend-friendly wrapper for periodic tasks list."""
results = ExchangePeriodicTaskSerializer(many=True, read_only=True)
class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
"""Входные данные для создания и изменения периодической задачи обмена."""
name = serializers.CharField(max_length=200, required=False)
description = serializers.CharField(required=False, allow_blank=True)
enabled = serializers.BooleanField(required=False)
schedule_type = serializers.ChoiceField(
choices=["interval", "crontab"],
required=False,
)
schedule_type = serializers.CharField(required=False)
interval_every = serializers.IntegerField(min_value=1, required=False)
interval_period = serializers.ChoiceField(
choices=[choice[0] for choice in IntervalSchedule.PERIOD_CHOICES],
required=False,
)
crontab_minute = serializers.CharField(max_length=64, required=False)
crontab_hour = serializers.CharField(max_length=64, required=False)
crontab_day_of_week = serializers.CharField(max_length=64, required=False)
crontab_day_of_month = serializers.CharField(max_length=64, required=False)
crontab_month_of_year = serializers.CharField(max_length=64, required=False)
mode = serializers.ChoiceField(
choices=["all", "single", "selected"],
required=False,
)
table = serializers.CharField(required=False)
tables = serializers.ListField(
child=serializers.CharField(),
required=False,
allow_empty=False,
)
truncate_before_copy = serializers.BooleanField(required=False)
crontab_minute = serializers.IntegerField(min_value=0, max_value=59, required=False)
crontab_hour = serializers.IntegerField(min_value=0, max_value=23, required=False)
notify_on_error = serializers.BooleanField(required=False)
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
if not self.instance and "name" not in attrs:
raise serializers.ValidationError({"name": "Обязательное поле."})
schedule_type = self._resolve_schedule_type(attrs)
if not schedule_type:
raise serializers.ValidationError(
@@ -213,27 +206,41 @@ class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
payload = self._build_payload(attrs)
schedule = self._build_schedule(attrs, schedule_type)
attrs["name"] = self._build_name()
attrs["payload"] = validate_exchange_copy_payload(payload)
attrs["schedule"] = schedule
return attrs
def _resolve_schedule_type(self, attrs: dict[str, Any]) -> str | None:
if "schedule_type" in attrs:
return attrs["schedule_type"]
schedule_type = str(attrs["schedule_type"]).strip().lower()
if schedule_type not in {"interval", "daily"}:
raise serializers.ValidationError(
{
"schedule_type": (
"Допустимые значения: interval или daily."
)
}
)
return schedule_type
if self.instance and self.instance.interval_id:
return "interval"
if self.instance and self.instance.crontab_id:
return "crontab"
return "daily"
return None
def _build_name(self) -> str:
if self.instance:
return self.instance.name
return f"exchange-periodic-{uuid.uuid4()}"
def _build_payload(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_payload = (
get_periodic_task_payload(self.instance) if self.instance else {}
)
mode = attrs.get("mode", current_payload.get("mode", "all"))
truncate_before_copy = attrs.get(
"truncate_before_copy",
current_payload.get("truncate_before_copy", True),
@@ -243,20 +250,10 @@ class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
current_payload.get("notify_on_error", False),
)
if "mode" in attrs and attrs["mode"] != "single" and "table" not in attrs:
table = None
else:
table = attrs.get("table", current_payload.get("table"))
if "mode" in attrs and attrs["mode"] != "selected" and "tables" not in attrs:
tables = None
else:
tables = attrs.get("tables", current_payload.get("tables"))
return {
"mode": mode,
"table": table,
"tables": tables,
"mode": current_payload.get("mode", "all"),
"table": current_payload.get("table"),
"tables": current_payload.get("tables"),
"truncate_before_copy": truncate_before_copy,
"notify_on_error": notify_on_error,
}
@@ -268,7 +265,7 @@ class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
) -> dict[str, Any]:
if schedule_type == "interval":
return self._build_interval_schedule(attrs)
return self._build_crontab_schedule(attrs)
return self._build_daily_schedule(attrs)
def _build_interval_schedule(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_schedule = self.instance.interval if self.instance else None
@@ -295,40 +292,30 @@ class ExchangePeriodicTaskUpsertSerializer(serializers.Serializer):
"period": interval_period,
}
def _build_crontab_schedule(self, attrs: dict[str, Any]) -> dict[str, Any]:
def _build_daily_schedule(self, attrs: dict[str, Any]) -> dict[str, Any]:
current_schedule = self.instance.crontab if self.instance else None
fields = {
"minute": attrs.get(
"crontab_minute",
current_schedule.minute if current_schedule else None,
),
"hour": attrs.get(
"crontab_hour",
current_schedule.hour if current_schedule else None,
),
"day_of_week": attrs.get(
"crontab_day_of_week",
current_schedule.day_of_week if current_schedule else None,
),
"day_of_month": attrs.get(
"crontab_day_of_month",
current_schedule.day_of_month if current_schedule else None,
),
"month_of_year": attrs.get(
"crontab_month_of_year",
current_schedule.month_of_year if current_schedule else None,
),
}
minute = attrs.get(
"crontab_minute",
int(current_schedule.minute) if current_schedule else None,
)
hour = attrs.get(
"crontab_hour",
int(current_schedule.hour) if current_schedule else None,
)
errors = {
f"crontab_{field_name}": "Обязательное поле для crontab."
for field_name, value in fields.items()
if value is None
}
errors = {}
if minute is None:
errors["crontab_minute"] = "Обязательное поле для daily."
if hour is None:
errors["crontab_hour"] = "Обязательное поле для daily."
if errors:
raise serializers.ValidationError(errors)
return {
"type": "crontab",
**fields,
"minute": str(minute),
"hour": str(hour),
"day_of_week": "*",
"day_of_month": "*",
"month_of_year": "*",
}

View File

@@ -8,8 +8,11 @@ from apps.core.services import BackgroundJobService
from apps.exchange.models import ExchangeConnection
from apps.exchange.serializers import (
ExchangeConnectionCreateSerializer,
ExchangeConnectionListResponseSerializer,
ExchangeConnectionSerializer,
ExchangeConnectionTestResponseSerializer,
ExchangeCopyRequestSerializer,
ExchangePeriodicTaskListResponseSerializer,
ExchangePeriodicTaskSerializer,
ExchangePeriodicTaskUpsertSerializer,
)
@@ -45,7 +48,7 @@ class ExchangeConnectionListCreateView(APIView):
"Пароль в ответ не возвращается."
),
responses={
200: ExchangeConnectionSerializer(many=True),
200: ExchangeConnectionListResponseSerializer,
**ErrorResponses.ADMIN,
},
)
@@ -100,16 +103,7 @@ class ExchangeConnectionTestView(APIView):
),
request_body=ExchangeConnectionCreateSerializer,
responses={
200: openapi.Response(
description="Проверка успешна",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"status": openapi.Schema(type=openapi.TYPE_STRING),
"message": openapi.Schema(type=openapi.TYPE_STRING),
},
),
),
200: ExchangeConnectionTestResponseSerializer,
400: CommonResponses.BAD_REQUEST,
**ErrorResponses.ADMIN,
},
@@ -232,7 +226,7 @@ class ExchangePeriodicTaskListCreateView(APIView):
"django_celery_beat."
),
responses={
200: ExchangePeriodicTaskSerializer(many=True),
200: ExchangePeriodicTaskListResponseSerializer,
**ErrorResponses.ADMIN,
},
)
@@ -263,8 +257,8 @@ class ExchangePeriodicTaskListCreateView(APIView):
try:
task = ExchangePeriodicTaskService.create_periodic_task(
name=serializer.validated_data["name"],
description=serializer.validated_data.get("description", ""),
enabled=serializer.validated_data.get("enabled", True),
description="",
enabled=True,
payload=serializer.validated_data["payload"],
schedule=serializer.validated_data["schedule"],
)
@@ -327,9 +321,6 @@ class ExchangePeriodicTaskDetailView(APIView):
try:
task = ExchangePeriodicTaskService.update_periodic_task(
task=task,
name=serializer.validated_data.get("name"),
description=serializer.validated_data.get("description"),
enabled=serializer.validated_data.get("enabled"),
payload=serializer.validated_data["payload"],
schedule=serializer.validated_data["schedule"],
)