fix: signature électronique

This commit is contained in:
N3WT DE COMPET
2026-04-05 16:06:04 +02:00
parent a81b76ecea
commit 92c3183153
18 changed files with 656 additions and 102 deletions

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:05
import django.contrib.auth.models
import django.contrib.auth.validators

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:04
import django.db.models.deletion
from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:04
import Establishment.models
import django.contrib.postgres.fields

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:05
import django.db.models.deletion
import django.utils.timezone

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:05
import django.db.models.deletion
from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:05
import django.db.models.deletion
from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:05
import django.contrib.postgres.fields
import django.db.models.deletion
@ -124,7 +124,6 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100)),
('updated_date', models.DateTimeField(auto_now=True)),
('color_code', models.CharField(default='#FFFFFF', max_length=7)),
('school_year', models.CharField(blank=True, max_length=9)),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specialities', to='Establishment.establishment')),
],
),
@ -154,7 +153,6 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_name', models.CharField(max_length=100)),
('first_name', models.CharField(max_length=100)),
('school_year', models.CharField(blank=True, max_length=9)),
('updated_date', models.DateTimeField(auto_now=True)),
('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='teacher_profile', to='Auth.profilerole')),
('specialities', models.ManyToManyField(blank=True, to='School.speciality')),

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:04
import django.db.models.deletion
from django.db import migrations, models

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.3 on 2026-04-05 08:05
# Generated by Django 5.1.3 on 2026-04-05 14:04
import Subscriptions.models
import django.db.models.deletion
@ -54,6 +54,8 @@ class Migration(migrations.Migration):
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
('isValidated', models.BooleanField(blank=True, default=None, null=True)),
('electronic_signature', models.TextField(blank=True, help_text='Signature électronique encodée en base64', null=True)),
('electronic_signature_date', models.DateTimeField(blank=True, help_text='Date de la signature électronique', null=True)),
],
),
migrations.CreateModel(
@ -169,6 +171,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='', max_length=255)),
('is_required', models.BooleanField(default=False)),
('requires_electronic_signature', models.BooleanField(default=False, help_text='Si activé, le parent devra signer électroniquement ce document')),
('formMasterData', models.JSONField(blank=True, default=list, null=True)),
('file', models.FileField(blank=True, help_text='Fichier du formulaire existant (PDF, DOC, etc.)', null=True, upload_to=Subscriptions.models.registration_school_file_master_upload_to)),
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')),

View File

