added api/pk/satellite, authorisation endpointsm, pagination

This commit is contained in:
afanasyev.aa 2025-04-05 23:49:07 +09:00
parent cc5187c3a1
commit 8225b18a2a
16 changed files with 284 additions and 26 deletions

58
.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.egg-info/
*.egg
*.log
# Environments
.env
.env.*
venv/
venv*/
ENV/
.envrc
# Django
*.sqlite3
db.sqlite3
media/
staticfiles/
*.pot
*.pyc
# Migrations (если хочешь игнорировать сгенерированные миграции)
# migrations/
# */migrations/
# IDEs
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.db
# Tests
.coverage
htmlcov/
*.cover
*.py,cover
.cache/
.tox/
# Git
*.orig
# Docker
*.pid
*.pid.lock
docker-compose.override.yml
# Build
dist/
build/

Binary file not shown.

View file

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import User, Prediction, UserPrediction from .models import User, Prediction, UserPrediction, Satellite, TelemetryPacket
admin.site.register(User) admin.site.register(User)
admin.site.register(Prediction) admin.site.register(Prediction)
admin.site.register(UserPrediction) admin.site.register(UserPrediction)
admin.site.register(Satellite)
admin.site.register(TelemetryPacket)

View file

@ -0,0 +1,35 @@
# Generated by Django 5.1.7 on 2025-04-05 13:30
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_alter_userprediction_unique_together_and_more'),
]
operations = [
migrations.CreateModel(
name='Satellite',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='TelemetryPacket',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('timestamp', models.BigIntegerField()),
('lat', models.FloatField()),
('lon', models.FloatField()),
('alt', models.FloatField()),
('payload', models.JSONField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('satellite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='telemetry', to='api.satellite')),
],
),
]

View file

@ -18,3 +18,16 @@ class UserPrediction(models.Model):
prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE) prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE)
created_at = models.DateTimeField() created_at = models.DateTimeField()
class Satellite(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
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)

8
api/permissions.py Normal file
View file

@ -0,0 +1,8 @@
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
)

View file

@ -84,3 +84,13 @@ class PredictionDetailSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Prediction model = Prediction
fields = ["id", "created_at", "updated_at", "result"] fields = ["id", "created_at", "updated_at", "result"]
from rest_framework import serializers
from .models import TelemetryPacket
class TelemetryPacketSerializer(serializers.ModelSerializer):
class Meta:
model = TelemetryPacket
fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload']

View file

@ -1,16 +1,26 @@
from django.urls import path from django.urls import path
from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView, from .views import (PredictionCreateView, PredictionListView,
PredictionHistoryListView, PredictionHistoryListView,
PredictionHistoryDetailView, PredictionHistoryDetailView,
PredictionHistoryDeleteView,) PredictionHistoryDeleteView,
SessionView,
WhoAmIView,
get_csrf,
login_view,
logout_view)
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
from .views import TelemetryListCreateView
urlpatterns = [ urlpatterns = [
path('predictions', PredictionCreateView.as_view(), name='create_prediction'), path('predictions', PredictionCreateView.as_view(), name='create_prediction'),
path('predictions', PredictionListView.as_view(), name='get_predictions'), path('predictions', PredictionListView.as_view(), name='get_predictions'),
path('predictions/<uuid:pk>', PredictionDeleteView.as_view(), name='delete_prediction'), path('token', obtain_auth_token, name = 'get_token'),
path('token/', obtain_auth_token), path("history", PredictionHistoryListView.as_view(), name='view_history_list'),
path("history/", PredictionHistoryListView.as_view()), path("history/<uuid:pk>/", PredictionHistoryDetailView.as_view(), name='view_history_detail'),
path("history/<uuid:pk>/", PredictionHistoryDetailView.as_view()), path("history/<uuid:pk>/delete/", PredictionHistoryDeleteView.as_view(), name='delete_history'),
path("history/<uuid:pk>/delete/", PredictionHistoryDeleteView.as_view()), 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'), # new
path('whoami/', WhoAmIView.as_view(), name='api-whoami'), # new
] ]

View file

