Merge branch 'entreprises' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-07-07 14:46:41 +02:00
commit 0140c404ea
15 changed files with 329 additions and 199 deletions

View File

@ -450,7 +450,7 @@ def etudiant_bulletin_semestre(
app.set_sco_dept(dept.acronym)
return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version
formsemestre, etud, version=version
)

View File

@ -68,7 +68,7 @@ from flask import g, url_for
from app import db
from app import log
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
from app.comp import inscr_mod, res_sem
from app.models import formsemestre
from app.models.but_refcomp import (
@ -219,15 +219,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair
"le second formsemestre de la même année scolaire (S2, S4, S6)"
self.annee_but = (
(formsemestre_impair.semestre_id + 1) // 2
if formsemestre_impair
else (formsemestre_pair.semestre_id + 1) // 2
)
formsemestre_last = formsemestre_pair or formsemestre_impair
"le formsemestre le plus avancé dans cette année"
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3)
self.rcues_annee = []
"RCUEs de l'année"
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
@ -255,13 +256,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
self.decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue)
ue.id: DecisionsProposeesUE(
etud, formsemestre_impair, ue, self.inscription_etat
)
for ue in self.ues_impair
}
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update(
{
ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue)
ue.id: DecisionsProposeesUE(
etud, formsemestre_pair, ue, self.inscription_etat
)
for ue in self.ues_pair
}
)
@ -291,8 +296,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
)
"le nb de comp. sous la barre de 8/20"
# année ADM si toutes RCUE validées (sinon PASD)
self.admis = self.nb_validables == self.nb_competences
# année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF
self.admis = (self.nb_validables == self.nb_competences) and (
self.inscription_etat == scu.INSCRIT
)
"vrai si l'année est réussie, tous niveaux validables"
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
# Peut passer si plus de la moitié validables et tous > 8
@ -310,6 +317,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.admis:
self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues
elif self.inscription_etat != scu.INSCRIT:
self.codes = [
sco_codes.DEM
if self.inscription_etat == scu.DEMISSION
else sco_codes.DEF,
# propose aussi d'autres codes, au cas où...
sco_codes.DEM
if self.inscription_etat != scu.DEMISSION
else sco_codes.DEF,
sco_codes.ABAN,
sco_codes.ABL,
sco_codes.EXCLU,
]
elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
self.explanation = expl_rcues
@ -482,6 +502,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ue_impair,
self.formsemestre_pair,
ue_pair,
self.inscription_etat,
)
ues_impair_sans_rcue.discard(ue_impair.id)
break
@ -509,7 +530,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
rcue = rc
break
if rcue is not None:
dec_rcue = DecisionsProposeesRCUE(self, rcue)
dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
rc_niveaux.append((dec_rcue, niveau.id))
# prévient les UE concernées :-)
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
@ -724,14 +745,26 @@ class DecisionsProposeesRCUE(DecisionsProposees):
]
def __init__(
self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE
self,
dec_prop_annee: DecisionsProposeesAnnee,
rcue: RegroupementCoherentUE,
inscription_etat: str = scu.INSCRIT,
):
super().__init__(etud=dec_prop_annee.etud)
self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = []
return
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
self.parcour = dec_prop_annee.parcour
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
return
self.validation = rcue.query_validations().first()
if self.validation is not None:
self.code_valide = self.validation.code
@ -828,12 +861,27 @@ class DecisionsProposeesUE(DecisionsProposees):
etud: Identite,
formsemestre: FormSemestre,
ue: UniteEns,
inscription_etat: str = scu.INSCRIT,
):
super().__init__(etud=etud)
self.formsemestre = formsemestre
self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None"
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
self.moy_ue = "-"
return
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
# mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by(
@ -841,10 +889,6 @@ class DecisionsProposeesUE(DecisionsProposees):
).first()
if self.validation is not None:
self.code_valide = self.validation.code
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
# Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
@ -863,6 +907,8 @@ class DecisionsProposeesUE(DecisionsProposees):
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""
if self.inscription_etat != scu.INSCRIT:
return
if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
self.codes.insert(0, sco_codes.ADM)
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)

View File

@ -409,7 +409,9 @@ def get_table_jury_but(
)}" class="stdlink">
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
décision</a>
""",
"""
if deca.inscription_etat == scu.INSCRIT
else deca.inscription_etat,
"col_lien_saisie_but",
)
rows.append(row)

