feat: Nommage des templates / Intégration dans formulaire d'inscription

parent [#22]
This commit is contained in:
N3WT DE COMPET
2025-03-01 22:08:00 +01:00
parent b52b265835
commit eb81bbba92
12 changed files with 142 additions and 81 deletions

View File

@ -58,15 +58,15 @@ def clone_template(request):
email = request.data.get('email') email = request.data.get('email')
# Vérifier les données requises # Vérifier les données requises
if not document_id or not email : if not document_id :
return Response({'error': 'template ID, email are required'}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST)
# URL de l'API de DocuSeal pour cloner le template # URL de l'API de DocuSeal pour cloner le template
clone_url = f'https://docuseal.com/api/templates/{document_id}/clone' clone_url = f'https://docuseal.com/api/templates/{document_id}/clone'
# Faire la requête pour cloner le template # Faire la requête pour cloner le template
try: try:
response = requests.post(clone_url, json={'submitters': [{'email': email}]}, headers={ response = requests.post(clone_url, headers={
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY'] 'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY']
}) })
@ -75,7 +75,27 @@ def clone_template(request):
return Response({'error': 'Failed to clone template'}, status=response.status_code) return Response({'error': 'Failed to clone template'}, status=response.status_code)
data = response.json() data = response.json()
return Response(data, status=status.HTTP_200_OK)
# URL de l'API de DocuSeal pour créer une submission
submission_url = f'https://docuseal.com/api/submissions'
# Faire la requête pour cloner le template
try:
clone_id = data['id']
response = requests.post(submission_url, json={'template_id':clone_id, 'send_email': False, 'submitters': [{'email': email}]}, headers={
'Content-Type': 'application/json',
'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY']
})
if response.status_code != status.HTTP_200_OK:
return Response({'error': 'Failed to create submission'}, status=response.status_code)
data = response.json()
data[0]['template_id'] = clone_id
return Response(data[0], status=status.HTTP_200_OK)
except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except requests.RequestException as e: except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -180,26 +180,6 @@ class RegistrationTemplateMaster(models.Model):
def __str__(self): def __str__(self):
return f'{self.group.name} - {self.template_id}' return f'{self.group.name} - {self.template_id}'
class RegistrationTemplate(models.Model):
master = models.ForeignKey(RegistrationTemplateMaster, on_delete=models.CASCADE, related_name='templates')
template_id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='templates')
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationTemplate.objects.filter(register_form_id=register_form_id).order_by('template__order')
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
class RegistrationForm(models.Model): class RegistrationForm(models.Model):
class RegistrationFormStatus(models.IntegerChoices): class RegistrationFormStatus(models.IntegerChoices):
RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_ABSENT = 0, _('Pas de dossier d\'inscription')
@ -238,3 +218,24 @@ class RegistrationForm(models.Model):
def __str__(self): def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name return "RF_" + self.student.last_name + "_" + self.student.first_name
class RegistrationTemplate(models.Model):
master = models.ForeignKey(RegistrationTemplateMaster, on_delete=models.CASCADE, related_name='templates')
template_id = models.IntegerField(primary_key=True)
slug = models.CharField(max_length=255, default="")
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='templates')
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationTemplate.objects.filter(register_form_id=register_form_id).order_by('template__order')
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames

View File

@ -28,10 +28,18 @@ class GuardianSimpleSerializer(serializers.ModelSerializer):
class RegistrationFormSimpleSerializer(serializers.ModelSerializer): class RegistrationFormSimpleSerializer(serializers.ModelSerializer):
guardians = GuardianSimpleSerializer(many=True, source='student.guardians') guardians = GuardianSimpleSerializer(many=True, source='student.guardians')
last_name = serializers.SerializerMethodField()
first_name = serializers.SerializerMethodField()
class Meta: class Meta:
model = RegistrationForm model = RegistrationForm
fields = ['student_id', 'guardians'] fields = ['student_id', 'last_name', 'first_name', 'guardians']
def get_last_name(self, obj):
return obj.student.last_name
def get_first_name(self, obj):
return obj.student.first_name
class RegistrationFileGroupSerializer(serializers.ModelSerializer): class RegistrationFileGroupSerializer(serializers.ModelSerializer):
registration_forms = serializers.SerializerMethodField() registration_forms = serializers.SerializerMethodField()

View File

