Edition préférences: sections dépliables. + Code cleaning.

This commit is contained in:
Emmanuel Viennet 2022-10-02 23:43:29 +02:00 committed by iziram
parent 4fb296bbfa
commit c28dcf677a
41 changed files with 15287 additions and 15236 deletions

View File

@ -1,139 +1,139 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: table synthèse résultats semestre / PV
"""
from flask import g, request, url_for
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from app import log
from app.but import jury_but
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_excel
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def _descr_cursus_but(etud: Identite) -> str:
"description de la liste des semestres BUT suivis"
# prend simplement tous les semestre de type APC, ce qui sera faux si
# l'étudiant change de spécialité au sein du même département
# (ce qui ne peut normalement pas se produire)
indices = sorted(
[
ins.formsemestre.semestre_id
if ins.formsemestre.semestre_id is not None
else -1
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.is_apc()
]
)
return ", ".join(f"S{indice}" for indice in indices)
def pvjury_table_but(formsemestre_id: int, format="html"):
"""Page récapitulant les décisions de jury BUT
formsemestre peut être pair ou impair
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
assert formsemestre.formation.is_apc()
title = "Procès-verbal de jury BUT annuel"
if format == "html":
line_sep = "<br/>"
else:
line_sep = "\n"
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
titles = {
"nom": "Nom",
"cursus": "Cursus",
"ues": "UE validées",
"niveaux": "Niveaux de compétences validés",
"decision_but": f"Décision BUT{annee_but}",
"diplome": "Résultat au diplôme",
"devenir": "Devenir",
"observations": "Observations",
}
rows = []
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
try:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.annee_but != annee_but: # wtf ?
log(
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
)
continue
except ScoValueError:
deca = None
row = {
"nom": etud.etat_civil_pv(line_sep=line_sep),
"_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(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ues": deca.descr_ues_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 "",
}
rows.append(row)
rows.sort(key=lambda x: x["_nom_order"])
# 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,
columns_ids=titles.keys(),
html_caption=title,
html_class="pvjury_table_but table_leftalign",
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
<span style="padding-left: 20px;">
<a href="{url_for("notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
class="stdlink">version excel</a></span></div>
""",
html_with_td_classes=True,
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
rows=rows,
table_id="formation_table_recap",
titles=titles,
xls_columns_width={
"nom": 32,
"cursus": 12,
"ues": 32,
"niveaux": 32,
"decision_but": 14,
"diplome": 17,
"devenir": 8,
"observations": 12,
},
xls_style_base=xls_style_base,
)
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: table synthèse résultats semestre / PV
"""
from flask import g, request, url_for
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from app import log
from app.but import jury_but
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_excel
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def _descr_cursus_but(etud: Identite) -> str:
"description de la liste des semestres BUT suivis"
# prend simplement tous les semestre de type APC, ce qui sera faux si
# l'étudiant change de spécialité au sein du même département
# (ce qui ne peut normalement pas se produire)
indices = sorted(
[
ins.formsemestre.semestre_id
if ins.formsemestre.semestre_id is not None
else -1
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.is_apc()
]
)
return ", ".join(f"S{indice}" for indice in indices)
def pvjury_table_but(formsemestre_id: int, format="html"):
"""Page récapitulant les décisions de jury BUT
formsemestre peut être pair ou impair
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
assert formsemestre.formation.is_apc()
title = "Procès-verbal de jury BUT annuel"
if format == "html":
line_sep = "<br>"
else:
line_sep = "\n"
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
titles = {
"nom": "Nom",
"cursus": "Cursus",
"ues": "UE validées",
"niveaux": "Niveaux de compétences validés",
"decision_but": f"Décision BUT{annee_but}",
"diplome": "Résultat au diplôme",
"devenir": "Devenir",
"observations": "Observations",
}
rows = []
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
try:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.annee_but != annee_but: # wtf ?
log(
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
)
continue
except ScoValueError:
deca = None
row = {
"nom": etud.etat_civil_pv(line_sep=line_sep),
"_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(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ues": deca.descr_ues_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 "",
}
rows.append(row)
rows.sort(key=lambda x: x["_nom_order"])
# 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,
columns_ids=titles.keys(),
html_caption=title,
html_class="pvjury_table_but table_leftalign",
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
<span style="padding-left: 20px;">
<a href="{url_for("notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
class="stdlink">version excel</a></span></div>
""",
html_with_td_classes=True,
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
rows=rows,
table_id="formation_table_recap",
titles=titles,
xls_columns_width={
"nom": 32,
"cursus": 12,
"ues": 32,
"niveaux": 32,
"decision_but": 14,
"diplome": 17,
"devenir": 8,
"observations": 12,
},
xls_style_base=xls_style_base,
)
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)

File diff suppressed because it is too large Load Diff

View File

@ -1,180 +1,180 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""ScoDoc : interface des fonctions de gestion des avis de poursuites d'étude
"""
from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_preferences
from app.pe import pe_tools
from app.pe import pe_jurype
from app.pe import pe_avislatex
def _pe_view_sem_recap_form(formsemestre_id):
H = [
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études.
<br/>
De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
voir la documentation</a>.
</p>
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
enctype="multipart/form-data">
<div class="pe_template_up">
Les templates sont généralement installés sur le serveur ou dans le
paramétrage de ScoDoc.
<br/>
Au besoin, vous pouvez spécifier ici votre propre fichier de template
(<tt>un_avis.tex</tt>):
<div class="pe_template_upb">Template:
<input type="file" size="30" name="avis_tmpl_file"/>
</div>
<div class="pe_template_upb">Pied de page:
<input type="file" size="30" name="footer_tmpl_file"/>
</div>
</div>
<input type="submit" value="Générer les documents"/>
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
</form>
""",
]
return "\n".join(H) + html_sco_header.sco_footer()
# called from the web, POST or GET
def pe_view_sem_recap(
formsemestre_id,
avis_tmpl_file=None,
footer_tmpl_file=None,
):
"""Génération des avis de poursuite d'étude"""
if request.method == "GET":
return _pe_view_sem_recap_form(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
jury = pe_jurype.JuryPE(semBase)
# Ajout avis LaTeX au même zip:
etudids = list(jury.syntheseJury.keys())
# Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
# (chaines unicodes, html non quoté)
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_tmpl"
)
template_latex = template_latex.strip()
if not template_latex:
# pas de preference pour le template: utilise fichier du serveur
template_latex = pe_avislatex.get_templates_from_distrib("avis")
# Footer:
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode("utf-8")
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_footer"
)
footer_latex = footer_latex.strip()
if not footer_latex:
# pas de preference pour le footer: utilise fichier du serveur
footer_latex = pe_avislatex.get_templates_from_distrib(
"footer"
) # fallback: footer vides
tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_tag_annotation_avis_latex"
)
# Ajout des annotations PE dans un fichier excel
sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
if sT:
jury.add_file_to_zip(
jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
)
latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
for etudid in etudids:
[nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
jury,
etudid,
template_latex,
tag_annotation_pe,
footer_latex,
prefs,
)
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
doc_latex = "\n% -----\n".join(
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
)
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
# Ajoute image, LaTeX class file(s) and modeles
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
data = jury.get_zipped_data()
return send_file(
data,
mimetype="application/zip",
download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
as_attachment=True,
)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""ScoDoc : interface des fonctions de gestion des avis de poursuites d'étude
"""
from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_preferences
from app.pe import pe_tools
from app.pe import pe_jurype
from app.pe import pe_avislatex
def _pe_view_sem_recap_form(formsemestre_id):
H = [
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études.
<br>
De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
voir la documentation</a>.
</p>
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
enctype="multipart/form-data">
<div class="pe_template_up">
Les templates sont généralement installés sur le serveur ou dans le
paramétrage de ScoDoc.
<br>
Au besoin, vous pouvez spécifier ici votre propre fichier de template
(<tt>un_avis.tex</tt>):
<div class="pe_template_upb">Template:
<input type="file" size="30" name="avis_tmpl_file"/>
</div>
<div class="pe_template_upb">Pied de page:
<input type="file" size="30" name="footer_tmpl_file"/>
</div>
</div>
<input type="submit" value="Générer les documents"/>
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
</form>
""",
]
return "\n".join(H) + html_sco_header.sco_footer()
# called from the web, POST or GET
def pe_view_sem_recap(
formsemestre_id,
avis_tmpl_file=None,
footer_tmpl_file=None,
):
"""Génération des avis de poursuite d'étude"""
if request.method == "GET":
return _pe_view_sem_recap_form(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
jury = pe_jurype.JuryPE(semBase)
# Ajout avis LaTeX au même zip:
etudids = list(jury.syntheseJury.keys())
# Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
# (chaines unicodes, html non quoté)
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_tmpl"
)
template_latex = template_latex.strip()
if not template_latex:
# pas de preference pour le template: utilise fichier du serveur
template_latex = pe_avislatex.get_templates_from_distrib("avis")
# Footer:
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode("utf-8")
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_footer"
)
footer_latex = footer_latex.strip()
if not footer_latex:
# pas de preference pour le footer: utilise fichier du serveur
footer_latex = pe_avislatex.get_templates_from_distrib(
"footer"
) # fallback: footer vides
tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_tag_annotation_avis_latex"
)
# Ajout des annotations PE dans un fichier excel
sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
if sT:
jury.add_file_to_zip(
jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
)
latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
for etudid in etudids:
[nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
jury,
etudid,
template_latex,
tag_annotation_pe,
footer_latex,
prefs,
)
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
doc_latex = "\n% -----\n".join(
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
)
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
# Ajoute image, LaTeX class file(s) and modeles
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
data = jury.get_zipped_data()
return send_file(
data,
mimetype="application/zip",
download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
as_attachment=True,
)

View File

