# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 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 # ############################################################################## """Liste des notes d'une évaluation """ import urllib from types import StringType import sco_utils as scu import notesdb as ndb from notes_log import log from TrivialFormulator import TrivialFormulator, TF import sco_formsemestre import sco_moduleimpl import sco_groups import sco_evaluations import htmlutils import sco_excel from gen_tables import GenTable from htmlutils import histogram_notes import VERSION def do_evaluation_listenotes(context, REQUEST): """ Affichage des notes d'une évaluation args: evaluation_id """ mode = None if REQUEST.form.has_key("evaluation_id"): evaluation_id = REQUEST.form["evaluation_id"] mode = "eval" evals = context.do_evaluation_list({"evaluation_id": evaluation_id}) if REQUEST.form.has_key("moduleimpl_id"): moduleimpl_id = REQUEST.form["moduleimpl_id"] mode = "module" evals = context.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) if not mode: raise ValueError("missing argument: evaluation or module") if not evals: return "

Aucune évaluation !

" format = REQUEST.form.get("format", "html") E = evals[0] # il y a au moins une evaluation # description de l'evaluation if mode == "eval": H = [ sco_evaluations.evaluation_describe( context, evaluation_id=evaluation_id, REQUEST=REQUEST ) ] else: H = [] # groupes groups = sco_groups.do_evaluation_listegroupes( context, E["evaluation_id"], include_default=True ) grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons grnams = [g["group_id"] for g in groups] # noms des checkbox if len(evals) > 1: descr = [ ("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"}) ] else: descr = [ ("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"}) ] if len(grnams) > 1: descr += [ ( "s", { "input_type": "separator", "title": "Choix du ou des groupes d'étudiants:", }, ), ( "group_ids", { "input_type": "checkbox", "title": "", "allowed_values": grnams, "labels": grlabs, "attributes": ('onclick="document.tf.submit();"',), }, ), ] else: if grnams: def_nam = grnams[0] else: def_nam = "" descr += [ ( "group_ids", {"input_type": "hidden", "type": "list", "default": [def_nam]}, ) ] descr += [ ( "anonymous_listing", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ('listing "anonyme"',), "attributes": ('onclick="document.tf.submit();"',), "template": '%(label)s%(elem)s   ', }, ), ( "note_sur_20", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("notes sur 20",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s   ", }, ), ( "hide_groups", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("masquer les groupes",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s   ", }, ), ( "with_emails", { "input_type": "checkbox", "title": "", "allowed_values": ("yes",), "labels": ("montrer les e-mails",), "attributes": ('onclick="document.tf.submit();"',), "template": "%(elem)s", }, ), ] tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, descr, cancelbutton=None, submitbutton=None, bottom_buttons=False, method="GET", cssclass="noprint", name="tf", is_submitted=True, # toujours "soumis" (démarre avec liste complète) ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] elif tf[0] == -1: return REQUEST.RESPONSE.redirect( "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % (context.ScoURL(), E["moduleimpl_id"]) ) else: anonymous_listing = tf[2]["anonymous_listing"] note_sur_20 = tf[2]["note_sur_20"] hide_groups = tf[2]["hide_groups"] with_emails = tf[2]["with_emails"] return _make_table_notes( context, REQUEST, tf[1], evals, format=format, note_sur_20=note_sur_20, anonymous_listing=anonymous_listing, group_ids=tf[2]["group_ids"], hide_groups=hide_groups, with_emails=with_emails, ) def _make_table_notes( context, REQUEST, html_form, evals, format="", note_sur_20=False, anonymous_listing=False, hide_groups=False, with_emails=False, group_ids=[], ): """Generate table for evaluations marks""" if not evals: return "

Aucune évaluation !

