Compare commits

...

19 Commits

Author SHA1 Message Date
156066137f reprise wip 2024-06-30 06:15:50 +02:00
cf18e37926 apres merge master 2024-06-24 06:47:48 +02:00
4403da99e9 Merge branch 'master' into prepajury9 2024-06-24 06:44:42 +02:00
9a6a90862c avant upgrade 2024-06-24 06:41:26 +02:00
75f01a0d9e avannt merge 2024-06-24 06:30:52 +02:00
08dfaeb436 Evite de rediriger vers login si user CAS déjà reconnu et CAS forcé: fix #757 2024-06-24 01:15:40 +02:00
ee050889f0 Fix API unit test 2024-06-23 21:13:40 +02:00
84d0b4fb9d Documentation API (QUERY pour carte syntaxique) 2024-06-23 17:40:48 +02:00
d35940cc0d Feuille saisie note: affichage du nom usuel 2024-06-23 17:39:58 +02:00
bea7b2ed8a Merge pull request 'create_api_map : ajout keyword "DOC_ANCHOR"' (#940) from iziram/ScoDoc:hotfix into master
Reviewed-on: ScoDoc/ScoDoc#940
2024-06-23 17:25:59 +02:00
Iziram
87d04905fe create_api_map : ajout keyword "DOC_ANCHOR" 2024-06-23 12:28:30 +02:00
f976b2fdb0 load competences 2024-06-23 11:45:35 +02:00
b566ae15ba lecture parcours etudiant 2024-06-23 11:11:40 +02:00
23d01029ce lecture parcours etudiant 2024-06-23 10:59:25 +02:00
3619772879 load all formsemestres 2024-06-23 07:44:44 +02:00
5cdf089a1b Merge pull request 'create_api_map : bug fix query avec 1 element + légères optimisations' (#939) from iziram/ScoDoc:hotfix into master
Reviewed-on: ScoDoc/ScoDoc#939
2024-06-22 17:33:13 +02:00
Iziram
476fb29065 create_api_map : bug fix query avec 1 element + légères optimisations 2024-06-22 17:30:59 +02:00
8cf11a2600 API departement: ameliore code et doc. 2024-06-22 17:02:29 +02:00
d92924701b etud_photo_orig_page: modernise code 2024-06-21 19:13:53 +02:00
19 changed files with 2400 additions and 391 deletions

View File

@ -226,50 +226,27 @@ def dept_formsemestres_ids_by_id(dept_id: int):
@bp.route("/departement/<string:acronym>/formsemestres_courants") @bp.route("/departement/<string:acronym>/formsemestres_courants")
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def dept_formsemestres_courants(acronym: str): def dept_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
""" """
Liste des semestres actifs d'un département d'acronyme donné Liste les semestres du département indiqué (par son acronyme ou son id)
contenant la date courante, ou à défaut celle indiquée en argument
(au format ISO).
QUERY
-----
date_courante:<string:date_courante>
Exemple de résultat :
[
{
"titre": "master machine info",
"gestion_semestrielle": false,
"scodoc7_id": null,
"date_debut": "01/09/2021",
"bul_bgcolor": null,
"date_fin": "15/12/2022",
"resp_can_edit": false,
"dept_id": 1,
"etat": true,
"resp_can_change_ens": false,
"id": 1,
"modalite": "FI",
"ens_can_edit_eval": false,
"formation_id": 1,
"gestion_compensation": false,
"elt_sem_apo": null,
"semestre_id": 1,
"bul_hide_xml": false,
"elt_annee_apo": null,
"block_moyennes": false,
"formsemestre_id": 1,
"titre_num": "master machine info semestre 1",
"date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-12-15",
"responsables": [
3,
2
]
},
...
]
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = (
Departement.query.filter_by(acronym=acronym).first_or_404()
if acronym
else Departement.query.get_or_404(dept_id)
)
date_courante = request.args.get("date_courante") date_courante = request.args.get("date_courante")
date_courante = datetime.fromisoformat(date_courante) if date_courante else None date_courante = datetime.fromisoformat(date_courante) if date_courante else None
return [ return [
@ -278,29 +255,3 @@ def dept_formsemestres_courants(acronym: str):
dept, date_courante dept, date_courante
) )
] ]
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def dept_formsemestres_courants_by_id(dept_id: int):
"""
Liste des semestres actifs d'un département d'id donné
"""
# Le département, spécifié par un id ou un acronyme
dept = Departement.query.get_or_404(dept_id)
date_courante = request.args.get("date_courante")
if date_courante:
test_date = datetime.fromisoformat(date_courante)
else:
test_date = db.func.current_date()
# Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
FormSemestre.date_debut <= test_date,
FormSemestre.date_fin >= test_date,
)
return [d.to_dict_api() for d in formsemestres]

View File