@ -38,6 +38,9 @@ def TrivialFormulator(
html_foot_markup="",
readonly=False,
is_submitted=False,
title="",
after_table="",
before_table="{title}",
):
"""
form_url : URL for this form
@ -74,7 +77,8 @@ def TrivialFormulator(
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
'hidden', 'separator', 'table_separator',
'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest',
'color'
(default text)
@ -111,6 +115,9 @@ def TrivialFormulator(
html_foot_markup=html_foot_markup,
readonly=readonly,
is_submitted=is_submitted,
title=title,
after_table=after_table,
before_table=before_table,
)
form = t.getform()
if t.canceled():
@ -144,6 +151,9 @@ class TF(object):
html_foot_markup="", # html snippet put at the end, just after the table
readonly=False,
is_submitted=False,
title="",
after_table="",
before_table="{title}",
):
self.form_url = form_url
self.values = values.copy()
@ -165,6 +175,9 @@ class TF(object):
self.top_buttons = top_buttons
self.bottom_buttons = bottom_buttons
self.html_foot_markup = html_foot_markup
self.title = title
self.after_table = after_table
self.before_table = before_table
self.readonly = readonly
self.result = None
self.is_submitted = is_submitted
@ -426,6 +439,7 @@ class TF(object):
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
if self.top_buttons:
R.append(buttons_markup + "<p></p>")
R.append(self.before_table.format(title=self.title))
R.append('<table class="tf">')
for field, descr in self.formdescription:
if descr.get("readonly", False):
@ -453,6 +467,16 @@ class TF(object):
etempl = separatortemplate
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
continue
elif input_type == "table_separator":
etempl = ""
# Table ouverte ?
if len([p for p in R if "<table" in p]) > len(
[p for p in R if "</table" in p]
):
R.append(f"""</table>{self.after_table}""")
R.append(
f"""{self.before_table.format(title=descr.get("title", ""))}<table class="tf">"""
)
else:
etempl = itemtemplate
lab = []
@ -610,7 +634,7 @@ class TF(object):
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
% (field, wid, values[field], attribs)
)
elif input_type == "separator":
elif (input_type == "separator") or (input_type == "table_separator"):
pass
elif input_type == "file":
lem.append(
@ -641,13 +665,15 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
)
lem.append(('value="%(' + field + ')s" >') % values)
else:
raise ValueError("unkown input_type for form (%s)!" % input_type)
raise ValueError(f"unkown input_type for form ({input_type})!")
explanation = descr.get("explanation", "")
if explanation:
lem.append('<span class="tf-explanation">%s</span>' % explanation)
lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
comment = descr.get("comment", "")
if comment:
lem.append('<br/><span class="tf-comment">%s</span>' % comment)
if (input_type != "checkbox") and (input_type != "boolcheckbox"):
lem.append("<br>")
lem.append(f"""<span class="tf-comment">{comment}</span>""")
R.append(
etempl
% {
@ -657,11 +683,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
}
)
R.append("</table>")
R.append(self.after_table)
R.append(self.html_foot_markup)
if self.bottom_buttons:
R.append("<br/>" + buttons_markup)
R.append("<br>" + buttons_markup)
if add_no_enter_js:
R.append(
@ -753,7 +779,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
if input_type == "separator": # separator
R.append('<td colspan="2">%s' % title)
else:
elif input_type != "table_separator":
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
R.append("%s</td>" % title)
R.append('<td class="tf-ro-field%s">' % klass)
@ -786,7 +812,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
R.append(
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
)
elif input_type == "separator" or input_type == "hidden":
elif (
input_type == "separator"
or input_type == "hidden"
or input_type == "table_separator"
):
pass
elif input_type == "file":
R.append("'%s'" % self.values[field])

View File

@ -1,321 +1,321 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""HTML Header/Footer for ScoDoc pages
"""
import html
from flask import render_template
from flask import request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app import scodoc_flash_status_messages
from app.scodoc import html_sidebar
import sco_version
# Some constants:
# Multiselect menus are used on a few pages and not loaded by default
BOOTSTRAP_MULTISELECT_JS = [
"libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
"libjs/bootstrap-multiselect/bootstrap-multiselect.js",
"libjs/purl.js",
]
BOOTSTRAP_MULTISELECT_CSS = [
"libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
"libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect/bootstrap-multiselect.css",
]
def standard_html_header():
"""Standard HTML header for pages outside depts"""
# not used in ZScolar, see sco_header
return f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>ScoDoc: accueil</title>
<META http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}">
<META http-equiv="Content-Style-Type" content="text/css">
<META name="LANG" content="fr">
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css"/>
</head><body>{scu.CUSTOM_HTML_HEADER_CNX}"""
def standard_html_footer():
"""Le pied de page HTML de la page d'accueil."""
return f"""<p class="footer">
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
<p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:{scu.SCO_USERS_LIST}">{scu.SCO_USERS_LIST}</a></p>
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
</body></html>"""
_HTML_BEGIN = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
<title>%(page_title)s</title>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
</script>
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
<script src="{scu.STATIC_DIR}/js/etud_info.js"></script>
"""
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
"""</head><body id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX,
]
return "\n".join(H)
# Header:
def sco_header(
# optional args
page_title="", # page title
no_side_bar=False, # hide sidebar
cssstyles=(), # additionals CSS sheets
javascripts=(), # additionals JS filenames to load
scripts=(), # script to put in page header
bodyOnLoad="", # JS
init_qtip=False, # include qTip
init_google_maps=False, # Google maps
init_datatables=True,
titrebandeau="", # titre dans bandeau superieur
head_message="", # message action (petit cadre jaune en haut)
user_check=True, # verifie passwords temporaires
etudid=None,
formsemestre_id=None,
):
"Main HTML page header for ScoDoc"
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
scodoc_flash_status_messages()
# Get head message from http request:
if not head_message:
if request.method == "POST":
head_message = request.form.get("head_message", "")
elif request.method == "GET":
head_message = request.args.get("head_message", "")
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
}
if bodyOnLoad:
params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
else:
params["bodyOnLoad_mkup"] = ""
if no_side_bar:
params["margin_left"] = "1em"
else:
params["margin_left"] = "140px"
H = [
"""<!DOCTYPE html><html lang="fr">
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
"""
% params
]
# jQuery UI
# can modify loaded theme here
H.append(
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
)
if init_google_maps:
# It may be necessary to add an API key:
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
# Feuilles de style additionnelles:
for cssstyle in cssstyles:
H.append(
f"""<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/{cssstyle}" />\n"""
)
H.append(
f"""
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
var SCO_URL="{scu.ScoURL()}";
</script>"""
)
# jQuery
H.append(
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
)
# qTip
if init_qtip:
H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
)
H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
)
if init_google_maps:
H.append(
f'<script src="{scu.STATIC_DIR}/libjs/jquery.ui.map.full.min.js"></script>'
)
if init_datatables:
H.append(
f"""<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css"/>
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>"""
)
# H.append(
# f'<link href="{scu.STATIC_DIR}/css/tooltip.css" rel="stylesheet" type="text/css" />'
# )
# JS additionels
for js in javascripts:
H.append(f"""<script src="{scu.STATIC_DIR}/{js}"></script>\n""")
H.append(
f"""<style>
#gtrcontent {{
margin-left: {params["margin_left"]};
height: 100%%;
margin-bottom: 10px;
}}
</style>
"""
)
# Scripts de la page:
if scripts:
H.append("""<script>""")
for script in scripts:
H.append(script)
H.append("""</script>""")
H.append("</head>")
# Body et bandeau haut:
H.append("""<body %(bodyOnLoad_mkup)s>""" % params)
H.append(scu.CUSTOM_HTML_HEADER)
#
if not no_side_bar:
H.append(html_sidebar.sidebar(etudid))
H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title(formsemestre_id))
# Avertissement si mot de passe à changer
if user_check:
if current_user.passwd_temp:
H.append(
f"""<div class="passwd_warn">
Attention !<br/>
Vous avez reçu un mot de passe temporaire.<br/>
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
</div>"""
)
#
if head_message:
H.append('<div class="head_message">' + html.escape(head_message) + "</div>")
#
# div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>')
#
return "".join(H)
def sco_footer():
"""Main HTMl pages footer"""
return (
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
)
def html_sem_header(
title, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...
if with_page_header:
h = sco_header(page_title="%s" % (page_title or title), **args)
else:
h = ""
if with_h2:
return h + f"""<h2 class="formsemestre">{title}</h2>"""
else:
return h
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""HTML Header/Footer for ScoDoc pages
"""
import html
from flask import render_template
from flask import request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app import scodoc_flash_status_messages
from app.scodoc import html_sidebar
import sco_version
# Some constants:
# Multiselect menus are used on a few pages and not loaded by default
BOOTSTRAP_MULTISELECT_JS = [
"libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
"libjs/bootstrap-multiselect/bootstrap-multiselect.js",
"libjs/purl.js",
]
BOOTSTRAP_MULTISELECT_CSS = [
"libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
"libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect/bootstrap-multiselect.css",
]
def standard_html_header():
"""Standard HTML header for pages outside depts"""
# not used in ZScolar, see sco_header
return f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>ScoDoc: accueil</title>
<META http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}">
<META http-equiv="Content-Style-Type" content="text/css">
<META name="LANG" content="fr">
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css"/>
</head><body>{scu.CUSTOM_HTML_HEADER_CNX}"""
def standard_html_footer():
"""Le pied de page HTML de la page d'accueil."""
return f"""<p class="footer">
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
<p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:{scu.SCO_USERS_LIST}">{scu.SCO_USERS_LIST}</a></p>
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
</body></html>"""
_HTML_BEGIN = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
<title>%(page_title)s</title>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
</script>
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
<script src="{scu.STATIC_DIR}/js/etud_info.js"></script>
"""
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
"""</head><body id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX,
]
return "\n".join(H)
# Header:
def sco_header(
# optional args
page_title="", # page title
no_side_bar=False, # hide sidebar
cssstyles=(), # additionals CSS sheets
javascripts=(), # additionals JS filenames to load
scripts=(), # script to put in page header
bodyOnLoad="", # JS
init_qtip=False, # include qTip
init_google_maps=False, # Google maps
init_datatables=True,
titrebandeau="", # titre dans bandeau superieur
head_message="", # message action (petit cadre jaune en haut)
user_check=True, # verifie passwords temporaires
etudid=None,
formsemestre_id=None,
):
"Main HTML page header for ScoDoc"
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
scodoc_flash_status_messages()
# Get head message from http request:
if not head_message:
if request.method == "POST":
head_message = request.form.get("head_message", "")
elif request.method == "GET":
head_message = request.args.get("head_message", "")
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
}
if bodyOnLoad:
params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
else:
params["bodyOnLoad_mkup"] = ""
if no_side_bar:
params["margin_left"] = "1em"
else:
params["margin_left"] = "140px"
H = [
"""<!DOCTYPE html><html lang="fr">
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
"""
% params
]
# jQuery UI
# can modify loaded theme here
H.append(
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
)
if init_google_maps:
# It may be necessary to add an API key:
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
# Feuilles de style additionnelles:
for cssstyle in cssstyles:
H.append(
f"""<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/{cssstyle}" />\n"""
)
H.append(
f"""
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
var SCO_URL="{scu.ScoURL()}";
</script>"""
)
# jQuery
H.append(
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
)
# qTip
if init_qtip:
H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
)
H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
)
if init_google_maps:
H.append(
f'<script src="{scu.STATIC_DIR}/libjs/jquery.ui.map.full.min.js"></script>'
)
if init_datatables:
H.append(
f"""<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css"/>
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>"""
)
# H.append(
# f'<link href="{scu.STATIC_DIR}/css/tooltip.css" rel="stylesheet" type="text/css" />'
# )
# JS additionels
for js in javascripts:
H.append(f"""<script src="{scu.STATIC_DIR}/{js}"></script>\n""")
H.append(
f"""<style>
#gtrcontent {{
margin-left: {params["margin_left"]};
height: 100%%;
margin-bottom: 10px;
}}
</style>
"""
)
# Scripts de la page:
if scripts:
H.append("""<script>""")
for script in scripts:
H.append(script)
H.append("""</script>""")
H.append("</head>")
# Body et bandeau haut:
H.append("""<body %(bodyOnLoad_mkup)s>""" % params)
H.append(scu.CUSTOM_HTML_HEADER)
#
if not no_side_bar:
H.append(html_sidebar.sidebar(etudid))
H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title(formsemestre_id))
# Avertissement si mot de passe à changer
if user_check:
if current_user.passwd_temp:
H.append(
f"""<div class="passwd_warn">
Attention !<br>
Vous avez reçu un mot de passe temporaire.<br>
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
</div>"""
)
#
if head_message:
H.append('<div class="head_message">' + html.escape(head_message) + "</div>")
#
# div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>')
#
return "".join(H)
def sco_footer():
"""Main HTMl pages footer"""
return (
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
)
def html_sem_header(
title, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...
if with_page_header:
h = sco_header(page_title="%s" % (page_title or title), **args)
else:
h = ""
if with_h2:
return h + f"""<h2 class="formsemestre">{title}</h2>"""
else:
return h

View File

@ -1,172 +1,172 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Génération de la "sidebar" (marge gauche des pages HTML)
"""
from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION
def sidebar_common():
"partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
H = [
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
<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)
}">{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">Programmes</a> <br/>
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
"""
]
if current_user.has_permission(
Permission.ScoUsersAdmin
) or current_user.has_permission(Permission.ScoUsersView):
H.append(
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br/>"""
)
if current_user.has_permission(Permission.ScoChangePreferences):
H.append(
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
class="sidebar">Paramétrage</a> <br/>"""
)
return "".join(H)
def sidebar(etudid: int = None):
"Main HTML page sidebar"
# rewritten from legacy DTML code
from app.scodoc import sco_abs
from app.scodoc import sco_etud
params = {}
H = [
f"""<div class="sidebar">
{ sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br/>
<form method="get" id="form-chercheetud"
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
</form></div>
<div class="etud-insidebar">
"""
]
# ---- Il y-a-t-il un etudiant selectionné ?
etudid = etudid if etudid is not None else g.get("etudid", None)
if etudid is None:
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid is not None:
etudi = int(etudid)
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
params.update(etud)
params["fiche_url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
# compte les absences du semestre en cours
H.append(
"""<h2 id="insidebar-etud"><a href="%(fiche_url)s" class="sidebar">
<font color="#FF0000">%(civilite_str)s %(nom_disp)s</font></a>
</h2>
<b>Absences</b>"""
% params
)
if etud["cursem"]:
cur_sem = etud["cursem"]
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, cur_sem)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
<br/>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
)
H.append("<ul>")
if current_user.has_permission(Permission.ScoAbsChange):
H.append(
f"""
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
"""
)
if sco_preferences.get_preference("handle_billets_abs"):
H.append(
f"""<li><a href="{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Billets</a></li>"""
)
H.append(
f"""
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
<li><a href="{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
</ul>
"""
)
else:
pass # H.append("(pas d'étudiant en cours)")
# ---------
H.append("</div>") # /etud-insidebar
# Logo
H.append(
f"""<div class="logo-insidebar">
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
</div></div>
<div class="logo-logo">
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div>
</div>
<!-- end of sidebar -->
"""
)
return "".join(H)
def sidebar_dept():
"""Partie supérieure de la marge de gauche"""
return render_template(
"sidebar_dept.html",
prefs=sco_preferences.SemPreferences(),
)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Génération de la "sidebar" (marge gauche des pages HTML)
"""
from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION
def sidebar_common():
"partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
H = [
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
<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)
}">{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">Programmes</a> <br>
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br>
"""
]
if current_user.has_permission(
Permission.ScoUsersAdmin
) or current_user.has_permission(Permission.ScoUsersView):
H.append(
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
)
if current_user.has_permission(Permission.ScoChangePreferences):
H.append(
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
class="sidebar">Paramétrage</a> <br>"""
)
return "".join(H)
def sidebar(etudid: int = None):
"Main HTML page sidebar"
# rewritten from legacy DTML code
from app.scodoc import sco_abs
from app.scodoc import sco_etud
params = {}
H = [
f"""<div class="sidebar">
{ sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud"
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
</form></div>
<div class="etud-insidebar">
"""
]
# ---- Il y-a-t-il un etudiant selectionné ?
etudid = etudid if etudid is not None else g.get("etudid", None)
if etudid is None:
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid is not None:
etudi = int(etudid)
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
params.update(etud)
params["fiche_url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
# compte les absences du semestre en cours
H.append(
"""<h2 id="insidebar-etud"><a href="%(fiche_url)s" class="sidebar">
<font color="#FF0000">%(civilite_str)s %(nom_disp)s</font></a>
</h2>
<b>Absences</b>"""
% params
)
if etud["cursem"]:
cur_sem = etud["cursem"]
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, cur_sem)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
)
H.append("<ul>")
if current_user.has_permission(Permission.ScoAbsChange):
H.append(
f"""
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
"""
)
if sco_preferences.get_preference("handle_billets_abs"):
H.append(
f"""<li><a href="{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Billets</a></li>"""
)
H.append(
f"""
<li><a href="{ url_for('absences.CalAbs', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
<li><a href="{ url_for('absences.ListeAbsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
</ul>
"""
)
else:
pass # H.append("(pas d'étudiant en cours)")
# ---------
H.append("</div>") # /etud-insidebar
# Logo
H.append(
f"""<div class="logo-insidebar">
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br>
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
</div></div>
<div class="logo-logo">
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div>
</div>
<!-- end of sidebar -->
"""
)
return "".join(H)
def sidebar_dept():
"""Partie supérieure de la marge de gauche"""
return render_template(
"sidebar_dept.html",
prefs=sco_preferences.SemPreferences(),
)

View File

@ -1,80 +1,80 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
from html.parser import HTMLParser
"""HTML sanitizing function
used to clean user submitted HTML
(Python 3 only)
"""
# permet de conserver les liens
def html_to_safe_html(text, convert_br=True): # was HTML2SafeHTML
# text = html_to_safe_html(text, valid_tags=("b", "a", "i", "br", "p"))
# New version (jul 2021) with our own parser
text = convert_html_to_text(text)
if convert_br:
return newline_to_br(text)
else:
return text
def convert_html_to_text(s):
parser = HTMLSanitizer()
parser.feed(s)
return parser.text
def newline_to_br(text):
return text.replace("\n", "<br/>")
class HTMLSanitizer(HTMLParser):
def __init__(self, allowed_tags=("i", "b", "em", "br", "p"), **kwargs):
super(HTMLSanitizer, self).__init__(**kwargs)
self.allowed_tags = set(allowed_tags)
self.text = ""
def handle_starttag(self, tag, attrs):
if tag in self.allowed_tags:
self.text += "<{} {}>".format(
tag, ", ".join(['{}="{}"'.format(k, v) for (k, v) in attrs])
)
def handle_endtag(self, tag):
if tag in self.allowed_tags:
self.text += "</" + tag + ">"
def handle_data(self, data):
self.text += data
if __name__ == "__main__":
test_parser = HTMLSanitizer()
test_parser.feed("""<p>Hello world <b z="1" >gras</b> <i a="2">italique</i></p>""")
print(test_parser.text)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
from html.parser import HTMLParser
"""HTML sanitizing function
used to clean user submitted HTML
(Python 3 only)
"""
# permet de conserver les liens
def html_to_safe_html(text, convert_br=True): # was HTML2SafeHTML
# text = html_to_safe_html(text, valid_tags=("b", "a", "i", "br", "p"))
# New version (jul 2021) with our own parser
text = convert_html_to_text(text)
if convert_br:
return newline_to_br(text)
else:
return text
def convert_html_to_text(s):
parser = HTMLSanitizer()
parser.feed(s)
return parser.text
def newline_to_br(text):
return text.replace("\n", "<br>")
class HTMLSanitizer(HTMLParser):
def __init__(self, allowed_tags=("i", "b", "em", "br", "p"), **kwargs):
super(HTMLSanitizer, self).__init__(**kwargs)
self.allowed_tags = set(allowed_tags)
self.text = ""
def handle_starttag(self, tag, attrs):
if tag in self.allowed_tags:
self.text += "<{} {}>".format(
tag, ", ".join(['{}="{}"'.format(k, v) for (k, v) in attrs])
)
def handle_endtag(self, tag):
if tag in self.allowed_tags:
self.text += "</" + tag + ">"
def handle_data(self, data):
self.text += data
if __name__ == "__main__":
test_parser = HTMLSanitizer()
test_parser.feed("""<p>Hello world <b z="1" >gras</b> <i a="2">italique</i></p>""")
print(test_parser.text)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,386 +1,386 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""ScoDoc : gestion des fichiers archivés associés aux étudiants
Il s'agit de fichiers quelconques, généralement utilisés pour conserver
les dossiers d'admission et autres pièces utiles.
"""
import flask
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_import_etuds
from app.scodoc import sco_groups
from app.scodoc import sco_trombino
from app.scodoc import sco_excel
from app.scodoc import sco_archives
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
class EtudsArchiver(sco_archives.BaseArchiver):
def __init__(self):
sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
EtudsArchive = EtudsArchiver()
def can_edit_etud_archive(authuser):
"""True si l'utilisateur peut modifier les archives etudiantes"""
return authuser.has_permission(Permission.ScoEtudAddAnnotations)
def etud_list_archives_html(etudid):
"""HTML snippet listing archives"""
can_edit = can_edit_etud_archive(current_user)
etuds = sco_etud.get_etud_info(etudid=etudid)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etudid
L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
a = {
"archive_id": archive_id,
"description": EtudsArchive.get_archive_description(archive_id),
"date": EtudsArchive.get_archive_date(archive_id),
"content": EtudsArchive.list_archive(archive_id),
}
L.append(a)
delete_icon = scu.icontag(
"delete_small_img", title="Supprimer fichier", alt="supprimer"
)
delete_disabled_icon = scu.icontag(
"delete_small_dis_img", title="Suppression non autorisée"
)
H = ['<div class="etudarchive"><ul>']
for a in L:
archive_name = EtudsArchive.get_archive_name(a["archive_id"])
H.append(
"""<li><span class ="etudarchive_descr" title="%s">%s</span>"""
% (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"])
)
for filename in a["content"]:
H.append(
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
% (etudid, archive_name, filename, filename)
)
if not a["content"]:
H.append("<em>aucun fichier !</em>")
if can_edit:
H.append(
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
% (etudid, archive_name, delete_icon)
)
else:
H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
H.append("</li>")
if can_edit:
H.append(
'<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>'
% etudid
)
H.append("</ul></div>")
return "".join(H)
def add_archives_info_to_etud_list(etuds):
"""Add key 'etudarchive' describing archive of etuds
(used to list all archives of a group)
"""
for etud in etuds:
l = []
etud_archive_id = etud["etudid"]
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
l.append(
"%s (%s)"
% (
EtudsArchive.get_archive_description(archive_id),
EtudsArchive.list_archive(archive_id)[0],
)
)
etud["etudarchive"] = ", ".join(l)
def etud_upload_file_form(etudid):
"""Page with a form to choose and upload a file, with a description."""
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % current_user)
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
H = [
html_sco_header.sco_header(
page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
),
"""<h2>Chargement d'un document associé à %(nomprenom)s</h2>
"""
% etud,
"""<p>Le fichier ne doit pas dépasser %sMo.</p>
"""
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("etudid", {"default": etudid, "input_type": "hidden"}),
("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
(
"description",
{
"input_type": "textarea",
"rows": 4,
"cols": 77,
"title": "Description",
},
),
),
submitlabel="Valider",
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
else:
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
filename = tf[2]["datafile"].filename
etud_archive_id = etud["etudid"]
_store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=descr
)
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
def _store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=""
) -> tuple[bool, str]:
"""Store data to new archive."""
filesize = len(data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description)
EtudsArchive.store(archive_id, filename, data)
return True, "ok"
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"""Delete an archive"""
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied(f"opération non autorisée pour {current_user}")
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name)
if not dialog_confirmed:
return scu.confirm_dialog(
"""<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le %s à l'étudiant %s</p>
<p>La suppression sera définitive.</p>"""
% (
EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
etud["nomprenom"],
),
dest_url="",
cancel_url=url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="annulation",
),
parameters={"etudid": etudid, "archive_name": archive_name},
)
EtudsArchive.delete_archive(archive_id)
return flask.redirect(
url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="Archive%20supprimée",
)
)
def etud_get_archived_file(etudid, archive_name, filename):
"""Send file to client."""
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
def etudarchive_generate_excel_sample(group_id=None):
"""Feuille excel pour import fichiers etudiants (utilisé pour admissions)"""
fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample(
fmt,
group_ids=[group_id],
only_tables=["identite"],
exclude_cols=[
"date_naissance",
"lieu_naissance",
"nationalite",
"statut",
"photo_filename",
],
extra_cols=["fichier_a_charger"],
)
return scu.send_file(
data,
"ImportFichiersEtudiants",
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
def etudarchive_import_files_form(group_id):
"""Formulaire pour importation fichiers d'un groupe"""
H = [
html_sco_header.sco_header(
page_title="Import de fichiers associés aux étudiants"
),
"""<h2 class="formsemestre">Téléchargement de fichier associés aux étudiants</h2>
<p>Les fichiers associés (dossiers d'admission, certificats, ...), de
types quelconques (pdf, doc, images) sont accessibles aux utilisateurs via
la fiche individuelle de l'étudiant.
</p>
<p class="warning">Ne pas confondre avec les photos des étudiants, qui se
chargent via l'onglet "Photos".</p>
<p><b>Vous pouvez aussi charger à tout moment de nouveaux fichiers, ou en
supprimer, via la fiche de chaque étudiant.</b>
</p>
<p class="help">Cette page permet de charger en une seule fois les fichiers
de plusieurs étudiants.<br/>
Il faut d'abord remplir une feuille excel donnant les noms
des fichiers (un fichier par étudiant).
</p>
<p class="help">Ensuite, réunir vos fichiers dans un fichier zip, puis
télécharger simultanément le fichier excel et le fichier zip.
</p>
<ol>
<li><a class="stdlink" href="etudarchive_generate_excel_sample?group_id=%s">
Obtenir la feuille excel à remplir</a>
</li>
<li style="padding-top: 2em;">
"""
% group_id,
]
F = html_sco_header.sco_footer()
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
(
"description",
{
"input_type": "textarea",
"rows": 4,
"cols": 77,
"title": "Description",
},
),
("group_id", {"input_type": "hidden"}),
),
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
if tf[0] == -1:
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=group["formsemestre_id"],
)
)
else:
return etudarchive_import_files(
formsemestre_id=group["formsemestre_id"],
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
description=tf[2]["description"],
)
def etudarchive_import_files(
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
):
"Importe des fichiers"
def callback(etud, data, filename):
return _store_etud_file_to_new_archive(
etud["etudid"], data, filename, description
)
# Utilise la fontion developpée au depart pour les photos
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = sco_trombino.zip_excel_import_files(
xlsfile=xlsfile,
zipfile=zipfile,
callback=callback,
filename_title="fichier_a_charger",
)
return render_template(
"scolar/photos_import_files.html",
page_title="Téléchargement de fichiers associés aux étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""ScoDoc : gestion des fichiers archivés associés aux étudiants
Il s'agit de fichiers quelconques, généralement utilisés pour conserver
les dossiers d'admission et autres pièces utiles.
"""
import flask
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_import_etuds
from app.scodoc import sco_groups
from app.scodoc import sco_trombino
from app.scodoc import sco_excel
from app.scodoc import sco_archives
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
class EtudsArchiver(sco_archives.BaseArchiver):
def __init__(self):
sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
EtudsArchive = EtudsArchiver()
def can_edit_etud_archive(authuser):
"""True si l'utilisateur peut modifier les archives etudiantes"""
return authuser.has_permission(Permission.ScoEtudAddAnnotations)
def etud_list_archives_html(etudid):
"""HTML snippet listing archives"""
can_edit = can_edit_etud_archive(current_user)
etuds = sco_etud.get_etud_info(etudid=etudid)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etudid
L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
a = {
"archive_id": archive_id,
"description": EtudsArchive.get_archive_description(archive_id),
"date": EtudsArchive.get_archive_date(archive_id),
"content": EtudsArchive.list_archive(archive_id),
}
L.append(a)
delete_icon = scu.icontag(
"delete_small_img", title="Supprimer fichier", alt="supprimer"
)
delete_disabled_icon = scu.icontag(
"delete_small_dis_img", title="Suppression non autorisée"
)
H = ['<div class="etudarchive"><ul>']
for a in L:
archive_name = EtudsArchive.get_archive_name(a["archive_id"])
H.append(
"""<li><span class ="etudarchive_descr" title="%s">%s</span>"""
% (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"])
)
for filename in a["content"]:
H.append(
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
% (etudid, archive_name, filename, filename)
)
if not a["content"]:
H.append("<em>aucun fichier !</em>")
if can_edit:
H.append(
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
% (etudid, archive_name, delete_icon)
)
else:
H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
H.append("</li>")
if can_edit:
H.append(
'<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>'
% etudid
)
H.append("</ul></div>")
return "".join(H)
def add_archives_info_to_etud_list(etuds):
"""Add key 'etudarchive' describing archive of etuds
(used to list all archives of a group)
"""
for etud in etuds:
l = []
etud_archive_id = etud["etudid"]
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
l.append(
"%s (%s)"
% (
EtudsArchive.get_archive_description(archive_id),
EtudsArchive.list_archive(archive_id)[0],
)
)
etud["etudarchive"] = ", ".join(l)
def etud_upload_file_form(etudid):
"""Page with a form to choose and upload a file, with a description."""
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % current_user)
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
H = [
html_sco_header.sco_header(
page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
),
"""<h2>Chargement d'un document associé à %(nomprenom)s</h2>
"""
% etud,
"""<p>Le fichier ne doit pas dépasser %sMo.</p>
"""
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("etudid", {"default": etudid, "input_type": "hidden"}),
("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
(
"description",
{
"input_type": "textarea",
"rows": 4,
"cols": 77,
"title": "Description",
},
),
),
submitlabel="Valider",
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
else:
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
filename = tf[2]["datafile"].filename
etud_archive_id = etud["etudid"]
_store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=descr
)
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
def _store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=""
) -> tuple[bool, str]:
"""Store data to new archive."""
filesize = len(data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description)
EtudsArchive.store(archive_id, filename, data)
return True, "ok"
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"""Delete an archive"""
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied(f"opération non autorisée pour {current_user}")
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name)
if not dialog_confirmed:
return scu.confirm_dialog(
"""<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le %s à l'étudiant %s</p>
<p>La suppression sera définitive.</p>"""
% (
EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
etud["nomprenom"],
),
dest_url="",
cancel_url=url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="annulation",
),
parameters={"etudid": etudid, "archive_name": archive_name},
)
EtudsArchive.delete_archive(archive_id)
return flask.redirect(
url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="Archive%20supprimée",
)
)
def etud_get_archived_file(etudid, archive_name, filename):
"""Send file to client."""
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
def etudarchive_generate_excel_sample(group_id=None):
"""Feuille excel pour import fichiers etudiants (utilisé pour admissions)"""
fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample(
fmt,
group_ids=[group_id],
only_tables=["identite"],
exclude_cols=[
"date_naissance",
"lieu_naissance",
"nationalite",
"statut",
"photo_filename",
],
extra_cols=["fichier_a_charger"],
)
return scu.send_file(
data,
"ImportFichiersEtudiants",
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
def etudarchive_import_files_form(group_id):
"""Formulaire pour importation fichiers d'un groupe"""
H = [
html_sco_header.sco_header(
page_title="Import de fichiers associés aux étudiants"
),
"""<h2 class="formsemestre">Téléchargement de fichier associés aux étudiants</h2>
<p>Les fichiers associés (dossiers d'admission, certificats, ...), de
types quelconques (pdf, doc, images) sont accessibles aux utilisateurs via
la fiche individuelle de l'étudiant.
</p>
<p class="warning">Ne pas confondre avec les photos des étudiants, qui se
chargent via l'onglet "Photos".</p>
<p><b>Vous pouvez aussi charger à tout moment de nouveaux fichiers, ou en
supprimer, via la fiche de chaque étudiant.</b>
</p>
<p class="help">Cette page permet de charger en une seule fois les fichiers
de plusieurs étudiants.<br>
Il faut d'abord remplir une feuille excel donnant les noms
des fichiers (un fichier par étudiant).
</p>
<p class="help">Ensuite, réunir vos fichiers dans un fichier zip, puis
télécharger simultanément le fichier excel et le fichier zip.
</p>
<ol>
<li><a class="stdlink" href="etudarchive_generate_excel_sample?group_id=%s">
Obtenir la feuille excel à remplir</a>
</li>
<li style="padding-top: 2em;">
"""
% group_id,
]
F = html_sco_header.sco_footer()
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
(
"description",
{
"input_type": "textarea",
"rows": 4,
"cols": 77,
"title": "Description",
},
),
("group_id", {"input_type": "hidden"}),
),
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
if tf[0] == -1:
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=group["formsemestre_id"],
)
)
else:
return etudarchive_import_files(
formsemestre_id=group["formsemestre_id"],
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
description=tf[2]["description"],
)
def etudarchive_import_files(
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
):
"Importe des fichiers"
def callback(etud, data, filename):
return _store_etud_file_to_new_archive(
etud["etudid"], data, filename, description
)
# Utilise la fontion developpée au depart pour les photos
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = sco_trombino.zip_excel_import_files(
xlsfile=xlsfile,
zipfile=zipfile,
callback=callback,
filename_title="fichier_a_charger",
)
return render_template(
"scolar/photos_import_files.html",
page_title="Téléchargement de fichiers associés aux étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)

View File

@ -1,368 +1,368 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@gmail.com
#
##############################################################################
"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)
class BulletinGenerator:
description
supported_formats = [ 'pdf', 'html' ]
.bul_title_pdf()
.bul_table(format)
.bul_part_below(format)
.bul_signatures_pdf()
.__init__ et .generate(format) methodes appelees par le client (sco_bulletin)
La préférence 'bul_class_name' donne le nom de la classe generateur.
La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
"""
import collections
import io
import time
import traceback
import reportlab
from reportlab.platypus import (
SimpleDocTemplate,
DocIf,
Paragraph,
Spacer,
Frame,
PageBreak,
)
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from flask import request
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
import sco_version
class BulletinGenerator:
"Virtual superclass for PDF bulletin generators" ""
# Here some helper methods
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
scale_table_in_page = True # rescale la table sur 1 page
multi_pages = False
def __init__(
self,
infos,
authuser=None,
version="long",
filigranne=None,
server_name=None,
):
from app.scodoc import sco_preferences
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
self.infos = infos
self.authuser = authuser # nécessaire pour version HTML qui contient liens dépendant de l'utilisateur
self.version = version
self.filigranne = filigranne
self.server_name = server_name
# Store preferences for convenience:
formsemestre_id = self.infos["formsemestre_id"]
self.preferences = sco_preferences.SemPreferences(formsemestre_id)
self.diagnostic = None # error message if any problem
# Common PDF styles:
# - Pour tous les champs du bulletin sauf les cellules de table:
self.FieldStyle = reportlab.lib.styles.ParagraphStyle({})
self.FieldStyle.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.FieldStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.FieldStyle.firstLineIndent = 0
# - Pour les cellules de table:
self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
self.CellStyle.leading = (
1.0 * self.preferences["SCOLAR_FONT_SIZE"]
) # vertical space
# Marges du document PDF
self.margins = (
self.preferences["left_margin"],
self.preferences["top_margin"],
self.preferences["right_margin"],
self.preferences["bottom_margin"],
)
def get_filename(self):
"""Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
if not format in self.supported_formats:
raise ValueError("unsupported bulletin format (%s)" % format)
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if format == "html":
return self.generate_html()
elif format == "pdf":
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError("invalid bulletin format (%s)" % format)
finally:
PDFLOCK.release()
def generate_html(self):
"""Return bulletin as an HTML string"""
H = ['<div class="notes_bulletin">']
# table des notes:
H.append(self.bul_table(format="html")) # pylint: disable=no-member
# infos sous la table:
H.append(self.bul_part_below(format="html")) # pylint: disable=no-member
H.append("</div>")
return "\n".join(H)
def generate_pdf(self, stand_alone=True):
"""Build PDF bulletin from distinct parts
Si stand_alone, génère un doc PDF complet et renvoie une string
Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
from app.scodoc import sco_preferences
formsemestre_id = self.infos["formsemestre_id"]
marque_debut_bulletin = sco_pdf.DebutBulletin(
self.infos["etud"]["nomprenom"],
filigranne=self.infos["filigranne"],
footer_content=f"""ScoDoc - Bulletin de {self.infos["etud"]["nomprenom"]} - {time.strftime("%d/%m/%Y %H:%M")}""",
)
story = []
# partie haute du bulletin
story += self.bul_title_pdf() # pylint: disable=no-member
index_obj_debut = len(story)
# table des notes
story += self.bul_table(format="pdf") # pylint: disable=no-member
# infos sous la table
story += self.bul_part_below(format="pdf") # pylint: disable=no-member
# signatures
story += self.bul_signatures_pdf() # pylint: disable=no-member
if self.scale_table_in_page:
# Réduit sur une page
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
else:
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
story.insert(index_obj_debut, marque_debut_bulletin)
#
# objects.append(sco_pdf.FinBulletin())
if not stand_alone:
if self.multi_pages:
# Bulletins sur plusieurs page, force début suivant sur page impaire
story.append(
DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
)
else:
story.append(PageBreak()) # insert page break at end
return story
else:
# Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
document,
author="%s %s (E. Viennet) [%s]"
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
title="Bulletin %s de %s"
% (sem["titremois"], self.infos["etud"]["nomprenom"]),
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
)
document.build(story)
data = report.getvalue()
return data
def buildTableObject(self, P, pdfTableStyle, colWidths):
"""Utility used by some old-style generators.
Build a platypus Table instance from a nested list of cells, style and widths.
P: table, as a list of lists
PdfTableStyle: commandes de style pour la table (reportlab)
"""
try:
# put each table cell in a Paragraph
Pt = [
[Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
]
except:
# enquête sur exception intermittente...
log("*** bug in PDF buildTableObject:")
log("P=%s" % P)
# compris: reportlab is not thread safe !
# see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
# (donc maintenant protégé dans ScoDoc par un Lock global)
self.diagnostic = "erreur lors de la génération du PDF<br/>"
self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
return []
return Table(Pt, colWidths=colWidths, style=pdfTableStyle)
# ---------------------------------------------------------------------------
def make_formsemestre_bulletinetud(
infos,
version=None, # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=True,
):
"""Bulletin de notes
Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
selon les préférences du semestre.
"""
from app.scodoc import sco_preferences
version = version or "long"
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = None
for bul_class_name in (
sco_preferences.get_preference("bul_class_name", formsemestre_id),
# si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
bulletin_default_class_name(),
):
if infos.get("type") == "BUT" and format.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None:
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
)
try:
PDFLOCK.acquire()
bul_generator = gen_class(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
if format not in bul_generator.supported_formats:
# use standard generator
log(
"Bulletin format %s not supported by %s, using %s"
% (format, bul_class_name, bulletin_default_class_name())
)
bul_class_name = bulletin_default_class_name()
gen_class = bulletin_get_class(bul_class_name)
bul_generator = gen_class(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
data = bul_generator.generate(format=format, stand_alone=stand_alone)
finally:
PDFLOCK.release()
if bul_generator.diagnostic:
log("bul_error: %s" % bul_generator.diagnostic)
raise NoteProcessError(bul_generator.diagnostic)
filename = bul_generator.get_filename()
return data, filename
####
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_class_names() -> list[str]:
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
return [
class_name
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name: str) -> BulletinGenerator:
"""La class de génération de bulletin de ce nom,
ou None si pas trouvée
"""
return BULLETIN_CLASSES.get(class_name)
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
return "invalide ! (voir paramètres)"
return gen_class.description
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@gmail.com
#
##############################################################################
"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)
class BulletinGenerator:
description
supported_formats = [ 'pdf', 'html' ]
.bul_title_pdf()
.bul_table(format)
.bul_part_below(format)
.bul_signatures_pdf()
.__init__ et .generate(format) methodes appelees par le client (sco_bulletin)
La préférence 'bul_class_name' donne le nom de la classe generateur.
La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
"""
import collections
import io
import time
import traceback
import reportlab
from reportlab.platypus import (
SimpleDocTemplate,
DocIf,
Paragraph,
Spacer,
Frame,
PageBreak,
)
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from flask import request
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
import sco_version
class BulletinGenerator:
"Virtual superclass for PDF bulletin generators" ""
# Here some helper methods
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
scale_table_in_page = True # rescale la table sur 1 page
multi_pages = False
def __init__(
self,
infos,
authuser=None,
version="long",
filigranne=None,
server_name=None,
):
from app.scodoc import sco_preferences
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
self.infos = infos
self.authuser = authuser # nécessaire pour version HTML qui contient liens dépendant de l'utilisateur
self.version = version
self.filigranne = filigranne
self.server_name = server_name
# Store preferences for convenience:
formsemestre_id = self.infos["formsemestre_id"]
self.preferences = sco_preferences.SemPreferences(formsemestre_id)
self.diagnostic = None # error message if any problem
# Common PDF styles:
# - Pour tous les champs du bulletin sauf les cellules de table:
self.FieldStyle = reportlab.lib.styles.ParagraphStyle({})
self.FieldStyle.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.FieldStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.FieldStyle.firstLineIndent = 0
# - Pour les cellules de table:
self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
self.CellStyle.leading = (
1.0 * self.preferences["SCOLAR_FONT_SIZE"]
) # vertical space
# Marges du document PDF
self.margins = (
self.preferences["left_margin"],
self.preferences["top_margin"],
self.preferences["right_margin"],
self.preferences["bottom_margin"],
)
def get_filename(self):
"""Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
if not format in self.supported_formats:
raise ValueError("unsupported bulletin format (%s)" % format)
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if format == "html":
return self.generate_html()
elif format == "pdf":
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError("invalid bulletin format (%s)" % format)
finally:
PDFLOCK.release()
def generate_html(self):
"""Return bulletin as an HTML string"""
H = ['<div class="notes_bulletin">']
# table des notes:
H.append(self.bul_table(format="html")) # pylint: disable=no-member
# infos sous la table:
H.append(self.bul_part_below(format="html")) # pylint: disable=no-member
H.append("</div>")
return "\n".join(H)
def generate_pdf(self, stand_alone=True):
"""Build PDF bulletin from distinct parts
Si stand_alone, génère un doc PDF complet et renvoie une string
Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
from app.scodoc import sco_preferences
formsemestre_id = self.infos["formsemestre_id"]
marque_debut_bulletin = sco_pdf.DebutBulletin(
self.infos["etud"]["nomprenom"],
filigranne=self.infos["filigranne"],
footer_content=f"""ScoDoc - Bulletin de {self.infos["etud"]["nomprenom"]} - {time.strftime("%d/%m/%Y %H:%M")}""",
)
story = []
# partie haute du bulletin
story += self.bul_title_pdf() # pylint: disable=no-member
index_obj_debut = len(story)
# table des notes
story += self.bul_table(format="pdf") # pylint: disable=no-member
# infos sous la table
story += self.bul_part_below(format="pdf") # pylint: disable=no-member
# signatures
story += self.bul_signatures_pdf() # pylint: disable=no-member
if self.scale_table_in_page:
# Réduit sur une page
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
else:
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
story.insert(index_obj_debut, marque_debut_bulletin)
#
# objects.append(sco_pdf.FinBulletin())
if not stand_alone:
if self.multi_pages:
# Bulletins sur plusieurs page, force début suivant sur page impaire
story.append(
DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
)
else:
story.append(PageBreak()) # insert page break at end
return story
else:
# Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
document,
author="%s %s (E. Viennet) [%s]"
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
title="Bulletin %s de %s"
% (sem["titremois"], self.infos["etud"]["nomprenom"]),
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
)
document.build(story)
data = report.getvalue()
return data
def buildTableObject(self, P, pdfTableStyle, colWidths):
"""Utility used by some old-style generators.
Build a platypus Table instance from a nested list of cells, style and widths.
P: table, as a list of lists
PdfTableStyle: commandes de style pour la table (reportlab)
"""
try:
# put each table cell in a Paragraph
Pt = [
[Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
]
except:
# enquête sur exception intermittente...
log("*** bug in PDF buildTableObject:")
log("P=%s" % P)
# compris: reportlab is not thread safe !
# see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
# (donc maintenant protégé dans ScoDoc par un Lock global)
self.diagnostic = "erreur lors de la génération du PDF<br>"
self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
return []
return Table(Pt, colWidths=colWidths, style=pdfTableStyle)
# ---------------------------------------------------------------------------
def make_formsemestre_bulletinetud(
infos,
version=None, # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=True,
):
"""Bulletin de notes
Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
selon les préférences du semestre.
"""
from app.scodoc import sco_preferences
version = version or "long"
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = None
for bul_class_name in (
sco_preferences.get_preference("bul_class_name", formsemestre_id),
# si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
bulletin_default_class_name(),
):
if infos.get("type") == "BUT" and format.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None:
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
)
try:
PDFLOCK.acquire()
bul_generator = gen_class(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
if format not in bul_generator.supported_formats:
# use standard generator
log(
"Bulletin format %s not supported by %s, using %s"
% (format, bul_class_name, bulletin_default_class_name())
)
bul_class_name = bulletin_default_class_name()
gen_class = bulletin_get_class(bul_class_name)
bul_generator = gen_class(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
data = bul_generator.generate(format=format, stand_alone=stand_alone)
finally:
PDFLOCK.release()
if bul_generator.diagnostic:
log("bul_error: %s" % bul_generator.diagnostic)
raise NoteProcessError(bul_generator.diagnostic)
filename = bul_generator.get_filename()
return data, filename
####
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_class_names() -> list[str]:
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
return [
class_name
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name: str) -> BulletinGenerator:
"""La class de génération de bulletin de ce nom,
ou None si pas trouvée
"""
return BULLETIN_CLASSES.get(class_name)
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
return "invalide ! (voir paramètres)"
return gen_class.description

View File

@ -1,197 +1,197 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Rapports estimation coût de formation basé sur le programme pédagogique
et les nombres de groupes.
(coût théorique en heures équivalent TD)
"""
from flask import request
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences
import sco_version
def formsemestre_table_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
):
"""
Rapports estimation coût de formation basé sur le programme pédagogique
et les nombres de groupes.
Coût théorique en heures équivalent TD.
Attention: ne prend en compte que les modules utilisés dans ce semestre.
Attention: prend en compte _tous_ les modules utilisés dans ce semestre, ce qui
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sco_formsemestre_status.fill_formsemestre(sem)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
T = []
for M in Mlist:
Mod = M["module"]
T.append(
{
"code": Mod["code"] or "",
"titre": Mod["titre"],
"heures_cours": Mod["heures_cours"],
"heures_td": Mod["heures_td"] * n_group_td,
"heures_tp": Mod["heures_tp"] * n_group_tp,
}
)
# calcul des heures:
for t in T:
t["HeqTD"] = (
t["heures_td"] + coef_cours * t["heures_cours"] + coef_tp * t["heures_tp"]
)
sum_cours = sum([t["heures_cours"] for t in T])
sum_td = sum([t["heures_td"] for t in T])
sum_tp = sum([t["heures_tp"] for t in T])
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
assert abs(sum([t["HeqTD"] for t in T]) - sum_heqtd) < 0.01, "%s != %s" % (
sum([t["HeqTD"] for t in T]),
sum_heqtd,
)
T.append(
{
"code": "TOTAL SEMESTRE",
"heures_cours": sum_cours,
"heures_td": sum_td,
"heures_tp": sum_tp,
"HeqTD": sum_heqtd,
"_table_part": "foot",
}
)
titles = {
"code": "Code",
"titre": "Titre",
"heures_cours": "Cours",
"heures_td": "TD",
"heures_tp": "TP",
"HeqTD": "HeqTD",
}
tab = GenTable(
titles=titles,
columns_ids=(
"code",
"titre",
"heures_cours",
"heures_td",
"heures_tp",
"HeqTD",
),
rows=T,
html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id),
html_class="table_leftalign table_listegroupe",
xls_before_table=[
["%(titre)s %(num_sem)s %(modalitestr)s" % sem],
["Formation %(titre)s version %(version)s" % sem["formation"]],
[],
["", "TD", "TP"],
["Nombre de groupes", n_group_td, n_group_tp],
[],
[],
],
html_caption="""<div class="help">
Estimation du coût de formation basé sur le programme pédagogique
et les nombres de groupes.<br/>
Coût théorique en heures équivalent TD.<br/>
Attention: ne prend en compte que les modules utilisés dans ce semestre.<br/>
Attention: prend en compte <em>tous les modules</em> utilisés dans ce semestre, ce qui
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
</div>
""",
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
filename="EstimCout-S%s" % sem["semestre_id"],
)
return tab
def formsemestre_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
format="html",
):
"""Page (formulaire) estimation coûts"""
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
tab = formsemestre_table_estim_cost(
formsemestre_id,
n_group_td=n_group_td,
n_group_tp=n_group_tp,
coef_tp=coef_tp,
coef_cours=coef_cours,
)
h = """
<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br/>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
<br/>
</form>
""" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
tab.html_before_table = h
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
return tab.make_page(format=format)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Rapports estimation coût de formation basé sur le programme pédagogique
et les nombres de groupes.
(coût théorique en heures équivalent TD)
"""
from flask import request
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences
import sco_version
def formsemestre_table_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
):
"""
Rapports estimation coût de formation basé sur le programme pédagogique
et les nombres de groupes.
Coût théorique en heures équivalent TD.
Attention: ne prend en compte que les modules utilisés dans ce semestre.
Attention: prend en compte _tous_ les modules utilisés dans ce semestre, ce qui
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sco_formsemestre_status.fill_formsemestre(sem)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
T = []
for M in Mlist:
Mod = M["module"]
T.append(
{
"code": Mod["code"] or "",
"titre": Mod["titre"],
"heures_cours": Mod["heures_cours"],
"heures_td": Mod["heures_td"] * n_group_td,
"heures_tp": Mod["heures_tp"] * n_group_tp,
}
)
# calcul des heures:
for t in T:
t["HeqTD"] = (
t["heures_td"] + coef_cours * t["heures_cours"] + coef_tp * t["heures_tp"]
)
sum_cours = sum([t["heures_cours"] for t in T])
sum_td = sum([t["heures_td"] for t in T])
sum_tp = sum([t["heures_tp"] for t in T])
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
assert abs(sum([t["HeqTD"] for t in T]) - sum_heqtd) < 0.01, "%s != %s" % (
sum([t["HeqTD"] for t in T]),
sum_heqtd,
)
T.append(
{
"code": "TOTAL SEMESTRE",
"heures_cours": sum_cours,
"heures_td": sum_td,
"heures_tp": sum_tp,
"HeqTD": sum_heqtd,
"_table_part": "foot",
}
)
titles = {
"code": "Code",
"titre": "Titre",
"heures_cours": "Cours",
"heures_td": "TD",
"heures_tp": "TP",
"HeqTD": "HeqTD",
}
tab = GenTable(
titles=titles,
columns_ids=(
"code",
"titre",
"heures_cours",
"heures_td",
"heures_tp",
"HeqTD",
),
rows=T,
html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id),
html_class="table_leftalign table_listegroupe",
xls_before_table=[
["%(titre)s %(num_sem)s %(modalitestr)s" % sem],
["Formation %(titre)s version %(version)s" % sem["formation"]],
[],
["", "TD", "TP"],
["Nombre de groupes", n_group_td, n_group_tp],
[],
[],
],
html_caption="""<div class="help">
Estimation du coût de formation basé sur le programme pédagogique
et les nombres de groupes.<br>
Coût théorique en heures équivalent TD.<br>
Attention: ne prend en compte que les modules utilisés dans ce semestre.<br>
Attention: prend en compte <em>tous les modules</em> utilisés dans ce semestre, ce qui
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
</div>
""",
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
filename="EstimCout-S%s" % sem["semestre_id"],
)
return tab
def formsemestre_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
format="html",
):
"""Page (formulaire) estimation coûts"""
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
tab = formsemestre_table_estim_cost(
formsemestre_id,
n_group_td=n_group_td,
n_group_tp=n_group_tp,
coef_tp=coef_tp,
coef_cours=coef_cours,
)
h = """
<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
<br>
</form>
""" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
tab.html_before_table = h
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
return tab.make_page(format=format)