" E = evals[0] moduleimpl_id = E["moduleimpl_id"] M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0] Mod = context.do_module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"]) # (debug) check that all evals are in same module: for e in evals: if e["moduleimpl_id"] != moduleimpl_id: raise ValueError("invalid evaluations list") if format == "xls": keep_numeric = True # pas de conversion des notes en strings else: keep_numeric = False # Si pas de groupe, affiche tout if not group_ids: group_ids = [sco_groups.get_default_group(context, M["formsemestre_id"])] groups = sco_groups.listgroups(context, group_ids) gr_title = sco_groups.listgroups_abbrev(groups) gr_title_filename = sco_groups.listgroups_filename(groups) etudids = sco_groups.do_evaluation_listeetuds_groups( context, E["evaluation_id"], groups, include_dems=True ) if anonymous_listing: columns_ids = ["code"] # cols in table else: if format == "xls" or format == "xml": columns_ids = ["nom", "prenom"] else: columns_ids = ["nomprenom"] if not hide_groups: columns_ids.append("group") titles = { "code": "Code", "group": "Groupe", "nom": "Nom", "prenom": "Prénom", "nomprenom": "Nom", "expl_key": "Rem.", "email": "e-mail", "emailperso": "e-mail perso", } rows = [] class keymgr(dict): # comment : key (pour regrouper les comments a la fin) def __init__(self): self.lastkey = 1 def nextkey(self): r = self.lastkey self.lastkey += 1 # self.lastkey = chr(ord(self.lastkey)+1) return str(r) K = keymgr() for etudid in etudids: css_row_class = None # infos identite etudiant etud = context.getEtudInfo(etudid=etudid, filled=1)[0] # infos inscription inscr = context.do_formsemestre_inscription_list( {"etudid": etudid, "formsemestre_id": M["formsemestre_id"]} )[0] if inscr["etat"] == "I": # si inscrit, indique groupe groups = sco_groups.get_etud_groups(context, etudid, sem) grc = sco_groups.listgroups_abbrev(groups) else: if inscr["etat"] == "D": grc = "DEM" # attention: ce code est re-ecrit plus bas, ne pas le changer (?) css_row_class = "etuddem" else: grc = inscr["etat"] code = "" # code pour listings anonyme, à la place du nom if context.get_preference("anonymous_lst_code") == "INE": code = etud["code_ine"] elif context.get_preference("anonymous_lst_code") == "NIP": code = etud["code_nip"] if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid code = etudid rows.append( { "code": code, "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"', "etudid": etudid, "nom": scu.strupper(etud["nom"]), "_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % (M["formsemestre_id"], etudid), "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]), "prenom": scu.strcapitalize(scu.strlower(etud["prenom"])), "nomprenom": etud["nomprenom"], "group": grc, "email": etud["email"], "emailperso": etud["emailperso"], "_css_row_class": css_row_class or "", } ) # Lignes en tête: coefs = { "nom": "", "prenom": "", "nomprenom": "", "group": "", "code": "", "_css_row_class": "sorttop fontitalic", "_table_part": "head", } note_max = { "nom": "", "prenom": "", "nomprenom": "", "group": "", "code": "", "_css_row_class": "sorttop fontitalic", "_table_part": "head", } moys = { "_css_row_class": "moyenne sortbottom", "_table_part": "foot", #'_nomprenom_td_attrs' : 'colspan="2" ', "nomprenom": "Moyenne (sans les absents) :", "comment": "", } # Ajoute les notes de chaque évaluation: for e in evals: e["eval_state"] = sco_evaluations.do_evaluation_etat( context, e["evaluation_id"] ) notes, nb_abs, nb_att = _add_eval_columns( context, e, rows, titles, coefs, note_max, moys, K, note_sur_20, keep_numeric, ) columns_ids.append(e["evaluation_id"]) # if anonymous_listing: rows.sort(key=lambda x: x["code"]) else: rows.sort(key=lambda x: (x["nom"], x["prenom"])) # sort by nom, prenom # Si module, ajoute moyenne du module: if len(evals) > 1: _add_moymod_column( context, sem["formsemestre_id"], e, rows, titles, coefs, note_max, moys, note_sur_20, keep_numeric, ) columns_ids.append("moymod") # Ajoute colonnes emails tout à droite: if with_emails: columns_ids += ["email", "emailperso"] # Ajoute lignes en tête et moyennes if len(evals) > 0: rows = [coefs, note_max] + rows rows.append(moys) # ajout liens HTMl vers affichage une evaluation: if format == "html" and len(evals) > 1: rlinks = {"_table_part": "head"} for e in evals: rlinks[e["evaluation_id"]] = "afficher" rlinks[ "_" + e["evaluation_id"] + "_help" ] = "afficher seulement les notes de cette évaluation" rlinks["_" + e["evaluation_id"] + "_target"] = ( "evaluation_listenotes?evaluation_id=" + e["evaluation_id"] ) rlinks["_" + e["evaluation_id"] + "_td_attrs"] = ' class="tdlink" ' rows.append(rlinks) if len(evals) == 1: # colonne "Rem." seulement si une eval if format == "html": # pas d'indication d'origine en pdf (pour affichage) columns_ids.append("expl_key") elif format == "xls" or format == "xml": columns_ids.append("comment") # titres divers: gl = "".join(["&group_ids%3Alist=" + g for g in group_ids]) if note_sur_20: gl = "&note_sur_20%3Alist=yes" + gl if anonymous_listing: gl = "&anonymous_listing%3Alist=yes" + gl if hide_groups: gl = "&hide_groups%3Alist=yes" + gl if with_emails: gl = "&with_emails%3Alist=yes" + gl if len(evals) == 1: evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"])) hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudids)) filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) caption = hh pdf_title = "%(description)s (%(jour)s)" % e html_title = "" base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl html_next_section = ( '
%d absents, %d en attente.
' % (nb_abs, nb_att) ) else: filename = scu.make_filename("notes_%s_%s" % (Mod["code"], gr_title_filename)) title = "Notes du module %(code)s %(titre)s" % Mod title += " semestre %(titremois)s" % sem if gr_title and gr_title != "tous": title += " %s" % gr_title caption = title html_next_section = "" if format == "pdf": caption = "" # same as pdf_title pdf_title = title html_title = ( """

