mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-06-04 13:26:11 +00:00
Compare commits
5 Commits
90b0d14418
...
409cf05f1a
| Author | SHA1 | Date | |
|---|---|---|---|
| 409cf05f1a | |||
| b0e04e3adc | |||
| 3c7266608d | |||
| 5bbbcb9dc1 | |||
| 053140c8be |
@ -9,6 +9,7 @@ from .models import Planning, Events, RecursionType
|
|||||||
from .serializers import PlanningSerializer, EventsSerializer
|
from .serializers import PlanningSerializer, EventsSerializer
|
||||||
|
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
from Subscriptions.util import getCurrentSchoolYear
|
||||||
|
|
||||||
|
|
||||||
class PlanningView(APIView):
|
class PlanningView(APIView):
|
||||||
@ -17,6 +18,7 @@ class PlanningView(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
current_school_year = getCurrentSchoolYear()
|
||||||
|
|
||||||
plannings = bdd.getAllObjects(Planning)
|
plannings = bdd.getAllObjects(Planning)
|
||||||
|
|
||||||
@ -25,7 +27,10 @@ class PlanningView(APIView):
|
|||||||
|
|
||||||
# Filtrer en fonction du planning_mode
|
# Filtrer en fonction du planning_mode
|
||||||
if planning_mode == "classSchedule":
|
if planning_mode == "classSchedule":
|
||||||
plannings = plannings.filter(school_class__isnull=False)
|
plannings = plannings.filter(
|
||||||
|
school_class__isnull=False,
|
||||||
|
school_class__school_year=current_school_year,
|
||||||
|
)
|
||||||
elif planning_mode == "planning":
|
elif planning_mode == "planning":
|
||||||
plannings = plannings.filter(school_class__isnull=True)
|
plannings = plannings.filter(school_class__isnull=True)
|
||||||
|
|
||||||
@ -79,6 +84,7 @@ class EventsView(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
current_school_year = getCurrentSchoolYear()
|
||||||
filterParams = {}
|
filterParams = {}
|
||||||
plannings=[]
|
plannings=[]
|
||||||
events = Events.objects.all()
|
events = Events.objects.all()
|
||||||
@ -86,6 +92,8 @@ class EventsView(APIView):
|
|||||||
filterParams['establishment'] = establishment_id
|
filterParams['establishment'] = establishment_id
|
||||||
if planning_mode is not None:
|
if planning_mode is not None:
|
||||||
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
||||||
|
if planning_mode == "classSchedule":
|
||||||
|
filterParams['school_class__school_year'] = current_school_year
|
||||||
if filterParams:
|
if filterParams:
|
||||||
plannings = Planning.objects.filter(**filterParams)
|
plannings = Planning.objects.filter(**filterParams)
|
||||||
events = Events.objects.filter(planning__in=plannings)
|
events = Events.objects.filter(planning__in=plannings)
|
||||||
|
|||||||
@ -21,7 +21,6 @@ from N3wtSchool import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pytz
|
import pytz
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
from N3wtSchool.mailManager import sendRegisterForm
|
|
||||||
|
|
||||||
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
||||||
student_name = serializers.SerializerMethodField()
|
student_name = serializers.SerializerMethodField()
|
||||||
@ -228,14 +227,6 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
||||||
profile_role_serializer.is_valid(raise_exception=True)
|
profile_role_serializer.is_valid(raise_exception=True)
|
||||||
profile_role = profile_role_serializer.save()
|
profile_role = profile_role_serializer.save()
|
||||||
# Envoi du mail d'inscription si un nouveau profil vient d'être créé
|
|
||||||
email = None
|
|
||||||
if profile_data and 'email' in profile_data:
|
|
||||||
email = profile_data['email']
|
|
||||||
elif profile_role and profile_role.profile:
|
|
||||||
email = profile_role.profile.email
|
|
||||||
if email:
|
|
||||||
sendRegisterForm(email, establishment_id)
|
|
||||||
elif profile_role:
|
elif profile_role:
|
||||||
# Récupérer un ProfileRole existant par son ID
|
# Récupérer un ProfileRole existant par son ID
|
||||||
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { getCurrentSchoolYear } from '@/utils/Date';
|
||||||
|
|
||||||
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
|
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
|
||||||
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
|
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
|
||||||
@ -54,6 +55,17 @@ export default function Page() {
|
|||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
|
const currentSchoolYear = getCurrentSchoolYear();
|
||||||
|
|
||||||
|
const scheduleClasses = classes.filter(
|
||||||
|
(classe) => classe?.school_year === currentSchoolYear
|
||||||
|
);
|
||||||
|
const scheduleSpecialities = specialities.filter(
|
||||||
|
(speciality) => speciality?.school_year === currentSchoolYear
|
||||||
|
);
|
||||||
|
const scheduleTeachers = teachers.filter(
|
||||||
|
(teacher) => teacher?.school_year === currentSchoolYear
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
@ -299,9 +311,9 @@ export default function Page() {
|
|||||||
<ClassesProvider>
|
<ClassesProvider>
|
||||||
<ScheduleManagement
|
<ScheduleManagement
|
||||||
handleUpdatePlanning={handleUpdatePlanning}
|
handleUpdatePlanning={handleUpdatePlanning}
|
||||||
classes={classes}
|
classes={scheduleClasses}
|
||||||
specialities={specialities}
|
specialities={scheduleSpecialities}
|
||||||
teachers={teachers}
|
teachers={scheduleTeachers}
|
||||||
/>
|
/>
|
||||||
</ClassesProvider>
|
</ClassesProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -55,6 +55,11 @@ export default function DynamicFormsList({
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDynamicForm = (template) =>
|
||||||
|
template.formTemplateData &&
|
||||||
|
Array.isArray(template.formTemplateData.fields) &&
|
||||||
|
template.formTemplateData.fields.length > 0;
|
||||||
|
|
||||||
const hasLocalCompletion = (templateId) => {
|
const hasLocalCompletion = (templateId) => {
|
||||||
if (formsValidation[templateId] === true) return true;
|
if (formsValidation[templateId] === true) return true;
|
||||||
|
|
||||||
@ -65,11 +70,22 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const savedResponses = existingResponses[templateId];
|
const savedResponses = existingResponses[templateId];
|
||||||
return !!(
|
if (
|
||||||
savedResponses &&
|
savedResponses &&
|
||||||
typeof savedResponses === 'object' &&
|
typeof savedResponses === 'object' &&
|
||||||
Object.keys(savedResponses).length > 0
|
Object.keys(savedResponses).length > 0
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les formulaires non dynamiques (upload de fichier),
|
||||||
|
// vérifier si un fichier a déjà été uploadé sur le template
|
||||||
|
const template = schoolFileTemplates.find((tpl) => tpl.id === templateId);
|
||||||
|
if (template && template.file && !isDynamicForm(template)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialiser les données avec les réponses existantes
|
// Initialiser les données avec les réponses existantes
|
||||||
@ -233,11 +249,6 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDynamicForm = (template) =>
|
|
||||||
template.formTemplateData &&
|
|
||||||
Array.isArray(template.formTemplateData.fields) &&
|
|
||||||
template.formTemplateData.fields.length > 0;
|
|
||||||
|
|
||||||
if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
|
if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
@ -497,10 +508,10 @@ export default function DynamicFormsList({
|
|||||||
{/* Cas non validé : bouton télécharger + upload */}
|
{/* Cas non validé : bouton télécharger + upload */}
|
||||||
{currentTemplate.isValidated !== true && (
|
{currentTemplate.isValidated !== true && (
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
{/* Bouton télécharger le document source */}
|
{/* Bouton télécharger le document source (fichier maître) */}
|
||||||
{currentTemplate.file && (
|
{(currentTemplate.master_file_url || currentTemplate.file) && (
|
||||||
<a
|
<a
|
||||||
href={getSecureFileUrl(currentTemplate.file)}
|
href={getSecureFileUrl(currentTemplate.master_file_url || currentTemplate.file)}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||||
download
|
download
|
||||||
>
|
>
|
||||||
@ -517,6 +528,7 @@ export default function DynamicFormsList({
|
|||||||
onFileSelect={(file) =>
|
onFileSelect={(file) =>
|
||||||
handleUpload(file, currentTemplate)
|
handleUpload(file, currentTemplate)
|
||||||
}
|
}
|
||||||
|
existingFile={currentTemplate.file_url || currentTemplate.file}
|
||||||
required
|
required
|
||||||
enable={true}
|
enable={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -67,11 +67,17 @@ const FilesModal = ({
|
|||||||
: null,
|
: null,
|
||||||
schoolFiles: fetchedSchoolFiles.map((file) => ({
|
schoolFiles: fetchedSchoolFiles.map((file) => ({
|
||||||
name: file.name || 'Document scolaire',
|
name: file.name || 'Document scolaire',
|
||||||
url: file.file ? getSecureFileUrl(file.file) : null,
|
url:
|
||||||
|
file.file_url || file.file
|
||||||
|
? getSecureFileUrl(file.file_url || file.file)
|
||||||
|
: null,
|
||||||
})),
|
})),
|
||||||
parentFiles: parentFiles.map((file) => ({
|
parentFiles: parentFiles.map((file) => ({
|
||||||
name: file.master_name || 'Document parent',
|
name: file.master_name || 'Document parent',
|
||||||
url: file.file ? getSecureFileUrl(file.file) : null,
|
url:
|
||||||
|
file.file_url || file.file
|
||||||
|
? getSecureFileUrl(file.file_url || file.file)
|
||||||
|
: null,
|
||||||
})),
|
})),
|
||||||
sepaFile: selectedRegisterForm.sepa_file
|
sepaFile: selectedRegisterForm.sepa_file
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import {
|
import {
|
||||||
editRegistrationSchoolFileTemplates,
|
editRegistrationSchoolFileTemplates,
|
||||||
|
editRegistrationParentFileTemplates,
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
import {
|
import {
|
||||||
fetchRegistrationPaymentModes,
|
fetchRegistrationPaymentModes,
|
||||||
@ -415,7 +416,8 @@ export default function InscriptionFormShared({
|
|||||||
const templateData = await fetchFormResponses(template.id);
|
const templateData = await fetchFormResponses(template.id);
|
||||||
if (templateData && templateData.formTemplateData) {
|
if (templateData && templateData.formTemplateData) {
|
||||||
if (templateData.formTemplateData.responses) {
|
if (templateData.formTemplateData.responses) {
|
||||||
responsesMap[template.id] = templateData.formTemplateData.responses;
|
responsesMap[template.id] =
|
||||||
|
templateData.formTemplateData.responses;
|
||||||
} else {
|
} else {
|
||||||
// Extraire les réponses depuis les champs
|
// Extraire les réponses depuis les champs
|
||||||
const responses = {};
|
const responses = {};
|
||||||
@ -554,7 +556,7 @@ export default function InscriptionFormShared({
|
|||||||
const updateData = new FormData();
|
const updateData = new FormData();
|
||||||
updateData.append('file', file, finalFileName);
|
updateData.append('file', file, finalFileName);
|
||||||
|
|
||||||
return editRegistrationSchoolFileTemplates(
|
return editRegistrationParentFileTemplates(
|
||||||
selectedFile.id,
|
selectedFile.id,
|
||||||
updateData,
|
updateData,
|
||||||
csrfToken
|
csrfToken
|
||||||
@ -596,6 +598,46 @@ export default function InscriptionFormShared({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSchoolFileUpload = (file, selectedFile) => {
|
||||||
|
if (!file || !selectedFile) {
|
||||||
|
logger.error('Données manquantes pour le téléversement.');
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('Données manquantes pour le téléversement.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = new FormData();
|
||||||
|
updateData.append('file', file);
|
||||||
|
|
||||||
|
return editRegistrationSchoolFileTemplates(
|
||||||
|
selectedFile.id,
|
||||||
|
updateData,
|
||||||
|
csrfToken
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
logger.debug('School file template mis à jour avec succès :', response);
|
||||||
|
|
||||||
|
// Mettre à jour uniquement schoolFileTemplates
|
||||||
|
setSchoolFileTemplates((prevTemplates) =>
|
||||||
|
prevTemplates.map((template) =>
|
||||||
|
template.id === selectedFile.id
|
||||||
|
? {
|
||||||
|
...template,
|
||||||
|
file: response.data.file,
|
||||||
|
file_url: response.data.file_url,
|
||||||
|
}
|
||||||
|
: template
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Erreur lors de la mise à jour du school file :', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteFile = (templateId) => {
|
const handleDeleteFile = (templateId) => {
|
||||||
const fileToDelete = uploadedFiles.find(
|
const fileToDelete = uploadedFiles.find(
|
||||||
(file) => parseInt(file.id) === templateId && file.fileName
|
(file) => parseInt(file.id) === templateId && file.fileName
|
||||||
@ -827,7 +869,7 @@ export default function InscriptionFormShared({
|
|||||||
onFormSubmit={handleDynamicFormSubmit}
|
onFormSubmit={handleDynamicFormSubmit}
|
||||||
onValidationChange={handleDynamicFormsValidationChange}
|
onValidationChange={handleDynamicFormsValidationChange}
|
||||||
enable={enable}
|
enable={enable}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleSchoolFileUpload}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession, getSession } from 'next-auth/react';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { WS_CHAT_URL } from '@/utils/Url';
|
import { WS_CHAT_URL } from '@/utils/Url';
|
||||||
|
|
||||||
@ -54,14 +54,15 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Configuration WebSocket
|
// Configuration WebSocket
|
||||||
const getWebSocketUrl = (userId) => {
|
const getWebSocketUrl = async (userId) => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
logger.warn('ChatConnection: No user ID provided for WebSocket URL');
|
logger.warn('ChatConnection: No user ID provided for WebSocket URL');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer le token d'authentification depuis NextAuth session
|
// Forcer un refresh de session pour obtenir un token JWT valide
|
||||||
const token = session?.user?.token;
|
const freshSession = await getSession();
|
||||||
|
const token = freshSession?.user?.token;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@ -78,7 +79,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Connexion WebSocket
|
// Connexion WebSocket
|
||||||
const connectToChat = (userId = null) => {
|
const connectToChat = async (userId = null) => {
|
||||||
const userIdToUse = userId || currentUserId;
|
const userIdToUse = userId || currentUserId;
|
||||||
|
|
||||||
// Vérifier que la session est chargée
|
// Vérifier que la session est chargée
|
||||||
@ -107,7 +108,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = getWebSocketUrl(userIdToUse);
|
const wsUrl = await getWebSocketUrl(userIdToUse);
|
||||||
|
|
||||||
if (!wsUrl) {
|
if (!wsUrl) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -27,5 +27,13 @@ export const getSecureFileUrl = (filePath) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Décoder le chemin au cas où il est déjà URL-encodé (ex: %20 pour les espaces)
|
||||||
|
// puis ré-encoder proprement pour éviter le double encodage (%2520).
|
||||||
|
try {
|
||||||
|
filePath = decodeURIComponent(filePath);
|
||||||
|
} catch {
|
||||||
|
// Si le décodage échoue, le chemin n'était pas encodé : on le garde tel quel.
|
||||||
|
}
|
||||||
|
|
||||||
return `/api/download?path=${encodeURIComponent(filePath)}`;
|
return `/api/download?path=${encodeURIComponent(filePath)}`;
|
||||||
};
|
};
|
||||||
|
|||||||
21
conf/create-establishment.sample.json
Normal file
21
conf/create-establishment.sample.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"establishments": [
|
||||||
|
{
|
||||||
|
"name": "Ecole Demo",
|
||||||
|
"address": "1 rue de la Paix, Paris",
|
||||||
|
"total_capacity": 300,
|
||||||
|
"establishment_type": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"evaluation_frequency": 1,
|
||||||
|
"licence_code": "LIC-DEMO-001",
|
||||||
|
"directeur": {
|
||||||
|
"email": "luc.sorignet@gmail.com",
|
||||||
|
"password": "a",
|
||||||
|
"last_name": "Dupont",
|
||||||
|
"first_name": "Jean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user