diff --git a/app/api/jury.py b/app/api/jury.py index 864dfe222..e2e29b7d6 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -32,6 +32,7 @@ from app.models import ( ScolarNews, Scolog, UniteEns, + ValidationDUT120, ) from app.scodoc import codes_cursus from app.scodoc import sco_cache @@ -62,7 +63,7 @@ def decisions_jury(formsemestre_id: int): raise ScoException("non implemente") -def _news_delete_jury_etud(etud: Identite): +def _news_delete_jury_etud(etud: Identite, detail: str = ""): "génère news sur effacement décision" # n'utilise pas g.scodoc_dept, pas toujours dispo en mode API url = url_for( @@ -71,7 +72,7 @@ def _news_delete_jury_etud(etud: Identite): ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=etud.id, - text=f"""Suppression décision jury pour {etud.nomprenom}""", + text=f"""Suppression décision jury {detail} pour {etud.nomprenom}""", url=url, ) @@ -320,11 +321,11 @@ def validation_rcue_delete(etudid: int, validation_id: int): validation = ApcValidationRCUE.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() - log(f"validation_ue_delete: etuid={etudid} {validation}") + log(f"delete validation_ue_delete: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() - _news_delete_jury_etud(etud) + _news_delete_jury_etud(etud, detail="UE") return "ok" @@ -348,9 +349,38 @@ def validation_annee_but_delete(etudid: int, validation_id: int): validation = ApcValidationAnnee.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() - log(f"validation_annee_but: etuid={etudid} {validation}") + ordre = validation.ordre + log(f"delete validation_annee_but: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() - _news_delete_jury_etud(etud) + _news_delete_jury_etud(etud, detail=f"année BUT{ordre}") + return "ok" + + +@bp.route( + "/etudiant//jury/validation_dut120//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_dut120//delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.EtudInscrit) +@as_json +def validation_dut120_delete(etudid: int, validation_id: int): + "Efface cette validation" + etud = tools.get_etud(etudid) + if etud is None: + return "étudiant inconnu", 404 + validation = ValidationDUT120.query.filter_by( + id=validation_id, etudid=etudid + ).first_or_404() + log(f"delete validation_dut120: etuid={etudid} {validation}") + db.session.delete(validation) + sco_cache.invalidate_formsemestre_etud(etud) + db.session.commit() + _news_delete_jury_etud(etud, detail="diplôme DUT120") return "ok" diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 83b921fbb..3a63a4db5 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7: """ import collections +from collections.abc import Iterable from operator import attrgetter from flask import g, url_for @@ -382,18 +383,28 @@ class FormSemestreCursusBUT: # "cache { competence_id : competence }" -def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> int: +def but_ects_valides( + etud: Identite, + referentiel_competence_id: int, + annees_but: None | Iterable[str] = None, +) -> int: """Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué. Ne prend que les UE associées à des niveaux de compétences, et ne les compte qu'une fois même en cas de redoublement avec re-validation. + Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années. """ validations = ( ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) .filter(ScolarFormSemestreValidation.ue_id != None) .join(UniteEns) .join(ApcNiveau) - .join(ApcCompetence) - .filter_by(referentiel_id=referentiel_competence_id) + ) + # restreint à certaines années (utile pour les ECTS du DUT120) + if annees_but: + validations = validations.filter(ApcNiveau.annee.in_(annees_but)) + # Et restreint au référentiel de compétence: + validations = validations.join(ApcCompetence).filter_by( + referentiel_id=referentiel_competence_id ) ects_dict = {} diff --git a/app/but/jury_dut120.py b/app/but/jury_dut120.py new file mode 100644 index 000000000..000314e80 --- /dev/null +++ b/app/but/jury_dut120.py @@ -0,0 +1,109 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury DUT120: gestion et vues + +Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS +de BUT 1 et BUT 2. + +""" +import time +from flask import flash, g, redirect, render_template, request, url_for +from flask_wtf import FlaskForm +from wtforms import SubmitField + +from app import db, log +from app.but import cursus_but +from app.decorators import scodoc, permission_required +from app.models.but_validations import ValidationDUT120 +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError +from app.scodoc.sco_permissions import Permission +from app.views import notes_bp as bp +from app.views import ScoData + + +def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool: + """Vrai si l'étudiant satisfait les conditions pour valider le DUT120""" + ects_but1_but2 = cursus_but.but_ects_valides( + etud, referentiel_competence_id, annees_but=("BUT1", "BUT2") + ) + return ects_but1_but2 >= 120 + + +class ValidationDUT120Form(FlaskForm): + "Formulaire validation DUT120" + submit = SubmitField("Enregistrer le diplôme DUT 120") + + +@bp.route( + "/validate_dut120/etudid//formsemestre/", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoView) +def validate_dut120_etud(etudid: int, formsemestre_id: int): + """Formulaire validation individuelle du DUT120""" + # Check arguments + etud = Identite.get_etud(etudid) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + refcomp = formsemestre.formation.referentiel_competence + if not refcomp: + raise ScoValueError("formation non associée à un référentiel de compétences") + # Permission + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + ects_but1_but2 = cursus_but.but_ects_valides( + etud, refcomp.id, annees_but=("BUT1", "BUT2") + ) + + form = ValidationDUT120Form() + # Check if ValidationDUT120 instance already exists + existing_validation = ValidationDUT120.query.filter_by( + etudid=etudid, referentiel_competence_id=refcomp.id + ).first() + if existing_validation: + flash("DUT120 déjà validé", "info") + etud_can_validate_dut = False + # Check if the student meets the criteria + elif ects_but1_but2 < 120: + flash("L'étudiant ne remplit pas les conditions", "warning") + etud_can_validate_dut = False # here existing_validation is None + else: + etud_can_validate_dut = True + + if etud_can_validate_dut and request.method == "POST" and form.validate_on_submit(): + new_validation = ValidationDUT120( + etudid=etudid, + referentiel_competence_id=refcomp.id, + formsemestre_id=formsemestre.id, # Replace with appropriate value + ) + db.session.add(new_validation) + db.session.commit() + log(f"ValidationDUT120 enregistrée pour {etud} depuis {formsemestre}") + flash("Validation DUT120 enregistrée", "success") + return redirect(etud.url_fiche()) + + return render_template( + "but/validate_dut120.j2", + ects_but1_but2=ects_but1_but2, + etud=etud, + etud_can_validate_dut=etud_can_validate_dut, + form=form, + formsemestre=formsemestre, + sco=ScoData(formsemestre=formsemestre, etud=etud), + time=time, + title="Délivrance du DUT", + validation=existing_validation, + ) diff --git a/app/but/jury_edit_manual.py b/app/but/jury_edit_manual.py index 29a81cedf..704376b8a 100644 --- a/app/but/jury_edit_manual.py +++ b/app/but/jury_edit_manual.py @@ -16,9 +16,10 @@ from app.models import ( ApcValidationAnnee, ApcValidationRCUE, Identite, - UniteEns, ScolarAutorisationInscription, ScolarFormSemestreValidation, + UniteEns, + ValidationDUT120, ) from app.views import ScoData @@ -60,6 +61,9 @@ def jury_delete_manual(etud: Identite): sem_vals=sem_vals, ue_vals=ue_vals, autorisations=autorisations, + dut120_vals=ValidationDUT120.query.filter_by(etudid=etud.id).order_by( + ValidationDUT120.date + ), rcue_vals=rcue_vals, annee_but_vals=annee_but_vals, sco=ScoData(), diff --git a/app/models/__init__.py b/app/models/__init__.py index 242615472..c7216e4b1 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,6 +3,7 @@ """Modèles base de données ScoDoc """ +from flask import abort, g import sqlalchemy from app import db @@ -116,6 +117,31 @@ class ScoDocModel(db.Model): args = {field.name: field.data for field in form} return self.from_dict(args) + @classmethod + def get_instance(cls, oid: int, accept_none=False): + """Instance du modèle ou ou 404 (ou None si accept_none), + cherche uniquement dans le département courant. + Ne fonctionne que si le modèle a un attribut dept_id. + Si accept_none, return None si l'id est invalide ou ne correspond + pas à une validation. + """ + if not isinstance(oid, int): + try: + oid = int(oid) + except (TypeError, ValueError): + if accept_none: + return None + abort(404, "oid invalide") + + query = ( + cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id) + if g.scodoc_dept + else cls.query.filter_by(id=oid) + ) + if accept_none: + return query.first() + return query.first_or_404() + from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.departements import Departement @@ -173,7 +199,11 @@ from app.models.but_refcomp import ( ApcReferentielCompetences, ApcSituationPro, ) -from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE +from app.models.but_validations import ( + ApcValidationAnnee, + ApcValidationRCUE, + ValidationDUT120, +) from app.models.config import ScoDocSiteConfig diff --git a/app/models/but_validations.py b/app/models/but_validations.py index e2e8692ce..31b7f6305 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -4,8 +4,10 @@ """ from collections import defaultdict +import sqlalchemy as sa + from app import db -from app.models import CODE_STR_LEN +from app.models import CODE_STR_LEN, ScoDocModel from app.models.but_refcomp import ApcNiveau from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre @@ -14,7 +16,7 @@ from app.scodoc import sco_preferences from app.scodoc import sco_utils as scu -class ApcValidationRCUE(db.Model): +class ApcValidationRCUE(ScoDocModel): """Validation des niveaux de compétences aka "regroupements cohérents d'UE" dans le jargon BUT. @@ -121,7 +123,7 @@ class ApcValidationRCUE(db.Model): return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue() -class ApcValidationAnnee(db.Model): +class ApcValidationAnnee(ScoDocModel): """Validation des années du BUT""" __tablename__ = "apc_validation_annee" @@ -277,3 +279,64 @@ def _build_decisions_rcue_list(decisions_rcue: dict) -> list[str]: ) ) return titres_rcues + + +class ValidationDUT120(ScoDocModel): + """Validations du DUT 120 + Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS + de BUT 1 et BUT 2. + """ + + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"), + nullable=False, + ) + """le semestre origine, dans la plupart des cas le S4 (le diplôme DUT120 + apparaîtra sur les PV de ce formsemestre)""" + referentiel_competence_id = db.Column( + db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False + ) # pas de cascade, on ne doit pas supprimer un référentiel utilisé + """Identifie la spécialité de DUT décernée""" + date = db.Column( + db.DateTime(timezone=True), server_default=db.func.now(), nullable=False + ) + """Date de délivrance""" + + etud = db.relationship("Identite", backref="validations_dut120") + formsemestre = db.relationship("FormSemestre", backref="validations_dut120") + + def __repr__(self): + return f"""""" + + def html(self) -> str: + "Affichage html" + date_str = ( + f"""le {self.date.strftime(scu.DATEATIME_FMT)}""" + if self.date + else "(sans date)" + ) + link = ( + self.formsemestre.html_link_status( + label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}", + title=self.formsemestre.titre_annee(), + ) + if self.formsemestre + else "externe/antérieure" + ) + specialite = ( + self.formsemestre.formation.referentiel_competence.get_title() + if self.formsemestre.formation.referentiel_competence + else "(désassociée!)" + ) + return f"""Diplôme de DUT en 120 ECTS du {specialite} émis par + {link} + {date_str} + """ diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 946b4f661..55a11dccf 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -179,7 +179,9 @@ class Identite(models.ScoDocModel): def html_link_fiche(self) -> str: "lien vers la fiche" - return f"""{self.nomprenom}""" + return ( + f"""{self.nom_prenom()}""" + ) def url_fiche(self) -> str: "url de la fiche étudiant" @@ -314,7 +316,7 @@ class Identite(models.ScoDocModel): @property def nomprenom(self, reverse=False) -> str: - """DEPRECATED + """DEPRECATED: préférer nom_prenom() Civilité/prénom/nom pour affichages: "M. Pierre Dupont" Si reverse, "Dupont Pierre", sans civilité. Prend l'identité courante et non celle de l'état civil si elles diffèrent. diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 94abc5ed4..0177cf483 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -37,7 +37,14 @@ import sqlalchemy as sa from app import log from app.auth.models import User from app.but import cursus_but, validations_view -from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig +from app.models import ( + Adresse, + EtudAnnotation, + FormSemestre, + Identite, + ScoDocSiteConfig, + ValidationDUT120, +) from app.scodoc import ( codes_cursus, html_sco_header, @@ -455,6 +462,20 @@ def fiche_etud(etudid=None): ects_total = sum((v.ects() for v in ue_validation_by_niveau.values())) else: ects_total = "" + + validation_dut120 = ValidationDUT120.query.filter_by(etudid=etudid).first() + validation_dut120_html = ( + f"""Diplôme DUT décerné + en  S{validation_dut120.formsemestre.semestre_id} + """ + if validation_dut120 + else "" + ) + info[ "but_cursus_mkup" ] = f""" @@ -463,6 +484,7 @@ def fiche_etud(etudid=None): "but/cursus_etud.j2", cursus=but_cursus, scu=scu, + validation_dut120_html=validation_dut120_html, )}
diff --git a/app/static/css/cursus_but.css b/app/static/css/cursus_but.css index 83438d250..340b8ff2a 100644 --- a/app/static/css/cursus_but.css +++ b/app/static/css/cursus_but.css @@ -40,6 +40,25 @@ div.code_rcue { position: relative; } +div.cursus_but .validation_dut120 { + grid-column: span 3; + justify-self: end; + /* align on right of BUT2 */ + width: auto; + /* fit its content */ + text-align: center; + padding: 6px; + font-weight: bold; + color: rgb(0, 0, 192); + background-color: #eee; + border-radius: 8px; + border: 1px solid rgb(0, 0, 192); +} + +div.validation_dut120 a.stdlink { + color: rgb(0, 0, 192); +} + div.no_niveau { background-color: rgb(245, 237, 200); } diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 815c12515..b3cc3d967 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -959,7 +959,7 @@ td.fichetitre2 .fl { div.section_but { display: flex; flex-direction: row; - align-items: flex-end; + align-items: center; justify-content: space-evenly; } @@ -974,7 +974,8 @@ div.fiche_total_etcs { margin-top: 16px; } -div.section_but>div.link_validation_rcues { +div.section_but div.link_validation_rcues, +div.section_but div.link_validation_rcues img { align-self: center; text-align: center; } @@ -4590,6 +4591,10 @@ table.table_recap tr td.jury_code_sem { border-left: 1px solid blue; } +table.table_recap tr td.col_jury_link { + border-left: 1px solid blue; +} + table.table_recap .admission { white-space: nowrap; color: rgb(6, 73, 6); diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py index 2cb25ede4..8c538ad66 100644 --- a/app/tables/jury_recap.py +++ b/app/tables/jury_recap.py @@ -18,7 +18,7 @@ from app.but import jury_but from app.but.jury_but import DecisionsProposeesRCUE from app.comp.res_compat import NotesTableCompat -from app.models import ApcNiveau, UniteEns +from app.models import ApcNiveau, UniteEns, ValidationDUT120 from app.models.etudiants import Identite from app.scodoc.codes_cursus import ( BUT_BARRE_RCUE, @@ -149,7 +149,7 @@ class TableJury(TableRecap): "ects_acquis", "ECTS", # res.get_etud_ects_valides(etud.id), - # cette recherche augmente de 10% le temps de contsruction de la table + # cette recherche augmente de 10% le temps de construction de la table cursus_but.but_ects_valides( etud, res.formsemestre.formation.referentiel_competence_id ), @@ -157,6 +157,16 @@ class TableJury(TableRecap): classes=["recorded_code"], target_attrs={"title": "crédits validés en BUT"}, ) + # Diplôme DUT120 + validation_dut120 = ValidationDUT120.query.filter_by(etudid=etud.id).first() + if validation_dut120: + row.add_cell( + "dut120", + "DUT", + "DUT", + group="jury_code_sem", + classes=["recorded_code"], + ) # Lien saisie ou visu jury a_saisir = (not res.validations) or (not res.validations.has_decision(etud)) row.add_cell( diff --git a/app/templates/but/cursus_etud.j2 b/app/templates/but/cursus_etud.j2 index 0a0782d6d..45330b185 100644 --- a/app/templates/but/cursus_etud.j2 +++ b/app/templates/but/cursus_etud.j2 @@ -30,4 +30,10 @@
{% endfor %} {% endfor %} + {% if validation_dut120_html %} +
+ {{validation_dut120_html | safe }} +
+ {% endif %} \ No newline at end of file diff --git a/app/templates/but/validate_dut120.j2 b/app/templates/but/validate_dut120.j2 new file mode 100644 index 000000000..3cfe91899 --- /dev/null +++ b/app/templates/but/validate_dut120.j2 @@ -0,0 +1,68 @@ +{% extends "sco_page.j2" %} + +{% block styles %} + {{super()}} + +{% endblock %} + + +{% block app_content %} + +
+
+
Validation du DUT en 120 ECTS dans un parcours BUT
+
{{etud.html_link_fiche()|safe}}
+
+ +
+ +
+

Les étudiants de BUT peuvent demander l’attribution du diplôme universitaire de technologie +(DUT) au terme de l’acquisition des 120 premiers crédits européens du cursus. +

+

Dans ScoDoc, c'est le référentiel de compétence qui détermine la spécialité (si un étudiant redouble dans +une formation utilisant une autre version de référentiel, pensez à revalider ses UEs). +

+
+ +
+ {{etud.html_link_fiche()|safe}} a acquis {{ects_but1_but2}} ECTS en BUT1 et BUT2. + {% if not validation %} + Son DUT n'est pas encore enregistré dans cette spécialité. + {% endif %} +
+ +{% if etud_can_validate_dut %} +
+ {{ form.hidden_tag() }} +
+ {{ form.submit() }} +
+
+{% else %} +
+ {% if validation %} + DUT déjà validé dans cette spécialité + {{formsemestre.formation.referentiel_competence.get_title()}} + pour l'étudiant{{etud.ne}} {{etud.html_link_fiche()|safe}} +
    +
  • DUT 120 spécialité {{formsemestre.formation.referentiel_competence.specialite_long}} + enregistré le {{time.strftime("%d/%m/%Y à %Hh%M")}} +
  • +
+ + {% else %} + L'étudiant ne satisfait pas les conditions : + vérifiez qu'il a validé et enregistré ses UEs de BUT1 et BUT2 + et ainsi acquis 120 crédits ECTS. + Voir la page compétences BUT. + + {% endif %} +{% endif %} +{% endblock %} diff --git a/app/templates/jury/jury_delete_manual.j2 b/app/templates/jury/jury_delete_manual.j2 index 3c53faed1..7eef80724 100644 --- a/app/templates/jury/jury_delete_manual.j2 +++ b/app/templates/jury/jury_delete_manual.j2 @@ -84,6 +84,22 @@ pages de saisie de jury habituelles).
{% endif %} +{% if dut120_vals.count() %} +
+
Diplôme de DUT en 120 ECTS (dans un parcours BUT)
+
    + {% for v in dut120_vals %} +
  • {{v.html()|safe}} +
    + +
    +
  • + {% endfor %} +
