added api/pk/satellite, authorisation endpointsm, pagination
This commit is contained in:
parent
cc5187c3a1
commit
8225b18a2a
16 changed files with 284 additions and 26 deletions
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,7 +1,9 @@
|
|||
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(Prediction)
|
||||
admin.site.register(UserPrediction)
|
||||
admin.site.register(UserPrediction)
|
||||
admin.site.register(Satellite)
|
||||
admin.site.register(TelemetryPacket)
|
||||
35
api/migrations/0004_satellite_telemetrypacket.py
Normal file
35
api/migrations/0004_satellite_telemetrypacket.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -18,3 +18,16 @@ class UserPrediction(models.Model):
|
|||
prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE)
|
||||
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
8
api/permissions.py
Normal 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
|
||||
)
|
||||
|
|
@ -84,3 +84,13 @@ class PredictionDetailSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Prediction
|
||||
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']
|
||||
|
||||
|
|
|
|||
26
api/urls.py
26
api/urls.py
|
|
@ -1,16 +1,26 @@
|
|||
from django.urls import path
|
||||
from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView,
|
||||
from .views import (PredictionCreateView, PredictionListView,
|
||||
PredictionHistoryListView,
|
||||
PredictionHistoryDetailView,
|
||||
PredictionHistoryDeleteView,)
|
||||
PredictionHistoryDeleteView,
|
||||
SessionView,
|
||||
WhoAmIView,
|
||||
get_csrf,
|
||||
login_view,
|
||||
logout_view)
|
||||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
|
||||
from .views import TelemetryListCreateView
|
||||
urlpatterns = [
|
||||
path('predictions', PredictionCreateView.as_view(), name='create_prediction'),
|
||||
path('predictions', PredictionListView.as_view(), name='get_predictions'),
|
||||
path('predictions/<uuid:pk>', PredictionDeleteView.as_view(), name='delete_prediction'),
|
||||
path('token/', obtain_auth_token),
|
||||
path("history/", PredictionHistoryListView.as_view()),
|
||||
path("history/<uuid:pk>/", PredictionHistoryDetailView.as_view()),
|
||||
path("history/<uuid:pk>/delete/", PredictionHistoryDeleteView.as_view()),
|
||||
path('token', obtain_auth_token, name = 'get_token'),
|
||||
path("history", PredictionHistoryListView.as_view(), name='view_history_list'),
|
||||
path("history/<uuid:pk>/", PredictionHistoryDetailView.as_view(), name='view_history_detail'),
|
||||
path("history/<uuid:pk>/delete/", PredictionHistoryDeleteView.as_view(), name='delete_history'),
|
||||
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
|
||||
]
|
||||
|
|
|
|||
129
api/views.py
129
api/views.py
|
|
@ -11,6 +11,15 @@ 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
|
||||
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()
|
||||
|
||||
|
|
@ -24,6 +33,8 @@ def get_prediction_from_tawhiri(params):
|
|||
else:
|
||||
raise Exception(f"Tawhiri error: {response.status_code} {response.text}")
|
||||
|
||||
|
||||
|
||||
class PredictionCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
|
@ -39,19 +50,20 @@ class PredictionCreateView(APIView):
|
|||
|
||||
try:
|
||||
prediction_result = TawhiriClient.get_prediction(validated_data)
|
||||
print(prediction_result)
|
||||
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)
|
||||
|
||||
prediction = Prediction.objects.create(result=prediction_result)
|
||||
UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now())
|
||||
|
||||
return Response({
|
||||
"id": prediction.id,
|
||||
"created_at": prediction.created_at,
|
||||
"result": prediction_result
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class PredictionListView(APIView):
|
||||
def get(self, request):
|
||||
user_id = request.query_params.get('user_id')
|
||||
|
|
@ -66,17 +78,6 @@ class PredictionListView(APIView):
|
|||
)
|
||||
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):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
|
@ -89,6 +90,7 @@ class PredictionHistoryListView(generics.ListAPIView):
|
|||
)
|
||||
|
||||
|
||||
|
||||
class PredictionHistoryDetailView(generics.RetrieveAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = PredictionDetailSerializer
|
||||
|
|
@ -100,6 +102,7 @@ class PredictionHistoryDetailView(generics.RetrieveAPIView):
|
|||
)
|
||||
|
||||
|
||||
|
||||
class PredictionHistoryDeleteView(generics.DestroyAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
|
@ -114,5 +117,103 @@ class PredictionHistoryDeleteView(generics.DestroyAPIView):
|
|||
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):
|
||||
#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']
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
|
@ -41,7 +41,7 @@ INSTALLED_APPS = [
|
|||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'corsheaders',
|
||||
'api'
|
||||
'api.apps.ApiConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -55,6 +55,13 @@ MIDDLEWARE = [
|
|||
'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
|
||||
|
||||
ROOT_URLCONF = 'testapi.urls'
|
||||
|
|
@ -140,6 +147,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
# ВАШИ НАСТРОЙКИ
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 100,
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
|
|
@ -147,5 +156,17 @@ REST_FRAMEWORK = {
|
|||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
#'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']
|
||||
Loading…
Add table
Add a link
Reference in a new issue