View File

@ -650,7 +650,7 @@ class ResultatsSemestre(ResultatsCache):
elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri
if mode_jury:
if mode_jury and self.validations:
dec_sem = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else ""
idx = add_cell(

View File

@ -1419,7 +1419,7 @@ def get_import_donnees_file_sample():
@permission_required(Permission.RelationsEntreprisesExport)
def import_donnees():
"""
Permet d'importer des entreprises a l'aide d'un fichier excel (.xlsx)
Permet d'importer des entreprises à partir d'un fichier excel (.xlsx)
"""
form = ImportForm()
if form.validate_on_submit():
@ -1428,7 +1428,7 @@ def import_donnees():
Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename)
)
file.save(file_path)
diag, lm = sco_excel.excel_file_to_list_are(file_path)
diag, lm = sco_excel.excel_workbook_to_list(file_path)
os.remove(file_path)
if lm is None or len(lm) < 2:
flash("Veuillez utilisez la feuille excel à remplir")

View File

@ -16,6 +16,7 @@ from app.models.ues import UniteEns
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model):
@ -42,6 +43,7 @@ class ApcValidationRCUE(db.Model):
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
"formsemestre pair du RCUE"
# Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
@ -84,6 +86,7 @@ class RegroupementCoherentUE:
ue_1: UniteEns,
formsemestre_2: FormSemestre,
ue_2: UniteEns,
inscription_etat: str,
):
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
@ -109,6 +112,11 @@ class RegroupementCoherentUE:
"semestre pair"
self.ue_2 = ue_2
# Stocke les moyennes d'UE
if inscription_etat != scu.INSCRIT:
self.moy_rcue = None
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
@ -201,8 +209,9 @@ class RegroupementCoherentUE:
return None
# unused
def find_rcues(
formsemestre: FormSemestre, ue: UniteEns, etud: Identite
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
) -> list[RegroupementCoherentUE]:
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
ce semestre pour cette UE.
@ -250,7 +259,9 @@ def find_rcues(
other_ue = UniteEns.query.get(ue_id)
other_formsemestre = FormSemestre.query.get(formsemestre_id)
rcues.append(
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue)
RegroupementCoherentUE(
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
)
)
# safety check: 1 seul niveau de comp. concerné:
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1

View File