View File

@ -1,399 +1,399 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Page accueil département (liste des semestres, etc)
"""
from flask import g, request
from flask import url_for
from flask_login import current_user
import app
from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
import app.scodoc.notesdb as ndb
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_modalites
from app.scodoc import sco_preferences
from app.scodoc import sco_users
def index_html(showcodes=0, showsemtable=0):
"Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable)
H = []
# News:
H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""")
# Liste de toutes les sessions:
sems = sco_formsemestre.do_formsemestre_list()
cursems = [] # semestres "courants"
othersems = [] # autres (verrouillés)
# icon image:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
# Sélection sur l'etat du semestre
for sem in sems:
if sem["etat"] and sem["modalite"] != "EXT":
sem["lockimg"] = ""
cursems.append(sem)
else:
sem["lockimg"] = lockicon
othersems.append(sem)
# Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes:
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else:
sem["tmpcode"] = ""
# Nombre d'inscrits:
args = {"formsemestre_id": sem["formsemestre_id"]}
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
nb = len(ins) # nb etudiants
sem["nb_inscrits"] = nb
if nb > 0:
sem["groupicon"] = groupicon
else:
sem["groupicon"] = emptygroupicon
# S'il n'y a pas d'utilisateurs dans la base, affiche message
if not sco_users.get_user_list(dept=g.scodoc_dept):
H.append(
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
<a href="Users">passez par la page Utilisateurs</a>.
<br/>
Définissez au moins un utilisateur avec le rôle AdminXXX (le responsable du département XXX).
</p>
"""
)
# Liste des formsemestres "courants"
if cursems:
H.append('<h2 class="listesems">Sessions en cours</h2>')
H.append(_sem_table(cursems))
else:
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Programmes</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>"""
)
if showsemtable:
H.append(
f"""<hr>
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
"""
)
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html',
scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir table des semestres (dont {len(othersems)}
verrouillé{'s' if len(othersems) else ''})</a>
</p>"""
)
H.append(
f"""<p>
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
</p>"""
)
#
if current_user.has_permission(Permission.ScoEtudInscrit):
H.append(
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
</ul>
"""
)
#
if current_user.has_permission(Permission.ScoEditApo):
H.append(
f"""<hr>
<h3>Exports Apogée</h3>
<ul>
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul>
"""
)
#
H.append(
"""<hr>
<h3>Assistance</h3>
<ul>
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
</ul>
"""
)
#
return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
def _sem_table(sems):
"""Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>
</td>
</tr>
"""
# Liste des semestres, groupés par modalités
sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
H = ['<table class="listesems">']
for modalite in modalites:
if len(modalites) > 1:
H.append('<tr><th colspan="3">%s</th></tr>' % modalite["titre"])
if sems_by_mod[modalite["modalite"]]:
cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
for sem in sems_by_mod[modalite["modalite"]]:
if cur_idx != sem["semestre_id"]:
sem["trclass"] = "firstsem" # separe les groupes de semestres
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
sem["notes_url"] = scu.NotesURL()
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres
Utilise une datatables.
"""
_style_sems(sems)
columns_ids = (
"lockimg",
"semestre_id_n",
"modalite",
#'mois_debut',
"dash_mois_fin",
"titre_resp",
"nb_inscrits",
"etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
)
if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.ScoEditApo):
html_class += " apo_editable"
tab = GenTable(
titles={
"formsemestre_id": "id",
"semestre_id_n": "S#",
"modalite": "",
"mois_debut": "Début",
"dash_mois_fin": "Année",
"titre_resp": "Semestre",
"nb_inscrits": "N",
"etapes_apo_str": "Étape Apo.",
"elt_annee_apo": "Elt. année Apo.",
"elt_sem_apo": "Elt. sem. Apo.",
},
columns_ids=columns_ids,
rows=sems,
table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True,
html_table_attrs=f"""
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(),
)
return tab
def _style_sems(sems):
"""ajoute quelques attributs de présentation pour la table"""
for sem in sems:
sem["notes_url"] = scu.NotesURL()
sem["_groupicon_target"] = (
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
% sem
)
sem["_formsemestre_id_class"] = "blacktt"
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["_dash_mois_fin_class"] = "datesem"
sem["titre_resp"] = (
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>"""
% sem
)
sem["_css_row_class"] = "css_S%d css_M%s" % (
sem["semestre_id"],
sem["modalite"],
)
sem["_semestre_id_class"] = "semestre_id"
sem["_modalite_class"] = "modalite"
if sem["semestre_id"] == -1:
sem["semestre_id_n"] = ""
else:
sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
sem[
"_elt_annee_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
sem[
"_elt_sem_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
def delete_dept(dept_id: int):
"""Suppression irréversible d'un département et de tous les objets rattachés"""
assert isinstance(dept_id, int)
# Un peu complexe, merci JMP :)
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor()
try:
# 1- Create temp tables to store ids
reqs = [
"create temp table etudids_temp as select id from identite where dept_id = %(dept_id)s",
"create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
"create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
"create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
"create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
]
for r in reqs:
cursor.execute(r, {"dept_id": dept_id})
# 2- Delete student-related informations
# ordered list of tables
etud_tables = [
"notes_notes",
"group_membership",
"admissions",
"billet_absence",
"adresse",
"absences",
"notes_notes_log",
"notes_moduleimpl_inscription",
"itemsuivi",
"notes_appreciations",
"scolar_autorisation_inscription",
"absences_notifications",
"notes_formsemestre_inscription",
"scolar_formsemestre_validation",
"scolar_events",
]
for table in etud_tables:
cursor.execute(
f"delete from {table} where etudid in (select id from etudids_temp)"
)
reqs = [
"delete from identite where dept_id = %(dept_id)s",
"delete from sco_prefs where dept_id = %(dept_id)s",
"delete from notes_semset_formsemestre where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_evaluation where moduleimpl_id in (select id from moduleimpls_temp)",
"delete from notes_modules_enseignants where moduleimpl_id in (select id from moduleimpls_temp)",
"delete from notes_formsemestre_uecoef where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_ue_computation_expr where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_responsables where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_modules_tags where tag_id in (select id from tags_temp)",
"delete from notes_tags where dept_id = %(dept_id)s",
"delete from notes_modules where formation_id in (select id from formations_temp)",
"delete from notes_matieres where ue_id in (select id from notes_ue where formation_id in (select id from formations_temp))",
"delete from notes_formsemestre_etapes where formsemestre_id in (select id from formsemestres_temp)",
"delete from group_descr where partition_id in (select id from partition where formsemestre_id in (select id from formsemestres_temp))",
"delete from partition where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_custommenu where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_ue where formation_id in (select id from formations_temp)",
"delete from notes_formsemestre where dept_id = %(dept_id)s",
"delete from scolar_news where dept_id = %(dept_id)s",
"delete from notes_semset where dept_id = %(dept_id)s",
"delete from notes_formations where dept_id = %(dept_id)s",
"delete from departement where id = %(dept_id)s",
"drop table tags_temp",
"drop table formations_temp",
"drop table moduleimpls_temp",
"drop table etudids_temp",
"drop table formsemestres_temp",
]
for r in reqs:
cursor.execute(r, {"dept_id": dept_id})
except:
cnx.rollback()
finally:
cnx.commit()
app.clear_scodoc_cache()
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Page accueil département (liste des semestres, etc)
"""
from flask import g, request
from flask import url_for
from flask_login import current_user
import app
from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
import app.scodoc.notesdb as ndb
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_modalites
from app.scodoc import sco_preferences
from app.scodoc import sco_users
def index_html(showcodes=0, showsemtable=0):
"Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable)
H = []
# News:
H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""")
# Liste de toutes les sessions:
sems = sco_formsemestre.do_formsemestre_list()
cursems = [] # semestres "courants"
othersems = [] # autres (verrouillés)
# icon image:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
# Sélection sur l'etat du semestre
for sem in sems:
if sem["etat"] and sem["modalite"] != "EXT":
sem["lockimg"] = ""
cursems.append(sem)
else:
sem["lockimg"] = lockicon
othersems.append(sem)
# Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes:
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else:
sem["tmpcode"] = ""
# Nombre d'inscrits:
args = {"formsemestre_id": sem["formsemestre_id"]}
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
nb = len(ins) # nb etudiants
sem["nb_inscrits"] = nb
if nb > 0:
sem["groupicon"] = groupicon
else:
sem["groupicon"] = emptygroupicon
# S'il n'y a pas d'utilisateurs dans la base, affiche message
if not sco_users.get_user_list(dept=g.scodoc_dept):
H.append(
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
<a href="Users">passez par la page Utilisateurs</a>.
<br>
Définissez au moins un utilisateur avec le rôle AdminXXX (le responsable du département XXX).
</p>
"""
)
# Liste des formsemestres "courants"
if cursems:
H.append('<h2 class="listesems">Sessions en cours</h2>')
H.append(_sem_table(cursems))
else:
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Programmes</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>"""
)
if showsemtable:
H.append(
f"""<hr>
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
"""
)
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html',
scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir table des semestres (dont {len(othersems)}
verrouillé{'s' if len(othersems) else ''})</a>
</p>"""
)
H.append(
f"""<p>
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
</p>"""
)
#
if current_user.has_permission(Permission.ScoEtudInscrit):
H.append(
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
</ul>
"""
)
#
if current_user.has_permission(Permission.ScoEditApo):
H.append(
f"""<hr>
<h3>Exports Apogée</h3>
<ul>
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul>
"""
)
#
H.append(
"""<hr>
<h3>Assistance</h3>
<ul>
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
</ul>
"""
)
#
return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
def _sem_table(sems):
"""Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>
</td>
</tr>
"""
# Liste des semestres, groupés par modalités
sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
H = ['<table class="listesems">']
for modalite in modalites:
if len(modalites) > 1:
H.append('<tr><th colspan="3">%s</th></tr>' % modalite["titre"])
if sems_by_mod[modalite["modalite"]]:
cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
for sem in sems_by_mod[modalite["modalite"]]:
if cur_idx != sem["semestre_id"]:
sem["trclass"] = "firstsem" # separe les groupes de semestres
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
sem["notes_url"] = scu.NotesURL()
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres
Utilise une datatables.
"""
_style_sems(sems)
columns_ids = (
"lockimg",
"semestre_id_n",
"modalite",
#'mois_debut',
"dash_mois_fin",
"titre_resp",
"nb_inscrits",
"etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
)
if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.ScoEditApo):
html_class += " apo_editable"
tab = GenTable(
titles={
"formsemestre_id": "id",
"semestre_id_n": "S#",
"modalite": "",
"mois_debut": "Début",
"dash_mois_fin": "Année",
"titre_resp": "Semestre",
"nb_inscrits": "N",
"etapes_apo_str": "Étape Apo.",
"elt_annee_apo": "Elt. année Apo.",
"elt_sem_apo": "Elt. sem. Apo.",
},
columns_ids=columns_ids,
rows=sems,
table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True,
html_table_attrs=f"""
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(),
)
return tab
def _style_sems(sems):
"""ajoute quelques attributs de présentation pour la table"""
for sem in sems:
sem["notes_url"] = scu.NotesURL()
sem["_groupicon_target"] = (
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
% sem
)
sem["_formsemestre_id_class"] = "blacktt"
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["_dash_mois_fin_class"] = "datesem"
sem["titre_resp"] = (
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>"""
% sem
)
sem["_css_row_class"] = "css_S%d css_M%s" % (
sem["semestre_id"],
sem["modalite"],
)
sem["_semestre_id_class"] = "semestre_id"
sem["_modalite_class"] = "modalite"
if sem["semestre_id"] == -1:
sem["semestre_id_n"] = ""
else:
sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
sem[
"_elt_annee_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
sem[
"_elt_sem_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
def delete_dept(dept_id: int):
"""Suppression irréversible d'un département et de tous les objets rattachés"""
assert isinstance(dept_id, int)
# Un peu complexe, merci JMP :)
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor()
try:
# 1- Create temp tables to store ids
reqs = [
"create temp table etudids_temp as select id from identite where dept_id = %(dept_id)s",
"create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
"create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
"create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
"create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
]
for r in reqs:
cursor.execute(r, {"dept_id": dept_id})
# 2- Delete student-related informations
# ordered list of tables
etud_tables = [
"notes_notes",
"group_membership",
"admissions",
"billet_absence",
"adresse",
"absences",
"notes_notes_log",
"notes_moduleimpl_inscription",
"itemsuivi",
"notes_appreciations",
"scolar_autorisation_inscription",
"absences_notifications",
"notes_formsemestre_inscription",
"scolar_formsemestre_validation",
"scolar_events",
]
for table in etud_tables:
cursor.execute(
f"delete from {table} where etudid in (select id from etudids_temp)"
)
reqs = [
"delete from identite where dept_id = %(dept_id)s",
"delete from sco_prefs where dept_id = %(dept_id)s",
"delete from notes_semset_formsemestre where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_evaluation where moduleimpl_id in (select id from moduleimpls_temp)",
"delete from notes_modules_enseignants where moduleimpl_id in (select id from moduleimpls_temp)",
"delete from notes_formsemestre_uecoef where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_ue_computation_expr where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_responsables where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_modules_tags where tag_id in (select id from tags_temp)",
"delete from notes_tags where dept_id = %(dept_id)s",
"delete from notes_modules where formation_id in (select id from formations_temp)",
"delete from notes_matieres where ue_id in (select id from notes_ue where formation_id in (select id from formations_temp))",
"delete from notes_formsemestre_etapes where formsemestre_id in (select id from formsemestres_temp)",
"delete from group_descr where partition_id in (select id from partition where formsemestre_id in (select id from formsemestres_temp))",
"delete from partition where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_formsemestre_custommenu where formsemestre_id in (select id from formsemestres_temp)",
"delete from notes_ue where formation_id in (select id from formations_temp)",
"delete from notes_formsemestre where dept_id = %(dept_id)s",
"delete from scolar_news where dept_id = %(dept_id)s",
"delete from notes_semset where dept_id = %(dept_id)s",
"delete from notes_formations where dept_id = %(dept_id)s",
"delete from departement where id = %(dept_id)s",
"drop table tags_temp",
"drop table formations_temp",
"drop table moduleimpls_temp",
"drop table etudids_temp",
"drop table formsemestres_temp",
]
for r in reqs:
cursor.execute(r, {"dept_id": dept_id})
except:
cnx.rollback()
finally:
cnx.commit()
app.clear_scodoc_cache()

File diff suppressed because it is too large Load Diff

View File

@ -907,7 +907,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
if etud["villelycee"]:
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
etud["ilycee"] += "<br/>"
etud["ilycee"] += "<br>"
else:
if etud.get("codelycee"):
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])

