forked from ScoDoc/ScoDoc
WIP: codes Apo et EDT sur chaque modimpl. (pas encore utilisé dans exports Apo).
This commit is contained in:
parent
d1bc546d7b
commit
0f61b0874a
52
app/forms/formsemestre/edit_modimpls_codes_apo.py
Normal file
52
app/forms/formsemestre/edit_modimpls_codes_apo.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Formulaire configuration des codes Apo et EDT des modimps d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import validators
|
||||||
|
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
||||||
|
|
||||||
|
from app.models import FormSemestre, ModuleImpl
|
||||||
|
|
||||||
|
|
||||||
|
class _EditModimplsCodesForm(FlaskForm):
|
||||||
|
"form. définition des liens personnalisés"
|
||||||
|
# construit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
|
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
|
||||||
|
"Création d'un formulaire pour éditer les codes"
|
||||||
|
|
||||||
|
# Formulaire dynamique, on créé une classe ad-hoc
|
||||||
|
class F(_EditModimplsCodesForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _gen_mod_form(modimpl: ModuleImpl):
|
||||||
|
field = StringField(
|
||||||
|
modimpl.module.code,
|
||||||
|
validators=[
|
||||||
|
validators.Optional(),
|
||||||
|
validators.Length(min=1, max=80),
|
||||||
|
],
|
||||||
|
default="",
|
||||||
|
render_kw={"size": 32},
|
||||||
|
)
|
||||||
|
setattr(F, f"modimpl_apo_{modimpl.id}", field)
|
||||||
|
field = StringField(
|
||||||
|
"",
|
||||||
|
validators=[
|
||||||
|
validators.Optional(),
|
||||||
|
validators.Length(min=1, max=80),
|
||||||
|
],
|
||||||
|
default="",
|
||||||
|
render_kw={"size": 12},
|
||||||
|
)
|
||||||
|
setattr(F, f"modimpl_edt_{modimpl.id}", field)
|
||||||
|
|
||||||
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
|
_gen_mod_form(modimpl)
|
||||||
|
|
||||||
|
F.submit = SubmitField("Valider")
|
||||||
|
F.cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
return F()
|
@ -2,9 +2,8 @@
|
|||||||
Formulaire configuration liens personalisés (menu "Liens")
|
Formulaire configuration liens personalisés (menu "Liens")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, url_for
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import FieldList, Form, validators
|
from wtforms import validators
|
||||||
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
||||||
|
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
|
@ -7,6 +7,7 @@ from flask_sqlalchemy.query import Query
|
|||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||||
@ -21,6 +22,10 @@ class ModuleImpl(db.Model):
|
|||||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
code_apogee = db.Column(db.String(APO_CODE_STR_LEN), index=True, nullable=True)
|
||||||
|
"id de l'element pedagogique Apogee correspondant"
|
||||||
|
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||||
|
"identifiant emplois du temps (unicité non imposée)"
|
||||||
moduleimpl_id = db.synonym("id")
|
moduleimpl_id = db.synonym("id")
|
||||||
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
@ -45,11 +50,21 @@ class ModuleImpl(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||||
|
|
||||||
|
def get_codes_apogee(self) -> set[str]:
|
||||||
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2").
|
||||||
|
(si non renseigné, ceux du module)
|
||||||
|
"""
|
||||||
|
if self.code_apogee:
|
||||||
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
|
return self.module.get_codes_apogee()
|
||||||
|
|
||||||
def get_edt_id(self) -> str:
|
def get_edt_id(self) -> str:
|
||||||
"l'id pour l'emploi du temps: actuellement celui du module"
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
return (
|
return (
|
||||||
self.module.get_edt_id()
|
self.edt_id
|
||||||
) # TODO à décliner pour autoriser des codes différents ?
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
|
or self.module.get_edt_id()
|
||||||
|
)
|
||||||
|
|
||||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||||
@ -102,6 +117,7 @@ class ModuleImpl(db.Model):
|
|||||||
d["module"] = self.module.to_dict(convert_objects=convert_objects)
|
d["module"] = self.module.to_dict(convert_objects=convert_objects)
|
||||||
else:
|
else:
|
||||||
d.pop("module", None)
|
d.pop("module", None)
|
||||||
|
d["code_apogee"] = d["code_apogee"] or "" # pas de None
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def can_edit_evaluation(self, user) -> bool:
|
def can_edit_evaluation(self, user) -> bool:
|
||||||
|
@ -464,6 +464,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||||
}">Modifier les coefficients des UE capitalisées</a>
|
}">Modifier les coefficients des UE capitalisées</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p><a class="stdlink" href="{url_for("notes.formsemestre_edit_modimpls_codes",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||||
|
}">Modifier les codes Apogée et emploi du temps des modules</a>
|
||||||
|
</p>
|
||||||
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
||||||
à inscrire:</h3>
|
à inscrire:</h3>
|
||||||
"""
|
"""
|
||||||
|
@ -11,11 +11,11 @@
|
|||||||
<h2>Changement de la formation du semestre</h2>
|
<h2>Changement de la formation du semestre</h2>
|
||||||
|
|
||||||
<p class="help"> On ne peut pas changer la formation d'un semestre existant car
|
<p class="help"> On ne peut pas changer la formation d'un semestre existant car
|
||||||
elle défini son organisation (modules, ...), SAUF si la nouvelle formation a
|
elle définit son organisation (modules, ...), SAUF si la nouvelle formation a
|
||||||
<em>exactement</em> le même contenu que l'existante.
|
<em>exactement</em> le même contenu que l'existante.
|
||||||
Cela peut arriver par exemple lorsqu'on crée une nouvelle version (pas encore modifiée)
|
Cela peut arriver par exemple lorsqu'on crée une nouvelle version (pas encore modifiée)
|
||||||
et que l'on a oublié d'y rattacher un semestre.
|
et que l'on a oublié d'y rattacher un semestre.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if formations %}
|
{% if formations %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -27,4 +27,4 @@ et que l'on a oublié d'y rattacher un semestre.
|
|||||||
<div class="fontred">Aucune formation ne peut se substituer à celle de ce semestre.</div>
|
<div class="fontred">Aucune formation ne peut se substituer à celle de ce semestre.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
91
app/templates/formsemestre/edit_modimpls_codes.j2
Normal file
91
app/templates/formsemestre/edit_modimpls_codes.j2
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
<style>
|
||||||
|
span.mod-label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
#mf .mod-label label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
.field-apo {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.field-edt {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
#mf input {
|
||||||
|
display: inline;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro render_text_field(field_apo, field_edt, codes_apo_module) %}
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="mod-label">
|
||||||
|
{{ field_apo.label(class_="form-label") }}
|
||||||
|
<span class="code-apo-module">{{codes_apo_module|join(", ") or ("<em>non défini</em>"|safe)}}</span>
|
||||||
|
</span>
|
||||||
|
<span class="field-apo">{{field_apo(class_="form-field")}}</span>
|
||||||
|
<span class="field-edt">{{field_edt(class_="field-edt")}}</span>
|
||||||
|
{%- for error in field_apo.errors %}
|
||||||
|
<span class="form-error">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{%- for error in field_edt.errors %}
|
||||||
|
<span class="form-error">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<h2>Codes Apogée et emploi du temps des modules du semestre</h2>
|
||||||
|
|
||||||
|
<p class="help">Les codes élément Apogée sont utilisés pour les exports des
|
||||||
|
résultats et peuvent aussi l'être pour connecter l'emploi du temps. Si votre
|
||||||
|
logiciel d'emploi du temps utilise des codes différents, vous pouvez aussi
|
||||||
|
indiquer un code EDT spécifique.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="help">Les codes Apogée modules rappelés à gauche sont ceux définis
|
||||||
|
dans la formation: il sont utilisés sauf si on spécifie un code ici.
|
||||||
|
Pour les modifier, aller dans l'édition de la formation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<form id="mf" class="form form-horizontal" method="post" enctype="multipart/form-data" role="form">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ wtf.form_errors(form, hiddens="only") }}
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="mod-label">
|
||||||
|
<label>Module</label>
|
||||||
|
<span class="code-apo-module">Code Apo. Module</span>
|
||||||
|
<span class="field-apo">Code(s) Apogée</span>
|
||||||
|
<span class="field-edt">Code EDT</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% for modimpl in formsemestre.modimpls_sorted %}
|
||||||
|
{{ render_text_field(form["modimpl_apo_" ~ modimpl.id], form["modimpl_edt_" ~ modimpl.id], modimpl.module.get_codes_apogee()) }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="form-group">
|
||||||
|
{{ wtf.form_field(form.submit) }}
|
||||||
|
{{ wtf.form_field(form.cancel) }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -32,11 +32,12 @@ Emmanuel Viennet, 2023
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
|
|
||||||
|
from app import db, log
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
scodoc,
|
scodoc,
|
||||||
permission_required,
|
permission_required,
|
||||||
)
|
)
|
||||||
from app.forms.formsemestre import change_formation
|
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
|
||||||
from app.models import Formation, FormSemestre
|
from app.models import Formation, FormSemestre
|
||||||
from app.scodoc import sco_formations, sco_formation_versions
|
from app.scodoc import sco_formations, sco_formation_versions
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -107,6 +108,50 @@ def formsemestre_change_formation(formsemestre_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/formsemestre_edit_modimpls_codes/<int:formsemestre_id>", methods=["GET", "POST"]
|
||||||
|
)
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EditFormSemestre)
|
||||||
|
def formsemestre_edit_modimpls_codes(formsemestre_id: int):
|
||||||
|
"""Edition des codes Apogée et EDT"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
form = edit_modimpls_codes_apo.EditModimplsCodesForm(formsemestre)
|
||||||
|
|
||||||
|
if request.method == "POST" and form.validate:
|
||||||
|
if not form.cancel.data:
|
||||||
|
# record codes
|
||||||
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
|
field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
|
||||||
|
field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
|
||||||
|
if field_apo and field_edt:
|
||||||
|
modimpl.code_apogee = field_apo.data.strip() or None
|
||||||
|
modimpl.edt_id = field_edt.data.strip() or None
|
||||||
|
log(f"setting codes for {modimpl}: apo={field_apo} edt={field_edt}")
|
||||||
|
db.session.add(modimpl)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Codes enregistrés")
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# GET
|
||||||
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
|
field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
|
||||||
|
field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
|
||||||
|
field_apo.data = modimpl.code_apogee or ""
|
||||||
|
field_edt.data = modimpl.edt_id or ""
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/edit_modimpls_codes.j2",
|
||||||
|
form=form,
|
||||||
|
formsemestre=formsemestre,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/edt/<int:formsemestre_id>")
|
@bp.route("/formsemestre/edt/<int:formsemestre_id>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
|
38
migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py
Normal file
38
migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""code apo sur modimpls
|
||||||
|
|
||||||
|
Revision ID: c8f66652c77f
|
||||||
|
Revises: 6fb956addd69
|
||||||
|
Create Date: 2023-11-12 10:01:42.424734
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "c8f66652c77f"
|
||||||
|
down_revision = "6fb956addd69"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column("code_apogee", sa.String(length=512), nullable=True)
|
||||||
|
)
|
||||||
|
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
|
||||||
|
batch_op.create_index(
|
||||||
|
batch_op.f("ix_notes_moduleimpl_code_apogee"), ["code_apogee"], unique=False
|
||||||
|
)
|
||||||
|
batch_op.create_index(
|
||||||
|
batch_op.f("ix_notes_moduleimpl_edt_id"), ["edt_id"], unique=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_edt_id"))
|
||||||
|
batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_code_apogee"))
|
||||||
|
batch_op.drop_column("edt_id")
|
||||||
|
batch_op.drop_column("code_apogee")
|
Loading…
Reference in New Issue
Block a user