@ -58,7 +58,6 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
from app.scodoc import sco_users
@ -66,15 +65,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note
import app.scodoc.notesdb as ndb
# ----- CLASSES DE BULLETINS DE NOTES
from app.scodoc import sco_bulletins_standard
from app.scodoc import sco_bulletins_legacy
# import sco_bulletins_example # format exemple (à désactiver en production)
# ... ajouter ici vos modules ...
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
def get_formsemestre_bulletin_etud_json(
formsemestre: FormSemestre,

View File

@ -92,7 +92,6 @@ def formsemestre_bulletinetud_published_dict(
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing:
published = True
else:
@ -134,6 +133,7 @@ def formsemestre_bulletinetud_published_dict(
)
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
# Disponible pour publication ?
d["publie"] = published
if not published:
return d # stop !
@ -364,8 +364,35 @@ def formsemestre_bulletinetud_published_dict(
return d
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False):
"dict avec decision pour bulletins json"
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
"""dict avec decision pour bulletins json
- decision : décision semestre
- decision_ue : list des décisions UE
- situation
with_decision donne les décision même si bul_show_decision est faux.
Exemple:
{
'autorisation_inscription': [{'semestre_id': 4}],
'decision': {'code': 'ADM',
'compense_formsemestre_id': None,
'date': '2022-01-21',
'etat': 'I'},
'decision_ue': [
{
'acronyme': 'UE31',
'code': 'ADM',
'ects': 16.0,
'numero': 23,
'titre': 'Approfondissement métiers',
'ue_id': 1787
},
...
],
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
'UE31, UE32. Diplôme obtenu.'}
"""
from app.scodoc import sco_bulletins
d = {}

View File

@ -40,10 +40,9 @@ from openpyxl.comments import Comment
from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb
from app.scodoc import sco_preferences
from app import log
from app.scodoc.sco_exceptions import ScoValueError
@ -593,71 +592,87 @@ def excel_feuille_saisie(e, titreannee, description, lines):
def excel_bytes_to_list(bytes_content):
try:
filelike = io.BytesIO(bytes_content)
return _excel_to_list(filelike)
except:
except Exception as exc:
raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
"""
)
) from exc
return _excel_to_list(filelike)
def excel_file_to_list(filename):
try:
return _excel_to_list(filename)
except:
except Exception as exc:
raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
"""
)
) from exc
def excel_file_to_list_are(filename):
def excel_workbook_to_list(filename):
try:
return _excel_to_list_are(filename)
except:
return _excel_workbook_to_list(filename)
except Exception as exc:
raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
"""
)
) from exc
def _open_workbook(filelike, dump_debug=False) -> Workbook:
"""Open document.
On error, if dump-debug is True, dump data in /tmp for debugging purpose
"""
try:
workbook = load_workbook(filename=filelike, read_only=True, data_only=True)
except Exception as exc:
log("Excel_to_list: failure to import document")
if dump_debug:
dump_filename = "/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX
log(f"Dumping problemetic file on {dump_filename}")
with open(dump_filename, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel xlsx !"
) from exc
return workbook
def _excel_to_list(filelike):
"""returns list of list
convert_to_string is a conversion function applied to all non-string values (ie numbers)
"""
try:
wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except:
log("Excel_to_list: failure to import document")
with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
)
"""returns list of list"""
workbook = _open_workbook(filelike)
diag = [] # liste de chaines pour former message d'erreur
# n'utilise que la première feuille
if len(wb.get_sheet_names()) < 1:
if len(workbook.get_sheet_names()) < 1:
diag.append("Aucune feuille trouvée dans le classeur !")
return diag, None
if len(wb.get_sheet_names()) > 1:
# n'utilise que la première feuille:
if len(workbook.get_sheet_names()) > 1:
diag.append("Attention: n'utilise que la première feuille du classeur !")
sheet_name = workbook.get_sheet_names()[0]
ws = workbook[sheet_name]
matrix, diag_sheet = _excel_sheet_to_list(ws, sheet_name)
diag += diag_sheet
return diag, matrix
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]:
"""read a spreadsheet sheet, and returns:
- diag : a list of strings (error messages aimed at helping the user)
- a list of lists: the spreadsheet cells
"""
diag = []
# fill matrix
sheet_name = wb.get_sheet_names()[0]
ws = wb.get_sheet_by_name(sheet_name)
sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace")
values = {}
for row in ws.iter_rows():
for row in sheet.iter_rows():
for cell in row:
if cell.value is not None:
values[(cell.row - 1, cell.column - 1)] = str(cell.value)
if not values:
diag.append(
"Aucune valeur trouvée dans la feuille %s !"
% sheet_name.decode(scu.SCO_ENCODING)
)
diag.append(f"Aucune valeur trouvée dans la feuille {sheet_name} !")
return diag, None
indexes = list(values.keys())
# search numbers of rows and cols
@ -665,76 +680,38 @@ def _excel_to_list(filelike):
cols = [x[1] for x in indexes]
nbcols = max(cols) + 1
nbrows = max(rows) + 1
m = []
matrix = []
for _ in range(nbrows):
m.append([""] * nbcols)
matrix.append([""] * nbcols)
for row_idx, col_idx in indexes:
v = values[(row_idx, col_idx)]
# if isinstance(v, six.text_type):
# v = v.encode(scu.SCO_ENCODING, "backslashreplace")
# elif convert_to_string:
# v = convert_to_string(v)
m[row_idx][col_idx] = v
diag.append(
'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m))
)
# diag.append(str(M))
#
return diag, m
matrix[row_idx][col_idx] = v
diag.append(f'Feuille "{sheet_name}", {len(matrix)} lignes')
return diag, matrix
def _excel_to_list_are(filelike):
"""returns list of list
convert_to_string is a conversion function applied to all non-string values (ie numbers)
def _excel_workbook_to_list(filelike):
"""Lit un classeur (workbook): chaque feuille est lue
et est convertie en une liste de listes.
Returns:
- diag : a list of strings (error messages aimed at helping the user)
- a list of lists: the spreadsheet cells
"""
try:
wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except:
log("Excel_to_list: failure to import document")
with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
)
workbook = _open_workbook(filelike)
diag = [] # liste de chaines pour former message d'erreur
if len(wb.get_sheet_names()) < 1:
if len(workbook.get_sheet_names()) < 1:
diag.append("Aucune feuille trouvée dans le classeur !")
return diag, None
lm = []
for sheet_name in wb.get_sheet_names():
matrix_list = []
for sheet_name in workbook.get_sheet_names():
# fill matrix
ws = wb.get_sheet_by_name(sheet_name)
sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace")
values = {}
for row in ws.iter_rows():
for cell in row:
if cell.value is not None:
values[(cell.row - 1, cell.column - 1)] = str(cell.value)
if not values:
diag.append(
"Aucune valeur trouvée dans la feuille %s !"
% sheet_name.decode(scu.SCO_ENCODING)
)
return diag, None
indexes = list(values.keys())
# search numbers of rows and cols
rows = [x[0] for x in indexes]
cols = [x[1] for x in indexes]
nbcols = max(cols) + 1
nbrows = max(rows) + 1
m = []
for _ in range(nbrows):
m.append([""] * nbcols)
for row_idx, col_idx in indexes:
v = values[(row_idx, col_idx)]
m[row_idx][col_idx] = v
diag.append(
'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m))
)
lm.append(m)
return diag, lm
sheet = workbook.get_sheet_by_name(sheet_name)
matrix, diag_sheet = _excel_sheet_to_list(sheet, sheet_name)
diag += diag_sheet
matrix_list.append(matrix)
return diag, matrix_list
def excel_feuille_listeappel(

View File

@ -35,13 +35,17 @@ from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models.notes import etud_has_notes_attente
from app.models.validations import (
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import *
@ -989,28 +993,32 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
"""Suppression des decisions de jury pour un etudiant."""
log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid))
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
try:
# -- Validation du semestre et des UEs
cursor.execute(
"""delete from scolar_formsemestre_validation
where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""",
args,
)
# -- Autorisations d'inscription
cursor.execute(
"""delete from scolar_autorisation_inscription
where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""",
args,
)
cnx.commit()
except:
cnx.rollback()
raise
"""Suppression des décisions de jury pour un étudiant/formsemestre.
Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant:
code semestre, UEs, autorisations d'inscription
"""
log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})")
# Validations jury classiques (semestres, UEs, autorisations)
for v in ScolarFormSemestreValidation.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre_id
):
db.session.delete(v)
for v in ScolarAutorisationInscription.query.filter_by(
etudid=etudid, origin_formsemestre_id=formsemestre_id
):
db.session.delete(v)
# Validations jury spécifiques BUT
for v in ApcValidationRCUE.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre_id
):
db.session.delete(v)
for v in ApcValidationAnnee.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre_id
):
db.session.delete(v)
db.session.commit()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_invalidate_etud_formation_caches(

View File

@ -224,9 +224,26 @@ class releveBUT extends HTMLElement {
<div class=abs>Non justifiées</div>
<div>${data.semestre.absences?.injustifie ?? "-"}</div>
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
</div>
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a>
`;
</div>`;
if(data.semestre.decision_rcue.length){
output += `
<div>
<div class=enteteSemestre>RCUE</div><div></div>
${(()=>{
let output = "";
data.semestre.decision_rcue.forEach(competence=>{
output += `<div class=rang>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
})
return output;
})()}
</div>
</div>`
}
output += `
<a class=photo href="${data.etudiant.fiche_url}">
<img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0">
</a>`;
/*${data.semestre.groupes.map(groupe => {
return `
<div>
@ -240,9 +257,11 @@ class releveBUT extends HTMLElement {
}).join("")
}*/
this.shadow.querySelector(".infoSemestre").innerHTML = output;
if(data.semestre.decision_annee?.code){
/*if(data.semestre.decision_annee?.code){
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
}
}*/
this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || "";
/*if (data.semestre.decision?.code) {

View File

@ -1,9 +1,12 @@
<div class="but_doc_codes">
<p><em>Ci-dessous la signification de chaque code est expliquée,
ainsi que la correspondance avec les codes préconisés par
l'AMUE pour Apogée dans un document informel qui a circulé début
2022 (les éventuelles erreurs n'engagent personne).
</em></p>
ainsi que la correspondance avec certains codes préconisés par
l'AMUE et l'ADIUT pour Apogée.
</em>
On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes
tables générées par ScoDoc) et leur transcription vers Apogée lors des exports
(transcription paramétrable par votre administrateur ScoDoc).
</p>
<div class="but_doc_section">Codes d'année</div>
<div class="but_doc">
<table>
@ -230,4 +233,34 @@
</tr>
</table>
</div>
<div class="but_doc_section">Rappels de l'arrêté BUT (extraits)</div>
<div class="but_doc">
<ul>
<li>Au sein de chaque regroupement cohérent dUE, la compensation est intégrale.
Si une UE na pas été acquise en raison dune moyenne inférieure à 10,
cette UE sera acquise par compensation si et seulement si létudiant
a obtenu la moyenne au regroupement cohérent auquel lUE appartient.</li>
<li>La poursuite d'études dans un semestre pair dune même année est de droit
pour tout étudiant.
La poursuite détudes dans un semestre impair est possible
<em>si et seulement si</em> létudiant a obtenu :
<ul>
<li>la moyenne à plus de la moitié des regroupements cohérents dUE</li>
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent dUE.</li>
</ul>
</li>
<li>La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des
semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.</li>
</ul>
<b>Textes de référence:</b>
<ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li>
<li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version
pdf complète</a></li>
</ul>
</div>
</div>

View File

@ -57,7 +57,7 @@ from app.models.ues import UniteEns
from app import api
from app import db
from app import models
from app.models import ScolarNews
from app.models import ScolarNews, but_validations
from app.auth.models import User
from app.but import apc_edit_ue, jury_but_recap
from app.decorators import (
@ -71,7 +71,7 @@ from app.views import notes_bp as bp
# ---------------
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json, sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm
@ -2515,51 +2515,68 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud(
formsemestre_id, etudid, dialog_confirmed=False
):
"""Suppression des decisions de jury pour un etudiant."""
"""Suppression des décisions de jury pour un étudiant."""
if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog(
message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(),
)
etud = Identite.query.get_or_404(etudid)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc():
next_url = url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
)
else:
next_url = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem = formsemestre.to_dict()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision_jury = nt.get_etud_decision_sem(etudid)
if decision_jury:
existing = (
"<p>Décision existante: %(code)s du %(event_date)s</p>" % decision_jury
)
d = sco_bulletins_json.dict_decision_jury(
etudid, formsemestre_id, with_decisions=True
)
d.update(but_validations.dict_decision_jury(etud, formsemestre))
descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
dec_annee = d.get("decision_annee")
if dec_annee:
descr_annee = dec_annee.get("code", "-")
else:
existing = ""
descr_annee = "-"
existing = f"""
<ul>
<li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
<li>Année BUT: {descr_annee}</li>
<li>UEs : {", ".join(descr_ues)}</li>
<li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
</ul>
"""
return scu.confirm_dialog(
"""<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s
<p>Cette opération est irréversible.
</p>
"""
% (
sem["titre_num"],
sem["date_debut"],
sem["date_fin"],
etud["nomprenom"],
existing,
),
f"""<h2>Confirmer la suppression des décisions du semestre
{formsemestre.titre_mois()} pour {etud.nomprenom}
</h2>
<p>Cette opération est irréversible.</p>
<div>
{existing}
</div>
""",
OK="Supprimer",
dest_url="",
cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s"
% (formsemestre_id, etudid),
cancel_url=next_url,
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
)
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre_id, etudid
)
return flask.redirect(
scu.ScoURL()
+ "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée"
% (formsemestre_id, etudid)
)
flash("Décisions supprimées")
return flask.redirect(next_url)
# ------------- PV de JURY et archives

View File

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

View File

@ -33,7 +33,7 @@ except NameError:
load_dotenv(os.path.join(BASEDIR, ".env"))
CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000"
SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000"
API_URL = SCODOC_URL + "/ScoDoc/api"
SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"]
@ -85,13 +85,13 @@ if r.status_code != 200:
print(f"{len(r.json())} étudiants courants")
# Bulletin d'un BUT
formsemestre_id = 1052 # A adapter
etudid = 16400
formsemestre_id = 1063 # A adapter
etudid = 16450
bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")
# d'un DUT
formsemestre_id = 1028 # A adapter
etudid = 14721
formsemestre_id = 1062 # A adapter
etudid = 16309
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")