+
+{% endif %} + {% if autorisations.first() %}
Autorisations d'inscriptions (passages)
@@ -140,6 +156,7 @@ document.addEventListener('DOMContentLoaded', () => { if (response.ok) { location.reload(); } else { + console.log(`Error: ${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`); throw new Error('Request failed'); } }); diff --git a/app/views/notes.py b/app/views/notes.py index 9ab4e1999..9157af762 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -53,7 +53,7 @@ from app.but import ( jury_but_view, ) from app.but import bulletin_but_court # ne pas enlever: ajoute des routes ! - +from app.but import jury_dut120 # ne pas enlever: ajoute des routes ! from app.but.forms import jury_but_forms @@ -2568,6 +2568,13 @@ def formsemestre_validation_but( scodoc_dept=g.scodoc_dept, etudid=deca.etud.id, formsemestre_id=formsemestre_id)}" >enregistrer des UEs antérieures + + décerner le DUT "120ECTS" """ H.append( f"""
diff --git a/migrations/versions/9794534db935_validationdut120.py b/migrations/versions/9794534db935_validationdut120.py new file mode 100644 index 000000000..9273bc310 --- /dev/null +++ b/migrations/versions/9794534db935_validationdut120.py @@ -0,0 +1,58 @@ +"""ValidationDUT120 + +Revision ID: 9794534db935 +Revises: 60119446aab6 +Create Date: 2024-07-06 17:36:41.576748 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9794534db935" +down_revision = "60119446aab6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "validation_dut120", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=False), + sa.Column("referentiel_competence_id", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["referentiel_competence_id"], + ["apc_referentiel_competences.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("validation_dut120", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("ix_validation_dut120_etudid"), ["etudid"], unique=False + ) + + # ### end Alembic commands ### + + +def downgrade(): + with op.batch_alter_table("validation_dut120", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_validation_dut120_etudid")) + + op.drop_table("validation_dut120") + # ### end Alembic commands ###