View File

@ -1,257 +1,257 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Vérification des absences à une évaluation
"""
from flask import url_for, g
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
# matin et/ou après-midi ?
def _eval_demijournee(E):
"1 si matin, 0 si apres midi, 2 si toute la journee"
am, pm = False, False
if E["heure_debut"] < "13:00":
am = True
if E["heure_fin"] > "13:00":
pm = True
if am and pm:
demijournee = 2
elif am:
demijournee = 1
else:
demijournee = 0
pm = True
return am, pm, demijournee
def evaluation_check_absences(evaluation_id):
"""Vérifie les absences au moment de cette évaluation.
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
note et absent
ABS et pas noté absent
ABS et absent justifié
EXC et pas noté absent
EXC et pas justifie
Ramene 3 listes d'etudid
"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
if not E["jour"]:
return [], [], [], [], [] # evaluation sans date
am, pm, demijournee = _eval_demijournee(E)
# Liste les absences à ce moment:
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
Just = sco_abs.list_abs_jour(
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
)
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
# Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
ValButAbs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True
):
if etudid in notes_db:
val = notes_db[etudid]["value"]
if (
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
) and etudid in As:
# note valide et absent
ValButAbs.append(etudid)
if val is None and not etudid in As:
# absent mais pas signale comme tel
AbsNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and not etudid in As:
# Neutralisé mais pas signale absent
ExcNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
# EXC mais pas justifié
ExcNonJust.append(etudid)
if val is None and etudid in Justs:
# ABS mais justificatif
AbsButExc.append(etudid)
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
"""Affiche état vérification absences d'une évaluation"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
am, pm, demijournee = _eval_demijournee(E)
(
ValButAbs,
AbsNonSignalee,
ExcNonSignalee,
ExcNonJust,
AbsButExc,
) = evaluation_check_absences(evaluation_id)
if with_header:
H = [
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
]
else:
# pas de header, mais un titre
H = [
"""<h2 class="eval_check_absences">%s du %s """
% (E["description"], E["jour"])
]
if (
not ValButAbs
and not AbsNonSignalee
and not ExcNonSignalee
and not ExcNonJust
):
H.append(': <span class="eval_check_absences_ok">ok</span>')
H.append("</h2>")
def etudlist(etudids, linkabs=False):
H.append("<ul>")
if not etudids and show_ok:
H.append("<li>aucun</li>")
for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H.append(
'<li><a class="discretelink" href="%s">'
% url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
+ "%(nomprenom)s</a>" % etud
)
if linkabs:
H.append(
f"""<a class="stdlink" href="{url_for(
'absences.doSignaleAbsence',
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
datedebut=E["jour"],
datefin=E["jour"],
demijournee=demijournee,
moduleimpl_id=E["moduleimpl_id"],
)
}">signaler cette absence</a>"""
)
H.append("</li>")
H.append("</ul>")
if ValButAbs or show_ok:
H.append(
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
)
etudlist(ValButAbs)
if AbsNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(AbsNonSignalee, linkabs=True)
if ExcNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(ExcNonSignalee)
if ExcNonJust or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
)
etudlist(ExcNonJust)
if AbsButExc or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
)
etudlist(AbsButExc)
if with_header:
H.append(html_sco_header.sco_footer())
return "\n".join(H)
def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
</p>""",
]
# Modules, dans l'ordre
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for M in Mlist:
evals = sco_evaluation_db.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]}
)
if evals:
H.append(
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
% (
M["moduleimpl_id"],
M["module"]["code"] or "",
M["module"]["abbrev"] or "",
)
)
for E in evals:
H.append(
evaluation_check_absences_html(
E["evaluation_id"],
with_header=False,
show_ok=False,
)
)
if evals:
H.append("</div>")
H.append(html_sco_header.sco_footer())
return "\n".join(H)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Vérification des absences à une évaluation
"""
from flask import url_for, g
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
# matin et/ou après-midi ?
def _eval_demijournee(E):
"1 si matin, 0 si apres midi, 2 si toute la journee"
am, pm = False, False
if E["heure_debut"] < "13:00":
am = True
if E["heure_fin"] > "13:00":
pm = True
if am and pm:
demijournee = 2
elif am:
demijournee = 1
else:
demijournee = 0
pm = True
return am, pm, demijournee
def evaluation_check_absences(evaluation_id):
"""Vérifie les absences au moment de cette évaluation.
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
note et absent
ABS et pas noté absent
ABS et absent justifié
EXC et pas noté absent
EXC et pas justifie
Ramene 3 listes d'etudid
"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
if not E["jour"]:
return [], [], [], [], [] # evaluation sans date
am, pm, demijournee = _eval_demijournee(E)
# Liste les absences à ce moment:
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
Just = sco_abs.list_abs_jour(
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
)
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
# Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
ValButAbs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True
):
if etudid in notes_db:
val = notes_db[etudid]["value"]
if (
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
) and etudid in As:
# note valide et absent
ValButAbs.append(etudid)
if val is None and not etudid in As:
# absent mais pas signale comme tel
AbsNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and not etudid in As:
# Neutralisé mais pas signale absent
ExcNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
# EXC mais pas justifié
ExcNonJust.append(etudid)
if val is None and etudid in Justs:
# ABS mais justificatif
AbsButExc.append(etudid)
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
"""Affiche état vérification absences d'une évaluation"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
am, pm, demijournee = _eval_demijournee(E)
(
ValButAbs,
AbsNonSignalee,
ExcNonSignalee,
ExcNonJust,
AbsButExc,
) = evaluation_check_absences(evaluation_id)
if with_header:
H = [
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
]
else:
# pas de header, mais un titre
H = [
"""<h2 class="eval_check_absences">%s du %s """
% (E["description"], E["jour"])
]
if (
not ValButAbs
and not AbsNonSignalee
and not ExcNonSignalee
and not ExcNonJust
):
H.append(': <span class="eval_check_absences_ok">ok</span>')
H.append("</h2>")
def etudlist(etudids, linkabs=False):
H.append("<ul>")
if not etudids and show_ok:
H.append("<li>aucun</li>")
for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H.append(
'<li><a class="discretelink" href="%s">'
% url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
+ "%(nomprenom)s</a>" % etud
)
if linkabs:
H.append(
f"""<a class="stdlink" href="{url_for(
'absences.doSignaleAbsence',
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
datedebut=E["jour"],
datefin=E["jour"],
demijournee=demijournee,
moduleimpl_id=E["moduleimpl_id"],
)
}">signaler cette absence</a>"""
)
H.append("</li>")
H.append("</ul>")
if ValButAbs or show_ok:
H.append(
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
)
etudlist(ValButAbs)
if AbsNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(AbsNonSignalee, linkabs=True)
if ExcNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(ExcNonSignalee)
if ExcNonJust or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
)
etudlist(ExcNonJust)
if AbsButExc or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
)
etudlist(AbsButExc)
if with_header:
H.append(html_sco_header.sco_footer())
return "\n".join(H)
def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
Sont listés tous les modules avec des évaluations.<br>Aucune action n'est effectuée:
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
</p>""",
]
# Modules, dans l'ordre
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for M in Mlist:
evals = sco_evaluation_db.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]}
)
if evals:
H.append(
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
% (
M["moduleimpl_id"],
M["module"]["code"] or "",
M["module"]["abbrev"] or "",
)
)
for E in evals:
H.append(
evaluation_check_absences_html(
E["evaluation_id"],
with_header=False,
show_ok=False,
)
)
if evals:
H.append("</div>")
H.append(html_sco_header.sco_footer())
return "\n".join(H)

View File

@ -1,411 +1,411 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Recherche d'étudiants
"""
import flask
from flask import url_for, g, request
from flask_login import current_user
import app
from app.models import Departement
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
def form_search_etud(
dest_url=None,
parameters=None,
parameters_keys=None,
title="Rechercher un étudiant par nom&nbsp;: ",
add_headers=False, # complete page
):
"form recherche par nom"
H = []
H.append(
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
<b>{title}</b>
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher">
<br/>(entrer une partie du nom)
"""
)
if dest_url:
H.append('<input type="hidden" name="dest_url" value="%s"/>' % dest_url)
if parameters:
for param in parameters.keys():
H.append(
'<input type="hidden" name="%s" value="%s"/>'
% (param, parameters[param])
)
H.append(
'<input type="hidden" name="parameters_keys" value="%s"/>'
% (",".join(parameters.keys()))
)
elif parameters_keys:
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
for key in parameters_keys.split(","):
v = vals.get(key, False)
if v:
H.append('<input type="hidden" name="%s" value="%s"/>' % (key, v))
H.append(
'<input type="hidden" name="parameters_keys" value="%s"/>' % parameters_keys
)
H.append("</form>")
if add_headers:
return (
html_sco_header.sco_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
else:
return "\n".join(H)
def search_etud_in_dept(expnom=""):
"""Page recherche d'un etudiant.
Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats,
la liste des étudiants correspondants.
Appelée par:
- boite de recherche barre latérale gauche.
- choix d'un étudiant à inscrire (en POST avec dest_url et parameters_keys)
Args:
expnom: string, regexp sur le nom ou un code_nip ou un etudid
"""
if isinstance(expnom, int) or len(expnom) > 1:
try:
etudid = int(expnom)
except ValueError:
etudid = None
if etudid is not None:
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
if (etudid is None) or len(etuds) != 1:
expnom_str = str(expnom)
if scu.is_valid_code_nip(expnom_str):
etuds = search_etuds_infos(code_nip=expnom_str)
else:
etuds = search_etuds_infos(expnom=expnom_str)
else:
etuds = [] # si expnom est trop court, n'affiche rien
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
url_args = {"scodoc_dept": g.scodoc_dept}
if "dest_url" in vals:
endpoint = vals["dest_url"]
else:
endpoint = "scolar.ficheEtud"
if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","):
url_args[key] = vals[key]
if len(etuds) == 1:
# va directement a la fiche
url_args["etudid"] = etuds[0]["etudid"]
return flask.redirect(url_for(endpoint, **url_args))
H = [
html_sco_header.sco_header(
page_title="Recherche d'un étudiant",
no_side_bar=False,
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
if len(etuds) == 0 and len(etuds) <= 1:
H.append("""<h2>chercher un étudiant:</h2>""")
else:
H.append(
f"""<h2>{len(etuds)} résultats pour "<tt>{expnom}</tt>": choisissez un étudiant:</h2>"""
)
H.append(
form_search_etud(
dest_url=endpoint,
parameters=vals.get("parameters"),
parameters_keys=vals.get("parameters_keys"),
title="Autre recherche",
)
)
if len(etuds) > 0:
# Choix dans la liste des résultats:
for e in etuds:
url_args["etudid"] = e["etudid"]
target = url_for(endpoint, **url_args)
e["_nomprenom_target"] = target
e["inscription_target"] = target
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
sco_groups.etud_add_group_infos(
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
)
tab = GenTable(
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
titles={
"nomprenom": "Étudiant",
"code_nip": "NIP",
"inscription": "Inscription",
"groupes": "Groupes",
},
rows=etuds,
html_sortable=True,
html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(),
)
H.append(tab.html())
if len(etuds) > 20: # si la page est grande
H.append(
form_search_etud(
dest_url=endpoint,
parameters=vals.get("parameters"),
parameters_keys=vals.get("parameters_keys"),
title="Autre recherche",
)
)
else:
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
H.append(
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.</p>"""
)
return "\n".join(H) + html_sco_header.sco_footer()
# Was chercheEtudsInfo()
def search_etuds_infos(expnom=None, code_nip=None):
"""recherche les étudiants correspondants à expnom ou au code_nip
et ramene liste de mappings utilisables en DTML.
"""
may_be_nip = scu.is_valid_code_nip(expnom)
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase
try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else:
code_nip = code_nip or expnom
if code_nip:
etuds = sco_etud.etudident_list(cnx, args={"code_nip": str(code_nip)})
else:
etuds = []
sco_etud.fill_etuds_info(etuds)
return etuds
def search_etud_by_name(term: str) -> list:
"""Recherche noms étudiants par début du nom, pour autocomplete
Accepte aussi un début de code NIP (au moins 6 caractères)
Renvoie une liste de dicts
{ "label" : "<nip> <nom> <prenom>", "value" : etudid }
"""
may_be_nip = scu.is_valid_code_nip(term)
# term = term.upper() # conserve les accents
term = term.upper()
if (
not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
and not may_be_nip
):
data = []
else:
if may_be_nip:
r = ndb.SimpleDictFetch(
"""SELECT nom, prenom, code_nip
FROM identite
WHERE
dept_id = %(dept_id)s
AND code_nip LIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [
{
"label": "%s %s %s"
% (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
"value": x["code_nip"],
}
for x in r
]
else:
r = ndb.SimpleDictFetch(
"""SELECT id AS etudid, nom, prenom
FROM identite
WHERE
dept_id = %(dept_id)s
AND nom LIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [
{
"label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
"value": x["etudid"],
}
for x in r
]
return data
# ---------- Recherche sur plusieurs département
def search_etud_in_accessible_depts(expnom=None, code_nip=None):
"""
result is a list of (sorted) etuds, one list per dept.
"""
result = []
accessible_depts = []
depts = Departement.query.filter_by(visible=True).all()
for dept in depts:
if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
if expnom or code_nip:
accessible_depts.append(dept.acronym)
app.set_sco_dept(dept.acronym)
etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
else:
etuds = []
result.append(etuds)
return result, accessible_depts
def table_etud_in_accessible_depts(expnom=None):
"""
Page avec table étudiants trouvés, dans tous les departements.
Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
"""
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
H = [
"""<div class="table_etud_in_accessible_depts">""",
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
]
for etuds in result:
if etuds:
dept_id = etuds[0]["dept"]
# H.append('<h3>Département %s</h3>' % DeptId)
for e in etuds:
e["_nomprenom_target"] = url_for(
"scolar.ficheEtud", scodoc_dept=dept_id, etudid=e["etudid"]
)
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
tab = GenTable(
titles={"nomprenom": "Étudiants en " + dept_id},
columns_ids=("nomprenom",),
rows=etuds,
html_sortable=True,
html_class="table_leftalign",
)
H.append('<div class="table_etud_in_dept">')
H.append(tab.html())
H.append("</div>")
if len(accessible_depts) > 1:
ss = "s"
else:
ss = ""
H.append(
f"""<p>(recherche menée dans le{ss} département{ss}:
{", ".join(accessible_depts)})
</p>
<p>
<a href="{url_for("scodoc.index")}" class="stdlink">Retour à l'accueil</a>
</p>
</div>
"""
)
return (
html_sco_header.scodoc_top_html_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.standard_html_footer()
)
def search_inscr_etud_by_nip(code_nip, format="json"):
"""Recherche multi-departement d'un étudiant par son code NIP
Seuls les départements accessibles par l'utilisateur sont cherchés.
Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
"""
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
T = []
for etuds in result:
if etuds:
dept_id = etuds[0]["dept"]
for e in etuds:
for sem in e["sems"]:
T.append(
{
"dept": dept_id,
"etudid": e["etudid"],
"code_nip": e["code_nip"],
"civilite_str": e["civilite_str"],
"nom": e["nom"],
"prenom": e["prenom"],
"formsemestre_id": sem["formsemestre_id"],
"date_debut_iso": sem["date_debut_iso"],
"date_fin_iso": sem["date_fin_iso"],
}
)
columns_ids = (
"dept",
"etudid",
"code_nip",
"civilite_str",
"nom",
"prenom",
"formsemestre_id",
"date_debut_iso",
"date_fin_iso",
)
tab = GenTable(columns_ids=columns_ids, rows=T)
return tab.make_page(format=format, with_html_headers=False, publish=True)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Recherche d'étudiants
"""
import flask
from flask import url_for, g, request
from flask_login import current_user
import app
from app.models import Departement
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
def form_search_etud(
dest_url=None,
parameters=None,
parameters_keys=None,
title="Rechercher un étudiant par nom&nbsp;: ",
add_headers=False, # complete page
):
"form recherche par nom"
H = []
H.append(
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
<b>{title}</b>
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher">
<br>(entrer une partie du nom)
"""
)
if dest_url:
H.append('<input type="hidden" name="dest_url" value="%s"/>' % dest_url)
if parameters:
for param in parameters.keys():
H.append(
'<input type="hidden" name="%s" value="%s"/>'
% (param, parameters[param])
)
H.append(
'<input type="hidden" name="parameters_keys" value="%s"/>'
% (",".join(parameters.keys()))
)
elif parameters_keys:
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
for key in parameters_keys.split(","):
v = vals.get(key, False)
if v:
H.append('<input type="hidden" name="%s" value="%s"/>' % (key, v))
H.append(
'<input type="hidden" name="parameters_keys" value="%s"/>' % parameters_keys
)
H.append("</form>")
if add_headers:
return (
html_sco_header.sco_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
else:
return "\n".join(H)
def search_etud_in_dept(expnom=""):
"""Page recherche d'un etudiant.
Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats,
la liste des étudiants correspondants.
Appelée par:
- boite de recherche barre latérale gauche.
- choix d'un étudiant à inscrire (en POST avec dest_url et parameters_keys)
Args:
expnom: string, regexp sur le nom ou un code_nip ou un etudid
"""
if isinstance(expnom, int) or len(expnom) > 1:
try:
etudid = int(expnom)
except ValueError:
etudid = None
if etudid is not None:
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
if (etudid is None) or len(etuds) != 1:
expnom_str = str(expnom)
if scu.is_valid_code_nip(expnom_str):
etuds = search_etuds_infos(code_nip=expnom_str)
else:
etuds = search_etuds_infos(expnom=expnom_str)
else:
etuds = [] # si expnom est trop court, n'affiche rien
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
url_args = {"scodoc_dept": g.scodoc_dept}
if "dest_url" in vals:
endpoint = vals["dest_url"]
else:
endpoint = "scolar.ficheEtud"
if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","):
url_args[key] = vals[key]
if len(etuds) == 1:
# va directement a la fiche
url_args["etudid"] = etuds[0]["etudid"]
return flask.redirect(url_for(endpoint, **url_args))
H = [
html_sco_header.sco_header(
page_title="Recherche d'un étudiant",
no_side_bar=False,
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
if len(etuds) == 0 and len(etuds) <= 1:
H.append("""<h2>chercher un étudiant:</h2>""")
else:
H.append(
f"""<h2>{len(etuds)} résultats pour "<tt>{expnom}</tt>": choisissez un étudiant:</h2>"""
)
H.append(
form_search_etud(
dest_url=endpoint,
parameters=vals.get("parameters"),
parameters_keys=vals.get("parameters_keys"),
title="Autre recherche",
)
)
if len(etuds) > 0:
# Choix dans la liste des résultats:
for e in etuds:
url_args["etudid"] = e["etudid"]
target = url_for(endpoint, **url_args)
e["_nomprenom_target"] = target
e["inscription_target"] = target
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
sco_groups.etud_add_group_infos(
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
)
tab = GenTable(
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
titles={
"nomprenom": "Étudiant",
"code_nip": "NIP",
"inscription": "Inscription",
"groupes": "Groupes",
},
rows=etuds,
html_sortable=True,
html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(),
)
H.append(tab.html())
if len(etuds) > 20: # si la page est grande
H.append(
form_search_etud(
dest_url=endpoint,
parameters=vals.get("parameters"),
parameters_keys=vals.get("parameters_keys"),
title="Autre recherche",
)
)
else:
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
H.append(
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.</p>"""
)
return "\n".join(H) + html_sco_header.sco_footer()
# Was chercheEtudsInfo()
def search_etuds_infos(expnom=None, code_nip=None):
"""recherche les étudiants correspondants à expnom ou au code_nip
et ramene liste de mappings utilisables en DTML.
"""
may_be_nip = scu.is_valid_code_nip(expnom)
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase
try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else:
code_nip = code_nip or expnom
if code_nip:
etuds = sco_etud.etudident_list(cnx, args={"code_nip": str(code_nip)})
else:
etuds = []
sco_etud.fill_etuds_info(etuds)
return etuds
def search_etud_by_name(term: str) -> list:
"""Recherche noms étudiants par début du nom, pour autocomplete
Accepte aussi un début de code NIP (au moins 6 caractères)
Renvoie une liste de dicts
{ "label" : "<nip> <nom> <prenom>", "value" : etudid }
"""
may_be_nip = scu.is_valid_code_nip(term)
# term = term.upper() # conserve les accents
term = term.upper()
if (
not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
and not may_be_nip
):
data = []
else:
if may_be_nip:
r = ndb.SimpleDictFetch(
"""SELECT nom, prenom, code_nip
FROM identite
WHERE
dept_id = %(dept_id)s
AND code_nip LIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [
{
"label": "%s %s %s"
% (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
"value": x["code_nip"],
}
for x in r
]
else:
r = ndb.SimpleDictFetch(
"""SELECT id AS etudid, nom, prenom
FROM identite
WHERE
dept_id = %(dept_id)s
AND nom LIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [
{
"label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
"value": x["etudid"],
}
for x in r
]
return data
# ---------- Recherche sur plusieurs département
def search_etud_in_accessible_depts(expnom=None, code_nip=None):
"""
result is a list of (sorted) etuds, one list per dept.
"""
result = []
accessible_depts = []
depts = Departement.query.filter_by(visible=True).all()
for dept in depts:
if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
if expnom or code_nip:
accessible_depts.append(dept.acronym)
app.set_sco_dept(dept.acronym)
etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
else:
etuds = []
result.append(etuds)
return result, accessible_depts
def table_etud_in_accessible_depts(expnom=None):
"""
Page avec table étudiants trouvés, dans tous les departements.
Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
"""
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
H = [
"""<div class="table_etud_in_accessible_depts">""",
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
]
for etuds in result:
if etuds:
dept_id = etuds[0]["dept"]
# H.append('<h3>Département %s</h3>' % DeptId)
for e in etuds:
e["_nomprenom_target"] = url_for(
"scolar.ficheEtud", scodoc_dept=dept_id, etudid=e["etudid"]
)
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
tab = GenTable(
titles={"nomprenom": "Étudiants en " + dept_id},
columns_ids=("nomprenom",),
rows=etuds,
html_sortable=True,
html_class="table_leftalign",
)
H.append('<div class="table_etud_in_dept">')
H.append(tab.html())
H.append("</div>")
if len(accessible_depts) > 1:
ss = "s"
else:
ss = ""
H.append(
f"""<p>(recherche menée dans le{ss} département{ss}:
{", ".join(accessible_depts)})
</p>
<p>
<a href="{url_for("scodoc.index")}" class="stdlink">Retour à l'accueil</a>
</p>
</div>
"""
)
return (
html_sco_header.scodoc_top_html_header(page_title="Choix d'un étudiant")
+ "\n".join(H)
+ html_sco_header.standard_html_footer()
)
def search_inscr_etud_by_nip(code_nip, format="json"):
"""Recherche multi-departement d'un étudiant par son code NIP
Seuls les départements accessibles par l'utilisateur sont cherchés.
Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
"""
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
T = []
for etuds in result:
if etuds:
dept_id = etuds[0]["dept"]
for e in etuds:
for sem in e["sems"]:
T.append(
{
"dept": dept_id,
"etudid": e["etudid"],
"code_nip": e["code_nip"],
"civilite_str": e["civilite_str"],
"nom": e["nom"],
"prenom": e["prenom"],
"formsemestre_id": sem["formsemestre_id"],
"date_debut_iso": sem["date_debut_iso"],
"date_fin_iso": sem["date_fin_iso"],
}
)
columns_ids = (
"dept",
"etudid",
"code_nip",
"civilite_str",
"nom",
"prenom",
"formsemestre_id",
"date_debut_iso",
"date_fin_iso",
)
tab = GenTable(columns_ids=columns_ids, rows=T)
return tab.make_page(format=format, with_html_headers=False, publish=True)

View File

@ -1659,7 +1659,7 @@ def formsemestre_change_publication_bul(
"<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
<br/>
<br>
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.
""",
dest_url="",

View File

@ -893,7 +893,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
H.append("</ul>")
H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
H.append(
"""<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br/>Sauf exception, cette situation est anormale:</p>
"""<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br>Sauf exception, cette situation est anormale:</p>
<ul>
<li>vérifier que les dates des semestres se suivent sans se chevaucher</li>
<li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li>

View File

@ -349,7 +349,7 @@ def formsemestre_validation_etud_form(
H.append("</table>")
H.append(
'<p><br/></p><input type="submit" value="Valider ce choix" disabled="1" id="subut"/>'
'<p><br></p><input type="submit" value="Valider ce choix" disabled="1" id="subut"/>'
)
H.append("</form>")
@ -1299,7 +1299,7 @@ def check_formation_ues(formation_id):
H = [
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
sont utilisées dans des
semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour
semestres de rangs différents (eg S1 et S3). <br>Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre),
soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une

View File

@ -302,7 +302,7 @@ def scolars_import_excel_file(
else:
unknown.append(f)
raise ScoValueError(
"""Nombre de colonnes incorrect (devrait être %d, et non %d)<br/>
"""Nombre de colonnes incorrect (devrait être %d, et non %d)<br>
(colonnes manquantes: %s, colonnes invalides: %s)"""
% (len(titles), len(fs), list(missing.keys()), unknown)
)

View File

@ -1,312 +1,312 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Import d'utilisateurs via fichier Excel
"""
import random
import time
from email.mime.multipart import MIMEMultipart
from flask import g, url_for
from flask_login import current_user
from app import db
from app import email
from app.auth.models import User, UserRole
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_excel
from app.scodoc import sco_preferences
from app.scodoc import sco_users
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
COMMENTS = (
"""user_name:
Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
""",
"""nom:
Maximum 64 caractères""",
"""prenom:
Maximum 64 caractères""",
"""email:
Maximum 120 caractères""",
"""roles:
un plusieurs rôles séparés par ','
chaque role est fait de 2 composantes séparées par _:
1. Le role (Ens, Secr ou Admin)
2. Le département (en majuscule)
Exemple: "Ens_RT,Admin_INFO"
""",
"""dept:
Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements
""",
)
def generate_excel_sample():
"""generates an excel document suitable to import users"""
style = sco_excel.excel_make_style(bold=True)
titles = TITLES
titles_styles = [style] * len(titles)
return sco_excel.excel_simple_table(
titles=titles,
titles_styles=titles_styles,
sheet_name="Utilisateurs ScoDoc",
comments=COMMENTS,
)
def import_excel_file(datafile, force=""):
"""
Import scodoc users from Excel file.
This method:
* checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
* Once the check is done ans successfull, build the list of users (does not check the data)
* call :func:`import_users` to actually do the job
history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
:param datafile: the stream from to the to be imported
:return: same as import users
"""
# Check current user privilege
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
# Récupération des informations sur l'utilisateur courant
log("sco_import_users.import_excel_file by %s" % auth_name)
# Read the data from the stream
exceldata = datafile.read()
if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata)
if not data:
raise ScoValueError(
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
# check cols
cols = {}.fromkeys(TITLES)
unknown = []
for tit in fs:
if tit not in cols:
unknown.append(tit)
else:
del cols[tit]
if cols or unknown:
raise ScoValueError(
"""colonnes incorrectes (on attend %d, et non %d) <br/>
(colonnes manquantes: %s, colonnes invalides: %s)"""
% (len(TITLES), len(fs), list(cols.keys()), unknown)
)
# ok, same titles... : build the list of dictionaries
users = []
for line in data[1:]:
d = {}
for i in range(len(fs)):
d[fs[i]] = line[i]
users.append(d)
return import_users(users=users, force=force)
def import_users(users, force=""):
"""
Import users from a list of users_descriptors.
descriptors are dictionaries hosting users's data.
The operation is atomic (all the users are imported or none)
:param users: list of descriptors to be imported
:return: a tuple that describe the result of the import:
* ok: import ok or aborted
* messages: the list of messages
* the # of users created
Implémentation:
Pour chaque utilisateur à créer:
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
* générer mot de passe aléatoire
* créer utilisateur et mettre le mot de passe
* envoyer mot de passe par mail
Les utilisateurs à créer sont stockés dans un dictionnaire.
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
"""
created = {} # uid créés
if len(users) == 0:
import_ok = False
msg_list = ["Feuille vide ou illisible"]
else:
msg_list = []
line = 1 # start from excel line #2
import_ok = True
def append_msg(msg):
msg_list.append("Ligne %s : %s" % (line, msg))
try:
for u in users:
line = line + 1
user_ok, msg = sco_users.check_modif_user(
0,
enforce_optionals=not force,
user_name=u["user_name"],
nom=u["nom"],
prenom=u["prenom"],
email=u["email"],
roles=[r for r in u["roles"].split(",") if r],
dept=u["dept"],
)
if not user_ok:
append_msg("identifiant '%s' %s" % (u["user_name"], msg))
u["passwd"] = generate_password()
#
# check identifiant
if u["user_name"] in created.keys():
user_ok = False
append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s"
% (u["user_name"], created[u["user_name"]]["line"])
)
# check roles / ignore whitespaces around roles / build roles_string
# roles_string (expected by User) appears as column 'roles' in excel file
roles_list = []
for role in u["roles"].split(","):
try:
role = role.strip()
if role:
_, _ = UserRole.role_dept_from_string(role)
roles_list.append(role)
except ScoValueError as value_error:
user_ok = False
append_msg("role %s : %s" % (role, value_error))
u["roles_string"] = ",".join(roles_list)
if user_ok:
u["line"] = line
created[u["user_name"]] = u
else:
import_ok = False
except ScoValueError as value_error:
log(f"import_users: exception: abort create {str(created.keys())}")
raise ScoValueError(msg) from value_error
if import_ok:
for u in created.values():
# Création de l'utilisateur (via SQLAlchemy)
user = User()
user.from_dict(u, new_user=True)
db.session.add(user)
db.session.commit()
mail_password(u)
else:
created = {} # reset # of created users to 0
return import_ok, msg_list, len(created)
# --------- Génération du mot de passe initial -----------
# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
# Alphabet tres simple pour des mots de passe simples...
ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
PASSLEN = 8
RNG = random.Random(time.time())
def generate_password():
"""This function creates a pseudo random number generator object, seeded with
the cryptographic hash of the passString. The contents of the character set
is then shuffled and a selection of passLength words is made from this list.
This selection is returned as the generated password."""
l = list(ALPHABET) # make this mutable so that we can shuffle the characters
RNG.shuffle(l) # shuffle the character set
# pick up only a subset from the available characters:
return "".join(RNG.sample(l, PASSLEN))
def mail_password(user: dict, reset=False) -> None:
"Send password by email"
if not user["email"]:
return
user["url"] = url_for("scodoc.index", _external=True)
txt = (
"""
Bonjour %(prenom)s %(nom)s,
"""
% user
)
if reset:
txt += (
"""
votre mot de passe ScoDoc a été -initialisé.
Le nouveau mot de passe est: %(passwd)s
Votre nom d'utilisateur est %(user_name)s
Vous devrez changer ce mot de passe lors de votre première connexion
sur %(url)s
"""
% user
)
else:
txt += (
"""
vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.
Votre nom d'utilisateur est %(user_name)s
Votre mot de passe est: %(passwd)s
Le logiciel est accessible sur: %(url)s
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
"""
% user
)
txt += (
"""
_______
ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
Pour plus d'informations sur ce logiciel, voir %s
"""
% scu.SCO_WEBSITE
)
msg = MIMEMultipart()
if reset:
subject = "Mot de passe ScoDoc"
else:
subject = "Votre accès ScoDoc"
sender = sco_preferences.get_preference("email_from_addr")
email.send_email(subject, sender, [user["email"]], txt)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Import d'utilisateurs via fichier Excel
"""
import random
import time
from email.mime.multipart import MIMEMultipart
from flask import g, url_for
from flask_login import current_user
from app import db
from app import email
from app.auth.models import User, UserRole
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_excel
from app.scodoc import sco_preferences
from app.scodoc import sco_users
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
COMMENTS = (
"""user_name:
Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
""",
"""nom:
Maximum 64 caractères""",
"""prenom:
Maximum 64 caractères""",
"""email:
Maximum 120 caractères""",
"""roles:
un plusieurs rôles séparés par ','
chaque role est fait de 2 composantes séparées par _:
1. Le role (Ens, Secr ou Admin)
2. Le département (en majuscule)
Exemple: "Ens_RT,Admin_INFO"
""",
"""dept:
Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements
""",
)
def generate_excel_sample():
"""generates an excel document suitable to import users"""
style = sco_excel.excel_make_style(bold=True)
titles = TITLES
titles_styles = [style] * len(titles)
return sco_excel.excel_simple_table(
titles=titles,
titles_styles=titles_styles,
sheet_name="Utilisateurs ScoDoc",
comments=COMMENTS,
)
def import_excel_file(datafile, force=""):
"""
Import scodoc users from Excel file.
This method:
* checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
* Once the check is done ans successfull, build the list of users (does not check the data)
* call :func:`import_users` to actually do the job
history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
:param datafile: the stream from to the to be imported
:return: same as import users
"""
# Check current user privilege
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
# Récupération des informations sur l'utilisateur courant
log("sco_import_users.import_excel_file by %s" % auth_name)
# Read the data from the stream
exceldata = datafile.read()
if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata)
if not data:
raise ScoValueError(
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
# check cols
cols = {}.fromkeys(TITLES)
unknown = []
for tit in fs:
if tit not in cols:
unknown.append(tit)
else:
del cols[tit]
if cols or unknown:
raise ScoValueError(
"""colonnes incorrectes (on attend %d, et non %d) <br>
(colonnes manquantes: %s, colonnes invalides: %s)"""
% (len(TITLES), len(fs), list(cols.keys()), unknown)
)
# ok, same titles... : build the list of dictionaries
users = []
for line in data[1:]:
d = {}
for i in range(len(fs)):
d[fs[i]] = line[i]
users.append(d)
return import_users(users=users, force=force)
def import_users(users, force=""):
"""
Import users from a list of users_descriptors.
descriptors are dictionaries hosting users's data.
The operation is atomic (all the users are imported or none)
:param users: list of descriptors to be imported
:return: a tuple that describe the result of the import:
* ok: import ok or aborted
* messages: the list of messages
* the # of users created
Implémentation:
Pour chaque utilisateur à créer:
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
* générer mot de passe aléatoire
* créer utilisateur et mettre le mot de passe
* envoyer mot de passe par mail
Les utilisateurs à créer sont stockés dans un dictionnaire.
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
"""
created = {} # uid créés
if len(users) == 0:
import_ok = False
msg_list = ["Feuille vide ou illisible"]
else:
msg_list = []
line = 1 # start from excel line #2
import_ok = True
def append_msg(msg):
msg_list.append("Ligne %s : %s" % (line, msg))
try:
for u in users:
line = line + 1
user_ok, msg = sco_users.check_modif_user(
0,
enforce_optionals=not force,
user_name=u["user_name"],
nom=u["nom"],
prenom=u["prenom"],
email=u["email"],
roles=[r for r in u["roles"].split(",") if r],
dept=u["dept"],
)
if not user_ok:
append_msg("identifiant '%s' %s" % (u["user_name"], msg))
u["passwd"] = generate_password()
#
# check identifiant
if u["user_name"] in created.keys():
user_ok = False
append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s"
% (u["user_name"], created[u["user_name"]]["line"])
)
# check roles / ignore whitespaces around roles / build roles_string
# roles_string (expected by User) appears as column 'roles' in excel file
roles_list = []
for role in u["roles"].split(","):
try:
role = role.strip()
if role:
_, _ = UserRole.role_dept_from_string(role)
roles_list.append(role)
except ScoValueError as value_error:
user_ok = False
append_msg("role %s : %s" % (role, value_error))
u["roles_string"] = ",".join(roles_list)
if user_ok:
u["line"] = line
created[u["user_name"]] = u
else:
import_ok = False
except ScoValueError as value_error:
log(f"import_users: exception: abort create {str(created.keys())}")
raise ScoValueError(msg) from value_error
if import_ok:
for u in created.values():
# Création de l'utilisateur (via SQLAlchemy)
user = User()
user.from_dict(u, new_user=True)
db.session.add(user)
db.session.commit()
mail_password(u)
else:
created = {} # reset # of created users to 0
return import_ok, msg_list, len(created)
# --------- Génération du mot de passe initial -----------
# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
# Alphabet tres simple pour des mots de passe simples...
ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
PASSLEN = 8
RNG = random.Random(time.time())
def generate_password():
"""This function creates a pseudo random number generator object, seeded with
the cryptographic hash of the passString. The contents of the character set
is then shuffled and a selection of passLength words is made from this list.
This selection is returned as the generated password."""
l = list(ALPHABET) # make this mutable so that we can shuffle the characters
RNG.shuffle(l) # shuffle the character set
# pick up only a subset from the available characters:
return "".join(RNG.sample(l, PASSLEN))
def mail_password(user: dict, reset=False) -> None:
"Send password by email"
if not user["email"]:
return
user["url"] = url_for("scodoc.index", _external=True)
txt = (
"""
Bonjour %(prenom)s %(nom)s,
"""
% user
)
if reset:
txt += (
"""
votre mot de passe ScoDoc a été -initialisé.
Le nouveau mot de passe est: %(passwd)s
Votre nom d'utilisateur est %(user_name)s
Vous devrez changer ce mot de passe lors de votre première connexion
sur %(url)s
"""
% user
)
else:
txt += (
"""
vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.
Votre nom d'utilisateur est %(user_name)s
Votre mot de passe est: %(passwd)s
Le logiciel est accessible sur: %(url)s
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
"""
% user
)
txt += (
"""
_______
ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
Pour plus d'informations sur ce logiciel, voir %s
"""
% scu.SCO_WEBSITE
)
msg = MIMEMultipart()
if reset:
subject = "Mot de passe ScoDoc"
else:
subject = "Votre accès ScoDoc"
sender = sco_preferences.get_preference("email_from_addr")
email.send_email(subject, sender, [user["email"]], txt)

View File

@ -517,18 +517,18 @@ def _make_table_notes(
hh += "s"
hh += ", %d en attente." % (nb_att)
pdf_title = "<br/> BORDEREAU DE SIGNATURES"
pdf_title += "<br/><br/>%(titre)s" % sem
pdf_title += "<br/>(%(mois_debut)s - %(mois_fin)s)" % sem
pdf_title = "<br> BORDEREAU DE SIGNATURES"
pdf_title += "<br><br>%(titre)s" % sem
pdf_title += "<br>(%(mois_debut)s - %(mois_fin)s)" % sem
pdf_title += " semestre %s %s" % (
sem["semestre_id"],
sem.get("modalite", ""),
)
pdf_title += f"<br/>Notes du module {module.code} - {module.titre}"
pdf_title += "<br/>Evaluation : %(description)s " % e
pdf_title += f"<br>Notes du module {module.code} - {module.titre}"
pdf_title += "<br>Evaluation : %(description)s " % e
if len(e["jour"]) > 0:
pdf_title += " (%(jour)s)" % e
pdf_title += "(noté sur %(note_max)s )<br/><br/>" % e
pdf_title += "(noté sur %(note_max)s )<br><br>" % e
else:
hh = " %s, %s (%d étudiants)" % (
E["description"],
@ -623,11 +623,11 @@ def _make_table_notes(
commentkeys.sort(key=lambda x: int(x[1]))
for (comment, key) in commentkeys:
C.append(
'<span class="colcomment">(%s)</span> <em>%s</em><br/>' % (key, comment)
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment)
)
if commentkeys:
C.append(
'<span><a class=stdlink" href="evaluation_list_operations?evaluation_id=%s">Gérer les opérations</a></span><br/>'
'<span><a class=stdlink" href="evaluation_list_operations?evaluation_id=%s">Gérer les opérations</a></span><br>'
% E["evaluation_id"]
)
eval_info = "xxx"

View File

@ -1,259 +1,259 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Rapports sur lycées d'origine des étudiants d'un semestre.
- statistiques decisions
- suivi cohortes
"""
from operator import itemgetter
from flask import url_for, g, request
import app
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_report
from app.scodoc import sco_etud
import sco_version
from app.scodoc.gen_tables import GenTable
def formsemestre_table_etuds_lycees(
formsemestre_id, group_lycees=True, only_primo=False
):
"""Récupère liste d'etudiants avec etat et decision."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
if only_primo:
primostr = "primo-entrants du "
else:
primostr = "du "
title = "Lycées des étudiants %ssemestre " % primostr + sem["titreannee"]
return _table_etuds_lycees(
etuds,
group_lycees,
title,
sco_preferences.SemPreferences(formsemestre_id),
)
def scodoc_table_etuds_lycees(format="html"):
"""Table avec _tous_ les étudiants des semestres non verrouillés
de _tous_ les départements.
"""
cur_dept = g.scodoc_dept
semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
etuds = []
try:
for (sem, dept) in semdepts:
app.set_sco_dept(dept.acronym)
etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
finally:
app.set_sco_dept(cur_dept)
tab, etuds_by_lycee = _table_etuds_lycees(
etuds,
False,
"Lycées de TOUS les étudiants",
sco_preferences.SemPreferences(),
no_links=True,
)
tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
return t
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des %d étudiants (%d semestres)</h2>"""
% (len(etuds), len(semdepts)),
t,
"""<div id="lyc_map_canvas"></div>
""",
js_coords_lycees(etuds_by_lycee),
html_sco_header.sco_footer(),
]
return "\n".join(H)
def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False):
etuds = [sco_etud.etud_add_lycee_infos(e) for e in etuds]
etuds_by_lycee = scu.group_by_key(etuds, "codelycee")
#
if group_lycees:
L = [etuds_by_lycee[codelycee][0] for codelycee in etuds_by_lycee]
for l in L:
l["nbetuds"] = len(etuds_by_lycee[l["codelycee"]])
# L.sort( key=operator.itemgetter('codepostallycee', 'nomlycee') ) argh, only python 2.5+ !!!
L.sort(key=itemgetter("codepostallycee", "nomlycee"))
columns_ids = (
"nbetuds",
"codelycee",
"codepostallycee",
"villelycee",
"nomlycee",
)
bottom_titles = {
"nbetuds": len(etuds),
"nomlycee": "%d lycées"
% len([x for x in etuds_by_lycee if etuds_by_lycee[x][0]["codelycee"]]),
}
else:
L = etuds
columns_ids = (
"civilite_str",
"nom",
"prenom",
"codelycee",
"codepostallycee",
"villelycee",
"nomlycee",
)
bottom_titles = None
if not no_links:
for etud in etuds:
fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
etud["_nom_target"] = fiche_url
etud["_prenom_target"] = fiche_url
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
tab = GenTable(
columns_ids=columns_ids,
rows=L,
titles={
"nbetuds": "Nb d'étudiants",
"civilite_str": "",
"nom": "Nom",
"prenom": "Prénom",
"etudid": "etudid",
"codelycee": "Code Lycée",
"codepostallycee": "Code postal",
"nomlycee": "Lycée",
"villelycee": "Commune",
},
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
caption=title,
page_title="Carte lycées d'origine",
html_sortable=True,
html_class="table_leftalign table_listegroupe",
bottom_titles=bottom_titles,
preferences=preferences,
)
return tab, etuds_by_lycee
def formsemestre_etuds_lycees(
formsemestre_id,
format="html",
only_primo=False,
no_grouping=False,
):
"""Table des lycées d'origine"""
tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo:
tab.base_url += "&only_primo=1"
if no_grouping:
tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
return t
F = [
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, format
)
]
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
"\n".join(F),
t,
"""<div id="lyc_map_canvas"></div>
""",
js_coords_lycees(etuds_by_lycee),
html_sco_header.sco_footer(),
]
return "\n".join(H)
def qjs(txt): # quote for JS
return txt.replace("'", r"\'").replace('"', r"\"")
def js_coords_lycees(etuds_by_lycee):
"""Formatte liste des lycees en JSON pour Google Map"""
L = []
for codelycee in etuds_by_lycee:
if codelycee:
lyc = etuds_by_lycee[codelycee][0]
if not lyc.get("positionlycee", False):
continue
listeetuds = "<br/>%d étudiants: " % len(
etuds_by_lycee[codelycee]
) + ", ".join(
[
'<a class="discretelink" href="%s" title="">%s</a>'
% (
url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=e["etudid"],
),
qjs(e["nomprenom"]),
)
for e in etuds_by_lycee[codelycee]
]
)
pos = qjs(lyc["positionlycee"])
legend = "%s %s" % (qjs("%(nomlycee)s (%(villelycee)s)" % lyc), listeetuds)
L.append(
"{'position' : '%s', 'name' : '%s', 'number' : %d }"
% (pos, legend, len(etuds_by_lycee[codelycee]))
)
return """<script type="text/javascript">
var lycees_coords = [%s];
</script>""" % ",".join(
L
)
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Rapports sur lycées d'origine des étudiants d'un semestre.
- statistiques decisions
- suivi cohortes
"""
from operator import itemgetter
from flask import url_for, g, request
import app
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_report
from app.scodoc import sco_etud
import sco_version
from app.scodoc.gen_tables import GenTable
def formsemestre_table_etuds_lycees(
formsemestre_id, group_lycees=True, only_primo=False
):
"""Récupère liste d'etudiants avec etat et decision."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
if only_primo:
primostr = "primo-entrants du "
else:
primostr = "du "
title = "Lycées des étudiants %ssemestre " % primostr + sem["titreannee"]
return _table_etuds_lycees(
etuds,
group_lycees,
title,
sco_preferences.SemPreferences(formsemestre_id),
)
def scodoc_table_etuds_lycees(format="html"):
"""Table avec _tous_ les étudiants des semestres non verrouillés
de _tous_ les départements.
"""
cur_dept = g.scodoc_dept
semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
etuds = []
try:
for (sem, dept) in semdepts:
app.set_sco_dept(dept.acronym)
etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
finally:
app.set_sco_dept(cur_dept)
tab, etuds_by_lycee = _table_etuds_lycees(
etuds,
False,
"Lycées de TOUS les étudiants",
sco_preferences.SemPreferences(),
no_links=True,
)
tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
return t
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des %d étudiants (%d semestres)</h2>"""
% (len(etuds), len(semdepts)),
t,
"""<div id="lyc_map_canvas"></div>
""",
js_coords_lycees(etuds_by_lycee),
html_sco_header.sco_footer(),
]
return "\n".join(H)
def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False):
etuds = [sco_etud.etud_add_lycee_infos(e) for e in etuds]
etuds_by_lycee = scu.group_by_key(etuds, "codelycee")
#
if group_lycees:
L = [etuds_by_lycee[codelycee][0] for codelycee in etuds_by_lycee]
for l in L:
l["nbetuds"] = len(etuds_by_lycee[l["codelycee"]])
# L.sort( key=operator.itemgetter('codepostallycee', 'nomlycee') ) argh, only python 2.5+ !!!
L.sort(key=itemgetter("codepostallycee", "nomlycee"))
columns_ids = (
"nbetuds",
"codelycee",
"codepostallycee",
"villelycee",
"nomlycee",
)
bottom_titles = {
"nbetuds": len(etuds),
"nomlycee": "%d lycées"
% len([x for x in etuds_by_lycee if etuds_by_lycee[x][0]["codelycee"]]),
}
else:
L = etuds
columns_ids = (
"civilite_str",
"nom",
"prenom",
"codelycee",
"codepostallycee",
"villelycee",
"nomlycee",
)
bottom_titles = None
if not no_links:
for etud in etuds:
fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
etud["_nom_target"] = fiche_url
etud["_prenom_target"] = fiche_url
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
tab = GenTable(
columns_ids=columns_ids,
rows=L,
titles={
"nbetuds": "Nb d'étudiants",
"civilite_str": "",
"nom": "Nom",
"prenom": "Prénom",
"etudid": "etudid",
"codelycee": "Code Lycée",
"codepostallycee": "Code postal",
"nomlycee": "Lycée",
"villelycee": "Commune",
},
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
caption=title,
page_title="Carte lycées d'origine",
html_sortable=True,
html_class="table_leftalign table_listegroupe",
bottom_titles=bottom_titles,
preferences=preferences,
)
return tab, etuds_by_lycee
def formsemestre_etuds_lycees(
formsemestre_id,
format="html",
only_primo=False,
no_grouping=False,
):
"""Table des lycées d'origine"""
tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo:
tab.base_url += "&only_primo=1"
if no_grouping:
tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
return t
F = [
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, format
)
]
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
"\n".join(F),
t,
"""<div id="lyc_map_canvas"></div>
""",
js_coords_lycees(etuds_by_lycee),
html_sco_header.sco_footer(),
]
return "\n".join(H)
def qjs(txt): # quote for JS
return txt.replace("'", r"\'").replace('"', r"\"")
def js_coords_lycees(etuds_by_lycee):
"""Formatte liste des lycees en JSON pour Google Map"""
L = []
for codelycee in etuds_by_lycee:
if codelycee:
lyc = etuds_by_lycee[codelycee][0]
if not lyc.get("positionlycee", False):
continue
listeetuds = "<br>%d étudiants: " % len(
etuds_by_lycee[codelycee]
) + ", ".join(
[
'<a class="discretelink" href="%s" title="">%s</a>'
% (
url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=e["etudid"],
),
qjs(e["nomprenom"]),
)
for e in etuds_by_lycee[codelycee]
]
)
pos = qjs(lyc["positionlycee"])
legend = "%s %s" % (qjs("%(nomlycee)s (%(villelycee)s)" % lyc), listeetuds)
L.append(
"{'position' : '%s', 'name' : '%s', 'number' : %d }"
% (pos, legend, len(etuds_by_lycee[codelycee]))
)
return """<script type="text/javascript">
var lycees_coords = [%s];
</script>""" % ",".join(
L
)

