Compare commits

...

20 Commits

Author SHA1 Message Date
Emmanuel Viennet e44a5ee55d Corrige templates formsemestre 2024-04-07 19:52:22 +02:00
Emmanuel Viennet a747ed22e2 Ajoute équivalences pour ref. comp. QLIO 2024-04-07 19:51:34 +02:00
Emmanuel Viennet 5d0a932634 Bulletins BUT: utilisation de l'abbréviation du titre module si présente. 2024-04-06 12:33:07 +02:00
Emmanuel Viennet 2b150cf521 Modif config Jina2. Refonte ScoData, fournit par défaut à tous les templates. 2024-04-06 12:16:53 +02:00
Emmanuel Viennet 5a5ddcacd7 Associer une formation BUT à un nouveau référentiel 'équivalent'. 2024-04-05 23:41:34 +02:00
Emmanuel Viennet 3f6e65b9da Elimine @cached_property sur Identite, pourrait provoquer incohérences temporaires en multithread 2024-04-05 11:00:01 +02:00
Emmanuel Viennet 5eba6170a5 Fix: typo bloquant affichage formations avec UEs sans semestre_idx 2024-04-05 10:11:34 +02:00
Emmanuel Viennet bd9bf87112 Enrichissement du tableau des formations (coche 'détails') 2024-04-05 00:23:29 +02:00
Emmanuel Viennet a0e2af481f Fonction expérimentale pour changer le ref. de compétences d'une formation 2024-04-05 00:22:14 +02:00
Emmanuel Viennet 42e8f97441 Fix: missing exception 2024-04-04 11:23:26 +02:00
Emmanuel Viennet 8ec0171ca0 Script préparation démos: renommage de tous les étudiants 2024-04-03 19:02:40 +02:00
Emmanuel Viennet 6dfab2d843 Fix typo affichage heures 2024-04-03 18:47:44 +02:00
Emmanuel Viennet 523ec59833 Harmonisation formats affichage dates et heures 2024-04-02 23:37:23 +02:00
Emmanuel Viennet bde6325391 Enrichi tableau jury BUT PV 2024-04-02 17:11:07 +02:00
Emmanuel Viennet 0577347622 Tableau décision jury BUT excel: améliore colonne ECTS 2024-04-02 16:53:04 +02:00
Emmanuel Viennet 28d46e413d Filtrage par groupes dans els pages statistiques: fix #791 2024-03-31 23:04:54 +02:00
Emmanuel Viennet 126ea0741a Edition UE: cosmetic + arg check + invalidation cache desassoc_ue_niveau 2024-03-31 10:21:44 +02:00
Emmanuel Viennet a5b5f49f76 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-29 16:39:36 +01:00
Emmanuel Viennet 3e0b19c4a8 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-29 16:37:19 +01:00
Emmanuel Viennet 1dd5187fae lien vers doc évaluations 2024-03-29 16:36:05 +01:00
93 changed files with 1258 additions and 775 deletions

View File

@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
# Add some globals
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
from app.api import api_bp
from app.api import api_web_bp
# Jinja2 configuration
# Enable autoescaping of all templates, including .j2
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
# https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp)

View File

@ -329,6 +329,8 @@ def desassoc_ue_niveau(ue_id: int):
ue.niveau_competence = None
db.session.add(ue)
db.session.commit()
# Invalidation du cache
ue.formation.invalidate_cached_sems()
log(f"desassoc_ue_niveau: {ue}")
if g.scodoc_dept:
# "usage web"

View File

@ -21,7 +21,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
return ""
ref_comp = ue.formation.referentiel_competence
if ref_comp is None:
return f"""<div class="ue_advanced">
return f"""<div class="scobox ue_advanced">
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
@ -31,8 +31,8 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
H = [
"""
<div class="ue_advanced">
<h3>Parcours du BUT</h3>
<div class="scobox ue_advanced">
<div class="scobox-title">Parcours du BUT</div>
"""
]
# Choix des parcours
@ -43,7 +43,6 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
} != {None}
for parcour in ref_comp.parcours:
ects_parcour = ue.get_ects(parcour)
ects_parcour_txt = (
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
)

View File

@ -229,7 +229,7 @@ class BulletinBUT:
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
"titre": modimpl.module.titre_str(),
"code_apogee": modimpl.module.code_apogee,
"url": (
url_for(
@ -540,9 +540,9 @@ class BulletinBUT:
d.update(infos)
# --- Rangs
d[
"rang_nt"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
d["rang_nt"] = (
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
)
d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -31,6 +31,7 @@ from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc.sco_logos import Logo
from app.scodoc.sco_pdf import PDFLOCK, SU
from app.scodoc.sco_preferences import SemPreferences
from app.scodoc import sco_utils as scu
def make_bulletin_but_court_pdf(
@ -343,9 +344,11 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
for mod in self.bul[mod_type]:
row = [mod, bul[mod_type][mod]["titre"]]
row += [
bul["ues"][ue][mod_type][mod]["moyenne"]
if mod in bul["ues"][ue][mod_type]
else ""
(
bul["ues"][ue][mod_type][mod]["moyenne"]
if mod in bul["ues"][ue][mod_type]
else ""
)
for ue in self.ues_acronyms
]
rows.append(row)
@ -523,7 +526,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
if self.bul["semestre"].get("decision_annee", None):
txt += f"""
Décision saisie le {
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y")
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
<br/>

View File

@ -269,7 +269,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
date_capitalisation = ue.get("date_capitalisation")
if date_capitalisation:
fields_bmr.append(
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}"""
)
t = {
"titre": " - ".join(fields_bmr),

92
app/but/change_refcomp.py Normal file
View File

@ -0,0 +1,92 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Code expérimental: si deux référentiel sont presques identiques
(mêmes compétences, niveaux, parcours)
essaie de changer une formation de référentiel.
"""
from app import clear_scodoc_cache, db
from app.models import (
ApcParcours,
ApcReferentielCompetences,
ApcValidationRCUE,
Formation,
FormSemestreInscription,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoValueError
def formation_change_referentiel(
formation: Formation, new_ref: ApcReferentielCompetences
):
"""Try to change ref."""
if not formation.referentiel_competence:
raise ScoValueError("formation non associée à un référentiel")
if not isinstance(new_ref, ApcReferentielCompetences):
raise ScoValueError("nouveau référentiel invalide")
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
if isinstance(r, str):
raise ScoValueError(f"référentiels incompatibles: {r}")
parcours_map, competences_map, niveaux_map = r
formation.referentiel_competence = new_ref
db.session.add(formation)
# UEs - Niveaux et UEs - parcours
for ue in formation.ues:
if ue.niveau_competence:
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
db.session.add(ue)
if ue.parcours:
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
ue.parcours.clear()
ue.parcours.extend(new_list)
db.session.add(ue)
# Modules / parcours et app_critiques
for module in formation.modules:
if module.parcours:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
]
module.parcours.clear()
module.parcours.extend(new_list)
db.session.add(module)
if module.app_critiques: # efface les apprentissages critiques
module.app_critiques.clear()
db.session.add(module)
# ApcValidationRCUE
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
# FormSemestre / parcours_formsemestre
for formsemestre in formation.formsemestres:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
]
formsemestre.parcours.clear()
formsemestre.parcours.extend(new_list)
db.session.add(formsemestre)
# FormSemestreInscription.parcour_id
for inscr in FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id
).filter(FormSemestreInscription.parcour_id != None):
if inscr.parcour_id is not None:
inscr.parcour_id = parcours_map[inscr.parcour_id]
#
db.session.commit()
clear_scodoc_cache()

View File

@ -10,9 +10,11 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField
from wtforms.validators import DataRequired
class FormationRefCompForm(FlaskForm):
"Choix d'un référentiel"
referentiel_competence = SelectField(
"Choisir parmi les référentiels déjà chargés :"
)
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
class RefCompLoadForm(FlaskForm):
"Upload d'un référentiel"
referentiel_standard = SelectField(
"Choisir un référentiel de compétences officiel BUT"
)
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
)
return False
return True
class FormationChangeRefCompForm(FlaskForm):
"choix d'un nouveau ref. comp. pour une formation"
object_select = SelectField(
"Choisir le nouveau référentiel", validators=[DataRequired()]
)
submit = SubmitField("Changer le référentiel de la formation")
cancel = SubmitField("Annuler")

View File

@ -55,11 +55,21 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
else:
line_sep = "\n"
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
if fmt.startswith("xls"):
titles.update(
{
"etudid": "etudid",
"code_nip": "nip",
"code_ine": "ine",
"ects_but": "Total ECTS BUT",
"civilite": "Civ.",
"nom": "Nom",
"prenom": "Prénom",
}
)
# Style excel... passages à la ligne sur \n
xls_style_base = sco_excel.excel_make_style()
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
tab = GenTable(
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
caption=title,
@ -116,7 +126,7 @@ def pvjury_table_but(
"pas de référentiel de compétences associé à la formation de ce semestre !"
)
titles = {
"nom": "Code" if anonymous else "Nom",
"nom_pv": "Code" if anonymous else "Nom",
"cursus": "Cursus",
"ects": "ECTS",
"ues": "UE validées",
@ -144,33 +154,47 @@ def pvjury_table_but(
except ScoValueError:
deca = None
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
row = {
"nom": etud.code_ine or etud.code_nip or etud.id
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
else etud.etat_civil_pv(
line_sep=line_sep, with_paragraph=with_paragraph_nom
"nom_pv": (
etud.code_ine or etud.code_nip or etud.id
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
else etud.etat_civil_pv(
line_sep=line_sep, with_paragraph=with_paragraph_nom
)
),
"_nom_order": etud.sort_key,
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_target": url_for(
"_nom_pv_order": etud.sort_key,
"_nom_pv_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_pv_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_pv_target": url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}""",
"_ects_xls": deca.ects_annee(),
"ects_but": ects_but_valides,
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca
else "-",
"niveaux": (
deca.descr_niveaux_validation(line_sep=line_sep) if deca else "-"
),
"decision_but": deca.code_valide if deca else "",
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else "",
"devenir": (
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else ""
),
# pour exports excel seulement:
"civilite": etud.civilite_etat_civil_str,
"nom": etud.nom,
"prenom": etud.prenom_etat_civil or etud.prenom or "",
"etudid": etud.id,
"code_nip": etud.code_nip,
"code_ine": etud.code_ine,
}
if deca.valide_diplome() or not only_diplome:
rows.append(row)
rows.sort(key=lambda x: x["_nom_order"])
rows.sort(key=lambda x: x["_nom_pv_order"])
return rows, titles

View File

@ -21,8 +21,6 @@ from app.but.jury_but import (
DecisionsProposeesRCUE,
DecisionsProposeesUE,
)
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.models import (
ApcNiveau,
FormSemestre,
@ -33,11 +31,8 @@ from app.models import (
ScolarFormSemestreValidation,
ScolarNews,
)
from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
@ -217,7 +212,7 @@ def _gen_but_niveau_ue(
scoplement = f"""<div class="scoplement">
<div>
<b>UE {ue.acronyme} capitalisée </b>
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
<span>le {dec_ue.ue_status["event_date"].strftime(scu.DATE_FMT)}
</span>
</div>
<div>
@ -233,7 +228,7 @@ def _gen_but_niveau_ue(
<div>
<b>UE {ue.acronyme} antérieure </b>
<span>validée {dec_ue.validation.code}
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
le {dec_ue.validation.event_date.strftime(scu.DATE_FMT)}
</span>
</div>
<div>Non reprise dans l'année en cours</div>
@ -251,9 +246,7 @@ def _gen_but_niveau_ue(
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
if dec_ue.code_valide:
date_str = (
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
à {dec_ue.validation.event_date.strftime("%Hh%M")}
"""
f"""enregistré le {dec_ue.validation.event_date.strftime(scu.DATEATIME_FMT)}"""
if dec_ue.validation and dec_ue.validation.event_date
else ""
)

View File

@ -23,6 +23,7 @@ from app.models import (
)
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu
class ValidationsSemestre(ResultatsCache):
@ -84,7 +85,7 @@ class ValidationsSemestre(ResultatsCache):
"code": decision.code,
"assidu": decision.assidu,
"compense_formsemestre_id": decision.compense_formsemestre_id,
"event_date": decision.event_date.strftime("%d/%m/%Y"),
"event_date": decision.event_date.strftime(scu.DATE_FMT),
}
self.decisions_jury = decisions_jury
@ -107,7 +108,7 @@ class ValidationsSemestre(ResultatsCache):
decisions_jury_ues[decision.etudid][decision.ue.id] = {
"code": decision.code,
"ects": ects, # 0. si UE non validée
"event_date": decision.event_date.strftime("%d/%m/%Y"),
"event_date": decision.event_date.strftime(scu.DATE_FMT),
}
self.decisions_jury_ues = decisions_jury_ues

View File

@ -21,6 +21,7 @@ from app.scodoc import sco_abs_notification
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
@ -113,9 +114,9 @@ class Assiduite(ScoDocModel):
"entry_date": self.entry_date,
"user_id": None if user is None else user.id, # l'uid
"user_name": None if user is None else user.user_name, # le login
"user_nom_complet": None
if user is None
else user.get_nomcomplet(), # "Marie Dupont"
"user_nom_complet": (
None if user is None else user.get_nomcomplet()
), # "Marie Dupont"
"est_just": self.est_just,
"external_data": self.external_data,
}
@ -364,7 +365,7 @@ class Assiduite(ScoDocModel):
retourne le texte "saisie le <date> par <User>"
"""
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
date: str = self.entry_date.strftime(scu.DATEATIME_FMT)
utilisateur: str = ""
if self.user is not None:
self.user: User

View File

@ -8,16 +8,19 @@
from datetime import datetime
import functools
from operator import attrgetter
import yaml
from flask import g
from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app import db, log
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
def get_title(self) -> str:
"Titre affichable"
# utilise type_titre (B.U.T.), spécialité, version
return f"{self.type_titre} {self.specialite} {self.get_version()}"
def get_version(self) -> str:
"La version, normalement sous forme de date iso yyy-mm-dd"
if not self.version_orebut:
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"type_departement": self.type_departement,
"type_titre": self.type_titre,
"version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
"scodoc_date_loaded": (
self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else ""
),
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
@ -234,6 +244,92 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return parcours_info
def equivalents(self) -> set["ApcReferentielCompetences"]:
"""Ensemble des référentiels du même département
qui peuvent être considérés comme "équivalents", au sens
une formation de ce référentiel pourrait changer vers un équivalent,
en ignorant les apprentissages critiques.
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
niveaux et parcours (voir map_to_other_referentiel).
"""
candidats = ApcReferentielCompetences.query.filter_by(
dept_id=self.dept_id
).filter(ApcReferentielCompetences.id != self.id)
return {
referentiel
for referentiel in candidats
if not isinstance(self.map_to_other_referentiel(referentiel), str)
}
def map_to_other_referentiel(
self, other: "ApcReferentielCompetences"
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
"""Build mapping between this referentiel and ref2.
If successful, returns 3 dicts mapping self ids to other ids.
Else return a string, error message.
"""
if self.type_structure != other.type_structure:
return "type_structure mismatch"
if self.type_departement != other.type_departement:
return "type_departement mismatch"
# Table d'équivalences entre refs:
equiv = self._load_config_equivalences()
# mêmes parcours ?
eq_parcours = equiv.get("parcours", {})
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
parcours_by_code_2 = {
eq_parcours.get(p.code, p.code): p for p in other.parcours
}
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
return "parcours mismatch"
parcours_map = {
parcours_by_code_1[eq_parcours.get(code, code)]
.id: parcours_by_code_2[eq_parcours.get(code, code)]
.id
for code in parcours_by_code_1
}
# mêmes compétences ?
competence_by_code_1 = {c.titre: c for c in self.competences}
competence_by_code_2 = {c.titre: c for c in other.competences}
if competence_by_code_1.keys() != competence_by_code_2.keys():
return "competences mismatch"
competences_map = {
competence_by_code_1[titre].id: competence_by_code_2[titre].id
for titre in competence_by_code_1
}
# mêmes niveaux (dans chaque compétence) ?
niveaux_map = {}
for titre in competence_by_code_1:
c1 = competence_by_code_1[titre]
c2 = competence_by_code_2[titre]
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
return f"niveaux mismatch in comp. '{titre}'"
niveaux_map.update(
{
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
for a in niveau_by_attr_1
}
)
return parcours_map, competences_map, niveaux_map
def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels
"""
try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
doc = yaml.safe_load(f.read())
except FileNotFoundError:
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
return {}
except yaml.parser.ParserError as exc:
raise ScoValueError(
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
) from exc
return doc.get(self.specialite, {})
class ApcCompetence(db.Model, XMLModel):
"Compétence"
@ -374,9 +470,11 @@ class ApcNiveau(db.Model, XMLModel):
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {},
"app_critiques": (
{x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {}
),
}
def to_dict_bul(self):
@ -464,9 +562,9 @@ class ApcNiveau(db.Model, XMLModel):
return []
if competence is None:
parcour_niveaux: list[
ApcParcoursNiveauCompetence
] = annee_parcour.niveaux_competences
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
annee_parcour.niveaux_competences
)
niveaux: list[ApcNiveau] = [
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
for pn in parcour_niveaux

View File

@ -10,6 +10,7 @@ from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model):
@ -63,14 +64,13 @@ class ApcValidationRCUE(db.Model):
def __str__(self):
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
self.code} enregistrée le {self.date.strftime(scu.DATE_FMT)}"""
def html(self) -> str:
"description en HTML"
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
<b>{self.code}</b>
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
à {self.date.strftime("%Hh%M")}</em>"""
<em>enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}</em>"""
def annee(self) -> str:
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
@ -164,7 +164,7 @@ class ApcValidationAnnee(db.Model):
def html(self) -> str:
"Affichage html"
date_str = (
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
if self.date
else "(sans date)"
)

View File

@ -297,7 +297,7 @@ class Identite(models.ScoDocModel):
else:
return self.nom
@cached_property
@property
def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
@ -481,7 +481,9 @@ class Identite(models.ScoDocModel):
"code_ine": self.code_ine or "",
"code_nip": self.code_nip or "",
"date_naissance": (
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
self.date_naissance.strftime(scu.DATE_FMT)
if self.date_naissance
else ""
),
"dept_acronym": self.departement.acronym,
"dept_id": self.dept_id,
@ -733,7 +735,7 @@ class Identite(models.ScoDocModel):
"""
if with_paragraph:
return f"""{self.etat_civil}{line_sep}{self.code_nip or ""}{line_sep}{self.e} le {
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
self.date_naissance.strftime(scu.DATE_FMT) if self.date_naissance else ""}{
line_sep}à {self.lieu_naissance or ""}"""
return self.etat_civil

