2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
2023-01-02 13:16:27 +01:00
|
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""Evaluations
|
|
|
|
"""
|
2023-03-08 22:23:55 +01:00
|
|
|
import collections
|
2021-06-21 11:22:55 +02:00
|
|
|
import datetime
|
|
|
|
import operator
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-07-29 10:19:00 +02:00
|
|
|
from flask import url_for
|
|
|
|
from flask import g
|
|
|
|
from flask_login import current_user
|
2021-08-27 18:17:45 +02:00
|
|
|
from flask import request
|
2021-07-29 10:19:00 +02:00
|
|
|
|
2023-08-29 19:30:56 +02:00
|
|
|
from app import db
|
|
|
|
from app.auth.models import User
|
2022-02-09 23:22:00 +01:00
|
|
|
from app.comp import res_sem
|
2022-03-27 22:25:00 +02:00
|
|
|
from app.comp.res_compat import NotesTableCompat
|
2023-08-29 19:30:56 +02:00
|
|
|
from app.models import Evaluation, FormSemestre
|
2022-02-09 23:22:00 +01:00
|
|
|
|
2021-06-19 23:21:37 +02:00
|
|
|
import app.scodoc.sco_utils as scu
|
2021-11-12 22:17:46 +01:00
|
|
|
from app.scodoc.sco_utils import ModuleType
|
2021-06-19 23:21:37 +02:00
|
|
|
import app.scodoc.notesdb as ndb
|
|
|
|
from app.scodoc.gen_tables import GenTable
|
|
|
|
from app.scodoc import html_sco_header
|
2023-08-27 21:49:50 +02:00
|
|
|
from app.scodoc import sco_cal
|
2021-11-12 22:17:46 +01:00
|
|
|
from app.scodoc import sco_evaluation_db
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc import sco_edit_module
|
|
|
|
from app.scodoc import sco_edit_ue
|
2021-06-21 10:17:16 +02:00
|
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc import sco_groups
|
|
|
|
from app.scodoc import sco_moduleimpl
|
|
|
|
from app.scodoc import sco_permissions_check
|
|
|
|
from app.scodoc import sco_preferences
|
2021-07-03 23:35:32 +02:00
|
|
|
from app.scodoc import sco_users
|
2022-04-12 17:12:51 +02:00
|
|
|
import sco_version
|
2021-06-19 23:21:37 +02:00
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
# MISC AUXILIARY FUNCTIONS
|
|
|
|
#
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
def notes_moyenne_median_mini_maxi(notes):
|
|
|
|
"calcule moyenne et mediane d'une liste de valeurs (floats)"
|
|
|
|
notes = [
|
|
|
|
x
|
|
|
|
for x in notes
|
2020-12-23 23:49:11 +01:00
|
|
|
if (x != None) and (x != scu.NOTES_NEUTRALISE) and (x != scu.NOTES_ATTENTE)
|
2020-09-26 16:19:37 +02:00
|
|
|
]
|
|
|
|
n = len(notes)
|
|
|
|
if not n:
|
|
|
|
return None, None, None, None
|
|
|
|
moy = sum(notes) / n
|
|
|
|
median = ListMedian(notes)
|
|
|
|
mini = min(notes)
|
|
|
|
maxi = max(notes)
|
|
|
|
return moy, median, mini, maxi
|
|
|
|
|
|
|
|
|
|
|
|
def ListMedian(L):
|
|
|
|
"""Median of a list L"""
|
|
|
|
n = len(L)
|
|
|
|
if not n:
|
|
|
|
raise ValueError("empty list")
|
|
|
|
L.sort()
|
|
|
|
if n % 2:
|
2021-07-09 19:50:40 +02:00
|
|
|
return L[n // 2]
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2021-07-09 19:50:40 +02:00
|
|
|
return (L[n // 2] + L[n // 2 - 1]) / 2
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
2023-05-12 12:50:26 +02:00
|
|
|
def do_evaluation_etat(
|
|
|
|
evaluation_id: int, partition_id: int = None, select_first_partition=False
|
|
|
|
) -> dict:
|
|
|
|
"""Donne infos sur l'état de l'évaluation.
|
|
|
|
Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
|
2023-08-25 17:58:57 +02:00
|
|
|
XXX utilisée par de très nombreuses fonctions, dont
|
|
|
|
- _eval_etat<do_evaluation_etat_in_sem (en cours de remplacement)
|
|
|
|
|
|
|
|
- _eval_etat<do_evaluation_etat_in_mod<formsemestre_tableau_modules
|
|
|
|
qui a seulement besoin de
|
|
|
|
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, attente
|
|
|
|
|
|
|
|
renvoie:
|
2023-05-12 12:50:26 +02:00
|
|
|
{
|
|
|
|
nb_inscrits : inscrits au module
|
|
|
|
nb_notes
|
|
|
|
nb_abs,
|
|
|
|
nb_neutre,
|
|
|
|
nb_att,
|
|
|
|
moy, median, mini, maxi : # notes, en chaine, sur 20
|
|
|
|
last_modif: datetime,
|
|
|
|
gr_complets, gr_incomplets,
|
|
|
|
evalcomplete
|
|
|
|
}
|
2020-09-26 16:19:37 +02:00
|
|
|
evalcomplete est vrai si l'eval est complete (tous les inscrits
|
|
|
|
à ce module ont des notes)
|
|
|
|
evalattente est vrai s'il ne manque que des notes en attente
|
|
|
|
"""
|
|
|
|
nb_inscrits = len(
|
2021-07-29 10:19:00 +02:00
|
|
|
sco_groups.do_evaluation_listeetuds_groups(evaluation_id, getallstudents=True)
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-12-17 23:50:34 +01:00
|
|
|
etuds_notes_dict = sco_evaluation_db.do_evaluation_get_all_notes(
|
2021-11-12 22:17:46 +01:00
|
|
|
evaluation_id
|
2021-12-17 23:50:34 +01:00
|
|
|
) # { etudid : note }
|
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
# ---- Liste des groupes complets et incomplets
|
2023-08-25 17:58:57 +02:00
|
|
|
E = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id})[0]
|
2021-10-15 14:00:51 +02:00
|
|
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
2021-10-16 19:20:36 +02:00
|
|
|
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
2021-11-12 22:17:46 +01:00
|
|
|
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
|
2020-09-26 16:19:37 +02:00
|
|
|
formsemestre_id = M["formsemestre_id"]
|
|
|
|
# Si partition_id is None, prend 'all' ou bien la premiere:
|
|
|
|
if partition_id is None:
|
|
|
|
if select_first_partition:
|
2021-08-19 10:28:35 +02:00
|
|
|
partitions = sco_groups.get_partitions_list(formsemestre_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
partition = partitions[0]
|
|
|
|
else:
|
2021-08-19 10:28:35 +02:00
|
|
|
partition = sco_groups.get_default_partition(formsemestre_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
partition_id = partition["partition_id"]
|
|
|
|
|
|
|
|
# Il faut considerer les inscriptions au semestre
|
|
|
|
# (pour avoir l'etat et le groupe) et aussi les inscriptions
|
|
|
|
# au module (pour gerer les modules optionnels correctement)
|
2021-06-21 10:17:16 +02:00
|
|
|
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
2021-08-19 10:28:35 +02:00
|
|
|
formsemestre_id
|
2021-06-21 10:17:16 +02:00
|
|
|
)
|
2021-01-17 22:31:28 +01:00
|
|
|
insmod = sco_moduleimpl.do_moduleimpl_inscription_list(
|
2021-08-20 01:09:55 +02:00
|
|
|
moduleimpl_id=E["moduleimpl_id"]
|
2021-01-17 22:31:28 +01:00
|
|
|
)
|
2023-05-12 12:50:26 +02:00
|
|
|
insmodset = {x["etudid"] for x in insmod}
|
2020-09-26 16:19:37 +02:00
|
|
|
# retire de insem ceux qui ne sont pas inscrits au module
|
|
|
|
ins = [i for i in insem if i["etudid"] in insmodset]
|
|
|
|
|
|
|
|
# Nombre de notes valides d'étudiants inscrits au module
|
|
|
|
# (car il peut y avoir des notes d'étudiants désinscrits depuis l'évaluation)
|
2021-12-17 23:50:34 +01:00
|
|
|
etudids_avec_note = insmodset.intersection(etuds_notes_dict)
|
|
|
|
nb_notes = len(etudids_avec_note)
|
|
|
|
# toutes saisies, y compris chez des non-inscrits:
|
|
|
|
nb_notes_total = len(etuds_notes_dict)
|
|
|
|
|
|
|
|
notes = [etuds_notes_dict[etudid]["value"] for etudid in etudids_avec_note]
|
|
|
|
nb_abs = len([x for x in notes if x is None])
|
|
|
|
nb_neutre = len([x for x in notes if x == scu.NOTES_NEUTRALISE])
|
|
|
|
nb_att = len([x for x in notes if x == scu.NOTES_ATTENTE])
|
|
|
|
moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
|
|
|
|
if moy_num is None:
|
|
|
|
median, moy = "", ""
|
|
|
|
mini, maxi = "", ""
|
2023-05-12 12:50:26 +02:00
|
|
|
maxi_num = None
|
2021-12-17 23:50:34 +01:00
|
|
|
else:
|
|
|
|
median = scu.fmt_note(median_num)
|
2023-05-12 12:50:26 +02:00
|
|
|
moy = scu.fmt_note(moy_num, E["note_max"])
|
|
|
|
mini = scu.fmt_note(mini_num, E["note_max"])
|
|
|
|
maxi = scu.fmt_note(maxi_num, E["note_max"])
|
2021-12-17 23:50:34 +01:00
|
|
|
# cherche date derniere modif note
|
|
|
|
if len(etuds_notes_dict):
|
|
|
|
t = [x["date"] for x in etuds_notes_dict.values()]
|
|
|
|
last_modif = max(t)
|
|
|
|
else:
|
|
|
|
last_modif = None
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
# On considere une note "manquante" lorsqu'elle n'existe pas
|
|
|
|
# ou qu'elle est en attente (ATT)
|
2023-03-08 22:23:55 +01:00
|
|
|
GrNbMissing = collections.defaultdict(int) # group_id : nb notes manquantes
|
|
|
|
GrNotes = collections.defaultdict(list) # group_id: liste notes valides
|
2020-09-26 16:19:37 +02:00
|
|
|
TotalNbMissing = 0
|
|
|
|
TotalNbAtt = 0
|
|
|
|
groups = {} # group_id : group
|
2021-08-19 10:28:35 +02:00
|
|
|
etud_groups = sco_groups.get_etud_groups_in_partition(partition_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
for i in ins:
|
|
|
|
group = etud_groups.get(i["etudid"], None)
|
|
|
|
if group and not group["group_id"] in groups:
|
|
|
|
groups[group["group_id"]] = group
|
|
|
|
#
|
|
|
|
isMissing = False
|
2021-12-17 23:50:34 +01:00
|
|
|
if i["etudid"] in etuds_notes_dict:
|
|
|
|
val = etuds_notes_dict[i["etudid"]]["value"]
|
2020-12-23 23:49:11 +01:00
|
|
|
if val == scu.NOTES_ATTENTE:
|
2020-09-26 16:19:37 +02:00
|
|
|
isMissing = True
|
|
|
|
TotalNbAtt += 1
|
|
|
|
if group:
|
|
|
|
GrNotes[group["group_id"]].append(val)
|
|
|
|
else:
|
|
|
|
if group:
|
2020-12-23 23:49:11 +01:00
|
|
|
_ = GrNotes[group["group_id"]] # create group
|
2020-09-26 16:19:37 +02:00
|
|
|
isMissing = True
|
|
|
|
if isMissing:
|
|
|
|
TotalNbMissing += 1
|
|
|
|
if group:
|
|
|
|
GrNbMissing[group["group_id"]] += 1
|
|
|
|
|
|
|
|
gr_incomplets = [x for x in GrNbMissing.keys()]
|
|
|
|
gr_incomplets.sort()
|
|
|
|
if (
|
|
|
|
(TotalNbMissing > 0)
|
2020-12-23 23:49:11 +01:00
|
|
|
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
|
2021-03-11 14:49:37 +01:00
|
|
|
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
|
2020-09-26 16:19:37 +02:00
|
|
|
):
|
|
|
|
complete = False
|
|
|
|
else:
|
|
|
|
complete = True
|
2023-07-31 20:37:58 +02:00
|
|
|
|
|
|
|
complete = (
|
|
|
|
(TotalNbMissing == 0)
|
|
|
|
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
|
|
|
|
or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
|
|
|
|
)
|
|
|
|
evalattente = (TotalNbMissing > 0) and (
|
|
|
|
(TotalNbMissing == TotalNbAtt) or E["publish_incomplete"]
|
|
|
|
)
|
2021-03-02 23:53:43 +01:00
|
|
|
# mais ne met pas en attente les evals immediates sans aucune notes:
|
2021-08-11 00:36:07 +02:00
|
|
|
if E["publish_incomplete"] and nb_notes == 0:
|
2021-03-02 23:53:43 +01:00
|
|
|
evalattente = False
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
# Calcul moyenne dans chaque groupe de TD
|
|
|
|
gr_moyennes = [] # group : {moy,median, nb_notes}
|
2023-05-12 12:50:26 +02:00
|
|
|
for group_id, notes in GrNotes.items():
|
2020-09-26 16:19:37 +02:00
|
|
|
gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
|
|
|
|
gr_moyennes.append(
|
|
|
|
{
|
|
|
|
"group_id": group_id,
|
|
|
|
"group_name": groups[group_id]["group_name"],
|
2023-05-12 12:50:26 +02:00
|
|
|
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
|
|
|
|
"gr_median": scu.fmt_note(gr_median, E["note_max"]),
|
|
|
|
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
|
|
|
|
"gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]),
|
2020-09-26 16:19:37 +02:00
|
|
|
"gr_nb_notes": len(notes),
|
2020-12-23 23:49:11 +01:00
|
|
|
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
|
2020-09-26 16:19:37 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
gr_moyennes.sort(key=operator.itemgetter("group_name"))
|
|
|
|
|
|
|
|
return {
|
|
|
|
"evaluation_id": evaluation_id,
|
|
|
|
"nb_inscrits": nb_inscrits,
|
|
|
|
"nb_notes": nb_notes, # nb notes etudiants inscrits
|
|
|
|
"nb_notes_total": nb_notes_total, # nb de notes (incluant desinscrits)
|
|
|
|
"nb_abs": nb_abs,
|
|
|
|
"nb_neutre": nb_neutre,
|
|
|
|
"nb_att": nb_att,
|
2023-05-12 12:50:26 +02:00
|
|
|
"moy": moy, # chaine formattée, sur 20
|
2020-09-26 16:19:37 +02:00
|
|
|
"median": median,
|
|
|
|
"mini": mini,
|
|
|
|
"maxi": maxi,
|
2023-05-12 12:50:26 +02:00
|
|
|
"maxi_num": maxi_num, # note maximale, en nombre
|
2020-09-26 16:19:37 +02:00
|
|
|
"last_modif": last_modif,
|
|
|
|
"gr_incomplets": gr_incomplets,
|
|
|
|
"gr_moyennes": gr_moyennes,
|
|
|
|
"groups": groups,
|
|
|
|
"evalcomplete": complete,
|
|
|
|
"evalattente": evalattente,
|
|
|
|
"is_malus": is_malus,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-29 10:19:00 +02:00
|
|
|
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
2023-08-25 17:58:57 +02:00
|
|
|
"""Liste les évaluations de tous les modules de ce semestre.
|
|
|
|
Triée par module, numero desc, date_debut desc
|
2020-12-02 01:00:23 +01:00
|
|
|
Donne pour chaque eval son état (voir do_evaluation_etat)
|
|
|
|
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
|
|
|
|
|
|
|
|
Exemple:
|
|
|
|
[ {
|
|
|
|
'coefficient': 1.0,
|
|
|
|
'description': 'QCM et cas pratiques',
|
2023-05-12 12:50:26 +02:00
|
|
|
'etat': {
|
|
|
|
'evalattente': False,
|
2020-12-02 01:00:23 +01:00
|
|
|
'evalcomplete': True,
|
|
|
|
'evaluation_id': 'GEAEVAL82883',
|
|
|
|
'gr_incomplets': [],
|
2023-05-12 12:50:26 +02:00
|
|
|
'gr_moyennes': [{
|
|
|
|
'gr_median': '12.00', # sur 20
|
|
|
|
'gr_moy': '11.88',
|
|
|
|
'gr_nb_att': 0,
|
|
|
|
'gr_nb_notes': 166,
|
|
|
|
'group_id': 'GEAG266762',
|
|
|
|
'group_name': None
|
|
|
|
}],
|
2020-12-02 01:00:23 +01:00
|
|
|
'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
|
|
|
|
'group_id': 'GEAG266762',
|
|
|
|
'group_name': None,
|
|
|
|
'partition_id': 'GEAP266761'}
|
|
|
|
},
|
|
|
|
'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16),
|
|
|
|
'median': '12.00',
|
|
|
|
'moy': '11.84',
|
|
|
|
'nb_abs': 2,
|
|
|
|
'nb_att': 0,
|
|
|
|
'nb_inscrits': 166,
|
|
|
|
'nb_neutre': 0,
|
|
|
|
'nb_notes': 168,
|
|
|
|
'nb_notes_total': 169
|
|
|
|
},
|
|
|
|
'evaluation_id': 'GEAEVAL82883',
|
|
|
|
'evaluation_type': 0,
|
|
|
|
'heure_debut': datetime.time(8, 0),
|
|
|
|
'heure_fin': datetime.time(9, 30),
|
2023-08-25 17:58:57 +02:00
|
|
|
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
|
2020-12-02 01:00:23 +01:00
|
|
|
'moduleimpl_id': 'GEAMIP80490',
|
|
|
|
'note_max': 20.0,
|
|
|
|
'numero': 0,
|
|
|
|
'publish_incomplete': 0,
|
|
|
|
'visibulletin': 1} ]
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
"""
|
2023-08-25 17:58:57 +02:00
|
|
|
req = """SELECT E.id AS evaluation_id, E.*
|
2021-08-10 12:57:38 +02:00
|
|
|
FROM notes_evaluation E, notes_moduleimpl MI
|
|
|
|
WHERE MI.formsemestre_id = %(formsemestre_id)s
|
|
|
|
and MI.id = E.moduleimpl_id
|
2023-08-25 17:58:57 +02:00
|
|
|
ORDER BY MI.id, numero desc, date_debut desc
|
2021-08-10 12:57:38 +02:00
|
|
|
"""
|
2021-06-15 13:59:56 +02:00
|
|
|
cnx = ndb.GetDBConnexion()
|
2021-06-19 23:21:37 +02:00
|
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
2020-09-26 16:19:37 +02:00
|
|
|
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
|
|
|
res = cursor.dictfetchall()
|
|
|
|
# etat de chaque evaluation:
|
|
|
|
for r in res:
|
2021-07-21 14:58:49 +02:00
|
|
|
if with_etat:
|
2021-07-29 10:19:00 +02:00
|
|
|
r["etat"] = do_evaluation_etat(r["evaluation_id"])
|
2023-08-25 17:58:57 +02:00
|
|
|
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
def _eval_etat(evals):
|
|
|
|
"""evals: list of mappings (etats)
|
|
|
|
-> nb_eval_completes, nb_evals_en_cours,
|
|
|
|
nb_evals_vides, date derniere modif
|
|
|
|
|
|
|
|
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
|
|
|
|
|
|
|
"""
|
|
|
|
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
|
|
|
|
dates = []
|
|
|
|
for e in evals:
|
|
|
|
if e["etat"]["evalcomplete"]:
|
|
|
|
nb_evals_completes += 1
|
|
|
|
elif e["etat"]["nb_notes"] == 0:
|
|
|
|
nb_evals_vides += 1
|
|
|
|
else:
|
|
|
|
nb_evals_en_cours += 1
|
2021-08-14 18:54:32 +02:00
|
|
|
last_modif = e["etat"]["last_modif"]
|
|
|
|
if last_modif is not None:
|
|
|
|
dates.append(e["etat"]["last_modif"])
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-05-12 12:50:26 +02:00
|
|
|
if dates:
|
2021-08-14 18:54:32 +02:00
|
|
|
dates = scu.sort_dates(dates)
|
2020-09-26 16:19:37 +02:00
|
|
|
last_modif = dates[-1] # date de derniere modif d'une note dans un module
|
|
|
|
else:
|
|
|
|
last_modif = ""
|
|
|
|
|
|
|
|
return {
|
|
|
|
"nb_evals_completes": nb_evals_completes,
|
|
|
|
"nb_evals_en_cours": nb_evals_en_cours,
|
|
|
|
"nb_evals_vides": nb_evals_vides,
|
|
|
|
"last_modif": last_modif,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-29 10:19:00 +02:00
|
|
|
def do_evaluation_etat_in_sem(formsemestre_id):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
|
2023-08-25 17:58:57 +02:00
|
|
|
date derniere modif, attente
|
|
|
|
|
|
|
|
XXX utilisé par
|
|
|
|
- formsemestre_status_head
|
|
|
|
- gen_formsemestre_recapcomplet_xml
|
|
|
|
- gen_formsemestre_recapcomplet_json
|
|
|
|
|
|
|
|
"nb_evals_completes"
|
|
|
|
"nb_evals_en_cours"
|
|
|
|
"nb_evals_vides"
|
|
|
|
"date_derniere_note"
|
|
|
|
"last_modif"
|
|
|
|
"attente"
|
|
|
|
"""
|
2023-03-20 11:17:38 +01:00
|
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
2022-02-09 23:22:00 +01:00
|
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
|
|
evals = nt.get_evaluations_etats()
|
2020-09-26 16:19:37 +02:00
|
|
|
etat = _eval_etat(evals)
|
|
|
|
# Ajoute information sur notes en attente
|
|
|
|
etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
|
|
|
|
return etat
|
|
|
|
|
|
|
|
|
2021-07-29 10:19:00 +02:00
|
|
|
def do_evaluation_etat_in_mod(nt, moduleimpl_id):
|
2020-12-02 01:00:23 +01:00
|
|
|
""""""
|
2020-09-26 16:19:37 +02:00
|
|
|
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
|
|
|
|
etat = _eval_etat(evals)
|
2022-02-06 16:09:17 +01:00
|
|
|
# Il y a-t-il des notes en attente dans ce module ?
|
|
|
|
etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
|
2020-09-26 16:19:37 +02:00
|
|
|
return etat
|
|
|
|
|
|
|
|
|
2021-09-27 10:20:10 +02:00
|
|
|
def formsemestre_evaluations_cal(formsemestre_id):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
2023-03-20 11:17:38 +01:00
|
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
2022-02-13 23:53:11 +01:00
|
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
evaluations = formsemestre.get_evaluations() # TODO
|
|
|
|
nb_evals = len(evaluations)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
color_incomplete = "#FF6060"
|
|
|
|
color_complete = "#A0FFA0"
|
|
|
|
color_futur = "#70E0FF"
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
year = formsemestre.annee_scolaire()
|
2020-09-26 16:19:37 +02:00
|
|
|
events = {} # (day, halfday) : event
|
2023-08-25 17:58:57 +02:00
|
|
|
for e in evaluations:
|
|
|
|
if e.date_debut is None:
|
|
|
|
continue # éval. sans date
|
|
|
|
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
|
|
|
if e.date_debut == e.date_fin:
|
|
|
|
heure_debut_txt, heure_fin_txt = "?", "?"
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2023-08-25 17:58:57 +02:00
|
|
|
heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
|
|
|
|
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
|
|
|
|
|
|
|
|
description = f"""{
|
|
|
|
e.moduleimpl.module.titre
|
|
|
|
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
|
|
|
|
|
|
|
# Etat (notes completes) de l'évaluation:
|
|
|
|
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
|
|
|
if modimpl_result.evaluations_etat[e.id].is_complete:
|
2020-09-26 16:19:37 +02:00
|
|
|
color = color_complete
|
|
|
|
else:
|
|
|
|
color = color_incomplete
|
2023-08-25 17:58:57 +02:00
|
|
|
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
2020-09-26 16:19:37 +02:00
|
|
|
color = color_futur
|
2023-08-25 17:58:57 +02:00
|
|
|
href = url_for(
|
|
|
|
"notes.moduleimpl_status",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
moduleimpl_id=e.moduleimpl_id,
|
|
|
|
)
|
|
|
|
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
|
|
|
event = events.get(day)
|
|
|
|
if not event:
|
|
|
|
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2023-08-25 17:58:57 +02:00
|
|
|
if event[-1].id != e.moduleimpl.id:
|
2020-09-26 16:19:37 +02:00
|
|
|
# plusieurs evals de modules differents a la meme date
|
2023-08-25 17:58:57 +02:00
|
|
|
event[1] += ", " + txt
|
|
|
|
event[4] += ", " + description
|
|
|
|
if color == color_incomplete:
|
|
|
|
event[2] = color_incomplete
|
|
|
|
if color == color_futur:
|
|
|
|
event[2] = color_futur
|
|
|
|
|
2023-08-27 21:49:50 +02:00
|
|
|
cal_html = sco_cal.YearTable(
|
2021-08-21 00:24:51 +02:00
|
|
|
year, events=list(events.values()), halfday=False, pad_width=None
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
return f"""
|
|
|
|
{
|
2021-06-13 19:12:20 +02:00
|
|
|
html_sco_header.html_sem_header(
|
|
|
|
"Evaluations du semestre",
|
|
|
|
cssstyles=["css/calabs.css"],
|
2023-08-25 17:58:57 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
<div class="cal_evaluations">
|
|
|
|
{ cal_html }
|
|
|
|
</div>
|
|
|
|
<p>soit {nb_evals} évaluations planifiées;
|
|
|
|
</p>
|
|
|
|
<ul>
|
|
|
|
<li>en <span style=
|
|
|
|
"background-color: {color_incomplete}">rouge</span>
|
|
|
|
les évaluations passées auxquelles il manque des notes
|
|
|
|
</li>
|
|
|
|
<li>en <span style=
|
|
|
|
"background-color: {color_complete}">vert</span>
|
|
|
|
les évaluations déjà notées
|
|
|
|
</li>
|
|
|
|
<li>en <span style=
|
|
|
|
"background-color: {color_futur}">bleu</span>
|
|
|
|
les évaluations futures
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
<p><a href="{
|
|
|
|
url_for("notes.formsemestre_evaluations_delai_correction",
|
|
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
|
|
|
)
|
|
|
|
}" class="stdlink">voir les délais de correction</a>
|
|
|
|
</p>
|
|
|
|
{ html_sco_header.sco_footer() }
|
|
|
|
"""
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
def evaluation_date_first_completion(evaluation_id) -> datetime.datetime:
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Première date à laquelle l'évaluation a été complète
|
|
|
|
ou None si actuellement incomplète
|
|
|
|
"""
|
2021-07-29 10:19:00 +02:00
|
|
|
etat = do_evaluation_etat(evaluation_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
if not etat["evalcomplete"]:
|
|
|
|
return None
|
|
|
|
|
2020-12-23 23:49:11 +01:00
|
|
|
# XXX inachevé ou à revoir ?
|
2020-09-26 16:19:37 +02:00
|
|
|
# Il faut considerer les inscriptions au semestre
|
|
|
|
# (pour avoir l'etat et le groupe) et aussi les inscriptions
|
|
|
|
# au module (pour gerer les modules optionnels correctement)
|
2023-08-25 17:58:57 +02:00
|
|
|
# E = get_evaluation_dict({"id":evaluation_id})[0]
|
2021-10-15 14:00:51 +02:00
|
|
|
# M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
2020-12-23 23:49:11 +01:00
|
|
|
# formsemestre_id = M["formsemestre_id"]
|
2021-08-19 10:28:35 +02:00
|
|
|
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
|
2021-08-20 01:09:55 +02:00
|
|
|
# insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"])
|
2020-12-23 23:49:11 +01:00
|
|
|
# insmodset = set([x["etudid"] for x in insmod])
|
2020-09-26 16:19:37 +02:00
|
|
|
# retire de insem ceux qui ne sont pas inscrits au module
|
2020-12-23 23:49:11 +01:00
|
|
|
# ins = [i for i in insem if i["etudid"] in insmodset]
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-07-09 17:47:06 +02:00
|
|
|
notes = list(
|
2021-11-12 22:17:46 +01:00
|
|
|
sco_evaluation_db.do_evaluation_get_all_notes(
|
|
|
|
evaluation_id, filter_suppressed=False
|
|
|
|
).values()
|
2021-07-09 17:47:06 +02:00
|
|
|
)
|
|
|
|
notes_log = list(
|
2021-11-12 22:17:46 +01:00
|
|
|
sco_evaluation_db.do_evaluation_get_all_notes(
|
2021-07-29 10:19:00 +02:00
|
|
|
evaluation_id, filter_suppressed=False, table="notes_notes_log"
|
2021-07-09 17:47:06 +02:00
|
|
|
).values()
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
date_premiere_note = {} # etudid : date
|
|
|
|
for note in notes + notes_log:
|
|
|
|
etudid = note["etudid"]
|
|
|
|
if etudid in date_premiere_note:
|
|
|
|
date_premiere_note[etudid] = min(note["date"], date_premiere_note[etudid])
|
|
|
|
else:
|
|
|
|
date_premiere_note[etudid] = note["date"]
|
|
|
|
|
|
|
|
if not date_premiere_note:
|
|
|
|
return None # complete mais aucun etudiant non démissionnaires
|
|
|
|
# complet au moment du max (date la plus tardive) des premieres dates de saisie
|
|
|
|
return max(date_premiere_note.values())
|
|
|
|
|
|
|
|
|
2021-09-27 10:20:10 +02:00
|
|
|
def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Experimental: un tableau indiquant pour chaque évaluation
|
|
|
|
le nombre de jours avant la publication des notes.
|
|
|
|
|
2022-02-18 22:59:05 +01:00
|
|
|
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2023-03-20 11:17:38 +01:00
|
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
2023-08-25 17:58:57 +02:00
|
|
|
evaluations = formsemestre.get_evaluations()
|
|
|
|
rows = []
|
|
|
|
for e in evaluations:
|
|
|
|
if (e.evaluation_type != scu.EVALUATION_NORMALE) or (
|
|
|
|
e.moduleimpl.module.module_type == ModuleType.MALUS
|
2020-09-26 16:19:37 +02:00
|
|
|
):
|
|
|
|
continue
|
2023-08-25 17:58:57 +02:00
|
|
|
date_first_complete = evaluation_date_first_completion(e.id)
|
|
|
|
if date_first_complete and e.date_fin:
|
|
|
|
delai_correction = (date_first_complete.date() - e.date_fin).days
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2023-08-25 17:58:57 +02:00
|
|
|
delai_correction = None
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
rows.append(
|
|
|
|
{
|
|
|
|
"date_first_complete": date_first_complete,
|
|
|
|
"delai_correction": delai_correction,
|
|
|
|
"jour": e.date_debut.strftime("%d/%m/%Y")
|
|
|
|
if e.date_debut
|
|
|
|
else "sans date",
|
|
|
|
"_jour_target": url_for(
|
|
|
|
"notes.evaluation_listenotes",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
evaluation_id=e["evaluation_id"],
|
|
|
|
),
|
|
|
|
"module_code": e.moduleimpl.module.code,
|
|
|
|
"_module_code_target": url_for(
|
|
|
|
"notes.moduleimpl_status",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
moduleimpl_id=e.moduleimpl.id,
|
|
|
|
),
|
|
|
|
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
|
|
|
|
"responsable_id": e.moduleimpl.responsable_id,
|
|
|
|
"responsable_nomplogin": sco_users.user_info(
|
|
|
|
e.moduleimpl.responsable_id
|
|
|
|
)["nomplogin"],
|
|
|
|
}
|
2021-08-15 22:08:38 +02:00
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
columns_ids = (
|
|
|
|
"module_code",
|
|
|
|
"module_titre",
|
|
|
|
"responsable_nomplogin",
|
|
|
|
"jour",
|
|
|
|
"date_first_complete",
|
|
|
|
"delai_correction",
|
|
|
|
"description",
|
|
|
|
)
|
|
|
|
titles = {
|
|
|
|
"module_code": "Code",
|
|
|
|
"module_titre": "Module",
|
|
|
|
"responsable_nomplogin": "Responsable",
|
|
|
|
"jour": "Date",
|
|
|
|
"date_first_complete": "Fin saisie",
|
|
|
|
"delai_correction": "Délai",
|
|
|
|
"description": "Description",
|
|
|
|
}
|
|
|
|
tab = GenTable(
|
|
|
|
titles=titles,
|
|
|
|
columns_ids=columns_ids,
|
2023-08-25 17:58:57 +02:00
|
|
|
rows=rows,
|
2020-09-26 16:19:37 +02:00
|
|
|
html_class="table_leftalign table_coldate",
|
|
|
|
html_sortable=True,
|
|
|
|
html_title="<h2>Correction des évaluations du semestre</h2>",
|
|
|
|
caption="Correction des évaluations du semestre",
|
2021-07-28 17:03:54 +02:00
|
|
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
2021-09-18 10:10:02 +02:00
|
|
|
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
2023-08-25 17:58:57 +02:00
|
|
|
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
2022-02-18 22:59:05 +01:00
|
|
|
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-09-16 00:15:10 +02:00
|
|
|
return tab.make_page(format=format)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
# -------------- VIEWS
|
2022-06-02 11:37:13 +02:00
|
|
|
def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""HTML description of evaluation, for page headers
|
|
|
|
edit_in_place: allow in-place editing when permitted (not implemented)
|
|
|
|
"""
|
2023-08-29 19:30:56 +02:00
|
|
|
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
|
|
|
modimpl = evaluation.moduleimpl
|
|
|
|
responsable: User = db.session.get(User, modimpl.responsable_id)
|
|
|
|
resp_nomprenom = responsable.get_prenomnom()
|
|
|
|
resp_nomcomplet = responsable.get_nomcomplet()
|
|
|
|
can_edit = modimpl.can_edit_notes(current_user, allow_ens=False)
|
|
|
|
|
|
|
|
mod_descr = f"""<a href="{url_for("notes.moduleimpl_status",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
moduleimpl_id=modimpl.id,
|
|
|
|
)}">{modimpl.module.code or ""} {modimpl.module.abbrev or modimpl.module.titre or "?"}</a>
|
|
|
|
<span class="resp">(resp. <a title="{resp_nomcomplet}">{resp_nomprenom}</a>)</span>
|
|
|
|
<span class="evallink"><a class="stdlink"
|
|
|
|
href="{url_for(
|
|
|
|
"notes.evaluation_listenotes",
|
|
|
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
|
|
|
}">voir toutes les notes du module</a></span>
|
|
|
|
"""
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-08-29 19:30:56 +02:00
|
|
|
eval_titre = f' "{evaluation.description}"' if evaluation.description else ""
|
|
|
|
if modimpl.module.module_type == ModuleType.MALUS:
|
|
|
|
eval_titre += ' <span class="eval_malus">(points de malus)</span>'
|
2020-09-26 16:19:37 +02:00
|
|
|
H = [
|
2023-08-29 19:30:56 +02:00
|
|
|
f"""<span class="eval_title">Évaluation{eval_titre}</span>
|
|
|
|
<p><b>Module : {mod_descr}</b>
|
|
|
|
</p>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
]
|
2023-08-29 19:30:56 +02:00
|
|
|
if modimpl.module.module_type == ModuleType.MALUS:
|
2020-09-26 16:19:37 +02:00
|
|
|
# Indique l'UE
|
2023-08-29 19:30:56 +02:00
|
|
|
ue = modimpl.module.ue
|
|
|
|
H.append(f"<p><b>UE : {ue.acronyme}</b></p>")
|
2020-09-26 16:19:37 +02:00
|
|
|
# store min/max values used by JS client-side checks:
|
|
|
|
H.append(
|
2023-08-29 19:30:56 +02:00
|
|
|
"""<span id="eval_note_min" class="sco-hidden">-20.</span>
|
|
|
|
<span id="eval_note_max" class="sco-hidden">20.</span>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
# date et absences (pas pour evals de malus)
|
2023-08-29 19:30:56 +02:00
|
|
|
if evaluation.date_debut is not None:
|
|
|
|
H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
|
|
|
|
group_id = sco_groups.get_default_group(modimpl.formsemestre_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
H.append(
|
2023-08-29 19:30:56 +02:00
|
|
|
f"""<span class="noprint"><a class="stdlink" href="{url_for(
|
|
|
|
'assiduites.get_etat_abs_date',
|
2021-11-06 16:35:21 +01:00
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
group_ids=group_id,
|
2023-08-29 19:30:56 +02:00
|
|
|
desc=evaluation.description or "",
|
|
|
|
date_debut=evaluation.date_debut.isoformat(),
|
|
|
|
date_fin=evaluation.date_fin.isoformat(),
|
2021-11-06 16:35:21 +01:00
|
|
|
)
|
|
|
|
}">(absences ce jour)</a></span>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-12-06 14:04:03 +01:00
|
|
|
else:
|
2023-08-29 19:30:56 +02:00
|
|
|
H.append("<p><em>sans date</em> ")
|
2021-12-12 12:31:51 +01:00
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
H.append(
|
2023-08-29 19:30:56 +02:00
|
|
|
f"""</p><p>Coefficient dans le module: <b>{evaluation.coefficient or "0"}</b>,
|
|
|
|
notes sur <span id="eval_note_max">{(evaluation.note_max or 0):g}</span> """
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
|
|
|
|
if can_edit:
|
|
|
|
H.append(
|
2021-12-12 12:31:51 +01:00
|
|
|
f"""
|
|
|
|
<a class="stdlink" href="{url_for(
|
|
|
|
"notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
|
|
|
}">modifier l'évaluation</a>
|
2022-06-02 11:37:13 +02:00
|
|
|
"""
|
|
|
|
)
|
|
|
|
if link_saisie:
|
|
|
|
H.append(
|
|
|
|
f"""
|
2023-08-29 19:30:56 +02:00
|
|
|
<a style="margin-left: 12px;" class="stdlink" href="{url_for(
|
2021-12-12 12:31:51 +01:00
|
|
|
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
|
|
|
}">saisie des notes</a>
|
|
|
|
"""
|
2022-06-02 11:37:13 +02:00
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
H.append("</p>")
|
|
|
|
|
|
|
|
return '<div class="eval_description">' + "\n".join(H) + "</div>"
|