WIP: distinction SAE/ressources, poids de evals

This commit is contained in:
Emmanuel Viennet 2021-11-08 19:44:25 +01:00
parent 981f2c0e46
commit 3bb9a5cb76
12 changed files with 415 additions and 32 deletions

View File

@ -51,9 +51,11 @@ from app.models.formsemestre import (
notes_modules_enseignants, notes_modules_enseignants,
NotesModuleImplInscription, NotesModuleImplInscription,
NotesEvaluation, NotesEvaluation,
EvaluationUEPoids,
NotesSemSet, NotesSemSet,
notes_semset_formsemestre, notes_semset_formsemestre,
) )
from app.models.but_pn import AppCrit
from app.models.groups import Partition, GroupDescr, group_membership from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import ( from app.models.notes import (
ScolarEvent, ScolarEvent,

View File

@ -1,4 +1,41 @@
"""ScoDoc 9 models : Formation BUT 2021 """ScoDoc 9 models : Formation BUT 2021
""" """
from enum import unique
from typing import Any
# insérer ici idk from app import db
from app.scodoc.sco_utils import ModuleType
Modules_ACs = db.Table(
"modules_acs",
db.Column("module_id", db.ForeignKey("notes_modules.id")),
db.Column("ac_id", db.ForeignKey("app_crit.id")),
)
class AppCrit(db.Model):
"Apprentissage Critique BUT"
__tablename__ = "app_crit"
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.Text(), nullable=False, info={"label": "Code"})
titre = db.Column(db.Text(), info={"label": "Titre"})
modules = db.relationship(
"NotesModule", secondary=Modules_ACs, lazy="dynamic", backref="acs"
)
def to_dict(self):
result = dict(self.__dict__)
result.pop("_sa_instance_state", None)
return result
def get_label(self):
return self.code + " - " + self.titre
def __repr__(self):
return "<AC {}>".format(self.code)
def get_saes(self):
"""Liste des SAE associées"""
return [m for m in self.modules if m.module_type == ModuleType.SAE]

View File

@ -5,6 +5,7 @@ from typing import Any
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.scodoc.sco_utils import ModuleType
class NotesFormation(db.Model): class NotesFormation(db.Model):
@ -116,10 +117,16 @@ class NotesModule(db.Model):
numero = db.Column(db.Integer) # ordre de présentation numero = db.Column(db.Integer) # ordre de présentation
# id de l'element pedagogique Apogee correspondant: # id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS) # Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
module_type = db.Column(db.Integer)
# Relations: # Relations:
modimpls = db.relationship("NotesModuleImpl", backref="module", lazy="dynamic") modimpls = db.relationship("NotesModuleImpl", backref="module", lazy="dynamic")
def __repr__(self):
return (
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
)
class NotesTag(db.Model): class NotesTag(db.Model):
"""Tag sur un module""" """Tag sur un module"""

View File

@ -8,6 +8,7 @@ from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models import NotesUE
class FormSemestre(db.Model): class FormSemestre(db.Model):
@ -305,7 +306,7 @@ class NotesEvaluation(db.Model):
heure_fin = db.Column(db.Time) heure_fin = db.Column(db.Time)
description = db.Column(db.Text) description = db.Column(db.Text)
note_max = db.Column(db.Float) note_max = db.Column(db.Float)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float) # non BUT
visibulletin = db.Column( visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true" db.Boolean, nullable=False, default=True, server_default="true"
) )
@ -319,6 +320,56 @@ class NotesEvaluation(db.Model):
# ordre de presentation (par défaut, le plus petit numero # ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval): # est la plus ancienne eval):
numero = db.Column(db.Integer) numero = db.Column(db.Integer)
ues = db.relationship("NotesUE", secondary="evaluation_ue_poids", viewonly=True)
def set_ue_poids(self, ue, poids: float):
"""Set poids évaluation vers cette UE"""
self.update_ue_poids_dict({ue.id: poids})
def set_ue_poids_dict(self, ue_poids_dict: dict):
"""set poids vers les UE (remplace existants)
ue_poids_dict = { ue_id : poids }
"""
L = []
for ue_id, poids in ue_poids_dict.items():
ue = NotesUE.query.get(ue_id)
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
self.ue_poids = L
def update_ue_poids_dict(self, ue_poids_dict: dict):
"""update poids vers UE (ajoute aux existants)"""
current = self.get_ue_poids_dict()
current.update(ue_poids_dict)
self.set_ue_poids_dict(current)
def get_ue_poids_dict(self):
"""returns { ue_id : poids }"""
return {p.ue.id: p.poids for p in self.ue_poids}
class EvaluationUEPoids(db.Model):
"""Poids des évaluations (BUT)
association many to many
"""
evaluation_id = db.Column(
db.Integer, db.ForeignKey("notes_evaluation.id"), primary_key=True
)
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), primary_key=True)
poids = db.Column(
db.Float,
nullable=False,
)
evaluation = db.relationship(
NotesEvaluation,
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
)
ue = db.relationship(
NotesUE, backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan")
)
def __repr__(self):
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
class NotesSemSet(db.Model): class NotesSemSet(db.Model):

