Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | 18b1f00586 | |
Iziram | 6b985620e9 | |
Iziram | 4d234ba353 | |
Iziram | 5d45fcf656 | |
Iziram | 0a5919b788 | |
Iziram | 09f4525e66 | |
Emmanuel Viennet | 0bc57807de | |
Emmanuel Viennet | 87aaf12d27 | |
Emmanuel Viennet | c8ab9b9b6c | |
Emmanuel Viennet | ad7b48e110 | |
Emmanuel Viennet | f2ce16f161 | |
Emmanuel Viennet | 1ddf9b6ab8 | |
Emmanuel Viennet | 0a2e39cae1 | |
Emmanuel Viennet | a194b4b6e0 | |
Emmanuel Viennet | cbe85dfb7d | |
Emmanuel Viennet | beba69bfe4 | |
Emmanuel Viennet | 41fec29452 | |
Emmanuel Viennet | 9bd05ea241 | |
Emmanuel Viennet | 58b831513d | |
Emmanuel Viennet | b861aba6a3 | |
Emmanuel Viennet | c2443c361f | |
Emmanuel Viennet | ab4731bd43 | |
Emmanuel Viennet | c17bc8b61b | |
Emmanuel Viennet | e44a5ee55d | |
Emmanuel Viennet | a747ed22e2 | |
Emmanuel Viennet | 5d0a932634 | |
Emmanuel Viennet | 2b150cf521 | |
Emmanuel Viennet | 5a5ddcacd7 | |
Emmanuel Viennet | 3f6e65b9da | |
Emmanuel Viennet | 5eba6170a5 | |
Emmanuel Viennet | bd9bf87112 | |
Emmanuel Viennet | a0e2af481f | |
Emmanuel Viennet | 42e8f97441 | |
Emmanuel Viennet | 8ec0171ca0 | |
Emmanuel Viennet | 6dfab2d843 | |
Emmanuel Viennet | 523ec59833 | |
Emmanuel Viennet | bde6325391 | |
Emmanuel Viennet | 0577347622 |
|
@ -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)
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
|
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
|
|||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
try:
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
except ObjectDeletedError:
|
||||
return json_error(404, "Assiduité supprimée / inexistante")
|
||||
|
||||
return {"OK": True}
|
||||
|
||||
|
|
|
@ -603,8 +603,19 @@ class Role(db.Model):
|
|||
"""Create default roles if missing, then, if reset_permissions,
|
||||
reset their permissions to default values.
|
||||
"""
|
||||
Role.reset_roles_permissions(
|
||||
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
|
||||
"""Ajoute les permissions aux roles
|
||||
roles_perms : { "role_name" : (permission, ...) }
|
||||
reset_permissions : si vrai efface permissions déja existantes
|
||||
Si le role n'existe pas, il est (re) créé.
|
||||
"""
|
||||
default_role = "Observateur"
|
||||
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
||||
for role_name, permissions in roles_perms.items():
|
||||
role = Role.query.filter_by(name=role_name).first()
|
||||
if role is None:
|
||||
role = Role(name=role_name)
|
||||
|
|
|
@ -37,7 +37,17 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
|||
]
|
||||
# Choix des parcours
|
||||
ue_pids = [p.id for p in ue.parcours]
|
||||
H.append("""<form id="choix_parcours">""")
|
||||
H.append(
|
||||
"""
|
||||
<div class="help">
|
||||
Cocher tous les parcours dans lesquels cette UE est utilisée,
|
||||
même si vous n'offrez pas ce parcours dans votre département.
|
||||
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
|
||||
Ne cocher aucun parcours est équivalent à tous les cocher.
|
||||
</div>
|
||||
<form id="choix_parcours" style="margin-top: 12px;">
|
||||
"""
|
||||
)
|
||||
|
||||
ects_differents = {
|
||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
|
||||
import collections
|
||||
import datetime
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from flask import g, has_request_context, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import Evaluation, FormSemestre, Identite
|
||||
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
|
||||
from app.models.groups import GroupDescr
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||
|
@ -229,7 +231,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(
|
||||
|
@ -249,59 +251,88 @@ class BulletinBUT:
|
|||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": (
|
||||
[
|
||||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if (e.visibulletin or version == "long")
|
||||
and (e.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
]
|
||||
self.etud_list_modimpl_evaluations(
|
||||
etud, modimpl, modimpl_results, version
|
||||
)
|
||||
if version != "short"
|
||||
else []
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
||||
def etud_list_modimpl_evaluations(
|
||||
self,
|
||||
etud: Identite,
|
||||
modimpl: ModuleImpl,
|
||||
modimpl_results: ModuleImplResults,
|
||||
version: str,
|
||||
) -> list[dict]:
|
||||
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
|
||||
evaluation: Evaluation
|
||||
eval_results = []
|
||||
for evaluation in modimpl.evaluations:
|
||||
if (
|
||||
(evaluation.visibulletin or version == "long")
|
||||
and (evaluation.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[evaluation.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
):
|
||||
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
|
||||
evaluation.id
|
||||
]
|
||||
|
||||
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
|
||||
not np.isnan(eval_notes[etud.id])
|
||||
):
|
||||
eval_results.append(
|
||||
self.etud_eval_results(etud, evaluation, eval_notes)
|
||||
)
|
||||
return eval_results
|
||||
|
||||
def etud_eval_results(
|
||||
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
|
||||
) -> dict:
|
||||
"dict resultats d'un étudiant à une évaluation"
|
||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
||||
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
|
||||
try:
|
||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||
poids = {
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
|
||||
for ue in self.res.ues
|
||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
d = {
|
||||
"id": e.id,
|
||||
"id": evaluation.id,
|
||||
"coef": (
|
||||
fmt_note(e.coefficient)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
fmt_note(evaluation.coefficient)
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else None
|
||||
),
|
||||
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
||||
"description": e.description,
|
||||
"evaluation_type": e.evaluation_type,
|
||||
"date_debut": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"date_fin": (
|
||||
evaluation.date_fin.isoformat() if evaluation.date_fin else None
|
||||
),
|
||||
"description": evaluation.description,
|
||||
"evaluation_type": evaluation.evaluation_type,
|
||||
"note": (
|
||||
{
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
note_max=evaluation.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
|
||||
}
|
||||
if not e.is_blocked()
|
||||
if not evaluation.is_blocked()
|
||||
else {}
|
||||
),
|
||||
"poids": poids,
|
||||
|
@ -309,17 +340,25 @@ class BulletinBUT:
|
|||
url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
evaluation_id=evaluation.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
# deprecated (supprimer avant #sco9.7)
|
||||
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"heure_debut": (
|
||||
e.date_debut.time().isoformat("minutes") if e.date_debut else None
|
||||
"date": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||
),
|
||||
"heure_debut": (
|
||||
evaluation.date_debut.time().isoformat("minutes")
|
||||
if evaluation.date_debut
|
||||
else None
|
||||
),
|
||||
"heure_fin": (
|
||||
evaluation.date_fin.time().isoformat("minutes")
|
||||
if evaluation.date_fin
|
||||
else None
|
||||
),
|
||||
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||
}
|
||||
return d
|
||||
|
||||
|
@ -540,9 +579,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"]))
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -59,3 +59,4 @@ def check_taxe_now(taxes):
|
|||
|
||||
|
||||
from app.entreprises import routes
|
||||
from app.entreprises.activate import activate_module
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Activation du module entreprises
|
||||
|
||||
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
|
||||
|
||||
Au moment de l'activation, il est en général utile de proposer de configurer les
|
||||
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
|
||||
|
||||
Voir associations dans sco_roles_default
|
||||
|
||||
"""
|
||||
from app.auth.models import Role
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
|
||||
|
||||
|
||||
def activate_module(
|
||||
enable: bool = True, set_default_roles_permission: bool = False
|
||||
) -> bool:
|
||||
"""Active le module et en option donne les permissions aux rôles standards.
|
||||
True si l'état d'activation a changé.
|
||||
"""
|
||||
change = ScoDocSiteConfig.enable_entreprises(enable)
|
||||
if enable and set_default_roles_permission:
|
||||
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
|
||||
return change
|
|
@ -0,0 +1,17 @@
|
|||
"""
|
||||
Formulaire activation module entreprises
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields.simple import BooleanField, SubmitField
|
||||
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
||||
|
||||
class ActivateEntreprisesForm(FlaskForm):
|
||||
"Formulaire activation module entreprises"
|
||||
set_default_roles_permission = BooleanField(
|
||||
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -54,7 +54,6 @@ class BonusConfigurationForm(FlaskForm):
|
|||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||
disable_passerelle = BooleanField( # disable car par défaut activée
|
||||
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
|
||||
)
|
||||
|
@ -127,13 +126,6 @@ def configuration():
|
|||
flash("Fonction bonus inchangée.")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||
if ScoDocSiteConfig.enable_entreprises(
|
||||
enabled=form_scodoc.data["enable_entreprises"]
|
||||
):
|
||||
flash(
|
||||
"Module entreprise "
|
||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||
)
|
||||
if ScoDocSiteConfig.disable_passerelle(
|
||||
disabled=form_scodoc.data["disable_passerelle"]
|
||||
):
|
||||
|
@ -182,6 +174,7 @@ def configuration():
|
|||
|
||||
return render_template(
|
||||
"configuration.j2",
|
||||
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
scu=scu,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
@ -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}n°{self.code_nip or ""}{line_sep}né{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
|
||||
|
||||
|
|
|
@ -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();",
|
||||
)
|
||||
|
|
|
@ -232,7 +232,9 @@ class ScolarNews(db.Model):
|
|||
)
|
||||
|
||||
# Transforme les URL en URL absolues
|
||||
base = scu.ScoURL()
|
||||
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/index_html")
|
||||
]
|
||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||
|
||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -409,6 +409,14 @@ class UniteEns(models.ScoDocModel):
|
|||
Renvoie (True, "") si ok, sinon (False, error_message)
|
||||
"""
|
||||
msg = ""
|
||||
# Safety check
|
||||
if self.formation.referentiel_competence is None:
|
||||
return False, "pas de référentiel de compétence"
|
||||
# Si tous les parcours, aucun (tronc commun)
|
||||
if {p.id for p in parcours} == {
|
||||
p.id for p in self.formation.referentiel_competence.parcours
|
||||
}:
|
||||
parcours = []
|
||||
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
|
||||
prev_niveau = self.niveau_competence
|
||||
if (
|
||||
|
@ -424,6 +432,7 @@ class UniteEns(models.ScoDocModel):
|
|||
self.niveau_competence, parcours
|
||||
)
|
||||
if not ok:
|
||||
self.formation.invalidate_cached_sems()
|
||||
self.niveau_competence = prev_niveau # restore
|
||||
return False, error_message
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -48,6 +48,7 @@ from typing import Any
|
|||
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
import reportlab
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
from reportlab.platypus import Table, KeepInFrame
|
||||
from reportlab.lib.colors import Color
|
||||
|
@ -263,16 +264,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(
|
||||
(
|
||||
|
@ -812,7 +813,10 @@ if __name__ == "__main__":
|
|||
document,
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = doc.getvalue()
|
||||
with open("/tmp/gen_table.pdf", "wb") as f:
|
||||
f.write(data)
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
import html
|
||||
|
||||
from flask import g, render_template
|
||||
from flask import g, render_template, url_for
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
|
@ -163,7 +163,7 @@ def sco_header(
|
|||
params = {
|
||||
"page_title": page_title or sco_version.SCONAME,
|
||||
"no_side_bar": no_side_bar,
|
||||
"ScoURL": scu.ScoURL(),
|
||||
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
|
||||
"encoding": scu.SCO_ENCODING,
|
||||
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
|
||||
"authuser": current_user.user_name,
|
||||
|
@ -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>
|
||||
|
@ -219,7 +220,7 @@ def sco_header(
|
|||
<script>
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
|
||||
const SCO_URL="{scu.ScoURL()}";
|
||||
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||
</script>"""
|
||||
)
|
||||
|
|
|
@ -102,25 +102,33 @@ def sidebar_common():
|
|||
<a href="{home_link}" class="sidebar">Accueil</a> <br>
|
||||
<div id="authuser"><a id="authuserlink" href="{
|
||||
url_for("users.user_info_page",
|
||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
}">{current_user.user_name}</a>
|
||||
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
|
||||
</div>
|
||||
{sidebar_dept()}
|
||||
<h2 class="insidebar">Scolarité</h2>
|
||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
|
||||
<a href="{
|
||||
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Formations</a> <br>
|
||||
"""
|
||||
]
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
H.append(
|
||||
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """
|
||||
f""" <a href="{
|
||||
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Assiduité</a> <br> """
|
||||
)
|
||||
if current_user.has_permission(
|
||||
Permission.UsersAdmin
|
||||
) or current_user.has_permission(Permission.UsersView):
|
||||
H.append(
|
||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
|
||||
f"""<a href="{
|
||||
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}" class="sidebar">Utilisateurs</a> <br>"""
|
||||
)
|
||||
|
||||
if current_user.has_permission(Permission.EditPreferences):
|
||||
|
@ -141,7 +149,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 +190,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>"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -49,11 +49,13 @@
|
|||
"""
|
||||
import datetime
|
||||
import glob
|
||||
import gzip
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import chardet
|
||||
|
||||
|
@ -62,7 +64,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:
|
||||
|
@ -241,11 +243,13 @@ class BaseArchiver:
|
|||
filename: str,
|
||||
data: str | bytes,
|
||||
dept_id: int = None,
|
||||
compress=False,
|
||||
):
|
||||
"""Store data in archive, under given filename.
|
||||
Filename may be modified (sanitized): return used filename
|
||||
The file is created or replaced.
|
||||
data may be str or bytes
|
||||
If compress, data is gziped and filename suffix ".gz" added.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
|
@ -255,8 +259,14 @@ class BaseArchiver:
|
|||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
if compress:
|
||||
if not fname.endswith(".gz"):
|
||||
fname += ".gz"
|
||||
with gzip.open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
except FileNotFoundError as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur stockage archive (dossier inexistant, chemin {fname})"
|
||||
|
@ -274,8 +284,17 @@ class BaseArchiver:
|
|||
fname = os.path.join(archive_id, filename)
|
||||
log(f"reading archive file {fname}")
|
||||
try:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
if fname.endswith(".gz"):
|
||||
try:
|
||||
with gzip.open(fname) as f:
|
||||
data = f.read()
|
||||
except (OSError, EOFError, zlib.error) as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur lecture archive ({fname} invalide)"
|
||||
) from exc
|
||||
else:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur lecture archive (inexistant, chemin {fname})"
|
||||
|
@ -288,6 +307,8 @@ class BaseArchiver:
|
|||
"""
|
||||
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
|
||||
data = self.get(archive_id, filename)
|
||||
if filename.endswith(".gz"):
|
||||
filename = filename[:-3]
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
if mime is None:
|
||||
mime = "application/octet-stream"
|
||||
|
|
|
@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
|
|||
|
||||
|
||||
def do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre: FormSemestre,
|
||||
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
|
||||
description="",
|
||||
date_jury="",
|
||||
|
@ -92,19 +92,18 @@ def do_formsemestre_archive(
|
|||
raise ScoValueError(
|
||||
"do_formsemestre_archive: version de bulletin demandée invalide"
|
||||
)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem_archive_id = formsemestre_id
|
||||
sem_archive_id = formsemestre.id
|
||||
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
|
||||
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
||||
group_ids = [sco_groups.get_default_group(formsemestre.id)]
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, formsemestre_id=formsemestre_id
|
||||
group_ids, formsemestre_id=formsemestre.id
|
||||
)
|
||||
groups_filename = "-" + groups_infos.groups_filename
|
||||
etudids = [m["etudid"] for m in groups_infos.members]
|
||||
|
@ -142,19 +141,23 @@ def do_formsemestre_archive(
|
|||
)
|
||||
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
|
||||
archive_id,
|
||||
"Bulletins.json",
|
||||
data_js,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Décisions de jury, en XLS
|
||||
if formsemestre.formation.is_apc():
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
|
||||
data = response.get_data()
|
||||
else: # formations classiques
|
||||
data = sco_pv_forms.formsemestre_pvjury(
|
||||
formsemestre_id, fmt="xls", publish=False
|
||||
formsemestre.id, fmt="xls", publish=False
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -165,7 +168,7 @@ def do_formsemestre_archive(
|
|||
)
|
||||
# Classeur bulletins (PDF)
|
||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||
formsemestre_id, version=bul_version
|
||||
formsemestre.id, version=bul_version
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -173,10 +176,11 @@ def do_formsemestre_archive(
|
|||
"Bulletins.pdf",
|
||||
data,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Lettres individuelles (PDF):
|
||||
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
|
||||
formsemestre_id,
|
||||
formsemestre.id,
|
||||
etudids=etudids,
|
||||
date_jury=date_jury,
|
||||
date_commission=date_commission,
|
||||
|
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
|
|||
"""Make and store new archive for this formsemestre.
|
||||
(all students or only selected groups)
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.can_edit_pv():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
|
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
else:
|
||||
tf[2]["anonymous"] = False
|
||||
do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre,
|
||||
group_ids=group_ids,
|
||||
description=tf[2]["description"],
|
||||
date_jury=tf[2]["date_jury"],
|
||||
|
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
"""Page listing archives"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre_id
|
||||
L = []
|
||||
archives_descr = []
|
||||
for archive_id in PV_ARCHIVER.list_obj_archives(
|
||||
sem_archive_id, dept_id=formsemestre.dept_id
|
||||
):
|
||||
|
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
archive_id, dept_id=formsemestre.dept_id
|
||||
),
|
||||
}
|
||||
L.append(a)
|
||||
archives_descr.append(a)
|
||||
|
||||
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
|
||||
if not L:
|
||||
if not archives_descr:
|
||||
H.append("<p>aucune archive enregistrée</p>")
|
||||
else:
|
||||
H.append("<ul>")
|
||||
for a in L:
|
||||
for a in archives_descr:
|
||||
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
|
||||
H.append(
|
||||
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
||||
% (
|
||||
a["date"].strftime("%d/%m/%Y %H:%M"),
|
||||
a["description"],
|
||||
formsemestre_id,
|
||||
archive_name,
|
||||
)
|
||||
f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
|
||||
(<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, archive_name=archive_name
|
||||
)}">supprimer</a>)
|
||||
<ul>"""
|
||||
)
|
||||
for filename in a["content"]:
|
||||
H.append(
|
||||
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
||||
% (formsemestre_id, archive_name, filename, filename)
|
||||
f"""<li><a href="{
|
||||
url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
archive_name=archive_name,
|
||||
filename=filename
|
||||
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
|
||||
)
|
||||
if not a["content"]:
|
||||
H.append("<li><em>aucun fichier !</em></li>")
|
||||
|
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
|
||||
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre.id
|
||||
return PV_ARCHIVER.get_archived_file(
|
||||
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
|
||||
|
|
|
@ -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
|
||||
|
@ -446,7 +446,8 @@ def _ue_mod_bulletin(
|
|||
):
|
||||
"""Infos sur les modules (et évaluations) dans une UE
|
||||
(ajoute les informations aux modimpls)
|
||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement
|
||||
ceux où l'étudiant est inscrit).
|
||||
"""
|
||||
bul_show_mod_rangs = sco_preferences.get_preference(
|
||||
"bul_show_mod_rangs", formsemestre_id
|
||||
|
|
|
@ -61,7 +61,7 @@ from flask_login import current_user
|
|||
|
||||
from app.models import FormSemestre, Identite, ScoDocSiteConfig
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import NoteProcessError
|
||||
from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
|
||||
from app import log
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
|
@ -228,7 +228,15 @@ class BulletinGenerator:
|
|||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
)
|
||||
document.build(story)
|
||||
try:
|
||||
document.build(story)
|
||||
except (
|
||||
ValueError,
|
||||
KeyError,
|
||||
reportlab.platypus.doctemplate.LayoutError,
|
||||
) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
"""
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import html
|
||||
import time
|
||||
|
||||
|
@ -231,41 +230,41 @@ def next_iso_day(date):
|
|||
|
||||
def YearTable(
|
||||
year,
|
||||
events=[],
|
||||
events_by_day: dict[str, list[dict]],
|
||||
firstmonth=9,
|
||||
lastmonth=7,
|
||||
halfday=0,
|
||||
dayattributes="",
|
||||
pad_width=8,
|
||||
):
|
||||
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
|
||||
"""Generate a calendar table
|
||||
events = list of tuples (date, text, color, href [,halfday])
|
||||
where date is a string in ISO format (yyyy-mm-dd)
|
||||
halfday is boolean (true: morning, false: afternoon)
|
||||
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
||||
if halfday, generate 2 cells per day (morning, afternoon)
|
||||
"""
|
||||
T = [
|
||||
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
|
||||
"""<table id="maincalendar" class="maincalendar"
|
||||
border="3" cellpadding="1" cellspacing="1" frame="box">"""
|
||||
]
|
||||
T.append("<tr>")
|
||||
month = firstmonth
|
||||
while 1:
|
||||
while True:
|
||||
T.append('<td valign="top">')
|
||||
T.append(MonthTableHead(month))
|
||||
T.append(_month_table_head(month))
|
||||
T.append(
|
||||
MonthTableBody(
|
||||
_month_table_body(
|
||||
month,
|
||||
year,
|
||||
events,
|
||||
halfday,
|
||||
events_by_day,
|
||||
dayattributes,
|
||||
is_work_saturday(),
|
||||
pad_width=pad_width,
|
||||
)
|
||||
)
|
||||
T.append(MonthTableTail())
|
||||
T.append("</td>")
|
||||
T.append(
|
||||
"""
|
||||
</table>
|
||||
</td>"""
|
||||
)
|
||||
if month == lastmonth:
|
||||
break
|
||||
month = month + 1
|
||||
|
@ -323,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
|
|||
WEEKENDCOLOR = GREEN3
|
||||
|
||||
|
||||
def MonthTableHead(month):
|
||||
def _month_table_head(month):
|
||||
color = WHITE
|
||||
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
|
||||
color,
|
||||
MONTHNAMES_ABREV[month - 1],
|
||||
)
|
||||
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||
<tr bgcolor="{color}">
|
||||
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
|
||||
</tr>\n"""
|
||||
|
||||
|
||||
def MonthTableTail():
|
||||
return "</table>\n"
|
||||
|
||||
|
||||
def MonthTableBody(
|
||||
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
|
||||
):
|
||||
def _month_table_body(
|
||||
month,
|
||||
year,
|
||||
events_by_day: dict[str, list[dict]],
|
||||
trattributes="",
|
||||
work_saturday=False,
|
||||
) -> str:
|
||||
"""
|
||||
events : [event]
|
||||
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
|
||||
"""
|
||||
firstday, nbdays = calendar.monthrange(year, month)
|
||||
localtime = time.localtime()
|
||||
current_weeknum = time.strftime("%U", localtime)
|
||||
current_year = localtime[0]
|
||||
T = []
|
||||
rows = []
|
||||
# cherche date du lundi de la 1ere semaine de ce mois
|
||||
monday = ddmmyyyy("1/%d/%d" % (month, year))
|
||||
monday = ddmmyyyy(f"1/{month}/{year}")
|
||||
while monday.weekday != 0:
|
||||
monday = monday.prev()
|
||||
|
||||
|
@ -354,158 +356,51 @@ def MonthTableBody(
|
|||
else:
|
||||
weekend = ("S", "D")
|
||||
|
||||
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")
|
||||
)
|
||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||
if day in weekend:
|
||||
bgcolor = WEEKENDCOLOR
|
||||
weekclass = "wkend"
|
||||
attrs = ""
|
||||
else:
|
||||
bgcolor = WEEKDAYCOLOR
|
||||
weekclass = "wk" + str(monday).replace("/", "_")
|
||||
attrs = trattributes
|
||||
color = None
|
||||
legend = ""
|
||||
href = ""
|
||||
descr = ""
|
||||
# event this day ?
|
||||
# each event is a tuple (date, text, color, href)
|
||||
# where date is a string in ISO format (yyyy-mm-dd)
|
||||
for ev in events:
|
||||
ev_year = int(ev[0][:4])
|
||||
ev_month = int(ev[0][5:7])
|
||||
ev_day = int(ev[0][8:10])
|
||||
if year == ev_year and month == ev_month and ev_day == d:
|
||||
if ev[1]:
|
||||
legend = ev[1]
|
||||
if ev[2]:
|
||||
color = ev[2]
|
||||
if ev[3]:
|
||||
href = ev[3]
|
||||
if len(ev) > 4 and ev[4]:
|
||||
descr = ev[4]
|
||||
#
|
||||
cc = []
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
|
||||
for d in range(1, nbdays + 1):
|
||||
weeknum = time.strftime(
|
||||
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
||||
)
|
||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||
if day in weekend:
|
||||
bgcolor = WEEKENDCOLOR
|
||||
weekclass = "wkend"
|
||||
attrs = ""
|
||||
else:
|
||||
bgcolor = WEEKDAYCOLOR
|
||||
weekclass = "wk" + str(monday).replace("/", "_")
|
||||
attrs = trattributes
|
||||
# events this day ?
|
||||
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
|
||||
color = None
|
||||
ev_txts = []
|
||||
for ev in events:
|
||||
color = ev.get("color")
|
||||
href = ev.get("href", "")
|
||||
description = ev.get("description", "")
|
||||
if href:
|
||||
href = 'href="%s"' % href
|
||||
if descr:
|
||||
descr = 'title="%s"' % html.escape(descr, quote=True)
|
||||
if href or descr:
|
||||
cc.append("<a %s %s>" % (href, descr))
|
||||
|
||||
if legend or d == 1:
|
||||
if pad_width is not None:
|
||||
n = pad_width - len(legend) # pad to 8 cars
|
||||
if n > 0:
|
||||
legend = (
|
||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||
)
|
||||
href = f'href="{href}"'
|
||||
if description:
|
||||
description = f"""title="{html.escape(description, quote=True)}" """
|
||||
if href or description:
|
||||
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
|
||||
else:
|
||||
legend = " " # empty cell
|
||||
cc.append(legend)
|
||||
if href or descr:
|
||||
cc.append("</a>")
|
||||
cc.append("</td>")
|
||||
cell = "".join(cc)
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
if (
|
||||
weeknum == current_weeknum
|
||||
and current_year == year
|
||||
and weekclass != "wkend"
|
||||
):
|
||||
weekclass += " currentweek"
|
||||
T.append(
|
||||
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
|
||||
% (bgcolor, weekclass, attrs, d, day, cell)
|
||||
)
|
||||
else:
|
||||
# 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")
|
||||
)
|
||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||
if day in weekend:
|
||||
bgcolor = WEEKENDCOLOR
|
||||
weekclass = "wkend"
|
||||
attrs = ""
|
||||
else:
|
||||
bgcolor = WEEKDAYCOLOR
|
||||
weekclass = "wk" + str(monday).replace("/", "_")
|
||||
attrs = trattributes
|
||||
if (
|
||||
weeknum == current_weeknum
|
||||
and current_year == year
|
||||
and weekclass != "wkend"
|
||||
):
|
||||
weeknum += " currentweek"
|
||||
ev_txts.append(ev.get("title", " "))
|
||||
#
|
||||
cc = []
|
||||
if color is not None:
|
||||
cc.append(f'<td bgcolor="{color}" class="calcell">')
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
T.append(
|
||||
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
|
||||
% (bgcolor, weekclass, attrs, d, day)
|
||||
)
|
||||
cc = []
|
||||
for morning in (True, False):
|
||||
color = None
|
||||
legend = ""
|
||||
href = ""
|
||||
descr = ""
|
||||
for ev in events:
|
||||
ev_year = int(ev[0][:4])
|
||||
ev_month = int(ev[0][5:7])
|
||||
ev_day = int(ev[0][8:10])
|
||||
if ev[4] is not None:
|
||||
ev_half = int(ev[4])
|
||||
else:
|
||||
ev_half = 0
|
||||
if (
|
||||
year == ev_year
|
||||
and month == ev_month
|
||||
and ev_day == d
|
||||
and morning == ev_half
|
||||
):
|
||||
if ev[1]:
|
||||
legend = ev[1]
|
||||
if ev[2]:
|
||||
color = ev[2]
|
||||
if ev[3]:
|
||||
href = ev[3]
|
||||
if len(ev) > 5 and ev[5]:
|
||||
descr = ev[5]
|
||||
#
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
if href:
|
||||
href = 'href="%s"' % href
|
||||
if descr:
|
||||
descr = 'title="%s"' % html.escape(descr, quote=True)
|
||||
if href or descr:
|
||||
cc.append("<a %s %s>" % (href, descr))
|
||||
if legend or d == 1:
|
||||
n = 3 - len(legend) # pad to 3 cars
|
||||
if n > 0:
|
||||
legend = (
|
||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
||||
)
|
||||
else:
|
||||
legend = " " # empty cell
|
||||
cc.append(legend)
|
||||
if href or descr:
|
||||
cc.append("</a>")
|
||||
cc.append("</td>\n")
|
||||
T.append("".join(cc) + "</tr>")
|
||||
return "\n".join(T)
|
||||
cc.append(f"{', '.join(ev_txts)}</td>")
|
||||
cells = "".join(cc)
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
|
||||
weekclass += " currentweek"
|
||||
rows.append(
|
||||
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
|
||||
<td class="calday">{d}{day}</td>{cells}</tr>"""
|
||||
)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
|
|
@ -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(),
|
||||
|
@ -192,7 +192,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
|||
"elt_sem_apo",
|
||||
]
|
||||
if showcodes:
|
||||
columns_ids = ("formsemestre_id",) + columns_ids
|
||||
columns_ids.insert(0, "formsemestre_id") # prepend
|
||||
|
||||
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
|
||||
if current_user.has_permission(Permission.EditApogee):
|
||||
|
|
|
@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
|||
html_sco_header.sco_header(page_title="Suppression d'une formation"),
|
||||
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
|
||||
]
|
||||
|
||||
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
|
||||
if sems:
|
||||
formsemestres = formation.formsemestres.all()
|
||||
if formsemestres:
|
||||
H.append(
|
||||
"""<p class="warning">Impossible de supprimer cette formation,
|
||||
car les sessions suivantes l'utilisent:</p>
|
||||
<ul>"""
|
||||
)
|
||||
for sem in sems:
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
|
||||
% sem
|
||||
)
|
||||
for formsemestre in formsemestres:
|
||||
H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
|
||||
H.append(
|
||||
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL()
|
||||
f"""</ul>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}">Revenir</a></p>"""
|
||||
)
|
||||
else:
|
||||
if not dialog_confirmed:
|
||||
|
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
|||
</p>
|
||||
""",
|
||||
OK="Supprimer cette formation",
|
||||
cancel_url=scu.NotesURL(),
|
||||
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
|
||||
parameters={"formation_id": formation_id},
|
||||
)
|
||||
else:
|
||||
do_formation_delete(formation_id)
|
||||
H.append(
|
||||
f"""<p>OK, formation supprimée.</p>
|
||||
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>"""
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
}">continuer</a></p>"""
|
||||
)
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
|
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
|
|||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(scu.NotesURL())
|
||||
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
else:
|
||||
# check unicity : constraint UNIQUE(acronyme,titre,version)
|
||||
if create:
|
||||
|
|
|
@ -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>""",
|
||||
},
|
||||
),
|
||||
|
|
|
@ -837,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> """
|
||||
msg_refcomp = "changer"
|
||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 ""
|
||||
|
@ -405,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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -360,6 +360,7 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
|||
return etat
|
||||
|
||||
|
||||
# View
|
||||
def formsemestre_evaluations_cal(formsemestre_id):
|
||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -373,20 +374,17 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
color_futur = "#70E0FF"
|
||||
|
||||
year = formsemestre.annee_scolaire()
|
||||
events = {} # (day, halfday) : event
|
||||
events_by_day = collections.defaultdict(list) # date_iso : event
|
||||
for e in evaluations:
|
||||
if e.date_debut is None:
|
||||
continue # éval. sans date
|
||||
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
if e.date_debut == e.date_fin:
|
||||
heure_debut_txt, heure_fin_txt = "?", "?"
|
||||
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 "?"
|
||||
|
||||
description = f"""{
|
||||
e.moduleimpl.module.titre
|
||||
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
||||
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 ""
|
||||
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
||||
|
@ -396,28 +394,27 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
color = color_incomplete
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = color_futur
|
||||
href = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
||||
event = events.get(day)
|
||||
if not event:
|
||||
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
||||
else:
|
||||
if event[-1].id != e.moduleimpl.id:
|
||||
# plusieurs evals de modules differents a la meme date
|
||||
event[1] += ", " + txt
|
||||
event[4] += ", " + description
|
||||
if color == color_incomplete:
|
||||
event[2] = color_incomplete
|
||||
if color == color_futur:
|
||||
event[2] = color_futur
|
||||
event = {
|
||||
"color": color,
|
||||
"date_iso": day,
|
||||
"title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.",
|
||||
"description": f"""{e.description or e.moduleimpl.module.titre_str()}"""
|
||||
+ (
|
||||
f""" de {heure_debut_txt} à {heure_fin_txt}"""
|
||||
if heure_debut_txt
|
||||
else ""
|
||||
),
|
||||
"href": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
),
|
||||
"modimpl": e.moduleimpl,
|
||||
}
|
||||
events_by_day[day].append(event)
|
||||
|
||||
cal_html = sco_cal.YearTable(
|
||||
year, events=list(events.values()), halfday=False, pad_width=None
|
||||
)
|
||||
cal_html = sco_cal.YearTable(year, events_by_day=events_by_day)
|
||||
|
||||
return f"""
|
||||
{
|
||||
|
@ -526,7 +523,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",
|
||||
|
@ -539,7 +536,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl.id,
|
||||
),
|
||||
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
|
||||
"module_titre": e.moduleimpl.module.abbrev
|
||||
or e.moduleimpl.module.titre
|
||||
or "",
|
||||
"responsable_id": e.moduleimpl.responsable_id,
|
||||
"responsable_nomplogin": sco_users.user_info(
|
||||
e.moduleimpl.responsable_id
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class ScoPDFFormatError(ScoValueError):
|
|||
super().__init__(
|
||||
f"""Erreur dans un format pdf:
|
||||
<p>{msg}</p>
|
||||
<p>Vérifiez les paramètres (polices de caractères, balisage)
|
||||
<p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
|
||||
dans les paramètres ou préférences.
|
||||
</p>
|
||||
""",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -1431,18 +1431,25 @@ Ceci n'est possible que si :
|
|||
|
||||
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||
"""Delete a formsemestre (confirmation)"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Confirmation dialog
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
|
||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
|
||||
<p>(opération irréversible)</p>
|
||||
""",
|
||||
dest_url="",
|
||||
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
|
||||
parameters={"formsemestre_id": formsemestre_id},
|
||||
cancel_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
parameters={"formsemestre_id": formsemestre.id},
|
||||
)
|
||||
# Bon, s'il le faut...
|
||||
do_formsemestre_delete(formsemestre_id)
|
||||
do_formsemestre_delete(formsemestre.id)
|
||||
flash("Semestre supprimé !")
|
||||
return flask.redirect(scu.ScoURL())
|
||||
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
|
||||
|
||||
|
||||
def formsemestre_has_decisions_or_compensations(
|
||||
|
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
@ -635,7 +634,7 @@ def formsemestre_description_table(
|
|||
"UE": modimpl.module.ue.acronyme,
|
||||
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
|
||||
"Code": modimpl.module.code or "",
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre,
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre or "",
|
||||
"_Module_class": "scotext",
|
||||
"Inscrits": mod_nb_inscrits,
|
||||
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
|
||||
|
@ -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"]
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
),
|
||||
(
|
||||
|
|
|
@ -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>"
|
||||
)
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
),
|
||||
|
|
|
@ -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 ""
|
||||
)
|
||||
|
|
|
@ -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">"""
|
||||
|
|
|
@ -180,7 +180,7 @@ def fiche_etud(etudid=None):
|
|||
)
|
||||
else:
|
||||
info["etat_civil"] = ""
|
||||
info["ScoURL"] = scu.ScoURL()
|
||||
info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
info["authuser"] = current_user
|
||||
if restrict_etud_data:
|
||||
info["info_naissance"] = ""
|
||||
|
@ -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>
|
||||
"""
|
||||
|
|
|
@ -458,7 +458,12 @@ def pdf_basic_page(
|
|||
if title:
|
||||
head = Paragraph(SU(title), StyleSheet["Heading3"])
|
||||
objects = [head] + objects
|
||||
document.build(objects)
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -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()
|
||||
}""",
|
||||
|
|
|
@ -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"] = ""
|
||||
|
|
|
@ -611,16 +611,17 @@ class BasePreferences:
|
|||
"explanation": "toute saisie d'absence doit indiquer le module concerné",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "forcer_present",
|
||||
# {
|
||||
# "initvalue": 0,
|
||||
# "title": "Forcer l'appel des présents",
|
||||
# "input_type": "boolcheckbox",
|
||||
# "labels": ["non", "oui"],
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_present",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Désactiver la saisie des présences",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "assi",
|
||||
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||
},
|
||||
),
|
||||
(
|
||||
"periode_defaut",
|
||||
{
|
||||
|
@ -644,18 +645,18 @@ class BasePreferences:
|
|||
"category": "assi",
|
||||
},
|
||||
),
|
||||
(
|
||||
"assi_etat_defaut",
|
||||
{
|
||||
"explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
"initvalue": "aucun",
|
||||
"input_type": "menu",
|
||||
"labels": ["aucun", "present", "retard", "absent"],
|
||||
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
"title": "Définir l'état par défaut",
|
||||
"category": "assi",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "assi_etat_defaut",
|
||||
# {
|
||||
# "explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
# "initvalue": "aucun",
|
||||
# "input_type": "menu",
|
||||
# "labels": ["aucun", "present", "retard", "absent"],
|
||||
# "allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
# "title": "Définir l'état par défaut",
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_travail",
|
||||
{
|
||||
|
@ -2260,16 +2261,17 @@ class BasePreferences:
|
|||
before_table="<details><summary>{title}</summary>",
|
||||
after_table="</details>",
|
||||
)
|
||||
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(scu.ScoURL()) # cancel
|
||||
else:
|
||||
for pref in self.prefs_definition:
|
||||
self.prefs[None][pref[0]] = tf[2][pref[0]]
|
||||
self.save()
|
||||
flash("Préférences modifiées")
|
||||
return flask.redirect(scu.ScoURL())
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(dest_url) # cancel
|
||||
#
|
||||
for pref in self.prefs_definition:
|
||||
self.prefs[None][pref[0]] = tf[2][pref[0]]
|
||||
self.save()
|
||||
flash("Préférences modifiées")
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
|
||||
"""Build list of elements for TrivialFormulator.
|
||||
|
@ -2433,10 +2435,12 @@ function set_global_pref(el, pref_name) {
|
|||
before_table="<details><summary>{title}</summary>",
|
||||
after_table="</details>",
|
||||
)
|
||||
dest_url = (
|
||||
scu.NotesURL()
|
||||
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
|
||||
dest_url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre_id,
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
|
@ -2482,7 +2486,9 @@ function set_global_pref(el, pref_name) {
|
|||
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
|
||||
)
|
||||
elif destination == "global":
|
||||
return flask.redirect(scu.ScoURL() + "/edit_preferences")
|
||||
return flask.redirect(
|
||||
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -50,7 +50,7 @@ from app.scodoc import sco_bulletins_pdf
|
|||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
|
||||
from app.scodoc.sco_cursus_dut import SituationEtudCursus
|
||||
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
|
||||
import sco_version
|
||||
|
@ -132,7 +132,11 @@ def pdf_lettres_individuelles(
|
|||
)
|
||||
)
|
||||
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
@ -241,13 +245,14 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
|
|||
titre_jury_court = "s"
|
||||
else:
|
||||
titre_jury_court = ""
|
||||
params[
|
||||
"autorisations_txt"
|
||||
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
|
||||
etud.e,
|
||||
titre_jury_court,
|
||||
titre_jury_court,
|
||||
decision["autorisations_descr"],
|
||||
params["autorisations_txt"] = (
|
||||
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
|
||||
% (
|
||||
etud.e,
|
||||
titre_jury_court,
|
||||
titre_jury_court,
|
||||
decision["autorisations_descr"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
params["autorisations_txt"] = ""
|
||||
|
|
|
@ -126,7 +126,11 @@ def pvjury_pdf(
|
|||
)
|
||||
)
|
||||
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
|
|
|
@ -670,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"] = "%"
|
||||
|
@ -717,7 +717,7 @@ def table_suivi_cohorte(
|
|||
else:
|
||||
expl = ["<h3>Semestres associés à chaque date:</h3><ul>"]
|
||||
for p in P:
|
||||
expl.append(f"""<li><b>{p.datedebut.strftime("%d/%m/%y")}</b>:""")
|
||||
expl.append(f"""<li><b>{p.datedebut.strftime(scu.DATE_FMT)}</b>:""")
|
||||
ls = []
|
||||
for s in p.sems:
|
||||
ls.append(formsemestre.html_link_status())
|
||||
|
@ -1535,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]
|
||||
|
|
|
@ -51,7 +51,24 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.UsersView,
|
||||
p.ViewEtudData,
|
||||
),
|
||||
# Rôles pour l'application relations entreprises
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.ScoView,),
|
||||
"Observateur": (p.Observateur,),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
# (doit avoir un rôle Ens en plus !)
|
||||
"RespPe": (p.EditFormationTags,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
"SuperAdmin": p.ALL_PERMISSIONS,
|
||||
}
|
||||
|
||||
# Rôles pour l'application relations entreprises
|
||||
# séparés pour pouvoir les réinitialiser lors de l'activation du module Entreprises
|
||||
# Note: Admin (chef de dept n'a par défaut aucun rôle lié à ce module)
|
||||
|
||||
SCO_ROLES_ENTREPRISES_DEFAULT = {
|
||||
# ObservateurEntreprise est un observateur de l'application entreprise
|
||||
"ObservateurEntreprise": (p.RelationsEntrepView,),
|
||||
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
|
||||
|
@ -70,19 +87,10 @@ SCO_ROLES_DEFAULTS = {
|
|||
p.RelationsEntrepValidate,
|
||||
p.RelationsEntrepViewCorrs,
|
||||
),
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.ScoView,),
|
||||
"Observateur": (p.Observateur,),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
# (doit avoir un rôle Ens en plus !)
|
||||
"RespPe": (p.EditFormationTags,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
"SuperAdmin": p.ALL_PERMISSIONS,
|
||||
}
|
||||
|
||||
SCO_ROLES_DEFAULTS.update(SCO_ROLES_ENTREPRISES_DEFAULT)
|
||||
|
||||
# Les rôles accessibles via la page d'admin utilisateurs
|
||||
# - associés à un département:
|
||||
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")
|
||||
|
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
@ -47,12 +47,11 @@ from app import db, log
|
|||
from app.models import Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_import_etuds
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_pdf
|
||||
|
@ -250,7 +249,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()
|
||||
|
@ -389,7 +387,10 @@ def _trombino_pdf(groups_infos):
|
|||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
report.seek(0)
|
||||
return send_file(
|
||||
report,
|
||||
|
@ -466,7 +467,10 @@ def _listeappel_photos_pdf(groups_infos):
|
|||
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"""
|
||||
|
||||
import io
|
||||
|
||||
import reportlab
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.colors import black
|
||||
from reportlab.lib.pagesizes import A4, A3
|
||||
|
@ -277,10 +277,12 @@ def pdf_trombino_tours(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError) as exc:
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
@ -470,7 +472,10 @@ def pdf_feuille_releve_absences(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
|
|
@ -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.
|
||||
|
@ -780,51 +785,6 @@ BULLETINS_VERSIONS_BUT = BULLETINS_VERSIONS | {
|
|||
"butcourt": "Version courte spéciale BUT"
|
||||
}
|
||||
|
||||
# ----- Support for ScoDoc7 compatibility
|
||||
|
||||
|
||||
def ScoURL():
|
||||
"""base URL for this sco instance.
|
||||
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite
|
||||
= page accueil département
|
||||
"""
|
||||
return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/index_html")
|
||||
]
|
||||
|
||||
|
||||
def NotesURL():
|
||||
"""URL of Notes
|
||||
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes
|
||||
= url de base des méthodes de notes
|
||||
(page accueil programmes).
|
||||
"""
|
||||
return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
|
||||
|
||||
|
||||
def AbsencesURL():
|
||||
"""URL of Absences"""
|
||||
return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/index_html")
|
||||
]
|
||||
|
||||
|
||||
def AssiduitesURL():
|
||||
"""URL of Assiduités"""
|
||||
return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
|
||||
: -len("/BilanDept")
|
||||
]
|
||||
|
||||
|
||||
def UsersURL():
|
||||
"""URL of Users
|
||||
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
|
||||
= url de base des requêtes ZScoUsers
|
||||
et page accueil users
|
||||
"""
|
||||
return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
|
||||
|
||||
|
||||
# ---- Simple python utilities
|
||||
|
||||
|
||||
|
@ -1067,9 +1027,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/
|
||||
|
|
|
@ -485,6 +485,10 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mass-selection em {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.fieldsplit {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
@ -726,31 +730,11 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
background-color: var(--color-defaut) !important;
|
||||
}
|
||||
|
||||
.color.est_just.sans_etat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: var(--color-justi) !important;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color.invalide::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.attente {
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
|
@ -758,6 +742,10 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.color.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
}
|
||||
|
||||
#gtrcontent .pdp {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -2411,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 {
|
||||
|
@ -2455,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 {
|
||||
|
|
|
@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
|
|||
// Création des boutons d'assiduités
|
||||
if (readOnly) {
|
||||
} else if (currentAssiduite.type != "conflit") {
|
||||
["present", "retard", "absent"].forEach((abs) => {
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
if (!window.nonPresent) {
|
||||
etats.splice(0, 0, "present");
|
||||
}
|
||||
|
||||
etats.forEach((abs) => {
|
||||
const btn = document.createElement("input");
|
||||
btn.type = "checkbox";
|
||||
btn.value = abs;
|
||||
|
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
|
|||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
|
||||
return moduleimpls[id];
|
||||
})
|
||||
.catch((_) => {
|
||||
|
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
|
|||
|
||||
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||
const modimpl_id = $("#moduleimpl_select").val();
|
||||
if (
|
||||
assiduite &&
|
||||
assiduite.etat.toLowerCase() === etat &&
|
||||
assiduite.moduleimpl_id == modimpl_id
|
||||
)
|
||||
type = "suppression";
|
||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||
|
||||
const { deb, fin } = getPeriodAsDate();
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
function _partition_set_attr(partition_id, attr_name, attr_value) {
|
||||
$.post(
|
||||
SCO_URL + "/partition_set_attr",
|
||||
SCO_URL + "partition_set_attr",
|
||||
{
|
||||
partition_id: partition_id,
|
||||
attr: attr_name,
|
||||
|
|
|
@ -33,7 +33,7 @@ function update_ue_list() {
|
|||
let ue_code = $("#tf_ue_code")[0].value;
|
||||
let query =
|
||||
SCO_URL +
|
||||
"/Notes/ue_sharing_code?ue_code=" +
|
||||
"Notes/ue_sharing_code?ue_code=" +
|
||||
ue_code +
|
||||
"&hide_ue_id=" +
|
||||
ue_id +
|
||||
|
|
|
@ -16,7 +16,7 @@ function display_itemsuivis(active) {
|
|||
.off("click")
|
||||
.click(function (e) {
|
||||
e.preventDefault();
|
||||
$.post(SCO_URL + "/itemsuivi_create", {
|
||||
$.post(SCO_URL + "itemsuivi_create", {
|
||||
etudid: etudid,
|
||||
fmt: "json",
|
||||
}).done(item_insert_new);
|
||||
|
@ -26,7 +26,7 @@ function display_itemsuivis(active) {
|
|||
}
|
||||
// add existing items
|
||||
$.get(
|
||||
SCO_URL + "/itemsuivi_list_etud",
|
||||
SCO_URL + "itemsuivi_list_etud",
|
||||
{ etudid: etudid, fmt: "json" },
|
||||
function (L) {
|
||||
for (var i in L) {
|
||||
|
@ -95,7 +95,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
|
|||
dp.blur(function (e) {
|
||||
var date = this.value;
|
||||
// console.log('selected text: ' + date);
|
||||
$.post(SCO_URL + "/itemsuivi_set_date", {
|
||||
$.post(SCO_URL + "itemsuivi_set_date", {
|
||||
item_date: date,
|
||||
itemsuivi_id: itemsuivi_id,
|
||||
});
|
||||
|
@ -103,7 +103,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
|
|||
dp.datepicker({
|
||||
onSelect: function (date, instance) {
|
||||
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
|
||||
$.post(SCO_URL + "/itemsuivi_set_date", {
|
||||
$.post(SCO_URL + "itemsuivi_set_date", {
|
||||
item_date: date,
|
||||
itemsuivi_id: itemsuivi_id,
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ function Date2DMY(date) {
|
|||
}
|
||||
|
||||
function itemsuivi_suppress(itemsuivi_id) {
|
||||
$.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
|
||||
$.post(SCO_URL + "itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
|
||||
// Clear items and rebuild:
|
||||
$("ul.listdebouches li.itemsuivi").remove();
|
||||
display_itemsuivis(0);
|
||||
|
|
|
@ -37,7 +37,7 @@ $().ready(function () {
|
|||
ajax: {
|
||||
url:
|
||||
SCO_URL +
|
||||
"/etud_info_html?etudid=" +
|
||||
"etud_info_html?etudid=" +
|
||||
get_etudid_from_elem(elems[i]) +
|
||||
qs,
|
||||
type: "GET",
|
||||
|
|
|
@ -19,7 +19,7 @@ function loadGroupes() {
|
|||
$("#gmsg")[0].style.display = "block";
|
||||
var partition_id = document.formGroup.partition_id.value;
|
||||
|
||||
$.get(SCO_URL + "/XMLgetGroupsInPartition", {
|
||||
$.get(SCO_URL + "XMLgetGroupsInPartition", {
|
||||
partition_id: partition_id,
|
||||
}).done(function (data) {
|
||||
var nodes = data.getElementsByTagName("group");
|
||||
|
@ -384,7 +384,7 @@ function handleError(msg) {
|
|||
}
|
||||
|
||||
function submitGroups() {
|
||||
var url = SCO_URL + "/setGroups";
|
||||
var url = SCO_URL + "setGroups";
|
||||
// build post request body: groupname \n etudid; ...
|
||||
var groupsLists = "";
|
||||
var groupsToCreate = "";
|
||||
|
@ -443,7 +443,7 @@ function GotoAnother() {
|
|||
} else
|
||||
document.location =
|
||||
SCO_URL +
|
||||
"/affect_groups?partition_id=" +
|
||||
"affect_groups?partition_id=" +
|
||||
document.formGroup.other_partition_id.value;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ $().ready(function () {
|
|||
for (var i = 0; i < spans.length; i++) {
|
||||
var sp = spans[i];
|
||||
var etudid = sp.id;
|
||||
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
|
||||
$(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -194,7 +194,7 @@ $().ready(function () {
|
|||
ajax: {
|
||||
url:
|
||||
SCO_URL +
|
||||
"/etud_info_html?with_photo=0&etudid=" +
|
||||
"etud_info_html?with_photo=0&etudid=" +
|
||||
get_etudid_from_elem(elems[i]),
|
||||
},
|
||||
text: "Loading...",
|
||||
|
|
|
@ -34,7 +34,7 @@ function get_notes_and_draw(formsemestre_id, etudid) {
|
|||
*/
|
||||
var query =
|
||||
SCO_URL +
|
||||
"/Notes/formsemestre_bulletinetud?formsemestre_id=" +
|
||||
"Notes/formsemestre_bulletinetud?formsemestre_id=" +
|
||||
formsemestre_id +
|
||||
"&etudid=" +
|
||||
etudid +
|
||||
|
|
|
@ -42,7 +42,7 @@ async function save_note(elem, v, etudid) {
|
|||
$("#sco_msg").html("en cours...").show();
|
||||
try {
|
||||
const response = await fetch(
|
||||
SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set",
|
||||
SCO_URL + "../api/evaluation/" + evaluation_id + "/notes/set",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
|
@ -6,7 +6,7 @@ $(function () {
|
|||
delay: 300, // wait 300ms before suggestions
|
||||
minLength: 2, // min nb of chars before suggest
|
||||
position: { collision: "flip" }, // automatic menu position up/down
|
||||
source: SCO_URL + "/search_etud_by_name",
|
||||
source: SCO_URL + "search_etud_by_name",
|
||||
select: function (event, ui) {
|
||||
$(".in-expnom").val(ui.item.value);
|
||||
$("#form-chercheetud").submit();
|
||||
|
|
|
@ -5,6 +5,6 @@ $().ready(function () {
|
|||
for (var i = 0; i < spans.size(); i++) {
|
||||
var sp = spans[i];
|
||||
var etudid = sp.id;
|
||||
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
|
||||
$(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
async function delete_validation(etudid, validation_type, validation_id) {
|
||||
const response = await fetch(
|
||||
`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
|
||||
`${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ async function delete_validation(etudid, validation_type, validation_id) {
|
|||
function update_ue_list() {
|
||||
var ue_id = $("#tf_ue_id")[0].value;
|
||||
if (ue_id) {
|
||||
var query = SCO_URL + "/Notes/ue_sharing_code?ue_id=" + ue_id;
|
||||
var query = SCO_URL + "Notes/ue_sharing_code?ue_id=" + ue_id;
|
||||
$.get(query, "", function (data) {
|
||||
$("#ue_list_code").html(data);
|
||||
});
|
||||
|
|
|
@ -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 "?"
|
||||
),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -265,6 +265,8 @@ class Table(Element):
|
|||
title: str = None,
|
||||
classes: list[str] = None,
|
||||
raw_title: str = None,
|
||||
no_excel: bool = False,
|
||||
only_excel: bool = False,
|
||||
) -> tuple["Cell", "Cell"]:
|
||||
"""Record this title,
|
||||
and create cells for footer and header if they don't already exist.
|
||||
|
@ -282,6 +284,8 @@ class Table(Element):
|
|||
classes=classes,
|
||||
group=self.column_group.get(col_id),
|
||||
raw_content=raw_title or title,
|
||||
no_excel=no_excel,
|
||||
only_excel=only_excel,
|
||||
)
|
||||
if self.foot_title_row:
|
||||
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
|
||||
|
@ -370,6 +374,7 @@ class Row(Element):
|
|||
target_attrs: dict = None,
|
||||
target: str = None,
|
||||
column_classes: set[str] = None,
|
||||
only_excel: bool = False,
|
||||
no_excel: bool = False,
|
||||
) -> "Cell":
|
||||
"""Create cell and add it to the row.
|
||||
|
@ -397,6 +402,7 @@ class Row(Element):
|
|||
column_group=group,
|
||||
title=title,
|
||||
raw_title=raw_title,
|
||||
only_excel=only_excel,
|
||||
no_excel=no_excel,
|
||||
)
|
||||
|
||||
|
@ -406,6 +412,7 @@ class Row(Element):
|
|||
cell: "Cell",
|
||||
column_group: str | None = None,
|
||||
title: str | None = None,
|
||||
only_excel: bool = False,
|
||||
no_excel: bool = False,
|
||||
raw_title: str | None = None,
|
||||
) -> "Cell":
|
||||
|
@ -414,10 +421,10 @@ class Row(Element):
|
|||
"""
|
||||
cell.data["group"] = column_group or ""
|
||||
self.cells[col_id] = cell
|
||||
if col_id not in self.table.column_ids:
|
||||
if not only_excel and col_id not in self.table.column_ids:
|
||||
self.table.column_ids.append(col_id)
|
||||
if not no_excel:
|
||||
self.table.raw_column_ids.append(col_id)
|
||||
if not no_excel and col_id not in self.table.raw_column_ids:
|
||||
self.table.raw_column_ids.append(col_id)
|
||||
|
||||
self.table.insert_group(column_group)
|
||||
if column_group is not None:
|
||||
|
@ -425,7 +432,12 @@ class Row(Element):
|
|||
|
||||
if title is not None:
|
||||
self.table.add_title(
|
||||
col_id, title, classes=cell.classes, raw_title=raw_title
|
||||
col_id,
|
||||
title,
|
||||
classes=cell.classes,
|
||||
raw_title=raw_title,
|
||||
no_excel=no_excel,
|
||||
only_excel=only_excel,
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Liste simple d'étudiants
|
||||
"""
|
||||
"""Liste simple d'étudiants"""
|
||||
|
||||
import datetime
|
||||
from flask import g, url_for
|
||||
from app import log
|
||||
from app.models import FormSemestre, Identite, Justificatif
|
||||
from app.tables import table_builder as tb
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class TableAssi(tb.Table):
|
||||
|
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
|
|||
):
|
||||
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = ["gt_table"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
try:
|
||||
self.dates = [
|
||||
datetime.datetime.fromisoformat(str(dates[0]) + "T00:00"),
|
||||
datetime.datetime.fromisoformat(str(dates[1]) + "T00:00"),
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("invalid dates") from exc
|
||||
self.formsemestre = formsemestre
|
||||
self.formsemestre_modimpls = formsemestre_modimpls
|
||||
if convert_values:
|
||||
|
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
|
|||
bilan_etud = url_for(
|
||||
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
)
|
||||
self.add_cell(
|
||||
"etudid",
|
||||
"etudid",
|
||||
etud.etudid,
|
||||
"etudinfo",
|
||||
only_excel=True,
|
||||
)
|
||||
self.add_cell(
|
||||
"code_nip",
|
||||
"code_nip",
|
||||
etud.code_nip,
|
||||
"etudinfo",
|
||||
only_excel=True,
|
||||
)
|
||||
self.add_cell(
|
||||
"nom_disp",
|
||||
"Nom",
|
||||
|
@ -119,6 +140,13 @@ class RowAssi(tb.Row):
|
|||
)
|
||||
stats = self._get_etud_stats(etud)
|
||||
for key, value in stats.items():
|
||||
if key == "present" and sco_preferences.get_preference(
|
||||
"non_present",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
formsemestre_id=self.table.formsemestre.id,
|
||||
):
|
||||
continue
|
||||
|
||||
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
|
||||
if key != "present":
|
||||
self.add_cell(
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.j2" %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<div class="help">
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form class="form form-horizontal spacediv" method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ wtf.form_errors(form, hiddens="only") }}
|
||||
|
||||
{% if is_enabled %}
|
||||
<p>Le module <em>relations entreprises</em> est actuellement activé.</p>
|
||||
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
|
||||
données (la désactivation le fait simplement disparaitre des pages
|
||||
utilisateurs).
|
||||
<p>
|
||||
{% else %}
|
||||
<p>Le module <em>relations entreprises</em> est actuellement désactivé.
|
||||
</p>
|
||||
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
|
||||
données (la désactivation le fait simplement disparaitre des pages
|
||||
utilisateurs).
|
||||
<p>
|
||||
<p>
|
||||
Lors de son activation, vous pouvez (re)positionner les rôles qu'il utilise
|
||||
à leurs valeurs par défaut en cochant la case ci-dessous.
|
||||
</p>
|
||||
{{ wtf.form_field(form.set_default_roles_permission) }}
|
||||
{% endif %}
|
||||
<div class="form-group spacediv">
|
||||
{{ wtf.form_field(form.submit) }}
|
||||
{{ wtf.form_field(form.cancel) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.spacediv {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -51,8 +51,6 @@ Calendrier de l'assiduité
|
|||
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>Assiduité du</span>
|
||||
<br>
|
||||
<span>{{jour.get_date()}}</span>
|
||||
{{jour.generate_minitimeline() | safe}}
|
||||
</div>
|
||||
|
@ -77,36 +75,7 @@ Calendrier de l'assiduité
|
|||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
|
||||
<li><span title="Quart Bleu" class="est_just demo"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
<ul class="couleurs print">
|
||||
<li><span title="Vert" class="present demo"></span> présence
|
||||
|
@ -149,7 +118,7 @@ Calendrier de l'assiduité
|
|||
list-style-type: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.pageContent {
|
||||
margin-top: 1vh;
|
||||
|
@ -158,7 +127,7 @@ Calendrier de l'assiduité
|
|||
|
||||
.calendrier {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
justify-content: center;
|
||||
overflow-x: scroll;
|
||||
border: 1px solid #444;
|
||||
border-radius: 12px;
|
||||
|
@ -182,21 +151,8 @@ Calendrier de l'assiduité
|
|||
justify-content: start;
|
||||
}
|
||||
|
||||
.demo.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.demo.attente {
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.demo.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.demi .day.nonwork>span {
|
||||
|
@ -335,7 +291,7 @@ Calendrier de l'assiduité
|
|||
document.querySelectorAll('[assi_id]').forEach((el, i) => {
|
||||
el.addEventListener('click', () => {
|
||||
const assi_id = el.getAttribute('assi_id');
|
||||
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
||||
.cell, .header {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
|
@ -111,7 +111,7 @@
|
|||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
|
||||
.pointer{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ async function nouvellePeriode(period = null) {
|
|||
let periodeDiv = document.createElement("div");
|
||||
periodeDiv.classList.add("cell", "header");
|
||||
periodeDiv.id = `periode-${periodId}`;
|
||||
|
||||
|
||||
const periodP = document.createElement("p");
|
||||
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
|
||||
|
||||
|
@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
|
|||
|
||||
const assi_btns = document.createElement('div');
|
||||
assi_btns.classList.add('assi-btns');
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
["present", "retard", "absent"].forEach((value) => {
|
||||
if(!window.nonPresent){
|
||||
etats.splice(0,0,"present");
|
||||
}
|
||||
|
||||
etats.forEach((value) => {
|
||||
const cbox = document.createElement("input");
|
||||
cbox.type = "checkbox";
|
||||
cbox.value = value;
|
||||
|
@ -402,12 +407,12 @@ function sauvegarderAssiduites() {
|
|||
await nouvellePeriode(periode);
|
||||
}
|
||||
|
||||
// Si il y n'a pas d'erreur, on affiche un message de succès
|
||||
// Si il n'y a pas d'erreur, on affiche un message de succès
|
||||
if (data.errors.length == 0) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = "Les assiduités ont bien été sauvegardées.";
|
||||
span.textContent = "Le relevé d'assiduité a été enregistré.";
|
||||
openAlertModal(
|
||||
"Sauvegarde des assiduités",
|
||||
"Enregistrement de l'assiduité",
|
||||
span,
|
||||
null,
|
||||
"var(--color-present)"
|
||||
|
@ -499,6 +504,8 @@ const moduleimpls = new Map();
|
|||
const inscriptionsModules = new Map();
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
// Vérification du forçage de module
|
||||
window.forceModule = "{{ forcer_module }}" == "True";
|
||||
if (window.forceModule) {
|
||||
|
@ -518,12 +525,29 @@ if (window.forceModule) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultPlage = {{ nouv_plage | safe}} || [];
|
||||
|
||||
/**
|
||||
* Fonction exécutée au lancement de la page
|
||||
* - On affiche ou non les photos des étudiants
|
||||
* - On vérifie si la date est un jour travaillé
|
||||
*/
|
||||
async function main() {
|
||||
|
||||
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
|
||||
if (defaultPlage.every((e) => e)) {
|
||||
$("#date").datepicker("setDate", defaultPlage[0]);
|
||||
$("#debut").val(defaultPlage[1]);
|
||||
$("#fin").val(defaultPlage[2]);
|
||||
|
||||
// On ajoute la période si la date est un jour travaillé
|
||||
if(dateCouranteEstTravaillee()){
|
||||
await nouvellePeriode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
|
||||
afficherPDP(checked);
|
||||
$("#date").on("change", async function (d) {
|
||||
|
@ -532,7 +556,7 @@ async function main() {
|
|||
});
|
||||
}
|
||||
|
||||
main();
|
||||
window.addEventListener("load", main);
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -597,21 +621,23 @@ main();
|
|||
</label>
|
||||
|
||||
<label for="etatDef">
|
||||
Intialiser les étudiants comme :
|
||||
Intialiser les étudiants comme :
|
||||
<select name="etatDef" id="etatDef">
|
||||
<option value="">-</option>
|
||||
{% if not non_present %}
|
||||
<option value="present">présents</option>
|
||||
{% endif %}
|
||||
<option value="retard">en retard</option>
|
||||
<option value="absent">absents</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tableau à double entrée
|
||||
<!-- Tableau à double entrée
|
||||
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
|
||||
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
|
||||
Contenu :
|
||||
Contenu :
|
||||
- bouton assiduité (présent, retard, absent)
|
||||
- Bouton conflit si conflit de période
|
||||
--->
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
const readOnly = {{ readonly }};
|
||||
|
||||
window.forceModule = "{{ forcer_module }}" == "True"
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
|
@ -61,7 +62,7 @@
|
|||
$('#date').on('change', async function(d) {
|
||||
// On vérifie si la date est un jour travaillé
|
||||
dateCouranteEstTravaillee();
|
||||
|
||||
|
||||
|
||||
|
||||
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
||||
|
@ -87,7 +88,7 @@
|
|||
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
||||
}
|
||||
creerTousLesEtudiants(etuds);
|
||||
|
||||
|
||||
// affichage ou non des PDP
|
||||
afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" )
|
||||
}
|
||||
|
@ -159,8 +160,10 @@
|
|||
<div class="mass-selection">
|
||||
<span>Mettre tout le monde :</span>
|
||||
<fieldset class="btns_field mass">
|
||||
{% if not non_present %}
|
||||
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
|
||||
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
|
||||
{% endif %}
|
||||
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
|
||||
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
|
||||
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
|
||||
|
@ -168,20 +171,26 @@
|
|||
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
|
||||
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
|
||||
</fieldset>
|
||||
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div class="etud_holder">
|
||||
<p class="placeholder">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue