ScoDoc-Lille/app/scodoc/sco_moduleimpl_status.py

717 lines
27 KiB
Python

# -*- 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
#
##############################################################################
"""Tableau de bord module
"""
import time
from flask import g, url_for
from flask_login import current_user
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import ModuleImpl
from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoInvalidIdType
from app.scodoc.sco_parcours_dut import formsemestre_has_decisions
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_abs
from app.scodoc import sco_compute_moy
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc import sco_users
# ported from old DTML code in oct 2009
# menu evaluation dans moduleimpl
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
"Menu avec actions sur une evaluation"
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
if (
sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
)
and nbnotes != 0
):
sup_label = "Supprimer évaluation impossible (il y a des notes)"
else:
sup_label = "Supprimer évaluation"
menuEval = [
{
"title": "Saisir notes",
"endpoint": "notes.saisie_notes",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"]
),
},
{
"title": "Modifier évaluation",
"endpoint": "notes.evaluation_edit",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
),
},
{
"title": sup_label,
"endpoint": "notes.evaluation_delete",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": nbnotes == 0
and sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
),
},
{
"title": "Supprimer toutes les notes",
"endpoint": "notes.evaluation_suppress_alln",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"], allow_ens=False
),
},
{
"title": "Afficher les notes",
"endpoint": "notes.evaluation_listenotes",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": nbnotes > 0,
},
{
"title": "Placement étudiants",
"endpoint": "notes.placement_eval_selectetuds",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"]
),
},
{
"title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate",
"args": {
"date": E["jour"],
"group_ids": group_id,
},
"enabled": E["jour"],
},
{
"title": "Vérifier notes vs absents",
"endpoint": "notes.evaluation_check_absences_html",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": nbnotes > 0 and E["jour"],
},
]
return htmlutils.make_menu("actions", menuEval, alone=True)
def _ue_coefs_html(coefs_lst) -> str:
""" """
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
H = """
<div id="modimpl_coefs">
<div>Coefficients vers les UE</div>
"""
if coefs_lst:
H += (
f"""
<div class="coefs_histo" style="--max:{max_coef}">
"""
+ "\n".join(
[
f"""<div style="--coef:{coef};
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
]
)
+ "</div>"
)
else:
H += """<div class="missing_value">non définis</div>"""
H += "</div>"
return H
def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"""Tableau de bord module (liste des evaluations etc)"""
if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict()
formsemestre_id = modimpl.formsemestre_id
Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=M["moduleimpl_id"]
)
nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre)
mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
mod_evals.sort(
key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
) # la plus RECENTE en tête
#
sem_locked = not sem["etat"]
can_edit_evals = (
sco_permissions_check.can_edit_notes(
current_user, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"]
)
and not sem_locked
)
can_edit_notes = (
sco_permissions_check.can_edit_notes(current_user, moduleimpl_id)
and not sem_locked
)
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
#
module_resp = User.query.get(M["responsable_id"])
mod_type_name = scu.MODULE_TYPE_NAMES[Mod["module_type"]]
H = [
html_sco_header.sco_header(
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{Mod['code']}</tt> {Mod['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""}
</h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(Mod['module_type']).name.lower()}">
<table>
<tr>
<td class="fichetitre2">Responsable: </td><td class="redboldtext">
{module_resp.get_nomcomplet()}
<span class="blacktt">({module_resp.user_name})</span>
""",
]
try:
sco_moduleimpl.can_change_module_resp(moduleimpl_id)
H.append(
"""<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>"""
% moduleimpl_id
)
except:
pass
H.append("""</td><td>""")
H.append(
", ".join([sco_users.user_info(m["ens_id"])["nomprenom"] for m in M["ens"]])
)
H.append("""</td><td>""")
try:
sco_moduleimpl.can_change_ens(moduleimpl_id)
H.append(
"""<a class="stdlink" href="edit_enseignants_form?moduleimpl_id=%s">modifier les enseignants</a>"""
% moduleimpl_id
)
except:
pass
H.append("""</td></tr>""")
# 2ieme ligne: Semestre, Coef
H.append("""<tr><td class="fichetitre2">""")
if sem["semestre_id"] >= 0:
H.append("""Semestre: </td><td>%s""" % sem["semestre_id"])
else:
H.append("""</td><td>""")
if sem_locked:
H.append(scu.icontag("lock32_img", title="verrouillé"))
H.append("""</td><td class="fichetitre2">""")
if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
else:
H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}")
H.append("""</td><td></td></tr>""")
# 3ieme ligne: Formation
H.append(
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>""" % F
)
# Ligne: Inscrits
H.append(
"""<tr><td class="fichetitre2">Inscrits: </td><td> %d étudiants"""
% len(mod_inscrits)
)
if current_user.has_permission(Permission.ScoEtudInscrit):
H.append(
"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
% M["moduleimpl_id"]
)
H.append("</td></tr>")
# Ligne: règle de calcul
has_expression = sco_compute_moy.moduleimpl_has_expression(M)
if has_expression:
H.append(
'<tr><td class="fichetitre2" colspan="4">Règle de calcul: <span class="formula" title="mode de calcul de la moyenne du module">moyenne=<tt>%s</tt></span>'
% M["computation_expr"]
)
H.append('<span class="warning">inutilisée dans cette version de ScoDoc</span>')
H.append("</td></tr>")
else:
H.append(
'<tr><td colspan="4">' # <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
)
# if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
# H.append(
# f' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id={moduleimpl_id}">changer</a>)'
# )
H.append("</td></tr>")
H.append(
'<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink" href="view_module_abs?moduleimpl_id=%s">Absences dans ce module</a></span>'
% moduleimpl_id
)
# Adapté à partir d'une suggestion de DS (Le Havre)
# Liens saisies absences seulement si permission et date courante dans le semestre
if current_user.has_permission(
Permission.ScoAbsChange
) and sco_formsemestre.sem_est_courant(sem):
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
group_id = sco_groups.get_default_group(formsemestre_id)
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink"
href="{url_for("absences.SignaleAbsenceGrHebdo",
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id,
moduleimpl_id=moduleimpl_id, datelundi=datelundi, group_ids=group_id)}">
Saisie Absences hebdo.</a></span>
"""
)
H.append("</td></tr></table>")
#
if not modimpl.check_apc_conformity():
H.append(
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
pas encore conformes au PN.
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
</div>"""
)
#
if has_expression and nt.expr_diagnostics:
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
#
if formsemestre_has_decisions(formsemestre_id):
H.append(
"""<ul class="tf-msg"><li class="tf-msg warning">Décisions de jury saisies: seul le responsable du semestre peut saisir des notes (il devra modifier les décisions de jury).</li></ul>"""
)
#
H.append(
"""<p><form name="f"><span style="font-size:120%%; font-weight: bold;">%d évaluations :</span>
<span style="padding-left: 30px;">
<input type="hidden" name="moduleimpl_id" value="%s"/>"""
% (len(mod_evals), moduleimpl_id)
)
#
# Liste les noms de partitions
partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
H.append(
"""Afficher les groupes de&nbsp;<select name="partition_id" onchange="document.f.submit();">"""
)
been_selected = False
for partition in partitions:
if not partition_id and not been_selected:
selected = "selected"
been_selected = True
if partition["partition_id"] == partition_id:
selected = "selected"
else:
selected = ""
name = partition["partition_name"]
if name is None:
name = "Tous"
H.append(
"""<option value="%s" %s>%s</option>"""
% (partition["partition_id"], selected, name)
)
H.append(
"""</select>
&nbsp;&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%(moduleimpl_id)s">Voir toutes les notes</a>
</span>
</form>
</p>
"""
% M
)
# -------- Tableau des evaluations
top_table_links = ""
if can_edit_evals:
top_table_links = f"""<a class="stdlink" href="{
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id'])
}">Créer nouvelle évaluation</a>
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.module_evaluation_renumber", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id'],
redirect=1)
}">Trier par date</a>
"""
if mod_evals:
H.append(
'<div class="moduleimpl_evaluations_top_links">'
+ top_table_links
+ "</div>"
)
H.append("""<table class="moduleimpl_evaluations">""")
eval_index = len(mod_evals) - 1
first_eval = True
for eval in mod_evals:
evaluation: Evaluation = Evaluation.query.get(
eval["evaluation_id"]
) # TODO unifier
etat = sco_evaluations.do_evaluation_etat(
eval["evaluation_id"],
partition_id=partition_id,
select_first_partition=True,
)
if eval["evaluation_type"] in (
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
):
tr_class = "mievr mievr_rattr"
else:
tr_class = "mievr"
tr_class_1 = "mievr"
if not first_eval:
H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
tr_class_1 += " mievr_spaced"
H.append("""<tr class="%s"><td class="mievr_tit" colspan="8">""" % tr_class_1)
if eval["jour"]:
H.append("""Le %(jour)s%(descrheure)s""" % eval)
else:
H.append(
"""<a href="evaluation_edit?evaluation_id=%(evaluation_id)s" class="mievr_evalnodate">Evaluation sans date</a>"""
% eval
)
H.append("&nbsp;&nbsp;&nbsp; <em>%(description)s</em>" % eval)
if eval["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
H.append(
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>"""
)
elif eval["evaluation_type"] == scu.EVALUATION_SESSION2:
H.append(
"""<span class="mievr_rattr" title="remplace autres notes">session 2</span>"""
)
if etat["last_modif"]:
H.append(
"""<span class="mievr_lastmodif">(dernière modif le %s)</span>"""
% etat["last_modif"].strftime("%d/%m/%Y à %Hh%M")
)
H.append('<span class="evalindex_cont">')
if has_expression or True:
H.append(
"""<span class="evalindex" title="Indice dans les vecteurs (formules)">%2d</span>"""
% eval_index
)
# Fleches:
H.append('<span class="eval_arrows_chld">')
if eval_index != (len(mod_evals) - 1) and can_edit_evals:
H.append(
'<a href="module_evaluation_move?evaluation_id=%s&after=0" class="aud">%s</a>'
% (eval["evaluation_id"], arrow_up)
)
else:
H.append(arrow_none)
if (eval_index > 0) and can_edit_evals:
H.append(
'<a href="module_evaluation_move?evaluation_id=%s&after=1" class="aud">%s</a>'
% (eval["evaluation_id"], arrow_down)
)
else:
H.append(arrow_none)
H.append("</span></span>")
eval_index -= 1
H.append("""</td></tr>""")
H.append(
"""<tr class="%s"><th class="moduleimpl_evaluations" colspan="2">&nbsp;</th><th class="moduleimpl_evaluations">Durée</th><th class="moduleimpl_evaluations">Coef.</th><th class="moduleimpl_evaluations">Notes</th><th class="moduleimpl_evaluations">Abs</th><th class="moduleimpl_evaluations">N</th><th class="moduleimpl_evaluations">Moyenne """
% tr_class
)
if etat["evalcomplete"]:
etat_txt = """(prise en compte)"""
etat_descr = "notes utilisées dans les moyennes"
elif eval["publish_incomplete"]:
etat_txt = """(prise en compte <b>immédiate</b>)"""
etat_descr = (
"il manque des notes, mais la prise en compte immédiate a été demandée"
)
elif etat["nb_notes"] != 0:
etat_txt = "(<b>non</b> prise en compte)"
etat_descr = "il manque des notes"
else:
etat_txt = ""
if can_edit_evals and etat_txt:
etat_txt = (
'<a href="evaluation_edit?evaluation_id=%s" title="%s">%s</a>'
% (eval["evaluation_id"], etat_descr, etat_txt)
)
H.append(etat_txt)
H.append("""</th></tr>""")
H.append("""<tr class="%s"><td class="mievr">""" % tr_class)
if can_edit_evals:
H.append(
"""<a class="smallbutton" href="evaluation_edit?evaluation_id=%s">%s</a>"""
% (
eval["evaluation_id"],
scu.icontag(
"edit_img", alt="modifier", title="Modifier informations"
),
)
)
if can_edit_notes:
H.append(
"""<a class="smallbutton" href="saisie_notes?evaluation_id=%s">%s</a>"""
% (
eval["evaluation_id"],
scu.icontag(
"notes_img", alt="saisie notes", title="Saisie des notes"
),
)
)
if etat["nb_notes"] == 0:
if can_edit_evals:
H.append(
"""<a class="smallbutton" href="evaluation_delete?evaluation_id=%(evaluation_id)s">"""
% eval
)
H.append(scu.icontag("delete_img", alt="supprimer", title="Supprimer"))
if can_edit_evals:
H.append("""</a>""")
elif etat["evalcomplete"]:
H.append(
"""<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
% (eval["evaluation_id"], scu.icontag("status_green_img", title="ok"))
)
else:
if etat["evalattente"]:
H.append(
"""<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
% (
eval["evaluation_id"],
scu.icontag(
"status_greenorange_img",
file_format="gif",
title="notes en attente",
),
)
)
else:
H.append(
"""<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
% (
eval["evaluation_id"],
scu.icontag("status_orange_img", title="il manque des notes"),
)
)
#
if eval["visibulletin"]:
H.append(
scu.icontag(
"status_visible_img", title="visible dans bulletins intermédiaires"
)
)
else:
H.append("&nbsp;")
H.append('</td><td class="mievr_menu">')
if can_edit_notes:
H.append(
moduleimpl_evaluation_menu(
eval["evaluation_id"],
nbnotes=etat["nb_notes"],
)
)
H.append("</td>")
#
H.append(
"""
<td class="mievr_dur">%s</td><td class="rightcell mievr_coef">%s</td>"""
% (eval["duree"], "%g" % eval["coefficient"])
)
H.append(
"""<td class="rightcell mievr_nbnotes">%(nb_notes)s / %(nb_inscrits)s</td>
<td class="rightcell mievr_coef">%(nb_abs)s</td>
<td class="rightcell mievr_coef">%(nb_neutre)s</td>
<td class="rightcell">"""
% etat
)
if etat["moy"]:
H.append("<b>%s / %g</b>" % (etat["moy"], eval["note_max"]))
H.append(
"""&nbsp; (<a href="evaluation_listenotes?evaluation_id=%s">afficher</a>)"""
% (eval["evaluation_id"],)
)
else:
H.append(
"""<a class="redlink" href="saisie_notes?evaluation_id=%s">saisir notes</a>"""
% (eval["evaluation_id"])
)
H.append("""</td></tr>""")
#
if etat["nb_notes"] == 0:
H.append("""<tr class="%s"><td></td>""" % tr_class)
if modimpl.module.is_apc():
H.append(
f"""<td colspan="7" class="eval_poids">{
evaluation.get_ue_poids_str()}</td>"""
)
else:
H.append('<td colspan="7"></td>')
H.append("""</tr>""")
else: # il y a deja des notes saisies
gr_moyennes = etat["gr_moyennes"]
first_group = True
for gr_moyenne in gr_moyennes:
H.append("""<tr class="%s">""" % tr_class)
H.append("""<td>&nbsp;</td>""")
if first_group and modimpl.module.is_apc():
H.append(
f"""<td class="eval_poids" colspan="3">{
evaluation.get_ue_poids_str()}</td>"""
)
else:
H.append("""<td colspan="3"></td>""")
first_group = False
if gr_moyenne["group_name"] is None:
name = "Tous" # tous
else:
name = "Groupe %s" % gr_moyenne["group_name"]
H.append(
"""<td colspan="3" class="mievr_grtit">%s &nbsp;</td><td>""" % name
)
if gr_moyenne["gr_nb_notes"] > 0:
H.append("%(gr_moy)s" % gr_moyenne)
H.append(
"""&nbsp; (<a href="evaluation_listenotes?tf_submitted=1&evaluation_id=%s&group_ids%%3Alist=%s">%s notes</a>"""
% (
eval["evaluation_id"],
gr_moyenne["group_id"],
gr_moyenne["gr_nb_notes"],
)
)
if gr_moyenne["gr_nb_att"] > 0:
H.append(
""", <span class="redboldtext">%s en attente</span>"""
% gr_moyenne["gr_nb_att"]
)
H.append(""")""")
if gr_moyenne["group_id"] in etat["gr_incomplets"]:
H.append("""[<font color="red">""")
if can_edit_notes:
H.append(
"""<a class="redlink" href="saisie_notes?evaluation_id=%s&group_ids:list=%s">incomplet</a></font>]"""
% (eval["evaluation_id"], gr_moyenne["group_id"])
)
else:
H.append("""incomplet</font>]""")
else:
H.append("""<span class="redboldtext">&nbsp; """)
if can_edit_notes:
H.append(
"""<a class="redlink" href="saisie_notes?evaluation_id=%s&group_ids:list=%s">"""
% (eval["evaluation_id"], gr_moyenne["group_id"])
)
H.append("pas de notes")
if can_edit_notes:
H.append("""</a>""")
H.append("</span>")
H.append("""</td></tr>""")
first_eval = False
#
H.append("""<tr><td colspan="8">""")
if sem_locked:
H.append(f"""{scu.icontag("lock32_img")} semestre verrouillé""")
elif can_edit_evals:
H.append(top_table_links)
H.append(
"""</td></tr>
</table>
</div>
<!-- LEGENDE -->
<hr>
<h4>Légende</h4>
<ul>
<li>%s : modifie description de l'évaluation (date, heure, coefficient, ...)</li>
<li>%s : saisie des notes</li>
<li>%s : indique qu'il n'y a aucune note entrée (cliquer pour supprimer cette évaluation)</li>
<li>%s : indique qu'il manque quelques notes dans cette évaluation</li>
<li>%s : toutes les notes sont entrées (cliquer pour les afficher)</li>
<li>%s : indique que cette évaluation sera mentionnée dans les bulletins au format "intermédiaire"
</ul>
<p>Rappel : seules les notes des évaluations complètement saisies (affichées en vert) apparaissent dans les bulletins.
</p>
"""
% (
scu.icontag("edit_img"),
scu.icontag("notes_img"),
scu.icontag("delete_img"),
scu.icontag("status_orange_img"),
scu.icontag("status_green_img"),
scu.icontag("status_visible_img"),
)
)
H.append(html_sco_header.sco_footer())
return "".join(H)