View File

@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission
def sidebar_common(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
H = [ H = [
f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a> f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9.1</a>
<div id="authuser"><a id="authuserlink" href="{ <div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)

View File

@ -28,6 +28,39 @@
"""Semestres: Codes gestion parcours (constantes) """Semestres: Codes gestion parcours (constantes)
""" """
import collections import collections
import enum
@enum.unique
class CodesParcours(enum.IntEnum):
"""Codes numériques de sparcours, enregistrés en base
dans notes_formations.type_parcours
Ne pas modifier.
"""
Legacy = 0
DUT = 100
DUT4 = 110
DUTMono = 120
DUT2 = 130
LP = 200
LP2sem = 210
LP2semEvry = 220
LP2014 = 230
LP2sem2014 = 240
M2 = 250
M2noncomp = 251
Mono = 300
MasterLMD = 402
MasterIG = 403
LicenceUCAC3 = 501
MasterUCAC2 = 502
MonoUCAC = 503
GEN_6_SEM = 600
BUT = 700
ISCID6 = 1001
ISCID4 = 1002
NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999) # (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
@ -214,6 +247,7 @@ class TypeParcours(object):
ALLOWED_UE_TYPES = list( ALLOWED_UE_TYPES = list(
UE_TYPE_NAME.keys() UE_TYPE_NAME.keys()
) # par defaut, autorise tous les types d'UE ) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs
def check(self, formation=None): def check(self, formation=None):
return True, "" # status, diagnostic_message return True, "" # status, diagnostic_message
@ -262,6 +296,20 @@ def register_parcours(Parcours):
TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours
class ParcoursBUT(TypeParcours):
"""BUT Bachelor Universitaire de Technologie"""
TYPE_PARCOURS = 700
NAME = "BUT"
NB_SEM = 6
COMPENSATION_UE = False
APC_SAE = True
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
register_parcours(ParcoursBUT())
class ParcoursDUT(TypeParcours): class ParcoursDUT(TypeParcours):
"""DUT selon l'arrêté d'août 2005""" """DUT selon l'arrêté d'août 2005"""
@ -302,7 +350,7 @@ register_parcours(ParcoursDUTMono())
class ParcoursDUT2(ParcoursDUT): class ParcoursDUT2(ParcoursDUT):
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)""" """DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
TYPE_PARCOURS = 130 TYPE_PARCOURS = CodesParcours.DUT2
NAME = "DUT2" NAME = "DUT2"
NB_SEM = 2 NB_SEM = 2
@ -315,7 +363,7 @@ class ParcoursLP(TypeParcours):
(pour anciennes LP. Après 2014, préférer ParcoursLP2014) (pour anciennes LP. Après 2014, préférer ParcoursLP2014)
""" """
TYPE_PARCOURS = 200 TYPE_PARCOURS = CodesParcours.LP
NAME = "LP" NAME = "LP"
NB_SEM = 1 NB_SEM = 1
COMPENSATION_UE = False COMPENSATION_UE = False
@ -332,7 +380,7 @@ register_parcours(ParcoursLP())
class ParcoursLP2sem(ParcoursLP): class ParcoursLP2sem(ParcoursLP):
"""Licence Pro (en deux "semestres")""" """Licence Pro (en deux "semestres")"""
TYPE_PARCOURS = 210 TYPE_PARCOURS = CodesParcours.LP2sem
NAME = "LP2sem" NAME = "LP2sem"
NB_SEM = 2 NB_SEM = 2
COMPENSATION_UE = True COMPENSATION_UE = True
@ -345,7 +393,7 @@ register_parcours(ParcoursLP2sem())
class ParcoursLP2semEvry(ParcoursLP): class ParcoursLP2semEvry(ParcoursLP):
"""Licence Pro (en deux "semestres", U. Evry)""" """Licence Pro (en deux "semestres", U. Evry)"""
TYPE_PARCOURS = 220 TYPE_PARCOURS = CodesParcours.LP2semEvry
NAME = "LP2semEvry" NAME = "LP2semEvry"
NB_SEM = 2 NB_SEM = 2
COMPENSATION_UE = True COMPENSATION_UE = True
@ -371,7 +419,7 @@ class ParcoursLP2014(TypeParcours):
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_ # l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
# vérifiés par ScoDoc) # vérifiés par ScoDoc)
TYPE_PARCOURS = 230 TYPE_PARCOURS = CodesParcours.LP2014
NAME = "LP2014" NAME = "LP2014"
NB_SEM = 1 NB_SEM = 1
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP] ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
@ -415,7 +463,7 @@ register_parcours(ParcoursLP2014())
class ParcoursLP2sem2014(ParcoursLP): class ParcoursLP2sem2014(ParcoursLP):
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)""" """Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
TYPE_PARCOURS = 240 TYPE_PARCOURS = CodesParcours.LP2sem2014
NAME = "LP2014_2sem" NAME = "LP2014_2sem"
NB_SEM = 2 NB_SEM = 2
@ -427,7 +475,7 @@ register_parcours(ParcoursLP2sem2014())
class ParcoursM2(TypeParcours): class ParcoursM2(TypeParcours):
"""Master 2 (en deux "semestres")""" """Master 2 (en deux "semestres")"""
TYPE_PARCOURS = 250 TYPE_PARCOURS = CodesParcours.M2
NAME = "M2sem" NAME = "M2sem"
NB_SEM = 2 NB_SEM = 2
COMPENSATION_UE = True COMPENSATION_UE = True
@ -440,7 +488,7 @@ register_parcours(ParcoursM2())
class ParcoursM2noncomp(ParcoursM2): class ParcoursM2noncomp(ParcoursM2):
"""Master 2 (en deux "semestres") sans compensation""" """Master 2 (en deux "semestres") sans compensation"""
TYPE_PARCOURS = 251 TYPE_PARCOURS = CodesParcours.M2noncomp
NAME = "M2noncomp" NAME = "M2noncomp"
COMPENSATION_UE = False COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB)) UNUSED_CODES = set((ADC, ATT, ATB))
@ -452,7 +500,7 @@ register_parcours(ParcoursM2noncomp())
class ParcoursMono(TypeParcours): class ParcoursMono(TypeParcours):
"""Formation générique en une session""" """Formation générique en une session"""
TYPE_PARCOURS = 300 TYPE_PARCOURS = CodesParcours.Mono
NAME = "Mono" NAME = "Mono"
NB_SEM = 1 NB_SEM = 1
COMPENSATION_UE = False COMPENSATION_UE = False
@ -465,7 +513,7 @@ register_parcours(ParcoursMono())
class ParcoursLegacy(TypeParcours): class ParcoursLegacy(TypeParcours):
"""DUT (ancien ScoDoc, ne plus utiliser)""" """DUT (ancien ScoDoc, ne plus utiliser)"""
TYPE_PARCOURS = 0 TYPE_PARCOURS = CodesParcours.Legacy
NAME = "DUT" NAME = "DUT"
NB_SEM = 4 NB_SEM = 4
COMPENSATION_UE = None # backward compat: defini dans formsemestre COMPENSATION_UE = None # backward compat: defini dans formsemestre
@ -499,7 +547,7 @@ class ParcoursBachelorISCID6(ParcoursISCID):
"""ISCID: Bachelor en 3 ans (6 sem.)""" """ISCID: Bachelor en 3 ans (6 sem.)"""
NAME = "ParcoursBachelorISCID6" NAME = "ParcoursBachelorISCID6"
TYPE_PARCOURS = 1001 TYPE_PARCOURS = CodesParcours.ISCID6
NAME = "" NAME = ""
NB_SEM = 6 NB_SEM = 6
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
@ -510,7 +558,7 @@ register_parcours(ParcoursBachelorISCID6())
class ParcoursMasterISCID4(ParcoursISCID): class ParcoursMasterISCID4(ParcoursISCID):
"ISCID: Master en 2 ans (4 sem.)" "ISCID: Master en 2 ans (4 sem.)"
TYPE_PARCOURS = 1002 TYPE_PARCOURS = CodesParcours.ISCID4
NAME = "ParcoursMasterISCID4" NAME = "ParcoursMasterISCID4"
NB_SEM = 4 NB_SEM = 4
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
@ -536,7 +584,7 @@ class ParcoursUCAC(TypeParcours):
class ParcoursLicenceUCAC3(ParcoursUCAC): class ParcoursLicenceUCAC3(ParcoursUCAC):
"""UCAC: Licence en 3 sessions d'un an""" """UCAC: Licence en 3 sessions d'un an"""
TYPE_PARCOURS = 501 TYPE_PARCOURS = CodesParcours.LicenceUCAC3
NAME = "Licence UCAC en 3 sessions d'un an" NAME = "Licence UCAC en 3 sessions d'un an"
NB_SEM = 3 NB_SEM = 3
@ -547,7 +595,7 @@ register_parcours(ParcoursLicenceUCAC3())
class ParcoursMasterUCAC2(ParcoursUCAC): class ParcoursMasterUCAC2(ParcoursUCAC):
"""UCAC: Master en 2 sessions d'un an""" """UCAC: Master en 2 sessions d'un an"""
TYPE_PARCOURS = 502 TYPE_PARCOURS = CodesParcours.MasterUCAC2
NAME = "Master UCAC en 2 sessions d'un an" NAME = "Master UCAC en 2 sessions d'un an"
NB_SEM = 2 NB_SEM = 2
@ -558,7 +606,7 @@ register_parcours(ParcoursMasterUCAC2())
class ParcoursMonoUCAC(ParcoursUCAC): class ParcoursMonoUCAC(ParcoursUCAC):
"""UCAC: Formation en 1 session de durée variable""" """UCAC: Formation en 1 session de durée variable"""
TYPE_PARCOURS = 503 TYPE_PARCOURS = CodesParcours.MonoUCAC
NAME = "Formation UCAC en 1 session de durée variable" NAME = "Formation UCAC en 1 session de durée variable"
NB_SEM = 1 NB_SEM = 1
UNUSED_CODES = set((ADC, ATT, ATB)) UNUSED_CODES = set((ADC, ATT, ATB))
@ -570,7 +618,7 @@ register_parcours(ParcoursMonoUCAC())
class Parcours6Sem(TypeParcours): class Parcours6Sem(TypeParcours):
"""Parcours générique en 6 semestres""" """Parcours générique en 6 semestres"""
TYPE_PARCOURS = 600 TYPE_PARCOURS = CodesParcours.GEN_6_SEM
NAME = "Formation en 6 semestres" NAME = "Formation en 6 semestres"
NB_SEM = 6 NB_SEM = 6
COMPENSATION_UE = True COMPENSATION_UE = True
@ -592,7 +640,7 @@ register_parcours(Parcours6Sem())
class ParcoursMasterLMD(TypeParcours): class ParcoursMasterLMD(TypeParcours):
"""Master générique en 4 semestres dans le LMD""" """Master générique en 4 semestres dans le LMD"""
TYPE_PARCOURS = 402 TYPE_PARCOURS = CodesParcours.MasterLMD
NAME = "Master LMD" NAME = "Master LMD"
NB_SEM = 4 NB_SEM = 4
COMPENSATION_UE = True # variabale inutilisée COMPENSATION_UE = True # variabale inutilisée
@ -605,7 +653,7 @@ register_parcours(ParcoursMasterLMD())
class ParcoursMasterIG(ParcoursMasterLMD): class ParcoursMasterIG(ParcoursMasterLMD):
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)""" """Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
TYPE_PARCOURS = 403 TYPE_PARCOURS = CodesParcours.MasterIG
NAME = "Master IG P13" NAME = "Master IG P13"
BARRE_MOY = 10.0 BARRE_MOY = 10.0
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE

View File

@ -420,8 +420,8 @@ def module_edit(module_id=None):
"input_type": "menu", "input_type": "menu",
"title": "Type", "title": "Type",
"explanation": "", "explanation": "",
"labels": ("Standard", "Malus"), "labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)), "allowed_values": [str(int(x)) for x in scu.ModuleType],
"enabled": unlocked, "enabled": unlocked,
}, },
), ),

View File

@ -775,8 +775,12 @@ def _ue_table_ues(
) )
else: else:
H.append('<span class="locked">[verrouillé]</span>') H.append('<span class="locked">[verrouillé]</span>')
if parcours.APC_SAE:
func_html_list = _ue_table_ressources_saes
else:
func_html_list = _ue_table_matieres
H.append( H.append(
_ue_table_matieres( func_html_list(
parcours, parcours,
ue, ue,
editable, editable,
@ -837,6 +841,8 @@ def _ue_table_matieres(
delete_disabled_icon, delete_disabled_icon,
) )
) )
if not parcours.UE_IS_MODULE:
H.append("</li>")
if not matieres: if not matieres:
H.append("<li>Aucune matière dans cette UE ! ") H.append("<li>Aucune matière dans cette UE ! ")
if editable: if editable:
@ -855,6 +861,76 @@ def _ue_table_matieres(
return "\n".join(H) return "\n".join(H)
def _ue_table_ressources_saes(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des ressources et SAÉs d'une UE.
(pour les parcours APC_SAE)
"""
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
if not matieres:
# Les formations APC (BUT) n'utilisent pas de matières
# mais il doit y en avoir une par UE
# silently fix this on-the-fly to ease migration
_ = sco_edit_matiere.do_matiere_create(
{"ue_id": ue["ue_id"], "titre": "APC", "numero": 1},
)
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
assert matieres
mat = matieres[0]
H = [
"""
<ul class="notes_matiere_list but_matiere_list">
"""
]
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
for titre, element_name, module_type in (
("Ressources", "ressource", scu.ModuleType.RESSOURCE),
("SAÉs", "SAÉ", scu.ModuleType.SAE),
("Autres modules", "xxx", None),
):
H.append(f'<li class="notes_matiere_list">{titre}')
elements = [
m
for m in modules
if module_type == m["module_type"]
or (
(module_type is None)
and m["module_type"]
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
)
]
H.append(
_ue_table_modules(
parcours,
mat,
elements,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
empty_list_msg="Aucune " + element_name,
create_element_msg="créer une " + element_name,
add_suppress_link=False,
)
)
H.append("</li></ul>")
return "\n".join(H)
def _ue_table_modules( def _ue_table_modules(
parcours, parcours,
mat, mat,
@ -866,6 +942,10 @@ def _ue_table_modules(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
unit_name="matière",
add_suppress_link=True, # lien "supprimer cette matière"
empty_list_msg="Aucun élément dans cette matière",
create_element_msg="créer un module",
): ):
"""Édition de programme: liste des modules d'une matière d'une UE""" """Édition de programme: liste des modules d'une matière d'une UE"""
H = ['<ul class="notes_module_list">'] H = ['<ul class="notes_module_list">']
@ -948,8 +1028,8 @@ def _ue_table_modules(
) )
H.append("</li>") H.append("</li>")
if not modules: if not modules:
H.append("<li>Aucun module dans cette matière ! ") H.append(f"<li>{empty_list_msg} ! ")
if editable: if editable and add_suppress_link:
H.append( H.append(
f"""<a class="stdlink" href="{ f"""<a class="stdlink" href="{
url_for("notes.matiere_delete", url_for("notes.matiere_delete",
@ -963,11 +1043,10 @@ def _ue_table_modules(
f"""<li> <a class="stdlink" href="{ f"""<li> <a class="stdlink" href="{
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}" scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
>créer un module</a></li> >{create_element_msg}</a></li>
""" """
) )
H.append("</ul>") H.append("</ul>")
H.append("</li>")
return "\n".join(H) return "\n".join(H)

View File

@ -32,13 +32,12 @@ import base64
import bisect import bisect
import copy import copy
import datetime import datetime
from enum import IntEnum
import json import json
from hashlib import md5 from hashlib import md5
import numbers import numbers
import os import os
import pydot
import re import re
import requests
import _thread import _thread
import time import time
import unicodedata import unicodedata
@ -46,6 +45,8 @@ import urllib
from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode
from PIL import Image as PILImage from PIL import Image as PILImage
import pydot
import requests
from flask import g, request from flask import g, request
from flask import url_for, make_response from flask import url_for, make_response
@ -73,6 +74,17 @@ NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutrali
MODULE_STANDARD = 0 MODULE_STANDARD = 0
MODULE_MALUS = 1 MODULE_MALUS = 1
class ModuleType(IntEnum):
"""Code des types de module."""
# Stockés en BD dans NotesModule.module_type: ne pas modifier ces valeurs
STANDARD = 0
MALUS = 1
RESSOURCE = 2 # BUT
SAE = 3 # BUT
MALUS_MAX = 20.0 MALUS_MAX = 20.0
MALUS_MIN = -20.0 MALUS_MIN = -20.0

View File

@ -0,0 +1,64 @@
"""BUT: poids evaluations, AC
Revision ID: ada0d1f3d84f
Revises: 75cf18659984
Create Date: 2021-11-07 22:49:22.697211
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ada0d1f3d84f"
down_revision = "75cf18659984"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"app_crit",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("code", sa.Text(), nullable=False),
sa.Column("titre", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"modules_acs",
sa.Column("module_id", sa.Integer(), nullable=True),
sa.Column("ac_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["ac_id"],
["app_crit.id"],
),
sa.ForeignKeyConstraint(
["module_id"],
["notes_modules.id"],
),
)
op.create_table(
"evaluation_ue_poids",
sa.Column("evaluation_id", sa.Integer(), nullable=False),
sa.Column("ue_id", sa.Integer(), nullable=False),
sa.Column("poids", sa.Float(), nullable=False),
sa.ForeignKeyConstraint(
["evaluation_id"],
["notes_evaluation.id"],
),
sa.ForeignKeyConstraint(
["ue_id"],
["notes_ue.id"],
),
sa.PrimaryKeyConstraint("evaluation_id", "ue_id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("evaluation_ue_poids")
op.drop_table("modules_acs")
op.drop_table("app_crit")
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.0.61" SCOVERSION = "9.1.0"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -0,0 +1,83 @@
"""
Test modèles évaluations avec poids BUT
"""
from tests.unit import sco_fake_gen
from app import db
from app import models
"""
mapp.set_sco_dept("RT")
from app.auth.models import get_super_admin
admin_user = get_super_admin()
ctx.push()
login_user(admin_user)
"""
def test_evaluation_poids(test_client):
"""Association de poids vers les UE"""
G = sco_fake_gen.ScoFake(verbose=False)
_f = G.create_formation(
acronyme="F3", titre="Formation 2", titre_officiel="Titre officiel 2"
)
_ue1 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1")
_ue2 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2")
_ue3 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3")
_mat = G.create_matiere(ue_id=_ue1["ue_id"], titre="matière test")
_mod = G.create_module(
matiere_id=_mat["matiere_id"],
code="TSM1",
coefficient=1.0,
titre="module test",
ue_id=_ue1["ue_id"],
formation_id=_f["formation_id"],
)
sem = G.create_formsemestre(
formation_id=_f["formation_id"],
semestre_id=1,
date_debut="01/01/2021",
date_fin="30/06/2021",
) # formsemestre_id=716
mi = G.create_moduleimpl(
module_id=_mod["module_id"],
formsemestre_id=sem["formsemestre_id"],
)
moduleimpl_id = mi["id"]
_e1 = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
jour="01/01/2021",
description="evaluation 1",
coefficient=0,
)
evaluation_id = _e1["evaluation_id"] # evaluation_id=25246
ue1_id = _ue1["id"] # ue1_id=1684
formation_id = _f["id"] # formation_id=199
#
e1 = models.NotesEvaluation.query.get(evaluation_id)
ue1 = models.NotesUE.query.get(ue1_id)
assert e1.ue_poids == []
p1 = 3.14
e1.set_ue_poids(ue1, p1)
db.session.commit()
assert e1.get_ue_poids_dict()[ue1_id] == p1
ues = models.NotesUE.query.filter_by(formation_id=formation_id).all()
poids = [1.0, 2.0, 3.0]
for (ue, p) in zip(ues, poids):
e1.set_ue_poids(ue, p)
assert len(e1.ue_poids) == len(ues)
assert e1.get_ue_poids_dict()[ues[1].id] == poids[1]
e1.set_ue_poids(ue1, p1)
db.session.commit()
poids2 = [10, 20]
e1.update_ue_poids_dict({ue.id: p for (ue, p) in zip(ues[:-1], poids2)})
assert e1.get_ue_poids_dict()[ues[0].id] == poids2[0]
assert e1.get_ue_poids_dict()[ues[1].id] == poids2[1]
assert e1.get_ue_poids_dict()[ues[2].id] == poids[2]
# Delete UE
db.session.delete(ues[2])
db.session.commit()
# Delete eval
db.session.delete(e1)
db.session.commit()
assert len(models.EvaluationUEPoids.query.all()) == 0