@ -89,12 +89,17 @@ def _get_etud_by_code(
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def etudiants_courants(long=False): def etudiants_courants(long: bool = False):
""" """
La liste des étudiants des semestres "courants" (tous départements) La liste des étudiants des semestres "courants".
(date du jour comprise dans la période couverte par le sem.) Considère tous les départements dans lesquels l'utilisateur a la
dans lesquels l'utilisateur a la permission ScoView permission `ScoView` (donc tous si le dépt. du rôle est `None`),
(donc tous si le dept du rôle est None). et les formsemestres contenant la date courante,
ou à défaut celle indiquée en argument (au format ISO).
QUERY
-----
date_courante:<string:date_courante>
Exemple de résultat : Exemple de résultat :
[ [
@ -183,8 +188,13 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None): def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
""" """
Retourne la photo de l'étudiant Retourne la photo de l'étudiant ou un placeholder si non existant.
correspondant ou un placeholder si non existant. Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
QUERY
-----
size:<string:size>
etudid : l'etudid de l'étudiant etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant nip : le code nip de l'étudiant

View File

@ -108,6 +108,17 @@ def formsemestres_query():
dept_id : id du département dept_id : id du département
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit. ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
etat: 0 si verrouillé, 1 sinon etat: 0 si verrouillé, 1 sinon
QUERY
-----
etape_apo:<string:etape_apo>
annee_scolaire:<string:annee_scolaire>
dept_acronym:<string:dept_acronym>
dept_id:<int:dept_id>
etat:<int:etat>
nip:<string:nip>
ine:<string:ine>
""" """
etape_apo = request.args.get("etape_apo") etape_apo = request.args.get("etape_apo")
annee_scolaire = request.args.get("annee_scolaire") annee_scolaire = request.args.get("annee_scolaire")
@ -376,7 +387,16 @@ def formsemestre_programme(formsemestre_id: int):
def formsemestre_etudiants( def formsemestre_etudiants(
formsemestre_id: int, with_query: bool = False, long: bool = False formsemestre_id: int, with_query: bool = False, long: bool = False
): ):
"""Étudiants d'un formsemestre.""" """Étudiants d'un formsemestre.
Si l'état est spécifié, ne renvoie que les inscrits (`I`), les
démissionnaires (`D`) ou les défaillants (`DEF`)
QUERY
-----
etat:<string:etat>
"""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
@ -531,6 +551,13 @@ def etat_evals(formsemestre_id: int):
def formsemestre_resultat(formsemestre_id: int): def formsemestre_resultat(formsemestre_id: int):
"""Tableau récapitulatif des résultats """Tableau récapitulatif des résultats
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules. Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
Si `format=raw`, ne converti pas les valeurs.
QUERY
-----
format:<string:format>
""" """
format_spec = request.args.get("format", None) format_spec = request.args.get("format", None)
if format_spec is not None and format_spec != "raw": if format_spec is not None and format_spec != "raw":
@ -623,6 +650,12 @@ def formsemestre_edt(formsemestre_id: int):
group_ids permet de filtrer sur les groupes ScoDoc. group_ids permet de filtrer sur les groupes ScoDoc.
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code. show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
QUERY
-----
group_ids:<string:group_ids>
show_modules_titles:<bool:show_modules_titles>
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:

View File

@ -151,7 +151,13 @@ def etud_in_group(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def etud_in_group_query(group_id: int): def etud_in_group_query(group_id: int):
"""Étudiants du groupe, filtrés par état""" """Étudiants du groupe, filtrés par état (aucun, I, D, DEF)
QUERY
-----
etat:<string:etat>
"""
etat = request.args.get("etat") etat = request.args.get("etat")
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}: if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
return json_error(API_CLIENT_ERROR, "etat: valeur invalide") return json_error(API_CLIENT_ERROR, "etat: valeur invalide")

View File

@ -59,6 +59,13 @@ def users_info_query():
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés. Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
Si accès via API web, le département de l'URL est ignoré, seules Si accès via API web, le département de l'URL est ignoré, seules
les permissions de l'utilisateur sont prises en compte. les permissions de l'utilisateur sont prises en compte.
QUERY
-----
active:<bool:active>
departement:<string:departement>
starts_with:<string:starts_with>
""" """
query = User.query query = User.query
active = request.args.get("active") active = request.args.get("active")

View File

@ -35,9 +35,9 @@ def after_cas_login():
if user.cas_allow_login: if user.cas_allow_login:
current_app.logger.info(f"CAS: login {user.user_name}") current_app.logger.info(f"CAS: login {user.user_name}")
if login_user(user): if login_user(user):
flask.session[ flask.session["scodoc_cas_login_date"] = (
"scodoc_cas_login_date" datetime.datetime.now().isoformat()
] = datetime.datetime.now().isoformat() )
user.cas_last_login = datetime.datetime.utcnow() user.cas_last_login = datetime.datetime.utcnow()
if flask.session.get("CAS_EDT_ID"): if flask.session.get("CAS_EDT_ID"):
# essaie de récupérer l'edt_id s'il est présent # essaie de récupérer l'edt_id s'il est présent
@ -45,8 +45,10 @@ def after_cas_login():
# via l'expression `cas_edt_id_from_xml_regexp` # via l'expression `cas_edt_id_from_xml_regexp`
# voir flask_cas.routing # voir flask_cas.routing
edt_id = flask.session.get("CAS_EDT_ID") edt_id = flask.session.get("CAS_EDT_ID")
current_app.logger.info(f"""after_cas_login: storing edt_id for { current_app.logger.info(
user.user_name}: '{edt_id}'""") f"""after_cas_login: storing edt_id for {
user.user_name}: '{edt_id}'"""
)
user.edt_id = edt_id user.edt_id = edt_id
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -55,12 +57,17 @@ def after_cas_login():
current_app.logger.info( current_app.logger.info(
f"CAS login denied for {user.user_name} (not allowed to use CAS)" f"CAS login denied for {user.user_name} (not allowed to use CAS)"
) )
else: else: # pas d'utilisateur ScoDoc ou bien compte inactif
current_app.logger.info( current_app.logger.info(
f"""CAS login denied for { f"""CAS login denied for {
user.user_name if user else "" user.user_name if user else ""
} cas_id={cas_id} (unknown or inactive)""" } cas_id={cas_id} (unknown or inactive)"""
) )
if ScoDocSiteConfig.is_cas_forced():
# Dans ce cas, pas de redirect vers la page de login pour éviter de boucler
raise ScoValueError(
"compte ScoDoc inexistant ou inactif pour cet utilisateur CAS"
)
else: else:
current_app.logger.info( current_app.logger.info(
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found ! f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !

View File

@ -691,9 +691,13 @@ def module_edit(
str(parcour.id) for parcour in ref_comp.parcours str(parcour.id) for parcour in ref_comp.parcours
] ]
+ ["-1"], + ["-1"],
"explanation": """Parcours dans lesquels est utilisé ce module.<br> "explanation": """Parcours dans lesquels est utilisé ce module (inutile
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours, hors BUT, pour les modules standards et dans les UEs de bonus).
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""", <br>
Attention: si le module ne doit pas avoir les mêmes coefficients suivant
le parcours, il faut en créer plusieurs versions, car dans ScoDoc chaque
module a ses coefficients.
""",
}, },
) )
] ]

View File

@ -161,7 +161,10 @@ def excel_make_style(
) )
style["font"] = font style["font"] = font
if bgcolor: if bgcolor:
style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) if isinstance(bgcolor, str):
style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor)
else:
style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value)
if halign or valign: if halign or valign:
al = Alignment() al = Alignment()
if halign: if halign:
@ -348,15 +351,21 @@ class ScoExcelSheet:
return cell return cell
def make_row(self, values: list, style=None, comments=None) -> list: def make_row(self, values: list, styles=None, comments=None) -> list:
"build a row" "build a row"
# TODO make possible differents styles in a row # TODO make possible differents styles in a row
if comments is None: if comments is None:
comments = [None] * len(values) comments = [None] * len(values)
return [ if isinstance(styles, list):
self.make_cell(value, style, comment) return [
for value, comment in zip(values, comments) self.make_cell(value, style, comment)
] for value, style, comment in zip(values, styles, comments)
]
else:
return [
self.make_cell(value, style, comment)
for value, comment in zip(values, comments)
]
def append_single_cell_row(self, value: any, style=None): def append_single_cell_row(self, value: any, style=None):
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille. """construit une ligne composée d'une seule cellule et l'ajoute à la feuille.

221
app/scodoc/sco_excel_add.py Normal file
View File

@ -0,0 +1,221 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2019 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
""" Excel file handling
"""
# from sco_utils import *
# from sco_excel import *
import time, datetime
from openpyxl.styles import Font, Alignment, PatternFill, Side, Border
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
def make_font(
bold=False,
italic=False,
font_name=None,
font_size=None,
color=COLORS.BLACK.value,
style=None,
uline=False,
):
font = None
if bold or italic or font_name or font_size:
font = Font()
if bold:
font.bold = bold
if uline:
font.underline = Font.UNDERLINE_SINGLE
if italic:
font.italic = italic
font.name = font_name if font_name else "Arial"
if font_size:
font.height = 20 * font_size
if color:
font.color = color
if font and style:
style["font"] = font
return font
def make_alignment(halign=None, valign=None, orientation=None, style=None):
alignment = None
if halign or valign or orientation:
alignment = Alignment()
if halign:
alignment.horz = halign
if valign:
alignment.vert = valign
if orientation:
alignment.rota = orientation
if alignment and style:
breakpoint()
style["alignment"] = alignment
return alignment
def make_numfmt(fmt, style=None):
if fmt and style:
style["num_format_str"] = fmt
def make_pattern(bgcolor, style=None):
pattern = PatternFill(fill_type="solid", fgColor=bgcolor)
if pattern and style:
style["fill"] = pattern
return pattern
Sides = {
"none": Side(border_style=None),
"thin": Side(border_style="thin"),
"medium": Side(border_style="medium"),
"thick": Side(border_style="thick"),
"filet": Side(border_style="hair"),
}
def make_borders(left=None, top=None, bottom=None, right=None, style=None):
border = None
if left or right or top or bottom:
border = Border()
if left:
border.left = Sides[left]
if right:
border.right = Sides[right]
if top:
border.top = Sides[top]
if bottom:
border.bottom = Sides[bottom]
if border and style:
style["border"] = border
return border
#
#
# def Excel_MakeStyle(
# bold=False,
# italic=False,
# color="black",
# bgcolor=None,
# halign=None,
# valign=None,
# orientation=None,
# font_name=None,
# font_size=None,
# ):
# style = XFStyle()
# make_font(bold, italic, font_name, font_size, color, style=style)
# make_alignment(halign, valign, orientation, style=style)
# make_pattern(bgcolor, style=style)
# return style
#
#
# class ScoExcelSheetExt(ScoExcelSheet):
# def __init__(self, sheet_name="feuille", default_style=None):
# ScoExcelSheet.__init__(self, sheet_name, default_style)
# self.cols_width = {}
# self.row_height = {}
# self.merged = []
# self.panes_frozen = False
# self.horz_split_pos = 0
# self.vert_split_pos = 0
#
# def set_panes(self, panes_frozen=True, horz_split_pos=0, vert_split_pos=0):
# self.panes_frozen = panes_frozen
# self.horz_split_pos = horz_split_pos
# self.vert_split_pos = vert_split_pos
#
# def set_cols_width(self, cols_width):
# self.cols_width = cols_width
#
# def set_row_height(self, row_height):
# self.row_height = row_height
#
# def add_merged(self, first_row, last_row, first_col, last_col, value, style):
# self.merged.append((first_row, last_row, first_col, last_col, value, style))
#
# def gen_workbook(self, wb=None):
# """Generates and returns a workbook from stored data.
# If wb, add a sheet (tab) to the existing workbook (in this case, returns None).
# """
# if wb == None:
# wb = Workbook() # Création du fichier
# sauvegarde = True
# else:
# sauvegarde = False
# ws0 = wb.add_sheet(self.sheet_name.decode(SCO_ENCODING))
# li = 0
# for l in self.cells:
# co = 0
# for c in l:
# # safety net: allow only str, int and float
# if type(c) == LongType:
# c = int(c) # assume all ScoDoc longs fits in int !
# elif type(c) not in (IntType, FloatType):
# c = str(c).decode(SCO_ENCODING)
#
# ws0.write(li, co, c, self.get_cell_style(li, co))
# co += 1
# li += 1
# for first_row, last_row, first_col, last_col, value, style in self.merged:
# ws0.write_merge(
# first_row,
# last_row,
# first_col,
# last_col,
# str(value).decode(SCO_ENCODING),
# style,
# )
# for col_no in range(len(self.cols_width)):
# width = self.cols_width[col_no]
# ws0.col(col_no).width = abs(width) * 110 / 3
# if width < 0:
# ws0.col(col_no).hidden = True
# row_styles = {}
# for row_no in range(len(self.row_height)):
# height = self.row_height[row_no]
# if height not in row_styles:
# fnt = Font()
# fnt.height = height * 12
# row_styles[height] = XFStyle()
# row_styles[height].font = fnt
# ws0.row(row_no).set_style(row_styles[height]) # Excel way
# ws0.row(row_no).height = height * 19 / 2 # Libre-Office compatibilité
# if self.panes_frozen:
# ws0.panes_frozen = self.panes_frozen
# ws0.horz_split_pos = self.vert_split_pos
# ws0.vert_split_pos = self.horz_split_pos
# if sauvegarde:
# return wb.savetostr()
# else:
# return None