@ -384,6 +384,7 @@ class RegistrationSchoolFileMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
name = models.CharField(max_length=255, default="")
is_required = models.BooleanField(default=False)
requires_electronic_signature = models.BooleanField(default=False, help_text="Si activé, le parent devra signer électroniquement ce document")
formMasterData = models.JSONField(default=list, blank=True, null=True)
file = models.FileField(
upload_to=registration_school_file_master_upload_to,
@ -558,6 +559,9 @@ class RegistrationSchoolFileTemplate(models.Model):
formTemplateData = models.JSONField(default=list, blank=True, null=True)
# Tri-etat: None=en attente, True=valide, False=refuse
isValidated = models.BooleanField(null=True, blank=True, default=None)
# Signature électronique (base64 SVG ou PNG)
electronic_signature = models.TextField(null=True, blank=True, help_text="Signature électronique encodée en base64")
electronic_signature_date = models.DateTimeField(null=True, blank=True, help_text="Date de la signature électronique")
def __str__(self):
return self.name

View File

@ -57,6 +57,8 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
file_url = serializers.SerializerMethodField()
master_file_url = serializers.SerializerMethodField()
requires_electronic_signature = serializers.SerializerMethodField()
is_electronically_signed = serializers.SerializerMethodField()
class Meta:
model = RegistrationSchoolFileTemplate
@ -72,6 +74,27 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
return obj.master.file.url
return None
def get_requires_electronic_signature(self, obj):
# Retourne si le document nécessite une signature électronique
if obj.master:
return obj.master.requires_electronic_signature
return False
def get_is_electronically_signed(self, obj):
# Retourne True si le document a été signé électroniquement
return bool(obj.electronic_signature)
def update(self, instance, validated_data):
# Auto-remplir la date de signature si electronic_signature est fournie
from django.utils import timezone
if 'electronic_signature' in validated_data and validated_data['electronic_signature']:
# Nouvelle signature ou re-signature : enregistrer la date
validated_data['electronic_signature_date'] = timezone.now()
# Si le document était refusé, le repasser en attente de validation
if instance.isValidated == False:
validated_data['isValidated'] = None
return super().update(instance, validated_data)
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
file_url = serializers.SerializerMethodField()

View File

@ -0,0 +1,245 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{ pdf_title }}</title>
<style>
@page {
size: A4;
margin: 1.4cm 1.6cm;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 11pt;
color: #111827;
margin: 0;
padding: 0;
line-height: 1.35;
background: #ffffff;
}
.page-shell {
border-left: 1px dashed #cbd5e1;
border-right: 1px dashed #cbd5e1;
padding: 0 22px 8px 22px;
}
.title {
font-size: 22pt;
font-weight: bold;
text-align: center;
margin: 0 0 18px 0;
}
.heading1 {
font-size: 26pt;
font-weight: bold;
margin: 18px 0 10px 0;
}
.heading2 {
font-size: 18pt;
font-weight: bold;
margin: 14px 0 8px 0;
}
.heading3 {
font-size: 14pt;
font-weight: bold;
margin: 12px 0 6px 0;
}
.heading4,
.heading5,
.heading6 {
font-size: 11pt;
font-weight: bold;
margin: 10px 0 6px 0;
}
.paragraph {
margin: 0 0 12px 0;
color: #374151;
}
.field-block {
margin: 10px 0 14px 0;
page-break-inside: avoid;
}
.field-label {
font-size: 9pt;
font-weight: bold;
color: #475569;
text-transform: uppercase;
margin: 0 0 4px 0;
}
.input-box {
border: 1px solid #d1d5db;
background: #ffffff;
padding: 8px 10px;
min-height: 18px;
}
.input-placeholder {
color: #9ca3af;
}
.phone-table {
width: 100%;
border-collapse: collapse;
}
.phone-prefix {
width: 78px;
border: 1px solid #d1d5db;
background: #f8fafc;
padding: 8px 10px;
font-size: 11pt;
}
.phone-value {
border: 1px solid #d1d5db;
padding: 8px 10px;
font-size: 11pt;
}
.file-card {
border: 1px solid #d1d5db;
background: #f8fafc;
padding: 12px;
}
.preview-page {
margin: 8px 0;
border: 1px solid #d1d5db;
background: #ffffff;
text-align: center;
}
.preview-page img {
height: auto;
}
.preview-meta {
margin-top: 6px;
font-size: 8.5pt;
color: #64748b;
}
.empty-file {
border: 2px dashed #94a3b8;
background: #ffffff;
padding: 18px 12px;
text-align: center;
color: #64748b;
}
.signature-box {
border: 1px solid #d1d5db;
background: #ffffff;
height: 84px;
text-align: center;
vertical-align: middle;
}
.signature-box img {
max-width: 200px;
max-height: 70px;
margin: 6px auto;
}
.field-meta {
margin-top: 4px;
color: #6b7280;
font-size: 8.5pt;
}
</style>
</head>
<body>
<div class="page-shell">
<h1 class="title">{{ pdf_title }}</h1>
{% for field in fields %}
{% if field.kind == 'heading' %}
{% if field.level == 'heading1' %}
<div class="heading1">{{ field.text }}</div>
{% elif field.level == 'heading2' %}
<div class="heading2">{{ field.text }}</div>
{% elif field.level == 'heading3' %}
<div class="heading3">{{ field.text }}</div>
{% elif field.level == 'heading4' %}
<div class="heading4">{{ field.text }}</div>
{% elif field.level == 'heading5' %}
<div class="heading5">{{ field.text }}</div>
{% else %}
<div class="heading6">{{ field.text }}</div>
{% endif %}
{% elif field.kind == 'paragraph' %}
<p class="paragraph">{{ field.text }}</p>
{% elif field.kind == 'file' %}
<div class="field-block">
<div class="field-label">{{ field.label }}</div>
<div class="file-card">
{% if field.has_preview %}
{% for preview in field.preview_pages %}
<div class="preview-page">
<img src="{{ preview.src }}" alt="{{ preview.alt }}" width="{{ preview.width }}" height="{{ preview.height }}" />
</div>
{% endfor %}
{% if field.total_pages > 1 %}
<div class="preview-meta">Aperçu de la première page sur {{ field.total_pages }} pages</div>
{% endif %}
{% else %}
<div class="empty-file">Aucun document source fourni</div>
{% endif %}
</div>
</div>
{% elif field.kind == 'phone' %}
<div class="field-block">
<div class="field-label">{{ field.label }}</div>
<table class="phone-table">
<tr>
<td class="phone-prefix">{{ field.prefix }}</td>
<td class="phone-value">
{% if field.value %}
{{ field.value }}
{% else %}
<span class="input-placeholder">&nbsp;</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% elif field.kind == 'signature' %}
<div class="field-block">
<div class="field-label">{{ field.label }}</div>
<div class="signature-box">
{% if field.signature_src %}
<img src="{{ field.signature_src }}" alt="Signature" />
{% else %}
&nbsp;
{% endif %}
</div>
</div>
{% else %}
<div class="field-block">
<div class="field-label">{{ field.label }}</div>
<div class="input-box">
{% if field.value %}
{{ field.value }}
{% else %}
<span class="input-placeholder">&nbsp;</span>
{% endif %}
</div>
{% if field.field_type %}
<div class="field-meta">Type: {{ field.field_type }}</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</body>
</html>

View File

@ -1 +1 @@
__version__ = "0.0.3"
__version__ = "0.0.4"