Notes du module %s %s

""" % (moduleimpl_id, Mod["code"], Mod["titre"]) ) base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl # display tab = GenTable( titles=titles, columns_ids=columns_ids, rows=rows, html_sortable=True, base_url=base_url, filename=filename, origin="Généré par %s le " % VERSION.SCONAME + scu.timedate_human_repr() + "", caption=caption, html_next_section=html_next_section, page_title="Notes de " + sem["titremois"], html_title=html_title, pdf_title=pdf_title, html_class="table_leftalign notes_evaluation", preferences=context.get_preferences(M["formsemestre_id"]), # html_generate_cells=False # la derniere ligne (moyennes) est incomplete ) t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST) if format != "html": return t if len(evals) > 1: all_complete = True for e in evals: if not e["eval_state"]["evalcomplete"]: all_complete = False if all_complete: eval_info = 'Evaluations prises en compte dans les moyennes' else: eval_info = 'Les évaluations en vert et orange sont prises en compte dans les moyennes. Celles en rouge n\'ont pas toutes leurs notes.' return html_form + eval_info + t + "

" else: # Une seule evaluation: ajoute histogramme histo = histogram_notes(notes) # 2 colonnes: histo, comments C = [ "\n", '

Répartition des notes:

" + histo + "

', ] commentkeys = K.items() # [ (comment, key), ... ] commentkeys.sort(lambda x, y: cmp(int(x[1]), int(y[1]))) for (comment, key) in commentkeys: C.append( '(%s) %s
' % (key, comment) ) if commentkeys: C.append( 'Gérer les opérations
' % E["evaluation_id"] ) eval_info = "xxx" if E["eval_state"]["evalcomplete"]: eval_info = 'Evaluation prise en compte dans les moyennes' elif E["eval_state"]["evalattente"]: eval_info = 'Il y a des notes en attente (les autres sont prises en compte)' else: eval_info = 'Notes incomplètes, évaluation non prise en compte dans les moyennes' return ( sco_evaluations.evaluation_describe( context, evaluation_id=E["evaluation_id"], REQUEST=REQUEST ) + eval_info + html_form + t + "\n".join(C) ) def _add_eval_columns( context, e, rows, titles, coefs, note_max, moys, K, note_sur_20, keep_numeric ): """Add eval e""" nb_notes = 0 nb_abs = 0 nb_att = 0 sum_notes = 0 notes = [] # liste des notes numeriques, pour calcul histogramme uniquement evaluation_id = e["evaluation_id"] NotesDB = context._notes_getall(evaluation_id) for row in rows: etudid = row["etudid"] if NotesDB.has_key(etudid): val = NotesDB[etudid]["value"] if val is None: nb_abs += 1 if val == scu.NOTES_ATTENTE: nb_att += 1 # calcul moyenne SANS LES ABSENTS if val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE: if e["note_max"] > 0: valsur20 = val * 20.0 / e["note_max"] # remet sur 20 else: valsur20 = 0 notes.append(valsur20) # toujours sur 20 pour l'histogramme if note_sur_20: val = valsur20 # affichage notes / 20 demandé nb_notes = nb_notes + 1 sum_notes += val val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric) comment = NotesDB[etudid]["comment"] if comment is None: comment = "" explanation = "%s (%s) %s" % ( NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"), NotesDB[etudid]["uid"], comment, ) else: explanation = "" val_fmt = "" val = None if val is None: row["_" + evaluation_id + "_td_attrs"] = 'class="etudabs" ' if not row.get("_css_row_class", ""): row["_css_row_class"] = "etudabs" # regroupe les commentaires if explanation: if K.has_key(explanation): expl_key = "(%s)" % K[explanation] else: K[explanation] = K.nextkey() expl_key = "(%s)" % K[explanation] else: expl_key = "" row.update( { evaluation_id: val_fmt, "_" + evaluation_id + "_help": explanation, # si plusieurs evals seront ecrasés et non affichés: "comment": explanation, "expl_key": expl_key, "_expl_key_help": explanation, } ) coefs[evaluation_id] = "coef. %s" % e["coefficient"] if note_sur_20: nmax = 20.0 else: nmax = e["note_max"] if keep_numeric: note_max[evaluation_id] = nmax else: note_max[evaluation_id] = "/ %s" % nmax if nb_notes > 0: moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes) moys["_" + evaluation_id + "_help"] = "moyenne sur %d notes (%s le %s)" % ( nb_notes, e["description"], e["jour"], ) else: moys[evaluation_id] = "" titles[evaluation_id] = "%(description)s (%(jour)s)" % e if e["eval_state"]["evalcomplete"]: titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_complete"' elif e["eval_state"]["evalattente"]: titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_attente"' else: titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_incomplete"' return notes, nb_abs, nb_att # pour histogramme def _add_moymod_column( context, formsemestre_id, e, rows, titles, coefs, note_max, moys, note_sur_20, keep_numeric, ): """Ajoute la colonne moymod à rows""" col_id = "moymod" nt = context._getNotesCache().get_NotesTable( context, formsemestre_id ) # > get_etud_mod_moy nb_notes = 0 sum_notes = 0 notes = [] # liste des notes numeriques, pour calcul histogramme uniquement for row in rows: etudid = row["etudid"] val = nt.get_etud_mod_moy( e["moduleimpl_id"], etudid ) # note sur 20, ou 'NA','NI' row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric) row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' if type(val) != StringType: notes.append(val) nb_notes = nb_notes + 1 sum_notes += val coefs[col_id] = "" if keep_numeric: note_max[col_id] = 20.0 else: note_max[col_id] = "/ 20" titles[col_id] = "Moyenne module" if nb_notes > 0: moys[col_id] = "%.3g" % (sum_notes / nb_notes) moys["_" + col_id + "_help"] = "moyenne des moyennes" else: moys[col_id] = "" # --------------------------------------------------------------------------------- # 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(context, 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 = context.do_evaluation_list({"evaluation_id": evaluation_id})[0] if not E["jour"]: return [], [], [], [], [] # evaluation sans date etudids = sco_groups.do_evaluation_listeetuds_groups( context, evaluation_id, getallstudents=True ) am, pm, demijournee = _eval_demijournee(E) # Liste les absences à ce moment: A = context.Absences.ListeAbsJour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm) As = set([x["etudid"] for x in A]) # ensemble des etudiants absents NJ = context.Absences.ListeAbsNonJustJour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm) NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies Just = context.Absences.ListeAbsJour( 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: NotesDB = context._notes_getall(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 etudids: if NotesDB.has_key(etudid): val = NotesDB[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( context, evaluation_id, with_header=True, show_ok=True, REQUEST=None ): """Affiche etat verification absences d'une evaluation""" E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0] am, pm, demijournee = _eval_demijournee(E) ( ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc, ) = evaluation_check_absences(context, evaluation_id) if with_header: H = [ context.html_sem_header(REQUEST, "Vérification absences à l'évaluation"), sco_evaluations.evaluation_describe( context, evaluation_id=evaluation_id, REQUEST=REQUEST ), """

