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
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:
@@ -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": "*",
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user