@ -9,13 +9,14 @@ from .views import StudentView, GuardianView, ChildrenListView, StudentListView
# Files # Files
from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .views import registration_file_views from .views import registration_file_views, get_templates_by_rf
urlpatterns = [ urlpatterns = [
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"), re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"), re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"),
re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"), re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"),
re_path(r'^registerForms/(?P<id>[0-9]+)$', RegisterFormWithIdView.as_view(), name="registerForm"), re_path(r'^registerForms/(?P<id>[0-9]+)$', RegisterFormWithIdView.as_view(), name="registerForm"),
re_path(r'^registerForms/(?P<id>[0-9]+)/templates$', get_templates_by_rf, name="get_templates_by_rf"),
re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"), re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"),
# Page INSCRIPTION - Liste des élèves # Page INSCRIPTION - Liste des élèves

View File

@ -1,4 +1,4 @@
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_templates_by_rf
from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView from .student_views import StudentView, StudentListView, ChildrenListView
@ -17,6 +17,7 @@ __all__ = [
'RegistrationFileGroupView', 'RegistrationFileGroupView',
'RegistrationFileGroupSimpleView', 'RegistrationFileGroupSimpleView',
'get_registration_files_by_group', 'get_registration_files_by_group',
'get_templates_by_rf',
'StudentView', 'StudentView',
'StudentListView', 'StudentListView',
'ChildrenListView', 'ChildrenListView',

View File

@ -383,3 +383,23 @@ def resend(request,id):
return JsonResponse({"message": f"Le dossier a été renvoyé à l'adresse {email}"}, safe=False) return JsonResponse({"message": f"Le dossier a été renvoyé à l'adresse {email}"}, safe=False)
return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(
method='get',
responses={200: openapi.Response('Success', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING)
}
))},
operation_description="Récupère les fichiers à signer d'un dossier d'inscription donné",
operation_summary="Récupérer les fichiers à signer d'un dossier d'inscription donné"
)
@api_view(['GET'])
def get_templates_by_rf(request, id):
try:
templates = RegistrationTemplate.objects.filter(registration_form=id)
templates_data = list(templates.values())
return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist:
return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404)

View File

@ -118,8 +118,8 @@ class RegistrationFileGroupSimpleView(APIView):
def get_registration_files_by_group(request, id): def get_registration_files_by_group(request, id):
try: try:
group = RegistrationFileGroup.objects.get(id=id) group = RegistrationFileGroup.objects.get(id=id)
templates = RegistrationTemplateMaster.objects.filter(groups=group) templateMasters = RegistrationTemplateMaster.objects.filter(groups=group)
templates_data = list(templates.values()) templates_data = list(templateMasters.values())
return JsonResponse(templates_data, safe=False) return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist: except RegistrationFileGroup.DoesNotExist:
return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404) return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404)

View File

