Merge branch 'scodoc-master' into pe-moy-par-ue

This commit is contained in:
Cléo Baras 2024-02-16 09:37:52 +01:00
commit d8381884dc
27 changed files with 399 additions and 542 deletions

View File

@ -38,14 +38,11 @@ import datetime
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from app import log
from app import db, log
from app.but import bulletin_but
from app.models import BulAppreciations, FormSemestre, Identite
from app.models import BulAppreciations, FormSemestre, Identite, UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_xml
@ -202,12 +199,12 @@ def bulletin_but_xml_compat(
if e.visibulletin or version == "long":
x_eval = Element(
"evaluation",
date_debut=e.date_debut.isoformat()
if e.date_debut
else "",
date_fin=e.date_fin.isoformat()
if e.date_debut
else "",
date_debut=(
e.date_debut.isoformat() if e.date_debut else ""
),
date_fin=(
e.date_fin.isoformat() if e.date_debut else ""
),
coefficient=str(e.coefficient),
# pas les poids en XML compat
evaluation_type=str(e.evaluation_type),
@ -215,9 +212,9 @@ def bulletin_but_xml_compat(
# notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max),
# --- deprecated
jour=e.date_debut.isoformat()
if e.date_debut
else "",
jour=(
e.date_debut.isoformat() if e.date_debut else ""
),
heure_debut=e.heure_debut(),
heure_fin=e.heure_fin(),
)
@ -294,14 +291,15 @@ def bulletin_but_xml_compat(
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
ue = db.session.get(UniteEns, ue_id)
if ue:
doc.append(
Element(
"decision_ue",
ue_id=str(ue["ue_id"]),
numero=quote_xml_attr(ue["numero"]),
acronyme=quote_xml_attr(ue["acronyme"]),
titre=quote_xml_attr(ue["titre"]),
ue_id=str(ue.id),
numero=quote_xml_attr(ue.numero),
acronyme=quote_xml_attr(ue.acronyme),
titre=quote_xml_attr(ue.titre or ""),
code=decision["decisions_ue"][ue_id]["code"],
)
)

View File

@ -58,7 +58,6 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA"
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_cursus()
self._modimpls_dict_by_ue = {} # local cache
@ -217,9 +216,9 @@ class NotesTableCompat(ResultatsSemestre):
# Rangs / UEs:
for ue in ues:
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
self.ue_rangs_by_group.setdefault(ue.id, {})[
group.id
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
)
def get_etud_rang(self, etudid: int) -> str:
"""Le rang (classement) de l'étudiant dans le semestre.

View File

@ -79,6 +79,7 @@ class Evaluation(db.Model):
):
"""Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs.
Add to session, do not commit.
"""
if not moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
@ -94,6 +95,8 @@ class Evaluation(db.Model):
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
#
evaluation = Evaluation(**args)
db.session.add(evaluation)
db.session.flush()
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
url = url_for(
"notes.moduleimpl_status",
@ -210,9 +213,9 @@ class Evaluation(db.Model):
"visibulletin": self.visibulletin,
# Deprecated (supprimer avant #sco9.7)
"date": self.date_debut.date().isoformat() if self.date_debut else "",
"heure_debut": self.date_debut.time().isoformat()
if self.date_debut
else "",
"heure_debut": (
self.date_debut.time().isoformat() if self.date_debut else ""
),
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
}

View File

@ -1,8 +1,10 @@
"""ScoDoc 9 models : Modules
"""
from flask import current_app, g
from app import db
from app import models
from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
from app.scodoc import sco_utils as scu
@ -11,7 +13,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
class Module(db.Model):
class Module(models.ScoDocModel):
"""Module"""
__tablename__ = "notes_modules"
@ -76,6 +78,28 @@ class Module(db.Model):
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No other side effect.
returns: dict to store in model's db.
"""
# s'assure que ects etc est non ''
fs_empty_stored_as_nulls = {
"coefficient",
"ects",
"heures_cours",
"heures_td",
"heures_tp",
}
args_dict = {}
for key, value in args.items():
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
if key in fs_empty_stored_as_nulls and value == "":
value = None
args_dict[key] = value
return args_dict
def clone(self):
"""Create a new copy of this module."""
mod = Module(

View File

@ -409,6 +409,7 @@ class CursusBUT(TypeCursus):
APC_SAE = True
USE_REFERENTIEL_COMPETENCES = True
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
ECTS_DIPLOME = 180
register_cursus(CursusBUT())

View File

@ -44,13 +44,15 @@ import random
from collections import OrderedDict
from xml.etree import ElementTree
import json
from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color
from reportlab.lib import styles
from reportlab.lib.units import inch, cm, mm
from reportlab.rl_config import defaultPageSize # pylint: disable=no-name-in-module
from reportlab.lib.units import cm
from app.scodoc import html_sco_header
from app.scodoc import sco_utils as scu
@ -62,16 +64,32 @@ from app.scodoc.sco_pdf import SU
from app import log, ScoDocJSONEncoder
def mark_paras(L, tags) -> list[str]:
"""Put each (string) element of L between <tag>...</tag>,
def mark_paras(items: list[Any], tags: list[str]) -> list[str]:
"""Put each string element of items between <tag>...</tag>,
for each supplied tag.
Leave non string elements untouched.
"""
for tag in tags:
start = "<" + tag + ">"
end = "</" + tag.split()[0] + ">"
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
return L
items = [(start + (x or "") + end) if isinstance(x, str) else x for x in items]
return items
def add_query_param(url: str, key: str, value: str) -> str:
"add parameter key=value to the given URL"
# Parse the URL
parsed_url = urlparse(url)
# Parse the query parameters
query_params = parse_qs(parsed_url.query)
# Add or update the query parameter
query_params[key] = [value]
# Encode the query parameters
encoded_query_params = urlencode(query_params, doseq=True)
# Construct the new URL
new_url_parts = parsed_url._replace(query=encoded_query_params)
new_url = urlunparse(new_url_parts)
return new_url
class DEFAULT_TABLE_PREFERENCES(object):
@ -477,13 +495,15 @@ class GenTable:
H.append('<span class="gt_export_icons">')
if self.xls_link:
H.append(
' <a href="%s&fmt=xls">%s</a>' % (self.base_url, scu.ICON_XLS)
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
}">{scu.ICON_XLS}</a>"""
)
if self.xls_link and self.pdf_link:
H.append("&nbsp;")
if self.pdf_link:
H.append(
' <a href="%s&fmt=pdf">%s</a>' % (self.base_url, scu.ICON_PDF)
f""" <a href="{add_query_param(self.base_url, "fmt", "pdf")
}">{scu.ICON_PDF}</a>"""
)
H.append("</span>")
H.append("</p>")
@ -582,9 +602,11 @@ class GenTable:
for line in data_list:
Pt.append(
[
(
Paragraph(SU(str(x)), CellStyle)
if (not isinstance(x, Paragraph))
else x
)
for x in line
]
)

View File

@ -109,7 +109,7 @@ def sidebar_common():
{sidebar_dept()}
<h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
"""
]
if current_user.has_permission(Permission.AbsChange):

View File

@ -114,7 +114,7 @@ def index_html(showcodes=0, showsemtable=0):
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Programmes</a>,
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
@ -336,15 +336,15 @@ def _style_sems(sems):
else:
sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
sem[
"_elt_annee_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
sem[
"_elt_sem_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
sem["_etapes_apo_str_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
)
sem["_elt_annee_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
)
sem["_elt_sem_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
)
def delete_dept(dept_id: int) -> str:

View File

@ -412,7 +412,7 @@ def module_move(module_id, after=0, redirect=True):
db.session.add(neigh)
db.session.commit()
module.formation.invalidate_cached_sems()
# redirect to ue_list page:
# redirect to ue_table page:
if redirect:
return flask.redirect(
url_for(
@ -454,7 +454,7 @@ def ue_move(ue_id, after=0, redirect=1):
db.session.commit()
ue.formation.invalidate_cached_sems()
# redirect to ue_list page
# redirect to ue_table page
if redirect:
return flask.redirect(
url_for(

View File

@ -106,9 +106,9 @@ def do_module_create(args) -> int:
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
# create
cnx = ndb.GetDBConnexion()
module_id = _moduleEditor.create(cnx, args)
log(f"do_module_create: created {module_id} with {args}")
module = Module.create_from_dict(args)
db.session.commit()
log(f"do_module_create: created {module.id} with {args}")
# news
ScolarNews.add(
@ -117,7 +117,7 @@ def do_module_create(args) -> int:
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
return module_id
return module.id
def module_create(
@ -666,7 +666,7 @@ def module_edit(
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
"type": "int",
"default": default_num,
"allow_null": False,
"allow_null": True,
},
),
]
@ -811,6 +811,10 @@ def module_edit(
)
)
else:
if isinstance(tf[2]["numero"], str):
tf[2]["numero"] = tf[2]["numero"].strip()
if not isinstance(tf[2]["numero"], int) and not tf[2]["numero"]:
tf[2]["numero"] = tf[2]["numero"] or default_num
if create:
if not matiere_id:
# formulaire avec choix UE de rattachement

View File

@ -766,7 +766,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
page_title=f"Programme {formation.acronyme} v{formation.version}",
page_title=f"Formation {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.html()} {lockicon}
</h2>
@ -888,7 +888,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append(
f"""
<div class="formation_ue_list">
<div class="ue_list_tit">Programme pédagogique:</div>
<div class="ue_list_tit">Formation (programme pédagogique):</div>
<form>
<input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
@ -1054,7 +1054,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
# <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
warn, _ = sco_formsemestre_validation.check_formation_ues(formation_id)
warn, _ = sco_formsemestre_validation.check_formation_ues(formation)
H.append(warn)
H.append(html_sco_header.sco_footer())

View File

@ -30,7 +30,7 @@
import xml.dom.minidom
import flask
from flask import flash, g, url_for
from flask import flash, g, request, url_for
from flask_login import current_user
import app.scodoc.sco_utils as scu
@ -495,7 +495,7 @@ def formation_list_table() -> GenTable:
returns a table
"""
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
title = "Programmes pédagogiques"
title = "Formations (programmes pédagogiques)"
lockicon = scu.icontag(
"lock32_img", title="Comporte des semestres verrouillés", border="0"
)
@ -627,7 +627,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url="{request.base_url}?formation_id={formation_id}",
base_url=f"{request.base_url}",
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),

View File

@ -304,12 +304,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"input_type": "text_suggest",
"size": 50,
"title": "(Co-)Directeur(s) des études"
"title": (
"(Co-)Directeur(s) des études"
if index
else "Directeur des études",
"explanation": "(facultatif) taper le début du nom et choisir dans le menu"
else "Directeur des études"
),
"explanation": (
"(facultatif) taper le début du nom et choisir dans le menu"
if index
else "(obligatoire) taper le début du nom et choisir dans le menu",
else "(obligatoire) taper le début du nom et choisir dans le menu"
),
"allowed_values": allowed_user_names,
"allow_null": index, # > 0, # il faut au moins un responsable de semestre
"text_suggest_options": {
@ -356,9 +360,11 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"title": "Semestre dans la formation",
"allowed_values": semestre_id_list,
"labels": semestre_id_labels,
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
"explanation": (
"en BUT, on ne peut pas modifier le semestre après création"
if is_apc
else "",
else ""
),
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
},
),
@ -1636,13 +1642,13 @@ def formsemestre_change_publication_bul(
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""Changement manuel des coefficients des UE capitalisées."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
if not ok:
return err
footer = html_sco_header.sco_footer()
help = """<p class="help">
help_msg = """<p class="help">
Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
</p>
<p class="help">ScoDoc calcule normalement le coefficient d'une UE comme la somme des
@ -1665,17 +1671,16 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""
H = [
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
help,
help_msg,
]
#
ues, modimpls = _get_sem_ues_modimpls(formsemestre_id)
ues, modimpls = _get_sem_ues_modimpls(formsemestre)
sum_coefs_by_ue_id = {}
for ue in ues:
ue["sum_coefs"] = sum(
[
mod["module"]["coefficient"]
for mod in modimpls
if mod["module"]["ue_id"] == ue["ue_id"]
]
sum_coefs_by_ue_id[ue.id] = sum(
modimpl.module.coefficient
for modimpl in modimpls
if modimpl.module.ue_id == ue.id
)
cnx = ndb.GetDBConnexion()
@ -1684,20 +1689,20 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
form = [("formsemestre_id", {"input_type": "hidden"})]
for ue in ues:
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if coefs:
initvalues["ue_" + str(ue["ue_id"])] = coefs[0]["coefficient"]
initvalues["ue_" + str(ue.id)] = coefs[0]["coefficient"]
else:
initvalues["ue_" + str(ue["ue_id"])] = "auto"
initvalues["ue_" + str(ue.id)] = "auto"
descr = {
"size": 10,
"title": ue["acronyme"],
"explanation": "somme coefs modules = %s" % ue["sum_coefs"],
"title": ue.acronyme,
"explanation": f"somme coefs modules = {sum_coefs_by_ue_id[ue.id]}",
}
if ue["ue_id"] == err_ue_id:
if ue.id == err_ue_id:
descr["dom_id"] = "erroneous_ue"
form.append(("ue_" + str(ue["ue_id"]), descr))
form.append(("ue_" + str(ue.id), descr))
tf = TrivialFormulator(
request.base_url,
@ -1722,12 +1727,12 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
# 1- supprime les coef qui ne sont plus forcés
# 2- modifie ou cree les coefs
ue_deleted = []
ue_modified = []
ue_modified: list[tuple[UniteEns, float]] = []
msg = []
for ue in ues:
val = tf[2]["ue_" + str(ue["ue_id"])]
val = tf[2]["ue_" + str(ue.id)]
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if val == "" or val == "auto":
# supprime ce coef (il sera donc calculé automatiquement)
@ -1737,13 +1742,11 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
try:
val = float(val)
if (not coefs) or (coefs[0]["coefficient"] != val):
ue["coef"] = val
ue_modified.append(ue)
except:
ue_modified.append((ue, val))
except ValueError:
ok = False
msg.append(
"valeur invalide (%s) pour le coefficient de l'UE %s"
% (val, ue["acronyme"])
f"valeur invalide ({val}) pour le coefficient de l'UE {ue.acronyme}"
)
if not ok:
@ -1755,26 +1758,24 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
)
# apply modifications
for ue in ue_modified:
for ue, val in ue_modified:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue["ue_id"], ue["coef"]
cnx, formsemestre_id, ue.id, val
)
for ue in ue_deleted:
sco_formsemestre.do_formsemestre_uecoef_delete(
cnx, formsemestre_id, ue["ue_id"]
)
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue.id)
if ue_modified or ue_deleted:
message = ["""<h3>Modification effectuées</h3>"""]
if ue_modified:
message.append("""<h4>Coefs modifiés dans les UE:<h4><ul>""")
for ue in ue_modified:
message.append("<li>%(acronyme)s : %(coef)s</li>" % ue)
for ue, val in ue_modified:
message.append(f"<li>{ue.acronyme} : {val}</li>")
message.append("</ul>")
if ue_deleted:
message.append("""<h4>Coefs supprimés dans les UE:<h4><ul>""")
for ue in ue_deleted:
message.append("<li>%(acronyme)s</li>" % ue)
message.append(f"<li>{ue.acronyme}</li>")
message.append("</ul>")
else:
message = ["""<h3>Aucune modification</h3>"""]
@ -1792,21 +1793,19 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""
def _get_sem_ues_modimpls(formsemestre_id, modimpls=None):
def _get_sem_ues_modimpls(
formsemestre: FormSemestre,
) -> tuple[list[UniteEns], list[ModuleImpl]]:
"""Get liste des UE du semestre (à partir des moduleimpls)
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
"""
if modimpls is None:
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
uedict = {}
modimpls = formsemestre.modimpls.all()
for modimpl in modimpls:
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
modimpl["module"] = mod
if not mod["ue_id"] in uedict:
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
uedict[ue["ue_id"]] = ue
if not modimpl.module.ue_id in uedict:
uedict[modimpl.module.ue.id] = modimpl.module.ue
ues = list(uedict.values())
ues.sort(key=lambda u: u["numero"])
ues.sort(key=lambda u: u.numero)
return ues, modimpls

View File

@ -305,18 +305,15 @@ def do_formsemestre_inscription_with_modules(
# 2- inscrit aux groupes
for group_id in group_ids:
if group_id and group_id not in gdone:
group = GroupDescr.query.get_or_404(group_id)
_ = GroupDescr.query.get_or_404(group_id)
sco_groups.set_group(etudid, group_id)
gdone[group_id] = 1
# Inscription à tous les modules de ce semestre
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
for mod in modimpls:
if mod["ue"]["type"] != UE_SPORT:
for modimpl in formsemestre.modimpls:
if modimpl.module.ue.type != UE_SPORT:
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
{"moduleimpl_id": modimpl.id, "etudid": etudid},
formsemestre_id=formsemestre_id,
)
# Mise à jour des inscriptions aux parcours:
@ -531,19 +528,17 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
if not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
footer = html_sco_header.sco_footer()
H = [
html_sco_header.sco_header()
+ "<h2>Inscription de %s aux modules de %s (%s - %s)</h2>"
% (etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"])
html_sco_header.sco_header(),
f"""<h2>Inscription de {etud.nomprenom} aux modules de {formsemestre.titre_mois()}</h2>""",
]
# Cherche les moduleimpls et les inscriptions
mods = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid)
# Formulaire
modimpls_by_ue_ids = collections.defaultdict(list) # ue_id : [ moduleimpl_id ]
@ -551,26 +546,26 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
ues = []
ue_ids = set()
initvalues = {}
for mod in mods:
ue_id = mod["ue"]["ue_id"]
for modimpl in formsemestre.modimpls:
ue_id = modimpl.module.ue.id
if not ue_id in ue_ids:
ues.append(mod["ue"])
ues.append(modimpl.module.ue)
ue_ids.add(ue_id)
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
modimpls_by_ue_ids[ue_id].append(modimpl.id)
modimpls_by_ue_names[ue_id].append(
"%s %s" % (mod["module"]["code"] or "", mod["module"]["titre"] or "")
f"{modimpl.module.code or ''} {modimpl.module.titre or ''}"
)
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
# inscrit ?
for ins in inscr:
if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
key = "moduleimpls_%s" % ue_id
if ins["moduleimpl_id"] == modimpl.id:
key = f"moduleimpls_{ue_id}"
if key in initvalues:
initvalues[key].append(str(mod["moduleimpl_id"]))
initvalues[key].append(str(modimpl.id))
else:
initvalues[key] = [str(mod["moduleimpl_id"])]
initvalues[key] = [str(modimpl.id)]
break
descr = [
@ -578,10 +573,10 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
("etudid", {"input_type": "hidden"}),
]
for ue in ues:
ue_id = ue["ue_id"]
ue_descr = ue["acronyme"]
if ue["type"] != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
ue_id = ue.id
ue_descr = ue.acronyme
if ue.type != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue.type]
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
@ -606,7 +601,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
)
descr.append(
(
"moduleimpls_%s" % ue_id,
f"moduleimpls_{ue_id}",
{
"input_type": "checkbox",
"title": "",
@ -654,21 +649,20 @@ function chkbx_select(field_id, state) {
"""
)
return "\n".join(H) + "\n" + tf[1] + footer
elif tf[0] == -1:
if tf[0] == -1:
return flask.redirect(
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
else:
# Inscriptions aux modules choisis
# il faut desinscrire des modules qui ne figurent pas
# et inscrire aux autres, sauf si deja inscrit
a_desinscrire = {}.fromkeys([x["moduleimpl_id"] for x in mods])
a_desinscrire = {}.fromkeys([x.id for x in formsemestre.modimpls])
insdict = {}
for ins in inscr:
insdict[ins["moduleimpl_id"]] = ins
for ue in ues:
ue_id = ue["ue_id"]
for moduleimpl_id in [int(x) for x in tf[2]["moduleimpls_%s" % ue_id]]:
for moduleimpl_id in [int(x) for x in tf[2][f"moduleimpls_{ue.id}"]]:
if moduleimpl_id in a_desinscrire:
del a_desinscrire[moduleimpl_id]
# supprime ceux auxquel pas inscrit
@ -679,42 +673,36 @@ function chkbx_select(field_id, state) {
a_inscrire = set()
for ue in ues:
ue_id = ue["ue_id"]
a_inscrire.update(
int(x) for x in tf[2]["moduleimpls_%s" % ue_id]
int(x) for x in tf[2][f"moduleimpls_{ue.id}"]
) # conversion en int !
# supprime ceux auquel deja inscrit:
for ins in inscr:
if ins["moduleimpl_id"] in a_inscrire:
a_inscrire.remove(ins["moduleimpl_id"])
# dict des modules:
modsdict = {}
for mod in mods:
modsdict[mod["moduleimpl_id"]] = mod
modimpls_by_id = {modimpl.id: modimpl for modimpl in formsemestre.modimpls}
#
if (not a_inscrire) and (not a_desinscrire):
H.append(
"""<h3>Aucune modification à effectuer</h3>
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
f"""<h3>Aucune modification à effectuer</h3>
<p><a class="stdlink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">retour à la fiche étudiant</a></p>
"""
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
return "\n".join(H) + footer
H.append("<h3>Confirmer les modifications:</h3>")
if a_desinscrire:
H.append(
"<p>%s va être <b>désinscrit%s</b> des modules:<ul><li>"
% (etud["nomprenom"], etud["ne"])
f"""<p>{etud.nomprenom} va être <b>désinscrit{etud.e}</b> des modules:<ul><li>"""
)
H.append(
"</li><li>".join(
[
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
f"""{modimpls_by_id[x].module.titre or ''} ({
modimpls_by_id[x].module.code or '(module sans code)'})"""
for x in a_desinscrire
]
)
@ -723,17 +711,13 @@ function chkbx_select(field_id, state) {
H.append("</li></ul>")
if a_inscrire:
H.append(
"<p>%s va être <b>inscrit%s</b> aux modules:<ul><li>"
% (etud["nomprenom"], etud["ne"])
f"""<p>{etud.nomprenom} va être <b>inscrit{etud.e}</b> aux modules:<ul><li>"""
)
H.append(
"</li><li>".join(
[
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
f"""{modimpls_by_id[x].module.titre or ''} ({
modimpls_by_id[x].module.code or '(module sans code)'})"""
for x in a_inscrire
]
)
@ -743,20 +727,17 @@ function chkbx_select(field_id, state) {
modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
H.append(
"""<form action="do_moduleimpl_incription_options">
<input type="hidden" name="etudid" value="%s"/>
<input type="hidden" name="modulesimpls_ainscrire" value="%s"/>
<input type="hidden" name="modulesimpls_adesinscrire" value="%s"/>
f"""
<form action="do_moduleimpl_incription_options">
<input type="hidden" name="etudid" value="{etudid}"/>
<input type="hidden" name="modulesimpls_ainscrire" value="{modulesimpls_ainscrire}"/>
<input type="hidden" name="modulesimpls_adesinscrire" value="{modulesimpls_adesinscrire}"/>
<input type ="submit" value="Confirmer"/>
<input type ="button" value="Annuler" onclick="document.location='%s';"/>
<input type ="button" value="Annuler" onclick="document.location='{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}';"/>
</form>
"""
% (
etudid,
modulesimpls_ainscrire,
modulesimpls_adesinscrire,
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
)
)
return "\n".join(H) + footer

View File

@ -909,37 +909,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
return "\n".join(H)
def html_expr_diagnostic(diagnostics):
"""Affiche messages d'erreur des formules utilisateurs"""
H = []
H.append('<div class="ue_warning">Erreur dans des formules utilisateurs:<ul>')
last_id, last_msg = None, None
for diag in diagnostics:
if "moduleimpl_id" in diag:
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=diag["moduleimpl_id"]
)[0]
H.append(
'<li>module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a>: %s</li>'
% (
diag["moduleimpl_id"],
mod["module"]["abbrev"] or mod["module"]["code"] or "?",
diag["msg"],
)
)
else:
if diag["ue_id"] != last_id or diag["msg"] != last_msg:
ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0]
H.append(
'<li>UE "%s": %s</li>'
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
)
last_id, last_msg = diag["ue_id"], diag["msg"]
H.append("</ul></div>")
return "".join(H)
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
"""En-tête HTML des pages "semestre" """
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
@ -1081,9 +1050,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
>Toutes évaluations (même incomplètes) visibles</div>"""
)
if nt.expr_diagnostics:
H.append(html_expr_diagnostic(nt.expr_diagnostics))
if nt.parcours.APC_SAE:
# BUT: tableau ressources puis SAE
ressources = [

View File

@ -1217,7 +1217,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
<div id="ue_list_code" class="sco_box sco_green_bg">
<!-- filled by ue_sharing_code -->
</div>
{check_formation_ues(formation.id)[0]}
{check_formation_ues(formation)[0]}
{html_sco_header.sco_footer()}
"""
@ -1376,15 +1376,14 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
def check_formation_ues(formation_id):
def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[UniteEns]]]:
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
définition du programme: cette fonction retourne un bout de HTML
à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
"""
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
ue_multiples = {} # { ue_id : [ liste des formsemestre ] }
for ue in ues:
for ue in formation.ues:
# formsemestres utilisant cette ue ?
sems = ndb.SimpleDictFetch(
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.*
@ -1394,9 +1393,9 @@ def check_formation_ues(formation_id):
AND mi.formsemestre_id = sem.id
AND mod.ue_id = %(ue_id)s
""",
{"ue_id": ue["ue_id"], "formation_id": formation_id},
{"ue_id": ue.id, "formation_id": formation.id},
)
semestre_ids = set([x["semestre_id"] for x in sems])
semestre_ids = {x["semestre_id"] for x in sems}
if (
len(semestre_ids) > 1
): # plusieurs semestres d'indices differents dans le cursus
@ -1416,11 +1415,11 @@ def check_formation_ues(formation_id):
<ul>
"""
]
for ue in ues:
if ue["ue_id"] in ue_multiples:
for ue in formation.ues:
if ue.id in ue_multiples:
sems = [
sco_formsemestre.get_formsemestre(x["formsemestre_id"])
for x in ue_multiples[ue["ue_id"]]
for x in ue_multiples[ue.id]
]
slist = ", ".join(
[
@ -1429,7 +1428,7 @@ def check_formation_ues(formation_id):
for s in sems
]
)
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
H.append("<li><b>%s</b> : %s</li>" % (ue.acronyme, slist))
H.append("</ul></div>")
return "\n".join(H), ue_multiples

View File

@ -56,6 +56,7 @@ _moduleimplEditor = ndb.EditableTable(
def do_moduleimpl_create(args):
"create a moduleimpl"
# TODO remplacer par une methode de ModuleImpl qui appelle super().create_from_dict() puis invalide le formsemestre
cnx = ndb.GetDBConnexion()
r = _moduleimplEditor.create(cnx, args)
sco_cache.invalidate_formsemestre(
@ -109,91 +110,6 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
) # > modif moduleimpl
def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, sort_by_ue=False
) -> list:
"""Liste les moduleimpls et ajoute dans chacun
l'UE, la matière et le module auxquels ils appartiennent.
Tri la liste par:
- pour les formations classiques: semestre/UE/numero_matiere/numero_module;
- pour le BUT: ignore UEs sauf si sort_by_ue et matières dans le tri.
NB: Cette fonction faisait partie de l'API ScoDoc 7.
"""
from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
modimpls = moduleimpl_list(
**{
"moduleimpl_id": moduleimpl_id,
"formsemestre_id": formsemestre_id,
"module_id": module_id,
}
)
if not modimpls:
return []
ues = {}
matieres = {}
modules = {}
for mi in modimpls:
module_id = mi["module_id"]
if not mi["module_id"] in modules:
modules[module_id] = sco_edit_module.module_list(
args={"module_id": module_id}
)[0]
mi["module"] = modules[module_id]
ue_id = mi["module"]["ue_id"]
if not ue_id in ues:
ues[ue_id] = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
mi["ue"] = ues[ue_id]
matiere_id = mi["module"]["matiere_id"]
if not matiere_id in matieres:
matieres[matiere_id] = sco_edit_matiere.matiere_list(
args={"matiere_id": matiere_id}
)[0]
mi["matiere"] = matieres[matiere_id]
mod = modimpls[0]["module"]
formation = db.session.get(Formation, mod["formation_id"])
if formation.is_apc():
# tri par numero_module
if sort_by_ue:
modimpls.sort(
key=lambda x: (
x["ue"]["numero"],
x["ue"]["ue_id"],
x["module"]["module_type"],
x["module"]["numero"],
x["module"]["code"],
)
)
else:
modimpls.sort(
key=lambda x: (
x["module"]["module_type"],
x["module"]["numero"],
x["module"]["code"],
)
)
else:
# Formations classiques, avec matières:
# tri par semestre/UE/numero_matiere/numero_module
modimpls.sort(
key=lambda x: (
x["ue"]["numero"],
x["ue"]["ue_id"],
x["matiere"]["numero"],
x["matiere"]["matiere_id"],
x["module"]["numero"],
x["module"]["code"],
)
)
return modimpls
def moduleimpls_in_external_ue(ue_id):
"""List of modimpls in this ue"""
cursor = ndb.SimpleQuery(
@ -254,9 +170,9 @@ _moduleimpl_inscriptionEditor = ndb.EditableTable(
)
def do_moduleimpl_inscription_create(args, formsemestre_id=None):
def do_moduleimpl_inscription_create(args, formsemestre_id=None, cnx=None):
"create a moduleimpl_inscription"
cnx = ndb.GetDBConnexion()
cnx = cnx or ndb.GetDBConnexion()
try:
r = _moduleimpl_inscriptionEditor.create(cnx, args)
except psycopg2.errors.UniqueViolation as exc:
@ -270,7 +186,7 @@ def do_moduleimpl_inscription_create(args, formsemestre_id=None):
cnx,
method="moduleimpl_inscription",
etudid=args["etudid"],
msg="inscription module %s" % args["moduleimpl_id"],
msg=f"inscription module {args['moduleimpl_id']}",
commit=False,
)
return r
@ -297,32 +213,29 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
)
if not insem:
raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
raise ScoValueError(f"{etudid} n'est pas inscrit au semestre !")
cnx = ndb.GetDBConnexion()
# Desinscriptions
if reset:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"delete from notes_moduleimpl_inscription where moduleimpl_id = %(moduleimpl_id)s",
{"moduleimpl_id": moduleimpl_id},
)
# Inscriptions au module:
inmod_set = set(
[
# hum ?
x["etudid"]
for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
]
)
inmod_set = {
x["etudid"] for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
}
for etudid in etudids:
# deja inscrit ?
# déja inscrit ?
if not etudid in inmod_set:
do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
formsemestre_id=formsemestre_id,
cnx=cnx,
)
cnx.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > moduleimpl_inscrit_etuds

View File

@ -409,34 +409,32 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
'<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
)
ues = [
sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys()
]
ues.sort(key=lambda u: u["numero"])
ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
ues.sort(key=lambda u: u.numero)
for ue in ues:
H.append(
f"""<li class="tit"><span class="tit">{ue['acronyme']}: {ue['titre']}</span>"""
f"""<li class="tit"><span class="tit">{ue.acronyme}: {ue.titre or ''}</span>"""
)
H.append("<ul>")
for info in ues_cap_info[ue["ue_id"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
for info in ues_cap_info[ue.id]:
etud = Identite.get_etud(info["etudid"])
H.append(
f"""<li class="etud"><a class="discretelink etudinfo"
id="{info['etudid']}"
id="{etud.id}"
href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
etudid=etud.id,
)
}">{etud["nomprenom"]}</a>"""
}">{etud.nomprenom}</a>"""
)
if info["ue_status"]["event_date"]:
H.append(
f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
)
if is_apc:
is_inscrit_ue = (etud["etudid"], ue["id"]) not in res.dispense_ues
is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
else:
# CLASSIQUE
is_inscrit_ue = info["is_ins"]
@ -468,8 +466,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
f"""<div><a class="stdlink" href="{
url_for("notes.etud_desinscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=formsemestre_id, ue_id=ue.id)
}">désinscrire {"des modules" if not is_apc else ""} de cette UE</a></div>
"""
)
@ -479,8 +477,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
f"""<div><a class="stdlink" href="{
url_for("notes.etud_inscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=formsemestre_id, ue_id=ue.id)
}">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
"""
)

