replace DRF with django-ninja
This commit is contained in:
parent
4299d46b7b
commit
813780a18b
|
@ -0,0 +1,112 @@
|
|||
from ninja import Router, Schema
|
||||
from ninja.errors import HttpError
|
||||
from concierge import models
|
||||
from typing import List
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import now
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import json
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ClaimReference(Schema):
|
||||
uuid: str
|
||||
|
||||
|
||||
class AvailableTask(Schema):
|
||||
uuid: str
|
||||
type: str
|
||||
|
||||
|
||||
class HeartbeatResponse(Schema):
|
||||
success: bool = True
|
||||
claims: List[ClaimReference]
|
||||
available: List[AvailableTask]
|
||||
|
||||
|
||||
class ClaimResponse(Schema):
|
||||
uuid: str
|
||||
type: str
|
||||
configuration: dict
|
||||
|
||||
|
||||
class ReleaseResponse(Schema):
|
||||
success: bool = True
|
||||
uuid: str
|
||||
type: str
|
||||
|
||||
|
||||
@router.post('/heartbeat/{identity}', response=HeartbeatResponse)
|
||||
@csrf_exempt
|
||||
def receive_heartbeat(request, identity: str):
|
||||
try:
|
||||
id = models.Identity.objects.get(identity=identity)
|
||||
except models.Identity.DoesNotExist:
|
||||
raise HttpError(403, "identity unknown")
|
||||
|
||||
# update heartbeat
|
||||
id.heartbeat = now()
|
||||
id.save()
|
||||
|
||||
# get current claims and available tasks
|
||||
claims = models.Task.objects.filter(claimed_by=id).all()
|
||||
available = models.Task.objects.filter(claimed_by=None).all()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'claims': [{'uuid': str(o.uuid)} for o in list(claims)],
|
||||
'available': [{'uuid': str(o.uuid), 'type': o.type} for o in list(available)],
|
||||
}
|
||||
|
||||
|
||||
@router.post('/claim/{identity}/{task_uuid}', response=ClaimResponse)
|
||||
@csrf_exempt
|
||||
def claim_task(request, identity: str, task_uuid: str):
|
||||
try:
|
||||
id = models.Identity.objects.get(identity=identity)
|
||||
except models.Identity.DoesNotExist:
|
||||
raise HttpError(403, "identity unknown")
|
||||
|
||||
with transaction.atomic():
|
||||
task = get_object_or_404(models.Task, uuid=task_uuid)
|
||||
|
||||
if task.claimed_by:
|
||||
raise HttpError(423, "task already claimed")
|
||||
|
||||
task.claimed_by = id
|
||||
task.save()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'uuid': task.uuid,
|
||||
'type': task.type,
|
||||
'configuration': json.loads(task.configuration)
|
||||
}
|
||||
|
||||
|
||||
@router.post('/release/{identity}/{task_uuid}')
|
||||
@csrf_exempt
|
||||
def release_task(request, identity: str, task_uuid: str):
|
||||
try:
|
||||
id = models.Identity.objects.get(identity=identity)
|
||||
except models.Identity.DoesNotExist:
|
||||
raise HttpError(403, "identity unknown")
|
||||
|
||||
with transaction.atomic():
|
||||
task = get_object_or_404(models.Task, uuid=task_uuid)
|
||||
|
||||
if task.claimed_by != id:
|
||||
raise HttpError(403, "task not claimed by this identity")
|
||||
|
||||
task.claimed_by = None
|
||||
task.save()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'uuid': task.uuid,
|
||||
'type': task.type,
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('api/<uuid:identity>/heartbeat', views.heartbeat, name='heartbeat'),
|
||||
path('api/<uuid:identity>/claim/<uuid:task_uuid>', views.claim, name='claim'),
|
||||
path('api/<uuid:identity>/release/<uuid:task_uuid>', views.release, name='release'),
|
||||
]
|
|
@ -1,94 +0,0 @@
|
|||
import json
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.timezone import now
|
||||
|
||||
from .models import Identity, Task
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def heartbeat(request, identity):
|
||||
try:
|
||||
id = Identity.objects.get(identity=identity)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({'error': 'identity unknown'}, status=403)
|
||||
|
||||
# update heartbeat
|
||||
id.heartbeat = now()
|
||||
id.save()
|
||||
|
||||
# get current claims and available tasks
|
||||
claims = Task.objects.filter(claimed_by=id).all()
|
||||
available = Task.objects.filter(claimed_by=None).all()
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'claims': [{'uuid': str(o.uuid)} for o in list(claims)],
|
||||
'available': [{'uuid': str(o.uuid), 'type': o.type} for o in list(available)],
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def claim(request, identity, task_uuid):
|
||||
try:
|
||||
id = Identity.objects.get(identity=identity)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({'error': 'identity unknown'}, status=403)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
task = Task.objects.get(uuid=task_uuid)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({'error': 'task unknown'}, status=404)
|
||||
|
||||
if task.claimed_by:
|
||||
return JsonResponse({'error': 'task already claimed'}, status=423)
|
||||
|
||||
task.claimed_by = id
|
||||
task.save()
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'uuid': task.uuid,
|
||||
'type': task.type,
|
||||
'configuration': json.loads(task.configuration)
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def release(request, identity, task_uuid):
|
||||
try:
|
||||
id = Identity.objects.get(identity=identity)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({'error': 'identity unknown'}, status=403)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
task = Task.objects.get(uuid=task_uuid)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({'error': 'task unknown'}, status=404)
|
||||
|
||||
if task.claimed_by != id:
|
||||
return JsonResponse({'error': 'task claimed by other identity'}, status=403)
|
||||
|
||||
task.claimed_by = None
|
||||
task.save()
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'uuid': task.uuid,
|
||||
'type': task.type,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
|
@ -0,0 +1,144 @@
|
|||
from ninja import Router, ModelSchema, Schema
|
||||
from ninja.errors import HttpError
|
||||
from config import models
|
||||
from typing import List
|
||||
from guardian.shortcuts import get_objects_for_user, assign_perm
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class Restream(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Restream
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class RestreamPatch(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Restream
|
||||
exclude = ["id"]
|
||||
fields_optional = "__all__"
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class Stream(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Stream
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class StreamPatch(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Stream
|
||||
fields = ["name"]
|
||||
fields_optional = "__all__"
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class StreamCreate(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Stream
|
||||
fields = ["name"]
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
@router.get('/streams', response=List[Stream])
|
||||
def list_streams(request):
|
||||
return get_objects_for_user(request.user, 'view_stream', models.Stream.objects.all())
|
||||
|
||||
|
||||
@router.post('/streams', response=Stream)
|
||||
def create_stream(request, payload: StreamCreate):
|
||||
stream = models.Stream.objects.create(**payload.dict())
|
||||
assign_perm('view_stream', request.user, stream)
|
||||
assign_perm('change_stream', request.user, stream)
|
||||
assign_perm('delete_stream', request.user, stream)
|
||||
return stream
|
||||
|
||||
|
||||
@router.get('/streams/{id}', response=Stream)
|
||||
def get_stream(request, id: int):
|
||||
stream = get_object_or_404(models.Stream, id=id)
|
||||
|
||||
if not request.user.has_perm('view_stream', stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
return stream
|
||||
|
||||
|
||||
@router.patch('/streams/{id}', response=Stream)
|
||||
def update_stream(request, id: int, payload: StreamPatch):
|
||||
stream = get_object_or_404(models.Stream, id=id)
|
||||
|
||||
if not request.user.has_perm('change_stream', stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
stream.name = payload.name
|
||||
stream.save()
|
||||
return stream
|
||||
|
||||
|
||||
@router.delete('/streams/{id}', response=None)
|
||||
def delete_stream(request, id: int):
|
||||
stream = get_object_or_404(models.Stream, id=id)
|
||||
|
||||
if not request.user.has_perm('delete_stream', stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
stream.delete()
|
||||
|
||||
|
||||
@router.get('/restreams', response=List[Restream])
|
||||
def list_restreams(request):
|
||||
return get_objects_for_user(request.user, 'view_restream', models.Restream.objects.all())
|
||||
|
||||
|
||||
@router.post('/restreams', response=Restream)
|
||||
def create_restream(request, payload: Restream):
|
||||
if not request.user.has_perm('view_stream', payload.stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
restream = models.Restream.objects.create(**payload.dict())
|
||||
assign_perm('view_restream', request.user, restream)
|
||||
assign_perm('change_restream', request.user, restream)
|
||||
assign_perm('delete_restream', request.user, restream)
|
||||
return restream
|
||||
|
||||
|
||||
@router.get('/restreams/{id}', response=Restream)
|
||||
def get_restream(request, id: int):
|
||||
restream = get_object_or_404(models.Restream, id=id)
|
||||
|
||||
if not request.user.has_perm('view_restream', restream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
return restream
|
||||
|
||||
|
||||
@router.patch('/restreams/{id}', response=Restream)
|
||||
def update_restream(request, id: int, payload: RestreamPatch):
|
||||
restream = get_object_or_404(models.Restream, id=id)
|
||||
|
||||
if not request.user.has_perm('change_restream', restream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
if payload.stream:
|
||||
payload.stream = get_object_or_404(models.Stream, id=payload.stream)
|
||||
|
||||
if not request.user.has_perm('view_stream', payload.stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
for key, value in payload.dict(exclude_unset=True).items():
|
||||
setattr(restream, key, value)
|
||||
restream.save()
|
||||
return restream
|
||||
|
||||
|
||||
@router.delete('/restreams/{id}', response=None)
|
||||
def delete_restream(request, id: int):
|
||||
restream = get_object_or_404(models.Restream, id=id)
|
||||
|
||||
if not request.user.has_perm('delete_restream', restream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
restream.delete()
|
|
@ -0,0 +1,30 @@
|
|||
from ninja import NinjaAPI, ModelSchema, Router
|
||||
from ninja.security import django_auth
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from config.api import router as config_router
|
||||
from concierge.api import router as concierge_router
|
||||
|
||||
core_router = Router()
|
||||
|
||||
class UserSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email"]
|
||||
|
||||
@core_router.get("/me", response=UserSchema)
|
||||
def me(request):
|
||||
return request.user
|
||||
|
||||
|
||||
|
||||
api = NinjaAPI(
|
||||
title="Portier API",
|
||||
version="2.0.0",
|
||||
description="HTTP API for Portier. Use this to interact with the Portier backend.",
|
||||
csrf=True,
|
||||
)
|
||||
|
||||
api.add_router("/", core_router, auth=django_auth, tags=["Core"])
|
||||
api.add_router("/config/", config_router, auth=django_auth, tags=["Configuration"])
|
||||
api.add_router("/concierge/", concierge_router, auth=None, tags=["Concierge"])
|
|
@ -41,7 +41,8 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'ninja',
|
||||
#'rest_framework',
|
||||
'guardian',
|
||||
'django_registration',
|
||||
'bootstrap4',
|
||||
|
@ -194,7 +195,7 @@ DJANGO_CELERY_BEAT_TZ_AWARE = False
|
|||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
if DEBUG:
|
||||
if DEBUG == "LOLOLOL":
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from .api import api
|
||||
|
||||
urlpatterns = [
|
||||
path('accounts/', include('django_registration.backends.activation.urls')),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('admin/', admin.site.urls),
|
||||
path('config/', include('config.urls')),
|
||||
path('concierge/', include('concierge.urls')),
|
||||
path('api/v1/', include('restapi.urls')),
|
||||
path('api/v2/', api.urls),
|
||||
path('', include('core.urls')),
|
||||
]
|
||||
|
|
|
@ -5,8 +5,7 @@ django-guardian
|
|||
django-fontawesome-5
|
||||
django-celery-beat
|
||||
django-filter
|
||||
djangorestframework
|
||||
djangorestframework-guardian
|
||||
django-ninja
|
||||
celery>=5.3
|
||||
gunicorn>=20
|
||||
psycopg2-binary
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RestapiConfig(AppConfig):
|
||||
name = 'restapi'
|
|
@ -1,16 +0,0 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import StreamViewSet, RestreamViewSet
|
||||
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'stream', StreamViewSet)
|
||||
router.register(r'restream', RestreamViewSet)
|
||||
|
||||
app_name = 'restapi'
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
]
|
|
@ -1,51 +0,0 @@
|
|||
from rest_framework_guardian.serializers import ObjectPermissionsAssignmentMixin
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework_guardian import filters
|
||||
from config.models import Stream, Restream
|
||||
|
||||
|
||||
class StreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Stream
|
||||
fields = '__all__'
|
||||
read_only_fields = ['publish_counter']
|
||||
|
||||
def get_permissions_map(self, created):
|
||||
current_user = self.context['request'].user
|
||||
return {
|
||||
'view_stream': [current_user],
|
||||
'change_stream': [current_user],
|
||||
'delete_stream': [current_user]
|
||||
}
|
||||
|
||||
|
||||
class StreamViewSet(viewsets.ModelViewSet):
|
||||
queryset = Stream.objects.all()
|
||||
serializer_class = StreamSerializer
|
||||
filter_backends = [filters.ObjectPermissionsFilter]
|
||||
|
||||
|
||||
class RestreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Restream
|
||||
fields = '__all__'
|
||||
|
||||
def get_permissions_map(self, created):
|
||||
current_user = self.context['request'].user
|
||||
return {
|
||||
'view_restream': [current_user],
|
||||
'change_restream': [current_user],
|
||||
'delete_restream': [current_user]
|
||||
}
|
||||
|
||||
def validate_stream(self, value):
|
||||
request = self.context['request']
|
||||
if not request.user.has_perm('config.view_stream', value):
|
||||
raise serializers.ValidationError('Access to stream is not authorized')
|
||||
return value
|
||||
|
||||
|
||||
class RestreamViewSet(viewsets.ModelViewSet):
|
||||
queryset = Restream.objects.all()
|
||||
serializer_class = RestreamSerializer
|
||||
filter_backends = [filters.ObjectPermissionsFilter]
|
|
@ -13,7 +13,7 @@ var app = new Vue({
|
|||
},
|
||||
toggleActive(cfg) {
|
||||
axios
|
||||
.patch('/api/v1/restream/' + cfg.id + '/', { active: !cfg.active })
|
||||
.patch('/api/v2/config/restreams/' + cfg.id, { active: !cfg.active })
|
||||
.then(response => {
|
||||
i = this.cfgs.findIndex((obj => obj.id == cfg.id))
|
||||
Vue.set(this.cfgs, i, response.data)
|
||||
|
@ -22,7 +22,7 @@ var app = new Vue({
|
|||
},
|
||||
fetchData() {
|
||||
axios
|
||||
.get('/api/v1/restream/')
|
||||
.get('/api/v2/config/restreams')
|
||||
.then(response => {
|
||||
this.cfgs = response.data
|
||||
this.isLoading = false
|
||||
|
|
|
@ -16,7 +16,7 @@ var app = new Vue({
|
|||
},
|
||||
fetchData() {
|
||||
axios
|
||||
.get('/api/v1/stream/')
|
||||
.get('/api/v2/config/streams')
|
||||
.then(response => {
|
||||
this.streams = response.data
|
||||
this.isLoading = false
|
||||
|
|
Loading…
Reference in New Issue