diff --git a/app/api/assiduites.py b/app/api/assiduites.py index c679f910f7..f67e582ae0 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -594,6 +594,11 @@ def _create_singular( desc: str = data.get("desc", None) + external_data = data.get("external_data", False) + if external_data is not False: + if not isinstance(external_data, dict): + errors.append("param 'external_data' : n'est pas un objet JSON") + if errors: err: str = ", ".join(errors) return (404, err) @@ -608,6 +613,7 @@ def _create_singular( moduleimpl=moduleimpl, description=desc, user_id=current_user.id, + external_data=external_data, ) db.session.add(nouv_assiduite) @@ -738,6 +744,13 @@ def assiduite_edit(assiduite_id: int): else: assiduite_unique.est_just = est_just + external_data = data.get("external_data") + if external_data is not None: + if not isinstance(external_data, dict): + errors.append("param 'external_data' : n'est pas un objet JSON") + else: + assiduite_unique.external_data = external_data + if errors: err: str = ", ".join(errors) return json_error(404, err) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 3166130277..f12859fa25 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -248,6 +248,13 @@ def _create_singular( raison: str = data.get("raison", None) + external_data = data.get("external_data") + if external_data is not None: + if not isinstance(external_data, dict): + errors.append("param 'external_data' : n'est pas un objet JSON") + else: + assiduite_unique.external_data = external_data + if errors: err: str = ", ".join(errors) return (404, err, None) @@ -262,6 +269,7 @@ def _create_singular( etud=etud, raison=raison, user_id=current_user.id, + external_data=external_data, ) db.session.add(nouv_justificatif) @@ -346,6 +354,13 @@ def justif_edit(justif_id: int): deb = deb if deb is not None else justificatif_unique.date_debut fin = fin if fin is not None else justificatif_unique.date_fin + external_data = data.get("external_data") + if external_data is not None: + if not isinstance(external_data, dict): + errors.append("param 'external_data' : n'est pas un objet JSON") + else: + justificatif_unique.external_data = external_data + if fin <= deb: errors.append("param 'dates' : Date de début après date de fin") diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 6a42b34434..251bde7de4 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -59,6 +59,8 @@ class Assiduite(db.Model): est_just = db.Column(db.Boolean, server_default="false", nullable=False) + external_data = db.Column(db.JSON, nullable=True) + # Déclare la relation "joined" car on va très souvent vouloir récupérer # l'étudiant en même tant que l'assiduité (perf.: évite nouvelle requete SQL) etudiant = db.relationship("Identite", back_populates="assiduites", lazy="joined") @@ -88,6 +90,7 @@ class Assiduite(db.Model): "entry_date": self.entry_date, "user_id": username, "est_just": self.est_just, + "external_data": self.external_data, } return data @@ -117,6 +120,7 @@ class Assiduite(db.Model): entry_date: datetime = None, user_id: int = None, est_just: bool = False, + external_data: dict = None, ) -> object or int: """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes @@ -138,6 +142,7 @@ class Assiduite(db.Model): entry_date=entry_date, user_id=user_id, est_just=est_just, + external_data=external_data, ) else: raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") @@ -151,8 +156,9 @@ class Assiduite(db.Model): entry_date=entry_date, user_id=user_id, est_just=est_just, + external_data=external_data, ) - + db.session.add(nouv_assiduite) log(f"create_assiduite: {etud.id} {nouv_assiduite}") Scolog.logdb( method="create_assiduite", @@ -207,8 +213,13 @@ class Justificatif(db.Model): # Archive_id -> sco_archives_justificatifs.py fichier = db.Column(db.Text()) - # XXX Faudrait-il le déclarer "joined" comme dans Assiduite ? - etudiant = db.relationship("Identite", back_populates="justificatifs") + # Déclare la relation "joined" car on va très souvent vouloir récupérer + # l'étudiant en même tant que le justificatif (perf.: évite nouvelle requete SQL) + etudiant = db.relationship( + "Identite", back_populates="justificatifs", lazy="joined" + ) + + external_data = db.Column(db.JSON, nullable=True) def to_dict(self, format_api: bool = False) -> dict: """transformation de l'objet en dictionnaire sérialisable""" @@ -228,6 +239,7 @@ class Justificatif(db.Model): data = { "justif_id": self.justif_id, "etudid": self.etudid, + "code_nip": self.etudiant.code_nip, "date_debut": self.date_debut, "date_fin": self.date_fin, "etat": etat, @@ -235,6 +247,7 @@ class Justificatif(db.Model): "fichier": self.fichier, "entry_date": self.entry_date, "user_id": username, + "external_data": self.external_data, } return data @@ -260,6 +273,7 @@ class Justificatif(db.Model): raison: str = None, entry_date: datetime = None, user_id: int = None, + external_data: dict = None, ) -> object or int: """Créer un nouveau justificatif pour l'étudiant""" nouv_justificatif = Justificatif( @@ -270,7 +284,11 @@ class Justificatif(db.Model): raison=raison, entry_date=entry_date, user_id=user_id, + external_data=external_data, ) + + db.session.add(nouv_justificatif) + log(f"create_justificatif: {etud.id} {nouv_justificatif}") Scolog.logdb( method="create_justificatif", diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 0c1697b7d9..f8779cb0e3 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -269,7 +269,7 @@ function executeMassActionQueue() { }; assiduite = setModuleImplId(assiduite); - if (assiduite.moduleimpl_id == null && window.forceModule) { + if (!hasModuleImpl(assiduite) && window.forceModule) { const html = `

Aucun module n'a été spécifié

`; @@ -868,7 +868,7 @@ function createAssiduite(etat, etudid) { assiduite = setModuleImplId(assiduite); - if (assiduite.moduleimpl_id == null && window.forceModule) { + if (!hasModuleImpl(assiduite) && window.forceModule) { const html = `

Aucun module n'a été spécifié

`; @@ -922,6 +922,18 @@ function deleteAssiduite(assiduite_id) { return true; } +function hasModuleImpl(assiduite) { + if (assiduite.moduleimpl_id != null) return true; + if ( + "external_data" in assiduite && + assiduite.external_data instanceof Object && + "module" in assiduite.external_data + ) + return true; + + return false; +} + /** * * @param {String | Number} assiduite_id l'identifiant d'une assiduité @@ -929,14 +941,14 @@ function deleteAssiduite(assiduite_id) { * @returns {boolean} si l'édition a fonctionné * TODO : Rendre asynchrone */ -function editAssiduite(assiduite_id, etat) { +function editAssiduite(assiduite_id, etat, assi) { let assiduite = { etat: etat, - moduleimpl_id: getModuleImplId(), + external_data: assi ? assi.external_data : null, }; assiduite = setModuleImplId(assiduite); - if (assiduite.moduleimpl_id == null && window.forceModule) { + if (!hasModuleImpl(assiduite) && window.forceModule) { const html = `

Aucun module n'a été spécifié

`; @@ -1121,7 +1133,13 @@ function assiduiteAction(element) { if (etat === "remove") { done = deleteAssiduite(assiduite_id); } else { - done = editAssiduite(assiduite_id, etat); + done = editAssiduite( + assiduite_id, + etat, + assiduites[etudid].reduce((a) => { + if (a.assiduite_id == assiduite_id) return a; + }) + ); } break; case "conflit": @@ -1393,16 +1411,23 @@ function getModuleImplId() { function setModuleImplId(assiduite, module = null) { const moduleimpl = module == null ? getModuleImplId() : module; if (moduleimpl === "autre") { - if ("desc" in assiduite && assiduite["desc"] != null) { - if (assiduite["desc"].indexOf("Module:Autre") == -1) { - assiduite["desc"] = "Module:Autre\n" + assiduite["desc"]; + if ("external_data" in assiduite && assiduite.external_data != undefined) { + if ("module" in assiduite.external_data) { + assiduite.external_data.module = "Autre"; + } else { + assiduite["external_data"] = { module: "Autre" }; } } else { - assiduite["desc"] = "Module:Autre"; + assiduite["external_data"] = { module: "Autre" }; } - assiduite["moduleimpl_id"] = null; + assiduite.moduleimpl_id = null; } else { assiduite["moduleimpl_id"] = moduleimpl; + if ("external_data" in assiduite && assiduite.external_data != undefined) { + if ("module" in assiduite.external_data) { + delete assiduite.external_data.module; + } + } } return assiduite; } @@ -1451,11 +1476,11 @@ function getCurrentAssiduiteModuleImplId() { let mod = currentAssiduites[0].moduleimpl_id; if ( mod == null && - "desc" in currentAssiduites[0] && - currentAssiduites[0].desc != null && - currentAssiduites[0].desc.indexOf("Module:Autre") != -1 + "external_data" in currentAssiduites[0] && + currentAssiduites[0].external_data instanceof Object && + "module" in currentAssiduites[0].external_data ) { - mod = "autre"; + mod = currentAssiduites[0].external_data.module; } return mod == null ? "" : mod; } @@ -1665,11 +1690,11 @@ function getModuleImpl(assiduite) { if (id == null || id == undefined) { if ( - "desc" in assiduite && - assiduite.desc != null && - assiduite.desc.indexOf("Module:Autre") != -1 + "external_data" in assiduite && + assiduite.external_data instanceof Object && + "module" in assiduite.external_data ) { - return "Autre"; + return assiduite.external_data.module; } else { return "Pas de module"; } diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 0a133eb2da..596336e478 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -116,7 +116,7 @@ const entry_date = moment.tz(data.entry_date, TIMEZONE).format("DD/MM/YYYY HH:mm"); const etat = data.etat.capitalize(); - const desc = data.desc == null ? "" : data.desc.replace("Module:Autre\n", ""); + const desc = data.desc == null ? "" : data.desc; const id = data.assiduite_id; const est_just = data.est_just ? "Oui" : "Non"; @@ -185,12 +185,12 @@ (data) => { let module = data.moduleimpl_id; + if (module == null && "external_data" in data && "module" in data.external_data) { + module = data.external_data.module.toLowerCase(); + } + const etat = data.etat; let desc = data.desc == null ? "" : data.desc; - if (desc.indexOf("Module:Autre\n") != -1) { - desc = data.desc.replace("Module:Autre\n", ""); - module = "autre"; - } const html = `
@@ -230,6 +230,7 @@ let edit = { "etat": etat, "desc": desc, + "external_data": data.external_data } edit = setModuleImplId(edit, module); diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index 301fcc654e..e9fef33715 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -102,7 +102,12 @@ td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; } else { - td.textContent = `${justificatif[k]}`.capitalize() + if (justificatif[k] != null) { + td.textContent = `${justificatif[k]}`.capitalize() + } + else { + td.textContent = ""; + } } row.appendChild(td) @@ -295,7 +300,7 @@
` - const desc = data.raison + const desc = data.raison == null ? "" : data.raison; const fichier = data.fichier != null ? "Oui" : "Non"; @@ -304,7 +309,7 @@ const assiEdit = el.firstElementChild; assiEdit.querySelector('#justi_etat').value = data.etat.toLowerCase(); - assiEdit.querySelector('#justi_raison').value = desc != null ? desc : ""; + assiEdit.querySelector('#justi_raison').value = desc; assiEdit.querySelector('#justi_date_debut').value = moment.tz(data.date_debut, TIMEZONE).format("YYYY-MM-DDTHH:MM") assiEdit.querySelector('#justi_date_fin').value = moment.tz(data.date_fin, TIMEZONE).format("YYYY-MM-DDTHH:MM") diff --git a/migrations/versions/45e0a855b8eb_assiduites_external_data.py b/migrations/versions/45e0a855b8eb_assiduites_external_data.py new file mode 100644 index 0000000000..4b26e10264 --- /dev/null +++ b/migrations/versions/45e0a855b8eb_assiduites_external_data.py @@ -0,0 +1,33 @@ +"""assiduites_external_data + +Revision ID: 45e0a855b8eb +Revises: 50f7e0b6229f +Create Date: 2023-07-31 07:32:18.674345 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "45e0a855b8eb" +down_revision = "50f7e0b6229f" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("assiduites", schema=None) as batch_op: + batch_op.add_column(sa.Column("external_data", sa.JSON(), nullable=True)) + + with op.batch_alter_table("justificatifs", schema=None) as batch_op: + batch_op.add_column(sa.Column("external_data", sa.JSON(), nullable=True)) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("justificatifs", schema=None) as batch_op: + batch_op.drop_column("external_data") + + with op.batch_alter_table("assiduites", schema=None) as batch_op: + batch_op.drop_column("external_data") diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py index cc0c3c5fe6..c8581d7454 100644 --- a/tests/api/test_api_assiduites.py +++ b/tests/api/test_api_assiduites.py @@ -33,6 +33,7 @@ ASSIDUITES_FIELDS = { "entry_date": str, "user_id": str, "est_just": bool, + "external_data": dict, } CREATE_FIELD = {"assiduite_id": int} @@ -56,10 +57,14 @@ def check_fields(data: dict, fields: dict = None): fields = ASSIDUITES_FIELDS assert set(data.keys()) == set(fields.keys()) for key in data: - if key in ("moduleimpl_id", "desc", "user_id"): - assert isinstance(data[key], fields[key]) or data[key] is None + if key in ("moduleimpl_id", "desc", "user_id", "external_data"): + assert ( + isinstance(data[key], fields[key]) or data[key] is None + ), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]" else: - assert isinstance(data[key], fields[key]) + assert isinstance( + data[key], fields[key] + ), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]" def check_failure_get(path: str, headers: dict, err: str = None): diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 33c3710159..53e1b37005 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -25,6 +25,7 @@ FAUX = 42069 JUSTIFICATIFS_FIELDS = { "justif_id": int, "etudid": int, + "code_nip": str, "date_debut": str, "date_fin": str, "etat": str, @@ -32,6 +33,7 @@ JUSTIFICATIFS_FIELDS = { "entry_date": str, "fichier": str, "user_id": int, + "external_data": dict, } CREATE_FIELD = {"justif_id": int, "couverture": list} @@ -53,10 +55,14 @@ def check_fields(data, fields: dict = None): fields = JUSTIFICATIFS_FIELDS assert set(data.keys()) == set(fields.keys()) for key in data: - if key in ("raison", "fichier", "user_id"): - assert isinstance(data[key], fields[key]) or data[key] is None + if key in ("raison", "fichier", "user_id", "external_data"): + assert ( + isinstance(data[key], fields[key]) or data[key] is None + ), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]" else: - assert isinstance(data[key], fields[key]) + assert isinstance( + data[key], fields[key] + ), f"error [{key}:{type(data[key])}, {data[key]}, {fields[key]}]" def check_failure_get(path, headers, err=None): diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 79f7d430b1..a9205659e6 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -648,7 +648,7 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific # Justifications des assiduites - assert len(scass.justifies(justificatifs[2])) == 2, "Justifications mauvais" + assert len(scass.justifies(justificatifs[2])) == 1, "Justifications mauvais" assert len(scass.justifies(justificatifs[0])) == 0, "Justifications mauvais"