View File

@ -207,7 +207,9 @@ class Evaluation(models.ScoDocModel):
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
# Deprecated
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
e_dict["jour"] = (
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
)
return evaluation_enrich_dict(self, e_dict)
@ -315,10 +317,10 @@ class Evaluation(models.ScoDocModel):
def descr_heure(self) -> str:
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
return f"""à {self.date_debut.strftime("%Hh%M")}"""
return f"""à {self.date_debut.strftime(scu.TIME_FMT)}"""
elif self.date_debut and self.date_fin:
return f"""de {self.date_debut.strftime("%Hh%M")
} à {self.date_fin.strftime("%Hh%M")}"""
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
else:
return ""
@ -345,7 +347,7 @@ class Evaluation(models.ScoDocModel):
def _h(dt: datetime.datetime) -> str:
if dt.minute:
return dt.strftime("%Hh%M")
return dt.strftime(scu.TIME_FMT)
return f"{dt.hour}h"
if self.date_fin is None:
@ -539,8 +541,8 @@ class EvaluationUEPoids(db.Model):
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
"""add or convert some fields in an evaluation dict"""
# For ScoDoc7 compat
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
# Calcule durée en minutes
e_dict["descrheure"] = e.descr_heure()
@ -614,7 +616,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
):
raise ScoValueError(
f"""La date de début de l'évaluation ({
data["date_debut"].strftime("%d/%m/%Y")
data["date_debut"].strftime(scu.DATE_FMT)
}) n'est pas dans le semestre !""",
dest_url="javascript:history.back();",
)
@ -629,7 +631,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
):
raise ScoValueError(
f"""La date de fin de l'évaluation ({
data["date_fin"].strftime("%d/%m/%Y")
data["date_fin"].strftime(scu.DATE_FMT)
}) n'est pas dans le semestre !""",
dest_url="javascript:history.back();",
)

View File

@ -1,5 +1,7 @@
"""ScoDoc 9 models : Formations
"""
from flask import abort, g
from flask_sqlalchemy.query import Query
import app
@ -64,6 +66,21 @@ class Formation(db.Model):
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
@classmethod
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
"""Formation ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formation_id, int):
try:
formation_id = int(formation_id)
except (TypeError, ValueError):
abort(404, "formation_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
if dept_id is not None:
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
return cls.query.filter_by(id=formation_id).first_or_404()
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
"""As a dict.
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.

View File

@ -224,12 +224,12 @@ class FormSemestre(models.ScoDocModel):
d["formsemestre_id"] = self.id
d["titre_num"] = self.titre_num()
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
@ -255,12 +255,12 @@ class FormSemestre(models.ScoDocModel):
d["annee_scolaire"] = self.annee_scolaire()
d["bul_hide_xml"] = self.bul_hide_xml
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
@ -945,7 +945,7 @@ class FormSemestre(models.ScoDocModel):
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
}
@cached_property
@property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions}

View File

@ -72,7 +72,7 @@ class ScolarFormSemestreValidation(db.Model):
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
} ({self.ue_id}): {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
self.event_date.strftime("%d/%m/%Y")}"""
self.event_date.strftime(scu.DATE_FMT)}"""
def delete(self):
"Efface cette validation"
@ -113,14 +113,14 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue.parcours else ""}
{("émise par " + link)}
: <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
le {self.event_date.strftime(scu.DATEATIME_FMT)}
"""
else:
return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"}
{self.formsemestre.html_link_status() if self.formsemestre else ""}
: <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
le {self.event_date.strftime(scu.DATEATIME_FMT)}
"""
def ects(self) -> float:
@ -175,8 +175,8 @@ class ScolarAutorisationInscription(db.Model):
)
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
{link}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
le {self.date.strftime(scu.DATEATIME_FMT)}
"""
@classmethod
def autorise_etud(

View File

@ -263,16 +263,16 @@ class GenTable:
colspan_count -= 1
# if colspan_count > 0:
# continue # skip cells after a span
if pdf_mode:
content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
elif xls_mode:
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
if pdf_mode and f"_{cid}_pdf" in row:
content = row[f"_{cid}_pdf"]
elif xls_mode and f"_{cid}_xls" in row:
content = row[f"_{cid}_xls"]
else:
content = row.get(cid, "")
# Convert None to empty string ""
content = "" if content is None else content
colspan = row.get("_%s_colspan" % cid, 0)
colspan = row.get(f"_{cid}_colspan", 0)
if colspan > 1:
pdf_style_list.append(
(
@ -676,6 +676,7 @@ class GenTable:
fmt="html",
page_title="",
filename=None,
cssstyles=[],
javascripts=[],
with_html_headers=True,
publish=True,
@ -696,6 +697,7 @@ class GenTable:
H.append(
self.html_header
or html_sco_header.sco_header(
cssstyles=cssstyles,
page_title=page_title,
javascripts=javascripts,
init_qtip=init_qtip,
@ -721,7 +723,7 @@ class GenTable:
)
else:
return pdf_doc
elif fmt == "xls" or fmt == "xlsx": # dans les 2 cas retourne du xlsx
elif fmt in ("xls", "xlsx"): # dans les 2 cas retourne du xlsx
xls = self.excel()
if publish:
return scu.send_file(
@ -730,8 +732,7 @@ class GenTable:
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
else:
return xls
return xls
elif fmt == "text":
return self.text()
elif fmt == "csv":

View File

@ -179,6 +179,7 @@ def sco_header(
H = [
"""<!DOCTYPE html><html lang="fr">
<!-- ScoDoc legacy -->
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>

View File

