Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
8 changed files with 255 additions and 10 deletions
Showing only changes of commit 0f61b0874a - Show all commits

View 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()

View File

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

View File

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

View File

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

View File

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

View 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 %}

View File

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

View 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")