View File

@ -127,12 +127,12 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
"args": {
"group_ids": group_id,
"evaluation_id": evaluation.id,
"date_debut": evaluation.date_debut.isoformat()
if evaluation.date_debut
else "",
"date_fin": evaluation.date_fin.isoformat()
if evaluation.date_fin
else "",
"date_debut": (
evaluation.date_debut.isoformat() if evaluation.date_debut else ""
),
"date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else ""
),
},
"enabled": evaluation.date_debut is not None
and evaluation.date_fin is not None,
@ -355,10 +355,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
</div>"""
)
#
if has_expression and nt.expr_diagnostics:
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
#
if formsemestre_has_decisions(formsemestre_id):
H.append(
"""<ul class="tf-msg">

View File

@ -139,9 +139,8 @@ def dict_pvjury(
dec_ue_list = _descr_decisions_ues(
nt, etudid, d["decisions_ue"], d["decision_sem"]
)
d["decisions_ue_nb"] = len(
dec_ue_list
) # avec les UE capitalisées, donc des éventuels doublons
# avec les UE capitalisées, donc des éventuels doublons:
d["decisions_ue_nb"] = len(dec_ue_list)
# Mais sur la description (eg sur les bulletins), on ne veut pas
# afficher ces doublons: on uniquifie sur ue_code
_codes = set()
@ -291,8 +290,10 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
)
)
):
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
uelist.append(ue)
ue = UniteEns.query.get(ue_id)
assert ue
# note modernisation code: on utilise des dict tant que get_etud_ue_status renvoie des dicts
uelist.append(ue.to_dict())
# Les UE capitalisées dans d'autres semestres:
if etudid in nt.validations.ue_capitalisees.index:
for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:

