initial commit
This commit is contained in:
commit
c6961c03c3
33 changed files with 1782 additions and 0 deletions
0
stratoflights_api/__init__.py
Normal file
0
stratoflights_api/__init__.py
Normal file
8
stratoflights_api/admin.py
Normal file
8
stratoflights_api/admin.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import User, Prediction, Satellite, TelemetryPacket
|
||||
|
||||
admin.site.register(User)
|
||||
admin.site.register(Prediction)
|
||||
admin.site.register(Satellite)
|
||||
admin.site.register(TelemetryPacket)
|
||||
6
stratoflights_api/apps.py
Normal file
6
stratoflights_api/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StratoflightsApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'stratoflights_api'
|
||||
38
stratoflights_api/build_qstring.py
Normal file
38
stratoflights_api/build_qstring.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import urllib.parse
|
||||
|
||||
def build_query_string(data: dict) -> str:
|
||||
required_keys = [
|
||||
"profile",
|
||||
"pred_type",
|
||||
"launch_datetime",
|
||||
"launch_latitude",
|
||||
"launch_longitude",
|
||||
"launch_altitude",
|
||||
"ascent_rate",
|
||||
"burst_altitude",
|
||||
"descent_rate"
|
||||
]
|
||||
|
||||
# Проверяем, что все ключи на месте
|
||||
missing_keys = [key for key in required_keys if key not in data]
|
||||
if missing_keys:
|
||||
raise ValueError(f"Missing required keys: {', '.join(missing_keys)}")
|
||||
|
||||
# Собираем строку запроса
|
||||
return urllib.parse.urlencode({k: data[k] for k in required_keys})
|
||||
|
||||
# Пример:
|
||||
json_data = {
|
||||
"profile": "standard_profile",
|
||||
"pred_type": "single",
|
||||
"launch_datetime": "2025-03-16T08:47:00Z",
|
||||
"launch_latitude": "56.6992",
|
||||
"launch_longitude": "38.8247",
|
||||
"launch_altitude": "0",
|
||||
"ascent_rate": "5",
|
||||
"burst_altitude": "30000",
|
||||
"descent_rate": "5"
|
||||
}
|
||||
|
||||
query_string = build_query_string(json_data)
|
||||
print(query_string)
|
||||
106
stratoflights_api/consumers.py
Normal file
106
stratoflights_api/consumers.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import json
|
||||
import time
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
class TelemetryConsumer(AsyncWebsocketConsumer):
|
||||
group_prefix = None
|
||||
write_enabled = False
|
||||
|
||||
async def connect(self):
|
||||
User = get_user_model()
|
||||
from rest_framework.authtoken.models import Token
|
||||
from .models import Satellite
|
||||
|
||||
token_key = self.scope["query_string"].decode().split("token=")[-1]
|
||||
self.satellite_id = self.scope["url_route"]["kwargs"]["pk"]
|
||||
self.group_name = f"telemetry_{self.satellite_id}"
|
||||
|
||||
user = await self.get_user_from_token(token_key, Token)
|
||||
if not user:
|
||||
await self.send(text_data=json.dumps({"error": "Invalid token"}))
|
||||
await self.close()
|
||||
return
|
||||
self.scope["user"] = user
|
||||
|
||||
try:
|
||||
self.satellite = await database_sync_to_async(Satellite.objects.get)(id=self.satellite_id)
|
||||
except Satellite.DoesNotExist:
|
||||
await self.send(text_data=json.dumps({"error": "Invalid satellite"}))
|
||||
await self.close()
|
||||
return
|
||||
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
await self.accept()
|
||||
|
||||
async def receive(self, text_data):
|
||||
|
||||
from .serializers import TelemetryPacketSerializer
|
||||
|
||||
if not self.write_enabled:
|
||||
await self.send(text_data=json.dumps({"error": "Read-only mode"}))
|
||||
return
|
||||
|
||||
data = json.loads(text_data)
|
||||
serializer = TelemetryPacketSerializer(data=data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
await self.send(text_data=json.dumps({"error": serializer.errors}))
|
||||
return
|
||||
|
||||
saved_data = await self.save_telemetry(data)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.group_name,
|
||||
{
|
||||
"type": "telemetry_message",
|
||||
"data": {
|
||||
"id": str(saved_data.id),
|
||||
"timestamp": saved_data.timestamp,
|
||||
"lat": saved_data.lat,
|
||||
"lon": saved_data.lon,
|
||||
"alt": saved_data.alt,
|
||||
"payload": saved_data.payload,
|
||||
"raw_data": saved_data.raw_data,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def telemetry_message(self, event):
|
||||
await self.send(text_data=json.dumps(event["data"]))
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_from_token(self, token_key, Token):
|
||||
User = get_user_model()
|
||||
try:
|
||||
token = Token.objects.select_related("user").get(key=token_key)
|
||||
return token.user
|
||||
except Token.DoesNotExist:
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def save_telemetry(self, data):
|
||||
User = get_user_model()
|
||||
from .models import TelemetryPacket
|
||||
packet = TelemetryPacket.objects.create(
|
||||
satellite=self.satellite,
|
||||
user=self.scope["user"],
|
||||
timestamp=data.get("timestamp", int(time.time())),
|
||||
lat=data.get("lat", 0.0),
|
||||
lon=data.get("lon", 0.0),
|
||||
alt=data.get("alt", 0.0),
|
||||
payload=data.get("payload", {}),
|
||||
raw_data=data.get("raw_data", {}),
|
||||
)
|
||||
return packet
|
||||
|
||||
|
||||
class SatelliteTelemetryConsumer(TelemetryConsumer):
|
||||
group_prefix = "satellite"
|
||||
write_enabled = True
|
||||
|
||||
|
||||
class StationTelemetryConsumer(TelemetryConsumer):
|
||||
group_prefix = "station"
|
||||
write_enabled = False
|
||||
17
stratoflights_api/custom_pagination.py
Normal file
17
stratoflights_api/custom_pagination.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
class CustomLimitOffsetPagination(LimitOffsetPagination):
|
||||
limit_query_param = 'limit'
|
||||
offset_query_param = 'skip'
|
||||
max_limit = 100
|
||||
default_limit = 10
|
||||
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response({
|
||||
'total': self.count,
|
||||
'limit': self.limit,
|
||||
'skip': self.offset,
|
||||
'predictions': data
|
||||
})
|
||||
149
stratoflights_api/migrations/0001_initial.py
Normal file
149
stratoflights_api/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Generated by Django 4.2.23 on 2025-08-11 16:20
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Satellite',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='satellite_images/')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TelemetryPacket',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('timestamp', models.BigIntegerField()),
|
||||
('lat', models.FloatField(default=0.0)),
|
||||
('lon', models.FloatField(default=0.0)),
|
||||
('alt', models.FloatField(default=0.0)),
|
||||
('payload', models.JSONField(blank=True, default=dict)),
|
||||
('raw_data', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('satellite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='telemetry', to='stratoflights_api.satellite')),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SavedRateProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('type', models.CharField(default='ascent', max_length=50)),
|
||||
('rate_profile_data', models.JSONField(blank=True, default=dict)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SavedPoint',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('lat', models.FloatField(default=0.0)),
|
||||
('lon', models.FloatField(default=0.0)),
|
||||
('alt', models.FloatField(default=0.0)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PreditctionTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('prediction_mode', models.CharField(default='', max_length=50)),
|
||||
('model', models.CharField(default='', max_length=50)),
|
||||
('dataset', models.CharField(default='', max_length=50)),
|
||||
('flight_parameters', models.JSONField(blank=True, default=dict)),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Prediction',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('request', models.JSONField(default=dict)),
|
||||
('result', models.JSONField(default=dict)),
|
||||
('rate_profile', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.savedrateprofile')),
|
||||
('satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='predictions', to='stratoflights_api.satellite')),
|
||||
('start_point', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.savedpoint')),
|
||||
('template', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.preditctiontemplate')),
|
||||
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='predictions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='satellites',
|
||||
field=models.ManyToManyField(related_name='users', to='stratoflights_api.satellite'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
|
||||
),
|
||||
]
|
||||
0
stratoflights_api/migrations/__init__.py
Normal file
0
stratoflights_api/migrations/__init__.py
Normal file
106
stratoflights_api/models.py
Normal file
106
stratoflights_api/models.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
satellites = models.ManyToManyField("Satellite", related_name="users")
|
||||
|
||||
|
||||
class Prediction(models.Model):
|
||||
user = models.ForeignKey(get_user_model(
|
||||
), on_delete=models.CASCADE, default=0, related_name='predictions')
|
||||
satellite = models.ForeignKey(
|
||||
'Satellite', on_delete=models.CASCADE, related_name='predictions', null=True)
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
request = models.JSONField(default=dict)
|
||||
result = models.JSONField(default=dict)
|
||||
start_point = models.ForeignKey(
|
||||
'SavedPoint', on_delete=models.SET_NULL, related_name='predictions', null=True)
|
||||
template = models.ForeignKey(
|
||||
'PreditctionTemplate', on_delete=models.SET_NULL, related_name='predictions', null=True)
|
||||
rate_profile = models.ForeignKey(
|
||||
'SavedRateProfile', on_delete=models.SET_NULL, related_name='predictions', null=True)
|
||||
|
||||
|
||||
class Satellite(models.Model):
|
||||
user = models.ForeignKey(
|
||||
get_user_model(), on_delete=models.CASCADE, default=0)
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
metadata = models.JSONField(blank=True, default=dict)
|
||||
image = models.ImageField(
|
||||
upload_to='satellite_images/', blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class TelemetryPacket(models.Model):
|
||||
user = models.ForeignKey(
|
||||
get_user_model(), on_delete=models.CASCADE, default=0)
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
satellite = models.ForeignKey(
|
||||
Satellite, on_delete=models.CASCADE, related_name="telemetry")
|
||||
timestamp = models.BigIntegerField() # unix time
|
||||
lat = models.FloatField(default=0.0)
|
||||
lon = models.FloatField(default=0.0)
|
||||
alt = models.FloatField(default=0.0)
|
||||
payload = models.JSONField(blank=True, default=dict)
|
||||
raw_data = models.JSONField(blank=True, default=dict)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class PreditctionTemplate(models.Model):
|
||||
user = models.ForeignKey(
|
||||
get_user_model(), on_delete=models.CASCADE, default=0)
|
||||
name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
|
||||
description = models.TextField(blank=True, null=True)
|
||||
prediction_mode = models.CharField(
|
||||
max_length=50,
|
||||
default="") # TODO: add choices for prediction modes
|
||||
model = models.CharField(
|
||||
max_length=50,
|
||||
default="") # TODO: add choices for models
|
||||
dataset = models.CharField(
|
||||
max_length=50,
|
||||
default="")
|
||||
flight_parameters = models.JSONField(blank=True, default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'name')
|
||||
|
||||
|
||||
class SavedPoint(models.Model):
|
||||
user = models.ForeignKey(
|
||||
get_user_model(), on_delete=models.CASCADE, default=0)
|
||||
name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
lat = models.FloatField(default=0.0)
|
||||
lon = models.FloatField(default=0.0)
|
||||
alt = models.FloatField(default=0.0)
|
||||
is_default = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'name')
|
||||
|
||||
|
||||
class SavedRateProfile(models.Model):
|
||||
user = models.ForeignKey(
|
||||
get_user_model(), on_delete=models.CASCADE, default=0)
|
||||
name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
type = models.CharField(
|
||||
max_length=50, default="ascent") # TODO: add choices for rate profile types
|
||||
rate_profile_data = models.JSONField(blank=True, default=dict)
|
||||
is_default = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'name')
|
||||
16
stratoflights_api/permissions.py
Normal file
16
stratoflights_api/permissions.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
class ReadOnlyOrAuthenticated(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
request.method in SAFE_METHODS or
|
||||
request.user and request.user.is_authenticated
|
||||
)
|
||||
|
||||
|
||||
class IsOwner(BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return obj.user == request.user
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user and request.user.is_authenticated
|
||||
7
stratoflights_api/routing.py
Normal file
7
stratoflights_api/routing.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r"^api/ws/satellite/(?P<pk>[0-9a-f-]+)/telemetry/$", consumers.SatelliteTelemetryConsumer.as_asgi()),
|
||||
re_path(r"^api/ws/station/(?P<pk>[0-9a-f-]+)/telemetry/$", consumers.StationTelemetryConsumer.as_asgi()),
|
||||
]
|
||||
185
stratoflights_api/serializers.py
Normal file
185
stratoflights_api/serializers.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Prediction, SavedPoint, SavedRateProfile, PreditctionTemplate
|
||||
from datetime import datetime
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.contrib.auth import get_user_model
|
||||
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']
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
PROFILE_STANDARD = "standard_profile"
|
||||
PROFILE_FLOAT = "float_profile"
|
||||
PROFILE_REVERSE = "reverse_profile"
|
||||
PROFILE_CUSTOM = "custom_profile"
|
||||
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)
|
||||
start_point = serializers.PrimaryKeyRelatedField(
|
||||
queryset=SavedPoint.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
rate_profile = serializers.PrimaryKeyRelatedField(
|
||||
queryset=SavedRateProfile.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=PreditctionTemplate.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def create(self, validated_data):
|
||||
if 'ascent_curve' in validated_data:
|
||||
validated_data['ascent_curve'] = base64_to_curve(validated_data['ascent_curve'])
|
||||
if 'descent_curve' in validated_data:
|
||||
validated_data['descent_curve'] = base64_to_curve(validated_data['descent_curve'])
|
||||
|
||||
prediction = Prediction(
|
||||
user=validated_data.get('user'),
|
||||
request=validated_data.get('request', {}),
|
||||
result=validated_data.get('result', {}),
|
||||
start_point=validated_data.get('start_point'),
|
||||
template=validated_data.get('template'),
|
||||
rate_profile=validated_data.get('rate_profile')
|
||||
)
|
||||
prediction.save()
|
||||
|
||||
return prediction
|
||||
|
||||
|
||||
class PredictionListSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Prediction
|
||||
fields = ["id", "created_at", "updated_at", "start_point", "template", "rate_profile"]
|
||||
|
||||
|
||||
class PredictionDetailSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Prediction
|
||||
fields = ["id", "created_at", "updated_at", "result", "start_point", "template", "rate_profile"]
|
||||
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import TelemetryPacket
|
||||
|
||||
class TelemetryPacketSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TelemetryPacket
|
||||
fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload']
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class SavedPointSerializer(serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
class Meta:
|
||||
model = SavedPoint
|
||||
fields = ['user', 'id', 'name', 'lat', 'lon', 'alt']
|
||||
read_only_fields = ['id']
|
||||
|
||||
validators = [
|
||||
serializers.UniqueTogetherValidator(
|
||||
queryset=SavedPoint.objects.all(),
|
||||
fields=['user', 'name'],
|
||||
message="A saved point with this name already exists for the user."
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class SavedRateProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SavedRateProfile
|
||||
fields = ['id', 'name', 'type', 'rate_profile_data']
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class PreditctionTemplateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PreditctionTemplate
|
||||
fields = ['id', 'name', 'is_default', 'description', 'prediction_mode', 'model', 'dataset', 'flight_parameters']
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'first_name', 'last_name']
|
||||
extra_kwargs = {
|
||||
'username': {'read_only': True}
|
||||
}
|
||||
|
||||
def validate_email(self, value):
|
||||
try:
|
||||
validate_email(value)
|
||||
except DjangoValidationError:
|
||||
raise serializers.ValidationError("Invalid email format")
|
||||
return value
|
||||
|
||||
class ChangePasswordSerializer(serializers.Serializer):
|
||||
old_password = serializers.CharField(required=True)
|
||||
new_password = serializers.CharField(required=True)
|
||||
|
||||
def validate_new_password(self, value):
|
||||
validate_password(value)
|
||||
return value
|
||||
|
||||
class DeleteAccountSerializer(serializers.Serializer):
|
||||
password = serializers.CharField(required=True)
|
||||
52
stratoflights_api/services/tawhiri.py
Normal file
52
stratoflights_api/services/tawhiri.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
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()
|
||||
3
stratoflights_api/tests.py
Normal file
3
stratoflights_api/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
48
stratoflights_api/urls.py
Normal file
48
stratoflights_api/urls.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
from .views import (
|
||||
PredictionViewSet,
|
||||
SavedPointViewset,
|
||||
PreditctionTemplateViewset,
|
||||
TelemetryListCreateView,
|
||||
get_csrf,
|
||||
login_view,
|
||||
logout_view,
|
||||
SessionView,
|
||||
WhoAmIView,
|
||||
UserProfileView,
|
||||
ChangePasswordView,
|
||||
TokenManagementView,
|
||||
DeleteUserDataView,
|
||||
DeleteAccountView
|
||||
)
|
||||
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'predictions', PredictionViewSet, basename='predictions')
|
||||
router.register(r'saved-points', SavedPointViewset, basename='saved-points')
|
||||
router.register(r'saved-templates', PreditctionTemplateViewset, basename='saved-templates')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("csrf/", get_csrf, name='api-csrf'),
|
||||
path('token', obtain_auth_token, name = 'get_token'),
|
||||
path("login/", login_view, name='api-login'),
|
||||
path("logout/", logout_view, name='api-logout'),
|
||||
path("session/", SessionView.as_view(), name='api-session'),
|
||||
path("whoami/", WhoAmIView.as_view(), name='api-whoami'),
|
||||
path("<uuid:pk>/telemetry/", TelemetryListCreateView.as_view(), name="create_telemetry"),
|
||||
path('csrf/', get_csrf, name='api-csrf'),
|
||||
path('login/', login_view, name='api-login'),
|
||||
path('logout/', logout_view, name='api-logout'),
|
||||
path('session/', SessionView.as_view(), name='api-session'),
|
||||
path('whoami/', WhoAmIView.as_view(), name='api-whoami'),
|
||||
path("profile/", UserProfileView.as_view(), name='api-profile'),
|
||||
path("profile/change-password/", ChangePasswordView.as_view(), name='api-change-password'),
|
||||
path("profile/token/", TokenManagementView.as_view(), name='api-token'),
|
||||
path("profile/delete-data/", DeleteUserDataView.as_view(), name='api-delete-data'),
|
||||
path("profile/delete-account/", DeleteAccountView.as_view(), name='api-delete-account'),
|
||||
]
|
||||
urlpatterns += router.urls
|
||||
31
stratoflights_api/validators.py
Normal file
31
stratoflights_api/validators.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
354
stratoflights_api/views.py
Normal file
354
stratoflights_api/views.py
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
import requests
|
||||
import time
|
||||
import json
|
||||
from rest_framework import status, generics, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet, ViewSet, GenericViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication
|
||||
from rest_framework.decorators import api_view, permission_classes, authentication_classes, action
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth import authenticate, login, logout, get_user_model
|
||||
from django.middleware.csrf import get_token
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from .models import Prediction, User, Satellite, SavedPoint, SavedRateProfile, PreditctionTemplate, TelemetryPacket
|
||||
from .serializers import PredictionSerializer, TelemetryPacketSerializer, PredictionRequestSerializer, PredictionListSerializer, PredictionDetailSerializer, SavedPointSerializer, SavedRateProfileSerializer, PreditctionTemplateSerializer, UserSerializer, ChangePasswordSerializer, DeleteAccountSerializer
|
||||
from .services.tawhiri import TawhiriClient
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from .permissions import ReadOnlyOrAuthenticated, IsOwner
|
||||
from .custom_pagination import CustomLimitOffsetPagination
|
||||
from datetime import datetime
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_prediction_from_tawhiri(params):
|
||||
|
||||
base_url = "https://fly.stratonautica.ru/api/v2"
|
||||
response = requests.get(base_url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json() # получаем результат предсказания
|
||||
else:
|
||||
raise Exception(
|
||||
f"Tawhiri error: {response.status_code} {response.text}")
|
||||
|
||||
|
||||
class PredictionViewSet(GenericViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = CustomLimitOffsetPagination
|
||||
|
||||
def list(self, request):
|
||||
queryset = Prediction.objects.filter(user=request.user)
|
||||
return Response(PredictionSerializer(queryset, many=True).data)
|
||||
|
||||
def create(self, request):
|
||||
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
|
||||
|
||||
try:
|
||||
prediction_result = TawhiriClient.get_prediction(validated_data)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY)
|
||||
|
||||
# prediction = Prediction.objects.create(
|
||||
# result=prediction_result, user=request.user, request=request.data, validated_data=validated_data)
|
||||
prediction = serializer.save(
|
||||
user=request.user,
|
||||
result=prediction_result,
|
||||
request=request.data
|
||||
)
|
||||
|
||||
return Response({
|
||||
"id": prediction.id,
|
||||
"created_at": prediction.created_at,
|
||||
"result": prediction_result
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def list_user(self, request):
|
||||
user = request.user
|
||||
satellite_id = request.query_params.get('satellite_id')
|
||||
created_from = request.query_params.get('created_from')
|
||||
created_till = request.query_params.get('created_till')
|
||||
|
||||
filters = {
|
||||
'user': user,
|
||||
}
|
||||
|
||||
if created_from:
|
||||
filters['created_at__gte'] = parse_datetime(created_from)
|
||||
|
||||
if created_till:
|
||||
filters['created_at__lte'] = parse_datetime(created_till)
|
||||
|
||||
if satellite_id:
|
||||
if not user.satellites.filter(id=satellite_id).exists():
|
||||
return Response({'detail': 'Access denied'}, status=403)
|
||||
|
||||
filters['satellite_id'] = satellite_id
|
||||
|
||||
queryset = Prediction.objects.filter(**filters)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if page is not None:
|
||||
serializer = PredictionSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = PredictionSerializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def history(self, request):
|
||||
queryset = Prediction.objects.filter(user=request.user)
|
||||
return Response(PredictionListSerializer(queryset, many=True).data)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def detail(self, request, pk=None):
|
||||
prediction = Prediction.objects.filter(
|
||||
user=request.user, pk=pk).first()
|
||||
if not prediction:
|
||||
return Response({'detail': 'Not found'}, status=404)
|
||||
return Response(PredictionDetailSerializer(prediction).data)
|
||||
|
||||
@action(detail=True, methods=["delete"])
|
||||
def delete(self, request, pk=None):
|
||||
prediction = Prediction.objects.filter(
|
||||
user=request.user, pk=pk).first()
|
||||
if not prediction:
|
||||
return Response({'detail': 'Not found'}, status=404)
|
||||
prediction.delete()
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class TelemetryListCreateView(generics.ListCreateAPIView):
|
||||
serializer_class = TelemetryPacketSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = TelemetryPacket.objects.filter(satellite_id=self.kwargs["pk"])
|
||||
|
||||
from_ts = self.request.query_params.get("from")
|
||||
till_ts = self.request.query_params.get("till")
|
||||
|
||||
if from_ts:
|
||||
qs = qs.filter(timestamp__gte=int(from_ts))
|
||||
if till_ts:
|
||||
qs = qs.filter(timestamp__lte=int(till_ts))
|
||||
|
||||
return qs.order_by("-timestamp")
|
||||
|
||||
def post(self, request, pk):
|
||||
serializer = TelemetryPacketSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
TelemetryPacket.objects.create(timestamp=time.time(),
|
||||
satellite=Satellite.objects.get(id=pk),
|
||||
lat=validated_data["lat"],
|
||||
lon=validated_data["lon"],
|
||||
alt=validated_data["alt"],
|
||||
payload=validated_data['payload'],
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class SessionView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@staticmethod
|
||||
def get(request, format=None):
|
||||
return JsonResponse({'isAuthenticated': True})
|
||||
|
||||
|
||||
class WhoAmIView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@staticmethod
|
||||
def get(request, format=None):
|
||||
return JsonResponse({'username': request.user.username})
|
||||
|
||||
|
||||
@extend_schema(methods=["GET"], description="Get CSRF token")
|
||||
@csrf_exempt
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def get_csrf(request):
|
||||
response = JsonResponse({'detail': 'CSRF cookie set'})
|
||||
response['X-CSRFToken'] = get_token(request)
|
||||
return response
|
||||
|
||||
|
||||
@extend_schema(methods=["POST"], description="Login user")
|
||||
@csrf_exempt
|
||||
@api_view(["POST"])
|
||||
@authentication_classes([BasicAuthentication])
|
||||
@permission_classes([AllowAny])
|
||||
def login_view(request):
|
||||
data = json.loads(request.body)
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if username is None or password is None:
|
||||
return JsonResponse({'detail': 'Please provide username and password.'}, status=400)
|
||||
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is None:
|
||||
return JsonResponse({'detail': 'Invalid credentials.'}, status=400)
|
||||
|
||||
login(request, user)
|
||||
return JsonResponse({'detail': 'Successfully logged in.'})
|
||||
|
||||
|
||||
@extend_schema(methods=["POST"], description="Logout user")
|
||||
@api_view(["POST"])
|
||||
@permission_classes([AllowAny])
|
||||
def logout_view(request):
|
||||
if not request.user.is_authenticated:
|
||||
return JsonResponse({'detail': 'You\'re not logged in.'}, status=400)
|
||||
|
||||
logout(request)
|
||||
return JsonResponse({'detail': 'Successfully logged out.'})
|
||||
|
||||
|
||||
class SavedPointViewset(ModelViewSet):
|
||||
permission_classes = [IsOwner]
|
||||
serializer_class = SavedPointSerializer
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
return SavedPoint.objects.filter(user=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class PreditctionTemplateViewset(ModelViewSet):
|
||||
permission_classes = [IsOwner]
|
||||
serializer_class = PreditctionTemplateSerializer
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
return PreditctionTemplate.objects.filter(user=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class UserProfileView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
serializer = UserSerializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
serializer = UserSerializer(user, data=request.data, partial=True)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ChangePasswordView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
user = request.user
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not user.check_password(serializer.validated_data['old_password']):
|
||||
return Response({'detail': 'Old password is incorrect'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user.set_password(serializer.validated_data['new_password'])
|
||||
user.save()
|
||||
return Response({'detail': 'Password changed successfully'})
|
||||
|
||||
|
||||
class DeleteAccountView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def delete(self, request):
|
||||
user = request.user
|
||||
serializer = DeleteAccountSerializer(data=request.data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not user.check_password(serializer.validated_data['password']):
|
||||
return Response({'detail': 'Incorrect password'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
Prediction.objects.filter(user=user).delete()
|
||||
SavedPoint.objects.filter(user=user).delete()
|
||||
PreditctionTemplate.objects.filter(user=user).delete()
|
||||
|
||||
user.delete()
|
||||
|
||||
return Response({'detail': 'Account deleted successfully'})
|
||||
|
||||
|
||||
class DeleteUserDataView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def delete(self, request):
|
||||
user = request.user
|
||||
|
||||
Prediction.objects.filter(user=user).delete()
|
||||
SavedPoint.objects.filter(user=user).delete()
|
||||
PreditctionTemplate.objects.filter(user=user).delete()
|
||||
|
||||
return Response({'detail': 'All user data deleted successfully'})
|
||||
|
||||
|
||||
class TokenManagementView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
|
||||
token, created = Token.objects.get_or_create(user=request.user)
|
||||
return Response({"token": token.key})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
Token.objects.filter(user=request.user).delete()
|
||||
token = Token.objects.create(user=request.user)
|
||||
return Response({"token": token.key})
|
||||
|
||||
|
||||
|
||||
# class PredictionCreateView(APIView):
|
||||
# permission_classes = [IsAuthenticated]
|
||||
|
||||
# class TelemetryPacket(models.Model):
|
||||
# id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
# satellite = models.ForeignKey(Satellite, on_delete=models.CASCADE, related_name="telemetry")
|
||||
# timestamp = models.BigIntegerField() # unix time
|
||||
# lat = models.FloatField()
|
||||
# lon = models.FloatField()
|
||||
# alt = models.FloatField()
|
||||
# payload = models.JSONField(null=True, blank=True)
|
||||
# created_at = models.DateTimeField(auto_now_add=True)
|
||||
# fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload']
|
||||
62
stratoflights_api/ws_client.py
Normal file
62
stratoflights_api/ws_client.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import asyncio
|
||||
import json
|
||||
import websockets
|
||||
import uuid
|
||||
import time
|
||||
|
||||
BASE_URL = "ws://localhost:8000/api/ws"
|
||||
TOKEN = "ae397229f9ca50cd6320cb0416671ecc780671ac"
|
||||
PK = "cf1e36ff-c5ce-4852-8b90-046737974b97"
|
||||
|
||||
async def satellite_mode():
|
||||
"""
|
||||
Клиент для отправки данных телеметрии.
|
||||
"""
|
||||
url = f"{BASE_URL}/satellite/{PK}/telemetry/?token={TOKEN}"
|
||||
async with websockets.connect(url) as ws:
|
||||
print(f"[satellite] Connected to {url}")
|
||||
|
||||
while True:
|
||||
telemetry_data = {
|
||||
"timestamp": int(time.time()),
|
||||
"lat": 55.75,
|
||||
"lon": 37.61,
|
||||
"alt": 200.0,
|
||||
"payload": {"temp": 22.5, "status": "OK"},
|
||||
"raw_data": {"sensor": "gps", "accuracy": "high"}
|
||||
}
|
||||
await ws.send(json.dumps(telemetry_data))
|
||||
print(f"[satellite] Sent: {telemetry_data}")
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(ws.recv(), timeout=2)
|
||||
print(f"[satellite] Received: {response}")
|
||||
except asyncio.TimeoutError:
|
||||
print("[satellite] No response yet")
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def station_mode():
|
||||
"""
|
||||
Клиент для приёма данных телеметрии.
|
||||
"""
|
||||
url = f"{BASE_URL}/station/{PK}/telemetry/?token={TOKEN}"
|
||||
async with websockets.connect(url) as ws:
|
||||
print(f"[station] Connected to {url}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = await ws.recv()
|
||||
print(f"[station] Received: {message}")
|
||||
except websockets.ConnectionClosed:
|
||||
print("[station] Connection closed")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mode = input("Enter mode (satellite/station): ").strip().lower()
|
||||
if mode == "satellite":
|
||||
asyncio.run(satellite_mode())
|
||||
else:
|
||||
asyncio.run(station_mode())
|
||||
Loading…
Add table
Add a link
Reference in a new issue