forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
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")
|
||||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import FieldList, Form, validators
|
||||
from wtforms import validators
|
||||
from wtforms.fields.simple import BooleanField, StringField, SubmitField
|
||||
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
@ -7,6 +7,7 @@ from flask_sqlalchemy.query import Query
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||
@ -21,6 +22,10 @@ class ModuleImpl(db.Model):
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||
|
||||
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")
|
||||
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||
formsemestre_id = db.Column(
|
||||
@ -45,11 +50,21 @@ class ModuleImpl(db.Model):
|
||||
def __repr__(self):
|
||||
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:
|
||||
"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 (
|
||||
self.module.get_edt_id()
|
||||
) # TODO à décliner pour autoriser des codes différents ?
|
||||
self.edt_id
|
||||
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||
or self.module.get_edt_id()
|
||||
)
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""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)
|
||||
else:
|
||||
d.pop("module", None)
|
||||
d["code_apogee"] = d["code_apogee"] or "" # pas de None
|
||||
return d
|
||||
|
||||
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)
|
||||
}">Modifier les coefficients des UE capitalisées</a>
|
||||
</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
|
||||
à inscrire:</h3>
|
||||
"""
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Changement de la formation du semestre</h2>
|
||||
|
||||
<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.
|
||||
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.
|
||||
|
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 g, request
|
||||
|
||||
from app import db, log
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
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.scodoc import sco_formations, sco_formation_versions
|
||||
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>")
|
||||
@scodoc
|
||||
@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