View File

@ -564,7 +564,7 @@ def fiche_etud(etudid=None):
%(etat_civil)s %(etat_civil)s
<span>%(email_link)s</span> <span>%(email_link)s</span>
</td><td class="photocell"> </td><td class="photocell">
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a> <a href="etud_photo_orig_page/%(etudid)s">%(etudfoto)s</a>
</td></tr></table> </td></tr></table>
""" """
+ situation_template + situation_template

View File

@ -0,0 +1,175 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2019 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
""" Excel file formatting
"""
# from sco_excel_add import *
import copy
from enum import IntFlag, Enum
from app.scodoc.sco_excel import excel_make_style
from app.scodoc.sco_excel_add import make_font, make_pattern, make_numfmt, make_borders
class CellBorder(IntFlag):
TOP = 1 << 0
BOTTOM = 1 << 1
LEFT = 1 << 2
RIGHT = 1 << 3
# Liste des modificateurs de format
class CellFormat(IntFlag):
# Mise en Forme usuelle
FMT_CAP_UE = 1 << 4 # UE capitalisée
FMT_MOY = 1 << 5 # Moyenne de semestre
FMT_UE = 1 << 6 # Acquisition d'UE sans validation semestre
# Mise en Forme sur le contenu
EMPHASE_PREC = 1 << 7 # Cause échec de semestre
EMPHASE_ATB = 1 << 8 # Cause échec de semestre
EMPHASE_UEACQ = 1 << 9 # Cause échec de semestre
EMPHASE_ATT = 1 << 10 # Cause échec de semestre
EMPHASE_ABS = 1 << 11 # Absences
EMPHASE_COMP = 1 << 12 # Cause échec de semestre
# Mise en forme numérique
PRECISE = 1 << 20 # Précision de l'affichage
GRAYED = 1 << 22 # Grisé horizontal
# Color palette at : https://groups.google.com/g/python-excel/c/cePI-ojUJfc/m/dH72A-rm6_gJ
class CellColor(Enum):
COLOR_PREC = "D2B48C" # TAN
COLOR_ATB = "93CCEA" # LIGHT CORNFLOWER BLUE
COLOR_ATT = "FE8DB1" # LIGHT ORANGE
COLOR_COMP = "90EE90" # LIGHT GREEN
COLOR_UEACQ = "90EE90" # LIGHT GREEN
COLOR_ABS = "44845E" # LIGHT YELLOW
def format_note(valeur, warning, seuil, mini=True):
if seuil is not None:
if isinstance(valeur, (int, float)):
if mini:
diff = valeur - seuil
else:
diff = seuil - valeur
if -0.20 < diff < 0:
warning += CellFormat.PRECISE
return warning
class Formatter:
def __init__(self):
self.style_table = {}
self.last_rank = None
self.left_borders = []
self.right_borders = []
self.default_top = "none"
self.default_bottom = "none"
self.default_left = "none"
self.default_right = "none"
self.default_format = None
# def add_category(self, left, right):
# self.left_borders.append(left)
# self.right_borders.append(right)
def set_borders_default(self, left="none", top="none", bottom="none", right="none"):
self.default_bottom = bottom
self.default_left = left
self.default_right = right
self.default_top = top
def borders(self, li, co):
index = 0
index |= CellBorder.TOP if li == 0 else 0
index |= CellBorder.BOTTOM if self.last_rank and li == self.last_rank else 0
index |= CellBorder.LEFT if co in self.left_borders else 0
index |= CellBorder.RIGHT if co in self.right_borders else 0
return index
def get_style(self, index=0):
if index not in self.style_table:
border_enable = "filet"
if self.default_format:
style = copy.copy(self.default_format)
else:
style = excel_make_style()
make_font(
style=style,
font_name="Calibri",
font_size=9,
bold=(CellFormat.FMT_MOY & index),
uline=(CellFormat.FMT_CAP_UE & index),
)
make_borders(
style=style, left="none", right="none", top="filet", bottom="filet"
)
if index & CellFormat.EMPHASE_ATB:
make_pattern(style=style, bgcolor=CellColor.COLOR_ATB.value)
elif index & CellFormat.EMPHASE_ATT:
make_pattern(style=style, bgcolor=CellColor.COLOR_ATT.value)
elif index & CellFormat.EMPHASE_PREC:
make_pattern(style=style, bgcolor=CellColor.COLOR_PREC.value)
elif index & CellFormat.EMPHASE_COMP:
make_pattern(style=style, bgcolor=CellColor.COLOR_COMP.value)
elif index & CellFormat.EMPHASE_UEACQ:
make_pattern(style=style, bgcolor=CellColor.COLOR_UEACQ.value)
elif index & CellFormat.EMPHASE_ABS:
make_pattern(style=style, bgcolor=CellColor.COLOR_ABS.value)
# elif not index & GRAYED:
# make_pattern(style=style, bgcolor=0x43)
if index & (CellFormat.FMT_CAP_UE | CellFormat.FMT_UE | CellFormat.FMT_MOY):
if index & CellFormat.PRECISE:
make_numfmt("#0.00", style)
else:
make_numfmt("#0.0", style)
top = border_enable if index & CellBorder.TOP else self.default_top
bottom = border_enable if index & CellBorder.BOTTOM else self.default_bottom
left = border_enable if index & CellBorder.LEFT else self.default_left
right = border_enable if index & CellBorder.RIGHT else self.default_right
make_borders(style=style, top=top, left=left, right=right, bottom=bottom)
self.style_table[index] = style
return self.style_table[index]
def width(self, item):
if item is None:
return 1
else:
compte = 0
for subitem in item.values():
compte += self.width(subitem)
return compte
def set_default_format(self, default_format):
self.default_format = default_format

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # Gestion scolarite IUT
# #
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -28,18 +28,28 @@
"""Feuille excel pour préparation des jurys classiques (non BUT) """Feuille excel pour préparation des jurys classiques (non BUT)
""" """
import collections import collections
import logging
import time import time
from enum import Enum
from openpyxl.styles.numbers import FORMAT_NUMBER_00 from openpyxl.styles.numbers import FORMAT_NUMBER_00
from flask import flash from flask import flash, current_app
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from app import db
from app.but import bulletin_but, cursus_but
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, ScolarAutorisationInscription from app.models import (
from app.scodoc import sco_assiduites, codes_cursus FormSemestre,
Identite,
ScolarAutorisationInscription,
ApcParcours,
)
from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
@ -48,121 +58,512 @@ from app.scodoc import sco_cursus
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import sco_version import sco_version
from app.scodoc.sco_excel import excel_make_style, COLORS
from app.scodoc.sco_excel_add import make_pattern
from app.scodoc.sco_prepajury_formats import Formatter
COLORS = {
"ETUDIANT": ["FFFF99"],
"BUT1": ["95B3D7", "B8CCE4", "DCE6F1"],
"BUT2": ["C4D79B", "D8E4BC", "EBF1DE"],
"BUT3": ["FABF8F", "FCD5B4", "FDE9D9"],
"RECAP": ["D9D9D9"],
}
CATEGORIES = {
"ETUDIANT": {
"Id": None,
"NIP": None,
"civ": None,
"Nom": None,
"Prénom": None,
"Régime": None,
"Cursus": None,
"Absences": {
"Tot.": None,
"Non Just": None,
},
},
"BUT1": {
"+<comp 1>": {
"+UE 1.1": {
"Note": None,
"Rés.": None,
},
"+UE 2.1": {
"Note": None,
"Rés.": None,
},
"BCC 1": {
"Note": None,
"Rés.": None,
},
},
"+<comp 2>": {
"+UE 1.2": {
"Note": None,
"Rés.": None,
},
"+UE 2.2": {
"Note": None,
"Rés.": None,
},
"BCC 2": {
"Note": None,
"Rés.": None,
},
},
"+<comp 3>": {
"+UE 1.3": {
"Note": None,
"Rés.": None,
},
"+UE 2.3": {
"Note": None,
"Rés.": None,
},
"BCC 3": {
"Note": None,
"Rés.": None,
},
},
"+<comp 4>": {
"+UE 1.4": {
"Note": None,
"Rés.": None,
},
"+UE 2.4": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 5>": {
"+UE 1.5": {
"Note": None,
"Rés.": None,
},
"+UE 2.5": {
"Note": None,
"Rés.": None,
},
"BCC 5": {
"Note": None,
"Rés.": None,
},
},
"+<comp 6>": {
"+UE 1.6": {
"Note": None,
"Rés.": None,
},
"+UE 2.6": {
"Note": None,
"Rés.": None,
},
"BCC 6": {
"Note": None,
"Rés.": None,
},
},
"Nb RCUE": None,
"Moy.": None,
"Rés.": None,
},
"BUT2": {
"+<comp 1>": {
"+UE 3.1": {
"Note": None,
"Rés.": None,
},
"+UE 4.1": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 2>": {
"+UE 3.2": {
"Note": None,
"Rés.": None,
},
"+UE 4.2": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 3>": {
"+UE 3.3": {
"Note": None,
"Rés.": None,
},
"+UE 4.3": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 4>": {
"+UE 3.4": {
"Note": None,
"Rés.": None,
},
"+UE 4.4": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 5>": {
"+UE 3.5": {
"Note": None,
"Rés.": None,
},
"+UE 4.5": {
"Note": None,
"Rés.": None,
},
"BCC 5": {
"Note": None,
"Rés.": None,
},
},
"+<comp 6>": {
"+UE 3.6": {
"Note": None,
"Rés.": None,
},
"+UE 4.6": {
"Note": None,
"Rés.": None,
},
"BCC 6": {
"Note": None,
"Rés.": None,
},
},
"Nb RCUE": None,
"Moy.": None,
"Rés.": None,
"DUT": None,
},
"BUT3": {
"+<comp 1>": {
"+UE 5.1": {
"Note": None,
"Rés.": None,
},
"+UE 6.1": {
"Note": None,
"Rés.": None,
},
"BCC 1": {
"Note": None,
"Rés.": None,
},
},
"+<comp 2>": {
"+UE 5.2": {
"Note": None,
"Rés.": None,
},
"+UE 6.2": {
"Note": None,
"Rés.": None,
},
"BCC 2": {
"Note": None,
"Rés.": None,
},
},
"+<comp 3>": {
"+UE 5.3": {
"Note": None,
"Rés.": None,
},
"+UE 6.3": {
"Note": None,
"Rés.": None,
},
"BCC 3": {
"Note": None,
"Rés.": None,
},
},
"+<comp 4>": {
"+UE 5.4": {
"Note": None,
"Rés.": None,
},
"+UE 6.4": {
"Note": None,
"Rés.": None,
},
"BCC 4": {
"Note": None,
"Rés.": None,
},
},
"+<comp 5>": {
"+UE 5.5": {
"Note": None,
"Rés.": None,
},
"+UE 6.5": {
"Note": None,
"Rés.": None,
},
"BCC 5": {
"Note": None,
"Rés.": None,
},
},
"+<comp 6>": {
"+UE 5.6": {
"Note": None,
"Rés.": None,
},
"+UE 6.6": {
"Note": None,
"Rés.": None,
},
"BCC 6": {
"Note": None,
"Rés.": None,
},
},
"Nb RCUE": None,
"Moy.": None,
"Rés.": None,
"BUT": None,
},
"RECAP": {
"Devenir": None,
"Etape": None,
"Remarque": None,
},
}
class IndexApc:
def __init__(self):
self.niveau: int = None
self.rcue: int = None
self.semestre: int = None
def __init__(self, niveau):
self.__init__()
self.niveau = niveau
def __init__(self, niveau, ue):
self.__init__(niveau)
def __init__(self, niveau, ue, semestre):
self.__init__(niveau, ue)
self.semestre = semestre
class Export:
def __init__(self, formsemestre_id):
self.formsemestre_id: int = formsemestre_id
self.formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
self.nt: NotesTableCompat = res_sem.load_formsemestre_results(self.formsemestre)
etud_groups = sco_groups.formsemestre_get_etud_groupnames(formsemestre_id)
main_partition_id = sco_groups.formsemestre_get_main_partition(formsemestre_id)
self.etuds = self.nt.get_inscrits(order_by="moy")
self.formation_code = self.formsemestre.formation.formation_code
self.dept_name = scu.unescape_html(
sco_preferences.get_preference("DeptName", formsemestre_id)
)
self.parcours = {}
self.competences = {}
self.data_etud = {}
self.data_sems = {}
self.data_comp = {}
self.load_data()
self.formatteur = Formatter()
self.build_formator()
def load_parcours(self):
for etud in self.etuds:
parcours_id = self.nt.etuds_parcour_id[etud.id]
if parcours_id is not None and parcours_id not in self.data_comp:
parcours = db.session.get(ApcParcours, parcours_id)
self.data_comp[parcours_id] = parcours
def load_formsemestre_data(self, formsemestre: FormSemestre):
sem_id = formsemestre.id
self.data_sems[sem_id] = {}
self.data_sems[sem_id]["formsemestre"] = formsemestre
nt = res_sem.load_formsemestre_results(formsemestre)
self.data_sems[sem_id]["nt"] = nt
def load_formsemestre(self, etud, session):
for sem in session.sems:
if sem["formation"].formation_code == self.formation_code:
sem_id = sem["id"]
formsemestre: FormSemestre = FormSemestre.get_formsemestre(sem_id)
if sem_id not in self.data_sems:
self.load_formsemestre_data(formsemestre)
if session.prev_formsemestre:
prev_id = session.prev_formsemestre.id
prev_session = sco_cursus.get_situation_etud_cursus(
etud, prev_id
)
self.load_formsemestre(etud, prev_session)
semestre_id = formsemestre.semestre_id
parcours = self.data_sems[sem_id]["nt"].etuds_parcour_id[etud.id]
if parcours is None or parcours == self.nt.etuds_parcour_id[etud.id]:
self.data_etud[etud.id]["sems"][semestre_id] = formsemestre
else:
current_app.logger.info(f"{etud.id}: Changement de parcours: ")
def load_etu(self, etud):
# etudid=str(etudid),
# code_nip=etud.code_nip or "",
# code_ine=etud.code_ine or "",
# nom=quote_xml_attr(etud.nom),
# prenom=quote_xml_attr(etud.prenom),
# civilite=quote_xml_attr(etud.civilite_str),
# sexe=quote_xml_attr(etud.civilite_str), # compat
bulletins_sem = bulletin_but.BulletinBUT(self.formsemestre)
self.data_etud[etud.id]["bulletin_but"] = bulletins_sem.bulletin_etud(etud)
self.data_etud[etud.id]["cursus"] = cursus_but.EtudCursusBUT(
etud, self.formsemestre.formation
)
# self.data_etud[etud.id]["bulletin_sem"] = bulletins_sem.bulletin_etud(etud)
breakpoint()
def load_data(self):
self.load_parcours()
for etud in self.etuds:
self.data_etud[etud.id] = {}
self.data_etud[etud.id]["etud"] = etud
self.data_etud[etud.id]["sems"] = {}
self.load_etu(etud)
session = sco_cursus.get_situation_etud_cursus(etud, self.formsemestre_id)
breakpoint()
self.data_etud[etud.id]["cursus"] = session.parcours
self.load_formsemestre(etud, session)
def build_formator(self):
self.formatteur = Formatter()
self.formatteur.set_default_format(
excel_make_style(font_name="Calibri", size=9)
)
self.formatteur.set_borders_default(
left="none", right="none", top="filet", bottom="filet"
)
self.formatteur.last_rank = 10 # len(lignes)
# self.formatteur.set_categories(CATEGORIES)
def header_format(self, styles, items, row_no, left, color, level):
if row_no <= 4:
if items is None:
# styles[row_no].append(color[level])
styles[row_no].append(excel_make_style(bgcolor=color[level]))
self.header_format(styles, None, row_no + 1, left, color, level)
return 1
else:
color = items.get("COLOR", color)
compte = 0
for item in items:
if item in COLORS:
color = COLORS[item]
detail = items[item]
if item[0] == "+":
level2 = level + 1
else:
level2 = level
styles[row_no].append(excel_make_style(bgcolor=color[level2]))
nbelt = self.header_format(
styles,
detail,
row_no + 1,
left + compte,
color,
level2,
)
for _ in range(1, nbelt):
styles[row_no].append(excel_make_style(bgcolor=color[level2]))
compte += nbelt
# current_app.logger.info(f" {row_no}, {left}, {nbelt} ")
# sheet.ws.merge_cells(
# start_row=row_no + 1,
# end_row=row_no + 1,
# start_column=left + 1,
# end_column=left + nbelt + 1,
# )
return compte
else:
return 1
def header_append(self, raws, items, row_no):
if row_no <= 4:
if items is None:
raws[row_no].append("")
self.header_append(raws, None, row_no + 1)
return 1
else:
compte = 0
for item in items:
if item != "COLOR":
detail = items[item]
if item[0] != "+":
raws[row_no].append(item)
else:
raws[row_no].append(item[1:])
nbelt = self.header_append(raws, detail, row_no + 1)
for _ in range(1, nbelt):
raws[row_no].append("")
compte += nbelt
return compte
else:
return 1
def write_header(self, sheet):
raws = [[], [], [], [], []]
styles = [[], [], [], [], []]
col_no = 0
raw_no = 0
self.header_format(styles, CATEGORIES, 0, 0, ["FFFF00"], 0)
self.header_append(raws, CATEGORIES, 0)
for i in range(0, 4):
sheet.append_row(
sheet.make_row(
raws[i],
styles[i],
# style=excel_make_style(
# font_name="Calibri", size=12, bold=False, bgcolor="D0FFFF"
# ),
)
)
sheet.ws.merge_cells(start_row=9, end_row=8, start_column=1, end_column=10)
def feuille_preparation_lille(formsemestre_id): def feuille_preparation_lille(formsemestre_id):
"""Feuille excel pour préparation des jurys classiques. """Feuille excel pour préparation des jurys classiques."""
Non adaptée pour le BUT. export = Export(formsemestre_id)
"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
breakpoint()
etuds: list[Identite] = nt.get_inscrits(order_by="moy") # tri par moy gen
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etud_groups = sco_groups.formsemestre_get_etud_groupnames(formsemestre_id) sheet = sco_excel.ScoExcelSheet(sheet_name=f"Prepation Jury")
main_partition_id = sco_groups.formsemestre_get_main_partition(formsemestre_id)[
"partition_id"
]
prev_moy_ue = collections.defaultdict(dict) # ue_code_s : { etudid : moy ue }
prev_ue_acro = {} # ue_code_s : acronyme (à afficher)
prev_moy = {} # moyennes gen sem prec
moy_ue = collections.defaultdict(dict) # ue_acro : moyennes { etudid : moy ue }
ue_acro = {} # ue_code_s : acronyme (à afficher)
moy = {} # moyennes gen
moy_inter = {} # moyenne gen. sur les 2 derniers semestres
code = {} # decision existantes s'il y en a
autorisations = {}
prev_code = {} # decisions sem prec
assidu = {}
parcours = {} # etudid : parcours, sous la forme S1, S2, S2, S3
groupestd = {} # etudid : nom groupe principal
nbabs = {}
nbabsjust = {}
breakpoint()
for etud in etuds:
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if Se.prev_formsemestre:
ntp: NotesTableCompat = res_sem.load_formsemestre_results(
Se.prev_formsemestre
)
for ue in ntp.get_ues_stat_dict(filter_sport=True):
ue_status = ntp.get_etud_ue_status(etud.id, ue["ue_id"])
ue_code_s = (
ue["ue_code"] + "_%s" % ntp.sem["semestre_id"]
) # code indentifiant l'UE
prev_moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else ""
prev_ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
prev_moy[etud.id] = ntp.get_etud_moy_gen(etud.id)
prev_decision = ntp.get_etud_decision_sem(etud.id)
if prev_decision:
prev_code[etud.id] = prev_decision["code"]
if prev_decision["compense_formsemestre_id"]:
prev_code[etud.id] += "+" # indique qu'il a servi a compenser
moy[etud.id] = nt.get_etud_moy_gen(etud.id)
for ue in nt.get_ues_stat_dict(filter_sport=True):
ue_status = nt.get_etud_ue_status(etud.id, ue["ue_id"])
ue_code_s = f'{ue["ue_code"]}_{nt.sem["semestre_id"]}'
moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else ""
ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
if Se.prev_formsemestre:
try:
moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0
except (KeyError, TypeError):
pass
decision = nt.get_etud_decision_sem(etud.id)
if decision:
code[etud.id] = decision["code"]
if decision["compense_formsemestre_id"]:
code[etud.id] += "+" # indique qu'il a servi a compenser
assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "")
autorisations_etud = ScolarAutorisationInscription.query.filter_by(
etudid=etud.id, origin_formsemestre_id=formsemestre_id
).all()
autorisations[etud.id] = ", ".join(
[f"S{x.semestre_id}" for x in autorisations_etud]
)
# parcours:
parcours[etud.id] = "sco_prepajury_lille:132" # Se.get_parcours_descr()
# groupe principal (td)
groupestd[etud.id] = ""
for s in Se.etud.formsemestre_inscriptions:
if s.formsemestre_id == formsemestre_id:
groupestd[etud.id] = etud_groups.get(etud.id, {}).get(
main_partition_id, ""
)
# absences:
_, e_nbabsjust, e_nbabs = sco_assiduites.get_assiduites_count(etud.id, sem)
nbabs[etud.id] = e_nbabs
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
# Codes des UE "semestre précédent":
ue_prev_codes = list(prev_moy_ue.keys())
ue_prev_codes.sort(
key=lambda x, prev_ue_acro=prev_ue_acro: prev_ue_acro[ # pylint: disable=undefined-variable
x
]
)
# Codes des UE "semestre courant":
ue_codes = list(moy_ue.keys())
ue_codes.sort(
key=lambda x, ue_acro=ue_acro: ue_acro[x] # pylint: disable=undefined-variable
)
sid = sem["semestre_id"]
sn = sp = ""
if sid >= 0:
sn = f"S{sid}"
if prev_moy: # si qq chose dans precedent
sp = f"S{sid - 1}"
sheet = sco_excel.ScoExcelSheet(sheet_name=f"Prepa Jury {sn}")
# génération des styles # génération des styles
style_bold = sco_excel.excel_make_style(size=10, bold=True) style_bold = sco_excel.excel_make_style(size=10, bold=True)
style_center = sco_excel.excel_make_style(halign="center") style_center = sco_excel.excel_make_style(halign="center")
style_boldcenter = sco_excel.excel_make_style(bold=True, halign="center") style_boldcenter = sco_excel.excel_make_style(bold=True, halign="center")
@ -177,10 +578,11 @@ def feuille_preparation_lille(formsemestre_id):
) )
# Première ligne # Première ligne
sheet.append_single_cell_row( # sheet.append_single_cell_row(
"Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold # "Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold
) # )
sheet.append_blank_row() sheet.append_blank_row()
export.write_header(sheet)
# Ligne de titre # Ligne de titre
titles = ["Rang"] titles = ["Rang"]
@ -188,154 +590,138 @@ def feuille_preparation_lille(formsemestre_id):
titles.append("NIP") titles.append("NIP")
if sco_preferences.get_preference("prepa_jury_ine"): if sco_preferences.get_preference("prepa_jury_ine"):
titles.append("INE") titles.append("INE")
titles += [ # if prev_moy: # si qq chose dans precedent
"etudid", # titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [
"Civ.", # f"Moy {sp}",
"Nom", # f"Décision {sp}",
"Prénom", # ]
"Naissance", # titles += [ue_acro[x][1] for x in ue_codes] + [f"Moy {sn}"]
"Bac", # if moy_inter:
"Spe", # titles += [f"Moy {sp}-{sn}"]
"Rg Adm", # titles += ["Abs", "Abs Injust."]
"Parcours", # if code:
"Groupe", # titles.append("Proposit. {sn}")
] # if autorisations:
if prev_moy: # si qq chose dans precedent # titles.append("Autorisations")
titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [ # # titles.append('Assidu')
f"Moy {sp}", # sheet.append_row(sheet.make_row(titles, style_boldcenter))
f"Décision {sp}", # def fmt(x):
] # "reduit les notes a deux chiffres"
titles += [ue_acro[x][1] for x in ue_codes] + [f"Moy {sn}"] # x = scu.fmt_note(x, keep_numeric=False)
if moy_inter: # try:
titles += [f"Moy {sp}-{sn}"] # return float(x)
titles += ["Abs", "Abs Injust."] # except:
if code: # return x
titles.append("Proposit. {sn}")
if autorisations:
titles.append("Autorisations")
# titles.append('Assidu')
sheet.append_row(sheet.make_row(titles, style_boldcenter))
# if prev_moy:
# tit_prev_moy = "Moy " + sp
# # col_prev_moy = titles.index(tit_prev_moy)
# tit_moy = "Moy " + sn
# col_moy = titles.index(tit_moy)
# col_abs = titles.index("Abs")
def fmt(x):
"reduit les notes a deux chiffres"
x = scu.fmt_note(x, keep_numeric=False)
try:
return float(x)
except:
return x
i = 1 # numero etudiant
for etud in etuds:
cells = []
cells.append(sheet.make_cell(str(i)))
if sco_preferences.get_preference("prepa_jury_nip"):
cells.append(sheet.make_cell(etud.code_nip))
if sco_preferences.get_preference("prepa_jury_ine"):
cells.append(sheet.make_cell(etud.code_ine))
admission = etud.admission
cells += sheet.make_row(
[
etud.id,
etud.civilite_str,
sco_etud.format_nom(etud.nom),
sco_etud.format_prenom(etud.prenom),
etud.date_naissance,
admission.bac,
admission.specialite,
admission.classement,
parcours[etud.id],
groupestd[etud.id],
]
)
co = len(cells)
if prev_moy:
for ue_acro in ue_prev_codes:
cells.append(
sheet.make_cell(
fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
)
)
co += 1
cells.append(
sheet.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold)
) # moy gen prev
cells.append(
sheet.make_cell(fmt(prev_code.get(etud.id, "")), style_moy)
) # decision prev
co += 2
for ue_acro in ue_codes:
cells.append(
sheet.make_cell(
fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
)
)
co += 1
cells.append(
sheet.make_cell(fmt(moy.get(etud.id, "")), style_note_bold)
) # moy gen
co += 1
if moy_inter:
cells.append(sheet.make_cell(fmt(moy_inter.get(etud.id, "")), style_note))
cells.append(sheet.make_cell(str(nbabs.get(etud.id, "")), style_center))
cells.append(sheet.make_cell(str(nbabsjust.get(etud.id, "")), style_center))
if code:
cells.append(sheet.make_cell(code.get(etud.id, ""), style_moy))
cells.append(sheet.make_cell(autorisations.get(etud.id, ""), style_moy))
# l.append(assidu.get(etud.id, ''))
sheet.append_row(cells)
i += 1
# #
sheet.append_blank_row() # i = 1 # numero etudiant
# Explications des codes # for etud in etuds:
codes = list(codes_cursus.CODES_EXPL.keys()) # cells = []
codes.sort() # cells.append(sheet.make_cell(str(i)))
sheet.append_single_cell_row("Explication des codes") # if sco_preferences.get_preference("prepa_jury_nip"):
for code in codes: # cells.append(sheet.make_cell(etud.code_nip))
sheet.append_row( # if sco_preferences.get_preference("prepa_jury_ine"):
sheet.make_row(["", "", "", code, codes_cursus.CODES_EXPL[code]]) # cells.append(sheet.make_cell(etud.code_ine))
) # cells += sheet.make_row(
sheet.append_row( # [
sheet.make_row( # etud.id,
[ # etud.civilite_str,
"", # scu.format_nom(etud.nom),
"", # scu.format_prenom(etud.prenom),
"", # etud.date_naissance,
"ADM+", # etud.admission.bac if etud.admission else "",
"indique que le semestre a déjà servi à en compenser un autre", # etud.admission.specialite if etud.admission else "",
] # etud.admission.classement if etud.admission else "",
) # parcours[etud.id],
) # groupestd[etud.id],
# ]
# )
# co = len(cells)
# if prev_moy:
# for ue_acro in ue_prev_codes:
# cells.append(
# sheet.make_cell(
# fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
# )
# )
# co += 1
# cells.append(
# sheet.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold)
# ) # moy gen prev
# cells.append(
# sheet.make_cell(fmt(prev_code.get(etud.id, "")), style_moy)
# ) # decision prev
# co += 2
#
# for ue_acro in ue_codes:
# cells.append(
# sheet.make_cell(
# fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
# )
# )
# co += 1
# cells.append(
# sheet.make_cell(fmt(moy.get(etud.id, "")), style_note_bold)
# ) # moy gen
# co += 1
# if moy_inter:
# cells.append(sheet.make_cell(fmt(moy_inter.get(etud.id, "")), style_note))
# cells.append(sheet.make_cell(nbabs.get(etud.id, ""), style_center))
# cells.append(sheet.make_cell(nbabsjust.get(etud.id, ""), style_center))
# if code:
# cells.append(sheet.make_cell(code.get(etud.id, ""), style_moy))
# cells.append(sheet.make_cell(autorisations.get(etud.id, ""), style_moy))
# # l.append(assidu.get(etud.id, ''))
# sheet.append_row(cells)
# i += 1
# #
# sheet.append_blank_row()
# # Explications des codes
# codes = list(codes_cursus.CODES_EXPL.keys())
# codes.sort()
# sheet.append_single_cell_row("Explication des codes")
# for code in codes:
# sheet.append_row(
# sheet.make_row(["", "", "", code, codes_cursus.CODES_EXPL[code]])
# )
# sheet.append_row(
# sheet.make_row(
# [
# "",
# "",
# "",
# "ADM+",
# "indique que le semestre a déjà servi à en compenser un autre",
# ]
# )
# )
# UE : Correspondances acronyme et titre complet # UE : Correspondances acronyme et titre complet
sheet.append_blank_row() sheet.append_blank_row()
sheet.append_single_cell_row("Titre des UE")
if prev_moy: # sheet.append_single_cell_row("Titre des UE")
for ue in ntp.get_ues_stat_dict(filter_sport=True): # if prev_moy:
sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]])) # for ue in ntp.get_ues_stat_dict(filter_sport=True):
for ue in nt.get_ues_stat_dict(filter_sport=True): # sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]]))
sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]])) # for ue in nt.get_ues_stat_dict(filter_sport=True):
# # sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]]))
sheet.append_blank_row() # #
sheet.append_single_cell_row( # sheet.append_blank_row()
"Préparé par %s le %s sur %s pour %s" # sheet.append_single_cell_row(
% ( # "Préparé par %s le %s sur %s pour %s"
sco_version.SCONAME, # % (
time.strftime("%d/%m/%Y"), # sco_version.SCONAME,
request.url_root, # time.strftime(scu.DATE_FMT),
current_user, # request.url_root,
) # current_user,
) # )
# )
xls = sheet.generate() xls = sheet.generate()
flash("Feuille préparation jury générée") flash("Feuille préparation jury générée")
return scu.send_file( return scu.send_file(
xls, xls,
f"PrepaJury{sn}", f"PrepaJury",
scu.XLSX_SUFFIX, scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE, mime=scu.XLSX_MIMETYPE,
) )

View File

@ -931,11 +931,12 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
return "\n".join(H) return "\n".join(H)
def feuille_saisie_notes(evaluation_id, group_ids=[]): def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués""" """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id) evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
if not evaluation: if not evaluation:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("invalid evaluation_id")
group_ids = group_ids or []
modimpl = evaluation.moduleimpl modimpl = evaluation.moduleimpl
formsemestre = modimpl.formsemestre formsemestre = modimpl.formsemestre
mod_responsable = sco_users.user_info(modimpl.responsable_id) mod_responsable = sco_users.user_info(modimpl.responsable_id)
@ -950,7 +951,8 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
if evaluation.date_debut if evaluation.date_debut
else "(sans date)" else "(sans date)"
) )
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}""" eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"
} {date_str}"""
description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({ description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
evaluation.moduleimpl.module.code evaluation.moduleimpl.module.code
@ -986,7 +988,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
rows.append( rows.append(
[ [
str(etudid), str(etudid),
e["nom"].upper(), e.get("nom_disp", "") or e.get("nom_usuel", "") or e["nom"],
e["prenom"].lower().capitalize(), e["prenom"].lower().capitalize(),
e["inscr"]["etat"], e["inscr"]["etat"],
grc, grc,
@ -1206,7 +1208,7 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
def _form_saisie_notes( def _form_saisie_notes(
evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination="" evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination=""
): ):
"""Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M """Formulaire HTML saisie des notes dans l'évaluation du moduleimpl
pour les groupes indiqués. pour les groupes indiqués.
On charge tous les étudiants, ne seront montrés que ceux On charge tous les étudiants, ne seront montrés que ceux

View File

@ -1018,23 +1018,21 @@ sco_publish("/get_photo_image", sco_photos.get_photo_image, Permission.ScoView)
sco_publish("/etud_photo_html", sco_photos.etud_photo_html, Permission.ScoView) sco_publish("/etud_photo_html", sco_photos.etud_photo_html, Permission.ScoView)
@bp.route("/etud_photo_orig_page") @bp.route("/etud_photo_orig_page/<int:etudid>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func def etud_photo_orig_page(etudid):
def etud_photo_orig_page(etudid=None):
"Page with photo in orig. size" "Page with photo in orig. size"
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = Identite.get_etud(etudid)
H = [ return f"""{
html_sco_header.sco_header(page_title=etud["nomprenom"]), html_sco_header.sco_header(etudid=etud.id, page_title=etud.nomprenom)
"<h2>%s</h2>" % etud["nomprenom"], }
'<div><a href="%s">' <h2>{etud.nomprenom}</h2>
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid), <div>
sco_photos.etud_photo_orig_html(etud), <a href="{etud.url_fiche()}">{etud.photo_html(size='orig')}</a>
"</a></div>", </div>
html_sco_header.sco_footer(), {html_sco_header.sco_footer()}
] """
return "\n".join(H)
@bp.route("/form_change_photo", methods=["GET", "POST"]) @bp.route("/form_change_photo", methods=["GET", "POST"])

View File

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

View File

@ -280,3 +280,19 @@ def test_semestres_courant(api_headers):
assert len(result_a) > 0 assert len(result_a) > 0
sem = result_a[0] sem = result_a[0]
assert verify_fields(sem, FORMSEMESTRE_FIELDS) is True assert verify_fields(sem, FORMSEMESTRE_FIELDS) is True
# accès avec id incorrect
r = requests.get(
f"{API_URL}/departement/id/bad/formsemestres_courants?date_courante=2022-07-01",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 404
r = requests.get(
f"{API_URL}/departement/id/-1/formsemestres_courants?date_courante=2022-07-01",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 404

View File

@ -66,6 +66,7 @@ def test_permissions(api_headers):
"assiduite_id": 1, "assiduite_id": 1,
"justif_id": 1, "justif_id": 1,
"etudids": "1", "etudids": "1",
"ue_id": 1,
} }
# Arguments spécifiques pour certaines routes # Arguments spécifiques pour certaines routes
# par défaut, on passe tous les arguments de all_args # par défaut, on passe tous les arguments de all_args

View File

@ -172,7 +172,7 @@ class Token:
sub_group = ET.Element("g") sub_group = ET.Element("g")
# On décale l'élément de la query vers la droite par rapport à l'élément parent # On décale l'élément de la query vers la droite par rapport à l'élément parent
translate_x = x_offset + x_step translate_x = x_offset + _get_group_width(group) + x_step // 2
# création élément (param) # création élément (param)
param_el = _create_svg_element(key, COLORS.BLUE) param_el = _create_svg_element(key, COLORS.BLUE)
@ -192,7 +192,7 @@ class Token:
# création élément (=) # création élément (=)
equal_el = _create_svg_element("=", COLORS.GREY) equal_el = _create_svg_element("=", COLORS.GREY)
# On met à jour le décalage en fonction de l'élément précédent # On met à jour le décalage en fonction de l'élément précédent
translate_x = x_offset + x_step + _get_element_width(param_el) translate_x += _get_element_width(param_el)
equal_el.set( equal_el.set(
"transform", "transform",
f"translate({translate_x}, {query_y_offset})", f"translate({translate_x}, {query_y_offset})",
@ -202,11 +202,7 @@ class Token:
# création élément (value) # création élément (value)
value_el = _create_svg_element(value, COLORS.GREEN) value_el = _create_svg_element(value, COLORS.GREEN)
# On met à jour le décalage en fonction des éléments précédents # On met à jour le décalage en fonction des éléments précédents
translate_x = ( translate_x += _get_element_width(equal_el)
x_offset
+ x_step
+ sum(_get_element_width(el) for el in [param_el, equal_el])
)
value_el.set( value_el.set(
"transform", "transform",
f"translate({translate_x}, {query_y_offset})", f"translate({translate_x}, {query_y_offset})",
@ -214,16 +210,13 @@ class Token:
sub_group.append(value_el) sub_group.append(value_el)
# Si il y a qu'un seul élément dans la query, on ne met pas de `&` # Si il y a qu'un seul élément dans la query, on ne met pas de `&`
if len(self.query) == 1: if len(self.query) == 1:
query_sub_element.append(sub_group)
continue continue
# création élément (&) # création élément (&)
ampersand_group = _create_svg_element("&", "rgb(224,224,224)") ampersand_group = _create_svg_element("&", "rgb(224,224,224)")
# On met à jour le décalage en fonction des éléments précédents # On met à jour le décalage en fonction des éléments précédents
translate_x = ( translate_x += _get_element_width(value_el)
x_offset
+ x_step
+ sum(_get_element_width(el) for el in [param_el, equal_el, value_el])
)
ampersand_group.set( ampersand_group.set(
"transform", "transform",
f"translate({translate_x}, {query_y_offset})", f"translate({translate_x}, {query_y_offset})",
@ -466,6 +459,10 @@ def gen_api_map(app, endpoint_start="api"):
# On positionne le token courant sur le token racine # On positionne le token courant sur le token racine
current_token = api_map current_token = api_map
# Récupération de la fonction associée à la route
func = app.view_functions[rule.endpoint]
func_name = parse_doc_name(func.__doc__ or "") or func.__name__
# Pour chaque segment de la route # Pour chaque segment de la route
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
# On cherche si le segment est déjà un enfant du token courant # On cherche si le segment est déjà un enfant du token courant
@ -473,7 +470,6 @@ def gen_api_map(app, endpoint_start="api"):
# Si ce n'est pas le cas on crée un nouveau token et on l'ajoute comme enfant # Si ce n'est pas le cas on crée un nouveau token et on l'ajoute comme enfant
if child is None: if child is None:
func = app.view_functions[rule.endpoint]
# Si c'est le dernier segment, on marque le token comme une leaf # Si c'est le dernier segment, on marque le token comme une leaf
# On utilise force_leaf car il est possible que le token ne soit que # On utilise force_leaf car il est possible que le token ne soit que
# momentanément une leaf # momentanément une leaf
@ -499,7 +495,7 @@ def gen_api_map(app, endpoint_start="api"):
# On ajoute le token comme enfant du token courant # On ajoute le token comme enfant du token courant
# en donnant la méthode et le nom de la fonction associée # en donnant la méthode et le nom de la fonction associée
child.func_name = func.__name__ child.func_name = func_name
method: str = "POST" if "POST" in rule.methods else "GET" method: str = "POST" if "POST" in rule.methods else "GET"
child.method = method child.method = method
current_token.add_child(child) current_token.add_child(child)
@ -607,6 +603,9 @@ def generate_svg(element, fname):
) )
ET.SubElement(marker, "polygon", {"points": "0 0, 10 3.5, 0 7"}) ET.SubElement(marker, "polygon", {"points": "0 0, 10 3.5, 0 7"})
# Ajoute un décalage vertical pour avoir un peu de padding en haut
element.set("transform", "translate(0, 10)")
# Ajout de l'élément principal à l'élément racine # Ajout de l'élément principal à l'élément racine
svg.append(element) svg.append(element)
@ -615,6 +614,50 @@ def generate_svg(element, fname):
tree.write(fname, encoding="utf-8", xml_declaration=True) tree.write(fname, encoding="utf-8", xml_declaration=True)
def _get_doc_lines(keyword, doc) -> list[str]:
"""
Renvoie les lignes de la doc qui suivent le mot clé keyword
La doc doit contenir des lignes de la forme:
KEYWORD
-------
...
"""
# Récupérer les lignes de la doc
lines = [line.strip() for line in doc.split("\n")]
# On cherche la ligne "KEYWORD" et on vérifie que la ligne suivante est "-----"
# Si ce n'est pas le cas, on renvoie un dictionnaire vide
try:
kw_index = lines.index(keyword)
kw_line = "-" * len(keyword)
if lines[kw_index + 1] != kw_line:
return []
except ValueError:
return []
# On récupère les lignes de la doc qui correspondent au keyword (enfin on espère)
kw_lines = lines[kw_index + 2 :]
return kw_lines
def parse_doc_name(doc):
"""
renvoie le nom de la route à partir de la docstring
La doc doit contenir des lignes de la forme:
DOC_ANCHOR
----------
nom_de_la_route
Il ne peut y avoir qu'une seule ligne suivant -----
"""
name_lines: list[str] = _get_doc_lines("DOC_ANCHOR", doc)
return name_lines[0] if name_lines else None
def parse_query_doc(doc): def parse_query_doc(doc):
""" """
renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>}) renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>})
@ -630,18 +673,7 @@ def parse_query_doc(doc):
Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser
Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre
""" """
# Récupérer les lignes de la doc query_lines: list[str] = _get_doc_lines("QUERY", doc)
lines = [line.strip() for line in doc.split("\n")]
# On cherche la ligne "QUERY" et on vérifie que la ligne suivante est "-----"
# Si ce n'est pas le cas, on renvoie un dictionnaire vide
try:
query_index = lines.index("QUERY")
if lines[query_index + 1] != "-----":
return {}
except ValueError:
return {}
# On récupère les lignes de la doc qui correspondent à la query (enfin on espère)
query_lines = lines[query_index + 2 :]
query = {} query = {}
regex = re.compile(r"^(\w+):(<.+>)$") regex = re.compile(r"^(\w+):(<.+>)$")