@ -11,6 +11,15 @@ from django.utils.decorators import method_decorator
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from .services.tawhiri import TawhiriClient from .services.tawhiri import TawhiriClient
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .models import Satellite, TelemetryPacket
from .serializers import TelemetryPacketSerializer
from .permissions import ReadOnlyOrAuthenticated
import time
from django.http import JsonResponse
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from django.contrib.auth import authenticate, login, logout
import json
from django.middleware.csrf import get_token
User = get_user_model() User = get_user_model()
@ -24,6 +33,8 @@ def get_prediction_from_tawhiri(params):
else: else:
raise Exception(f"Tawhiri error: {response.status_code} {response.text}") raise Exception(f"Tawhiri error: {response.status_code} {response.text}")
class PredictionCreateView(APIView): class PredictionCreateView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -39,19 +50,20 @@ class PredictionCreateView(APIView):
try: try:
prediction_result = TawhiriClient.get_prediction(validated_data) prediction_result = TawhiriClient.get_prediction(validated_data)
print(prediction_result)
except requests.RequestException as e: except requests.RequestException as e:
print("Tawhiri error:", str(e), e.response.text if e.response else "no response") 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) return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY)
prediction = Prediction.objects.create(result=prediction_result) prediction = Prediction.objects.create(result=prediction_result)
UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now()) UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now())
return Response({ return Response({
"id": prediction.id, "id": prediction.id,
"created_at": prediction.created_at, "created_at": prediction.created_at,
"result": prediction_result "result": prediction_result
}, status=status.HTTP_201_CREATED) }, status=status.HTTP_201_CREATED)
class PredictionListView(APIView): class PredictionListView(APIView):
def get(self, request): def get(self, request):
user_id = request.query_params.get('user_id') user_id = request.query_params.get('user_id')
@ -66,17 +78,6 @@ class PredictionListView(APIView):
) )
return Response(PredictionSerializer(predictions, many=True).data) return Response(PredictionSerializer(predictions, many=True).data)
class PredictionDeleteView(APIView):
def delete(self, request, pk):
try:
prediction = Prediction.objects.get(pk=pk)
prediction.deleted_at = timezone.now()
prediction.save()
return Response({"deleted": True})
except Prediction.DoesNotExist:
return Response({"error": "Not found"}, status=404)
class PredictionHistoryListView(generics.ListAPIView): class PredictionHistoryListView(generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@ -89,6 +90,7 @@ class PredictionHistoryListView(generics.ListAPIView):
) )
class PredictionHistoryDetailView(generics.RetrieveAPIView): class PredictionHistoryDetailView(generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
serializer_class = PredictionDetailSerializer serializer_class = PredictionDetailSerializer
@ -100,6 +102,7 @@ class PredictionHistoryDetailView(generics.RetrieveAPIView):
) )
class PredictionHistoryDeleteView(generics.DestroyAPIView): class PredictionHistoryDeleteView(generics.DestroyAPIView):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@ -114,5 +117,103 @@ class PredictionHistoryDeleteView(generics.DestroyAPIView):
instance.save() instance.save()
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):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'isAuthenticated': True})
class WhoAmIView(APIView):
authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'username': request.user.username})
def get_csrf(request):
response = JsonResponse({'detail': 'CSRF cookie set'})
response['X-CSRFToken'] = get_token(request)
return response
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.'})
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 PredictionCreateView(APIView): #class PredictionCreateView(APIView):
#permission_classes = [IsAuthenticated] #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']

Binary file not shown.

View file

@ -41,7 +41,7 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'corsheaders', 'corsheaders',
'api' 'api.apps.ApiConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -55,6 +55,13 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
ROOT_URLCONF = 'testapi.urls' ROOT_URLCONF = 'testapi.urls'
@ -140,6 +147,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = { REST_FRAMEWORK = {
# ВАШИ НАСТРОЙКИ # ВАШИ НАСТРОЙКИ
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100,
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
@ -147,5 +156,17 @@ REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
#'rest_framework.permissions.AllowAny', #'rest_framework.permissions.AllowAny',
] ],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
} }
CSRF_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = ['http://localhost:5173']