File diff suppressed because it is too large Load Diff

View File

@ -189,7 +189,7 @@ def ficheEtud(etudid=None):
else:
info["paysdomicile"] = ""
if info["telephone"] or info["telephonemobile"]:
info["telephones"] = "<br/>%s &nbsp;&nbsp; %s" % (
info["telephones"] = "<br>%s &nbsp;&nbsp; %s" % (
info["telephonestr"],
info["telephonemobilestr"],
)
@ -506,9 +506,9 @@ def ficheEtud(etudid=None):
<b>Ajouter une annotation sur %(nomprenom)s: </b>
<table><tr>
<tr><td><textarea name="comment" rows="4" cols="50" value=""></textarea>
<br/><font size=-1>
<br><font size=-1>
<i>Ces annotations sont lisibles par tous les enseignants et le secrétariat.</i>
<br/>
<br>
<i>L'annotation commençant par "PE:" est un avis de poursuite d'études.</i>
</font>
</td></tr>

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ Au niveau du code interface, on défini pour chaque préférence:
- initvalue : valeur initiale
- explanation: explication en français
- size: longueur du chap texte
- input_type: textarea,separator,... type de widget TrivialFormulator a utiliser
- input_type: textarea, separator, ... type de widget TrivialFormulator a utiliser
- rows, rols: geometrie des textareas
- category: misc ou bul ou page_bulletins ou abs ou general ou portal
ou pdf ou pvpdf ou ...
@ -202,7 +202,7 @@ _INSTALLED_FONTS = ", ".join(sco_pdf.get_available_font_names())
PREF_CATEGORIES = (
# sur page "Paramètres"
("general", {}),
("general", {"title": ""}), # voir paramètre titlr de TrivialFormulator
("misc", {"title": "Divers"}),
("apc", {"title": "BUT et Approches par Compétences"}),
("abs", {"title": "Suivi des absences", "related": ("bul",)}),
@ -1008,7 +1008,7 @@ class BasePreferences(object):
(
"PV_LETTER_DIPLOMA_SIGNATURE",
{
"initvalue": """Le %(DirectorTitle)s, <br/>%(DirectorName)s""",
"initvalue": """Le %(DirectorTitle)s, <br>%(DirectorName)s""",
"title": """Signature des lettres individuelles de diplôme""",
"explanation": """%(DirectorName)s et %(DirectorTitle)s remplacés""",
"input_type": "textarea",
@ -1020,8 +1020,8 @@ class BasePreferences(object):
(
"PV_LETTER_PASSAGE_SIGNATURE",
{
"initvalue": """Pour le Directeur de l'IUT<br/>
et par délégation<br/>
"initvalue": """Pour le Directeur de l'IUT<br>
et par délégation<br>
Le Chef du département""",
"title": """Signature des lettres individuelles de passage d'un semestre à l'autre""",
"explanation": """%(DirectorName)s et %(DirectorTitle)s remplacés""",
@ -1056,7 +1056,7 @@ class BasePreferences(object):
<para leftindent="%(pv_htab1)s">%(codepostaldomicile)s %(villedomicile)s</para>
<para spaceBefore="25mm" fontSize="14" alignment="center">
<b>Jury de %(type_jury)s <br/> %(titre_formation)s</b>
<b>Jury de %(type_jury)s <br> %(titre_formation)s</b>
</para>
<para spaceBefore="10mm" fontSize="14" leftindent="0">
@ -1499,7 +1499,7 @@ class BasePreferences(object):
"bul_pdf_sig_left",
{
"initvalue": """<para>La direction des études
<br/>
<br>
%(responsable)s
</para>
""",
@ -1515,7 +1515,7 @@ class BasePreferences(object):
"bul_pdf_sig_right",
{
"initvalue": """<para>Le chef de département
<br/>
<br>
%(ChiefDeptName)s
</para>
""",
@ -1891,7 +1891,7 @@ class BasePreferences(object):
"explanation": """si cette adresse est indiquée, TOUS les mails
envoyés par ScoDoc de ce département vont aller vers elle
AU LIEU DE LEUR DESTINATION NORMALE !""",
"size": 30,
"size": 60,
"category": "debug",
"only_global": True,
},
@ -1935,7 +1935,7 @@ class BasePreferences(object):
value = _get_pref_default_value_from_config(name, pref[1])
self.default[name] = value
self.prefs[None][name] = value
log("creating missing preference for %s=%s" % (name, value))
log(f"creating missing preference for {name}={value}")
# add to db table
self._editor.create(
cnx, {"dept_id": self.dept_id, "name": name, "value": value}
@ -1999,7 +1999,7 @@ class BasePreferences(object):
if not pdb:
# crée préférence
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
log(f"create pref sem={formsemestre_id} {name}={value}")
self._editor.create(
cnx,
{
@ -2036,7 +2036,7 @@ class BasePreferences(object):
def set(self, formsemestre_id, name, value):
if not name or name[0] == "_" or name not in self.prefs_name:
raise ValueError("invalid preference name: %s" % name)
raise ValueError(f"invalid preference name: {name}")
if formsemestre_id and name in self.prefs_only_global:
raise ValueError("pref %s is always defined globaly")
if not formsemestre_id in self.prefs:
@ -2055,7 +2055,7 @@ class BasePreferences(object):
cnx, args={"formsemestre_id": formsemestre_id, "name": name}
)
if pdb:
log("deleting pref sem=%s %s" % (formsemestre_id, name))
log(f"deleting pref sem={formsemestre_id} {name}")
assert pdb[0]["dept_id"] == self.dept_id
self._editor.delete(cnx, pdb[0]["pref_id"])
sco_cache.invalidate_formsemestre() # > modif preferences
@ -2067,14 +2067,18 @@ class BasePreferences(object):
self.load()
H = [
html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
f"<h2>Préférences globales pour {scu.ScoURL()}</h2>",
# f"""<p><a href="{url_for("scodoc.configure_logos", scodoc_dept=g.scodoc_dept)
# }">modification des logos du département (pour documents pdf)</a></p>"""
# if current_user.is_administrator()
# else "",
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
""",
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres,
sauf si ceux-ci définissent des valeurs spécifiques.
</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications"
en bas de page pour appliquer vos changements !
</p>
""",
]
form = self.build_tf_form()
tf = TrivialFormulator(
@ -2083,6 +2087,9 @@ class BasePreferences(object):
form,
initvalues=self.prefs[None],
submitlabel="Enregistrer les modifications",
title="Département et institution",
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
@ -2094,7 +2101,7 @@ class BasePreferences(object):
self.save()
return flask.redirect(scu.ScoURL() + "?head_message=Préférences modifiées")
def build_tf_form(self, categories=[], formsemestre_id=None):
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator.
If formsemestre_id is not specified, edit global prefs.
"""
@ -2119,7 +2126,7 @@ class BasePreferences(object):
onclick="set_global_pref(this, '{pref_name}');"
>utiliser paramètre global</span>"""
if formsemestre_id and self.is_global(formsemestre_id, pref_name):
# valeur actuelle globale (ou vient d'etre supprimee localement):
# valeur actuelle globale (ou vient d'etre supprimée localement):
# montre la valeur et menus pour la rendre locale
descr["readonly"] = True
menu_global = f"""<select class="tf-selglobal"
@ -2138,8 +2145,11 @@ class BasePreferences(object):
if title:
form.append(
(
f"sep_{cat}",
{"input_type": "separator", "title": f"<h3>{title}</h3>"},
f"table_{cat}",
{
"input_type": "table_separator",
"title": f"{title}",
},
)
)
subtitle = cat_descr.get("subtitle", None)
@ -2246,6 +2256,9 @@ function set_global_pref(el, pref_name) {
initvalues=self,
cssclass="sco_pref",
submitlabel="Enregistrer les modifications",
title="Département et institution",
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
dest_url = (
scu.NotesURL()

File diff suppressed because it is too large Load Diff

View File

@ -384,7 +384,7 @@ def formsemestre_report_counts(
else:
checked = ""
F.append(
'<br/><input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
'<br><input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
% checked
)
F.append(
@ -928,7 +928,7 @@ def _gen_form_selectetuds(
else:
checked = ""
F.append(
'<br/><input type="checkbox" name="only_primo" onchange="javascript: submit(this);" %s/>Restreindre aux primo-entrants'
'<br><input type="checkbox" name="only_primo" onchange="javascript: submit(this);" %s/>Restreindre aux primo-entrants'
% checked
)
F.append(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -463,7 +463,7 @@ def list_synch(sem, anneeapogee=None):
"id": "etuds_noninscrits",
"title": "Étudiants non inscrits dans ce semestre",
"help": """Ces étudiants sont déjà connus par ScoDoc, sont inscrits dans cette étape Apogée mais ne sont pas inscrits à ce semestre ScoDoc. Cochez les étudiants à inscrire.""",
"comment": """ dans ScoDoc et Apogée, <br/>mais pas inscrits
"comment": """ dans ScoDoc et Apogée, <br>mais pas inscrits
dans ce semestre""",
"title_target": "",
"with_checkbox": True,

File diff suppressed because it is too large Load Diff

View File

@ -1,384 +1,384 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonction de gestion des UE "externes" (effectuees dans un cursus exterieur)
On rapatrie (saisie) les notes (et crédits ECTS).
Cas d'usage: les étudiants d'une formation gérée par ScoDoc peuvent
suivre un certain nombre d'UE à l'extérieur. L'établissement a reconnu
au préalable une forme d'équivalence entre ces UE et celles du
programme. Les UE effectuées à l'extérieur sont par nature variable
d'un étudiant à l'autre et d'une année à l'autre, et ne peuvent pas
être introduites dans le programme pédagogique ScoDoc sans alourdir
considérablement les opérations (saisie, affichage du programme,
gestion des inscriptions).
En outre, un suivi détaillé de ces UE n'est pas nécessaire: il suffit
de pouvoir y associer une note et une quantité de crédits ECTS.
Solution proposée (nov 2014):
- un nouveau type d'UE qui
- s'affichera à part dans le programme pédagogique
et les bulletins
- pas présentées lors de la mise en place de semestres
- affichage sur bulletin des étudiants qui y sont inscrit
- création en même temps que la saisie de la note
(chaine creation: UE/matière/module, inscription étudiant, entrée valeur note)
avec auto-suggestion du nom pour limiter la création de doublons
- seront aussi présentées (à part) sur la page "Voir les inscriptions aux modules"
"""
import flask
from flask import request
from flask_login import current_user
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
def external_ue_create(
formsemestre_id,
titre="",
acronyme="",
ue_type=sco_codes_parcours.UE_STANDARD,
ects=0.0,
):
"""Crée UE/matiere/module/evaluation puis saisie les notes"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
log(f"creating external UE in {formsemestre}: {acronyme}")
# Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement):
if (not formsemestre.resp_can_edit) or (
current_user.id not in [u.id for u in formsemestre.responsables]
):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
#
formation_id = formsemestre.formation.id
numero = sco_edit_ue.next_ue_numero(
formation_id, semestre_id=formsemestre.semestre_id
)
ue_id = sco_edit_ue.do_ue_create(
{
"formation_id": formation_id,
"semestre_idx": formsemestre.semestre_id,
"titre": titre,
"acronyme": acronyme,
"numero": numero,
"type": ue_type,
"ects": ects,
"is_external": True,
},
)
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
)
module_id = sco_edit_module.do_module_create(
{
"titre": "UE extérieure",
"code": acronyme,
"coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS
"ue_id": ue_id,
"matiere_id": matiere_id,
"formation_id": formation_id,
"semestre_id": formsemestre.semestre_id,
"module_type": scu.ModuleType.STANDARD,
},
)
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(
{
"module_id": module_id,
"formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module
"responsable_id": formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None,
},
)
return moduleimpl_id
def external_ue_inscrit_et_note(
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
):
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
)
# Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id,
formsemestre_id,
list(notes_etuds.keys()),
)
# Création d'une évaluation si il n'y en a pas déjà:
mod_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id}
)
if len(mod_evals):
# met la note dans le première évaluation existante:
evaluation_id = mod_evals[0]["evaluation_id"]
else:
# crée une évaluation:
evaluation_id = sco_evaluation_db.do_evaluation_create(
moduleimpl_id=moduleimpl_id,
note_max=20.0,
coefficient=1.0,
publish_incomplete=True,
evaluation_type=scu.EVALUATION_NORMALE,
visibulletin=False,
description="note externe",
)
# Saisie des notes
_, _, _ = sco_saisie_notes.notes_add(
current_user,
evaluation_id,
list(notes_etuds.items()),
do_it=True,
)
def get_existing_external_ue(formation_id: int) -> list[dict]:
"Liste de toutes les UE externes définies dans cette formation"
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch(
"""
SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo
WHERE mi.formsemestre_id = %(formsemestre_id)s
AND mi.module_id = mo.id
AND mo.ue_id = %(ue_id)s
""",
{"ue_id": ue_id, "formsemestre_id": formsemestre_id},
)
if r:
return r[0]["moduleimpl_id"]
else:
raise ScoValueError(
f"""Aucun module externe ne correspond
(formsemestre_id={formsemestre_id}, ue_id={ue_id})"""
)
# Web function
def external_ue_create_form(formsemestre_id: int, etudid: int):
"""Formulaire création UE externe + inscription étudiant et saisie note
- Demande UE: peut-être existante (liste les UE externes de cette formation),
ou sinon spécifier titre, acronyme, type, ECTS
- Demande note à enregistrer.
Note: pour l'édition éventuelle de ces informations, on utilisera les
fonctions standards sur les UE/modules/notes
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement):
if not sem["resp_can_edit"] or (current_user.id not in sem["responsables"]):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
formation_id = sem["formation_id"]
existing_external_ue = get_existing_external_ue(formation_id)
H = [
html_sco_header.html_sem_header(
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
dans un autre établissement et qu'elle doit être intégrée dans le semestre courant.<br/>
La note (/20) obtenue par l'étudiant doit toujours être spécifiée.</br>
On peut choisir une UE externe existante (dans le menu), ou bien en créer une, qui sera
alors ajoutée à la formation.
</p>
""",
]
html_footer = html_sco_header.sco_footer()
Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
ue_types = [
typ for typ in parcours.ALLOWED_UE_TYPES if typ != sco_codes_parcours.UE_SPORT
]
ue_types.sort()
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types]
if existing_external_ue:
default_label = "Nouvelle UE"
else:
default_label = "Aucune UE externe existante"
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("formsemestre_id", {"input_type": "hidden"}),
("etudid", {"input_type": "hidden"}),
(
"existing_ue",
{
"input_type": "menu",
"title": "UE externe existante:",
"allowed_values": [""]
+ [str(ue["ue_id"]) for ue in existing_external_ue],
"labels": [default_label]
+ [
"%s (%s)" % (ue["titre"], ue["acronyme"])
for ue in existing_external_ue
],
"attributes": ['onchange="update_external_ue_form();"'],
"explanation": "inscrire cet étudiant dans cette UE",
},
),
(
"sep",
{
"input_type": "separator",
"title": "Ou bien déclarer une nouvelle UE externe:",
"dom_id": "tf_extue_decl",
},
),
# champs a desactiver si une UE existante est choisie
(
"titre",
{"size": 30, "explanation": "nom de l'UE", "dom_id": "tf_extue_titre"},
),
(
"acronyme",
{
"size": 8,
"explanation": "abbréviation",
"allow_null": True, # attention: verifier
"dom_id": "tf_extue_acronyme",
},
),
(
"type",
{
"explanation": "type d'UE",
"input_type": "menu",
"allowed_values": ue_types,
"labels": ue_types_names,
"dom_id": "tf_extue_type",
},
),
(
"ects",
{
"size": 4,
"type": "float",
"min_value": 0,
"max_value": 1000,
"title": "ECTS",
"explanation": "nombre de crédits ECTS",
"dom_id": "tf_extue_ects",
},
),
#
(
"note",
{"size": 4, "explanation": "note sur 20", "dom_id": "tf_extue_note"},
),
),
submitlabel="Enregistrer",
cancelbutton="Annuler",
)
bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % (
formsemestre_id,
etudid,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + html_footer
elif tf[0] == -1:
return flask.redirect(bull_url)
else:
note = tf[2]["note"].strip().upper()
note_value, invalid = sco_saisie_notes.convert_note_from_string(note, 20.0)
if invalid:
return (
"\n".join(H)
+ "\n"
+ tf_error_message("valeur note invalide")
+ tf[1]
+ html_footer
)
if tf[2]["existing_ue"]:
ue_id = int(tf[2]["existing_ue"])
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
else:
acronyme = tf[2]["acronyme"].strip()
if not acronyme:
return (
"\n".join(H)
+ "\n"
+ tf_error_message("spécifier acronyme d'UE")
+ tf[1]
+ html_footer
)
moduleimpl_id = external_ue_create(
formsemestre_id,
titre=tf[2]["titre"],
acronyme=acronyme,
ue_type=tf[2]["type"], # type de l'UE
ects=tf[2]["ects"],
)
external_ue_inscrit_et_note(
moduleimpl_id,
formsemestre_id,
{etudid: note_value},
)
return flask.redirect(bull_url + "&head_message=Ajout%20effectué")
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonction de gestion des UE "externes" (effectuees dans un cursus exterieur)
On rapatrie (saisie) les notes (et crédits ECTS).
Cas d'usage: les étudiants d'une formation gérée par ScoDoc peuvent
suivre un certain nombre d'UE à l'extérieur. L'établissement a reconnu
au préalable une forme d'équivalence entre ces UE et celles du
programme. Les UE effectuées à l'extérieur sont par nature variable
d'un étudiant à l'autre et d'une année à l'autre, et ne peuvent pas
être introduites dans le programme pédagogique ScoDoc sans alourdir
considérablement les opérations (saisie, affichage du programme,
gestion des inscriptions).
En outre, un suivi détaillé de ces UE n'est pas nécessaire: il suffit
de pouvoir y associer une note et une quantité de crédits ECTS.
Solution proposée (nov 2014):
- un nouveau type d'UE qui
- s'affichera à part dans le programme pédagogique
et les bulletins
- pas présentées lors de la mise en place de semestres
- affichage sur bulletin des étudiants qui y sont inscrit
- création en même temps que la saisie de la note
(chaine creation: UE/matière/module, inscription étudiant, entrée valeur note)
avec auto-suggestion du nom pour limiter la création de doublons
- seront aussi présentées (à part) sur la page "Voir les inscriptions aux modules"
"""
import flask
from flask import request
from flask_login import current_user
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
def external_ue_create(
formsemestre_id,
titre="",
acronyme="",
ue_type=sco_codes_parcours.UE_STANDARD,
ects=0.0,
):
"""Crée UE/matiere/module/evaluation puis saisie les notes"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
log(f"creating external UE in {formsemestre}: {acronyme}")
# Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement):
if (not formsemestre.resp_can_edit) or (
current_user.id not in [u.id for u in formsemestre.responsables]
):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
#
formation_id = formsemestre.formation.id
numero = sco_edit_ue.next_ue_numero(
formation_id, semestre_id=formsemestre.semestre_id
)
ue_id = sco_edit_ue.do_ue_create(
{
"formation_id": formation_id,
"semestre_idx": formsemestre.semestre_id,
"titre": titre,
"acronyme": acronyme,
"numero": numero,
"type": ue_type,
"ects": ects,
"is_external": True,
},
)
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
)
module_id = sco_edit_module.do_module_create(
{
"titre": "UE extérieure",
"code": acronyme,
"coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS
"ue_id": ue_id,
"matiere_id": matiere_id,
"formation_id": formation_id,
"semestre_id": formsemestre.semestre_id,
"module_type": scu.ModuleType.STANDARD,
},
)
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(
{
"module_id": module_id,
"formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module
"responsable_id": formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None,
},
)
return moduleimpl_id
def external_ue_inscrit_et_note(
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
):
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
)
# Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id,
formsemestre_id,
list(notes_etuds.keys()),
)
# Création d'une évaluation si il n'y en a pas déjà:
mod_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id}
)
if len(mod_evals):
# met la note dans le première évaluation existante:
evaluation_id = mod_evals[0]["evaluation_id"]
else:
# crée une évaluation:
evaluation_id = sco_evaluation_db.do_evaluation_create(
moduleimpl_id=moduleimpl_id,
note_max=20.0,
coefficient=1.0,
publish_incomplete=True,
evaluation_type=scu.EVALUATION_NORMALE,
visibulletin=False,
description="note externe",
)
# Saisie des notes
_, _, _ = sco_saisie_notes.notes_add(
current_user,
evaluation_id,
list(notes_etuds.items()),
do_it=True,
)
def get_existing_external_ue(formation_id: int) -> list[dict]:
"Liste de toutes les UE externes définies dans cette formation"
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch(
"""
SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo
WHERE mi.formsemestre_id = %(formsemestre_id)s
AND mi.module_id = mo.id
AND mo.ue_id = %(ue_id)s
""",
{"ue_id": ue_id, "formsemestre_id": formsemestre_id},
)
if r:
return r[0]["moduleimpl_id"]
else:
raise ScoValueError(
f"""Aucun module externe ne correspond
(formsemestre_id={formsemestre_id}, ue_id={ue_id})"""
)
# Web function
def external_ue_create_form(formsemestre_id: int, etudid: int):
"""Formulaire création UE externe + inscription étudiant et saisie note
- Demande UE: peut-être existante (liste les UE externes de cette formation),
ou sinon spécifier titre, acronyme, type, ECTS
- Demande note à enregistrer.
Note: pour l'édition éventuelle de ces informations, on utilisera les
fonctions standards sur les UE/modules/notes
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement):
if not sem["resp_can_edit"] or (current_user.id not in sem["responsables"]):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
formation_id = sem["formation_id"]
existing_external_ue = get_existing_external_ue(formation_id)
H = [
html_sco_header.html_sem_header(
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
dans un autre établissement et qu'elle doit être intégrée dans le semestre courant.<br>
La note (/20) obtenue par l'étudiant doit toujours être spécifiée.</br>
On peut choisir une UE externe existante (dans le menu), ou bien en créer une, qui sera
alors ajoutée à la formation.
</p>
""",
]
html_footer = html_sco_header.sco_footer()
Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
ue_types = [
typ for typ in parcours.ALLOWED_UE_TYPES if typ != sco_codes_parcours.UE_SPORT
]
ue_types.sort()
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types]
if existing_external_ue:
default_label = "Nouvelle UE"
else:
default_label = "Aucune UE externe existante"
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("formsemestre_id", {"input_type": "hidden"}),
("etudid", {"input_type": "hidden"}),
(
"existing_ue",
{
"input_type": "menu",
"title": "UE externe existante:",
"allowed_values": [""]
+ [str(ue["ue_id"]) for ue in existing_external_ue],
"labels": [default_label]
+ [
"%s (%s)" % (ue["titre"], ue["acronyme"])
for ue in existing_external_ue
],
"attributes": ['onchange="update_external_ue_form();"'],
"explanation": "inscrire cet étudiant dans cette UE",
},
),
(
"sep",
{
"input_type": "separator",
"title": "Ou bien déclarer une nouvelle UE externe:",
"dom_id": "tf_extue_decl",
},
),
# champs a desactiver si une UE existante est choisie
(
"titre",
{"size": 30, "explanation": "nom de l'UE", "dom_id": "tf_extue_titre"},
),
(
"acronyme",
{
"size": 8,
"explanation": "abbréviation",
"allow_null": True, # attention: verifier
"dom_id": "tf_extue_acronyme",
},
),
(
"type",
{
"explanation": "type d'UE",
"input_type": "menu",
"allowed_values": ue_types,
"labels": ue_types_names,
"dom_id": "tf_extue_type",
},
),
(
"ects",
{
"size": 4,
"type": "float",
"min_value": 0,
"max_value": 1000,
"title": "ECTS",
"explanation": "nombre de crédits ECTS",
"dom_id": "tf_extue_ects",
},
),
#
(
"note",
{"size": 4, "explanation": "note sur 20", "dom_id": "tf_extue_note"},
),
),
submitlabel="Enregistrer",
cancelbutton="Annuler",
)
bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % (
formsemestre_id,
etudid,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + html_footer
elif tf[0] == -1:
return flask.redirect(bull_url)
else:
note = tf[2]["note"].strip().upper()
note_value, invalid = sco_saisie_notes.convert_note_from_string(note, 20.0)
if invalid:
return (
"\n".join(H)
+ "\n"
+ tf_error_message("valeur note invalide")
+ tf[1]
+ html_footer
)
if tf[2]["existing_ue"]:
ue_id = int(tf[2]["existing_ue"])
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
else:
acronyme = tf[2]["acronyme"].strip()
if not acronyme:
return (
"\n".join(H)
+ "\n"
+ tf_error_message("spécifier acronyme d'UE")
+ tf[1]
+ html_footer
)
moduleimpl_id = external_ue_create(
formsemestre_id,
titre=tf[2]["titre"],
acronyme=acronyme,
ue_type=tf[2]["type"], # type de l'UE
ects=tf[2]["ects"],
)
external_ue_inscrit_et_note(
moduleimpl_id,
formsemestre_id,
{etudid: note_value},
)
return flask.redirect(bull_url + "&head_message=Ajout%20effectué")

View File

@ -1,375 +1,375 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonctions sur les utilisateurs
"""
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy
import re
from flask import url_for, g, request
from flask.templating import render_template
from flask_login import current_user
from app import db, Departement
from app.auth.models import Permission
from app.auth.models import User
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_excel
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app import log, cache
from app.scodoc.scolog import logdb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoValueError,
)
# ---------------
# ---------------
def index_html(all_depts=False, with_inactives=False, format="html"):
"gestion utilisateurs..."
all_depts = int(all_depts)
with_inactives = int(with_inactives)
H = [html_sco_header.html_sem_header("Gestion des utilisateurs")]
if current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept):
H.append(
'<p><a href="{}" class="stdlink">Ajouter un utilisateur</a>'.format(
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
)
)
if current_user.is_administrator():
H.append(
'&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
)
)
else:
H.append(
"&nbsp;&nbsp; Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
)
if all_depts:
checked = "checked"
else:
checked = ""
if with_inactives:
olds_checked = "checked"
else:
olds_checked = ""
H.append(
"""<p><form name="f" action="%s" method="get">
<input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
<input type="checkbox" name="with_inactives" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
</form></p>"""
% (request.base_url, checked, olds_checked)
)
L = list_users(
g.scodoc_dept,
all_depts=all_depts,
with_inactives=with_inactives,
format=format,
with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept),
)
if format != "html":
return L
H.append(L)
F = html_sco_header.sco_footer()
return "\n".join(H) + F
def list_users(
dept,
all_depts=False, # tous les departements
with_inactives=False, # inclut les anciens utilisateurs (status "old")
format="html",
with_links=True,
):
"List users, returns a table in the specified format"
from app.scodoc.sco_permissions_check import can_handle_passwd
if dept and not all_depts:
users = get_user_list(dept=dept, with_inactives=with_inactives)
comm = "dept. %s" % dept
else:
users = get_user_list(with_inactives=with_inactives)
comm = "tous"
if with_inactives:
comm += ", avec anciens"
comm = "(" + comm + ")"
# -- Add some information and links:
r = []
for u in users:
# Can current user modify this user ?
can_modify = can_handle_passwd(u, allow_admindepts=True)
d = u.to_dict()
r.append(d)
# Add links
if with_links and can_modify:
target = url_for(
"users.user_info_page", scodoc_dept=dept, user_name=u.user_name
)
d["_user_name_target"] = target
d["_nom_target"] = target
d["_prenom_target"] = target
# Hide passwd modification date (depending on visitor's permission)
if not can_modify:
d["date_modif_passwd"] = "(non visible)"
columns_ids = [
"user_name",
"nom_fmt",
"prenom_fmt",
"email",
"dept",
"roles_string",
"date_expiration",
"date_modif_passwd",
"passwd_temp",
"status_txt",
]
# Seul l'admin peut voir les dates de dernière connexion
if current_user.is_administrator():
columns_ids.append("last_seen")
title = "Utilisateurs définis dans ScoDoc"
tab = GenTable(
rows=r,
columns_ids=columns_ids,
titles={
"user_name": "Login",
"nom_fmt": "Nom",
"prenom_fmt": "Prénom",
"email": "Mail",
"dept": "Dept.",
"roles_string": "Rôles",
"date_expiration": "Expiration",
"date_modif_passwd": "Modif. mot de passe",
"last_seen": "Dernière cnx.",
"passwd_temp": "Temp.",
"status_txt": "Etat",
},
caption=title,
page_title="title",
html_title="""<h2>%d utilisateurs %s</h2>
<p class="help">Cliquer sur un nom pour changer son mot de passe</p>"""
% (len(r), comm),
html_class="table_leftalign list_users",
html_with_td_classes=True,
html_sortable=True,
base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
pdf_link=False, # table is too wide to fit in a paper page => disable pdf
preferences=sco_preferences.SemPreferences(),
)
return tab.make_page(format=format, with_html_headers=False)
def get_user_list(dept=None, with_inactives=False):
"""Returns list of users.
If dept, select users from this dept,
else return all users.
"""
# was get_userlist
q = User.query
if dept is not None:
q = q.filter_by(dept=dept)
if not with_inactives:
q = q.filter_by(active=True)
return q.order_by(User.nom, User.user_name).all()
def _user_list(user_name):
"return user as a dict"
u = User.query.filter_by(user_name=user_name).first()
if u:
return u.to_dict()
else:
return None
@cache.memoize(timeout=50) # seconds
def user_info(user_name_or_id=None, user: User = None):
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
de User.
"""
if user_name_or_id is not None:
if isinstance(user_name_or_id, int):
u = User.query.filter_by(id=user_name_or_id).first()
else:
u = User.query.filter_by(user_name=user_name_or_id).first()
if u:
user_name = u.user_name
info = u.to_dict()
else:
info = None
user_name = "inconnu"
else:
info = user.to_dict()
user_name = user.user_name
if not info:
# special case: user is not in our database
return {
"user_name": user_name,
"nom": user_name,
"prenom": "",
"email": "",
"dept": "",
"nomprenom": user_name,
"prenomnom": user_name,
"prenom_fmt": "",
"nom_fmt": user_name,
"nomcomplet": user_name,
"nomplogin": user_name,
# "nomnoacc": scu.suppress_accents(user_name),
"passwd_temp": 0,
"status": "",
"date_expiration": None,
}
else:
# Ensure we never publish password hash
if "password_hash" in info:
del info["password_hash"]
return info
def check_modif_user(
edit,
enforce_optionals=False,
user_name="",
nom="",
prenom="",
email="",
dept="",
roles=[],
):
"""Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
Cherche homonymes.
returns (ok, msg)
- ok : si vrai, peut continuer avec ces parametres
(si ok est faux, l'utilisateur peut quand même forcer la creation)
- msg: message warning à presenter à l'utilisateur
"""
MSG_OPT = """<br/>Attention: (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
# ce login existe ?
user = _user_list(user_name)
if edit and not user: # safety net, le user_name ne devrait pas changer
return False, "identifiant %s inexistant" % user_name
if not edit and user:
return False, "identifiant %s déjà utilisé" % user_name
if not user_name or not nom or not prenom:
return False, "champ requis vide"
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", user_name):
return (
False,
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
% user_name,
)
if enforce_optionals and len(user_name) > 64:
return False, "identifiant '%s' trop long (64 caractères)" % user_name
if enforce_optionals and len(nom) > 64:
return False, "nom '%s' trop long (64 caractères)" % nom + MSG_OPT
if enforce_optionals and len(prenom) > 64:
return False, "prenom '%s' trop long (64 caractères)" % prenom + MSG_OPT
# check that tha same user_name has not already been described in this import
if not email:
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
if len(email) > 120:
return False, "email '%s' trop long (120 caractères)" % email
if not re.fullmatch(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
return False, "l'adresse mail semble incorrecte"
# check département
if (
enforce_optionals
and dept
and Departement.query.filter_by(acronym=dept).first() is None
):
return False, "département '%s' inexistant" % dept + MSG_OPT
if enforce_optionals and not roles:
return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT
# Unicité du mail
users_with_this_mail = User.query.filter_by(email=email).all()
if edit: # modification
if email != user["email"] and len(users_with_this_mail) > 0:
return False, "un autre utilisateur existe déjà avec cette adresse mail"
else: # création utilisateur
if len(users_with_this_mail) > 0:
return False, "un autre utilisateur existe déjà avec cette adresse mail"
# ok
# Des noms/prénoms semblables existent ?
nom = nom.lower().strip()
prenom = prenom.lower().strip()
similar_users = User.query.filter(
User.nom.ilike(nom), User.prenom.ilike(prenom)
).all()
if edit:
minmatch = 1
else:
minmatch = 0
if enforce_optionals and len(similar_users) > minmatch:
return (
False,
"des utilisateurs proches existent: "
+ ", ".join(
[
"%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name)
for x in similar_users
]
)
+ MSG_OPT,
)
# Roles ?
return True, ""
def user_edit(user_name, vals):
"""Edit the user specified by user_name
(ported from Zope to SQLAlchemy, hence strange !)
"""
u = User.query.filter_by(user_name=user_name).first()
if not u:
raise ScoValueError("Invalid user_name")
u.from_dict(vals)
db.session.add(u)
db.session.commit()
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonctions sur les utilisateurs
"""
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy
import re
from flask import url_for, g, request
from flask.templating import render_template
from flask_login import current_user
from app import db, Departement
from app.auth.models import Permission
from app.auth.models import User
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_excel
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app import log, cache
from app.scodoc.scolog import logdb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoValueError,
)
# ---------------
# ---------------
def index_html(all_depts=False, with_inactives=False, format="html"):
"gestion utilisateurs..."
all_depts = int(all_depts)
with_inactives = int(with_inactives)
H = [html_sco_header.html_sem_header("Gestion des utilisateurs")]
if current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept):
H.append(
'<p><a href="{}" class="stdlink">Ajouter un utilisateur</a>'.format(
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
)
)
if current_user.is_administrator():
H.append(
'&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
)
)
else:
H.append(
"&nbsp;&nbsp; Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
)
if all_depts:
checked = "checked"
else:
checked = ""
if with_inactives:
olds_checked = "checked"
else:
olds_checked = ""
H.append(
"""<p><form name="f" action="%s" method="get">
<input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
<input type="checkbox" name="with_inactives" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
</form></p>"""
% (request.base_url, checked, olds_checked)
)
L = list_users(
g.scodoc_dept,
all_depts=all_depts,
with_inactives=with_inactives,
format=format,
with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept),
)
if format != "html":
return L
H.append(L)
F = html_sco_header.sco_footer()
return "\n".join(H) + F
def list_users(
dept,
all_depts=False, # tous les departements
with_inactives=False, # inclut les anciens utilisateurs (status "old")
format="html",
with_links=True,
):
"List users, returns a table in the specified format"
from app.scodoc.sco_permissions_check import can_handle_passwd
if dept and not all_depts:
users = get_user_list(dept=dept, with_inactives=with_inactives)
comm = "dept. %s" % dept
else:
users = get_user_list(with_inactives=with_inactives)
comm = "tous"
if with_inactives:
comm += ", avec anciens"
comm = "(" + comm + ")"
# -- Add some information and links:
r = []
for u in users:
# Can current user modify this user ?
can_modify = can_handle_passwd(u, allow_admindepts=True)
d = u.to_dict()
r.append(d)
# Add links
if with_links and can_modify:
target = url_for(
"users.user_info_page", scodoc_dept=dept, user_name=u.user_name
)
d["_user_name_target"] = target
d["_nom_target"] = target
d["_prenom_target"] = target
# Hide passwd modification date (depending on visitor's permission)
if not can_modify:
d["date_modif_passwd"] = "(non visible)"
columns_ids = [
"user_name",
"nom_fmt",
"prenom_fmt",
"email",
"dept",
"roles_string",
"date_expiration",
"date_modif_passwd",
"passwd_temp",
"status_txt",
]
# Seul l'admin peut voir les dates de dernière connexion
if current_user.is_administrator():
columns_ids.append("last_seen")
title = "Utilisateurs définis dans ScoDoc"
tab = GenTable(
rows=r,
columns_ids=columns_ids,
titles={
"user_name": "Login",
"nom_fmt": "Nom",
"prenom_fmt": "Prénom",
"email": "Mail",
"dept": "Dept.",
"roles_string": "Rôles",
"date_expiration": "Expiration",
"date_modif_passwd": "Modif. mot de passe",
"last_seen": "Dernière cnx.",
"passwd_temp": "Temp.",
"status_txt": "Etat",
},
caption=title,
page_title="title",
html_title="""<h2>%d utilisateurs %s</h2>
<p class="help">Cliquer sur un nom pour changer son mot de passe</p>"""
% (len(r), comm),
html_class="table_leftalign list_users",
html_with_td_classes=True,
html_sortable=True,
base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
pdf_link=False, # table is too wide to fit in a paper page => disable pdf
preferences=sco_preferences.SemPreferences(),
)
return tab.make_page(format=format, with_html_headers=False)
def get_user_list(dept=None, with_inactives=False):
"""Returns list of users.
If dept, select users from this dept,
else return all users.
"""
# was get_userlist
q = User.query
if dept is not None:
q = q.filter_by(dept=dept)
if not with_inactives:
q = q.filter_by(active=True)
return q.order_by(User.nom, User.user_name).all()
def _user_list(user_name):
"return user as a dict"
u = User.query.filter_by(user_name=user_name).first()
if u:
return u.to_dict()
else:
return None
@cache.memoize(timeout=50) # seconds
def user_info(user_name_or_id=None, user: User = None):
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
de User.
"""
if user_name_or_id is not None:
if isinstance(user_name_or_id, int):
u = User.query.filter_by(id=user_name_or_id).first()
else:
u = User.query.filter_by(user_name=user_name_or_id).first()
if u:
user_name = u.user_name
info = u.to_dict()
else:
info = None
user_name = "inconnu"
else:
info = user.to_dict()
user_name = user.user_name
if not info:
# special case: user is not in our database
return {
"user_name": user_name,
"nom": user_name,
"prenom": "",
"email": "",
"dept": "",
"nomprenom": user_name,
"prenomnom": user_name,
"prenom_fmt": "",
"nom_fmt": user_name,
"nomcomplet": user_name,
"nomplogin": user_name,
# "nomnoacc": scu.suppress_accents(user_name),
"passwd_temp": 0,
"status": "",
"date_expiration": None,
}
else:
# Ensure we never publish password hash
if "password_hash" in info:
del info["password_hash"]
return info
def check_modif_user(
edit,
enforce_optionals=False,
user_name="",
nom="",
prenom="",
email="",
dept="",
roles=[],
):
"""Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
Cherche homonymes.
returns (ok, msg)
- ok : si vrai, peut continuer avec ces parametres
(si ok est faux, l'utilisateur peut quand même forcer la creation)
- msg: message warning à presenter à l'utilisateur
"""
MSG_OPT = """<br>Attention: (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
# ce login existe ?
user = _user_list(user_name)
if edit and not user: # safety net, le user_name ne devrait pas changer
return False, "identifiant %s inexistant" % user_name
if not edit and user:
return False, "identifiant %s déjà utilisé" % user_name
if not user_name or not nom or not prenom:
return False, "champ requis vide"
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", user_name):
return (
False,
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
% user_name,
)
if enforce_optionals and len(user_name) > 64:
return False, "identifiant '%s' trop long (64 caractères)" % user_name
if enforce_optionals and len(nom) > 64:
return False, "nom '%s' trop long (64 caractères)" % nom + MSG_OPT
if enforce_optionals and len(prenom) > 64:
return False, "prenom '%s' trop long (64 caractères)" % prenom + MSG_OPT
# check that tha same user_name has not already been described in this import
if not email:
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
if len(email) > 120:
return False, "email '%s' trop long (120 caractères)" % email
if not re.fullmatch(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
return False, "l'adresse mail semble incorrecte"
# check département
if (
enforce_optionals
and dept
and Departement.query.filter_by(acronym=dept).first() is None
):
return False, "département '%s' inexistant" % dept + MSG_OPT
if enforce_optionals and not roles:
return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT
# Unicité du mail
users_with_this_mail = User.query.filter_by(email=email).all()
if edit: # modification
if email != user["email"] and len(users_with_this_mail) > 0:
return False, "un autre utilisateur existe déjà avec cette adresse mail"
else: # création utilisateur
if len(users_with_this_mail) > 0:
return False, "un autre utilisateur existe déjà avec cette adresse mail"
# ok
# Des noms/prénoms semblables existent ?
nom = nom.lower().strip()
prenom = prenom.lower().strip()
similar_users = User.query.filter(
User.nom.ilike(nom), User.prenom.ilike(prenom)
).all()
if edit:
minmatch = 1
else:
minmatch = 0
if enforce_optionals and len(similar_users) > minmatch:
return (
False,
"des utilisateurs proches existent: "
+ ", ".join(
[
"%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name)
for x in similar_users
]
)
+ MSG_OPT,
)
# Roles ?
return True, ""
def user_edit(user_name, vals):
"""Edit the user specified by user_name
(ported from Zope to SQLAlchemy, hence strange !)
"""
u = User.query.filter_by(user_name=user_name).first()
if not u:
raise ScoValueError("Invalid user_name")
u.from_dict(vals)
db.session.add(u)
db.session.commit()

View File

@ -2863,10 +2863,13 @@ select.tf-selglobal {
}
td.tf-fieldlabel {
/* font-weight: bold; */
vertical-align: top;
}
td.tf-field {
max-width: 800px;
}
.tf-comment {
font-size: 80%;
font-style: italic;
@ -2876,6 +2879,12 @@ td.tf-fieldlabel {
font-style: italic;
}
#tf details summary {
font-size: 130%;
margin-top: 6px;
margin-bottom: 6px;
}
.radio_green {
background-color: green;
}

File diff suppressed because it is too large Load Diff

View File

@ -852,7 +852,7 @@ def formsemestre_change_lock(formsemestre_id, dialog_confirmed=False):
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur).
<br/>
<br>
Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
""",
dest_url="",
@ -3045,12 +3045,12 @@ def check_sem_integrity(formsemestre_id, fix=False):
if bad_ue:
H += [
"<h2>Modules d'une autre formation que leur UE:</h2>",
"<br/>".join([str(x) for x in bad_ue]),
"<br>".join([str(x) for x in bad_ue]),
]
if bad_sem:
H += [
"<h2>Module du semestre dans une autre formation:</h2>",
"<br/>".join([str(x) for x in bad_sem]),
"<br>".join([str(x) for x in bad_sem]),
]
if not bad_ue and not bad_sem:
H.append("<p>Aucun problème à signaler !</p>")
@ -3106,7 +3106,7 @@ def check_form_integrity(formation_id, fix=False):
if mod["formation_id"] != formation_id:
bad.append(mod)
if bad:
txth = "<br/>".join([str(x) for x in bad])
txth = "<br>".join([str(x) for x in bad])
txt = "\n".join([str(x) for x in bad])
log("check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id)
log(txt)
@ -3162,7 +3162,7 @@ def check_formsemestre_integrity(formsemestre_id):
diag = ["OK"]
log("ok")
return (
html_sco_header.sco_header() + "<br/>".join(diag) + html_sco_header.sco_footer()
html_sco_header.sco_header() + "<br>".join(diag) + html_sco_header.sco_footer()
)

View File

@ -2141,9 +2141,9 @@ def form_students_import_infos_admissions(formsemestre_id=None):
Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés,
les autres lignes de la feuille seront ignorées. Et seules les colonnes intéressant ScoDoc
seront importées: il est inutile d'éliminer les autres.
<br/>
<br>
<em>Seules les données "admission" seront modifiées (et pas l'identité de l'étudiant).</em>
<br/>
<br>
<em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em>
</p>
<p>
@ -2240,7 +2240,7 @@ def formsemestre_import_etud_admission(formsemestre_id, import_email=True):
H.append("<h3>Adresses mails modifiées:</h3>")
for (info, new_mail) in changed_mails:
H.append(
"%s: <tt>%s</tt> devient <tt>%s</tt><br/>"
"%s: <tt>%s</tt> devient <tt>%s</tt><br>"
% (info["nom"], info["email"], new_mail)
)
return "\n".join(H) + html_sco_header.sco_footer()

File diff suppressed because it is too large Load Diff