Compare commits

...

8 Commits

51 changed files with 574 additions and 539 deletions

View File

@ -685,6 +685,7 @@ class GenTable:
javascripts=(), javascripts=(),
with_html_headers=True, with_html_headers=True,
publish=True, publish=True,
template="sco_page.j2",
): ):
""" """
Build page at given format Build page at given format
@ -703,7 +704,7 @@ class GenTable:
H.append(self.html()) H.append(self.html())
if with_html_headers: if with_html_headers:
return render_template( return render_template(
"sco_page.j2", template,
content="\n".join(H), content="\n".join(H),
title=page_title, title=page_title,
javascripts=javascripts, javascripts=javascripts,

View File

@ -36,7 +36,6 @@ from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPermissionDenied from app.scodoc.sco_exceptions import ScoPermissionDenied
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
@ -123,18 +122,18 @@ def do_formsemestre_archive(
) )
if table_html: if table_html:
flash(f"Moyennes archivées le {date}", category="info") flash(f"Moyennes archivées le {date}", category="info")
data = "\n".join( data = render_template(
[ "sco_page.j2",
html_sco_header.sco_header(
page_title=f"Moyennes archivées le {date}",
no_sidebar=True, no_sidebar=True,
), title=f"Moyennes archivées le {date}",
content="\n".join(
[
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>', f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
"""<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); } """<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }
</style>""", </style>""",
table_html, table_html,
html_sco_header.sco_footer(),
] ]
),
) )
PV_ARCHIVER.store( PV_ARCHIVER.store(
archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id
@ -254,7 +253,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
}">Paramétrage</a>" }">Paramétrage</a>"
(accessible à l'administrateur du département).</em> (accessible à l'administrateur du département).</em>
</p>""", </p>""",
html_sco_header.sco_footer(),
] ]
descr = [ descr = [

View File

@ -29,7 +29,7 @@
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
""" """
import http import http
from flask import url_for, g, request from flask import g, render_template, request, url_for
from app import log from app import log
from app.comp import res_sem from app.comp import res_sem
@ -40,7 +40,6 @@ import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import safehtml from app.scodoc import safehtml
from app.scodoc import html_sco_header
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
@ -78,6 +77,7 @@ def report_debouche_date(start_year=None, fmt="html"):
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""", title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
fmt=fmt, fmt=fmt,
with_html_headers=True, with_html_headers=True,
template="sco_page_dept.j2",
) )
@ -226,14 +226,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
def report_debouche_ask_date(msg: str) -> str: def report_debouche_ask_date(msg: str) -> str:
"""Formulaire demande date départ""" """Formulaire demande date départ"""
return f"""{html_sco_header.sco_header()} return render_template(
"sco_page_dept.j2",
content=f"""
<h2>Table des débouchés des étudiants</h2> <h2>Table des débouchés des étudiants</h2>
<form method="GET"> <form method="GET">
{msg} {msg}
<input type="text" name="start_year" value="" size=10/> <input type="text" name="start_year" value="" size=10/>
</form> </form>
{html_sco_header.sco_footer()} """,
""" )
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@ -323,11 +325,11 @@ def itemsuivi_set_date(itemsuivi_id, item_date):
return ("", 204) return ("", 204)
def itemsuivi_set_situation(object, value): def itemsuivi_set_situation(obj, value):
"""set situation""" """set situation"""
if not sco_permissions_check.can_edit_suivi(): if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
itemsuivi_id = object itemsuivi_id = obj
situation = value.strip("-_ \t") situation = value.strip("-_ \t")
# log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation)) # log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()

View File

@ -29,7 +29,7 @@
(portage from DTML) (portage from DTML)
""" """
import flask import flask
from flask import flash, g, url_for, request from flask import flash, g, url_for, render_template, request
import sqlalchemy import sqlalchemy
from app import db from app import db
@ -54,7 +54,6 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
formation: Formation = Formation.query.get_or_404(formation_id) formation: Formation = Formation.query.get_or_404(formation_id)
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'une formation"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""", f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
] ]
formsemestres = formation.formsemestres.all() formsemestres = formation.formsemestres.all()
@ -85,6 +84,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
OK="Supprimer cette formation", OK="Supprimer cette formation",
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept), cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
parameters={"formation_id": formation_id}, parameters={"formation_id": formation_id},
template="sco_page_dept.j2",
) )
else: else:
do_formation_delete(formation_id) do_formation_delete(formation_id)
@ -95,8 +95,9 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
}">continuer</a></p>""" }">continuer</a></p>"""
) )
H.append(html_sco_header.sco_footer()) return render_template(
return "\n".join(H) "sco_page-dept.j2", content="\n".join(H), title="Suppression d'une formation"
)
def do_formation_delete(formation_id): def do_formation_delete(formation_id):

View File

@ -51,7 +51,6 @@ from app.scodoc.sco_exceptions import (
ScoGenError, ScoGenError,
ScoNonEmptyFormationObject, ScoNonEmptyFormationObject,
) )
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -208,8 +207,8 @@ def module_delete(module_id=None):
) )
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"}
f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""", ({module.code})</h2>""",
] ]
dest_url = url_for( dest_url = url_for(
@ -227,7 +226,11 @@ def module_delete(module_id=None):
cancelbutton="Annuler", cancelbutton="Annuler",
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return render_template(
"sco_page_dept.j2",
title="Suppression d'un module",
content="\n".join(H) + tf[1],
)
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
@ -372,16 +375,6 @@ def module_edit(
""" """
H = [ H = [
html_sco_header.sco_header(
page_title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
"js/module_edit.js",
],
),
f"""<h2>{title}</h2>""", f"""<h2>{title}</h2>""",
render_template( render_template(
"scodoc/help/modules.j2", "scodoc/help/modules.j2",
@ -780,7 +773,7 @@ def module_edit(
scu.get_request_args(), scu.get_request_args(),
descr, descr,
html_foot_markup=( html_foot_markup=(
f"""<div class="sco_tag_module_edit"><span f"""<div class="scobox sco_tag_module_edit"><span
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor" class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div> >{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
""" """
@ -793,8 +786,17 @@ def module_edit(
) )
# #
if tf[0] == 0: if tf[0] == 0:
return ( return render_template(
"\n".join(H) "sco_page_dept.j2",
title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
"js/module_edit.js",
],
content="\n".join(H)
+ tf[1] + tf[1]
+ ( + (
f""" f"""
@ -805,8 +807,7 @@ def module_edit(
""" """
if not create if not create
else "" else ""
) ),
+ html_sco_header.sco_footer()
) )
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
@ -904,9 +905,6 @@ def module_table(formation_id):
raise ScoValueError("invalid formation !") raise ScoValueError("invalid formation !")
formation: Formation = Formation.query.get_or_404(formation_id) formation: Formation = Formation.query.get_or_404(formation_id)
H = [ H = [
html_sco_header.sco_header(
page_title=f"Liste des modules de {formation.titre}"
),
f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2> f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2>
<ul class="notes_module_list"> <ul class="notes_module_list">
""", """,
@ -926,8 +924,11 @@ def module_table(formation_id):
) )
H.append("</li>") H.append("</li>")
H.append("</ul>") H.append("</ul>")
H.append(html_sco_header.sco_footer()) return render_template(
return "\n".join(H) "sco_page_dept.j2",
title=f"Liste des modules de {formation.titre}",
content="\n".join(H),
)
def module_is_locked(module_id): def module_is_locked(module_id):

View File

@ -505,8 +505,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
else: else:
clone_form = "" clone_form = ""
return f""" return render_template(
{html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])} "sco_page_dept.j2",
title=title,
javascripts=["js/edit_ue.js"],
content=f"""
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2> <h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
<p class="help">Les UEs sont des groupes de modules dans une formation donnée, <p class="help">Les UEs sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des utilisés pour la validation (on calcule des moyennes par UE et applique des
@ -529,9 +532,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
<div id="bonus_description" class="scobox"></div> <div id="bonus_description" class="scobox"></div>
<div id="ue_list_code" class="sco_box sco_green_bg"></div> <div id="ue_list_code" class="sco_box sco_green_bg"></div>
""",
{html_sco_header.sco_footer()} )
"""
elif tf[0] == 1: elif tf[0] == 1:
if create: if create:
if not tf[2]["ue_code"]: if not tf[2]["ue_code"]:

View File

@ -486,7 +486,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
<div class="cal_evaluations"> <div class="cal_evaluations">
{ cal_html } { cal_html }
</div> </div>
<p>soit {nb_evals} évaluations planifiées; <div class="scobox maxwidth">
<p>soit {nb_evals} évaluations planifiées&nbsp;:
</p> </p>
<ul> <ul>
<li>en <span style= <li>en <span style=
@ -508,6 +509,7 @@ def formsemestre_evaluations_cal(formsemestre_id):
) )
}" class="stdlink">voir les délais de correction</a> }" class="stdlink">voir les délais de correction</a>
</p> </p>
</div>
""", """,
) )

View File

@ -825,7 +825,7 @@ def excel_feuille_listeappel(
ws.append_blank_row() ws.append_blank_row()
# bas de page (date, serveur) # bas de page (date, serveur)
dt = time.strftime("%d/%m/%Y à %Hh%M") dt = time.strftime(scu.DATEATIME_FMT)
if server_name: if server_name:
dt += " sur " + server_name dt += " sur " + server_name
cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i) cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)

View File

@ -174,7 +174,9 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="formation_table_recap", table_id="formation_table_recap",
) )
return tab.make_page(fmt=fmt, javascripts=["js/formation_recap.js"]) return tab.make_page(
fmt=fmt, javascripts=["js/formation_recap.js"], template="sco_page_dept.j2"
)
def export_recap_formations_annee_scolaire(annee_scolaire): def export_recap_formations_annee_scolaire(annee_scolaire):

View File

@ -149,6 +149,7 @@ def formsemestre_associate_new_version(
"formation_id": formation_id, "formation_id": formation_id,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
}, },
template="sco_page_dept.j2",
) )
elif request.method == "POST": elif request.method == "POST":
if formsemestre_id is not None: # pas dans le form car checkbox disabled if formsemestre_id is not None: # pas dans le form car checkbox disabled

