Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into bac_a_sable_prod
This commit is contained in:
commit
580293207d
@ -9,11 +9,12 @@
|
|||||||
"""
|
"""
|
||||||
from operator import attrgetter, itemgetter
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, make_response, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
@ -30,6 +31,7 @@ from app.models import (
|
|||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
NotesNotes,
|
||||||
)
|
)
|
||||||
|
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
|
||||||
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
@ -496,3 +498,44 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||||||
row["partitions"] = etud_groups.get(row["etudid"], {})
|
row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||||
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def get_groups_auto_assignment(formsemestre_id: int):
|
||||||
|
"""rend les données"""
|
||||||
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
|
response = make_response(formsemestre.groups_auto_assignment_data or b"")
|
||||||
|
response.headers["Content-Type"] = scu.JSON_MIMETYPE
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def save_groups_auto_assignment(formsemestre_id: int):
|
||||||
|
"""enregistre les données"""
|
||||||
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
|
|
||||||
|
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||||
|
return json_error(413, "data too large")
|
||||||
|
formsemestre.groups_auto_assignment_data = request.data
|
||||||
|
db.session.add(formsemestre)
|
||||||
|
db.session.commit()
|
||||||
|
@ -38,13 +38,22 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||||||
# Choix des parcours
|
# Choix des parcours
|
||||||
ue_pids = [p.id for p in ue.parcours]
|
ue_pids = [p.id for p in ue.parcours]
|
||||||
H.append("""<form id="choix_parcours">""")
|
H.append("""<form id="choix_parcours">""")
|
||||||
|
|
||||||
|
ects_differents = {
|
||||||
|
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||||
|
} != {None}
|
||||||
for parcour in ref_comp.parcours:
|
for parcour in ref_comp.parcours:
|
||||||
|
ects_parcour = ue.get_ects(parcour)
|
||||||
|
ects_parcour_txt = (
|
||||||
|
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
||||||
|
)
|
||||||
H.append(
|
H.append(
|
||||||
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
||||||
{'checked' if parcour.id in ue_pids else ""}
|
{'checked' if parcour.id in ue_pids else ""}
|
||||||
onclick="set_ue_parcour(this);"
|
onclick="set_ue_parcour(this);"
|
||||||
data-setter="{url_for("apiweb.set_ue_parcours", scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
data-setter="{url_for("apiweb.set_ue_parcours",
|
||||||
>{parcour.code}</label>"""
|
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||||
|
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||||
)
|
)
|
||||||
H.append("""</form>""")
|
H.append("""</form>""")
|
||||||
#
|
#
|
||||||
|
@ -421,7 +421,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
+ '</div><div class="warning">'.join(messages)
|
+ '</div><div class="warning">'.join(messages)
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
#
|
|
||||||
|
# WIP TODO XXX def get_moyenne_annuelle(self)
|
||||||
|
|
||||||
def infos(self) -> str:
|
def infos(self) -> str:
|
||||||
"""informations, for debugging purpose."""
|
"""informations, for debugging purpose."""
|
||||||
|
@ -106,6 +106,8 @@ class BonusSport:
|
|||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
# BUT
|
# BUT
|
||||||
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
|
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
|
||||||
|
if nb_ues_no_bonus == 0: # aucune UE...
|
||||||
|
return # no bonus at all
|
||||||
# Duplique les inscriptions sur les UEs non bonus:
|
# Duplique les inscriptions sur les UEs non bonus:
|
||||||
modimpl_inscr_spo_stacked = np.stack(
|
modimpl_inscr_spo_stacked = np.stack(
|
||||||
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
|
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
|
||||||
|
@ -230,7 +230,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
}
|
}
|
||||||
self.etuds_parcour_id = etuds_parcour_id
|
self.etuds_parcour_id = etuds_parcour_id
|
||||||
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||||
|
ue_ids_set = set(ue_ids)
|
||||||
if self.formsemestre.formation.referentiel_competence is None:
|
if self.formsemestre.formation.referentiel_competence is None:
|
||||||
return pd.DataFrame(
|
return pd.DataFrame(
|
||||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||||
@ -240,7 +240,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||||
)
|
)
|
||||||
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
||||||
# (considère aussi le cas des semestres sans parcours: None)
|
# - considère aussi le cas des semestres sans parcours (clé parcour None)
|
||||||
|
# - retire les UEs qui ont un parcours mais qui ne sont pas dans l'un des
|
||||||
|
# parcours du semestre
|
||||||
|
|
||||||
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||||
for (
|
for (
|
||||||
parcour
|
parcour
|
||||||
@ -250,6 +253,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
for ue in self.formsemestre.formation.query_ues_parcour(parcour).filter(
|
for ue in self.formsemestre.formation.query_ues_parcour(parcour).filter(
|
||||||
UniteEns.semestre_idx == self.formsemestre.semestre_id
|
UniteEns.semestre_idx == self.formsemestre.semestre_id
|
||||||
)
|
)
|
||||||
|
if ue.id in ue_ids_set
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
for etudid in etuds_parcour_id:
|
for etudid in etuds_parcour_id:
|
||||||
|
@ -318,7 +318,7 @@ class OffreCreationForm(FlaskForm):
|
|||||||
duree = _build_string_field("Durée (*)")
|
duree = _build_string_field("Durée (*)")
|
||||||
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
||||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||||
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
|
correspondant = SelectField("Correspondant à contacter", validators=[Optional()])
|
||||||
fichier = FileField(
|
fichier = FileField(
|
||||||
"Fichier",
|
"Fichier",
|
||||||
validators=[
|
validators=[
|
||||||
@ -373,7 +373,7 @@ class OffreModificationForm(FlaskForm):
|
|||||||
duree = _build_string_field("Durée (*)")
|
duree = _build_string_field("Durée (*)")
|
||||||
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
depts = MultiCheckboxField("Départements (*)", validators=[Optional()], coerce=int)
|
||||||
expiration_date = DateField("Date expiration", validators=[Optional()])
|
expiration_date = DateField("Date expiration", validators=[Optional()])
|
||||||
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
|
correspondant = SelectField("Correspondant à contacter", validators=[Optional()])
|
||||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from app import models
|
|||||||
|
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc.sco_bac import Baccalaureat
|
from app.scodoc.sco_bac import Baccalaureat
|
||||||
from app.scodoc.sco_exceptions import ScoInvalidParamError
|
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ class Identite(db.Model):
|
|||||||
def create_etud(cls, **args):
|
def create_etud(cls, **args):
|
||||||
"Crée un étudiant, avec admission et adresse vides."
|
"Crée un étudiant, avec admission et adresse vides."
|
||||||
etud: Identite = cls(**args)
|
etud: Identite = cls(**args)
|
||||||
etud.adresses.append(Adresse())
|
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||||
etud.admission.append(Admission())
|
etud.admission.append(Admission())
|
||||||
return etud
|
return etud
|
||||||
|
|
||||||
@ -205,6 +205,50 @@ class Identite(db.Model):
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
|
"Convert fields in the given dict. No other side effect"
|
||||||
|
fs_uppercase = {"nom", "prenom", "prenom_etat_civil"}
|
||||||
|
fs_empty_stored_as_nulls = {
|
||||||
|
"nom",
|
||||||
|
"prenom",
|
||||||
|
"nom_usuel",
|
||||||
|
"date_naissance",
|
||||||
|
"lieu_naissance",
|
||||||
|
"dept_naissance",
|
||||||
|
"nationalite",
|
||||||
|
"statut",
|
||||||
|
"photo_filename",
|
||||||
|
"code_nip",
|
||||||
|
"code_ine",
|
||||||
|
}
|
||||||
|
args_dict = {}
|
||||||
|
for key, value in args.items():
|
||||||
|
if hasattr(cls, key):
|
||||||
|
# compat scodoc7 (mauvaise idée de l'époque)
|
||||||
|
if key in fs_empty_stored_as_nulls and value == "":
|
||||||
|
value = None
|
||||||
|
if key in fs_uppercase and value:
|
||||||
|
value = value.upper()
|
||||||
|
if key == "civilite" or key == "civilite_etat_civil":
|
||||||
|
value = input_civilite(value)
|
||||||
|
elif key == "boursier":
|
||||||
|
value = bool(value)
|
||||||
|
elif key == "date_naissance":
|
||||||
|
value = ndb.DateDMYtoISO(value)
|
||||||
|
args_dict[key] = value
|
||||||
|
return args_dict
|
||||||
|
|
||||||
|
def from_dict(self, args: dict):
|
||||||
|
"update fields given in dict. Add to session but don't commit."
|
||||||
|
args_dict = Identite.convert_dict_fields(args)
|
||||||
|
args_dict.pop("id", None)
|
||||||
|
args_dict.pop("etudid", None)
|
||||||
|
for key, value in args_dict.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
setattr(self, key, value)
|
||||||
|
db.session.add(self)
|
||||||
|
|
||||||
def to_dict_short(self) -> dict:
|
def to_dict_short(self) -> dict:
|
||||||
"""Les champs essentiels"""
|
"""Les champs essentiels"""
|
||||||
return {
|
return {
|
||||||
@ -547,6 +591,37 @@ def make_etud_args(
|
|||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def input_civilite(s):
|
||||||
|
"""Converts external representation of civilite to internal:
|
||||||
|
'M', 'F', or 'X' (and nothing else).
|
||||||
|
Raises ScoValueError if conversion fails.
|
||||||
|
"""
|
||||||
|
s = s.upper().strip()
|
||||||
|
if s in ("M", "M.", "MR", "H"):
|
||||||
|
return "M"
|
||||||
|
elif s in ("F", "MLLE", "MLLE.", "MELLE", "MME"):
|
||||||
|
return "F"
|
||||||
|
elif s == "X" or not s:
|
||||||
|
return "X"
|
||||||
|
raise ScoValueError(f"valeur invalide pour la civilité: {s}")
|
||||||
|
|
||||||
|
|
||||||
|
PIVOT_YEAR = 70
|
||||||
|
|
||||||
|
|
||||||
|
def pivot_year(y) -> int:
|
||||||
|
"converti et calcule l'année si saisie à deux chiffres"
|
||||||
|
if y == "" or y is None:
|
||||||
|
return None
|
||||||
|
y = int(round(float(y)))
|
||||||
|
if y >= 0 and y < 100:
|
||||||
|
if y < PIVOT_YEAR:
|
||||||
|
y = y + 2000
|
||||||
|
else:
|
||||||
|
y = y + 1900
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
class Adresse(db.Model):
|
class Adresse(db.Model):
|
||||||
"""Adresse d'un étudiant
|
"""Adresse d'un étudiant
|
||||||
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
||||||
@ -640,19 +715,51 @@ class Admission(db.Model):
|
|||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if no_nulls:
|
if no_nulls:
|
||||||
for k in d.keys():
|
for key, value in d.items():
|
||||||
if d[k] is None:
|
if value is None:
|
||||||
col_type = getattr(
|
col_type = getattr(
|
||||||
sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
|
sqlalchemy.inspect(models.Admission).columns, key
|
||||||
).expression.type
|
).expression.type
|
||||||
if isinstance(col_type, sqlalchemy.Text):
|
if isinstance(col_type, sqlalchemy.Text):
|
||||||
d[k] = ""
|
d[key] = ""
|
||||||
elif isinstance(col_type, sqlalchemy.Integer):
|
elif isinstance(col_type, sqlalchemy.Integer):
|
||||||
d[k] = 0
|
d[key] = 0
|
||||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||||
d[k] = False
|
d[key] = False
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
|
"Convert fields in the given dict. No other side effect"
|
||||||
|
fs_uppercase = {"bac", "specialite"}
|
||||||
|
args_dict = {}
|
||||||
|
for key, value in args.items():
|
||||||
|
if hasattr(cls, key):
|
||||||
|
if (
|
||||||
|
value == ""
|
||||||
|
): # les chaines vides donne des NULLS (scodoc7 convention)
|
||||||
|
value = None
|
||||||
|
if key in fs_uppercase and value:
|
||||||
|
value = value.upper()
|
||||||
|
if key == "civilite" or key == "civilite_etat_civil":
|
||||||
|
value = input_civilite(value)
|
||||||
|
elif key == "annee" or key == "annee_bac":
|
||||||
|
value = pivot_year(value)
|
||||||
|
elif key == "classement" or key == "apb_classement_gr":
|
||||||
|
value = ndb.int_null_is_null(value)
|
||||||
|
args_dict[key] = value
|
||||||
|
return args_dict
|
||||||
|
|
||||||
|
def from_dict(self, args: dict): # TODO à refactoriser dans une super-classe
|
||||||
|
"update fields given in dict. Add to session but don't commit."
|
||||||
|
args_dict = Admission.convert_dict_fields(args)
|
||||||
|
args_dict.pop("adm_id", None)
|
||||||
|
args_dict.pop("id", None)
|
||||||
|
for key, value in args_dict.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
setattr(self, key, value)
|
||||||
|
db.session.add(self)
|
||||||
|
|
||||||
|
|
||||||
# Suivi scolarité / débouchés
|
# Suivi scolarité / débouchés
|
||||||
class ItemSuivi(db.Model):
|
class ItemSuivi(db.Model):
|
||||||
|
@ -217,7 +217,7 @@ class Formation(db.Model):
|
|||||||
def query_ues_parcour(
|
def query_ues_parcour(
|
||||||
self, parcour: ApcParcours, with_sport: bool = False
|
self, parcour: ApcParcours, with_sport: bool = False
|
||||||
) -> Query:
|
) -> Query:
|
||||||
"""Les UEs (non bonus) d'un parcours de la formation
|
"""Les UEs (sans bonus, sauf si with_sport) d'un parcours de la formation
|
||||||
(déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours)
|
(déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours)
|
||||||
Si parcour est None, les UE sans parcours.
|
Si parcour est None, les UE sans parcours.
|
||||||
Exemple: pour avoir les UE du semestre 3, faire
|
Exemple: pour avoir les UE du semestre 3, faire
|
||||||
|
@ -15,10 +15,8 @@ from functools import cached_property
|
|||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
|
|
||||||
from flask import flash, g
|
from flask import flash, g
|
||||||
from sqlalchemy import and_, or_
|
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -26,10 +24,7 @@ from app import db, log
|
|||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
|
||||||
ApcNiveau,
|
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
|
||||||
ApcReferentielCompetences,
|
ApcReferentielCompetences,
|
||||||
parcours_formsemestre,
|
parcours_formsemestre,
|
||||||
)
|
)
|
||||||
@ -47,6 +42,8 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
class FormSemestre(db.Model):
|
class FormSemestre(db.Model):
|
||||||
"""Mise en oeuvre d'un semestre de formation"""
|
"""Mise en oeuvre d'un semestre de formation"""
|
||||||
@ -113,6 +110,10 @@ class FormSemestre(db.Model):
|
|||||||
elt_annee_apo = db.Column(db.Text())
|
elt_annee_apo = db.Column(db.Text())
|
||||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||||
|
|
||||||
|
# Data pour groups_auto_assignment
|
||||||
|
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||||
|
groups_auto_assignment_data = db.Column(db.LargeBinary(), nullable=True)
|
||||||
|
|
||||||
# Relations:
|
# Relations:
|
||||||
etapes = db.relationship(
|
etapes = db.relationship(
|
||||||
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
|
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
|
||||||
@ -298,12 +299,14 @@ class FormSemestre(db.Model):
|
|||||||
"""
|
"""
|
||||||
formation: Formation = self.formation
|
formation: Formation = self.formation
|
||||||
if formation.is_apc():
|
if formation.is_apc():
|
||||||
|
# UEs de tronc commun (sans parcours indiqué)
|
||||||
sem_ues = {
|
sem_ues = {
|
||||||
ue.id: ue
|
ue.id: ue
|
||||||
for ue in formation.query_ues_parcour(
|
for ue in formation.query_ues_parcour(
|
||||||
None, with_sport=with_sport
|
None, with_sport=with_sport
|
||||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||||
}
|
}
|
||||||
|
# Ajoute les UE de parcours
|
||||||
for parcour in self.parcours:
|
for parcour in self.parcours:
|
||||||
sem_ues.update(
|
sem_ues.update(
|
||||||
{
|
{
|
||||||
|
@ -186,7 +186,7 @@ def DBSelectArgs(
|
|||||||
cond = ""
|
cond = ""
|
||||||
i = 1
|
i = 1
|
||||||
cl = []
|
cl = []
|
||||||
for (_, aux_id) in aux_tables:
|
for _, aux_id in aux_tables:
|
||||||
cl.append("T0.%s = T%d.%s" % (id_name, i, aux_id))
|
cl.append("T0.%s = T%d.%s" % (id_name, i, aux_id))
|
||||||
i = i + 1
|
i = i + 1
|
||||||
cond += " and ".join(cl)
|
cond += " and ".join(cl)
|
||||||
@ -403,7 +403,7 @@ class EditableTable(object):
|
|||||||
|
|
||||||
def format_output(self, r, disable_formatting=False):
|
def format_output(self, r, disable_formatting=False):
|
||||||
"Format dict using provided output_formators"
|
"Format dict using provided output_formators"
|
||||||
for (k, v) in r.items():
|
for k, v in r.items():
|
||||||
if v is None and self.convert_null_outputs_to_empty:
|
if v is None and self.convert_null_outputs_to_empty:
|
||||||
v = ""
|
v = ""
|
||||||
# format value
|
# format value
|
||||||
|
@ -465,7 +465,7 @@ class ApoEtud(dict):
|
|||||||
return VOID_APO_RES
|
return VOID_APO_RES
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
N="",
|
N="", # n'exporte pas de moyenne indicative annuelle, car pas de définition officielle
|
||||||
B=20,
|
B=20,
|
||||||
J="",
|
J="",
|
||||||
R=ScoDocSiteConfig.get_code_apo(self.validation_annee_but.code),
|
R=ScoDocSiteConfig.get_code_apo(self.validation_annee_but.code),
|
||||||
|
@ -35,10 +35,10 @@ from operator import itemgetter
|
|||||||
|
|
||||||
from flask import url_for, g
|
from flask import url_for, g
|
||||||
|
|
||||||
from app import email
|
from app import db, email
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import Admission
|
from app.models import Admission, Identite
|
||||||
from app.models.etudiants import make_etud_args
|
from app.models.etudiants import input_civilite, make_etud_args, pivot_year
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||||
@ -72,6 +72,7 @@ def format_etud_ident(etud):
|
|||||||
etud["nom_disp"] = etud["nom"]
|
etud["nom_disp"] = etud["nom"]
|
||||||
|
|
||||||
etud["nomprenom"] = format_nomprenom(etud) # M. Pierre DUPONT
|
etud["nomprenom"] = format_nomprenom(etud) # M. Pierre DUPONT
|
||||||
|
etud["etat_civil"] = format_etat_civil(etud)
|
||||||
if etud["civilite"] == "M":
|
if etud["civilite"] == "M":
|
||||||
etud["ne"] = ""
|
etud["ne"] = ""
|
||||||
elif etud["civilite"] == "F":
|
elif etud["civilite"] == "F":
|
||||||
@ -127,21 +128,6 @@ def format_nom(s, uppercase=True):
|
|||||||
return format_prenom(s)
|
return format_prenom(s)
|
||||||
|
|
||||||
|
|
||||||
def input_civilite(s):
|
|
||||||
"""Converts external representation of civilite to internal:
|
|
||||||
'M', 'F', or 'X' (and nothing else).
|
|
||||||
Raises ScoValueError if conversion fails.
|
|
||||||
"""
|
|
||||||
s = s.upper().strip()
|
|
||||||
if s in ("M", "M.", "MR", "H"):
|
|
||||||
return "M"
|
|
||||||
elif s in ("F", "MLLE", "MLLE.", "MELLE", "MME"):
|
|
||||||
return "F"
|
|
||||||
elif s == "X" or not s:
|
|
||||||
return "X"
|
|
||||||
raise ScoValueError("valeur invalide pour la civilité: %s" % s)
|
|
||||||
|
|
||||||
|
|
||||||
def format_civilite(civilite):
|
def format_civilite(civilite):
|
||||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||||
personne ne souhaitant pas d'affichage).
|
personne ne souhaitant pas d'affichage).
|
||||||
@ -157,6 +143,14 @@ def format_civilite(civilite):
|
|||||||
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
|
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
|
||||||
|
|
||||||
|
|
||||||
|
def format_etat_civil(etud: dict):
|
||||||
|
if etud["prenom_etat_civil"]:
|
||||||
|
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
|
||||||
|
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
||||||
|
else:
|
||||||
|
return etud["nomprenom"]
|
||||||
|
|
||||||
|
|
||||||
def format_lycee(nomlycee):
|
def format_lycee(nomlycee):
|
||||||
nomlycee = nomlycee.strip()
|
nomlycee = nomlycee.strip()
|
||||||
s = nomlycee.lower()
|
s = nomlycee.lower()
|
||||||
@ -195,21 +189,6 @@ def format_pays(s):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
PIVOT_YEAR = 70
|
|
||||||
|
|
||||||
|
|
||||||
def pivot_year(y):
|
|
||||||
if y == "" or y is None:
|
|
||||||
return None
|
|
||||||
y = int(round(float(y)))
|
|
||||||
if y >= 0 and y < 100:
|
|
||||||
if y < PIVOT_YEAR:
|
|
||||||
y = y + 2000
|
|
||||||
else:
|
|
||||||
y = y + 1900
|
|
||||||
return y
|
|
||||||
|
|
||||||
|
|
||||||
def etud_sort_key(etud: dict) -> tuple:
|
def etud_sort_key(etud: dict) -> tuple:
|
||||||
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
|
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
|
||||||
Equivalent moderne: identite.sort_key
|
Equivalent moderne: identite.sort_key
|
||||||
@ -281,7 +260,9 @@ def identite_list(cnx, *a, **kw):
|
|||||||
|
|
||||||
def identite_edit_nocheck(cnx, args):
|
def identite_edit_nocheck(cnx, args):
|
||||||
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
||||||
_identiteEditor.edit(cnx, args)
|
etud = Identite.query.get(args["etudid"])
|
||||||
|
etud.from_dict(args)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
|
def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
|
||||||
@ -572,6 +553,7 @@ admission_delete = _admissionEditor.delete
|
|||||||
admission_list = _admissionEditor.list
|
admission_list = _admissionEditor.list
|
||||||
admission_edit = _admissionEditor.edit
|
admission_edit = _admissionEditor.edit
|
||||||
|
|
||||||
|
|
||||||
# Edition simultanee de identite et admission
|
# Edition simultanee de identite et admission
|
||||||
class EtudIdentEditor(object):
|
class EtudIdentEditor(object):
|
||||||
def create(self, cnx, args):
|
def create(self, cnx, args):
|
||||||
@ -615,7 +597,6 @@ class EtudIdentEditor(object):
|
|||||||
_etudidentEditor = EtudIdentEditor()
|
_etudidentEditor = EtudIdentEditor()
|
||||||
etudident_list = _etudidentEditor.list
|
etudident_list = _etudidentEditor.list
|
||||||
etudident_edit = _etudidentEditor.edit
|
etudident_edit = _etudidentEditor.edit
|
||||||
etudident_create = _etudidentEditor.create
|
|
||||||
|
|
||||||
|
|
||||||
def log_unknown_etud():
|
def log_unknown_etud():
|
||||||
@ -641,21 +622,8 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list[dict]:
|
|||||||
return etud
|
return etud
|
||||||
|
|
||||||
|
|
||||||
# Optim par cache local, utilité non prouvée mais
|
def create_etud(cnx, args: dict = None):
|
||||||
# on s'oriente vers un cahce persistent dans Redis ou bien l'utilisation de NT
|
"""Création d'un étudiant. Génère aussi évenement et "news".
|
||||||
# def get_etud_info_filled_by_etudid(etudid, cnx=None) -> dict:
|
|
||||||
# """Infos sur un étudiant, avec cache local à la requête"""
|
|
||||||
# if etudid in g.stored_etud_info:
|
|
||||||
# return g.stored_etud_info[etudid]
|
|
||||||
# cnx = cnx or ndb.GetDBConnexion()
|
|
||||||
# etud = etudident_list(cnx, args={"etudid": etudid})
|
|
||||||
# fill_etuds_info(etud)
|
|
||||||
# g.stored_etud_info[etudid] = etud[0]
|
|
||||||
# return etud[0]
|
|
||||||
|
|
||||||
|
|
||||||
def create_etud(cnx, args={}):
|
|
||||||
"""Creation d'un étudiant. génère aussi évenement et "news".
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
args: dict avec les attributs de l'étudiant
|
args: dict avec les attributs de l'étudiant
|
||||||
@ -666,16 +634,16 @@ def create_etud(cnx, args={}):
|
|||||||
from app.models import ScolarNews
|
from app.models import ScolarNews
|
||||||
|
|
||||||
# creation d'un etudiant
|
# creation d'un etudiant
|
||||||
etudid = etudident_create(cnx, args)
|
args_dict = Identite.convert_dict_fields(args)
|
||||||
# crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
|
args_dict["dept_id"] = g.scodoc_dept_id
|
||||||
_ = adresse_create(
|
etud = Identite.create_etud(**args_dict)
|
||||||
cnx,
|
db.session.add(etud)
|
||||||
{
|
db.session.commit()
|
||||||
"etudid": etudid,
|
admission = etud.admission.first()
|
||||||
"typeadresse": "domicile",
|
admission.from_dict(args)
|
||||||
"description": "(creation individuelle)",
|
db.session.add(admission)
|
||||||
},
|
db.session.commit()
|
||||||
)
|
etudid = etud.id
|
||||||
|
|
||||||
# event
|
# event
|
||||||
scolar_events_create(
|
scolar_events_create(
|
||||||
|
@ -40,7 +40,7 @@ import app.scodoc.sco_utils as scu
|
|||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import ScolarNews, GroupDescr
|
from app.models import ScolarNews, GroupDescr
|
||||||
|
from app.models.etudiants import input_civilite
|
||||||
from app.scodoc.sco_excel import COLORS
|
from app.scodoc.sco_excel import COLORS
|
||||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||||
do_formsemestre_inscription_with_modules,
|
do_formsemestre_inscription_with_modules,
|
||||||
@ -370,7 +370,7 @@ def scolars_import_excel_file(
|
|||||||
# xxx Ad-hoc checks (should be in format description)
|
# xxx Ad-hoc checks (should be in format description)
|
||||||
if titleslist[i].lower() == "sexe":
|
if titleslist[i].lower() == "sexe":
|
||||||
try:
|
try:
|
||||||
val = sco_etud.input_civilite(val)
|
val = input_civilite(val)
|
||||||
except:
|
except:
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"valeur invalide pour 'SEXE' (doit etre 'M', 'F', ou 'MME', 'H', 'X' ou vide, mais pas '%s') ligne %d, colonne %s"
|
"valeur invalide pour 'SEXE' (doit etre 'M', 'F', ou 'MME', 'H', 'X' ou vide, mais pas '%s') ligne %d, colonne %s"
|
||||||
|
@ -2058,6 +2058,7 @@ span.eval_coef_ue_titre {}
|
|||||||
div.list_but_ue_inscriptions {
|
div.list_but_ue_inscriptions {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
@ -693,7 +693,13 @@ def formation_import_xml_form():
|
|||||||
{ html_sco_header.sco_header(page_title="Import d'une formation") }
|
{ html_sco_header.sco_header(page_title="Import d'une formation") }
|
||||||
<h2>Import d'une formation</h2>
|
<h2>Import d'une formation</h2>
|
||||||
<p>Création d'une formation (avec UE, matières, modules)
|
<p>Création d'une formation (avec UE, matières, modules)
|
||||||
à partir un fichier XML (réservé aux utilisateurs avertis)
|
à partir un fichier XML (réservé aux utilisateurs avertis).
|
||||||
|
</p>
|
||||||
|
<p>S'il s'agit d'une formation par compétence (BUT), assurez-vous d'avoir
|
||||||
|
chargé le référentiel de compétences AVANT d'importer le fichier formation
|
||||||
|
(voir <a class="stdlink" href="{
|
||||||
|
url_for("notes.refcomp_table", scodoc_dept=g.scodoc_dept)
|
||||||
|
}">page des référentiels</a>).
|
||||||
</p>
|
</p>
|
||||||
{ tf[1] }
|
{ tf[1] }
|
||||||
{ html_sco_header.sco_footer() }
|
{ html_sco_header.sco_footer() }
|
||||||
|
@ -1742,8 +1742,15 @@ def _etudident_create_or_edit_form(edit):
|
|||||||
etudid = etud["etudid"]
|
etudid = etud["etudid"]
|
||||||
else:
|
else:
|
||||||
# modif d'un etudiant
|
# modif d'un etudiant
|
||||||
sco_etud.etudident_edit(cnx, tf[2])
|
etud_o = Identite.query.get(tf[2]["etudid"])
|
||||||
etud = sco_etud.etudident_list(cnx, {"etudid": etudid})[0]
|
etud_o.from_dict(tf[2])
|
||||||
|
db.session.add(etud_o)
|
||||||
|
admission = etud_o.admission.first()
|
||||||
|
admission.from_dict(tf[2])
|
||||||
|
db.session.add(admission)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
etud = sco_etud.etudident_list(cnx, {"etudid": etud_o.id})[0]
|
||||||
sco_etud.fill_etuds_info([etud])
|
sco_etud.fill_etuds_info([etud])
|
||||||
# Inval semesters with this student:
|
# Inval semesters with this student:
|
||||||
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
|
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
"""Add data for groups_auto_assignment
|
||||||
|
|
||||||
|
Revision ID: b8df1b913c79
|
||||||
|
Revises: 054dd6133b9c
|
||||||
|
Create Date: 2023-05-15 23:12:58.257709
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "b8df1b913c79"
|
||||||
|
down_revision = "054dd6133b9c"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column("groups_auto_assignment_data", sa.LargeBinary(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("groups_auto_assignment_data")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.4.74"
|
SCOVERSION = "9.4.77"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
71
tests/api/test_api_formsemestre_data.py
Normal file
71
tests/api/test_api_formsemestre_data.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Test formsemestre
|
||||||
|
|
||||||
|
Utilisation :
|
||||||
|
créer les variables d'environnement: (indiquer les valeurs
|
||||||
|
pour le serveur ScoDoc que vous voulez interroger)
|
||||||
|
|
||||||
|
export SCODOC_URL="https://scodoc.xxx.net/"
|
||||||
|
export SCODOC_USER="xxx"
|
||||||
|
export SCODOC_PASSWD="xxx"
|
||||||
|
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
|
||||||
|
|
||||||
|
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
|
||||||
|
|
||||||
|
Lancer :
|
||||||
|
pytest tests/api/test_api_formsemestre.py
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
from tests.api.setup_test_api import (
|
||||||
|
API_URL,
|
||||||
|
CHECK_CERTIFICATE,
|
||||||
|
api_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_groups_auto_assignment(api_headers):
|
||||||
|
"""
|
||||||
|
Routes:
|
||||||
|
/formsemestre/<id>/save_groups_auto_assignment
|
||||||
|
/formsemestre/<id>/get_groups_auto_assignment
|
||||||
|
"""
|
||||||
|
formsemestre_id = 1
|
||||||
|
r = requests.get(
|
||||||
|
f"{API_URL}/formsemestre/{formsemestre_id}",
|
||||||
|
headers=api_headers,
|
||||||
|
verify=CHECK_CERTIFICATE,
|
||||||
|
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
# On stocke une chaine quelconque
|
||||||
|
data_orig = (
|
||||||
|
"""{ "attribute" : "Un paquet de json", "valide": pas nécessairement +}--"""
|
||||||
|
)
|
||||||
|
r = requests.post(
|
||||||
|
f"{API_URL}/formsemestre/{formsemestre_id}/save_groups_auto_assignment",
|
||||||
|
data=data_orig.encode("utf-8"),
|
||||||
|
headers=api_headers,
|
||||||
|
verify=CHECK_CERTIFICATE,
|
||||||
|
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
# GET
|
||||||
|
r = requests.get(
|
||||||
|
f"{API_URL}/formsemestre/{formsemestre_id}/get_groups_auto_assignment",
|
||||||
|
headers=api_headers,
|
||||||
|
verify=CHECK_CERTIFICATE,
|
||||||
|
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.text == data_orig
|
||||||
|
# Tente d'envoyer trop de données
|
||||||
|
r = requests.post(
|
||||||
|
f"{API_URL}/formsemestre/{formsemestre_id}/save_groups_auto_assignment",
|
||||||
|
data="F*CK" * 1000000, # environ 4MB
|
||||||
|
headers=api_headers,
|
||||||
|
verify=CHECK_CERTIFICATE,
|
||||||
|
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||||
|
)
|
||||||
|
assert r.status_code == 413
|
@ -3,41 +3,221 @@
|
|||||||
# mais à ces niveaux sont associés des UEs dont les coefficients des ressources
|
# mais à ces niveaux sont associés des UEs dont les coefficients des ressources
|
||||||
# varient selon le parcours.
|
# varient selon le parcours.
|
||||||
#
|
#
|
||||||
|
# Mise en place cursus avec parcours A et B
|
||||||
|
|
||||||
ReferentielCompetences:
|
ReferentielCompetences:
|
||||||
filename: but-INFO-05012022-081701.xml
|
filename: but-INFO-05012022-081701.xml
|
||||||
specialite: INFO
|
specialite: INFO
|
||||||
|
|
||||||
Formation:
|
Formation:
|
||||||
filename: scodoc_formation_BUT_INFO_v1.xml
|
filename: scodoc_formation_BUT_INFO_v0514.xml
|
||||||
|
# nota: les associations UE/Niveaux sont déjà données dans ce fichier XML.
|
||||||
ues:
|
ues:
|
||||||
|
# S1
|
||||||
|
'UE11':
|
||||||
|
annee: BUT1
|
||||||
|
'UE12':
|
||||||
|
annee: BUT1
|
||||||
|
'UE13':
|
||||||
|
annee: BUT1
|
||||||
|
'UE14':
|
||||||
|
annee: BUT1
|
||||||
|
'UE15':
|
||||||
|
annee: BUT1
|
||||||
|
'UE16':
|
||||||
|
annee: BUT1
|
||||||
|
# S2
|
||||||
|
'UE21':
|
||||||
|
annee: BUT1
|
||||||
|
'UE22':
|
||||||
|
annee: BUT1
|
||||||
|
'UE23':
|
||||||
|
annee: BUT1
|
||||||
|
'UE24':
|
||||||
|
annee: BUT1
|
||||||
|
'UE25':
|
||||||
|
annee: BUT1
|
||||||
|
'UE26':
|
||||||
|
annee: BUT1
|
||||||
|
# S3
|
||||||
|
'UE31':
|
||||||
|
annee: BUT2
|
||||||
|
'UE32':
|
||||||
|
annee: BUT2
|
||||||
|
'UE33':
|
||||||
|
annee: BUT2
|
||||||
|
'UE34':
|
||||||
|
annee: BUT2
|
||||||
|
'UE35':
|
||||||
|
annee: BUT2
|
||||||
|
'UE36':
|
||||||
|
annee: BUT2
|
||||||
# S4
|
# S4
|
||||||
'UE41-A': # UE pour le parcours A
|
'UE41-A': # UE pour le parcours A
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Réaliser
|
'UE41-B': # UE pour le parcours B (même contenu, coefs différents)
|
||||||
'UE41-C': # UE pour le parcours C (même contenu, coefs différents)
|
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Réaliser
|
|
||||||
'UE42':
|
'UE42':
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Optimiser
|
|
||||||
'UE43':
|
'UE43':
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Administrer
|
|
||||||
'UE44':
|
'UE44':
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Gérer
|
|
||||||
'UE45':
|
'UE45':
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Conduire
|
|
||||||
'UE46':
|
'UE46':
|
||||||
annee: BUT2
|
annee: BUT2
|
||||||
competence: Collaborer
|
|
||||||
|
|
||||||
FormSemestres:
|
FormSemestres:
|
||||||
# S4 avec parcours A et C
|
# Semestres avec parcours A et B
|
||||||
S4:
|
S1:
|
||||||
idx: 1
|
idx: 1
|
||||||
date_debut: 2023-01-01
|
date_debut: 2021-09-01
|
||||||
|
date_fin: 2022-01-15
|
||||||
|
codes_parcours: ['A', 'B']
|
||||||
|
S2:
|
||||||
|
idx: 2
|
||||||
|
date_debut: 2022-01-16
|
||||||
|
date_fin: 2022-06-30
|
||||||
|
codes_parcours: ['A', 'B']
|
||||||
|
S3:
|
||||||
|
idx: 3
|
||||||
|
date_debut: 2022-09-01
|
||||||
|
date_fin: 2023-01-15
|
||||||
|
codes_parcours: ['A', 'B']
|
||||||
|
S4:
|
||||||
|
idx: 4
|
||||||
|
date_debut: 2023-01-16
|
||||||
date_fin: 2023-06-30
|
date_fin: 2023-06-30
|
||||||
codes_parcours: ['A', 'C']
|
codes_parcours: ['A', 'B']
|
||||||
|
S5:
|
||||||
|
idx: 5
|
||||||
|
date_debut: 2023-09-01
|
||||||
|
date_fin: 2024-01-15
|
||||||
|
codes_parcours: ['A', 'B']
|
||||||
|
S6:
|
||||||
|
idx: 6
|
||||||
|
date_debut: 2024-01-16
|
||||||
|
date_fin: 2024-06-30
|
||||||
|
codes_parcours: ['A', 'B']
|
||||||
|
|
||||||
|
Etudiants:
|
||||||
|
ex_a1: # cursus S1 -> S6, valide tout
|
||||||
|
prenom: Jean
|
||||||
|
civilite: M
|
||||||
|
formsemestres:
|
||||||
|
# on ne note que le portfolio, qui affecte toutes les UEs
|
||||||
|
S1:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P1": 11
|
||||||
|
S2:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P2": 12
|
||||||
|
S3:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P3": 13
|
||||||
|
S4:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P4-A": 14
|
||||||
|
S5:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P5-A": 15
|
||||||
|
S6:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P6-A": 16
|
||||||
|
|
||||||
|
ex_a2: # cursus S1 -> S6, valide tout sauf S5
|
||||||
|
prenom: Lucie
|
||||||
|
civilite: F
|
||||||
|
formsemestres:
|
||||||
|
# on ne note que le portfolio, qui affecte toutes les UEs
|
||||||
|
S1:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P1": 11
|
||||||
|
S2:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P2": 12
|
||||||
|
S3:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P3": 13
|
||||||
|
S4:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P4-A": 14
|
||||||
|
S5:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P5-A": 7
|
||||||
|
S6:
|
||||||
|
parcours: A
|
||||||
|
notes_modules:
|
||||||
|
"P6-A": 16
|
||||||
|
|
||||||
|
ex_b1: # cursus S1 -> S6, valide tout
|
||||||
|
prenom: Hélène
|
||||||
|
civilite: F
|
||||||
|
formsemestres:
|
||||||
|
# on ne note que le portfolio, qui affecte toutes les UEs
|
||||||
|
S1:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P1": 11
|
||||||
|
S2:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P2": 12
|
||||||
|
S3:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P3": 13
|
||||||
|
S4:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P4-B": 14
|
||||||
|
S5:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P5-B": 15
|
||||||
|
S6:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P6-B": 16
|
||||||
|
|
||||||
|
ex_b2: # cursus S1 -> S6, valide tout sauf S6
|
||||||
|
prenom: Rose
|
||||||
|
civilite: F
|
||||||
|
formsemestres:
|
||||||
|
# on ne note que le portfolio, qui affecte toutes les UEs
|
||||||
|
S1:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P1": 11
|
||||||
|
S2:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P2": 12
|
||||||
|
S3:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P3": 13
|
||||||
|
S4:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P4-B": 14
|
||||||
|
S5:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P5-B": 15
|
||||||
|
S6:
|
||||||
|
parcours: B
|
||||||
|
notes_modules:
|
||||||
|
"P6-B": 9
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
Ce test suppose une base département existante.
|
Ce test suppose une base département existante.
|
||||||
|
|
||||||
Usage: pytest tests/unit/test_cursus_but.py
|
Usage: pytest tests/unit/test_but_cursus.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +63,7 @@ def test_cursus_but_jury_gb(test_client):
|
|||||||
|
|
||||||
|
|
||||||
# @pytest.mark.skip # XXX WIP
|
# @pytest.mark.skip # XXX WIP
|
||||||
|
@pytest.mark.slow
|
||||||
def test_refcomp_niveaux_info(test_client):
|
def test_refcomp_niveaux_info(test_client):
|
||||||
"""Test niveaux / parcours / UE pour un BUT INFO
|
"""Test niveaux / parcours / UE pour un BUT INFO
|
||||||
avec parcours A et C, même compétences mais coefs différents
|
avec parcours A et C, même compétences mais coefs différents
|
||||||
|
@ -120,7 +120,9 @@ def create_formsemestre(
|
|||||||
modules = [
|
modules = [
|
||||||
m
|
m
|
||||||
for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id)
|
for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id)
|
||||||
if (not m.parcours) or ({p.id for p in m.parcours} & sem_parcours_ids)
|
if (not m.parcours) # module de tronc commun
|
||||||
|
or (not sem_parcours_ids) # semestre sans parcours => tous
|
||||||
|
or ({p.id for p in m.parcours} & sem_parcours_ids)
|
||||||
]
|
]
|
||||||
for module in modules:
|
for module in modules:
|
||||||
modimpl = ModuleImpl(module=module, responsable_id=a_user.id)
|
modimpl = ModuleImpl(module=module, responsable_id=a_user.id)
|
||||||
@ -298,10 +300,11 @@ def setup_from_yaml(filename: str) -> dict:
|
|||||||
with open(filename, encoding="utf-8") as f:
|
with open(filename, encoding="utf-8") as f:
|
||||||
doc = yaml.safe_load(f.read())
|
doc = yaml.safe_load(f.read())
|
||||||
|
|
||||||
|
# Charge de ref. comp. avant la formation, de façon à pouvoir
|
||||||
|
# re-créer les associations UE/Niveaux
|
||||||
|
yaml_setup_but.setup_formation_referentiel(doc.get("ReferentielCompetences", {}))
|
||||||
formation = setup_formation(doc["Formation"])
|
formation = setup_formation(doc["Formation"])
|
||||||
yaml_setup_but.setup_formation_referentiel(
|
|
||||||
formation, doc.get("ReferentielCompetences", {})
|
|
||||||
)
|
|
||||||
yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])
|
yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])
|
||||||
setup_formsemestres(formation, doc)
|
setup_formsemestres(formation, doc)
|
||||||
etudiants = doc.get("Etudiants")
|
etudiants = doc.get("Etudiants")
|
||||||
|
@ -32,13 +32,15 @@ from app.scodoc import sco_utils as scu
|
|||||||
from app.scodoc import sco_pv_dict
|
from app.scodoc import sco_pv_dict
|
||||||
|
|
||||||
|
|
||||||
def setup_formation_referentiel(formation: Formation, refcomp_infos: dict):
|
def setup_formation_referentiel(
|
||||||
|
refcomp_infos: dict, formation: Formation = None
|
||||||
|
) -> ApcReferentielCompetences:
|
||||||
"""Si il y a un référentiel de compétences, indiqué dans le YAML,
|
"""Si il y a un référentiel de compétences, indiqué dans le YAML,
|
||||||
le charge au besoin et l'associe à la formation.
|
le charge au besoin et l'associe à la formation.
|
||||||
"""
|
"""
|
||||||
if not refcomp_infos:
|
if not refcomp_infos:
|
||||||
return
|
return None
|
||||||
assert formation.is_apc() # si ref; comp., doit être APC
|
assert formation is None or formation.is_apc() # si ref. comp., doit être APC
|
||||||
refcomp_filename = refcomp_infos["filename"]
|
refcomp_filename = refcomp_infos["filename"]
|
||||||
refcomp_specialite = refcomp_infos["specialite"]
|
refcomp_specialite = refcomp_infos["specialite"]
|
||||||
# --- Chargement Référentiel
|
# --- Chargement Référentiel
|
||||||
@ -66,8 +68,10 @@ def setup_formation_referentiel(formation: Formation, refcomp_infos: dict):
|
|||||||
specialite=refcomp_specialite
|
specialite=refcomp_specialite
|
||||||
).first() # le recherche à nouveau (test)
|
).first() # le recherche à nouveau (test)
|
||||||
assert referentiel_competence
|
assert referentiel_competence
|
||||||
formation.referentiel_competence_id = referentiel_competence.id
|
if formation:
|
||||||
db.session.add(formation)
|
formation.referentiel_competence_id = referentiel_competence.id
|
||||||
|
db.session.add(formation)
|
||||||
|
return referentiel_competence
|
||||||
|
|
||||||
|
|
||||||
def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
|
def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
|
||||||
@ -100,15 +104,16 @@ def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
|
|||||||
ue.set_parcours(parcours)
|
ue.set_parcours(parcours)
|
||||||
|
|
||||||
# Niveaux compétences:
|
# Niveaux compétences:
|
||||||
competence = referentiel_competence.competences.filter_by(
|
if ue_infos.get("competence"):
|
||||||
titre=ue_infos["competence"]
|
competence = referentiel_competence.competences.filter_by(
|
||||||
).first()
|
titre=ue_infos["competence"]
|
||||||
assert competence is not None # La compétence de titre indiqué doit exister
|
).first()
|
||||||
niveau: ApcNiveau = competence.niveaux.filter_by(
|
assert competence is not None # La compétence de titre indiqué doit exister
|
||||||
annee=ue_infos["annee"]
|
niveau: ApcNiveau = competence.niveaux.filter_by(
|
||||||
).first()
|
annee=ue_infos["annee"]
|
||||||
assert niveau is not None # le niveau de l'année indiquée doit exister
|
).first()
|
||||||
ue.set_niveau_competence(niveau)
|
assert niveau is not None # le niveau de l'année indiquée doit exister
|
||||||
|
ue.set_niveau_competence(niveau)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
associe_modules_et_parcours(formation, formation_infos)
|
associe_modules_et_parcours(formation, formation_infos)
|
||||||
|
@ -4,4 +4,4 @@ Architecture: amd64
|
|||||||
Maintainer: Emmanuel Viennet <emmanuel@viennet.net>
|
Maintainer: Emmanuel Viennet <emmanuel@viennet.net>
|
||||||
Description: ScoDoc 9
|
Description: ScoDoc 9
|
||||||
Un logiciel pour le suivi de la scolarité universitaire.
|
Un logiciel pour le suivi de la scolarité universitaire.
|
||||||
Depends: adduser, curl, gcc, graphviz, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, libpq-dev, redis, ufw
|
Depends: adduser, curl, gcc, graphviz, graphviz-dev, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, libpq-dev, redis, ufw
|
||||||
|
Loading…
Reference in New Issue
Block a user