Vérification de la cohérence entre les notes saisies et les absences signalées.

""", ] else: # pas de header, mais un titre H = [ """

%s du %s """ % (E["description"], E["jour"]) ] if ( not ValButAbs and not AbsNonSignalee and not ExcNonSignalee and not ExcNonJust ): H.append(': ok') H.append("

") def etudlist(etudids, linkabs=False): H.append("
    ") if not etudids and show_ok: H.append("
  • aucun
  • ") for etudid in etudids: etud = context.getEtudInfo(etudid=etudid, filled=True)[0] H.append( '
  • %(nomprenom)s' % etud ) if linkabs: H.append( 'signaler cette absence' % ( etud["etudid"], urllib.quote(E["jour"]), urllib.quote(E["jour"]), demijournee, E["moduleimpl_id"], ) ) H.append("
  • ") H.append("
") if ValButAbs or show_ok: H.append( "

Etudiants ayant une note alors qu'ils sont signalés absents:

" ) etudlist(ValButAbs) if AbsNonSignalee or show_ok: H.append( """

Etudiants avec note "ABS" alors qu'ils ne sont pas signalés absents:

""" ) etudlist(AbsNonSignalee, linkabs=True) if ExcNonSignalee or show_ok: H.append( """

Etudiants avec note "EXC" alors qu'ils ne sont pas signalés absents:

""" ) etudlist(ExcNonSignalee) if ExcNonJust or show_ok: H.append( """

Etudiants avec note "EXC" alors qu'ils sont absents non justifiés:

""" ) etudlist(ExcNonJust) if AbsButExc or show_ok: H.append( """

Etudiants avec note "ABS" alors qu'ils ont une justification:

""" ) etudlist(AbsButExc) if with_header: H.append(context.sco_footer(REQUEST)) return "\n".join(H) def formsemestre_check_absences_html(context, formsemestre_id, REQUEST=None): """Affiche etat verification absences pour toutes les evaluations du semestre !""" sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) H = [ context.html_sem_header( REQUEST, "Vérification absences aux évaluations de ce semestre", sem ), """

Vérification de la cohérence entre les notes saisies et les absences signalées. Sont listés tous les modules avec des évaluations.
Aucune action n'est effectuée: il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.

""", ] # Modules, dans l'ordre Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( context, formsemestre_id=formsemestre_id ) for M in Mlist: evals = context.do_evaluation_list({"moduleimpl_id": M["moduleimpl_id"]}) if evals: H.append( '

%s: %s

' % (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"]) ) for E in evals: H.append( evaluation_check_absences_html( context, E["evaluation_id"], with_header=False, show_ok=False, REQUEST=REQUEST, ) ) if evals: H.append("
") H.append(context.sco_footer(REQUEST)) return "\n".join(H)