View File

@ -60,7 +60,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -80,20 +79,20 @@ def _default_sem_title(formation):
def formsemestre_createwithmodules(): def formsemestre_createwithmodules():
"""Page création d'un semestre""" """Page création d'un semestre"""
H = [ H = ["""<h2>Mise en place d'un semestre de formation</h2>"""]
html_sco_header.sco_header( r = do_formsemestre_createwithmodules()
page_title="Création d'un semestre", if not isinstance(r, str):
return r # response redirect
H.append(r)
return render_template(
"sco_page_dept.j2",
title="Création d'un semestre",
content="\n".join(H),
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"], javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
), )
"""<h2>Mise en place d'un semestre de formation</h2>""",
]
r = do_formsemestre_createwithmodules()
if isinstance(r, str):
H.append(r)
else:
return r # response redirect
return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_editwithmodules(formsemestre_id: int): def formsemestre_editwithmodules(formsemestre_id: int):
@ -1398,6 +1397,7 @@ def do_formsemestre_clone(
formsemestre.parcours = formsemestre_orig.parcours formsemestre.parcours = formsemestre_orig.parcours
# 6- Copy description # 6- Copy description
if formsemestre_orig.description:
formsemestre.description = formsemestre_orig.description.clone() formsemestre.description = formsemestre_orig.description.clone()
db.session.add(formsemestre) db.session.add(formsemestre)

View File

@ -802,7 +802,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
})</span></h3>""" })</span></h3>"""
) )
# #
H.append('<div class="sem-groups-abs">') H.append('<div class="sem-groups-abs space-before-18">')
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre) disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
show_abs: str = "hidden" if disable_abs else "" show_abs: str = "hidden" if disable_abs else ""
@ -812,6 +812,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
groups = partition.groups.all() groups = partition.groups.all()
effectifs = {g.id: g.get_nb_inscrits() for g in groups} effectifs = {g.id: g.get_nb_inscrits() for g in groups}
partition_is_empty = sum(effectifs.values()) == 0 partition_is_empty = sum(effectifs.values()) == 0
if partition_is_empty and (partition.is_default() or partition.is_parcours()):
continue # inutile de montrer des partitions vides non éditables
H.append( H.append(
f""" f"""
<div class="sem-groups-partition"> <div class="sem-groups-partition">
@ -909,7 +911,9 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
) )
H.append("</div>") # /sem-groups-assi H.append("</div>") # /sem-groups-assi
if partition_is_empty and not partition.is_default(): if partition_is_empty and not (
partition.is_default() or partition.is_parcours()
):
H.append( H.append(
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition' '<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
) )
@ -924,41 +928,72 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
H.append("</div>") H.append("</div>")
H.append("</div>") # /sem-groups-partition H.append("</div>") # /sem-groups-partition
# Boite avec liens divers
autres_liens = []
if formsemestre.can_change_groups(): if formsemestre.can_change_groups():
H.append( autres_liens.append(
f"""<h4><a class="stdlink" f"""<a class="stdlink"
title="une partition est un ensemble de groupes: TD, TP, ..."
href="{url_for("scolar.partition_editor", href="{url_for("scolar.partition_editor",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
edit_partition=1) edit_partition=1)
}">Ajouter une partition</a></h4>""" }">Ajouter une partition</a>"""
) )
# --- Formulaire importation Assiduité excel (si autorisé) # --- Formulaire importation Assiduité excel (si autorisé)
if current_user.has_permission(Permission.AbsChange) and not disable_abs: if current_user.has_permission(Permission.AbsChange) and not disable_abs:
H.append( autres_liens.append(
f"""<p> f"""
<a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre', <a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}"> formsemestre_id=formsemestre.id)}">
Importation de l'assiduité depuis un fichier excel</a> Importation de l'assiduité depuis un fichier excel</a>
</p>""" """
) )
# --- Lien Traitement Justificatifs: # --- Lien Traitement Justificatifs:
if ( if (
current_user.has_permission(Permission.AbsJustifView) current_user.has_permission(Permission.AbsJustifView)
and current_user.has_permission(Permission.JustifValidate) and current_user.has_permission(Permission.JustifValidate)
and not disable_abs and not disable_abs
): ):
H.append( autres_liens.append(
f"""<p> f"""
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs', <a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}"> formsemestre_id=formsemestre.id)}">
Traitement des justificatifs d'absence</a> Traitement des justificatifs d'absence</a>
</p>""" """
)
# --- Lien pour mail aux enseignants
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(u.email for u in formsemestre.responsables)
for modimpl in formsemestre.modimpls_sorted:
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
autres_liens.append(
f"""
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
len(adrlist)} enseignants du semestre</a>
"""
)
# Met le tout en boite
if autres_liens:
H.append(
f"""
<div class="sem-groups-partition sem-groups-autres-liens">
<div class="sem-groups-none">
<ul>
<li>{'</li><li>'.join(autres_liens)}</li>
</ul>
</div>
</div>
"""
) )
H.append("</div>") H.append("</div>")
@ -1033,8 +1068,9 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
) )
if evals["last_modif"]: if evals["last_modif"]:
H.append( H.append(
" <em>(dernière note saisie le %s)</em>" f""" <em>(dernière note saisie le {
% evals["last_modif"].strftime("%d/%m/%Y à %Hh%M") evals["last_modif"].strftime(scu.DATEATIME_FMT)
})</em>"""
) )
H.append("</td></tr>") H.append("</td></tr>")
H.append("</table>") H.append("</table>")
@ -1073,12 +1109,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
modimpls = formsemestre.modimpls_sorted modimpls = formsemestre.modimpls_sorted
nt = res_sem.load_formsemestre_results(formsemestre) nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(u.email for u in formsemestre.responsables)
for modimpl in modimpls:
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
can_edit = formsemestre.can_be_edited_by(current_user) can_edit = formsemestre.can_be_edited_by(current_user)
can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or ( can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
current_user.id in [resp.id for resp in formsemestre.responsables] current_user.id in [resp.id for resp in formsemestre.responsables]
@ -1206,20 +1236,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
) )
# --- LISTE DES ETUDIANTS # --- LISTE DES ETUDIANTS
H += [ H += [
'<div class="formsemestre-groupes">', '<div class="formsemestre-groupes space-before-24">',
_make_listes_sem(formsemestre), _make_listes_sem(formsemestre),
"</div>", "</div>",
] ]
# --- Lien mail enseignants:
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
H.append(
f"""<p>
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
len(adrlist)} enseignants du semestre</a>
</p>"""
)
return render_template( return render_template(
"sco_page.j2", "sco_page.j2",
content="".join(H), content="".join(H),

View File

@ -583,6 +583,11 @@ def formsemestre_recap_parcours_table(
) )
is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
num_sem += 1 num_sem += 1
url_status = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid]) dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
pv = dpv["decisions"][0] pv = dpv["decisions"][0]
@ -642,7 +647,7 @@ def formsemestre_recap_parcours_table(
H.append( H.append(
f""" f"""
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td> <td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
<td class="datedebut">{formsemestre.mois_debut()}</td> <td class="datedebut"><a href="{url_status}">{formsemestre.mois_debut()}</a></td>
<td class="rcp_titre_sem"><a class="formsemestre_status_link" <td class="rcp_titre_sem"><a class="formsemestre_status_link"
href="{ href="{
url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
@ -724,8 +729,9 @@ def formsemestre_recap_parcours_table(
f"""Autre formation: {formsemestre.formation.formation_code}""" f"""Autre formation: {formsemestre.formation.formation_code}"""
) )
H.append( H.append(
'<td class="datefin">%s</td><td class="sem_info">%s</td>' f"""<td class="datefin"><a href="{url_status}">{formsemestre.mois_fin()}</a></td>
% (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info)) <td class="sem_info">{sem_info.get(formsemestre.id, default_sem_info)}</td>
"""
) )
# Moy Gen (sous le code decision) # Moy Gen (sous le code decision)
H.append( H.append(

View File

@ -65,7 +65,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
) )
annotations = groups_list_annotation(groups_infos.group_ids) annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations: for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M") annotation["date_str"] = annotation["date"].strftime(scu.DATEATIME_FMT)
if fmt == "xls": if fmt == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment") columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else: else:

View File

@ -783,12 +783,17 @@ def groups_table(
tab.html(), tab.html(),
f""" f"""
<ul> <ul>
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a> <li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel
groupe(s) {groups_infos.groups_titles}</a>
</li> </li>
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a> <li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour
Moodle groupe(s) {groups_infos.groups_titles}</a>
</li> </li>
<li> <li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}"> <a class="stdlink" href="{{
url_for('notes.export_groups_as_moodle_csv',
scodoc_dept=g.scodoc_dept, formsemestre_id=groups_infos.formsemestre_id)
}}">
Fichier CSV pour Moodle (tous les groupes)</a> Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em> <em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>""", </li>""",

View File

@ -656,7 +656,7 @@ def _ligne_evaluation(
if etat["last_modif"]: if etat["last_modif"]:
H.append( H.append(
f"""<span class="mievr_lastmodif">(dernière modif le { f"""<span class="mievr_lastmodif">(dernière modif le {
etat["last_modif"].strftime("%d/%m/%Y à %Hh%M")})</span>""" etat["last_modif"].strftime(scu.DATEATIME_FMT)})</span>"""
) )
# #
H.append( H.append(

View File

@ -51,7 +51,7 @@ from wtforms import (
from app.models import Evaluation, ModuleImpl from app.models import Evaluation, ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header, sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_excel from app.scodoc import sco_excel
@ -204,14 +204,14 @@ def placement_eval_selectetuds(evaluation_id):
% runner.__dict__ % runner.__dict__
) )
return runner.exec_placement() # calcul et generation du fichier return runner.exec_placement() # calcul et generation du fichier
htmls = [
html_sco_header.sco_header(), return render_template(
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), "scodoc/forms/placement.j2",
"<h3>Placement et émargement des étudiants</h3>", evaluations_description=sco_evaluations.evaluation_describe(
render_template("scodoc/forms/placement.j2", form=form), evaluation_id=evaluation_id
] ),
footer = html_sco_header.sco_footer() form=form,
return "\n".join(htmls) + "<p>" + footer )
class PlacementRunner: class PlacementRunner:

View File

@ -350,7 +350,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
etudids = [etudid] etudids = [etudid]
else: else:
etuddescr = "" etud = None
if not group_ids: if not group_ids:
# tous les inscrits du semestre # tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)] group_ids = [sco_groups.get_default_group(formsemestre_id)]
@ -399,7 +399,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
if tf[0] == 0: if tf[0] == 0:
return render_template( return render_template(
"sco_page.j2", "sco_page.j2",
title=f"Édition du PV de jury de {etud.nom_prenom()}", title=f"Édition du PV de jury {('de ' + etud.nom_prenom()) if etud else ''}",
content=f"""<h2 class="formsemestre">Édition du PV de jury content=f"""<h2 class="formsemestre">Édition du PV de jury
de <a class="discretelink" href="{ de <a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)

View File

@ -37,7 +37,7 @@ import time
import datetime import datetime
from operator import itemgetter from operator import itemgetter
from flask import url_for, g, request from flask import url_for, g, render_template, request
import pydot import pydot
from app import log from app import log
@ -50,7 +50,6 @@ from app.models.etudiants import Identite
from app.scodoc import ( from app.scodoc import (
codes_cursus, codes_cursus,
html_sco_header,
sco_etud, sco_etud,
sco_formsemestre, sco_formsemestre,
sco_formsemestre_inscriptions, sco_formsemestre_inscriptions,
@ -411,11 +410,6 @@ def formsemestre_report_counts(
if fmt != "html": if fmt != "html":
return tableau return tableau
H = [ H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=title,
),
tableau, tableau,
"\n".join(F), "\n".join(F),
"""<p class="help">Le tableau affiche le nombre d'étudiants de ce semestre dans chacun """<p class="help">Le tableau affiche le nombre d'étudiants de ce semestre dans chacun
@ -423,9 +417,14 @@ def formsemestre_report_counts(
pour les lignes et les colonnes. Le <tt>codedecision</tt> est le code de la décision pour les lignes et les colonnes. Le <tt>codedecision</tt> est le code de la décision
du jury. du jury.
</p>""", </p>""",
html_sco_header.sco_footer(),
] ]
return "\n".join(H) return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
title=title,
content="\n".join(H),
)
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@ -813,11 +812,6 @@ def formsemestre_suivi_cohorte(
href="{burl}&percent=1">Afficher les résultats en pourcentages</a></p>""" href="{burl}&percent=1">Afficher les résultats en pourcentages</a></p>"""
H = [ H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=tab.page_title,
),
"""<h2 class="formsemestre">Suivi cohorte: devenir des étudiants de ce semestre</h2>""", """<h2 class="formsemestre">Suivi cohorte: devenir des étudiants de ce semestre</h2>""",
_gen_form_selectetuds( _gen_form_selectetuds(
formsemestre.id, formsemestre.id,
@ -853,9 +847,14 @@ def formsemestre_suivi_cohorte(
</p> </p>
""", """,
expl, expl,
html_sco_header.sco_footer(),
] ]
return "\n".join(H) return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
title=tab.page_title,
content="\n".join(H),
)
def _gen_form_selectetuds( def _gen_form_selectetuds(
@ -1365,15 +1364,11 @@ def formsemestre_suivi_cursus(
] ]
H = [ H = [
html_sco_header.sco_header(
page_title=tab.page_title,
),
"""<h2 class="formsemestre">Cursus suivis par les étudiants de ce semestre</h2>""", """<h2 class="formsemestre">Cursus suivis par les étudiants de ce semestre</h2>""",
"\n".join(F), "\n".join(F),
t, t,
html_sco_header.sco_footer(),
] ]
return "\n".join(H) return render_template("sco_page.j2", title=tab.page_title, content="\n".join(H))
# ------------- # -------------
@ -1742,12 +1737,6 @@ def formsemestre_graph_cursus(
) )
H = [ H = [
html_sco_header.sco_header(
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title="Graphe cursus de %(titreannee)s" % sem,
no_sidebar=True,
),
"""<h2 class="formsemestre">Cursus des étudiants de ce semestre</h2>""", """<h2 class="formsemestre">Cursus des étudiants de ce semestre</h2>""",
doc, doc,
f"<p>{len(etuds)} étudiants sélectionnés</p>", f"<p>{len(etuds)} étudiants sélectionnés</p>",
@ -1771,11 +1760,12 @@ def formsemestre_graph_cursus(
), ),
"""<p>Origine et devenir des étudiants inscrits dans %(titreannee)s""" """<p>Origine et devenir des étudiants inscrits dans %(titreannee)s"""
% sem, % sem,
"""(<a href="%s">version pdf</a>""" f"""(<a href="{
% url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw), url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw)
""", <a href="%s">image PNG</a>)""" }">version pdf</a>,
% url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw), <a href="{
f""" url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw)}">image PNG</a>)
</p> </p>
<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre <p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre
sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans
@ -1788,8 +1778,14 @@ def formsemestre_graph_cursus(
étudiants appartenant aux groupes indiqués <em>dans le semestre d'origine</em>. étudiants appartenant aux groupes indiqués <em>dans le semestre d'origine</em>.
</p> </p>
""", """,
html_sco_header.sco_footer(),
] ]
return "\n".join(H) return render_template(
"sco_page.j2",
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS,
page_title=f"Graphe cursus de {sem['titreannee']}",
no_sidebar=True,
content="\n".join(H),
)
else: else:
raise ValueError(f"invalid format: {fmt}") raise ValueError(f"invalid format: {fmt}")

View File

@ -31,21 +31,18 @@
from collections import defaultdict from collections import defaultdict
from flask import request from flask import render_template, request
from app import db from app import db
from app.but import jury_but from app.but import jury_but
from app.models import FormSemestre from app.models import FormSemestre
from app.models.formsemestre import FormSemestreInscription from app.models.formsemestre import FormSemestreInscription
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
import sco_version
# Titres, ordonnés # Titres, ordonnés
@ -107,13 +104,11 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
if fmt != "html": if fmt != "html":
return t return t
H = [ H = [
html_sco_header.sco_header(page_title=title),
t, t,
"""<p class="help"> """<p class="help">
</p>""", </p>""",
html_sco_header.sco_footer(),
] ]
return "\n".join(H) return render_template("sco_page.j2", title=title, content="\n".join(H))
def but_indicateurs_by_bac(formsemestre: FormSemestre) -> dict[str:dict]: def but_indicateurs_by_bac(formsemestre: FormSemestre) -> dict[str:dict]:

View File

@ -40,17 +40,15 @@ sem_set_list()
""" """
import flask import flask
from flask import g, url_for from flask import g, render_template, url_for
from app import db, log from app import db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_etape_apogee from app.scodoc import sco_etape_apogee
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_etape_bilan import EtapeBilan from app.scodoc.sco_etape_bilan import EtapeBilan
@ -412,7 +410,7 @@ def do_semset_delete(semset_id, dialog_confirmed=False):
s = SemSet(semset_id=semset_id) s = SemSet(semset_id=semset_id)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"<h2>Suppression de l'ensemble %(title)s ?</h2>" % s, f"<h2>Suppression de l'ensemble {s['title']} ?</h2>",
dest_url="", dest_url="",
parameters={"semset_id": semset_id}, parameters={"semset_id": semset_id},
cancel_url="semset_page", cancel_url="semset_page",
@ -421,14 +419,14 @@ def do_semset_delete(semset_id, dialog_confirmed=False):
return flask.redirect("semset_page") return flask.redirect("semset_page")
def edit_semset_set_title(id=None, value=None): def edit_semset_set_title(oid=None, value=None):
"""Change title of semset""" """Change title of semset"""
title = value.strip() title = value.strip()
if not id: if not oid:
raise ScoValueError("empty semset_id") raise ScoValueError("empty semset_id")
SemSet(semset_id=id) SemSet(semset_id=oid)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
semset_edit(cnx, {"semset_id": id, "title": title}) semset_edit(cnx, {"semset_id": oid, "title": title})
return title return title
@ -517,22 +515,18 @@ def semset_page(fmt="html"):
page_title = "Ensembles de semestres" page_title = "Ensembles de semestres"
H = [ H = [
html_sco_header.sco_header(
page_title=page_title,
javascripts=["libjs/jinplace-1.2.1.min.js"],
),
"""<script>$(function() { """<script>$(function() {
$('.inplace_edit').jinplace(); $('.inplace_edit').jinplace();
}); });
</script>""", </script>""",
"<h2>%s</h2>" % page_title, f"<h2>{page_title}</h2>",
] ]
H.append(tab.html()) H.append(tab.html())
annee_courante = int(scu.annee_scolaire()) annee_courante = int(scu.annee_scolaire())
menu_annee = "\n".join( menu_annee = "\n".join(
[ [
'<option value="%s">%s</option>' % (i, i) f"""<option value="{i}">{i}</option>"""
for i in range(2014, annee_courante + 1) for i in range(2014, annee_courante + 1)
] ]
) )
@ -561,8 +555,8 @@ def semset_page(fmt="html"):
H.append( H.append(
""" """
<div> <div class="scobox space-before-24">
<h4>Autres opérations:</h4> <div class="scobox-title">Autres opérations :</div>
<ul> <ul>
<li><a class="stdlink" href="scodoc_table_results"> <li><a class="stdlink" href="scodoc_table_results">
Table des résultats de tous les semestres Table des résultats de tous les semestres
@ -575,4 +569,9 @@ def semset_page(fmt="html"):
""" """
) )
return "\n".join(H) + html_sco_header.sco_footer() return render_template(
"sco_page_dept.j2",
title=page_title,
javascripts=["libjs/jinplace-1.2.1.min.js"],
content="\n".join(H),
)

View File

@ -49,7 +49,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_excel from app.scodoc import sco_excel
@ -90,12 +89,7 @@ def trombino(
return _listeappel_photos_pdf(groups_infos) return _listeappel_photos_pdf(groups_infos)
elif fmt == "doc": elif fmt == "doc":
return sco_trombino_doc.trombino_doc(groups_infos) return sco_trombino_doc.trombino_doc(groups_infos)
else: raise ValueError("invalid format")
raise Exception("invalid format")
def _trombino_html_header():
return html_sco_header.sco_header(javascripts=["js/trombino.js"])
def trombino_html(groups_infos): def trombino_html(groups_infos):
@ -251,14 +245,18 @@ def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
back_url = "groups_photos?" + str(groups_infos.groups_query_args) back_url = "groups_photos?" + str(groups_infos.groups_query_args)
portal_url = sco_portal_apogee.get_portal_url() portal_url = sco_portal_apogee.get_portal_url()
header = html_sco_header.sco_header(page_title="Chargement des photos")
footer = html_sco_header.sco_footer()
if not portal_url: if not portal_url:
return f"""{ header } return render_template(
"sco_page.j2",
content=f"""
<p>portail non configuré</p> <p>portail non configuré</p>
<p><a href="{back_url}" class="stdlink">Retour au trombinoscope</a></p> <div>
{ footer } <a class="stdlink" href="{back_url}" class="stdlink">
""" Retour au trombinoscope
</a>
</div>
""",
)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
f"""<h2>Copier les photos du portail vers ScoDoc ?</h2> f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
@ -285,14 +283,18 @@ def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
msg.append(f"<b>{nok} photos correctement chargées</b>") msg.append(f"<b>{nok} photos correctement chargées</b>")
return f"""{ header } return render_template(
"sco_page.j2",
content=f"""
<h2>Chargement des photos depuis le portail</h2> <h2>Chargement des photos depuis le portail</h2>
<ul><li> <ul><li>
{ '</li><li>'.join(msg) } { '</li><li>'.join(msg) }
</li></ul> </li></ul>
<p><a href="{back_url}">retour au trombinoscope</a> <div class="space-before-24">
{ footer } <a class="stdlink" href="{back_url}">retour au trombinoscope</a>
""" </div>
""",
)
def _get_etud_platypus_image(t, image_width=2 * cm): def _get_etud_platypus_image(t, image_width=2 * cm):
@ -506,7 +508,6 @@ def photos_import_files_form(group_ids=()):
back_url = f"groups_photos?{groups_infos.groups_query_args}" back_url = f"groups_photos?{groups_infos.groups_query_args}"
H = [ H = [
html_sco_header.sco_header(page_title="Import des photos des étudiants"),
f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2> f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche <p><b>Vous pouvez aussi charger les photos individuellement via la fiche
de chaque étudiant (menu "Étudiant" / "Changer la photo").</b> de chaque étudiant (menu "Étudiant" / "Changer la photo").</b>
@ -526,7 +527,6 @@ def photos_import_files_form(group_ids=()):
<li style="padding-top: 2em;"> <li style="padding-top: 2em;">
""", """,
] ]
F = html_sco_header.sco_footer()
vals = scu.get_request_args() vals = scu.get_request_args()
vals["group_ids"] = groups_infos.group_ids vals["group_ids"] = groups_infos.group_ids
tf = TrivialFormulator( tf = TrivialFormulator(
@ -540,10 +540,13 @@ def photos_import_files_form(group_ids=()):
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F return render_template(
elif tf[0] == -1: "sco_page.j2",
title="Import des photos des étudiants",
content="\n".join(H) + tf[1] + "</li></ol>",
)
if tf[0] == -1:
return flask.redirect(back_url) return flask.redirect(back_url)
else:
def callback(etud: Identite, data, filename): def callback(etud: Identite, data, filename):
return sco_photos.store_photo(etud, data, filename) return sco_photos.store_photo(etud, data, filename)

View File

@ -556,7 +556,7 @@ MONTH_NAMES = (
) )
DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche") DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
TIME_FMT = "%H:%M" # affichage des heures TIME_FMT = "%Hh%M" # affichage des heures
DATE_FMT = "%d/%m/%Y" # affichage des dates DATE_FMT = "%d/%m/%Y" # affichage des dates
DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
DATETIME_FMT = DATE_FMT + " " + TIME_FMT DATETIME_FMT = DATE_FMT + " " + TIME_FMT
@ -1339,7 +1339,7 @@ def format_telephone(n: str | None) -> str:
# #
def timedate_human_repr(): def timedate_human_repr():
"representation du temps courant pour utilisateur" "representation du temps courant pour utilisateur"
return time.strftime("%d/%m/%Y à %Hh%M") return time.strftime(DATEATIME_FMT)
def annee_scolaire_repr(year, month): def annee_scolaire_repr(year, month):
@ -1525,6 +1525,7 @@ def confirm_dialog(
help_msg=None, help_msg=None,
parameters: dict = None, parameters: dict = None,
target_variable="dialog_confirmed", target_variable="dialog_confirmed",
template="sco_page.j2",
): ):
"""HTML confirmation dialog: submit (POST) to same page or dest_url if given.""" """HTML confirmation dialog: submit (POST) to same page or dest_url if given."""
parameters = parameters or {} parameters = parameters or {}
@ -1565,7 +1566,7 @@ def confirm_dialog(
if help_msg: if help_msg:
H.append('<p class="help">' + help_msg + "</p>") H.append('<p class="help">' + help_msg + "</p>")
if add_headers: if add_headers:
return render_template("sco_page.j2", content="\n".join(H)) return render_template(template, content="\n".join(H))
else: else:
return "\n".join(H) return "\n".join(H)

View File

@ -55,11 +55,19 @@ div.container {
div.sco-app-content { div.sco-app-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-right: 8px; margin-right: 12px;
} }
.space-before-18 { .space-before-18 {
margin-top: 18px; margin-top: 18px !important;
}
.space-before-24 {
margin-top: 24px !important;
}
div.scobox.maxwidth {
max-width: none;
} }
div.scobox { div.scobox {
@ -748,14 +756,14 @@ div.fiche_etud {
/* rgb(255,240,128); */ /* rgb(255,240,128); */
border: 1px solid gray; border: 1px solid gray;
width: 910px; width: 910px;
padding: 10px; padding: 24px 10px 10px 10px;
margin-top: 10px; margin-top: 10px;
} }
div.menus_etud { div.menus_etud {
position: absolute; position: absolute;
margin-left: 1px; margin-left: 10px;
margin-top: 1px; margin-top: 16px;
} }
div.fiche_etud h2 { div.fiche_etud h2 {
@ -2059,6 +2067,10 @@ ul.ue_inscr_list li.etud {
text-underline-offset: 3px; text-underline-offset: 3px;
} }
.sem-groups-autres-liens {
background-color: var(--sco-color-box-bg);
}
.sem-groups-list .stdlink, .sem-groups-list .stdlink,
.sem-groups-list .stdlink:visited { .sem-groups-list .stdlink:visited {
color: rgb(0, 0, 192); color: rgb(0, 0, 192);

View File

@ -1,10 +0,0 @@
// Affichage progressif du trombinoscope html
$().ready(function () {
var spans = $(".unloaded_img");
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);
}
});

View File

@ -45,7 +45,7 @@
<h2 style="color: crimson;">{{titre_form}}</h2> <h2 style="color: crimson;">{{titre_form}}</h2>
<div class="scobox"> <div class="scobox">
<div id="excel-content"> <div id="excel-content">
<p class="hint">Avertissement : le fichier doit respecter le format suivant</p> <p class="hint">Le fichier importé doit respecter le format suivant</p>
<ul> <ul>
<li> <li>
@ -53,12 +53,12 @@
</li> </li>
<li class="star">colonne B : Date de début</li> <li class="star">colonne B : Date de début</li>
<li class="star">colonne C : Date de fin</li> <li class="star">colonne C : Date de fin</li>
<li class="opt">colonne D : État (ABS,RET,PRE, ABS si vide)</li> <li class="opt">colonne D : État (ABS, RET, PRE), considéré ABSent si vide</li>
<li class="opt">colonne E : code du module</li> <li class="opt">colonne E : code du module</li>
</ul> </ul>
<p class="hint"><span class="opt"></span> : Colonne optionnelle, les cases peuvent être vides</p> <p class="hint"><span class="opt"></span> : Colonne optionnelle, les cases peuvent être vides</p>
<p class="hint"><span class="star"></span> : Formats autorisés : <p class="hint"><span class="star"></span> : Formats de dates autorisés :
<ul> <ul>
<li> <li>
<pre>aaaa-mm-jjThh:mm:ss</pre> <pre>aaaa-mm-jjThh:mm:ss</pre>
@ -73,21 +73,23 @@
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
<label for="type_identifiant">Type d'identifiant d'étudiant (etudid, ine, nip)</label> <label for="type_identifiant">Type d'identifiant d'étudiant (etudid, INE, NIP)</label>
<select name="type_identifiant" id="type_identifiant"> <select name="type_identifiant" id="type_identifiant">
<option value="etudid">ETUDID</option> <option value="etudid">etudid</option>
<option value="ine">INE</option> <option value="ine">INE</option>
<option value="nip">NIP</option> <option value="nip">NIP</option>
</select> </select>
<br> <div class="space-before-18">
<label for="file"> <label for="file">
Sélectionnez un fichier: Sélectionnez un fichier:
</label> </label>
<input id="file" type="file" name="file" <input id="file" type="file" name="file"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" /> accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
<br> </div>
<div class="space-before-18">
<button type="submit">Importer</button> <button type="submit">Importer</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
<br /> <br />
Ces utilisateurs peuvent être réactivés à tout moment. Ces utilisateurs peuvent être réactivés à tout moment.
</div> </div>
<div class="row"> <div class="row space-before-24">
<div class="col-md-4"> <div class="col-md-4">
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }} {{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div> </div>

View File

@ -27,7 +27,7 @@
</span> </span>
</div> </div>
<div> <div>
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em> <em>établi le {{time.strftime(scu.DATEATIME_FMT)}} (notes sur 20)</em>
<span class="rightjust"> <span class="rightjust">
<select name="version" onchange="self.location.href='{{ <select name="version" onchange="self.location.href='{{
url_for('notes.formsemestre_bulletinetud', url_for('notes.formsemestre_bulletinetud',

View File

@ -158,7 +158,7 @@
</div> </div>
<div id="footer"> <div id="footer">
Bulletin généré par ScoDoc le {{time.strftime("%d/%m/%Y à %Hh%M")}}. Explication des codes sur Bulletin généré par ScoDoc le {{time.strftime(scu.DATEATIME_FMT)}}. Explication des codes sur
<a href="https://scodoc.org/BUTCodesJury">https://scodoc.org/BUTCodesJury</a> <a href="https://scodoc.org/BUTCodesJury">https://scodoc.org/BUTCodesJury</a>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
{% extends "sco_page.j2" %} {% extends "sco_page_dept.j2" %}
{% block styles %} {% block styles %}
{{super()}} {{super()}}

View File

@ -1,5 +1,5 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends "base.j2" %} {% extends "sco_page_dept.j2" %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
{% block app_content %} {% block app_content %}

View File

@ -1,5 +1,5 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends "sco_page.j2" %} {% extends "sco_page_dept.j2" %}
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" /> <link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />

View File

@ -1,5 +1,5 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends "sco_page.j2" %} {% extends "sco_page_dept.j2" %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
{% block app_content %} {% block app_content %}

View File

@ -51,7 +51,7 @@ une formation utilisant une autre version de référentiel, pensez à revalider
pour l'étudiant{{etud.ne}} {{etud.html_link_fiche()|safe}} pour l'étudiant{{etud.ne}} {{etud.html_link_fiche()|safe}}
<ul> <ul>
<li>DUT 120 spécialité {{formsemestre.formation.referentiel_competence.specialite_long}} <li>DUT 120 spécialité {{formsemestre.formation.referentiel_competence.specialite_long}}
enregistré le {{time.strftime("%d/%m/%Y à %Hh%M")}} enregistré le {{time.strftime(scu.DATEATIME_FMT)}}
</li> </li>
</ul> </ul>

View File

@ -1,5 +1,5 @@
{# Association d'ECTS à une UE par parcours #} {# Association d'ECTS à une UE par parcours #}
{% extends "sco_page.j2" %} {% extends "sco_page_dept.j2" %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
{% block styles %} {% block styles %}
@ -23,7 +23,7 @@
<form method="POST"> <form method="POST">
{% for field in form %} {% for field in form %}
{% if field.name != 'csrf_token' %} {% if field.name != 'csrf_token' %}
<div> <div class="space-before-18">
<label for="{{ field.id }}">{{ field.label }}</label> <label for="{{ field.id }}">{{ field.label }}</label>
{{ field }} {{ field }}
{% for error in field.errors %} {% for error in field.errors %}
@ -33,8 +33,10 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="space-before-24">
<input type="submit" name="submit" value="Enregistrer"> <input type="submit" name="submit" value="Enregistrer">
<input type="submit" name="cancel" value="Annuler"> <input type="submit" name="cancel" value="Annuler">
</div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -20,6 +20,7 @@
<div class="tab-content"> <div class="tab-content">
<h2>{{ title }}</h2> <h2>{{ title }}</h2>
<div class="scobox">
<p class="help"> <p class="help">
{{ explanation|safe }} {{ explanation|safe }}
</p> </p>
@ -47,4 +48,5 @@
</div> </div>
</form> </form>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,26 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends "sco_page_dept.j2" %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/table_editor.css">
{% endblock %}
{% block app_content %}
<h2>Formation {{formation.titre}} ({{formation.acronyme}})
[version {{formation.version}}] code {{formation.formation_code}}
{% if read_only %}
{{scu.icontag("lock32_img", title="verrouillé")|safe}}
{% endif %}
</h2>
{% if read_only %}
<span class="warning">
Formation verrouilée car un ou plusieurs semestres verrouillés l'utilisent.
</span>
{% endif %}
<h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2> <h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
<div class="help"> <div class="help">
{% if not read_only %} {% if not read_only %}
@ -59,8 +81,11 @@
<div class="champs_coef_hors_parcours champs_legende"></div> <div class="champs_coef_hors_parcours champs_legende"></div>
<div class="help">module non associé au parcours de cette UE, le coef devrait être nul.</div> <div class="help">module non associé au parcours de cette UE, le coef devrait être nul.</div>
</div> </div>
{% endblock %}
{% block scripts %}
{{super()}}
<script src="{{scu.STATIC_DIR}}/js/table_editor.js"></script>
<script> <script>
var read_only = {{ "true" if read_only else "false"}}; var read_only = {{ "true" if read_only else "false"}};
$(function () { $(function () {
@ -113,3 +138,4 @@
return true; return true;
} }
</script> </script>
{% endblock %}

View File

@ -1,3 +1,5 @@
{% extends 'sco_page.j2' %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
{% macro render_field(field) %} {% macro render_field(field) %}
@ -15,6 +17,12 @@
</tr> </tr>
{% endmacro %} {% endmacro %}
{% block app_content %}
{{ evaluations_description|safe}}
<h3 class="space-before-24">Placement et émargement des étudiants</h3>
<div class="saisienote_etape1 form_placement"> <div class="saisienote_etape1 form_placement">
<form method=post> <form method=post>
{{ form.evaluation_id }} {{ form.evaluation_id }}
@ -78,3 +86,4 @@
</li> </li>
</ul> </ul>
</div> </div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends 'base.j2' %} {% extends 'sco_page.j2' %}
{% block app_content %} {% block app_content %}

View File

@ -24,7 +24,8 @@
############################################################################## ##############################################################################
""" """
Module absences: remplacé par assiduité en août 2023, reste ici seulement la gestion des "billets" Module absences: remplacé par assiduité en août 2023,
reste ici seulement la gestion des "billets"
""" """
@ -32,8 +33,7 @@ import dateutil
import dateutil.parser import dateutil.parser
import flask import flask
from flask import g, request from flask import abort, flash, g, render_template, request, url_for
from flask import abort, flash, url_for
from flask_login import current_user from flask_login import current_user
from app import db, log from app import db, log
@ -54,11 +54,8 @@ from app.scodoc import sco_utils as scu
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_assiduites as scass from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_abs_billets from app.scodoc import sco_abs_billets
from app.scodoc import sco_etud
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -77,11 +74,7 @@ from app.scodoc import sco_preferences
def index_html(): def index_html():
"""Gestionnaire absences, page principale""" """Gestionnaire absences, page principale"""
H = [ H = []
html_sco_header.sco_header(
page_title="Billets d'absences",
),
]
if current_user.has_permission( if current_user.has_permission(
Permission.AbsChange Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"): ) and sco_preferences.get_preference("handle_billets_abs"):
@ -93,8 +86,9 @@ def index_html():
</li></ul> </li></ul>
""" """
) )
H.append(html_sco_header.sco_footer()) return render_template(
return "\n".join(H) "sco_page_dept.j2", title="Billets d'absences", content="\n".join(H)
)
# ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
@ -158,12 +152,8 @@ def add_billets_absence_form(etudid):
"""Formulaire ajout billet (pour tests seulement, le vrai """Formulaire ajout billet (pour tests seulement, le vrai
formulaire accessible aux etudiants étant sur le portail étudiant). formulaire accessible aux etudiants étant sur le portail étudiant).
""" """
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] _ = Identite.get_etud(etudid) # check
H = [ H = ["""<h2>Formulaire ajout billet (pour tests)</h2>"""]
html_sco_header.sco_header(
page_title="Billet d'absence de %s" % etud["nomprenom"]
)
]
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -179,7 +169,11 @@ def add_billets_absence_form(etudid):
), ),
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return render_template(
"sco_page_dept.j2",
title="""Billet d'absence de {etud["nomprenom"]}""",
content="\n".join(H) + tf[1],
)
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
@ -242,11 +236,8 @@ def list_billets():
et formulaire recherche d'un billet. et formulaire recherche d'un billet.
""" """
table = sco_abs_billets.table_billets_etud(etat=False) table = sco_abs_billets.table_billets_etud(etat=False)
T = table.html() table_html = table.html()
H = [ H = [
html_sco_header.sco_header(
page_title="Billet d'absence non traités",
),
f"<h2>Billets d'absence en attente de traitement ({table.get_nb_rows()})</h2>", f"<h2>Billets d'absence en attente de traitement ({table.get_nb_rows()})</h2>",
] ]
@ -257,8 +248,11 @@ def list_billets():
submitbutton=False, submitbutton=False,
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + T + html_sco_header.sco_footer() return render_template(
else: "sco_page.j2",
title="Billet d'absence non traités",
content="\n".join(H) + tf[1] + table_html,
)
return flask.redirect( return flask.redirect(
url_for( url_for(
"absences.process_billet_absence_form", "absences.process_billet_absence_form",
@ -337,7 +331,8 @@ def _ProcessBilletAbsence(
def process_billet_absence_form(billet_id: int): def process_billet_absence_form(billet_id: int):
"""Formulaire traitement d'un billet""" """Formulaire traitement d'un billet"""
if not isinstance(billet_id, int): if not isinstance(billet_id, int):
raise abort(404, "billet_id invalide") abort(404, "billet_id invalide")
return # safety guard
billet: BilletAbsence = ( billet: BilletAbsence = (
BilletAbsence.query.filter_by(id=billet_id) BilletAbsence.query.filter_by(id=billet_id)
.join(Identite) .join(Identite)
@ -352,9 +347,6 @@ def process_billet_absence_form(billet_id: int):
etud = billet.etudiant etud = billet.etudiant
H = [ H = [
html_sco_header.sco_header(
page_title=f"Traitement billet d'absence de {etud.nomprenom}",
),
f"""<h2>Traitement du billet {billet.id} : <a class="discretelink" href="{ f"""<h2>Traitement du billet {billet.id} : <a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.nomprenom}</a></h2> }">{etud.nomprenom}</a></h2>
@ -403,7 +395,11 @@ def process_billet_absence_form(billet_id: int):
</p> </p>
""" """
return "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer() return render_template(
"sco_page.j2",
title=f"Traitement billet d'absence de {etud.nomprenom}",
content="\n".join(H) + "<br>" + tf[1] + F,
)
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
@ -414,7 +410,7 @@ def process_billet_absence_form(billet_id: int):
j = "non justifiées" j = "non justifiées"
H.append('<div class="head_message">') H.append('<div class="head_message">')
if n > 0: if n > 0:
H.append("%d absences (1/2 journées) %s ajoutées" % (n, j)) H.append(f"{n} absences (1/2 journées) {j} ajoutées")
elif n == 0: elif n == 0:
H.append("Aucun jour d'absence dans les dates indiquées !") H.append("Aucun jour d'absence dans les dates indiquées !")
elif n < 0: elif n < 0:
@ -434,4 +430,8 @@ def process_billet_absence_form(billet_id: int):
) )
tab = sco_abs_billets.table_billets(billets, etud=etud) tab = sco_abs_billets.table_billets(billets, etud=etud)
H.append(tab.html()) H.append(tab.html())
return "\n".join(H) + html_sco_header.sco_footer() return render_template(
"sco_page.j2",
title=f"Traitement billet d'absence de {etud.nomprenom}",
content="\n".join(H),
)

View File

@ -1073,10 +1073,10 @@ def signal_assiduites_group():
select_all_when_unspecified=True, select_all_when_unspecified=True,
) )
if not groups_infos.members: if not groups_infos.members:
return ( return render_template(
html_sco_header.sco_header(page_title="Saisie de l'assiduité") "sco_page.j2",
+ "<h3>Aucun étudiant ! </h3>" title="Saisie de l'assiduité",
+ html_sco_header.sco_footer() content="<h3>Aucun étudiant !</h3>",
) )
# --- Filtrage par formsemestre --- # --- Filtrage par formsemestre ---
@ -1952,10 +1952,10 @@ def signal_assiduites_hebdo():
group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True
) )
if not groups_infos.members: if not groups_infos.members:
return ( return render_template(
html_sco_header.sco_header(page_title="Assiduité: saisie hebdomadaire") "sco_page.j2",
+ "<h3>Aucun étudiant ! </h3>" title="Assiduité: feuille saisie hebdomadaire",
+ html_sco_header.sco_footer() content="<h3>Aucun étudiant !</h3>",
) )
# Récupération des étudiants # Récupération des étudiants
@ -2305,12 +2305,10 @@ def feuille_abs_hebdo():
group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True
) )
if not groups_infos.members: if not groups_infos.members:
return ( return render_template(
html_sco_header.sco_header( "sco_page.j2",
page_title="Assiduité: feuille saisie hebdomadaire" title="Assiduité: feuille saisie hebdomadaire",
) content="<h3>Aucun étudiant !</h3>",
+ "<h3>Aucun étudiant ! </h3>"
+ html_sco_header.sco_footer()
) )
# Gestion des jours # Gestion des jours

View File

@ -60,7 +60,6 @@ from app.models import (
ScoDocSiteConfig, ScoDocSiteConfig,
) )
from app.scodoc import ( from app.scodoc import (
html_sco_header,
sco_bulletins_json, sco_bulletins_json,
sco_cache, sco_cache,
sco_formsemestre_exterieurs, sco_formsemestre_exterieurs,
@ -251,25 +250,21 @@ def formsemestre_validation_but(
</div> </div>
""" """
H = [ H = ["""<div class="jury_but">"""]
html_sco_header.sco_header( inscription = formsemestre.etuds_inscriptions.get(etudid)
page_title=f"Validation BUT S{formsemestre.semestre_id}", if not inscription:
formsemestre_id=formsemestre_id, raise ScoValueError("étudiant non inscrit au semestre")
etudid=etudid, if inscription.etat != scu.INSCRIT:
return render_template(
"sco_page.j2",
title=f"Validation BUT S{formsemestre.semestre_id}",
sco=ScoData(etud=etud, formsemestre=formsemestre),
cssstyles=[ cssstyles=[
"css/jury_but.css", "css/jury_but.css",
"css/cursus_but.css", "css/cursus_but.css",
], ],
javascripts=("js/jury_but.js",), javascripts=("js/jury_but.js",),
), content=(
"""<div class="jury_but">
""",
]
inscription = formsemestre.etuds_inscriptions.get(etudid)
if not inscription:
raise ScoValueError("étudiant non inscrit au semestre")
if inscription.etat != scu.INSCRIT:
return (
"\n".join(H) "\n".join(H)
+ f""" + f"""
<div> <div>
@ -291,7 +286,7 @@ def formsemestre_validation_but(
{navigation_div} {navigation_div}
</div> </div>
""" """
+ html_sco_header.sco_footer() ),
) )
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
@ -341,7 +336,7 @@ def formsemestre_validation_but(
êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;? êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
</div>""" </div>"""
if read_only: if read_only:
warning += f"""<div class="warning">Affichage en lecture seule</div>""" warning += """<div class="warning">Affichage en lecture seule</div>"""
if deca.formsemestre_impair: if deca.formsemestre_impair:
inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
@ -507,7 +502,17 @@ def formsemestre_validation_but(
</div> </div>
""" """
) )
return "\n".join(H) + html_sco_header.sco_footer() return render_template(
"sco_page.j2",
title=f"Validation BUT S{formsemestre.semestre_id}",
sco=ScoData(etud=etud, formsemestre=formsemestre),
cssstyles=[
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=("js/jury_but.js",),
content="\n".join(H),
)
@bp.route( @bp.route(

View File

@ -88,7 +88,6 @@ from app.scodoc.sco_exceptions import (
ScoInvalidIdType, ScoInvalidIdType,
) )
from app.scodoc import ( from app.scodoc import (
html_sco_header,
sco_apogee_compare, sco_apogee_compare,
sco_archives_formsemestre, sco_archives_formsemestre,
sco_assiduites, sco_assiduites,
@ -770,8 +769,10 @@ def formation_import_xml_form():
cancelbutton="Annuler", cancelbutton="Annuler",
) )
if tf[0] == 0: if tf[0] == 0:
return f""" return render_template(
{ html_sco_header.sco_header(page_title="Import d'une formation") } "sco_page_dept.j2",
title="Import d'une formation",
content=f"""
<h2>Import d'une formation</h2> <h2>Import d'une formation</h2>
<p>Création d'une formation (avec UE, matières, modules) <p>Création d'une formation (avec UE, matières, modules)
à partir un fichier XML (réservé aux utilisateurs avertis). à partir un fichier XML (réservé aux utilisateurs avertis).
@ -783,8 +784,8 @@ def formation_import_xml_form():
}">page des référentiels</a>). }">page des référentiels</a>).
</p> </p>
{ tf[1] } { tf[1] }
{ html_sco_header.sco_footer() } """,
""" )
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept)) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
@ -792,8 +793,10 @@ def formation_import_xml_form():
tf[2]["xmlfile"].read() tf[2]["xmlfile"].read()
) )
return f""" return render_template(
{ html_sco_header.sco_header(page_title="Import d'une formation") } "sco_page_dept.j2",
title="Import d'une formation",
content=f"""
<h2>Import effectué !</h2> <h2>Import effectué !</h2>
<ul> <ul>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
@ -806,8 +809,8 @@ def formation_import_xml_form():
(en cas d'erreur, par exemple pour charger auparavant le référentiel de compétences) (en cas d'erreur, par exemple pour charger auparavant le référentiel de compétences)
</li> </li>
</ul> </ul>
{ html_sco_header.sco_footer() } """,
""" )
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation) sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation)
@ -2182,15 +2185,17 @@ def formsemestre_bulletins_mailetuds(
if sent: if sent:
nb_sent += 1 nb_sent += 1
# #
return f""" return render_template(
{html_sco_header.sco_header()} "sco_page.j2",
title="Mailing bulletins",
content=f"""
<p>{nb_sent} bulletins sur {len(inscriptions)} envoyés par mail !</p> <p>{nb_sent} bulletins sur {len(inscriptions)} envoyés par mail !</p>
<p><a class="stdlink" href="{url_for('notes.formsemestre_status', <p><a class="stdlink" href="{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id) formsemestre_id=formsemestre_id)
}">continuer</a></p> }">continuer</a></p>
{html_sco_header.sco_footer()} """,
""" )
sco_publish( sco_publish(
@ -2258,10 +2263,8 @@ def appreciation_add_form(
else: else:
action = "Ajout" action = "Ajout"
H = [ H = [
html_sco_header.sco_header(),
f"""<h2>{action} d'une appréciation sur {etud.nomprenom}</h2>""", f"""<h2>{action} d'une appréciation sur {etud.nomprenom}</h2>""",
] ]
F = html_sco_header.sco_footer()
descr = [ descr = [
("edit", {"input_type": "hidden", "default": edit}), ("edit", {"input_type": "hidden", "default": edit}),
("etudid", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}),
@ -2286,7 +2289,7 @@ def appreciation_add_form(
submitlabel="Ajouter appréciation", submitlabel="Ajouter appréciation",
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + F return render_template("sco_page.j2", content="\n".join(H) + "\n" + tf[1])
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(bul_url) return flask.redirect(bul_url)
else: else:
@ -2643,7 +2646,7 @@ def check_form_integrity(formation_id, fix=False):
else: else:
txth = "OK" txth = "OK"
log("ok") log("ok")
return html_sco_header.sco_header() + txth + html_sco_header.sco_footer() return render_template("sco_page.j2", content=txth)
@bp.route("/check_formsemestre_integrity") @bp.route("/check_formsemestre_integrity")
@ -2681,6 +2684,4 @@ def check_formsemestre_integrity(formsemestre_id):
else: else:
diag = ["OK"] diag = ["OK"]
log("ok") log("ok")
return ( return render_template("sco_page.j2", content="<br>".join(diag))
html_sco_header.sco_header() + "<br>".join(diag) + html_sco_header.sco_footer()
)

View File

@ -31,24 +31,18 @@ PN / Edition des coefs
Emmanuel Viennet, 2021 Emmanuel Viennet, 2021
""" """
from flask import url_for from flask import g, render_template, request, url_for
from flask import g, request
from flask_json import as_json from flask_json import as_json
from flask_login import current_user from flask_login import current_user
from flask.templating import render_template
from app.scodoc.codes_cursus import UE_SPORT
from app import db, models from app import db, models
from app.comp import moy_ue from app.comp import moy_ue
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import ApcParcours, Formation, Module from app.models import ApcParcours, Formation, Module
from app.views import notes_bp as bp from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc import html_sco_header
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp
@bp.route("/table_modules_ue_coefs/<int:formation_id>") @bp.route("/table_modules_ue_coefs/<int:formation_id>")
@ -197,31 +191,7 @@ def edit_modules_ue_coefs():
formation_id=formation_id formation_id=formation_id
).first_or_404() ).first_or_404()
locked = formation.has_locked_sems(semestre_idx) locked = formation.has_locked_sems(semestre_idx)
if locked: return render_template(
lockicon = scu.icontag("lock32_img", title="verrouillé")
else:
lockicon = ""
H = [
html_sco_header.sco_header(
cssstyles=["css/table_editor.css"],
javascripts=[
"js/table_editor.js",
],
page_title=f"Coefs programme {formation.acronyme}",
),
f"""<h2>Formation {formation.titre} ({formation.acronyme})
[version {formation.version}] code {formation.formation_code}
{lockicon}
</h2>
""",
(
"""<span class="warning">Formation verrouilée car un ou plusieurs
semestres verrouillés l'utilisent.
</span>"""
if locked
else ""
),
render_template(
"pn/form_modules_ue_coefs.j2", "pn/form_modules_ue_coefs.j2",
formation=formation, formation=formation,
data_source=url_for( data_source=url_for(
@ -235,13 +205,9 @@ def edit_modules_ue_coefs():
"notes.set_module_ue_coef", "notes.set_module_ue_coef",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
), ),
read_only=locked read_only=locked or not current_user.has_permission(Permission.EditFormation),
or not current_user.has_permission(Permission.EditFormation),
semestre_idx=semestre_idx, semestre_idx=semestre_idx,
semestre_ids=range(1, formation.get_cursus().NB_SEM + 1), semestre_ids=range(1, formation.get_cursus().NB_SEM + 1),
parcours_id=parcours_id, parcours_id=parcours_id,
), title=f"Coefs programme {formation.acronyme}",
html_sco_header.sco_footer(), )
]
return "\n".join(H)

View File

@ -64,7 +64,7 @@ from app.decorators import (
permission_required, permission_required,
) )
from app.scodoc import html_sco_header, sco_import_users, sco_roles_default from app.scodoc import sco_import_users, sco_roles_default
from app.scodoc import sco_users from app.scodoc import sco_users
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml from app.scodoc import sco_xml
@ -74,7 +74,6 @@ from app.scodoc.sco_import_users import generate_password
from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.views import users_bp as bp from app.views import users_bp as bp
from app.views import scodoc_bp
_ = lambda x: x # sans babel _ = lambda x: x # sans babel
@ -92,7 +91,7 @@ class ChangePasswordForm(FlaskForm):
validators=[ validators=[
EqualTo( EqualTo(
"new_password", "new_password",
message="Les deux saisies sont " "différentes, recommencez", message="Les deux saisies sont différentes, recommencez",
), ),
], ],
) )
@ -106,9 +105,9 @@ class ChangePasswordForm(FlaskForm):
submit = SubmitField() submit = SubmitField()
cancel = SubmitField("Annuler") cancel = SubmitField("Annuler")
def validate_email(self, email): def validate_email(self, e_mail):
"vérifie que le mail est unique" "vérifie que le mail est unique (inline wtf validator)"
user = User.query.filter_by(email=email.data.strip()).first() user = User.query.filter_by(email=e_mail.data.strip()).first()
if user is not None and self.user_name.data != user.user_name: if user is not None and self.user_name.data != user.user_name:
raise ValidationError( raise ValidationError(
_("Cette adresse e-mail est déjà attribuée à un autre compte") _("Cette adresse e-mail est déjà attribuée à un autre compte")
@ -120,6 +119,7 @@ class ChangePasswordForm(FlaskForm):
raise ValidationError("Mot de passe trop simple, recommencez") raise ValidationError("Mot de passe trop simple, recommencez")
def validate_old_password(self, old_password): def validate_old_password(self, old_password):
"vérifie password actuel"
if not current_user.check_password(old_password.data): if not current_user.check_password(old_password.data):
raise ValidationError("Mot de passe actuel incorrect, ré-essayez") raise ValidationError("Mot de passe actuel incorrect, ré-essayez")
@ -855,9 +855,7 @@ def import_users_generate_excel_sample():
@scodoc7func @scodoc7func
def import_users_form(): def import_users_form():
"""Import utilisateurs depuis feuille Excel""" """Import utilisateurs depuis feuille Excel"""
head = html_sco_header.sco_header(page_title="Import utilisateurs")
H = [ H = [
head,
"""<h2>Téléchargement d'une liste d'utilisateurs</h2> """<h2>Téléchargement d'une liste d'utilisateurs</h2>
<p style="color: red">A utiliser pour importer de <b>nouveaux</b> <p style="color: red">A utiliser pour importer de <b>nouveaux</b>
utilisateurs (enseignants ou secrétaires) utilisateurs (enseignants ou secrétaires)
@ -886,12 +884,10 @@ def import_users_form():
<li><b>Étape 1: </b><a class="stdlink" href="{ <li><b>Étape 1: </b><a class="stdlink" href="{
url_for("users.import_users_generate_excel_sample", scodoc_dept=g.scodoc_dept) url_for("users.import_users_generate_excel_sample", scodoc_dept=g.scodoc_dept)
}">Obtenir la feuille excel vide à remplir</a> }">Obtenir la feuille excel vide à remplir</a>
ou bien la liste complète des utilisateurs.
</li> </li>
<li><b> Étape 2:</b> <li><b> Étape 2:</b>
""" """
) )
F = html_sco_header.sco_footer()
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -915,7 +911,11 @@ def import_users_form():
submitlabel="Télécharger", submitlabel="Télécharger",
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ul>" + help_msg + F return render_template(
"sco_page_dept.j2",
title="Import utilisateurs",
content="\n".join(H) + tf[1] + "</li></ul>" + help_msg,
)
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept)) return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept))
@ -923,8 +923,10 @@ def import_users_form():
ok, diags, nb_created = sco_import_users.import_excel_file( ok, diags, nb_created = sco_import_users.import_excel_file(
tf[2]["xlsfile"], tf[2]["force"] tf[2]["xlsfile"], tf[2]["force"]
) )
H = [html_sco_header.sco_header(page_title="Import utilisateurs")] H = [
H.append("<ul>") """<h2>Téléchargement d'une liste d'utilisateurs</h2>
<ul>"""
]
for diag in diags: for diag in diags:
H.append(f"<li>{diag}</li>") H.append(f"<li>{diag}</li>")
H.append("</ul>") H.append("</ul>")
@ -942,7 +944,9 @@ def import_users_form():
<p><a class="stdlink" href="{dest_url}">Continuer</a></p> <p><a class="stdlink" href="{dest_url}">Continuer</a></p>
""" """
) )
return "\n".join(H) + html_sco_header.sco_footer() return render_template(
"sco_page_dept.j2", title="Import utilisateurs", content="\n".join(H)
)
@bp.route("/user_info_page") @bp.route("/user_info_page")
@ -1061,76 +1065,6 @@ def form_change_password(user_name=None):
) )
@bp.route("/change_password", methods=["POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def change_password(user_name, password, password2):
"Change the password for user given by user_name"
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
u = User.query.filter_by(user_name=user_name).first()
# Check access permission
if not can_handle_passwd(u):
# access denied
log(
f"change_password: access denied (authuser={current_user}, user_name={user_name})"
)
raise AccessDenied("vous n'avez pas la permission de changer ce mot de passe")
H = []
F = html_sco_header.sco_footer()
# check password
dest_url = url_for(
"users.form_change_password", scodoc_dept=g.scodoc_dept, user_name=user_name
)
if password != password2:
H.append(
f"""<p>Les deux mots de passes saisis sont différents !</p>
<p><a href="{dest_url}" class="stdlink">Recommencer</a></p>
"""
)
else:
if not is_valid_password(password):
H.append(
f"""<p><b>ce mot de passe n'est pas assez compliqué !</b>
<br>(oui, il faut un mot de passe vraiment compliqué !)
</p>
<p><a href="{dest_url}" class="stdlink">Recommencer</a></p>
"""
)
else:
# ok, strong password
db.session.add(u)
u.set_password(password)
db.session.commit()
#
# ici page simplifiee car on peut ne plus avoir
# le droit d'acceder aux feuilles de style
return f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
<!DOCTYPE html>
<html>
<head>
<title>Mot de passe changé</title>
<meta http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}" />
<body>
<h1>Mot de passe changé !</h1>
<h2>Changement effectué</h2>
<p>Ne notez pas ce mot de passe, mais mémorisez le !</p>
<p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à
un tiers, même si c'est un collègue de confiance !</p>
<p><b>Si vous n'êtes pas administrateur, le système va vous redemander
votre login et nouveau mot de passe au prochain accès.</b>
</p>
<a href="{
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="stdlink">Continuer</a>
</body>
</html>
"""
return html_sco_header.sco_header() + "\n".join(H) + F
@bp.route("/toggle_active_user/<user_name>", methods=["GET", "POST"]) @bp.route("/toggle_active_user/<user_name>", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.UsersAdmin) @permission_required(Permission.UsersAdmin)

View File

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

View File

@ -18,7 +18,7 @@ umask 0022
export SCODOC_DIR=/opt/scodoc export SCODOC_DIR=/opt/scodoc
export SCODOC_VAR_DIR=/opt/scodoc-data export SCODOC_VAR_DIR=/opt/scodoc-data
export SCODOC_INSTALL_STATUS_FILE="${SCODOC_VAR_DIR}/.install_status"
export SCODOC_VERSION_DIR="${SCODOC_VAR_DIR}/config/version" export SCODOC_VERSION_DIR="${SCODOC_VAR_DIR}/config/version"
export SCODOC_LOGOS_DIR="${SCODOC_VAR_DIR}/config/logos" export SCODOC_LOGOS_DIR="${SCODOC_VAR_DIR}/config/logos"

View File

@ -3,11 +3,17 @@
# Post-installation de scodoc # Post-installation de scodoc
# ici, le répertoire /opt/scodoc vient d'être installé # ici, le répertoire /opt/scodoc vient d'être installé
set -euo pipefail
cd /opt/scodoc || (echo "Error chdir to /opt/scodoc"; exit 1) cd /opt/scodoc || (echo "Error chdir to /opt/scodoc"; exit 1)
# On peut donc charger la config: # On peut donc charger la config:
source /opt/scodoc/tools/config.sh source /opt/scodoc/tools/config.sh || (echo "Error loading /opt/scodoc/tools/config.sh"; exit 1)
source /opt/scodoc/tools/utils.sh source /opt/scodoc/tools/utils.sh || (echo "Error loading /opt/scodoc/tools/utils.sh"; exit 1)
# Enleve fichier de statut d'installation s'il existe
# (la présence de ce fichier indique que l'install s'est bien passée)
remove_install_status
# -- Création au besoin de notre utilisateur # -- Création au besoin de notre utilisateur
# adduser --system "${SCODOC_USER}" # adduser --system "${SCODOC_USER}"
@ -134,3 +140,6 @@ systemctl enable scodoc9
# --- RESTART SCODOC # --- RESTART SCODOC
systemctl restart scodoc9 systemctl restart scodoc9
# On est arrivé au bout du post-install !
create_install_status

19
tools/debian/prerm Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
# Pre-removal script for scodoc
# This script is called by dpkg before removing the package
cd /opt/scodoc || (echo "Error chdir to /opt/scodoc"; exit 1)
# On peut donc charger la config:
source /opt/scodoc/tools/config.sh || (echo "Error loading /opt/scodoc/tools/config.sh"; exit 1)
source /opt/scodoc/tools/utils.sh || (echo "Error loading /opt/scodoc/tools/utils.sh"; exit 1)
check_uid_root
remove_install_status

View File

@ -6,10 +6,11 @@
# #
# Upgrade also the Linux system using apt. # Upgrade also the Linux system using apt.
# #
# Script for ScoDoc 9 # Script for ScoDoc 9. Lancé automatiquement par scodoc-updater.
# #
# E. Viennet, sep 2013, mar 2017, jun 2019, aug 2020, dec 2020, aug 21 # E. Viennet, sep 2013, mar 2017, jun 2019, aug 2020, dec 2020, aug 21
set -euo pipefail
# Le répertoire de ce script: # Le répertoire de ce script:
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
@ -29,7 +30,7 @@ if [ -z "$SCODOC_UPGRADE_RUNNING" ]
# install spécifiquement scodoc9, utile si les dépendances Debian de scodoc9 # install spécifiquement scodoc9, utile si les dépendances Debian de scodoc9
# ont été changées, ce qui peut provoquer un # ont été changées, ce qui peut provoquer un
# "packages have been kept back" # "packages have been kept back"
apt install scodoc9 apt install scodoc9 || alarm_admin "apt install scodoc9 failed"
fi fi
systemctl restart redis systemctl restart redis
systemctl restart nginx systemctl restart nginx

View File

@ -110,3 +110,26 @@ gen_passwd() {
done done
echo "$password" echo "$password"
} }
# Status file: indique si l'install s'est complètement déroulée
create_install_status() {
touch "${SCODOC_INSTALL_STATUS_FILE}" || die "can't create ${SCODOC_INSTALL_STATUS_FILE}"
}
remove_install_status() {
if [ -e "${SCODOC_INSTALL_STATUS_FILE}" ]
then
rm -f "${SCODOC_INSTALL_STATUS_FILE}"
fi
}
#
alarm_admin() {
echo "Error: alarm_admin $1"
echo "Sending email to admin..."
echo "Subject: ScoDoc: $1" | mail -s "ScoDoc: $1" root <<EOF
Erreur lors de la mise à jour de ScoDoc sur $(hostname --fqdn)
EOF
}