@ -391,6 +391,7 @@ useEffect(()=>{
// Sauvegarde des templates clonés dans la base de données // Sauvegarde des templates clonés dans la base de données
const cloneData = { const cloneData = {
name: `clone_${clonedDocument.id}`, name: `clone_${clonedDocument.id}`,
slug: clonedDocument.slug,
template_id: clonedDocument.id, template_id: clonedDocument.id,
master: templateMaster.template_id, master: templateMaster.template_id,
registration_form: data.student.id registration_form: data.student.id
@ -468,7 +469,8 @@ useEffect(()=>{
// Sauvegarde des templates clonés dans la base de données // Sauvegarde des templates clonés dans la base de données
const cloneData = { const cloneData = {
name: `clone_${clonedDocument.id}`, name: `clone_${clonedDocument.id}`,
template_id: clonedDocument.id, slug: clonedDocument.slug,
template_id: clonedDocument.template_id,
master: templateMaster.template_id, master: templateMaster.template_id,
registration_form: data.student.id registration_form: data.student.id
}; };

View File

@ -139,3 +139,16 @@ export async function getRegisterFormFileTemplate(fileId) {
} }
return response.json(); return response.json();
} }
export const fetchTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/templates`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error('Erreur lors de la récupération des fichiers associés au groupe');
}
return response.json();
}

View File

@ -7,7 +7,7 @@ import Loader from '@/components/Loader';
import Button from '@/components/Button'; import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { fetchRegistrationTemplateMaster, createRegistrationTemplates, fetchRegisterForm, deleteRegistrationTemplates } from '@/app/actions/subscriptionAction'; import { fetchRegistrationTemplateMaster, createRegistrationTemplates, fetchRegisterForm, deleteRegistrationTemplates, fetchTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
import { fetchRegistrationFileFromGroup } from '@/app/actions/registerFileGroupAction'; import { fetchRegistrationFileFromGroup } from '@/app/actions/registerFileGroupAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
@ -18,6 +18,7 @@ import logger from '@/utils/logger';
import StudentInfoForm from '@/components/Inscription/StudentInfoForm'; import StudentInfoForm from '@/components/Inscription/StudentInfoForm';
import FilesToSign from '@/components/Inscription/FilesToSign'; import FilesToSign from '@/components/Inscription/FilesToSign';
import FilesToUpload from '@/components/Inscription/FilesToUpload'; import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react';
/** /**
* Composant de formulaire d'inscription partagé * Composant de formulaire d'inscription partagé
@ -83,7 +84,6 @@ export default function InscriptionFormShared({
}); });
setGuardians(data?.student?.guardians || []); setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []); setUploadedFiles(data.registration_files || []);
setFileGroup(data.fileGroup || null);
}); });
setIsLoading(false); setIsLoading(false);
@ -91,12 +91,10 @@ export default function InscriptionFormShared({
}, [studentId]); }, [studentId]);
useEffect(() => { useEffect(() => {
if(fileGroup){ fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
fetchRegistrationFileFromGroup(fileGroup).then((data) => { setFileTemplates(data);
setFileTemplates(data); })
}); }, []);
}
}, [fileGroup]);
// Fonctions de gestion du formulaire et des fichiers // Fonctions de gestion du formulaire et des fichiers
const updateFormField = (field, value) => { const updateFormField = (field, value) => {
@ -190,12 +188,7 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1); setCurrentPage(currentPage - 1);
}; };
const requiredFileTemplates = fileTemplates.filter(template => template.is_required); const requiredFileTemplates = fileTemplates;
// Ajout des logs pour débogage
console.log('BASE_URL:', BASE_URL);
console.log('requiredFileTemplates:', requiredFileTemplates);
console.log('currentPage:', currentPage);
// Configuration des colonnes pour le tableau des fichiers // Configuration des colonnes pour le tableau des fichiers
const columns = [ const columns = [
@ -275,15 +268,18 @@ export default function InscriptionFormShared({
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && ( {currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2> <h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2>
<iframe <DocusealForm
src={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}`} id="docusealForm"
width="100%" src={"https://docuseal.com/s/"+requiredFileTemplates[currentPage - 2].slug}
height="800px" withDownloadButton={false}
className="w-full" // Utiliser la classe CSS pour la largeur onComplete={() => {
title={requiredFileTemplates[currentPage - 2].name} const formContainer = document.getElementById('form_container');
if (formContainer) {
formContainer.style.display = 'none';
}
}}
> >
<p>Votre navigateur ne prend pas en charge les fichiers PDF. Vous pouvez télécharger le fichier en cliquant <a href={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}`}>ici</a>.</p> </DocusealForm>
</iframe>
</div> </div>
)} )}

View File

@ -16,8 +16,7 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
const [templateMaster, setTemplateMaster] = useState(null); const [templateMaster, setTemplateMaster] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState(''); const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedGroups, setSelectedGroups] = useState([]); const [selectedGroups, setSelectedGroups] = useState([]);
const [guardianEmails, setGuardianEmails] = useState([]); const [guardianDetails, setGuardianDetails] = useState([]);
const [registrationFormIds, setRegistrationFormIds] = useState([]);
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
@ -61,25 +60,22 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
const handleGroupChange = (selectedGroups) => { const handleGroupChange = (selectedGroups) => {
setSelectedGroups(selectedGroups); setSelectedGroups(selectedGroups);
const emails = selectedGroups.flatMap(group => group.registration_forms.flatMap(form => form.guardians.map(guardian => guardian.email))); const details = selectedGroups.flatMap(group =>
setGuardianEmails(emails); // Mettre à jour la variable d'état avec les emails des guardians group.registration_forms.flatMap(form =>
form.guardians.map(guardian => ({
const registrationFormIds = selectedGroups.flatMap(group => group.registration_forms.map(form => form.student_id)); email: guardian.email,
setRegistrationFormIds(registrationFormIds); // Mettre à jour la variable d'état avec les IDs des dossiers d'inscription last_name: form.last_name,
first_name: form.first_name,
logger.debug('Emails des Guardians associés aux groupes sélectionnés:', emails); registration_form: form.student_id
logger.debug('IDs des dossiers d\'inscription associés aux groupes sélectionnés:', registrationFormIds); }))
)
);
setGuardianDetails(details); // Mettre à jour la variable d'état avec les détails des guardians
}; };
const handleLoad = (detail) => { const handleLoad = (detail) => {
const templateId = detail?.id; const templateId = detail?.id;
setTemplateMaster(detail); setTemplateMaster(detail);
if (fileToEdit) {
logger.debug('Editing master ID :', templateId);
}
else {
logger.debug('Opening master ID :', templateId);
}
} }
const handleUpload = (detail) => { const handleUpload = (detail) => {
@ -87,6 +83,11 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
setUploadedFileName(detail.name); setUploadedFileName(detail.name);
}; };
const handleChange = (detail) => {
logger.debug(detail)
setUploadedFileName(detail.name);
}
const handleSubmit = () => { const handleSubmit = () => {
if (fileToEdit) { if (fileToEdit) {
logger.debug('Modification du template master:', templateMaster?.id); logger.debug('Modification du template master:', templateMaster?.id);
@ -95,8 +96,7 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
group_ids: selectedGroups.map(group => group.id), group_ids: selectedGroups.map(group => group.id),
template_id: templateMaster?.id template_id: templateMaster?.id
}); });
} } else {
else {
logger.debug('Création du template master:', templateMaster?.id); logger.debug('Création du template master:', templateMaster?.id);
handleCreateTemplateMaster({ handleCreateTemplateMaster({
name: uploadedFileName, name: uploadedFileName,
@ -104,18 +104,17 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
template_id: templateMaster?.id template_id: templateMaster?.id
}); });
guardianEmails.forEach((email, index) => { guardianDetails.forEach((guardian, index) => {
cloneTemplate(templateMaster?.id, email) cloneTemplate(templateMaster?.id, guardian.email)
.then(clonedDocument => { .then(clonedDocument => {
// Sauvegarde des templates clonés dans la base de données // Sauvegarde des templates clonés dans la base de données
const data = { const data = {
name: `clone_${clonedDocument.id}`, name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`,
template_id: clonedDocument.id, slug: clonedDocument.slug,
template_id: clonedDocument.template_id,
master: templateMaster?.id, master: templateMaster?.id,
registration_form: registrationFormIds[index] registration_form: guardian.registration_form
}; };
createRegistrationTemplates(data, csrfToken) createRegistrationTemplates(data, csrfToken)
.then(response => { .then(response => {
logger.debug('Template enregistré avec succès:', response); logger.debug('Template enregistré avec succès:', response);
@ -125,16 +124,14 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
logger.error('Erreur lors de l\'enregistrement du template:', error); logger.error('Erreur lors de l\'enregistrement du template:', error);
}); });
// Logique pour envoyer chaque template au submitter // Logique pour envoyer chaque template au submitter
logger.debug('Sending template to:', email); logger.debug('Sending template to:', guardian.email);
}) })
.catch(error => { .catch(error => {
logger.error('Error during cloning or sending:', error); logger.error('Error during cloning or sending:', error);
}); });
}); });
} }
}; };
return ( return (
@ -164,6 +161,7 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
language={'fr'} language={'fr'}
onLoad={handleLoad} onLoad={handleLoad}
onUpload={handleUpload} onUpload={handleUpload}
onChange={handleChange}
onSave={handleSubmit} onSave={handleSubmit}
className="h-full overflow-auto" // Ajouter overflow-auto pour permettre le défilement className="h-full overflow-auto" // Ajouter overflow-auto pour permettre le défilement
style={{ maxHeight: '70vh' }} // Limiter la hauteur maximale du composant style={{ maxHeight: '70vh' }} // Limiter la hauteur maximale du composant

View File

@ -166,8 +166,9 @@ export default function FilesGroupsManagement({ csrfToken }) {
// Transformer le fichier mis à jour avec les informations du groupe // Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups); const transformedFile = transformFileData(data, groups);
setTemplateMasters(prevFichiers => setTemplateMasters(prevFichiers =>
prevFichiers.map(f => f.id === template_id ? transformedFile : f) prevFichiers.map(f => f.template_id === template_id ? transformedFile : f)
); );
setIsModalOpen(false);
}) })
.catch(error => { .catch(error => {
console.error('Error editing file:', error); console.error('Error editing file:', error);