WIP: validation du DUT120 (#577). Manque PVs.

This commit is contained in:
Emmanuel Viennet 2024-07-06 23:28:20 +02:00
parent 48e1207fd8
commit b14c3938b7
16 changed files with 484 additions and 23 deletions

View File

@ -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 <a href="{url}">{etud.nomprenom}</a>""",
text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
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/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/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"

View File

@ -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 = {}

109
app/but/jury_dut120.py Normal file
View File

@ -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/<int:etudid>/formsemestre/<int:formsemestre_id>",
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,
)

View File

@ -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(),

View File

@ -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

View File

@ -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"""<ValidationDUT120 {self.etud}>"""
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 <b>DUT en 120 ECTS du {specialite}</b> émis par
{link}
{date_str}
"""

View File

@ -179,7 +179,9 @@ class Identite(models.ScoDocModel):
def html_link_fiche(self) -> str:
"lien vers la fiche"
return f"""<a class="etudlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
return (
f"""<a class="etudlink" href="{self.url_fiche()}">{self.nom_prenom()}</a>"""
)
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.

View File

@ -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&nbsp; <a class="stdlink" href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=validation_dut120.formsemestre.id)
}">S{validation_dut120.formsemestre.semestre_id}</a>
"""
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,
)}
<div class="fiche_but_col2">
<div class="link_validation_rcues">
@ -471,7 +493,7 @@ def fiche_etud(etudid=None):
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="132px"/>
<div style="text-align: center;">Compétences BUT</div>
</a>
</div>

View File

@ -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);
}

View File

@ -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);

View File

@ -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(

View File

@ -30,4 +30,10 @@
</div>
{% endfor %}
{% endfor %}
{% if validation_dut120_html %}
<div class="validation_dut120"
title="DUT en 120 ECTS dans un BUT">
{{validation_dut120_html | safe }}
</div>
{% endif %}
</div>

View File

@ -0,0 +1,68 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<link href="{{scu.STATIC_DIR}}/css/jury_but.css" rel="stylesheet" type="text/css" />
{% endblock %}
{% block app_content %}
<div class="bull_head jury_but">
<div>
<div class="titre_parcours">Validation du DUT en 120 ECTS dans un parcours BUT</div>
<div class="nom_etud">{{etud.html_link_fiche()|safe}}</div>
</div>
<div class="bull_photo">
<a href="{{etud.url_fiche()|safe}}">
{{etud.photo_html(title="fiche de " + etud.nomprenom)|safe}}
</a>
</div>
</div>
<div class="help">
<p>Les étudiants de BUT peuvent demander lattribution du <em>diplôme universitaire de technologie</em>
(DUT) au terme de lacquisition des 120 premiers crédits européens du cursus.
</p>
<p>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).
</p>
</div>
<div style="margin-top: 16px;">
{{etud.html_link_fiche()|safe}} a acquis <b>{{ects_but1_but2}} ECTS</b> en BUT1 et BUT2.
{% if not validation %}
Son DUT n'est pas encore enregistré dans cette spécialité.
{% endif %}
</div>
{% if etud_can_validate_dut %}
<form method="POST" action="">
{{ form.hidden_tag() }}
<div style="margin-top: 24px;">
{{ form.submit() }}
</div>
</form>
{% else %}
<div class="warning">
{% if validation %}
DUT déjà validé dans cette spécialité
<b>{{formsemestre.formation.referentiel_competence.get_title()}}</b>
pour l'étudiant{{etud.ne}} {{etud.html_link_fiche()|safe}}
<ul>
<li>DUT 120 spécialité {{formsemestre.formation.referentiel_competence.specialite_long}}
enregistré le {{time.strftime("%d/%m/%Y à %Hh%M")}}
</li>
</ul>
{% else %}
L'étudiant ne satisfait pas les conditions&nbsp;:
vérifiez qu'il a validé et enregistré ses UEs de BUT1 et BUT2
et ainsi acquis 120 crédits ECTS.
Voir la page <a class="stdlink" href="{{url_for('notes.validation_rcues',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etud.id)
}}">compétences BUT</a>.
{% endif %}
{% endif %}
{% endblock %}

View File

@ -84,6 +84,22 @@ pages de saisie de jury habituelles).
</div>
{% endif %}
{% if dut120_vals.count() %}
<div class="jury_decisions_list jury_decisions_dut120">
<div>Diplôme de DUT en 120 ECTS (dans un parcours BUT)</div>
<ul>
{% for v in dut120_vals %}
<li>{{v.html()|safe}}
<form>
<button data-v_id="{{v.id}}" data-type="validation_dut120" data-etudid="{{etud.id}}"
>effacer</button>
</form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if autorisations.first() %}
<div class="jury_decisions_list jury_decisions_autorisation_inscription">
<div>Autorisations d'inscriptions (passages)</div>
@ -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');
}
});

View File

@ -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</a>
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.validate_dut120_etud",
scodoc_dept=g.scodoc_dept,
etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
>décerner le DUT "120ECTS"</a>
"""
H.append(
f"""<div class="but_settings">

View File

@ -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 ###