View File

@ -528,6 +528,7 @@ def notes_add(
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
"""
assert evaluation_id is not None
now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note
@ -539,7 +540,7 @@ def notes_add(
}
for etudid, value in notes:
if check_inscription and (etudid not in inscrits):
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module")
if (value is not None) and not isinstance(value, float):
raise NoteProcessError(
f"etudiant {etudid}: valeur de note invalide ({value})"

View File

@ -60,16 +60,14 @@ from app.models.formsemestre import FormSemestre
from app import db, log
from app.models import Evaluation, ModuleImpl, UniteEns
from app.models import Evaluation, Identite, ModuleImpl, UniteEns
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@ -83,10 +81,10 @@ def external_ue_create(
acronyme="",
ue_type=codes_cursus.UE_STANDARD,
ects=0.0,
) -> int:
) -> ModuleImpl:
"""Crée UE/matiere/module dans la formation du formsemestre
puis un moduleimpl.
Return: moduleimpl_id
Return: moduleimpl
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
log(f"creating external UE in {formsemestre}: {acronyme}")
@ -139,28 +137,30 @@ def external_ue_create(
"module_id": module_id,
"formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module
"responsable_id": formsemestre.responsables[0].id
"responsable_id": (
formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None,
else None
),
},
)
return moduleimpl_id
modimpl = ModuleImpl.query.get(moduleimpl_id)
assert modimpl
return modimpl
def external_ue_inscrit_et_note(
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
moduleimpl: ModuleImpl, formsemestre_id: int, notes_etuds: dict
):
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl.id}, notes_etuds={notes_etuds})"
)
# Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id,
moduleimpl.id,
formsemestre_id,
list(notes_etuds.keys()),
)
@ -188,12 +188,12 @@ def external_ue_inscrit_et_note(
)
def get_existing_external_ue(formation_id: int) -> list[dict]:
"Liste de toutes les UE externes définies dans cette formation"
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
def get_existing_external_ue(formation_id: int) -> list[UniteEns]:
"Liste de toutes les UEs externes définies dans cette formation"
return UniteEns.query.filter_by(formation_id=formation_id, is_external=True).all()
def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
def get_external_moduleimpl(formsemestre_id: int, ue_id: int) -> ModuleImpl:
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch(
"""
@ -205,7 +205,10 @@ def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
{"ue_id": ue_id, "formsemestre_id": formsemestre_id},
)
if r:
return r[0]["moduleimpl_id"]
modimpl_id = r[0]["moduleimpl_id"]
modimpl = ModuleImpl.query.get(modimpl_id)
assert modimpl
return modimpl
else:
raise ScoValueError(
f"""Aucun module externe ne correspond
@ -225,20 +228,20 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
En BUT, pas d'UEs externes. Voir https://scodoc.org/git/ScoDoc/ScoDoc/issues/542
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Contrôle d'accès:
if not formsemestre.can_be_edited_by(current_user):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
if formsemestre.formation.is_apc():
raise ScoValueError("Impossible d'ajouter une UE externe en BUT")
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etud = Identite.get_etud(etudid)
formation_id = formsemestre.formation.id
existing_external_ue = get_existing_external_ue(formation_id)
H = [
html_sco_header.html_sem_header(
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
@ -275,10 +278,10 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
"input_type": "menu",
"title": "UE externe existante:",
"allowed_values": [""]
+ [str(ue["ue_id"]) for ue in existing_external_ue],
+ [str(ue.id) for ue in existing_external_ue],
"labels": [default_label]
+ [
"%s (%s)" % (ue["titre"], ue["acronyme"])
f"{ue.titre or ''} ({ue.acronyme})"
for ue in existing_external_ue
],
"attributes": ['onchange="update_external_ue_form();"'],
@ -364,7 +367,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
)
if tf[2]["existing_ue"]:
ue_id = int(tf[2]["existing_ue"])
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
modimpl = get_external_moduleimpl(formsemestre_id, ue_id)
else:
acronyme = tf[2]["acronyme"].strip()
if not acronyme:
@ -375,7 +378,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
+ tf[1]
+ html_footer
)
moduleimpl_id = external_ue_create(
modimpl = external_ue_create(
formsemestre_id,
titre=tf[2]["titre"],
acronyme=acronyme,
@ -384,7 +387,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
)
external_ue_inscrit_et_note(
moduleimpl_id,
modimpl,
formsemestre_id,
{etudid: note_value},
)

View File

@ -23,7 +23,7 @@
<h2 class="insidebar">Scolarité</h2>
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Formations</a> <br>
{% if current_user.has_permission(sco.Permission.AbsChange)%}
<a href="{{url_for('assiduites.bilan_dept', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Assiduité</a> <br>

View File

@ -30,6 +30,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021
"""
import html
from operator import itemgetter
import time
@ -487,7 +488,6 @@ def get_ue_niveaux_options_html():
return apc_edit_ue.get_ue_niveaux_options_html(ue)
@bp.route("/ue_list") # backward compat
@bp.route("/ue_table")
@scodoc
@permission_required(Permission.ScoView)
@ -682,21 +682,21 @@ def module_clone():
@bp.route("/index_html")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def index_html():
"Page accueil formations"
fmt = request.args.get("fmt", "html")
editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table()
if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
H = [
html_sco_header.sco_header(page_title="Programmes formations"),
"""<h2>Programmes pédagogiques</h2>
html_sco_header.sco_header(page_title="Formations (programmes)"),
"""<h2>Formations (programmes pédagogiques)</h2>
""",
table.html(),
]
T = sco_formations.formation_list_table()
H.append(T.html())
if editable:
H.append(
f"""
@ -804,7 +804,7 @@ def formation_import_xml_form():
<h2>Import effectué !</h2>
<ul>
<li><a class="stdlink" href="{
url_for("notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)}">Voir la formation</a>
</li>
<li><a class="stdlink" href="{
@ -817,19 +817,6 @@ def formation_import_xml_form():
"""
# sco_publish(
# "/formation_create_new_version",
# sco_formations.formation_create_new_version,
# Permission.EditFormation,
# )
# --- UE
sco_publish(
"/ue_list",
sco_edit_ue.ue_list,
Permission.ScoView,
)
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation)
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
@ -3284,11 +3271,12 @@ def check_sem_integrity(formsemestre_id, fix=False):
for modimpl in modimpls:
mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0]
formations_set.add(mod["formation_id"])
ue = sco_edit_ue.ue_list({"ue_id": mod["ue_id"]})[0]
formations_set.add(ue["formation_id"])
if ue["formation_id"] != mod["formation_id"]:
ue = UniteEns.query.get_or_404(mod["ue_id"])
ue_dict = ue.to_dict()
formations_set.add(ue_dict["formation_id"])
if ue_dict["formation_id"] != mod["formation_id"]:
modimpl["mod"] = mod
modimpl["ue"] = ue
modimpl["ue"] = ue_dict
bad_ue.append(modimpl)
if sem["formation_id"] != mod["formation_id"]:
bad_sem.append(modimpl)
@ -3341,30 +3329,28 @@ def check_sem_integrity(formsemestre_id, fix=False):
@permission_required(Permission.ScoView)
@scodoc7func
def check_form_integrity(formation_id, fix=False):
"debug"
log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix))
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
"debug (obsolete)"
log(f"check_form_integrity: formation_id={formation_id} fix={fix}")
formation: Formation = Formation.query.filter_by(
dept_id=g.scodoc_dept_id, formation_id=formation_id
).first_or_404()
bad = []
for ue in ues:
mats = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for mat in mats:
mods = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]})
for mod in mods:
if mod["ue_id"] != ue["ue_id"]:
for ue in formation.ues:
for matiere in ue.matieres:
for mod in matiere.modules:
if mod.ue_id != ue.id:
if fix:
# fix mod.ue_id
log(
"fix: mod.ue_id = %s (was %s)" % (ue["ue_id"], mod["ue_id"])
)
mod["ue_id"] = ue["ue_id"]
sco_edit_module.do_module_edit(mod)
log(f"fix: mod.ue_id = {ue.id} (was {mod.ue_id})")
mod.ue_id = ue.id
db.session.add(mod)
bad.append(mod)
if mod["formation_id"] != formation_id:
if mod.formation_id != formation_id:
bad.append(mod)
if bad:
txth = "<br>".join([str(x) for x in bad])
txth = "<br>".join([html.escape(str(x)) for x in bad])
txt = "\n".join([str(x) for x in bad])
log("check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id)
log(f"check_form_integrity: formation_id={formation_id}\ninconsistencies:")
log(txt)
# Notify by e-mail
send_scodoc_alarm("Notes: formation incoherente !", txt)
@ -3380,39 +3366,31 @@ def check_form_integrity(formation_id, fix=False):
@scodoc7func
def check_formsemestre_integrity(formsemestre_id):
"debug"
log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id))
log(f"check_formsemestre_integrity: formsemestre_id={formsemestre_id}")
# verifie que tous les moduleimpl d'un formsemestre
# se réfèrent à un module dont l'UE appartient a la même formation
# Ancien bug: les ue_id étaient mal copiés lors des création de versions
# de formations
diag = []
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for mod in Mlist:
if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
for modimpl in formsemestre.modimpls:
if modimpl.module.ue_id != modimpl.module.matiere.ue_id:
diag.append(
"moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s"
% (
mod["moduleimpl_id"],
mod["module"]["ue_id"],
mod["matiere"]["ue_id"],
f"""moduleimpl {modimpl.id}: module.ue_id={modimpl.module.ue_id
} != matiere.ue_id={modimpl.module.matiere.ue_id}"""
)
)
if mod["ue"]["formation_id"] != mod["module"]["formation_id"]:
if modimpl.module.ue.formation_id != modimpl.module.formation_id:
diag.append(
"moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s"
% (
mod["moduleimpl_id"],
mod["ue"]["formation_id"],
mod["module"]["formation_id"],
)
f"""moduleimpl {modimpl.id}: ue.formation_id={
modimpl.module.ue.formation_id} != mod.formation_id={
modimpl.module.formation_id}"""
)
if diag:
send_scodoc_alarm(
"Notes: formation incoherente dans semestre %s !" % formsemestre_id,
f"Notes: formation incoherente dans semestre {formsemestre_id} !",
"\n".join(diag),
)
log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id)
log(f"check_formsemestre_integrity: formsemestre_id={formsemestre_id}")
log("inconsistencies:\n" + "\n".join(diag))
else:
diag = ["OK"]

