diff --git a/api/__pycache__/models.cpython-313.pyc b/api/__pycache__/models.cpython-313.pyc index 23cd358..1468336 100644 Binary files a/api/__pycache__/models.cpython-313.pyc and b/api/__pycache__/models.cpython-313.pyc differ diff --git a/api/__pycache__/serializers.cpython-313.pyc b/api/__pycache__/serializers.cpython-313.pyc index 5099a66..70e2ce5 100644 Binary files a/api/__pycache__/serializers.cpython-313.pyc and b/api/__pycache__/serializers.cpython-313.pyc differ diff --git a/api/__pycache__/urls.cpython-313.pyc b/api/__pycache__/urls.cpython-313.pyc index 7ace33c..a73920c 100644 Binary files a/api/__pycache__/urls.cpython-313.pyc and b/api/__pycache__/urls.cpython-313.pyc differ diff --git a/api/__pycache__/validators.cpython-313.pyc b/api/__pycache__/validators.cpython-313.pyc deleted file mode 100644 index 57e4db6..0000000 Binary files a/api/__pycache__/validators.cpython-313.pyc and /dev/null differ diff --git a/api/__pycache__/views.cpython-313.pyc b/api/__pycache__/views.cpython-313.pyc index a7c45b3..f1db0f6 100644 Binary files a/api/__pycache__/views.cpython-313.pyc and b/api/__pycache__/views.cpython-313.pyc differ diff --git a/api/migrations/0003_alter_userprediction_unique_together_and_more.py b/api/migrations/0003_alter_userprediction_unique_together_and_more.py deleted file mode 100644 index 332ea40..0000000 --- a/api/migrations/0003_alter_userprediction_unique_together_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.1.7 on 2025-04-05 10:40 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_prediction_user_userprediction_delete_todo'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='userprediction', - unique_together=set(), - ), - migrations.AlterField( - model_name='userprediction', - name='created_at', - field=models.DateTimeField(), - ), - migrations.AlterField( - model_name='userprediction', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/api/migrations/__pycache__/0003_alter_userprediction_unique_together_and_more.cpython-313.pyc b/api/migrations/__pycache__/0003_alter_userprediction_unique_together_and_more.cpython-313.pyc deleted file mode 100644 index e13512f..0000000 Binary files a/api/migrations/__pycache__/0003_alter_userprediction_unique_together_and_more.cpython-313.pyc and /dev/null differ diff --git a/api/models.py b/api/models.py index 61621af..a66861d 100644 --- a/api/models.py +++ b/api/models.py @@ -1,6 +1,6 @@ import uuid from django.db import models -from django.contrib.auth import get_user_model + class User(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -11,10 +11,10 @@ class Prediction(models.Model): result = models.JSONField() deleted_at = models.DateTimeField(null=True, blank=True) - - class UserPrediction(models.Model): - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE) - created_at = models.DateTimeField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + prediction = models.ForeignKey(Prediction, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + class Meta: + unique_together = ('user', 'prediction') diff --git a/api/serializers.py b/api/serializers.py index 4823d4f..3f0dc4f 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,86 +1,7 @@ from rest_framework import serializers from .models import Prediction -from datetime import datetime -from .validators import ( - validate_custom_curve, rate_clip, - _rfc3339_to_timestamp, base64_to_curve -) class PredictionSerializer(serializers.ModelSerializer): class Meta: model = Prediction fields = ['id', 'created_at', 'updated_at', 'result'] - - - -PROFILE_STANDARD = "standard_profile" -PROFILE_FLOAT = "float" -PROFILE_REVERSE = "reverse" -PROFILE_CUSTOM = "custom" -LATEST_DATASET_KEYWORD = "latest" -SUPPORTED_PROFILES = [PROFILE_STANDARD, PROFILE_FLOAT, PROFILE_REVERSE, PROFILE_CUSTOM] - - -class PredictionRequestSerializer(serializers.Serializer): - launch_latitude = serializers.FloatField(min_value=-90, max_value=90) - launch_longitude = serializers.FloatField(min_value=0, max_value=360) - launch_datetime = serializers.DateTimeField() - launch_altitude = serializers.FloatField(required=False) - format = serializers.CharField(default="json") - profile = serializers.ChoiceField(choices=SUPPORTED_PROFILES, default=PROFILE_STANDARD) - dataset = serializers.CharField(default=LATEST_DATASET_KEYWORD) - - # --- профиль-dependent поля --- - ascent_rate = serializers.FloatField(required=False, min_value=0.01) - descent_rate = serializers.FloatField(required=False, min_value=0.01) - burst_altitude = serializers.FloatField(required=False) - float_altitude = serializers.FloatField(required=False) - stop_datetime = serializers.DateTimeField(required=False) - ascent_curve = serializers.CharField(required=False) - descent_curve = serializers.CharField(required=False) - interpolate = serializers.BooleanField(required=False, default=False) - - def validate(self, data): - profile = data.get("profile", PROFILE_STANDARD) - launch_alt = data.get("launch_altitude", 0) - - if profile == PROFILE_STANDARD: - if 'burst_altitude' not in data: - raise serializers.ValidationError("burst_altitude is required for standard profile.") - if data['burst_altitude'] <= launch_alt: - raise serializers.ValidationError("burst_altitude must be greater than launch_altitude.") - - elif profile == PROFILE_FLOAT: - if 'float_altitude' not in data or data['float_altitude'] <= launch_alt: - raise serializers.ValidationError("float_altitude must be greater than launch_altitude.") - if 'stop_datetime' not in data or data['stop_datetime'] <= data['launch_datetime']: - raise serializers.ValidationError("stop_datetime must be later than launch_datetime.") - - elif profile == PROFILE_CUSTOM: - if 'ascent_curve' not in data or not validate_custom_curve(data['ascent_curve']): - raise serializers.ValidationError("Invalid ascent_curve.") - if 'descent_curve' not in data or not validate_custom_curve(data['descent_curve']): - raise serializers.ValidationError("Invalid descent_curve.") - if 'burst_altitude' not in data or data['burst_altitude'] <= launch_alt: - raise serializers.ValidationError("burst_altitude must be greater than launch_altitude.") - - # кастомная логика clipping'а - if 'ascent_rate' in data: - data['ascent_rate'] = rate_clip(data['ascent_rate']) - if 'descent_rate' in data: - data['descent_rate'] = rate_clip(data['descent_rate']) - - return data - - - -class PredictionListSerializer(serializers.ModelSerializer): - class Meta: - model = Prediction - fields = ["id", "created_at", "updated_at"] - - -class PredictionDetailSerializer(serializers.ModelSerializer): - class Meta: - model = Prediction - fields = ["id", "created_at", "updated_at", "result"] diff --git a/api/services/__pycache__/tawhiri.cpython-313.pyc b/api/services/__pycache__/tawhiri.cpython-313.pyc deleted file mode 100644 index 4e13d6e..0000000 Binary files a/api/services/__pycache__/tawhiri.cpython-313.pyc and /dev/null differ diff --git a/api/services/tawhiri.py b/api/services/tawhiri.py deleted file mode 100644 index 44d1fb0..0000000 --- a/api/services/tawhiri.py +++ /dev/null @@ -1,52 +0,0 @@ -import requests -from urllib.parse import urlencode -from datetime import datetime -from typing import Any -from zoneinfo import ZoneInfo -from collections import OrderedDict - -class TawhiriClient: - BASE_URL = "https://fly.stratonautica.ru/api/v2/" - TIMEOUT = 15 - - @staticmethod - def _convert_value(value: Any) -> Any: - if isinstance(value, datetime): - return value.isoformat().replace("+00:00", "Z") - return value - - @classmethod - def get_prediction(cls, params: dict) -> dict: - url = cls.build_url(params) - print("🔍 URL:", url) - response = requests.get(url, timeout=cls.TIMEOUT) - response.raise_for_status() - return response.json() - - @classmethod - def build_url(cls, params: dict) -> str: - query = OrderedDict() - - query["profile"] = params.get("profile") - query["launch_datetime"] = cls._convert_value(params.get("launch_datetime")) - query["launch_latitude"] = params.get("launch_latitude") - query["launch_longitude"] = params.get("launch_longitude") - query["launch_altitude"] = params.get("launch_altitude", 0) - query["ascent_rate"] = params.get("ascent_rate") - query["burst_altitude"] = params.get("burst_altitude") - query["descent_rate"] = params.get("descent_rate") - query["interpolate"] = str(params.get("interpolate", False)).lower() - #query["dataset"] = cls._convert_value(params.get("dataset")) - query["format"] = params.get("format", "json") - query["pred_type"] = "single" # <-- в конце - - filtered = {k: v for k, v in query.items() if v is not None} - return f"{cls.BASE_URL}?{urlencode(filtered)}" - - - @classmethod - def get_prediction(cls, params: dict) -> dict: - url = cls.build_url(params) - response = requests.get(url, timeout=cls.TIMEOUT) - response.raise_for_status() - return response.json() diff --git a/api/urls.py b/api/urls.py index eaff5cb..c256ed2 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,5 @@ from django.urls import path -from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView, - PredictionHistoryListView, - PredictionHistoryDetailView, - PredictionHistoryDeleteView,) +from .views import PredictionCreateView, PredictionListView, PredictionDeleteView from rest_framework.authtoken.views import obtain_auth_token urlpatterns = [ @@ -10,7 +7,4 @@ urlpatterns = [ path('predictions', PredictionListView.as_view(), name='get_predictions'), path('predictions/', PredictionDeleteView.as_view(), name='delete_prediction'), path('token/', obtain_auth_token), - path("history/", PredictionHistoryListView.as_view()), - path("history//", PredictionHistoryDetailView.as_view()), - path("history//delete/", PredictionHistoryDeleteView.as_view()), ] diff --git a/api/validators.py b/api/validators.py deleted file mode 100644 index 222c170..0000000 --- a/api/validators.py +++ /dev/null @@ -1,31 +0,0 @@ -import base64 -import json -from datetime import datetime - - -def rate_clip(rate): - """Ограничивает допустимые значения скорости (например, 0.1 ≤ x ≤ 100)""" - return min(max(rate, 0.1), 100.0) - - -def _rfc3339_to_timestamp(value): - """Парсинг RFC 3339 строки в datetime""" - return datetime.fromisoformat(value.replace("Z", "+00:00")) - - -def base64_to_curve(encoded): - """Декодирует base64-encoded curve""" - try: - decoded = base64.b64decode(encoded).decode('utf-8') - return json.loads(decoded) - except Exception as e: - raise ValueError(f"Invalid curve format: {e}") - - -def validate_custom_curve(curve): - """Проверяет, что curve имеет ожидаемую структуру (например, список точек)""" - try: - points = base64_to_curve(curve) - return isinstance(points, list) and all(isinstance(p, list) and len(p) == 2 for p in points) - except Exception: - return False diff --git a/api/views.py b/api/views.py index a4420fc..803abe6 100644 --- a/api/views.py +++ b/api/views.py @@ -1,19 +1,11 @@ -from rest_framework import status, generics, permissions +from rest_framework import status, generics from rest_framework.response import Response from rest_framework.views import APIView from django.utils import timezone from .models import Prediction, User, UserPrediction -from .serializers import PredictionSerializer, PredictionRequestSerializer, PredictionListSerializer, PredictionDetailSerializer +from .serializers import PredictionSerializer from rest_framework.permissions import IsAuthenticated import requests -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from rest_framework.permissions import AllowAny -from .services.tawhiri import TawhiriClient -from django.contrib.auth import get_user_model - -User = get_user_model() - def get_prediction_from_tawhiri(params): base_url = "https://fly.stratonautica.ru/api/v2" @@ -23,34 +15,25 @@ def get_prediction_from_tawhiri(params): return response.json() # получаем результат предсказания else: raise Exception(f"Tawhiri error: {response.status_code} {response.text}") - + + class PredictionCreateView(APIView): - permission_classes = [IsAuthenticated] - def post(self, request): - print("DEBUG: request.user =", request.user) - print("DEBUG: request.user.id =", request.user.id) - - serializer = PredictionRequestSerializer(data=request.data) - if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - validated_data = serializer.validated_data + user_id = request.data.get('user_id') + user = User.objects.get(id=user_id) + # Передаём остальные параметры (кроме user_id) в Tawhiri + tawhiri_params = {k: v for k, v in request.data.items() if k != 'user_id'} + try: - prediction_result = TawhiriClient.get_prediction(validated_data) - except requests.RequestException as e: - print("❌ Tawhiri error:", str(e), e.response.text if e.response else "no response") - return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY) + prediction_result = get_prediction_from_tawhiri(tawhiri_params) + except Exception as e: + return Response({"error": str(e)}, status=500) prediction = Prediction.objects.create(result=prediction_result) - UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now()) + UserPrediction.objects.create(user=user, prediction=prediction) - return Response({ - "id": prediction.id, - "created_at": prediction.created_at, - "result": prediction_result - }, status=status.HTTP_201_CREATED) + return Response(PredictionSerializer(prediction).data) class PredictionListView(APIView): def get(self, request): @@ -76,43 +59,5 @@ class PredictionDeleteView(APIView): except Prediction.DoesNotExist: return Response({"error": "Not found"}, status=404) - - -class PredictionHistoryListView(generics.ListAPIView): - permission_classes = [permissions.IsAuthenticated] - serializer_class = PredictionListSerializer - - def get_queryset(self): - return Prediction.objects.filter( - id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True), - deleted_at__isnull=True - ) - - -class PredictionHistoryDetailView(generics.RetrieveAPIView): - permission_classes = [permissions.IsAuthenticated] - serializer_class = PredictionDetailSerializer - - def get_queryset(self): - return Prediction.objects.filter( - id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True), - deleted_at__isnull=True - ) - - -class PredictionHistoryDeleteView(generics.DestroyAPIView): - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Prediction.objects.filter( - id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True), - deleted_at__isnull=True - ) - - def perform_destroy(self, instance): - instance.deleted_at = timezone.now() - instance.save() - - -#class PredictionCreateView(APIView): - #permission_classes = [IsAuthenticated] \ No newline at end of file +class PredictionCreateView(APIView): + permission_classes = [IsAuthenticated] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 41a74d0..a39dc9f 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/testapi/__pycache__/settings.cpython-313.pyc b/testapi/__pycache__/settings.cpython-313.pyc index b7622bf..4edb2ac 100644 Binary files a/testapi/__pycache__/settings.cpython-313.pyc and b/testapi/__pycache__/settings.cpython-313.pyc differ diff --git a/testapi/settings.py b/testapi/settings.py index 9409c61..89ff0c7 100644 --- a/testapi/settings.py +++ b/testapi/settings.py @@ -40,12 +40,10 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework.authtoken', 'drf_spectacular', - 'corsheaders', 'api' ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -55,8 +53,6 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -CORS_ALLOW_ALL_ORIGINS = True - ROOT_URLCONF = 'testapi.urls' TEMPLATES = [ @@ -142,10 +138,9 @@ REST_FRAMEWORK = { # ВАШИ НАСТРОЙКИ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.TokenAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', - #'rest_framework.permissions.AllowAny', ] }