From e3d6c0ae7e5595615bf04944bd67b30201865c9b Mon Sep 17 00:00:00 2001 From: Yanis Date: Fri, 20 Feb 2026 16:42:42 +0100 Subject: [PATCH 1/3] modified views and serializers --- PrINTech-Back/back/apps/api/serializers.py | 44 +++++++++++++++++++++- PrINTech-Back/back/apps/api/views.py | 2 + 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/PrINTech-Back/back/apps/api/serializers.py b/PrINTech-Back/back/apps/api/serializers.py index 51786a6..7d7c26e 100644 --- a/PrINTech-Back/back/apps/api/serializers.py +++ b/PrINTech-Back/back/apps/api/serializers.py @@ -1,9 +1,12 @@ from django.contrib.auth.password_validation import validate_password from rest_framework import serializers -from .models import User +from rest_framework.fields import UUIDField + +from .models import User, Request, File class ChangePasswordSerializer(serializers.Serializer): + old_password = serializers.CharField(required=True, write_only=True) new_password = serializers.CharField(required=True, write_only=True) confirm_password = serializers.CharField(required=True, write_only=True) @@ -31,6 +34,8 @@ def save(self, **kwargs): class UserSerializer(serializers.ModelSerializer): + + def create(self, validated_data): user = User.objects.create_user( username=validated_data["username"], @@ -49,3 +54,40 @@ class Meta: fields = ["id", "username", "password", "email"] extra_kwargs = {"password": {"write_only": True}} + + +class CreateFileSerializer(serializers.ModelSerializer): + requests = serializers.PrimaryKeyRelatedField(many=False, pk_field=UUIDField(format='hex'), required=False, queryset=Request.objects.all()) + filaments = serializers.PrimaryKeyRelatedField(many=True, queryset=File.objects.all()) + + class Meta: + model = File + fields = [ + "user_id", + "path", + "number_of_printing", + "filaments", + "requests", + "para_slicer", + ] + + def validate(self, data): + user = self.context["request"].user + path = data["path"] + + if File.objects.filter(path=path, user_id=user).exists(): + raise serializers.ValidationError("File already exists") + + return data + + +class CreateRequestSerializer(serializers.ModelSerializer): + + + class Meta: + model = Request + fields = ["file_id", "printer_id", "created_at"] + extra_kwargs = {"created_at": {"read_only": True}} + + def validate(self, data): + pass \ No newline at end of file diff --git a/PrINTech-Back/back/apps/api/views.py b/PrINTech-Back/back/apps/api/views.py index d7fa2fc..d4e4d19 100644 --- a/PrINTech-Back/back/apps/api/views.py +++ b/PrINTech-Back/back/apps/api/views.py @@ -56,3 +56,5 @@ def update(self, request, *args, **kwargs): {"message": "Mot de passe changé avec succès."}, status=status.HTTP_200_OK ) +class CreateRequestView(generics.CreateAPIView): + permission_classes = [IsAuthenticated] \ No newline at end of file From 04823b47287c974a3af4e5dbe5d52f7a5386f4c4 Mon Sep 17 00:00:00 2001 From: Yanis Date: Mon, 23 Feb 2026 22:09:33 +0100 Subject: [PATCH 2/3] modified CreateRequest and added CreateFile --- .../back/apps/api/migrations/0001_initial.py | 32 ++-- PrINTech-Back/back/apps/api/models.py | 2 + PrINTech-Back/back/apps/api/serializers.py | 24 ++- PrINTech-Back/back/apps/api/views.py | 51 +++++- PrINTech-Back/back/settings/base.py | 6 + PrINTech-Back/back/urls.py | 13 +- diagrams/auth-diagram.svg | 66 +++++++ diagrams/db-diagram.svg | 162 ++++++++++++++++++ 8 files changed, 325 insertions(+), 31 deletions(-) create mode 100644 diagrams/auth-diagram.svg create mode 100644 diagrams/db-diagram.svg diff --git a/PrINTech-Back/back/apps/api/migrations/0001_initial.py b/PrINTech-Back/back/apps/api/migrations/0001_initial.py index 7145403..876b00d 100644 --- a/PrINTech-Back/back/apps/api/migrations/0001_initial.py +++ b/PrINTech-Back/back/apps/api/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 6.0.2 on 2026-02-17 23:53 +# Generated by Django 6.0.2 on 2026-02-23 19:59 import django.contrib.auth.models import django.contrib.auth.validators import django.db.models.deletion import django.utils.timezone +import uuid from django.conf import settings from django.db import migrations, models @@ -31,13 +32,12 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('type', models.CharField(choices=[('SLA/DLP/MSLA', 'Sla'), ('SLS/MJF', 'Sls'), ('FDM/FFF', 'Fdm'), ('MJP', 'Mjp'), ('Binder_Jetting', 'Binder Jetting'), ('DMLS/SLM', 'Dmls')], max_length=25)), - ('status', models.CharField(choices=[('UP', 'Up'), ('DOWN', 'Down'), ('USED', 'Used')], max_length=25)), + ('status', models.CharField(choices=[('UP', 'Up'), ('DOWN', 'Down'), ('USED', 'Used')], default='DOWN', max_length=25)), ], ), 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')), @@ -47,8 +47,10 @@ class Migration(migrations.Migration): ('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')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('email', models.EmailField(max_length=254, unique=True)), - ('credit', models.DecimalField(decimal_places=2, default=0, max_digits=6)), + ('credit', models.IntegerField(default=0)), + ('is_bot', models.BooleanField(default=False)), ('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')), ('user_permissions', 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')), ], @@ -64,9 +66,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='File', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('path', models.FileField(upload_to='uploads/%Y/%m/%d')), - ('number_of_printing', models.IntegerField(default=1)), + ('number_of_printing', models.PositiveIntegerField(default=1)), ('para_slicer', models.JSONField(blank=True, null=True)), ('filament_id', models.ManyToManyField(to='api.filament')), ('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), @@ -75,21 +77,21 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Operation', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('operation_type', models.CharField(choices=[('WITHDRAW', 'Withdraw'), ('ADDING', 'Adding')], max_length=25)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('operation_type', models.CharField(choices=[('CASH', 'Cash'), ('CARD', 'Card')], max_length=25)), ('comment', models.TextField(blank=True, null=True)), - ('amount', models.PositiveIntegerField(default=0)), - ('signer_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_signer', to=settings.AUTH_USER_MODEL)), - ('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_receiver', to=settings.AUTH_USER_MODEL)), + ('amount', models.IntegerField(default=0)), + ('agent_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_agent', to=settings.AUTH_USER_MODEL)), + ('beneficiary_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_beneficiary', to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( name='Request', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(choices=[('PENDING', 'Pending'), ('PROCESSING', 'Processing'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], max_length=25)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('PROCESSING', 'Processing'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], default='PENDING', max_length=25)), ('file_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.file')), ('printer_id', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.printer')), ], diff --git a/PrINTech-Back/back/apps/api/models.py b/PrINTech-Back/back/apps/api/models.py index 944db9e..679ac12 100644 --- a/PrINTech-Back/back/apps/api/models.py +++ b/PrINTech-Back/back/apps/api/models.py @@ -8,6 +8,7 @@ class User(AbstractUser): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.EmailField(blank=False, unique=True, null=False) credit = models.IntegerField(default=0) + is_bot = models.BooleanField(default=False) class Filament(models.Model): @@ -80,6 +81,7 @@ class Status(models.TextChoices): status = models.CharField(choices=Status.choices, max_length=25, null=False, blank=False, default=Status.PENDING) + class Operation(models.Model): class Type(models.TextChoices): diff --git a/PrINTech-Back/back/apps/api/serializers.py b/PrINTech-Back/back/apps/api/serializers.py index 7d7c26e..8068a99 100644 --- a/PrINTech-Back/back/apps/api/serializers.py +++ b/PrINTech-Back/back/apps/api/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from rest_framework.fields import UUIDField -from .models import User, Request, File +from .models import User, Request, File, Filament class ChangePasswordSerializer(serializers.Serializer): @@ -57,8 +57,7 @@ class Meta: class CreateFileSerializer(serializers.ModelSerializer): - requests = serializers.PrimaryKeyRelatedField(many=False, pk_field=UUIDField(format='hex'), required=False, queryset=Request.objects.all()) - filaments = serializers.PrimaryKeyRelatedField(many=True, queryset=File.objects.all()) + filaments = serializers.PrimaryKeyRelatedField(many=True, queryset=Filament.objects.all()) class Meta: model = File @@ -67,9 +66,9 @@ class Meta: "path", "number_of_printing", "filaments", - "requests", "para_slicer", ] + extra_kwargs = {"user_id": {"read_only": True}} def validate(self, data): user = self.context["request"].user @@ -81,13 +80,24 @@ def validate(self, data): return data -class CreateRequestSerializer(serializers.ModelSerializer): +class CreateRequestSerializer(serializers.ModelSerializer): + class Meta: model = Request - fields = ["file_id", "printer_id", "created_at"] + fields = ["file_id", "created_at"] extra_kwargs = {"created_at": {"read_only": True}} def validate(self, data): - pass \ No newline at end of file + user = self.context["request"].user + file = data["file_id"] + if file.user_id != user.id: + raise serializers.ValidationError("File doesn't belong to this user") + return data + +class ChangeRequestSerializer(serializers.ModelSerializer): + class Meta: + model = Request + fields = ["printer_id", "status"] + extra_kwargs = {"status": {"read_only": True}} diff --git a/PrINTech-Back/back/apps/api/views.py b/PrINTech-Back/back/apps/api/views.py index d4e4d19..6dcfb73 100644 --- a/PrINTech-Back/back/apps/api/views.py +++ b/PrINTech-Back/back/apps/api/views.py @@ -1,11 +1,16 @@ +from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import action from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken, AccessToken -from .models import User +from rest_framework_simplejwt.authentication import JWTAuthentication +from .models import User, Request from rest_framework import permissions, viewsets, generics, status from django.shortcuts import render -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.throttling import UserRateThrottle -from .serializers import UserSerializer, ChangePasswordSerializer +from .serializers import UserSerializer, ChangePasswordSerializer, CreateRequestSerializer, ChangeRequestSerializer, \ + CreateFileSerializer + class CreateUserView(generics.CreateAPIView): queryset = User.objects.all() @@ -56,5 +61,41 @@ def update(self, request, *args, **kwargs): {"message": "Mot de passe changé avec succès."}, status=status.HTTP_200_OK ) -class CreateRequestView(generics.CreateAPIView): - permission_classes = [IsAuthenticated] \ No newline at end of file + +class RequestViewSet(viewsets.ModelViewSet): + queryset = Request.objects.all() + permission_classes = [IsAuthenticated] + authentication_classes = [TokenAuthentication, JWTAuthentication] + throttle_classes = [UserRateThrottle] + + def get_serializer_class(self): + if self.action in ['update', 'partial_update', 'completed']: + return ChangeRequestSerializer + return CreateRequestSerializer + + @action(detail=True, methods=['post'], permission_classes=[IsAdminUser]) + def attributed(self, request, pk=None): + printing_request = self.get_object() + printer_id = request.data.get('printer_id') + if not printer_id: + return Response({'error': 'printer_id is required'}, status=400) + + printing_request.status = Request.Status.PROCESSING + printing_request.printer_id_id = printer_id + printing_request.save() + return Response({'status': 'print attributed, currently processing'}) + + @action(detail=True, methods=['post'], permission_classes=[IsAdminUser]) + def completed(self, request, pk=None): + printing_request = self.get_object() + printing_request.status = Request.Status.COMPLETED + printing_request.save() + return Response({'status': 'print completed'}) + +class CreateFileView(generics.CreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = CreateFileSerializer + throttle_classes = [UserRateThrottle] + + def perform_create(self, serializer): + serializer.save(user=self.request.user) diff --git a/PrINTech-Back/back/settings/base.py b/PrINTech-Back/back/settings/base.py index f58712a..66d88bd 100644 --- a/PrINTech-Back/back/settings/base.py +++ b/PrINTech-Back/back/settings/base.py @@ -108,6 +108,7 @@ 'rest_framework', 'drf_spectacular', 'rest_framework_simplejwt', + 'rest_framework.authtoken', ] LOCAL_APPS = [ @@ -124,6 +125,11 @@ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', ) } diff --git a/PrINTech-Back/back/urls.py b/PrINTech-Back/back/urls.py index 62e7194..5a351fa 100644 --- a/PrINTech-Back/back/urls.py +++ b/PrINTech-Back/back/urls.py @@ -4,21 +4,22 @@ from rest_framework import routers, permissions from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from .apps.api.views import CreateUserView, UserMeView, ChangePasswordView +from .apps.api.views import CreateUserView, UserMeView, ChangePasswordView, \ + RequestViewSet, CreateFileView from .views import home admin.autodiscover() router = routers.DefaultRouter() +router.register(r'requests', RequestViewSet, basename='request') urlpatterns = [ - # Examples: path('', home, name='home'), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - # path('app/', include('apps.app.urls')), - path("api/v1/", include(router.urls)), + path("api/v1/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/v1/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/v1/user/", CreateUserView.as_view(), name="create_user"), path("api/v1/user/me/", UserMeView.as_view(), name="user_info"), path( @@ -26,9 +27,13 @@ ChangePasswordView.as_view(), name="change-password", ), + + path("api/v1/file/", CreateFileView.as_view(), name="create_file"), + path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), path('admin/', admin.site.urls), + path("api/v1/", include(router.urls)), ] # debug toolbar for dev diff --git a/diagrams/auth-diagram.svg b/diagrams/auth-diagram.svg new file mode 100644 index 0000000..e1e6bc9 --- /dev/null +++ b/diagrams/auth-diagram.svg @@ -0,0 +1,66 @@ +"Sequencial DiagramAuthentification"UserAngular/Front-endDjango API (back.apps.api)PostgreSQLSign upFill the formPOST /api/v1/user/Check the mailalt[mail already been used]Conflict400 Bad RequestDisplay "This mail already been used"[new user]Save User (hash password)Success201 CreatedDisplay "Successful registration"Log in (with SimpleJWT)Enter identifiersPOST /api/v1/token/Verify Useralt[invalid identifiers]Not found / Wrong password401 UnauthorizedDisplay "Email or password wrong"[valid identifiers]200 OK {access, refresh}Tokens are stored in localStorage/Cookies/storeRedirect to main pageAPI call with expired access tokenGET /api/v1/user/me/ (expired access token)401 Unauthorizedalt[valid refresh token]POST /api/v1/token/refresh/ {refresh_token}200 OK {new_access_token}Retry the previous request[expired refresh token]Forced disconnection (redirect to login page) \ No newline at end of file diff --git a/diagrams/db-diagram.svg b/diagrams/db-diagram.svg new file mode 100644 index 0000000..10ec156 --- /dev/null +++ b/diagrams/db-diagram.svg @@ -0,0 +1,162 @@ +" UML DiagramDatabase"User EnvironmentMaterial AssetsRequestStatusPENDINGPROCESSINGCOMPLETEDFAILEDOperationTypeCASHCARDOTHERUserid : UUID «PK»username : varchar(150) «UNIQUE»password : varchar(128)email : varchar(254) «UNIQUE»credit : integer = 0is_staff = falseis_superuser = false...Operationid : UUID «PK»beneficiary_id : UUID «FK»agent_id : UUID «FK»operation_type : OperationTypeamount : integer = 0comment : textcreated_at : DateTimeFileid : UUID «PK»user_id : UUID «FK»path : varchar(100)number_of_printing : integer = 1 {>= 0}para_slicer : jsonbRequestid : UUID «PK»file_id : UUID «FK»printer_id : Bigint «FK»status : RequestStatus = PENDINGcreated_at : DateTimePrinterid : Bigint «PK»type : PrinterTypestatus : PrinterStatus = DOWNFilamentid : Bigint «PK»colour : varchar(25)type : FilamentTypequantity : integer = 0 {>= 0}FilamentTypePLAREINFORCED_PLAPETGABSTPE/TPUPrinterTypeSLASLSFDMMJPDMLSBINDER_JETTINGPrinterStatusUPDOWNUSEDInteractsprocessesbeneficiary or agentusesusesusesuses \ No newline at end of file From 4d2cb72b59098c8468120dbeb6b62000adb85fc4 Mon Sep 17 00:00:00 2001 From: Yanis Date: Tue, 24 Feb 2026 00:22:32 +0100 Subject: [PATCH 3/3] modified views and serializers --- PrINTech-Back/back/apps/api/serializers.py | 2 - PrINTech-Back/back/apps/api/views.py | 113 +++++++++++++++++---- PrINTech-Back/back/urls.py | 3 +- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/PrINTech-Back/back/apps/api/serializers.py b/PrINTech-Back/back/apps/api/serializers.py index 8068a99..88e7fc3 100644 --- a/PrINTech-Back/back/apps/api/serializers.py +++ b/PrINTech-Back/back/apps/api/serializers.py @@ -34,8 +34,6 @@ def save(self, **kwargs): class UserSerializer(serializers.ModelSerializer): - - def create(self, validated_data): user = User.objects.create_user( username=validated_data["username"], diff --git a/PrINTech-Back/back/apps/api/views.py b/PrINTech-Back/back/apps/api/views.py index 6dcfb73..951fd5d 100644 --- a/PrINTech-Back/back/apps/api/views.py +++ b/PrINTech-Back/back/apps/api/views.py @@ -1,16 +1,17 @@ +from django.db.models.signals import post_delete +from django.dispatch import receiver from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import action from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from rest_framework_simplejwt.authentication import JWTAuthentication -from .models import User, Request -from rest_framework import permissions, viewsets, generics, status -from django.shortcuts import render +from .models import User, Request, File, Printer +from rest_framework import viewsets, generics, status from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.throttling import UserRateThrottle from .serializers import UserSerializer, ChangePasswordSerializer, CreateRequestSerializer, ChangeRequestSerializer, \ CreateFileSerializer - +from django.db import transaction class CreateUserView(generics.CreateAPIView): queryset = User.objects.all() @@ -20,7 +21,8 @@ class CreateUserView(generics.CreateAPIView): def create(self, request, *args, **kwargs): if request.user.is_authenticated: - return Response({"Message": "Vous ne devez pas être authentifié."},status=status.HTTP_403_FORBIDDEN) + return Response({"Message": "You need to be disconnected"},status=status.HTTP_403_FORBIDDEN) + serializer = self.get_serializer( data=request.data, context={"request": request} ) @@ -58,39 +60,80 @@ def get_object(self): def update(self, request, *args, **kwargs): super().update(request, *args, **kwargs) return Response( - {"message": "Mot de passe changé avec succès."}, status=status.HTTP_200_OK + {"message": "Your password has been successfully modified."}, status=status.HTTP_200_OK ) class RequestViewSet(viewsets.ModelViewSet): queryset = Request.objects.all() - permission_classes = [IsAuthenticated] authentication_classes = [TokenAuthentication, JWTAuthentication] throttle_classes = [UserRateThrottle] + def get_permissions(self): + if self.action == 'destroy': + return [IsAdminUser()] + return [IsAuthenticated()] + + def get_queryset(self): + user = self.request.user + if user.is_staff: + return Request.objects.all() + return Request.objects.filter(file_id__user_id=user) + def get_serializer_class(self): if self.action in ['update', 'partial_update', 'completed']: return ChangeRequestSerializer return CreateRequestSerializer - @action(detail=True, methods=['post'], permission_classes=[IsAdminUser]) - def attributed(self, request, pk=None): - printing_request = self.get_object() - printer_id = request.data.get('printer_id') - if not printer_id: - return Response({'error': 'printer_id is required'}, status=400) - printing_request.status = Request.Status.PROCESSING - printing_request.printer_id_id = printer_id - printing_request.save() - return Response({'status': 'print attributed, currently processing'}) + def _assign_next_pending_request(self, printer): + with transaction.atomic(): + next_request = Request.objects.filter( + status=Request.Status.PENDING + ).order_by('created_at').select_for_update(skip_locked=True).first() + + if next_request: + next_request.printer_id = printer + next_request.status = Request.Status.PROCESSING + next_request.save() + + printer.status = Printer.Status.USED + printer.save() + return True + return False + + + def perform_create(self, serializer): + new_request = serializer.save() + with transaction.atomic(): + free_printer = Printer.objects.filter(status=Printer.Status.UP).first() + if free_printer: + self._assign_next_pending_request(free_printer) + + @action(detail=True, methods=['post'], permission_classes=[IsAdminUser]) def completed(self, request, pk=None): - printing_request = self.get_object() - printing_request.status = Request.Status.COMPLETED - printing_request.save() - return Response({'status': 'print completed'}) + current_request = self.get_object() + printer = current_request.printer_id + + if not request.user.is_bot : + return Response({"error": "you are not a bot"}, status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + current_request.status = Request.Status.COMPLETED + current_request.save() + + if printer: + printer.status = Printer.Status.UP + printer.save() + + if printer: + assigned = self._assign_next_pending_request(printer) + if assigned: + return Response({'status': 'completed and new request assigned'}) + + return Response({'status': 'completed, printer is now idle'}) class CreateFileView(generics.CreateAPIView): permission_classes = [IsAuthenticated] @@ -99,3 +142,31 @@ class CreateFileView(generics.CreateAPIView): def perform_create(self, serializer): serializer.save(user=self.request.user) + + +class FileViewSet(viewsets.ModelViewSet): + serializer_class = CreateFileSerializer + permission_classes = [IsAuthenticated] + throttle_classes = [UserRateThrottle] + + def get_queryset(self): + return File.objects.filter(user_id=self.request.user) + + @receiver(post_delete, sender=File) + def submission_delete(sender, instance, **kwargs): + instance.path.delete(False) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + has_active_print = instance.requests.filter( + status__in=[Request.Status.PROCESSING, Request.Status.PENDING] + ).exists() + + if has_active_print: + return Response( + {"error": "Impossible to delete this file, a request is being processed"}, + status=status.HTTP_400_BAD_REQUEST + ) + + return super().destroy(request, *args, **kwargs) + diff --git a/PrINTech-Back/back/urls.py b/PrINTech-Back/back/urls.py index 5a351fa..00ad222 100644 --- a/PrINTech-Back/back/urls.py +++ b/PrINTech-Back/back/urls.py @@ -5,13 +5,14 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from .apps.api.views import CreateUserView, UserMeView, ChangePasswordView, \ - RequestViewSet, CreateFileView + RequestViewSet, CreateFileView, FileViewSet from .views import home admin.autodiscover() router = routers.DefaultRouter() router.register(r'requests', RequestViewSet, basename='request') +router.register(r'files', FileViewSet, basename='file') urlpatterns = [ path('', home, name='home'),