View File

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

View File

@ -249,37 +249,7 @@ def test_formations(test_client):
assert len(lim_modid) == 1
lim_modimpl_id = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
# print(lim_modimpl_id)
# ---- Test de moduleimpl_withmodule_list
assert lim_modid == lim_modimpl_id # doit etre le meme resultat
liimp_sem1 = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=sem1["formsemestre_id"]
)
assert len(liimp_sem1) == 2
assert module_id in (
liimp_sem1[0]["module_id"],
liimp_sem1[1]["module_id"],
)
assert module_id2 in (
liimp_sem1[0]["module_id"],
liimp_sem1[1]["module_id"],
)
liimp_sem2 = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=sem2["formsemestre_id"]
)
assert module_id_t == liimp_sem2[0]["module_id"]
liimp_modid = sco_moduleimpl.moduleimpl_withmodule_list(module_id=module_id)
assert len(liimp_modid) == 1
liimp_modimplid = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=moduleimpl_id
)
assert liimp_modid == liimp_modimplid
assert lim_modid == lim_modimpl_id
# --- Suppression du module, matiere et ue test du semestre 2

View File

@ -54,7 +54,7 @@ RELEASE=1
ARCH="amd64"
FACTORY_DIR="/opt/factory"
DEST_DIR="$PACKAGE_NAME"_"$VERSION"-"$RELEASE"_"$ARCH"
GIT_RELEASE_URL="https://scodoc.org/git/viennet/ScoDoc/archive/${RELEASE_TAG}.tar.gz"
GIT_RELEASE_URL="https://scodoc.org/git/ScoDoc/ScoDoc/archive/${RELEASE_TAG}.tar.gz"
UNIT_TESTS_DIR="/opt/scodoc" # on lance les tests dans le rep. de travail, pas idéal
echo "Le paquet sera $DEST_DIR.deb"