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 51786a6..88e7fc3 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, Filament
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)
@@ -49,3 +52,50 @@ class Meta:
fields = ["id", "username", "password", "email"]
extra_kwargs = {"password": {"write_only": True}}
+
+
+class CreateFileSerializer(serializers.ModelSerializer):
+ filaments = serializers.PrimaryKeyRelatedField(many=True, queryset=Filament.objects.all())
+
+ class Meta:
+ model = File
+ fields = [
+ "user_id",
+ "path",
+ "number_of_printing",
+ "filaments",
+ "para_slicer",
+ ]
+ extra_kwargs = {"user_id": {"read_only": True}}
+
+ 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", "created_at"]
+ extra_kwargs = {"created_at": {"read_only": True}}
+
+ def validate(self, data):
+ 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 d7fa2fc..951fd5d 100644
--- a/PrINTech-Back/back/apps/api/views.py
+++ b/PrINTech-Back/back/apps/api/views.py
@@ -1,11 +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 .models import User
-from rest_framework import permissions, viewsets, generics, status
-from django.shortcuts import render
-from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework_simplejwt.authentication import JWTAuthentication
+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
+from .serializers import UserSerializer, ChangePasswordSerializer, CreateRequestSerializer, ChangeRequestSerializer, \
+ CreateFileSerializer
+from django.db import transaction
class CreateUserView(generics.CreateAPIView):
queryset = User.objects.all()
@@ -15,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}
)
@@ -53,6 +60,113 @@ 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()
+ 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
+
+
+ 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):
+ 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]
+ serializer_class = CreateFileSerializer
+ throttle_classes = [UserRateThrottle]
+
+ 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/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..00ad222 100644
--- a/PrINTech-Back/back/urls.py
+++ b/PrINTech-Back/back/urls.py
@@ -4,21 +4,23 @@
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, 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 = [
- # 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 +28,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 @@
+
\ 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 @@
+
\ No newline at end of file