@ -141,7 +141,9 @@ def sidebar(etudid: int = None):
params = {}
H = [
f"""<div class="sidebar">
f"""
<!-- sidebar py -->
<div class="sidebar">
{ sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud"
@ -180,9 +182,9 @@ def sidebar(etudid: int = None):
)
H.append(
f"""<span title="absences du {
formsemestre.date_debut.strftime("%d/%m/%Y")
formsemestre.date_debut.strftime(scu.DATE_FMT)
} au {
formsemestre.date_fin.strftime("%d/%m/%Y")
formsemestre.date_fin.strftime(scu.DATE_FMT)
}">({
sco_preferences.get_preference("assi_metrique", None)})
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""

View File

@ -12,6 +12,7 @@ import psycopg2.extras
from app import log
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
from app.scodoc import sco_utils as scu
quote_html = html.escape
@ -475,7 +476,7 @@ def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None:
if not isinstance(dmy, str):
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"')
try:
dt = datetime.datetime.strptime(dmy, "%d/%m/%Y")
dt = datetime.datetime.strptime(dmy, scu.DATE_FMT)
except ValueError:
try:
dt = datetime.datetime.fromisoformat(dmy)

View File

@ -34,6 +34,7 @@ from app.models.absences import BilletAbsence
from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
@ -89,12 +90,12 @@ def table_billets(
m = " matin"
else:
m = " après-midi"
billet_dict["abs_begin_str"] = billet.abs_begin.strftime("%d/%m/%Y") + m
billet_dict["abs_begin_str"] = billet.abs_begin.strftime(scu.DATE_FMT) + m
if billet.abs_end.hour < 12:
m = " matin"
else:
m = " après-midi"
billet_dict["abs_end_str"] = billet.abs_end.strftime("%d/%m/%Y") + m
billet_dict["abs_end_str"] = billet.abs_end.strftime(scu.DATE_FMT) + m
if billet.etat == 0:
if billet.justified:
billet_dict["etat_str"] = "à traiter"

View File

@ -515,11 +515,13 @@ class ApoEtud(dict):
# ne trouve pas de semestre impair
self.validation_annee_but = None
return
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first()
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first()
)
self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR
)
@ -1003,7 +1005,7 @@ def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
def nar_etuds_table(apo_data, nar_etuds):
"""Liste les NAR -> excel table"""
code_etape = apo_data.etape_apogee
today = datetime.datetime.today().strftime("%d/%m/%y")
today = datetime.datetime.today().strftime(scu.DATE_FMT)
rows = []
nar_etuds.sort(key=lambda k: k["nom"])
for e in nar_etuds:

View File

@ -62,7 +62,7 @@ from flask import g
import app.scodoc.sco_utils as scu
from config import Config
from app import log
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoException, ScoValueError
class BaseArchiver:

View File

@ -98,7 +98,7 @@ def do_formsemestre_archive(
archive_id = PV_ARCHIVER.create_obj_archive(
sem_archive_id, description, formsemestre.dept_id
)
date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
date = PV_ARCHIVER.get_archive_date(archive_id).strftime(scu.DATEATIME_FMT)
if not group_ids:
# tous les inscrits du semestre

View File

@ -126,7 +126,7 @@ def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
# ajoute date courante
t = time.localtime()
C["date_dmy"] = time.strftime("%d/%m/%Y", t)
C["date_dmy"] = time.strftime(scu.DATE_FMT, t)
C["date_iso"] = time.strftime("%Y-%m-%d", t)
return C

View File

@ -352,7 +352,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
H.append(
f"""<p>
<span class="bull_appreciations_date">{
appreciation.date.strftime("%d/%m/%y") if appreciation.date else ""
appreciation.date.strftime(scu.DATE_FMT) if appreciation.date else ""
}</span>
{appreciation.comment_safe()}
<span class="bull_appreciations_link">{mlink}</span>

View File

@ -182,7 +182,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
H.append(
f"""<p>
<span class="bull_appreciations_date">{
appreciation.date.strftime("%d/%m/%Y")
appreciation.date.strftime(scu.DATE_FMT)
if appreciation.date else ""}</span>
{appreciation.comment_safe()}
<span class="bull_appreciations_link">{mlink}</span>

View File

@ -55,7 +55,6 @@ from flask import g
import app
from app import db, log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException
@ -174,17 +173,15 @@ class EvaluationCache(ScoDocCache):
@classmethod
def invalidate_all_sems(cls):
"delete all evaluations in current dept from cache"
from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
evaluation_ids = [
x[0]
for x in ndb.SimpleQuery(
"""SELECT e.id
FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s
WHERE s.dept_id=%(dept_id)s
AND s.id = mi.formsemestre_id
AND mi.id = e.moduleimpl_id;
""",
{"dept_id": g.scodoc_dept_id},
)
e.id
for e in Evaluation.query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
]
cls.delete_many(evaluation_ids)

View File

@ -29,7 +29,6 @@
"""
import calendar
import datetime
import html
import time
@ -357,7 +356,7 @@ def MonthTableBody(
if not halfday:
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:
@ -432,7 +431,7 @@ def MonthTableBody(
# Calendar with 2 cells / day
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:

View File

@ -148,7 +148,7 @@ def _convert_formsemestres_to_dicts(
),
"formsemestre_id": formsemestre.id,
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
"lockimg": lockicon,
"lockimg": "" if formsemestre.etat else lockicon,
"modalite": formsemestre.modalite,
"mois_debut": formsemestre.mois_debut(),
"mois_fin": formsemestre.mois_fin(),

View File

@ -448,7 +448,7 @@ def module_edit(
(
"titre",
{
"size": 30,
"size": 64,
"explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""",
},
@ -456,8 +456,8 @@ def module_edit(
(
"abbrev",
{
"size": 20,
"explanation": """nom abrégé (pour bulletins).
"size": 32,
"explanation": """(optionnel) nom abrégé pour bulletins.
Exemple: <em>Intro. à l'ergonomie</em>""",
},
),

View File

@ -298,27 +298,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
cursus = formation.get_cursus()
is_apc = cursus.APC_SAE
semestres_indices = list(range(1, cursus.NB_SEM + 1))
H = [
html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"]),
"<h2>" + title,
f" (formation {formation.acronyme}, version {formation.version})</h2>",
"""
<p class="help">Les UE sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des
seuils ("barres")).
</p>
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients.
</p>""",
(
f"""
<h4>UE du semestre S{ue.semestre_idx}</h4>
"""
if is_apc and ue
else ""
),
]
ue_types = cursus.ALLOWED_UE_TYPES
ue_types.sort()
@ -489,7 +468,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
if ue and is_apc:
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
if ue and ue.modules.count() and ue.semestre_idx is not None:
modules_div = f"""<div id="ue_list_modules">
modules_div = f"""<div class="scobox" id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés
à cette UE</b> du semestre S{ue.semestre_idx},
elle ne peut donc pas être changée de semestre.</div>
@ -511,18 +490,34 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"""
else:
clone_form = ""
bonus_div = """<div id="bonus_description"></div>"""
ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
return (
"\n".join(H)
+ tf[1]
+ clone_form
+ ue_parcours_div
+ modules_div
+ bonus_div
+ ue_div
+ html_sco_header.sco_footer()
)
return f"""
{html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])}
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
<p class="help">Les UEs sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des
seuils ("barres")).
</p>
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients.
</p>
<div class="scobox">
<div class="scobox-title">
Édition de l'UE {('du semestre S'+str(ue.semestre_idx)) if is_apc and ue else ''}
</div>
{tf[1]}
</div>
{clone_form}
{ue_parcours_div}
{modules_div}
<div id="bonus_description"></div>
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
{html_sco_header.sco_footer()}
"""
elif tf[0] == 1:
if create:
if not tf[2]["ue_code"]:
@ -842,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
class="stdlink">
{formation.referentiel_competence.type_titre}
{formation.referentiel_competence.specialite_long}
{formation.referentiel_competence.get_title()}
</a>&nbsp;"""
msg_refcomp = "changer"
H.append(f"""<ul><li>{descr_refcomp}""")
@ -1170,14 +1164,17 @@ def _ue_table_ues(
if has_perm_change:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
url_for("notes.ue_set_internal",
scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">transformer en UE ordinaire</a>&nbsp;"""
)
H.append("</span>")
ue_editable = editable and not ue_is_locked(ue["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
f"""<a class="stdlink" href="{
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">modifier</a>"""
)
else:
H.append('<span class="locked">[verrouillé]</span>')

View File

@ -478,11 +478,11 @@ def convert_ics(
"heure_deb": event.decoded("dtstart")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
.strftime(scu.TIME_FMT),
"heure_fin": event.decoded("dtend")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
.strftime(scu.TIME_FMT),
"jour": event.decoded("dtstart").date().isoformat(),
"start": event.decoded("dtstart").isoformat(),
"end": event.decoded("dtend").isoformat(),

View File

@ -452,7 +452,7 @@ def table_apo_csv_list(semset):
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
t["nb_etuds"] = len(apo_data.etuds)
t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
t["date_str"] = t["date"].strftime(scu.DATEATIME_FMT)
view_link = url_for(
"notes.view_apo_csv",
scodoc_dept=g.scodoc_dept,

View File

@ -135,7 +135,7 @@ def evaluation_check_absences_html(
f"""<h2 class="eval_check_absences">{
evaluation.description or "évaluation"
} du {
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
evaluation.date_debut.strftime(scu.DATE_FMT) if evaluation.date_debut else ""
} """
]
if (

View File

@ -90,7 +90,7 @@ def evaluation_create_form(
raise ValueError("missing moduleimpl_id parameter")
numeros = [(e.numero or 0) for e in modimpl.evaluations]
initvalues = {
"jour": time.strftime("%d/%m/%Y", time.localtime()),
"jour": time.strftime(scu.DATE_FMT, time.localtime()),
"note_max": 20,
"numero": (max(numeros) + 1) if numeros else 0,
"publish_incomplete": is_malus,
@ -144,7 +144,7 @@ def evaluation_create_form(
if edit:
initvalues["blocked"] = evaluation.is_blocked()
initvalues["blocked_until"] = (
evaluation.blocked_until.strftime("%d/%m/%Y")
evaluation.blocked_until.strftime(scu.DATE_FMT)
if evaluation.blocked_until
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else ""
@ -231,7 +231,13 @@ def evaluation_create_form(
{
"input_type": "boolcheckbox",
"title": "Prise en compte immédiate",
"explanation": "notes utilisées même si incomplètes",
"explanation": """notes utilisées même si incomplètes (dangereux,
à n'utiliser que dans des cas particuliers
<a target="_blank" rel="noopener noreferrer"
href="https://scodoc.org/Evaluation/#pourquoi-eviter-dutiliser-prise-en-compte-immediate"
>voir la documentation</a>
)
""",
},
),
(
@ -399,7 +405,7 @@ def evaluation_create_form(
if args.get("blocked_until"):
try:
args["blocked_until"] = datetime.datetime.strptime(
args["blocked_until"], "%d/%m/%Y"
args["blocked_until"], scu.DATE_FMT
)
except ValueError as exc:
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc

View File

@ -126,13 +126,15 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
evaluation_id=evaluation_id,
),
"_titre_target_attrs": 'class="discretelink"',
"date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
"complete": "oui" if eval_etat.is_complete else "non",
"_complete_target": "#",
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
if eval_etat.is_complete
else 'class="bull_link incomplete" title="il manque des notes"',
"_complete_target_attrs": (
'class="bull_link" title="prise en compte dans les moyennes"'
if eval_etat.is_complete
else 'class="bull_link incomplete" title="il manque des notes"'
),
"manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]),
"inscrits": modimpl_results.nb_inscrits_module,
"nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE),

View File

@ -381,8 +381,10 @@ def formsemestre_evaluations_cal(formsemestre_id):
if e.date_debut == e.date_fin:
heure_debut_txt, heure_fin_txt = "?", "?"
else:
heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
heure_debut_txt = (
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "?"
)
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "?"
description = f"""{
e.moduleimpl.module.titre
@ -526,7 +528,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
"date_first_complete": date_first_complete,
"delai_correction": delai_correction,
"jour": (
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date"
e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "sans date"
),
"_jour_target": url_for(
"notes.evaluation_listenotes",

View File

@ -72,7 +72,7 @@ def xldate_as_datetime(xldate, datemode=0):
Peut lever une ValueError
"""
try:
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
return datetime.datetime.strptime(xldate, scu.DATE_FMT)
except:
return openpyxl.utils.datetime.from_ISO8601(xldate)

View File

@ -489,9 +489,10 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
return formation.id, modules_old2new, ues_old2new
def formation_list_table() -> GenTable:
def formation_list_table(detail: bool) -> GenTable:
"""List formation, grouped by titre and sorted by versions
and listing associated semestres
and listing associated semestres.
If detail, add column with more details.
returns a table
"""
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
@ -534,6 +535,15 @@ def formation_list_table() -> GenTable:
if formation.referentiel_competence
else ""
),
"_referentiel_target": (
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
if formation.referentiel_competence
else ""
),
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
@ -561,10 +571,15 @@ def formation_list_table() -> GenTable:
else []
)
)
# Répartition des UEs dans les semestres
# utilise pour voir si la formation couvre tous les semestres
row["semestres_ues"] = ", ".join(
"S" + str(x if (x is not None and x > 0) else "-")
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
)
# Date surtout utilisées pour le tri:
if row["formsemestres"]:
row["date_fin_dernier_sem"] = (
row["formsemestres"][-1].date_fin.isoformat(),
)
row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
else:
row["date_fin_dernier_sem"] = ""
@ -617,6 +632,8 @@ def formation_list_table() -> GenTable:
"commentaire",
"sems_list_txt",
)
if detail:
columns_ids += ("annee_dernier_sem", "semestres_ues")
titles = {
"buttons": "",
"commentaire": "Commentaire",
@ -627,6 +644,9 @@ def formation_list_table() -> GenTable:
"formation_code": "Code",
"sems_list_txt": "Semestres",
"referentiel": "Réf.",
"date_fin_dernier_sem": "Fin dernier sem.",
"annee_dernier_sem": "Année dernier sem.",
"semestres_ues": "Semestres avec UEs",
}
return GenTable(
columns_ids=columns_ids,
@ -639,7 +659,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url=f"{request.base_url}",
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),

View File

@ -521,7 +521,7 @@ def _record_ue_validations_and_coefs(
coef = _convert_field_to_float(coef)
if coef == "" or coef is False:
coef = None
now_dmy = time.strftime("%d/%m/%Y")
now_dmy = time.strftime(scu.DATE_FMT)
log(
f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"
)

View File

@ -106,7 +106,7 @@ def do_formsemestre_inscription_create(args, method=None):
cnx,
args={
"etudid": args["etudid"],
"event_date": time.strftime("%d/%m/%Y"),
"event_date": time.strftime(scu.DATE_FMT),
"formsemestre_id": args["formsemestre_id"],
"event_type": "INSCRIPTION",
},

View File

@ -64,14 +64,11 @@ from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus
from app.scodoc import sco_compute_moy
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
from app.scodoc.gen_tables import GenTable
@ -146,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
]
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
"""HTML to render menubar"""
if formsemestre is None:
return ""
formsemestre_id = formsemestre.id
if formsemestre.etat:
change_lock_msg = "Verrouiller"
@ -692,7 +691,7 @@ def formsemestre_description_table(
)
e["_date_evaluation_order"] = e["jour"].isoformat()
e["date_evaluation"] = (
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
e["jour"].strftime(scu.DATE_FMT) if e["jour"] else ""
)
e["UE"] = row["UE"]
e["_UE_td_attrs"] = row["_UE_td_attrs"]

View File

@ -1160,7 +1160,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
"input_type": "date",
"size": 9,
"explanation": "j/m/a",
"default": time.strftime("%d/%m/%Y"),
"default": time.strftime(scu.DATE_FMT),
},
),
(

View File

@ -453,6 +453,10 @@ class DisplayedGroupsInfos:
for i in to_remove:
del T[i]
def get_etudids(self) -> set[int]:
"Les etudids des groupes choisis"
return {member["etudid"] for member in self.members}
def get_form_elem(self):
"""html hidden input with groups"""
H = []

View File

@ -513,7 +513,7 @@ def _build_page(
<div>{scu.EMO_WARNING}
<em>Seuls les semestres dont la date de fin est proche de la date de début
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
de ce semestre ({formsemestre.date_debut.strftime(scu.DATE_FMT)}) sont pris en
compte.</em>
</div>
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
@ -704,7 +704,7 @@ def etuds_select_boxes(
elink += (
'<span class="finalisationinscription">'
+ " : inscription finalisée le "
+ etud["datefinalisationinscription"].strftime("%d/%m/%Y")
+ etud["datefinalisationinscription"].strftime(scu.DATE_FMT)
+ "</span>"
)

View File

@ -827,7 +827,7 @@ def _add_eval_columns(
nb_notes,
evaluation.description,
(
evaluation.date_debut.strftime("%d/%m/%Y")
evaluation.date_debut.strftime(scu.DATE_FMT)
if evaluation.date_debut
else ""
),

View File

@ -34,22 +34,28 @@ from operator import itemgetter
from flask import url_for, g, request
import app
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_report
from app.scodoc import sco_etud
import sco_version
from app.scodoc import (
html_sco_header,
sco_formsemestre,
sco_groups_view,
sco_preferences,
sco_report,
sco_etud,
)
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable
import app.scodoc.sco_utils as scu
import sco_version
def formsemestre_table_etuds_lycees(
formsemestre_id, group_lycees=True, only_primo=False
formsemestre: FormSemestre, groups_infos, group_lycees=True, only_primo=False
):
"""Récupère liste d'etudiants avec etat et decision."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre.id)
etuds = sco_report.tsp_etud_list(
formsemestre.id, groups_infos=groups_infos, only_primo=only_primo
)[0]
if only_primo:
primostr = "primo-entrants du "
else:
@ -59,7 +65,7 @@ def formsemestre_table_etuds_lycees(
etuds,
group_lycees,
title,
sco_preferences.SemPreferences(formsemestre_id),
sco_preferences.SemPreferences(formsemestre.id),
)
@ -180,13 +186,20 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
def formsemestre_etuds_lycees(
formsemestre_id,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
fmt="html",
only_primo=False,
no_grouping=False,
):
"""Table des lycées d'origine"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
formsemestre, groups_infos, only_primo=only_primo, group_lycees=not no_grouping
)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo:
@ -196,13 +209,19 @@ def formsemestre_etuds_lycees(
t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html":
return t
F = [sco_report.tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
F = [
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, fmt, groups_infos=groups_infos
)
]
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS
+ ["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
"\n".join(F),

View File

@ -50,8 +50,6 @@ from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
@ -431,7 +429,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
)
if info["ue_status"]["event_date"]:
H.append(
f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
f"""(cap. le {info["ue_status"]["event_date"].strftime(scu.DATE_FMT)})"""
)
if is_apc:
is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
@ -584,7 +582,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
validation = validations_ue[-1] if validations_ue else None
expl_validation = (
f"""Validée ({validation.code}) le {
validation.event_date.strftime("%d/%m/%Y")}"""
validation.event_date.strftime(scu.DATE_FMT)}"""
if validation
else ""
)

View File

@ -28,7 +28,6 @@
"""Tableau de bord module"""
import math
import time
import datetime
from flask import g, url_for
@ -48,13 +47,10 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cal
from app.scodoc import sco_compute_moy
from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.tables import list_etuds
@ -262,7 +258,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
# 2ieme ligne: Semestre, Coef
H.append("""<tr><td class="fichetitre2">""")
if formsemestre.semestre_id >= 0:
H.append("""Semestre: </td><td>%s""" % formsemestre.semestre_id)
H.append(f"""Semestre: </td><td>{formsemestre.semestre_id}""")
else:
H.append("""</td><td>""")
if sem_locked:
@ -290,7 +286,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
f"""<a class="stdlink" style="margin-left:2em;"
href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
)
H.append("</td></tr>")
# Ligne: règle de calcul
@ -614,7 +611,7 @@ def _ligne_evaluation(
if evaluation.is_blocked():
etat_txt = f"""évaluation bloquée {
"jusqu'au " + evaluation.blocked_until.strftime("%d/%m/%Y")
"jusqu'au " + evaluation.blocked_until.strftime(scu.DATE_FMT)
if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else "" }
"""
@ -657,7 +654,8 @@ def _ligne_evaluation(
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations moduleimpl_evaluation_moy" colspan="2"><span>{etat_txt}</span></th>
<th class="moduleimpl_evaluations moduleimpl_evaluation_moy"
colspan="2"><span>{etat_txt}</span></th>
</tr>
<tr class="{tr_class} mievr_in">
<td class="mievr">"""

View File

@ -730,7 +730,7 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
author = User.query.filter_by(user_name=annot.author).first()
html_annotations_list.append(
f"""<tr><td><span class="annodate">Le {
annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
par {author.get_prenomnom() if author else "?"} :
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
"""

View File

@ -335,7 +335,7 @@ class PlacementRunner:
def _production_pdf(self):
pdf_title = "<br>".join(self.desceval)
pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime("%d/%m/%Y")
pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime(scu.DATE_FMT)
if self.evaluation.date_debut else '-'
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
}"""
@ -485,7 +485,7 @@ class PlacementRunner:
worksheet.append_blank_row()
worksheet.append_single_cell_row(desceval, self.styles["titres"])
worksheet.append_single_cell_row(
f"""Date : {self.evaluation.date_debut.strftime("%d/%m/%Y")
f"""Date : {self.evaluation.date_debut.strftime(scu.DATE_FMT)
if self.evaluation.date_debut else '-'
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
}""",

View File

@ -517,7 +517,7 @@ def _normalize_apo_fields(infolist):
)
infos["datefinalisationinscription_str"] = infos[
"datefinalisationinscription"
].strftime("%d/%m/%Y")
].strftime(scu.DATE_FMT)
else:
infos["datefinalisationinscription"] = None
infos["datefinalisationinscription_str"] = ""

View File

@ -327,7 +327,7 @@ def feuille_preparation_jury(formsemestre_id):
"Préparé par %s le %s sur %s pour %s"
% (
sco_version.SCONAME,
time.strftime("%d/%m/%Y"),
time.strftime(scu.DATE_FMT),
request.url_root,
current_user,
)

View File

@ -40,6 +40,7 @@ from operator import itemgetter
from flask import url_for, g, request
import pydot
from app import log
from app.but import jury_but
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
@ -47,18 +48,21 @@ from app.models import FormSemestre, ScolarAutorisationInscription
from app.models import FormationModalite
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences
import sco_version
from app.scodoc import (
codes_cursus,
html_sco_header,
sco_etud,
sco_formsemestre,
sco_formsemestre_inscriptions,
sco_groups_view,
sco_preferences,
)
from app.scodoc.gen_tables import GenTable
from app import log
from app.scodoc.codes_cursus import code_semestre_validant
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu
import sco_version
MAX_ETUD_IN_DESCR = 20
@ -68,21 +72,25 @@ LEGENDES_CODES_BUT = {
}
def formsemestre_etuds_stats(sem: dict, only_primo=False):
def formsemestre_etuds_stats(
formsemestre: FormSemestre,
only_primo=False,
groups_infos: sco_groups_view.DisplayedGroupsInfos | None = None,
):
"""Récupère liste d'etudiants avec etat et decision."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
T = nt.get_table_moyennes_triees()
etudids = groups_infos.get_etudids() if groups_infos else set()
rows = nt.get_table_moyennes_triees()
# Décisions de jury BUT pour les semestres pairs seulement
jury_but_mode = (
formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0
)
# Construit liste d'étudiants du semestre avec leur decision
etuds = []
for t in T:
for t in rows:
etudid = t[-1]
if etudids and etudid not in etudids:
continue
etudiant = Identite.get_etud(etudid)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud["annee_admission"] = etud["annee"] # plus explicite
@ -96,7 +104,7 @@ def formsemestre_etuds_stats(sem: dict, only_primo=False):
etud["codedecision"] = "(nd)" # pas de decision jury
# Ajout devenir (autorisations inscriptions), utile pour stats passage
aut_list = ScolarAutorisationInscription.query.filter_by(
etudid=etudid, origin_formsemestre_id=sem["formsemestre_id"]
etudid=etudid, origin_formsemestre_id=formsemestre.id
).all()
autorisations = [f"S{a.semestre_id}" for a in aut_list]
autorisations.sort()
@ -115,27 +123,27 @@ def formsemestre_etuds_stats(sem: dict, only_primo=False):
bs.append(etud["specialite"])
etud["bac-specialite"] = " ".join(bs)
#
if (not only_primo) or is_primo_etud(etud, sem):
if (not only_primo) or is_primo_etud(etud, formsemestre):
etuds.append(etud)
return etuds
def is_primo_etud(etud: dict, sem: dict):
def is_primo_etud(etud: dict, formsemestre: FormSemestre):
"""Determine si un (filled) etud a été inscrit avant ce semestre.
Regarde la liste des semestres dans lesquels l'étudiant est inscrit.
Si semestre pair, considère comme primo-entrants ceux qui étaient
primo dans le précédent (S_{2n-1}).
"""
debut_cur = sem["date_debut_iso"]
debut_cur_iso = formsemestre.date_debut.isoformat()
# si semestre impair et sem. précédent contigu, recule date debut
if (
(len(etud["sems"]) > 1)
and (sem["semestre_id"] % 2 == 0)
and (etud["sems"][1]["semestre_id"] == (sem["semestre_id"] - 1))
and (formsemestre.semestre_id % 2 == 0)
and (etud["sems"][1]["semestre_id"] == (formsemestre.semestre_id - 1))
):
debut_cur = etud["sems"][1]["date_debut_iso"]
debut_cur_iso = etud["sems"][1]["date_debut_iso"]
for s in etud["sems"]: # le + recent d'abord
if s["date_debut_iso"] < debut_cur:
if s["date_debut_iso"] < debut_cur_iso:
return False
return True
@ -274,22 +282,6 @@ def formsemestre_report(
return tab
# def formsemestre_report_bacs(formsemestre_id, fmt='html'):
# """
# Tableau sur résultats par type de bac
# """
# sem = sco_formsemestre.get_formsemestre( formsemestre_id)
# title = 'Statistiques bacs ' + sem['titreannee']
# etuds = formsemestre_etuds_stats(sem)
# tab = formsemestre_report(formsemestre_id, etuds,
# category='bac', result='codedecision',
# category_name='Bac',
# title=title)
# return tab.make_page(
# title = """<h2>Résultats de <a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a></h2>""" % sem,
# fmt=fmt, page_title = title)
def formsemestre_report_counts(
formsemestre_id: int,
fmt="html",
@ -297,6 +289,7 @@ def formsemestre_report_counts(
result: str = None,
allkeys: bool = False,
only_primo: bool = False,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
):
"""
Tableau comptage avec choix des categories
@ -307,8 +300,12 @@ def formsemestre_report_counts(
si vrai, toutes les valeurs présentes dans les données
sinon liste prédéfinie (voir ci-dessous)
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
# Décisions de jury BUT pour les semestres pairs seulement
jury_but_mode = (
formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0
@ -319,7 +316,9 @@ def formsemestre_report_counts(
category_name = category.capitalize()
title = "Comptages " + category_name
etuds = formsemestre_etuds_stats(sem, only_primo=only_primo)
etuds = formsemestre_etuds_stats(
formsemestre, groups_infos=groups_infos, only_primo=only_primo
)
tab = formsemestre_report(
formsemestre_id,
etuds,
@ -329,7 +328,7 @@ def formsemestre_report_counts(
title=title,
only_primo=only_primo,
)
if not etuds:
if len(formsemestre.inscriptions) == 0:
F = ["""<p><em>Aucun étudiant</em></p>"""]
else:
if allkeys:
@ -357,9 +356,10 @@ def formsemestre_report_counts(
keys += ["nb_rcue_valides", "decision_annee"]
keys.sort(key=scu.heterogeneous_sorting_key)
F = [
"""<form name="f" method="get" action="%s"><p>
Colonnes: <select name="result" onchange="document.f.submit()">"""
% request.base_url
f"""<form id="group_selector" name="f" method="get" action="{request.base_url}">
Colonnes:
<select name="result" onchange="document.f.submit()">
"""
]
for k in keys:
if k == result:
@ -381,30 +381,38 @@ def formsemestre_report_counts(
'<option value="%s" %s>%s</option>'
% (k, selected, LEGENDES_CODES_BUT.get(k, k))
)
F.append("</select>")
if only_primo:
checked = 'checked="1"'
else:
checked = ""
F.append(
'<br><input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
% checked
f"""
</select>
<div style="margin-top:12px;">
<input type="checkbox" name="only_primo" onchange="document.f.submit()"
{'checked' if only_primo else ''}>
Restreindre aux primo-entrants</input>
<span style="margin: 12px;">
Restreindre au(x) groupe(s)&nbsp;:
{sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
if groups_infos else ''}
</span>
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
</div>
</form>
"""
)
F.append(
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
)
F.append("</p></form>")
t = tab.make_page(
title="""<h2 class="formsemestre">Comptes croisés</h2>""",
tableau = tab.make_page(
fmt=fmt,
title="""<h2 class="formsemestre">Comptes croisés</h2>""",
with_html_headers=False,
)
if fmt != "html":
return t
return tableau
H = [
html_sco_header.sco_header(page_title=title),
t,
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=title,
),
tableau,
"\n".join(F),
"""<p class="help">Le tableau affiche le nombre d'étudiants de ce semestre dans chacun
des cas choisis: à l'aide des deux menus, vous pouvez choisir les catégories utilisées
@ -418,7 +426,8 @@ def formsemestre_report_counts(
# --------------------------------------------------------------------------
def table_suivi_cohorte(
formsemestre_id,
formsemestre: FormSemestre,
groups_infos,
percent=False,
bac="", # selection sur type de bac
bacspecialite="",
@ -441,9 +450,8 @@ def table_suivi_cohorte(
Determination des dates: on regroupe les semestres commençant à des dates proches
"""
sem = sco_formsemestre.get_formsemestre(
formsemestre_id
) # sem est le semestre origine
sem = sco_formsemestre.get_formsemestre(formsemestre.id)
# sem est le semestre origine
t0 = time.time()
def logt(op):
@ -452,12 +460,12 @@ def table_suivi_cohorte(
logt("table_suivi_cohorte: start")
# 1-- Liste des semestres posterieurs dans lesquels ont été les etudiants de sem
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = nt.get_etudids()
etudids_inscrits = {ins.etudid for ins in formsemestre.inscriptions}
etudids_groups = groups_infos.get_etudids()
etudids = etudids_inscrits.intersection(etudids_groups)
logt("A: orig etuds set")
S = {formsemestre_id: sem} # ensemble de formsemestre_id
S = {formsemestre.id: sem} # ensemble de formsemestre_id
orig_set = set() # ensemble d'etudid du semestre d'origine
bacs = set()
bacspecialites = set()
@ -479,7 +487,7 @@ def table_suivi_cohorte(
)
and (not civilite or (civilite == etud["civilite"]))
and (not statut or (statut == etud["statut"]))
and (not only_primo or is_primo_etud(etud, sem))
and (not only_primo or is_primo_etud(etud, formsemestre))
):
orig_set.add(etudid)
# semestres suivants:
@ -524,17 +532,15 @@ def table_suivi_cohorte(
s["nb_dipl"] = nb_dipl
# 3-- Regroupe les semestres par date de debut
P = [] # liste de periodsem
class PeriodSem:
pass
def __init__(self, datedebut: datetime.datetime, sems: list[dict]):
self.datedebut = datedebut
self.sems = sems
# semestre de depart:
porigin = PeriodSem()
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
porigin.datedebut = datetime.datetime(y, m, d)
porigin.sems = [sem]
porigin = PeriodSem(datetime.datetime(y, m, d), [sem])
P = [] # liste de periodsem
#
tolerance = datetime.timedelta(days=45)
for s in sems:
@ -545,9 +551,7 @@ def table_suivi_cohorte(
merged = True
break
if not merged:
p = PeriodSem()
p.datedebut = s["date_debut_dt"]
p.sems = [s]
p = PeriodSem(s["date_debut_dt"], [s])
P.append(p)
# 4-- regroupe par indice de semestre S_i
@ -602,7 +606,7 @@ def table_suivi_cohorte(
L.append(d)
# Compte nb de démissions et de ré-orientation par période
logt("D: cout dems reos")
sem["dems"], sem["reos"] = _count_dem_reo(formsemestre_id, sem["members"])
sem["dems"], sem["reos"] = _count_dem_reo(formsemestre.id, sem["members"])
for p in P:
p.dems = set()
p.reos = set()
@ -666,8 +670,8 @@ def table_suivi_cohorte(
L.append(l)
columns_ids = [p.datedebut for p in P]
titles = dict([(p.datedebut, p.datedebut.strftime("%d/%m/%y")) for p in P])
titles[porigin.datedebut] = porigin.datedebut.strftime("%d/%m/%y")
titles = dict([(p.datedebut, p.datedebut.strftime(scu.DATE_FMT)) for p in P])
titles[porigin.datedebut] = porigin.datedebut.strftime(scu.DATE_FMT)
if percent:
pp = "(en % de la population initiale) "
titles["row_title"] = "%"
@ -703,7 +707,7 @@ def table_suivi_cohorte(
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
page_title="Suivi cohorte " + sem["titreannee"],
html_class="table_cohorte",
preferences=sco_preferences.SemPreferences(formsemestre_id),
preferences=sco_preferences.SemPreferences(formsemestre.id),
)
# Explication: liste des semestres associés à chaque date
if not P:
@ -713,13 +717,10 @@ def table_suivi_cohorte(
else:
expl = ["<h3>Semestres associés à chaque date:</h3><ul>"]
for p in P:
expl.append("<li><b>%s</b>:" % p.datedebut.strftime("%d/%m/%y"))
expl.append(f"""<li><b>{p.datedebut.strftime(scu.DATE_FMT)}</b>:""")
ls = []
for s in p.sems:
ls.append(
'<a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>'
% s
)
ls.append(formsemestre.html_link_status())
expl.append(", ".join(ls) + "</li>")
expl.append("</ul>")
return (
@ -737,6 +738,7 @@ def table_suivi_cohorte(
def formsemestre_suivi_cohorte(
formsemestre_id,
fmt="html",
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
percent=1,
bac="",
bacspecialite="",
@ -747,9 +749,19 @@ def formsemestre_suivi_cohorte(
only_primo=False,
) -> str:
"""Affiche suivi cohortes par numero de semestre"""
annee_bac = str(annee_bac or "")
annee_admission = str(annee_admission or "")
percent = int(percent)
try:
annee_bac = str(annee_bac or "")
annee_admission = str(annee_admission or "")
percent = int(percent)
except ValueError as exc:
raise ScoValueError("formsemestre_suivi_cohorte: argument invalide") from exc
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
(
tab,
expl,
@ -760,7 +772,8 @@ def formsemestre_suivi_cohorte(
civilites,
statuts,
) = table_suivi_cohorte(
formsemestre_id,
formsemestre,
groups_infos=groups_infos,
percent=percent,
bac=bac,
bacspecialite=bacspecialite,
@ -772,7 +785,7 @@ def formsemestre_suivi_cohorte(
)
tab.base_url = (
"%s?formsemestre_id=%s&percent=%s&bac=%s&bacspecialite=%s&civilite=%s"
% (request.base_url, formsemestre_id, percent, bac, bacspecialite, civilite)
% (request.base_url, formsemestre.id, percent, bac, bacspecialite, civilite)
)
if only_primo:
tab.base_url += "&only_primo=on"
@ -783,25 +796,29 @@ def formsemestre_suivi_cohorte(
base_url = request.base_url
burl = "%s?formsemestre_id=%s&bac=%s&bacspecialite=%s&civilite=%s&statut=%s" % (
base_url,
formsemestre_id,
formsemestre.id,
bac,
bacspecialite,
civilite,
statut,
)
if percent:
pplink = '<p><a href="%s&percent=0">Afficher les résultats bruts</a></p>' % burl
pplink = f"""<p><a class="stdlink"
href="{burl}&percent=0">Afficher les résultats bruts</a></p>"""
else:
pplink = (
'<p><a href="%s&percent=1">Afficher les résultats en pourcentages</a></p>'
% burl
)
pplink = f"""<p><a class="stdlink"
href="{burl}&percent=1">Afficher les résultats en pourcentages</a></p>"""
H = [
html_sco_header.sco_header(page_title=tab.page_title),
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=tab.page_title,
),
"""<h2 class="formsemestre">Suivi cohorte: devenir des étudiants de ce semestre</h2>""",
_gen_form_selectetuds(
formsemestre_id,
formsemestre.id,
groups_infos=groups_infos,
only_primo=only_primo,
bac=bac,
bacspecialite=bacspecialite,
@ -854,6 +871,7 @@ def _gen_form_selectetuds(
annee_admissions=None,
civilites=None,
statuts=None,
groups_infos: sco_groups_view.DisplayedGroupsInfos = None,
):
"""HTML form pour choix criteres selection etudiants"""
annee_bacs = annee_bacs or []
@ -877,9 +895,10 @@ def _gen_form_selectetuds(
else:
selected = 'selected="selected"'
F = [
f"""<form id="f" method="get" action="{request.base_url}">
<p>Bac: <select name="bac" onchange="javascript: submit(this);">
<option value="" {selected}>tous</option>
f"""<form id="group_selector" name="f" method="get" action="{request.base_url}">
<div>Bac:
<select name="bac" onchange="javascript: submit(this);">
<option value="" {selected}>tous</option>
"""
]
for b in bacs:
@ -894,7 +913,8 @@ def _gen_form_selectetuds(
else:
selected = 'selected="selected"'
F.append(
f"""&nbsp; Bac/Specialité: <select name="bacspecialite" onchange="javascript: submit(this);">
f"""&nbsp; Bac/Specialité:
<select name="bacspecialite" onchange="javascript: submit(this);">
<option value="" {selected}>tous</option>
"""
)
@ -938,17 +958,24 @@ def _gen_form_selectetuds(
else:
selected = ""
F.append(f'<option value="{b}" {selected}>{b}</option>')
F.append("</select>")
F.append(
f"""<br>
<input type="checkbox" name="only_primo"
onchange="javascript: submit(this);"
{'checked="1"' if only_primo else ""}/>Restreindre aux primo-entrants
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
<input type="hidden" name="percent" value="{percent}"/>
f"""
</select>
<div style="margin-top:12px;">
<input type="checkbox" name="only_primo"
onchange="javascript: submit(this);"
{'checked="1"' if only_primo else ""}/>Restreindre aux primo-entrants
</p>
<span style="margin: 12px;">
Restreindre au(x) groupe(s)&nbsp;:
{sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
if groups_infos else ''}
</span>
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
<input type="hidden" name="percent" value="{percent}"/>
</div>
</div>
</form>
"""
)
@ -1002,17 +1029,6 @@ def _count_dem_reo(formsemestre_id, etudids):
return dems, reos
"""OLDGEA:
27s pour S1 F.I. classique Semestre 1 2006-2007
B 2.3s
C 5.6s
D 5.9s
Z 27s => cache des semestres pour nt
à chaud: 3s
B: etuds sets: 2.4s => lent: N x getEtudInfo (non caché)
"""
EXP_LIC = re.compile(r"licence", re.I)
EXP_LPRO = re.compile(r"professionnelle", re.I)
@ -1125,6 +1141,7 @@ def get_code_cursus_etud(
def tsp_etud_list(
formsemestre_id,
groups_infos: sco_groups_view.DisplayedGroupsInfos | None = None,
only_primo=False,
bac="", # selection sur type de bac
bacspecialite="",
@ -1136,11 +1153,13 @@ def tsp_etud_list(
"""Liste des etuds a considerer dans table suivi cursus
ramene aussi ensembles des bacs, genres, statuts de (tous) les etudiants
"""
# log('tsp_etud_list(%s, bac="%s")' % (formsemestre_id,bac))
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = nt.get_etudids()
etudids_inscrits = {ins.etudid for ins in formsemestre.inscriptions}
if groups_infos:
etudids_groups = groups_infos.get_etudids()
etudids = etudids_inscrits.intersection(etudids_groups)
else:
etudids = etudids_inscrits
etuds = []
bacs = set()
bacspecialites = set()
@ -1162,7 +1181,7 @@ def tsp_etud_list(
)
and (not civilite or (civilite == etud["civilite"]))
and (not statut or (statut == etud["statut"]))
and (not only_primo or is_primo_etud(etud, sem))
and (not only_primo or is_primo_etud(etud, formsemestre))
):
etuds.append(etud)
@ -1173,7 +1192,6 @@ def tsp_etud_list(
civilites.add(etud["civilite"])
if etud["statut"]: # ne montre pas les statuts non renseignés
statuts.add(etud["statut"])
# log('tsp_etud_list: %s etuds' % len(etuds))
return etuds, bacs, bacspecialites, annee_bacs, annee_admissions, civilites, statuts
@ -1290,31 +1308,30 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
return tab
def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt):
"""Element de formulaire pour choisir si restriction aux primos entrants et groupement par lycees"""
F = ["""<form name="f" method="get" action="%s">""" % request.base_url]
if only_primo:
checked = 'checked="1"'
else:
checked = ""
F.append(
'<input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
% checked
)
if no_grouping:
checked = 'checked="1"'
else:
checked = ""
F.append(
'<input type="checkbox" name="no_grouping" onchange="document.f.submit()" %s>Lister chaque étudiant</input>'
% checked
)
F.append(
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
)
F.append('<input type="hidden" name="fmt" value="%s"/>' % fmt)
F.append("""</form>""")
return "\n".join(F)
def tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, fmt, groups_infos=None
) -> str:
"""Element de formulaire pour choisir si restriction aux primos entrants,
groupement par lycees et groupes
"""
primo_checked = 'checked="1"' if only_primo else ""
no_grouping_checked = 'checked="1"' if no_grouping else ""
return f"""
<form id="group_selector" name="f" method="get" action="{request.base_url}">
<input type="checkbox" name="only_primo"
onchange="document.f.submit()" {primo_checked}>Restreindre aux primo-entrants</input>
<input type="checkbox" name="no_grouping" onchange="document.f.submit()"
{no_grouping_checked}>Lister chaque étudiant</input>
<span style="margin: 12px;">
Restreindre au(x) groupe(s)&nbsp;:
{sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
if groups_infos else ''}
</span>
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
<input type="hidden" name="fmt" value="{fmt}"/>
</form>
"""
def formsemestre_suivi_cursus(
@ -1337,7 +1354,11 @@ def formsemestre_suivi_cursus(
t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html":
return t
F = [tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
F = [
tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, fmt, groups_infos=None
)
]
H = [
html_sco_header.sco_header(
@ -1356,6 +1377,7 @@ def formsemestre_suivi_cursus(
# -------------
def graph_cursus(
formsemestre_id,
groups_infos: sco_groups_view.DisplayedGroupsInfos | None = None,
fmt="svg",
only_primo=False,
bac="", # selection sur type de bac
@ -1376,6 +1398,7 @@ def graph_cursus(
statuts,
) = tsp_etud_list(
formsemestre_id,
groups_infos=groups_infos,
only_primo=only_primo,
bac=bac,
bacspecialite=bacspecialite,
@ -1512,6 +1535,9 @@ def graph_cursus(
# semestre de depart en vert
n = g.get_node("SEM" + str(formsemestre_id))[0]
n.set_color("green")
n.set_style("filled")
n.set_fillcolor("lightgreen")
n.set_penwidth(2.0)
# demissions en rouge, octagonal
for nid in dem_nodes.values():
n = g.get_node(nid)[0]
@ -1606,6 +1632,7 @@ def graph_cursus(
def formsemestre_graph_cursus(
formsemestre_id,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
fmt="html",
only_primo=False,
bac="", # selection sur type de bac
@ -1619,7 +1646,12 @@ def formsemestre_graph_cursus(
"""Graphe suivi cohortes"""
annee_bac = str(annee_bac or "")
annee_admission = str(annee_admission or "")
# log("formsemestre_graph_cursus")
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if fmt == "pdf":
(
@ -1633,6 +1665,7 @@ def formsemestre_graph_cursus(
statuts,
) = graph_cursus(
formsemestre_id,
groups_infos=groups_infos,
fmt="pdf",
only_primo=only_primo,
bac=bac,
@ -1657,6 +1690,7 @@ def formsemestre_graph_cursus(
statuts,
) = graph_cursus(
formsemestre_id,
groups_infos=groups_infos,
fmt="png",
only_primo=only_primo,
bac=bac,
@ -1695,6 +1729,7 @@ def formsemestre_graph_cursus(
statuts,
) = graph_cursus(
formsemestre_id,
groups_infos=groups_infos,
only_primo=only_primo,
bac=bac,
bacspecialite=bacspecialite,
@ -1706,14 +1741,17 @@ def formsemestre_graph_cursus(
H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title="Graphe cursus de %(titreannee)s" % sem,
no_side_bar=True,
),
"""<h2 class="formsemestre">Cursus des étudiants de ce semestre</h2>""",
doc,
"<p>%d étudiants sélectionnés</p>" % len(etuds),
f"<p>{len(etuds)} étudiants sélectionnés</p>",
_gen_form_selectetuds(
formsemestre_id,
groups_infos=groups_infos,
only_primo=only_primo,
bac=bac,
bacspecialite=bacspecialite,
@ -1743,6 +1781,10 @@ def formsemestre_graph_cursus(
passant d'un semestre à l'autre (s'il y en a moins de {MAX_ETUD_IN_DESCR}, vous
pouvez visualiser leurs noms en passant le curseur sur le chiffre).
</p>
<p class="help">
Le menu <em>Restreindre au(x) groupe(s)</em> permet de restreindre l'étude aux
étudiants appartenant aux groupes indiqués <em>dans le semestre d'origine</em>.
</p>
""",
html_sco_header.sco_footer(),
]

View File

@ -892,7 +892,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
date_str = (
f"""du {evaluation.date_debut.strftime("%d/%m/%Y")}"""
f"""du {evaluation.date_debut.strftime(scu.DATE_FMT)}"""
if evaluation.date_debut
else "(sans date)"
)

View File

@ -250,7 +250,6 @@ def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
"Copy photos from portal to ScoDoc (overwriting local copy)"
group_ids = [] if group_ids is None else group_ids
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
breakpoint()
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
portal_url = sco_portal_apogee.get_portal_url()

View File

@ -117,7 +117,7 @@ def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime:
ScoValueError si date invalide.
"""
try:
return datetime.datetime.strptime(date_str, "%d/%m/%Y")
return datetime.datetime.strptime(date_str, DATE_FMT)
except ValueError:
# Try to add century ?
m = re.match(r"^(\d{1,2})/(\d{1,2})/(\d\d)$", date_str)
@ -129,7 +129,7 @@ def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime:
year += 1900
try:
return datetime.datetime.strptime(
f"{m.group(1)}/{m.group(2)}/{year}", "%d/%m/%Y"
f"{m.group(1)}/{m.group(2)}/{year}", DATE_FMT
)
except ValueError:
pass
@ -550,6 +550,11 @@ MONTH_NAMES = (
)
DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
TIME_FMT = "%H:%M" # affichage des heures
DATE_FMT = "%d/%m/%Y" # affichage des dates
DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
DATETIME_FMT = DATE_FMT + " " + TIME_FMT
def fmt_note(val, note_max=None, keep_numeric=False):
"""conversion note en str pour affichage dans tables HTML ou PDF.
@ -1067,9 +1072,9 @@ def bul_filename(formsemestre, etud, prefix="bul"):
def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
for field, _ in form.errors.items():
flash(
"Erreur: voir le champ %s" % (getattr(form, field).label.text,),
f"Erreur: voir le champ {getattr(form, field).label.text}",
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/

View File

@ -141,111 +141,111 @@ table.dataTable.with-highlight tr:hover td {
background-color: rgba(255, 255, 0, 0.415);
}
table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr > .sorting_3,
table.dataTable.display tbody tr > .sorting_1,
table.dataTable.display tbody tr > .sorting_2,
table.dataTable.display tbody tr > .sorting_3 {
table.dataTable.order-column tbody tr>.sorting_1,
table.dataTable.order-column tbody tr>.sorting_2,
table.dataTable.order-column tbody tr>.sorting_3,
table.dataTable.display tbody tr>.sorting_1,
table.dataTable.display tbody tr>.sorting_2,
table.dataTable.display tbody tr>.sorting_3 {
background-color: #f9f9f9;
}
table.dataTable.order-column tbody tr.selected > .sorting_1,
table.dataTable.order-column tbody tr.selected > .sorting_2,
table.dataTable.order-column tbody tr.selected > .sorting_3,
table.dataTable.display tbody tr.selected > .sorting_1,
table.dataTable.display tbody tr.selected > .sorting_2,
table.dataTable.display tbody tr.selected > .sorting_3 {
table.dataTable.order-column tbody tr.selected>.sorting_1,
table.dataTable.order-column tbody tr.selected>.sorting_2,
table.dataTable.order-column tbody tr.selected>.sorting_3,
table.dataTable.display tbody tr.selected>.sorting_1,
table.dataTable.display tbody tr.selected>.sorting_2,
table.dataTable.display tbody tr.selected>.sorting_3 {
background-color: #acbad4;
}
table.dataTable.display tbody tr.odd > .sorting_1,
table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
table.dataTable.display tbody tr.odd>.sorting_1,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_1 {
background-color: #f1f1f1;
}
table.dataTable.display tbody tr.odd > .sorting_2,
table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
table.dataTable.display tbody tr.odd>.sorting_2,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_2 {
background-color: #f3f3f3;
}
table.dataTable.display tbody tr.odd > .sorting_3,
table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
table.dataTable.display tbody tr.odd>.sorting_3,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_3 {
background-color: whitesmoke;
}
table.dataTable.display tbody tr.odd.selected > .sorting_1,
table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
table.dataTable.display tbody tr.odd.selected>.sorting_1,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1 {
background-color: #a6b3cd;
}
table.dataTable.display tbody tr.odd.selected > .sorting_2,
table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
table.dataTable.display tbody tr.odd.selected>.sorting_2,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2 {
background-color: #a7b5ce;
}
table.dataTable.display tbody tr.odd.selected > .sorting_3,
table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
table.dataTable.display tbody tr.odd.selected>.sorting_3,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3 {
background-color: #a9b6d0;
}
table.dataTable.display tbody tr.even > .sorting_1,
table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
table.dataTable.display tbody tr.even>.sorting_1,
table.dataTable.order-column.stripe tbody tr.even>.sorting_1 {
background-color: #f9f9f9;
}
table.dataTable.display tbody tr.even > .sorting_2,
table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
table.dataTable.display tbody tr.even>.sorting_2,
table.dataTable.order-column.stripe tbody tr.even>.sorting_2 {
background-color: #fbfbfb;
}
table.dataTable.display tbody tr.even > .sorting_3,
table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
table.dataTable.display tbody tr.even>.sorting_3,
table.dataTable.order-column.stripe tbody tr.even>.sorting_3 {
background-color: #fdfdfd;
}
table.dataTable.display tbody tr.even.selected > .sorting_1,
table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
table.dataTable.display tbody tr.even.selected>.sorting_1,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1 {
background-color: #acbad4;
}
table.dataTable.display tbody tr.even.selected > .sorting_2,
table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
table.dataTable.display tbody tr.even.selected>.sorting_2,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2 {
background-color: #adbbd6;
}
table.dataTable.display tbody tr.even.selected > .sorting_3,
table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
table.dataTable.display tbody tr.even.selected>.sorting_3,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3 {
background-color: #afbdd8;
}
table.dataTable.display tbody tr:hover > .sorting_1,
table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
table.dataTable.display tbody tr:hover>.sorting_1,
table.dataTable.order-column.hover tbody tr:hover>.sorting_1 {
background-color: #eaeaea;
}
table.dataTable.display tbody tr:hover > .sorting_2,
table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
table.dataTable.display tbody tr:hover>.sorting_2,
table.dataTable.order-column.hover tbody tr:hover>.sorting_2 {
background-color: #ebebeb;
}
table.dataTable.display tbody tr:hover > .sorting_3,
table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
table.dataTable.display tbody tr:hover>.sorting_3,
table.dataTable.order-column.hover tbody tr:hover>.sorting_3 {
background-color: #eeeeee;
}
table.dataTable.display tbody tr:hover.selected > .sorting_1,
table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
table.dataTable.display tbody tr:hover.selected>.sorting_1,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1 {
background-color: #a1aec7;
}
table.dataTable.display tbody tr:hover.selected > .sorting_2,
table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
table.dataTable.display tbody tr:hover.selected>.sorting_2,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2 {
background-color: #a2afc8;
}
table.dataTable.display tbody tr:hover.selected > .sorting_3,
table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
table.dataTable.display tbody tr:hover.selected>.sorting_3,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3 {
background-color: #a4b2cb;
}
@ -420,13 +420,11 @@ table.dataTable td {
color: #333333 !important;
border: 1px solid #979797;
background-color: white;
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, white),
color-stop(100%, gainsboro)
);
background: -webkit-gradient(linear,
left top,
left bottom,
color-stop(0%, white),
color-stop(100%, gainsboro));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, gainsboro 100%);
/* Chrome10+,Safari5.1+ */
@ -454,13 +452,11 @@ table.dataTable td {
color: white !important;
border: 1px solid #111111;
background-color: #585858;
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #585858),
color-stop(100%, #111111)
);
background: -webkit-gradient(linear,
left top,
left bottom,
color-stop(0%, #585858),
color-stop(100%, #111111));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111111 100%);
/* Chrome10+,Safari5.1+ */
@ -477,13 +473,11 @@ table.dataTable td {
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none;
background-color: #2b2b2b;
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #2b2b2b),
color-stop(100%, #0c0c0c)
);
background: -webkit-gradient(linear,
left top,
left bottom,
color-stop(0%, #2b2b2b),
color-stop(100%, #0c0c0c));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */
@ -514,50 +508,38 @@ table.dataTable td {
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(
linear,
left top,
right top,
color-stop(0%, rgba(255, 255, 255, 0)),
color-stop(25%, rgba(255, 255, 255, 0.9)),
color-stop(75%, rgba(255, 255, 255, 0.9)),
color-stop(100%, rgba(255, 255, 255, 0))
);
background: -webkit-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -moz-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -ms-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -o-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -webkit-gradient(linear,
left top,
right top,
color-stop(0%, rgba(255, 255, 255, 0)),
color-stop(25%, rgba(255, 255, 255, 0.9)),
color-stop(75%, rgba(255, 255, 255, 0.9)),
color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%);
background: -moz-linear-gradient(left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%);
}
.dataTables_wrapper .dataTables_length,
@ -577,69 +559,17 @@ table.dataTable td {
-webkit-overflow-scrolling: touch;
}
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td {
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td {
vertical-align: middle;
}
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td
> div.dataTables_sizing {
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing {
height: 0;
overflow: hidden;
margin: 0 !important;
@ -650,8 +580,8 @@ table.dataTable td {
border-bottom: 1px solid #111111;
}
.dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
.dataTables_wrapper.no-footer div.dataTables_scrollHead>table,
.dataTables_wrapper.no-footer div.dataTables_scrollBody>table {
border-bottom: none;
}
@ -664,6 +594,7 @@ table.dataTable td {
}
@media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate {
float: none;
@ -676,6 +607,7 @@ table.dataTable td {
}
@media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
@ -703,6 +635,10 @@ table.table_leftalign tr td {
text-align: left;
}
p.gt_caption {
margin-top: 8px;
}
/* Ligne(s) de titre */
table.dataTable thead tr th {
background-color: rgb(90%, 90%, 90%);
@ -757,7 +693,8 @@ table.dataTable.gt_table {
table.dataTable.gt_table.gt_left {
margin-left: 16px;
}
table.dataTable.gt_table.gt_left td,
table.dataTable.gt_table.gt_left th {
text-align: left;
}
}

View File

@ -1188,10 +1188,11 @@ a.discretelink:hover {
.help {
max-width: var(--sco-content-max-width);
font-style: italic;
}
.help {
font-style: italic;
.help em {
font-style: normal;
}
.help_important {
@ -2410,10 +2411,10 @@ li.notes_formation_list {
padding-top: 10px;
}
table.formation_list_table {
width: 100%;
table.dataTable.formation_list_table.gt_table {
border-collapse: collapse;
background-color: rgb(0%, 90%, 90%);
margin-right: 12px;
margin-left: 12px;
}
table#formation_list_table tr.gt_hl {
@ -2454,8 +2455,8 @@ table.formation_list_table td.buttons span.but_placeholder {
text-align: center;
}
.formation_list_table td.titre {
width: 45%;
.formation_list_table td.sems_list_txt {
width: 15%;
}
.formation_list_table td.commentaire {

View File

@ -23,7 +23,7 @@ function groups_view_url() {
url.param()["formsemestre_id"] =
$("#group_selector")[0].formsemestre_id.value;
var selected_groups = $("#group_selector select").val();
var selected_groups = $("#group_selector select#group_ids_sel").val();
url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes
return url;

View File

@ -13,6 +13,8 @@ from sqlalchemy import desc, literal, union, asc
from app import db, g
from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif, Module
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
@ -401,8 +403,8 @@ class RowAssiJusti(tb.Row):
]
if multi_days and self.ligne["type"] != "justificatif":
date_affichees[0] = self.ligne["date_debut"].strftime("%d/%m/%y")
date_affichees[1] = self.ligne["date_fin"].strftime("%d/%m/%y")
date_affichees[0] = self.ligne["date_debut"].strftime(scu.DATE_FMT)
date_affichees[1] = self.ligne["date_fin"].strftime(scu.DATE_FMT)
self.add_cell(
"date_debut",
@ -443,7 +445,7 @@ class RowAssiJusti(tb.Row):
"entry_date",
"Saisie le",
(
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M")
self.ligne["entry_date"].strftime(scu.DATEATIME_FMT)
if self.ligne["entry_date"]
else "?"
),

View File

@ -386,7 +386,7 @@ class TableRecap(tb.Table):
for e in evals:
col_id = f"eval_{e.id}"
title = f"""{modimpl.module.code} {eval_index} {
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else ""
e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else ""
}"""
col_classes = []
if first_eval:

View File

@ -48,7 +48,7 @@ div.submit > input {
{% if justif %}
<div class="info-saisie">
Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}}
le {{justif.entry_date.strftime("%d/%m/%Y à %H:%M") if justif.entry_date else "?"}}
le {{justif.entry_date.strftime(scu.DATEATIME_FMT) if justif.entry_date else "?"}}
</div>
{% endif %}

View File

@ -10,8 +10,8 @@
{% block app_content %}
<div class="tab-content">
<h2>Présence du groupe {{group_title}} le {{date_debut.strftime("%d/%m/%Y")}}
de {{date_debut.strftime("%H:%M")}} à {{date_fin.strftime("%H:%M")}}
<h2>Présence du groupe {{group_title}} le {{date_debut.strftime(scu.DATE_FMT)}}
de {{date_debut.strftime(scu.TIME_FMT)}} à {{date_fin.strftime(scu.TIME_FMT)}}
</h2>
<div>

View File

@ -326,15 +326,15 @@ main();
<div class="plage">
{% if ligne.justif.date_debut.date() == ligne.justif.date_fin.date() %}
<span class="date">
{{ligne.justif.date_debut.strftime("%d/%m/%y")}} de {{ligne.justif.date_debut.strftime("%Hh%M")}} à
{{ligne.justif.date_fin.strftime("%Hh%M")}}
{{ligne.justif.date_debut.strftime(scu.DATE_FMT)}} de {{ligne.justif.date_debut.strftime(scu.TIME_FMT)}} à
{{ligne.justif.date_fin.strftime(scu.TIME_FMT)}}
</span>
{% else %}
<span class="date_debut">
du {{ligne.justif.date_debut.strftime("%d/%m/%y")}}
du {{ligne.justif.date_debut.strftime(scu.DATE_FMT)}}
</span>
<span class="date_fin">
au {{ligne.justif.date_fin.strftime("%d/%m/%y")}}
au {{ligne.justif.date_fin.strftime(scu.DATE_FMT)}}
</span>
{% endif %}
<span class="entry_date hint">
@ -352,8 +352,8 @@ main();
<li>
{{scu.EtatAssiduite(assi.etat).version_lisible()}}
{% if assi.date_debut.date() == assi.date_fin.date() %}
du {{assi.date_debut.strftime("%d/%m/%y")}} de {{assi.date_debut.strftime("%Hh%M")}} à
{{assi.date_fin.strftime("%Hh%M")}}
du {{assi.date_debut.strftime(scu.DATE_FMT)}} de {{assi.date_debut.strftime(scu.TIME_FMT)}} à
{{assi.date_fin.strftime(scu.TIME_FMT)}}
{% else %}
du {{assi.date_debut.strftime("%d/%m/%y %Hh%M")}} au {{assi.date_fin.strftime("%d/%m/%y
%Hh%M")}}

View File

@ -30,15 +30,15 @@
</div>
{% if current_user.is_administrator() %}
<div class="user_info_admin">
<b>Dernière vue :</b> {{user.last_seen.strftime("%d/%m/%Y à %H:%M") if user.last_seen else "-"}}<br>
<b>Dernière connexion CAS :</b> {{user.cas_last_login.strftime("%d/%m/%Y à %H:%M") if user.cas_last_login else "-"}}<br>
<b>Dernière vue :</b> {{user.last_seen.strftime(scu.DATEATIME_FMT) if user.last_seen else "-"}}<br>
<b>Dernière connexion CAS :</b> {{user.cas_last_login.strftime(scu.DATEATIME_FMT) if user.cas_last_login else "-"}}<br>
</div>
{% endif %}
<div class="user_basics">
<b>Dernière modif mot de passe:</b>
{{user.date_modif_passwd.strftime("%d/%m/%Y") if user.date_modif_passwd else ""}}<br>
{{user.date_modif_passwd.strftime(scu.DATE_FMT) if user.date_modif_passwd else ""}}<br>
<b>Date d'expiration:</b>
{{user.date_expiration.strftime("%d/%m/%Y") if user.date_expiration else "(sans limite)"}}
{{user.date_expiration.strftime(scu.DATE_FMT) if user.date_expiration else "(sans limite)"}}
</div>
<div>

View File

@ -1,6 +1,4 @@
{# Base de toutes les pages ScoDoc #}
{% block doc -%}
<!DOCTYPE html>
{%- block doc -%}<!DOCTYPE html>{# Base de toutes les pages ScoDoc #}
<html{% block html_attribs %}{% endblock html_attribs %}>
{%- block html %}
<head>

View File

@ -11,9 +11,9 @@
)}}">{{etud.nomprenom}}</a></h2>
{% endif %}
<form name="f" method="GET" action="{{request.base_url}}">
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input>
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
<input type="hidden" name="fmt" value="{{fmt}}"></input>
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}">
<input type="hidden" name="etudid" value="{{etud.id}}">
<input type="hidden" name="fmt" value="{{fmt}}">
<div class="bull_titre_semestre">
Bulletin
<span class="bull_liensemestre">
@ -36,7 +36,7 @@
formsemestre_id=formsemestre.id,
etudid=etud.id,
)
}}&version='+this.value;"" class="noprint">
}}&version='+this.value;" class="noprint">
{% if formsemestre.formation.is_apc() %}
{% set menu_items = scu.BULLETINS_VERSIONS_BUT.items() %}
{% else %}

View File

@ -134,7 +134,7 @@
<div class="descr_jury">
{% if bul.semestre.decision_annee %}
Décision saisie le {{
datetime.datetime.fromisoformat(bul.semestre.decision_annee.date).strftime("%d/%m/%Y")
datetime.datetime.fromisoformat(bul.semestre.decision_annee.date).strftime(scu.DATE_FMT)
}},
année <b>BUT{{bul.semestre.decision_annee.ordre}}</b>
<b>{{bul.semestre.decision_annee.code}}</b>.

View File

@ -0,0 +1,58 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>Changer le référentiel de compétences de la formation
{{formation.get_titre_version()}}
</h1>
<div class="help">
<p> Normalement, il n'est pas possible de changer une
formation de référentiel de compétences. En effet, de nombreux éléments sont
déduis du référentiel: les parcours, les compétences, les apprentissages
critiques, et donc les validations de jury des étudiants qui ont suivi la
formation.
</p>
<p> Cependant, dans certains cas, le ministère a publié une nouvelle version
d'un référentiel de spécialité, mais les changements ne sont que très légers:
détails des noms de parcours ou des compétences. Dans ces cas là, la structure
étant la même, il est autorisé de changer le référentiel.
</p>
<p>Seuls les référentiels déjà chargés et compatibles (ayant la même structure)
sont proposés dans le menu ci-dessous.
</p>
<p class="fontred">
Attention: tout changement de référentiel entraine la perte des associations
entre les modules et les apprentissages critiques.
</p>
<p class="fontred">
Attention: <b>fonction expérimentale</b>. Vérifiez vos sauvegardes.
</p>
</div>
<div>La formation est actuellement associée au référentiel
<a class="stdlink" href="{{
url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id)
}}">{{formation.referentiel_competence.get_title()}}</a>
</div>
<form method="POST" style="margin-top: 24px;">
{{ form.hidden_tag() }}
<div>
{{ form.object_select.label }}<br>
{{ form.object_select }}
</div>
<div style="margin-top: 24px;">{{ form.submit() }} {{ form.cancel() }}</div>
</form>
{% endblock %}

View File

@ -18,7 +18,7 @@
<div>{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}</div>
<div>Jury de {{validation.formsemestre.titre_annee() if validation.formsemestre else "-"}}</div>
<div>enregistré le {{
validation.date.strftime("%d/%m/%Y à %H:%M")
validation.date.strftime(scu.DATEATIME_FMT)
}}</div>
</div>
</div>

View File

@ -9,7 +9,7 @@
<h2>Référentiel de compétences {{ref.type_titre}} {{ref.specialite_long}}</h2>
<div class="help">
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à
Référentiel chargé le {{ref.scodoc_date_loaded.strftime(scu.DATEATIME_FMT) if ref.scodoc_date_loaded else ""}} à
partir du fichier <tt>{{ref.scodoc_orig_filename or "(inconnu)"}}</tt>.
</div>
@ -27,9 +27,18 @@
<li>Formations se référant à ce référentiel:
<ul>
{% for formation in ref.formations %}
<li><a class="stdlink" href="{{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{ formation.get_titre_version() }}</a></li>
<li>
<a class="stdlink" href="{{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{ formation.get_titre_version() }}</a>
{% if referentiels_equivalents %}
<a style="margin-left: 8px;" class="stdlink" href="
{{ url_for('notes.formation_change_refcomp',
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}
">(changer)</a>
{% endif %}
</li>
{% else %}
<li><em>aucune</em></li>
{% endfor %}

View File

@ -26,7 +26,7 @@
<div>Jury de {{validation.formsemestre.titre_annee()
if validation.formsemestre else "-"}}</div>
<div>enregistrée le {{
validation.event_date.strftime("%d/%m/%Y à %H:%M")
validation.event_date.strftime(scu.DATEATIME_FMT)
if validation.event_date else "-"
}}</div>
<div>{{"%g"|format(validation.ects())}} ECTS</div>
@ -116,7 +116,7 @@
<div class="scoplement">
<div>Validation du RCUE</div>
<div>enregistrée le {{
validation.date.strftime("%d/%m/%Y à %H:%M")
validation.date.strftime(scu.DATEATIME_FMT)
if validation.date else "-"
}}
</div>

View File

@ -3,39 +3,46 @@
<!-- formsemestre_header -->
<div class="formsemestre_page_title noprint">
<div class="infos">
<span class="semtitle"><a class="stdlink" title="{{sco.sem.session_id()}}" href="{{
<span class="semtitle"><a class="stdlink" title="{{sco.formsemestre.session_id()}}" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.sem.id)
}}">{{sco.sem.titre}}</a>
<a title="{{sco.sem.etapes_apo_str()}}">
{% if sco.sem.semestre_id != -1 %}, {{sco.sem.formation.get_cursus().SESSION_NAME}}
{{sco.sem.semestre_id}}
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)
}}">{{sco.formsemestre.titre}}</a>
<a title="{{sco.formsemestre.etapes_apo_str()}}">
{% if sco.formsemestre.semestre_id != -1 %}, {{sco.formsemestre.formation.get_cursus().SESSION_NAME}}
{{sco.formsemestre.semestre_id}}
{% endif %}</a>
{% if sco.sem.modalite %} en {{sco.sem.modalite}}{% endif %}</span>
{% if sco.formsemestre.modalite %} en {{sco.formsemestre.modalite}}{% endif %}
</span>
<span class="dates">
<a title="du {{sco.sem.date_debut.strftime('%d/%m/%Y')}}
au {{sco.sem.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.sem.date_debut.month - 1]}}
{{sco.sem.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.sem.date_fin.month - 1]}}
{{sco.sem.date_fin.year}}</a></span>
<a title="du {{sco.formsemestre.date_debut.strftime('%d/%m/%Y')}}
au {{sco.formsemestre.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.formsemestre.date_debut.month - 1]}}
{{sco.formsemestre.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.formsemestre.date_fin.month - 1]}}
{{sco.formsemestre.date_fin.year}}</a>
</span>
<span class="resp"><a
title="{{sco.sem.responsables_str(abbrev_prenom=False)}}">{{sco.sem.responsables_str()}}</a></span>
title="{{sco.formsemestre.responsables_str(abbrev_prenom=False)}}">{{sco.formsemestre.responsables_str()}}</a></span>
<span class="nbinscrits"><a class="discretelink" href="{{url_for('scolar.groups_view', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">{{sco.sem.inscriptions|length}} inscrits</a></span><span class="lock">{% if
not sco.sem.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre
verrouillé")|safe}}</a>{% endif %}</span><span class="eye">
formsemestre_id=sco.formsemestre.id)}}">{{sco.formsemestre.inscriptions|length}} inscrits</a></span>
<span class="lock">
{% if not sco.formsemestre.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.formsemestre.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre
verrouillé")|safe}}</a>
{% endif %}
</span>
<span class="eye">
{% if not scu.is_passerelle_disabled() %}
<a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">
{% if sco.sem.bul_hide_xml %}
formsemestre_id=sco.formsemestre.id)}}">
{% if sco.formsemestre.bul_hide_xml %}
{{ scu.ICON_HIDDEN|safe}}
{% else %}
{{ scu.ICON_PUBLISHED|safe }}
{% endif %}
</a>
{% endif %}
</span>
</div>
{{ sco.sem_menu_bar|safe }}
{{ sco.formsemestre_status_menu_bar()|safe }}
</div>
<!-- end of formsemestre_header -->

View File

@ -1,5 +1,5 @@
{%- extends 'babase.j2' -%}
{# -*- Base des pages ordinaires, dans départements -*- #}
{% extends 'babase.j2' %}
{% block styles %}
{{super()}}
@ -21,24 +21,25 @@
{% endblock %}
{% block content %}
<!-- sco_page -->
{% block scodoc_sidebar %}
{% include "sidebar.j2" %}
{% include "sidebar.j2" %}
{% endblock %}
<div id="gtrcontent" class="gtrcontent">
{% include "flashed_messages.j2" %}
{% if sco.sem %}
{% block formsemestre_header %}
{% include "formsemestre_header.j2" %}
{% endblock %}
{% endif %}
<div id="gtrcontent" class="gtrcontent">
{% include "flashed_messages.j2" %}
{% if sco.formsemestre %}
{% block formsemestre_header %}
{% include "formsemestre_header.j2" %}
{% endblock %}
{% endif %}
<div class="sco-app-content">
{% block app_content %}
page vide
{% endblock %}
<div class="sco-app-content">
{% block app_content %}
page vide
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}

View File

@ -58,7 +58,7 @@
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1g'|format(sco.nbabsjust)}} J., {{'%1g'|format(sco.nbabsnj)}} N.J.</span>
<br />{{'%1g'|format(sco.nb_abs_just)}} J., {{'%1g'|format(sco.nb_abs_nj)}} N.J.</span>
{% endif %}
<ul>
{% if current_user.has_permission(sco.Permission.AbsChange) %}

View File

@ -2,6 +2,7 @@
"""ScoDoc Flask views
"""
import datetime
from functools import cached_property
from flask import Blueprint
from flask import g, current_app, request
@ -57,8 +58,20 @@ class ScoData:
self.Permission = Permission
self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné:
if etud is None:
self._init_etud = etud
self._init_formsemestre = formsemestre
# les comptes d'absences sont initialisés lors de l'accès à etud_cur_sem
self.nb_abs_nj = 0
self.nb_abs_just = 0
self.nb_abs = 0
# .etud, .formsemestre, etc. sont des cached_property
# car ScoData() est créé par @context_processor
# AVANT le décorateur scodoc qui initialise g.scodoc_xxx
@cached_property
def etud(self) -> Identite | None:
"Informations étudiant courant, si sélectionné"
if self._init_etud is None:
etudid = g.get("etudid", None)
if etudid is None:
if request.method == "GET":
@ -66,50 +79,65 @@ class ScoData:
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid is not None:
etud = Identite.get_etud(etudid)
self.etud = etud
if etud is not None:
# Infos sur l'étudiant courant
ins = self.etud.inscription_courante()
if ins:
self.etud_cur_sem = ins.formsemestre
(
self.nbabsnj,
self.nbabsjust,
self.nbabs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
self.etud_cur_sem.date_debut.isoformat(),
self.etud_cur_sem.date_fin.isoformat(),
scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique")
),
)
else:
self.etud_cur_sem = None
else:
self.etud = None
# --- Informations sur semestre courant, si sélectionné
if formsemestre is None:
formsemestre_id = retreive_formsemestre_from_request()
if formsemestre_id is not None:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre is None:
self.sem = None
self.sem_menu_bar = None
else:
self.sem = formsemestre
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem
return Identite.get_etud(etudid)
return self._init_etud
@cached_property
def etud_cur_sem(self) -> FormSemestre | None:
"le semestre courant de l'étudiant courant"
etud = self.etud
if etud is None:
return None
ins = self.etud.inscription_courante()
cur_sem = ins.formsemestre
if ins:
(
self.nb_abs_nj,
self.nb_abs_just,
self.nb_abs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
cur_sem.date_debut.isoformat(),
cur_sem.date_fin.isoformat(),
scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique")
),
)
self.formsemestre = formsemestre
# --- Préférences
# prefs fallback to global pref if sem is None:
if formsemestre:
formsemestre_id = formsemestre.id
else:
formsemestre_id = None
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
return cur_sem
return None
@cached_property
def formsemestre(self) -> FormSemestre | None:
"Le formsemestre courant, si sélectionné"
if self._init_formsemestre is None:
formsemestre_id = retreive_formsemestre_from_request()
return (
FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre_id is not None
else None
)
return self._init_formsemestre
@cached_property
def sem_menu_bar(self) -> str | None:
"Le html de la bare de menu formsemestre s'il y en a un."
return (
sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
if self.formsemestre
else None
)
@cached_property
def prefs(self):
"Préférences"
# prefs fallback to global pref if no current formsemestre:
return sco_preferences.SemPreferences(
self.formsemestre.id if self.formsemestre else None
)
def formsemestre_status_menu_bar(self) -> str:
"Le HTML de la barre de menu semestre"
return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
from app.views import (

View File

@ -313,13 +313,13 @@ def _get_dates_from_assi_form(
date_fin = None
# On commence par convertir individuellement tous les champs
try:
date_debut = datetime.datetime.strptime(form.date_debut.data, "%d/%m/%Y")
date_debut = datetime.datetime.strptime(form.date_debut.data, scu.DATE_FMT)
except ValueError:
date_debut = None
form.set_error("date début invalide", form.date_debut)
try:
date_fin = (
datetime.datetime.strptime(form.date_fin.data, "%d/%m/%Y")
datetime.datetime.strptime(form.date_fin.data, scu.DATE_FMT)
if form.date_fin.data
else None
)
@ -358,7 +358,7 @@ def _get_dates_from_assi_form(
# La date de dépôt (si vide, la date actuelle)
try:
dt_entry_date = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
datetime.datetime.strptime(form.entry_date.data, scu.DATE_FMT)
if form.entry_date.data
else datetime.datetime.now() # local tz
)
@ -573,8 +573,8 @@ def bilan_etud():
abort(404, "étudiant inexistant dans ce département")
# Gestion des dates du bilan (par défaut l'année scolaire)
date_debut = scu.date_debut_annee_scolaire().strftime("%d/%m/%Y")
date_fin: str = scu.date_fin_annee_scolaire().strftime("%d/%m/%Y")
date_debut = scu.date_debut_annee_scolaire().strftime(scu.DATE_FMT)
date_fin: str = scu.date_fin_annee_scolaire().strftime(scu.DATE_FMT)
# Récupération de la métrique d'assiduité
assi_metric = scu.translate_assiduites_metric(
@ -634,18 +634,18 @@ def edit_justificatif_etud(justif_id: int):
form = AjoutJustificatifEtudForm(obj=justif)
# Set the default value for the etat field
if request.method == "GET":
form.date_debut.data = justif.date_debut.strftime("%d/%m/%Y")
form.date_fin.data = justif.date_fin.strftime("%d/%m/%Y")
form.date_debut.data = justif.date_debut.strftime(scu.DATE_FMT)
form.date_fin.data = justif.date_fin.strftime(scu.DATE_FMT)
if form.date_fin.data == form.date_debut.data:
# un seul jour: pas de date de fin, indique les heures
form.date_fin.data = ""
form.heure_debut.data = justif.date_debut.strftime("%H:%M")
form.heure_fin.data = justif.date_fin.strftime("%H:%M")
form.heure_debut.data = justif.date_debut.strftime(scu.TIME_FMT)
form.heure_fin.data = justif.date_fin.strftime(scu.TIME_FMT)
form.entry_date.data = (
justif.entry_date.strftime("%d/%m/%Y") if justif.entry_date else ""
justif.entry_date.strftime(scu.DATE_FMT) if justif.entry_date else ""
)
form.entry_time.data = (
justif.entry_date.strftime("%H:%M") if justif.entry_date else ""
justif.entry_date.strftime(scu.TIME_FMT) if justif.entry_date else ""
)
form.etat.data = str(justif.etat)
@ -968,7 +968,7 @@ def choix_date() -> str:
ok: bool = False
try:
date: datetime.date = datetime.datetime.strptime(
form.date.data, "%d/%m/%Y"
form.date.data, scu.DATE_FMT
).date()
if date < formsemestre.date_debut or date > formsemestre.date_fin:
form.set_error(
@ -999,8 +999,8 @@ def choix_date() -> str:
"assiduites/pages/choix_date.j2",
form=form,
sco=ScoData(formsemestre=formsemestre),
deb=formsemestre.date_debut.strftime("%d/%m/%Y"),
fin=formsemestre.date_fin.strftime("%d/%m/%Y"),
deb=formsemestre.date_debut.strftime(scu.DATE_FMT),
fin=formsemestre.date_fin.strftime(scu.DATE_FMT),
)
@ -1607,11 +1607,11 @@ def recup_assiduites_plage():
# Vérification des dates
try:
date_deb = datetime.datetime.strptime(date_deb, "%d/%m/%Y")
date_deb = datetime.datetime.strptime(date_deb, scu.DATE_FMT)
except ValueError as exc:
raise ScoValueError("date_debut invalide", dest_url=request.referrer) from exc
try:
date_fin = datetime.datetime.strptime(date_fin, "%d/%m/%Y")
date_fin = datetime.datetime.strptime(date_fin, scu.DATE_FMT)
except ValueError as exc:
raise ScoValueError("date_fin invalide", dest_url=request.referrer) from exc
@ -1893,12 +1893,12 @@ def _preparer_objet(
"filenames": filenames,
}
objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M")
objet_prepare["date_fin"] = objet.date_fin.strftime(scu.DATEATIME_FMT)
objet_prepare["real_date_fin"] = objet.date_fin.isoformat()
objet_prepare["date_debut"] = objet.date_debut.strftime("%d/%m/%y à %H:%M")
objet_prepare["date_debut"] = objet.date_debut.strftime(scu.DATEATIME_FMT)
objet_prepare["real_date_debut"] = objet.date_debut.isoformat()
objet_prepare["entry_date"] = objet.entry_date.strftime("%d/%m/%y à %H:%M")
objet_prepare["entry_date"] = objet.entry_date.strftime(scu.DATEATIME_FMT)
objet_prepare["etud_nom"] = objet.etudiant.nomprenom
@ -2243,7 +2243,7 @@ def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str:
"du dd/mm/yyyy hh:MM audd/mm/yyyy hh:MM" sinon
"""
if deb.date() == fin.date():
temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")]
temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime(scu.TIME_FMT)]
return f"le {temps[0]} de {temps[1]} à {temps[2]}"
return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}'
@ -2542,7 +2542,7 @@ class Jour:
"""
Renvoie la date du jour au format "dd/mm/yyyy"
"""
return self.date.strftime("%d/%m/%Y")
return self.date.strftime(scu.DATE_FMT)
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
"""

View File

@ -45,7 +45,6 @@ from app import models
from app.auth.models import User
from app.but import (
apc_edit_ue,
bulletin_but_court, # ne pas enlever: ajoute des routes !
cursus_but,
jury_edit_manual,
jury_but,
@ -53,6 +52,8 @@ from app.but import (
jury_but_validation_auto,
jury_but_view,
)
from app.but import bulletin_but_court # ne pas enlever: ajoute des routes !
from app.but.forms import jury_but_forms
@ -449,12 +450,14 @@ sco_publish(
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/ue_edit",
sco_edit_ue.ue_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
@bp.route("/ue_edit/<int:ue_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def ue_edit(ue_id: int):
"Edition de l'UE"
return sco_edit_ue.ue_edit(ue_id)
@bp.route("/set_ue_niveau_competence", methods=["POST"])
@ -504,7 +507,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""):
@bp.route("/ue_infos/<int:ue_id>")
@scodoc
@permission_required(Permission.ScoView)
def ue_infos(ue_id):
def ue_infos(ue_id: int):
ue = UniteEns.query.get_or_404(ue_id)
return sco_edit_apc.html_ue_infos(ue)
@ -688,15 +691,23 @@ def module_clone():
def index_html():
"Page accueil formations"
fmt = request.args.get("fmt", "html")
detail = scu.to_bool(request.args.get("detail", False))
editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table()
table = sco_formations.formation_list_table(detail=detail)
if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
H = [
html_sco_header.sco_header(page_title="Formations (programmes)"),
"""<h2>Formations (programmes pédagogiques)</h2>
f"""<h2>Formations (programmes pédagogiques)</h2>
<form>
<input type="checkbox" id="detailCheckbox" name="detail"
onchange="this.form.submit();"
{'checked' if detail else ''}>
<label for="detailCheckbox">Informations détaillées</label>
</form>
""",
table.html(),
]
@ -2452,7 +2463,7 @@ def formsemestre_validation_but(
if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12):
# encore loin de la fin du semestre de départ de ce jury ?
warning += f"""<div class="warning">Le semestre S{formsemestre.semestre_id}
terminera le {formsemestre.date_fin.strftime("%d/%m/%Y")}&nbsp;:
terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)}&nbsp;:
êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
</div>"""

View File

@ -3,6 +3,7 @@ PN / Référentiel de compétences
Emmanuel Viennet, 2021
"""
from pathlib import Path
import re
@ -19,11 +20,20 @@ from app import db, log
from app.decorators import scodoc, permission_required
from app.models import Formation
from app.models.but_refcomp import ApcReferentielCompetences
from app.but import change_refcomp
from app.but.import_refcomp import orebut_import_refcomp
from app.but.forms.refcomp_forms import FormationRefCompForm, RefCompLoadForm
from app.but.forms.refcomp_forms import (
FormationChangeRefCompForm,
FormationRefCompForm,
RefCompLoadForm,
)
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
from app.scodoc.sco_exceptions import (
ScoFormatError,
ScoNoReferentielCompetences,
ScoValueError,
)
from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp
from app.views import ScoData
@ -47,9 +57,12 @@ def refcomp(refcomp_id):
def refcomp_show(refcomp_id):
"""Affichage du référentiel de compétences."""
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = referentiel_competence.equivalents()
return render_template(
"but/refcomp_show.j2",
ref=referentiel_competence,
referentiels_equivalents=referentiels_equivalents,
title="Référentiel de compétences",
data_source=url_for(
"notes.refcomp",
@ -279,3 +292,55 @@ def refcomp_load(formation_id=None):
formation=formation,
title="Chargement réf. compétences",
)
@bp.route("/formation/<int:formation_id>/change_refcomp", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def formation_change_refcomp(formation_id: int):
"""Tente de changer le ref. de comp. de la formation"""
formation = Formation.get_formation(formation_id)
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
if ref_comp is None:
raise ScoNoReferentielCompetences(formation=formation)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = ref_comp.equivalents()
form = FormationChangeRefCompForm()
form.object_select.choices = [
(ref.id, ref.get_title()) for ref in referentiels_equivalents
]
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
)
if form.validate_on_submit():
try:
new_ref_id = int(form.object_select.data)
except TypeError as exc:
raise ScoValueError("nouveau refcomp id invalide") from exc
new_ref = None
for ref in referentiels_equivalents:
if ref.id == new_ref_id:
new_ref = ref
break
if new_ref is None:
raise ScoValueError("nouveau refcomp invalide")
change_refcomp.formation_change_referentiel(formation, new_ref)
flash("Formation changée de référentiel")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
)
)
return render_template(
"but/change_refcomp.j2",
form=form,
formation=formation,
referentiels_equivalents=referentiels_equivalents,
title="Changer de référentiel de compétences",
)

View File

@ -823,7 +823,8 @@ def form_change_coordonnees(etudid):
("telephonemobile", {"size": 13, "title": "Mobile"}),
),
initvalues=adr,
submitlabel="Valider le formulaire",
submitlabel="Enregistrer",
cancelbutton="Annuler",
)
dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
if tf[0] == 0:
@ -1162,7 +1163,7 @@ def _form_dem_of_def(
).first_or_404()
if not formsemestre.etat:
raise ScoValueError("Modification impossible: semestre verrouille")
nowdmy = time.strftime("%d/%m/%Y")
nowdmy = time.strftime(scu.DATE_FMT)
#
header = html_sco_header.sco_header(
page_title=f"""{operation_name} de {etud.nomprenom} (du semestre {formsemestre.titre_mois()})"""

View File

@ -245,7 +245,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
initvalues["roles"] = []
if "date_expiration" in initvalues:
initvalues["date_expiration"] = (
the_user.date_expiration.strftime("%d/%m/%Y")
the_user.date_expiration.strftime(scu.DATE_FMT)
if the_user.date_expiration
else ""
)
@ -391,9 +391,9 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{
"title": "e-mail",
"input_type": "text",
"explanation": "requis, doit fonctionner"
if not edit_only_roles
else "",
"explanation": (
"requis, doit fonctionner" if not edit_only_roles else ""
),
"size": 36,
"allow_null": False,
"readonly": edit_only_roles,
@ -448,10 +448,10 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"title": "e-mail institutionnel",
"input_type": "text",
"explanation": (
"requis" if require_email_institutionnel else "facultatif"
)
if not edit_only_roles
else "",
("requis" if require_email_institutionnel else "facultatif")
if not edit_only_roles
else ""
),
"size": 36,
"allow_null": not require_email_institutionnel,
"readonly": edit_only_roles,
@ -550,9 +550,11 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{
"title": "Date d'expiration", # j/m/a
"input_type": "datedmy",
"explanation": "j/m/a, laisser vide si pas de limite"
if not edit_only_roles
else "",
"explanation": (
"j/m/a, laisser vide si pas de limite"
if not edit_only_roles
else ""
),
"size": 9,
"allow_null": True,
"readonly": edit_only_roles,
@ -664,7 +666,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
try:
if vals["date_expiration"]:
vals["date_expiration"] = datetime.datetime.strptime(
vals["date_expiration"], "%d/%m/%Y"
vals["date_expiration"], scu.DATE_FMT
)
if vals["date_expiration"] < datetime.datetime.now():
H.append(tf_error_message("date expiration passée"))

View File

@ -0,0 +1,18 @@
# Certains référentiels de compétences peuvent être considérés
# comme équivalents
# Si ils ont la même structure
# Voir ApcReferentielCompetences.map_to_other_referentiel
# Mappings: nouveau : ancien
QLIO: # la clé est 'specialite'
parcours: # codes de parcours
OSC: MSC
QMI: MQSE
# et un transitoire UPHF:
MPBS: MP
PCLG: MSC
QPSMI: MQSE
ATN: MTD
# competences: # titres de compétences ('nom_court' dans le XML)
SD: STID

View File

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

View File

@ -45,7 +45,7 @@ from app.models.evaluations import Evaluation
from app.scodoc import sco_dump_db
from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar
from app.views import notes, scolar, ScoData
import app.scodoc.sco_utils as scu
import tools
from tools.fakedatabase import create_test_api_database
@ -58,9 +58,9 @@ cli.register(app)
@app.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
"Make scu and sco available in all Jinja templates"
# if modified, put the same in conftest.py#27
return dict(scu=scu)
return {"scu": scu, "sco": ScoData()}
@app.shell_context_processor

View File

@ -27,7 +27,7 @@ def test_client():
@apptest.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
return dict(scu=scu)
return {"scu": scu, "sco": ScoData()}
with apptest.test_request_context():
# initialize scodoc "g":

View File

@ -414,7 +414,7 @@ class ScoFake(object):
for e_idx in range(1, nb_evaluations_per_module + 1):
e = self.create_evaluation(
moduleimpl_id=moduleimpl_id,
date_debut=datetime.datetime.strptime(date_debut, "%d/%m/%Y"),
date_debut=datetime.datetime.strptime(date_debut, scu.DATE_FMT),
description="evaluation test %s" % e_idx,
coefficient=1.0,
)

View File

@ -962,8 +962,8 @@ def _create_abs(
date_debut, date_fin, demijournee, estjust=False, etudid=False, estabs=True
):
etud = Identite.from_request(etudid)
deb: dt.date = dt.datetime.strptime(date_debut, "%d/%m/%Y").date()
fin: dt.date = dt.datetime.strptime(date_fin, "%d/%m/%Y").date()
deb: dt.date = dt.datetime.strptime(date_debut, scu.DATE_FMT).date()
fin: dt.date = dt.datetime.strptime(date_fin, scu.DATE_FMT).date()
abs_list: list[Absence] = []
while deb < fin:
if deb.weekday() in [5, 6]:

View File

@ -3,7 +3,8 @@
"""Outils pour environnements de démo.
Change aléatoirement les identites (nip, civilite, nom, prenom) des étudiants d'un semestre.
Change aléatoirement les identites (nip, civilite, nom, prenom) des étudiants.
Un semestre ou tous si non spécifié.
Le NIP est choisi aléatoirement (nombre entier à 8 chiffres).
Les noms et prénoms sont issus des fichiers noms.txt, prenoms-h.txt, prenoms-f.txt
@ -19,37 +20,39 @@ from gen_nomprenoms import nomprenom
def usage():
print(f"Usage: {sys.argv[0]} dbname formsemestre_id")
print(f"Usage: {sys.argv[0]} dbname [formsemestre_id]")
sys.exit(1)
if len(sys.argv) != 3:
if len(sys.argv) not in (2, 3):
usage()
dbname = sys.argv[1]
formsemestre_id = sys.argv[2]
formsemestre_id = sys.argv[2] if len(sys.argv) > 2 else None
DBCNXSTRING = f"dbname={dbname}"
# Liste des etudiants inscrits à ce semestre
cnx = psycopg2.connect(DBCNXSTRING)
cursor = cnx.cursor()
cursor.execute(
"select count(*) from notes_formsemestre where id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
)
nsem = cursor.fetchone()[0]
if nsem != 1:
print(f"{nsem} formsemestre matching {formsemestre_id} in {dbname}")
sys.exit(2)
cursor.execute(
"""select i.id
from identite i, notes_formsemestre_inscription ins
where i.id=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
""",
{"formsemestre_id": formsemestre_id},
)
if formsemestre_id is None:
cursor.execute("SELECT i.id from identite i")
else:
cursor.execute(
"select count(*) from notes_formsemestre where id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
)
nsem = cursor.fetchone()[0]
if nsem != 1:
print(f"{nsem} formsemestre matching {formsemestre_id} in {dbname}")
sys.exit(2)
cursor.execute(
"""select i.id
from identite i, notes_formsemestre_inscription ins
where i.id=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
""",
{"formsemestre_id": formsemestre_id},
)
wcursor = cnx.cursor()
n = 0