Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions PrINTech-Back/back/apps/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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')),
Expand All @@ -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')),
],
Expand All @@ -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)),
Expand All @@ -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')),
],
Expand Down
2 changes: 2 additions & 0 deletions PrINTech-Back/back/apps/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
52 changes: 51 additions & 1 deletion PrINTech-Back/back/apps/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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}}
128 changes: 121 additions & 7 deletions PrINTech-Back/back/apps/api/views.py
Original file line number Diff line number Diff line change
@@ -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()
Expand 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}
)
Expand Down Expand Up @@ -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)

6 changes: 6 additions & 0 deletions PrINTech-Back/back/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
'rest_framework',
'drf_spectacular',
'rest_framework_simplejwt',
'rest_framework.authtoken',
]

LOCAL_APPS = [
Expand All @@ -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',
)
}

Expand Down
14 changes: 10 additions & 4 deletions PrINTech-Back/back/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,37 @@
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(
"api/v1/user/me/change-password/",
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
Expand Down
66 changes: 66 additions & 0 deletions diagrams/auth-diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading