1
0
forked from ScoDoc/ScoDoc

Merge branch 'sco96' of https://scodoc.org/git/viennet/ScoDoc into evaluations

This commit is contained in:
Emmanuel Viennet 2023-08-23 16:37:54 +02:00
commit f62d2277a8
19 changed files with 260 additions and 120 deletions

3
.flake8 Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max-line-length = 88
ignore = E203,W503

View File

@ -78,7 +78,11 @@ def compute_sem_moys_apc_using_ects(
else: else:
ects = ects_df.to_numpy() ects = ects_df.to_numpy()
# ects est maintenant un array nb_etuds x nb_ues # ects est maintenant un array nb_etuds x nb_ues
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1) moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
except ZeroDivisionError:
# peut arriver si aucun module... on ignore
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
except TypeError: except TypeError:
if None in ects: if None in ects:
formation = db.session.get(Formation, formation_id) formation = db.session.get(Formation, formation_id)

View File

@ -79,13 +79,15 @@ Adresses d'origine:
to : {orig_to} to : {orig_to}
cc : {orig_cc} cc : {orig_cc}
bcc: {orig_bcc} bcc: {orig_bcc}
--- ---
\n\n""" \n\n"""
+ msg.body + msg.body
) )
current_app.logger.info( current_app.logger.info(
f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients} f"""email sent to{
' (mode test)' if email_test_mode_address else ''
}: {msg.recipients}
from sender {msg.sender} from sender {msg.sender}
""" """
) )
@ -98,7 +100,8 @@ def get_from_addr(dept_acronym: str = None):
"""L'adresse "from" à utiliser pour envoyer un mail """L'adresse "from" à utiliser pour envoyer un mail
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe, Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
prend le `email_from_addr` des préférences de ce département si ce champ est non vide. prend le `email_from_addr` des préférences de ce département si ce champ
est non vide.
Sinon, utilise le paramètre global `email_from_addr`. Sinon, utilise le paramètre global `email_from_addr`.
Sinon, la variable de config `SCODOC_MAIL_FROM`. Sinon, la variable de config `SCODOC_MAIL_FROM`.
""" """

View File

@ -1,19 +1,17 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import html import html
import traceback import traceback
from flask import g, current_app, abort
import psycopg2 import psycopg2
import psycopg2.pool import psycopg2.pool
import psycopg2.extras import psycopg2.extras
from flask import g, current_app, abort
import app
import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
import datetime
quote_html = html.escape quote_html = html.escape

View File

@ -318,7 +318,7 @@ def list_abs_in_range(
Returns: Returns:
List of absences List of absences
""" """
if matin != None: if matin is not None:
matin = _toboolean(matin) matin = _toboolean(matin)
ismatin = " AND A.MATIN = %(matin)s " ismatin = " AND A.MATIN = %(matin)s "
else: else:
@ -387,7 +387,7 @@ def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int:
Returns: Returns:
An integer. An integer.
""" """
if matin != None: if matin is not None:
matin = _toboolean(matin) matin = _toboolean(matin)
ismatin = " AND A.MATIN = %(matin)s " ismatin = " AND A.MATIN = %(matin)s "
else: else:
@ -482,7 +482,9 @@ def _get_abs_description(a, cursor=None):
else: else:
a["matin"] = False a["matin"] = False
cursor.execute( cursor.execute(
"""select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""", """SELECT * FROM absences
WHERE etudid=%(etudid)s AND jour=%(jour)s AND matin=%(matin)s
ORDER BY entry_date desc""",
a, a,
) )
A = cursor.dictfetchall() A = cursor.dictfetchall()
@ -517,9 +519,9 @@ def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None):
req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A
WHERE A.jour = %(date)s WHERE A.jour = %(date)s
""" """
if is_abs != None: if is_abs is not None:
req += " AND A.estabs = %(is_abs)s" req += " AND A.estabs = %(is_abs)s"
if is_just != None: if is_just is not None:
req += " AND A.estjust = %(is_just)s" req += " AND A.estjust = %(is_just)s"
if not am: if not am:
req += " AND NOT matin " req += " AND NOT matin "
@ -883,7 +885,7 @@ def MonthTableBody(
descr = ev[4] descr = ev[4]
# #
cc = [] cc = []
if color != None: if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % color) cc.append('<td bgcolor="%s" class="calcell">' % color)
else: else:
cc.append('<td class="calcell">') cc.append('<td class="calcell">')
@ -896,7 +898,7 @@ def MonthTableBody(
cc.append("<a %s %s>" % (href, descr)) cc.append("<a %s %s>" % (href, descr))
if legend or d == 1: if legend or d == 1:
if pad_width != None: if pad_width is not None:
n = pad_width - len(legend) # pad to 8 cars n = pad_width - len(legend) # pad to 8 cars
if n > 0: if n > 0:
legend = ( legend = (
@ -959,7 +961,7 @@ def MonthTableBody(
ev_year = int(ev[0][:4]) ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7]) ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10]) ev_day = int(ev[0][8:10])
if ev[4] != None: if ev[4] is not None:
ev_half = int(ev[4]) ev_half = int(ev[4])
else: else:
ev_half = 0 ev_half = 0
@ -978,7 +980,7 @@ def MonthTableBody(
if len(ev) > 5 and ev[5]: if len(ev) > 5 and ev[5]:
descr = ev[5] descr = ev[5]
# #
if color != None: if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % (color)) cc.append('<td bgcolor="%s" class="calcell">' % (color))
else: else:
cc.append('<td class="calcell">') cc.append('<td class="calcell">')
@ -1072,7 +1074,8 @@ def invalidate_abs_count_sem(sem):
def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate
"""Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. """Doit etre appelé à chaque modification des absences
pour cet étudiant et cette date.
Invalide cache absence et caches semestre Invalide cache absence et caches semestre
date: date au format ISO date: date au format ISO
""" """

View File

@ -137,14 +137,14 @@ def doSignaleAbsence(
] ]
if dates: if dates:
H.append( H.append(
f"""<p>Ajout de {nbadded} absences <b>{just_str}justifiées</b> f"""<p>Ajout de {nbadded} absences <b>{just_str}justifiées</b>
du {datedebut} au {datefin} {indication_module} du {datedebut} au {datefin} {indication_module}
</p> </p>
""" """
) )
else: else:
H.append( H.append(
f"""<p class="warning">Aucune date ouvrable f"""<p class="warning">Aucune date ouvrable
entre le {datedebut} et le {datefin} ! entre le {datedebut} et le {datefin} !
</p> </p>
""" """
@ -152,11 +152,11 @@ def doSignaleAbsence(
H.append( H.append(
f"""<ul> f"""<ul>
<li><a class="stdlink" href="{url_for("absences.SignaleAbsenceEtud", <li><a class="stdlink" href="{url_for("absences.SignaleAbsenceEtud",
scodoc_dept=g.scodoc_dept, etudid=etud.id scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Autre absence pour <b>{etud.nomprenom}</b></a> )}">Autre absence pour <b>{etud.nomprenom}</b></a>
</li> </li>
<li><a class="stdlink" href="{url_for("absences.CalAbs", <li><a class="stdlink" href="{url_for("absences.CalAbs",
scodoc_dept=g.scodoc_dept, etudid=etud.id scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Calendrier de ses absences</a> )}">Calendrier de ses absences</a>
</li> </li>
@ -180,8 +180,12 @@ def SignaleAbsenceEtud(): # etudid implied
"abs_require_module" "abs_require_module"
) # on utilise la pref globale car pas de sem courant ) # on utilise la pref globale car pas de sem courant
if require_module: if require_module:
menu_module = """<div class="ue_warning">Pas inscrit dans un semestre courant, menu_module = """<div class="ue_warning">Pas
et l'indication du module est requise. Donc pas de saisie d'absence possible !</div>""" inscrit dans un semestre courant,
et l'indication du module est requise.
Donc pas de saisie d'absence possible !
</div>
"""
disabled = True disabled = True
else: else:
menu_module = "" menu_module = ""
@ -197,17 +201,17 @@ def SignaleAbsenceEtud(): # etudid implied
menu_module = """ menu_module = """
<script type="text/javascript"> <script type="text/javascript">
function form_enable_disable() { function form_enable_disable() {
if ( $("select#sel_moduleimpl_id").val() == "" ) { if ( $("select#sel_moduleimpl_id").val() == "" ) {
$("#butsubmit").prop("disabled", true); $("#butsubmit").prop("disabled", true);
} else { } else {
$("#butsubmit").prop("disabled", false); $("#butsubmit").prop("disabled", false);
}; };
} }
$(document).ready(function() { $(document).ready(function() {
form_enable_disable(); form_enable_disable();
}); });
</script> </script>
<p>Module: <p>Module:
<select id="sel_moduleimpl_id" name="moduleimpl_id" <select id="sel_moduleimpl_id" name="moduleimpl_id"
onChange="form_enable_disable();">""" onChange="form_enable_disable();">"""
else: else:
@ -250,7 +254,10 @@ def SignaleAbsenceEtud(): # etudid implied
<p> <p>
<table><tr> <table><tr>
<td>Date début : </td> <td>Date début : </td>
<td><input type="text" name="datedebut" size="10" class="datepicker"/> <em>j/m/a</em></td> <td>
<input type="text" name="datedebut" size="10" class="datepicker"/>
<em>j/m/a</em>
</td>
<td>&nbsp;&nbsp;&nbsp;Date fin (optionnelle):</td> <td>&nbsp;&nbsp;&nbsp;Date fin (optionnelle):</td>
<td><input type="text" name="datefin" size="10" class="datepicker"/> <em>j/m/a</em></td> <td><input type="text" name="datefin" size="10" class="datepicker"/> <em>j/m/a</em></td>
</tr> </tr>
@ -269,14 +276,14 @@ Raison: <input type="text" name="description" size="42"/> (optionnel)
</p> </p>
<p> <p>
<input id="butsubmit" type="submit" value="Envoyer" disable="%(disabled)s"/> <input id="butsubmit" type="submit" value="Envoyer" disable="%(disabled)s"/>
<em> <em>
<p>Seuls les modules du semestre en cours apparaissent.</p> <p>Seuls les modules du semestre en cours apparaissent.</p>
<p>Évitez de saisir une absence pour un module qui n'est pas en place à cette date.</p> <p>Évitez de saisir une absence pour un module qui n'est pas en place à cette date.</p>
<p>Toutes les dates sont au format jour/mois/annee.</p> <p>Toutes les dates sont au format jour/mois/annee.</p>
</em> </em>
</form> </form>
""" """
% { % {
"etudid": etud["etudid"], "etudid": etud["etudid"],
@ -354,7 +361,10 @@ def doJustifAbsence(
) )
H.append( H.append(
"""<ul><li><a href="JustifAbsenceEtud?etudid=%(etudid)s">Autre justification pour <b>%(nomprenom)s</b></a></li> """<ul>
<li><a href="JustifAbsenceEtud?etudid=%(etudid)s">Autre justification
pour <b>%(nomprenom)s</b>
</a></li>
<li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Signaler une absence</a></li> <li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Signaler une absence</a></li>
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li> <li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
<li><a href="ListeAbsEtud?etudid=%(etudid)s">Liste de ses absences</a></li> <li><a href="ListeAbsEtud?etudid=%(etudid)s">Liste de ses absences</a></li>
@ -389,12 +399,12 @@ def JustifAbsenceEtud(): # etudid implied
), ),
"""</a></td></tr></table>""", """</a></td></tr></table>""",
""" """
<form action="doJustifAbsence" method="get"> <form action="doJustifAbsence" method="get">
<input type="hidden" name="etudid" value="%(etudid)s"> <input type="hidden" name="etudid" value="%(etudid)s">
<p> <p>
<table><tr> <table><tr>
<td>Date d&eacute;but : </td> <td>Date début : </td>
<td> <td>
<input type="text" name="datedebut" size="10" class="datepicker"/> <input type="text" name="datedebut" size="10" class="datepicker"/>
</td> </td>
@ -412,7 +422,7 @@ def JustifAbsenceEtud(): # etudid implied
Raison: <input type="text" name="description" size="42"/> (optionnel) Raison: <input type="text" name="description" size="42"/> (optionnel)
<p> <p>
<input type="submit" value="Envoyer"> <input type="submit" value="Envoyer">
</form> """ </form> """
% etud, % etud,
@ -458,8 +468,10 @@ def doAnnuleAbsence(datedebut, datefin, demijournee, etudid=False): # etudid im
H.append( H.append(
"""<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une """<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une
autre absence pour <b>%(nomprenom)s</b></a></li> autre absence pour <b>%(nomprenom)s</b></a></li>
<li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une absence</a></li> <li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li> absence</a></li>
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses
absences</a></li>
</ul> </ul>
<hr>""" <hr>"""
% etud % etud
@ -480,10 +492,11 @@ def AnnuleAbsenceEtud(): # etudid implied
page_title="Annulation d'une absence pour %(nomprenom)s" % etud, page_title="Annulation d'une absence pour %(nomprenom)s" % etud,
), ),
"""<table><tr><td> """<table><tr><td>
<h2><font color="#FF0000">Annulation</font> d'une absence pour %(nomprenom)s</h2> <h2><font color="#FF0000">Annulation</font> d'une absence
pour %(nomprenom)s</h2>
</td><td> </td><td>
""" """
% etud, # " % etud,
"""<a href="%s">""" """<a href="%s">"""
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), % url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html( sco_photos.etud_photo_html(
@ -491,16 +504,19 @@ def AnnuleAbsenceEtud(): # etudid implied
title="fiche de " + etud["nomprenom"], title="fiche de " + etud["nomprenom"],
), ),
"""</a></td></tr></table>""", """</a></td></tr></table>""",
"""<p>A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que l'étudiant était en fait présent. </p> """<p>A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que
<p><font color="#FF0000">Si plusieurs modules sont affectés, les absences seront toutes effacées. </font></p> l'étudiant était en fait présent.
""" </p>
<p><font color="#FF0000">Si plusieurs modules sont affectés,
les absences seront toutes effacées. </font></p>
"""
% etud, % etud,
"""<table frame="border" border="1"><tr><td> """<table frame="border" border="1"><tr><td>
<form action="doAnnuleAbsence" method="get"> <form action="doAnnuleAbsence" method="get">
<input type="hidden" name="etudid" value="%(etudid)s"> <input type="hidden" name="etudid" value="%(etudid)s">
<p> <p>
<table><tr> <table><tr>
<td>Date d&eacute;but : </td> <td>Date début : </td>
<td> <td>
<input type="text" name="datedebut" size="10" class="datepicker"/> <em>j/m/a</em> <input type="text" name="datedebut" size="10" class="datepicker"/> <em>j/m/a</em>
</td> </td>
@ -511,22 +527,22 @@ def AnnuleAbsenceEtud(): # etudid implied
</tr> </tr>
</table> </table>
<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s) <input type="radio" name="demijournee" value="2" checked>journée(s)
&nbsp;<input type="radio" name="demijournee" value="1">Matin(s) &nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi &nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
<p> <p>
<input type="submit" value="Supprimer les absences"> <input type="submit" value="Supprimer les absences">
</form> </form>
</td></tr> </td></tr>
<tr><td> <tr><td>
<form action="doAnnuleJustif" method="get"> <form action="doAnnuleJustif" method="get">
<input type="hidden" name="etudid" value="%(etudid)s"> <input type="hidden" name="etudid" value="%(etudid)s">
<p> <p>
<table><tr> <table><tr>
<td>Date d&eacute;but : </td> <td>Date début : </td>
<td> <td>
<input type="text" name="datedebut0" size="10" class="datepicker"/> <em>j/m/a</em> <input type="text" name="datedebut0" size="10" class="datepicker"/> <em>j/m/a</em>
</td> </td>
@ -538,15 +554,16 @@ def AnnuleAbsenceEtud(): # etudid implied
</table> </table>
<p> <p>
<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s) <input type="radio" name="demijournee" value="2" checked>journée(s)
&nbsp;<input type="radio" name="demijournee" value="1">Matin(s) &nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi &nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
<p> <p>
<input type="submit" value="Supprimer les justificatifs"> <input type="submit" value="Supprimer les justificatifs">
<i>(utiliser ceci en cas de justificatif erron&eacute; saisi ind&eacute;pendemment d'une absence)</i> <i>(utiliser ceci en cas de justificatif erroné saisi indépendemment
</form> d'une absence)</i>
</form>
</td></tr></table>""" </td></tr></table>"""
% etud, % etud,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
@ -591,8 +608,10 @@ def doAnnuleJustif(datedebut0, datefin0, demijournee): # etudid implied
H.append( H.append(
"""<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une """<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une
autre absence pour <b>%(nomprenom)s</b></a></li> autre absence pour <b>%(nomprenom)s</b></a></li>
<li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une absence</a></li> <li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li> absence</a></li>
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses
absences</a></li>
</ul> </ul>
<hr>""" <hr>"""
% etud % etud
@ -634,8 +653,11 @@ def AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id=None):
# supr les absences non justifiees # supr les absences non justifiees
for date in dates: for date in dates:
cursor.execute( cursor.execute(
"""DELETE FROM absences """DELETE FROM absences
WHERE etudid=%(etudid)s and (not estjust) and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s WHERE etudid=%(etudid)s
AND (not estjust)
AND jour=%(date)s
AND moduleimpl_id=%(moduleimpl_id)s
""", """,
vars(), vars(),
) )
@ -643,8 +665,11 @@ def AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id=None):
# s'assure que les justificatifs ne sont pas "absents" # s'assure que les justificatifs ne sont pas "absents"
for date in dates: for date in dates:
cursor.execute( cursor.execute(
"""UPDATE absences SET estabs=FALSE """UPDATE absences
WHERE etudid=%(etudid)s AND jour=%(date)s AND moduleimpl_id=%(moduleimpl_id)s SET estabs=FALSE
WHERE etudid=%(etudid)s
AND jour=%(date)s
AND moduleimpl_id=%(moduleimpl_id)s
""", """,
vars(), vars(),
) )
@ -724,7 +749,7 @@ def _convert_sco_year(year) -> int:
year = int(year) year = int(year)
if year > 1900 and year < 2999: if year > 1900 and year < 2999:
return year return year
except: except ValueError:
raise ScoValueError("année scolaire invalide") raise ScoValueError("année scolaire invalide")
@ -771,7 +796,8 @@ def CalAbs(etudid, sco_year=None):
"""<b><font color="#EE0000">A : absence NON justifiée</font><br> """<b><font color="#EE0000">A : absence NON justifiée</font><br>
<font color="#F8B7B0">a : absence justifiée</font><br> <font color="#F8B7B0">a : absence justifiée</font><br>
<font color="#8EA2C6">X : justification sans absence</font><br> <font color="#8EA2C6">X : justification sans absence</font><br>
%d absences sur l'année, dont %d justifiées (soit %d non justifiées)</b> <em>(%d justificatifs inutilisés)</em> %d absences sur l'année, dont %d justifiées (soit %d non justifiées)</b>
<em>(%d justificatifs inutilisés)</em>
</p> </p>
""" """
% (nbabs, nbabsjust, nbabs - nbabsjust, len(justifs_noabs)), % (nbabs, nbabsjust, nbabs - nbabsjust, len(justifs_noabs)),
@ -790,7 +816,8 @@ def CalAbs(etudid, sco_year=None):
"""<form method="GET" action="CalAbs" name="f">""", """<form method="GET" action="CalAbs" name="f">""",
"""<input type="hidden" name="etudid" value="%s"/>""" % etudid, """<input type="hidden" name="etudid" value="%s"/>""" % etudid,
"""Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1), """Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1),
"""&nbsp;&nbsp;Changer année: <select name="sco_year" onchange="document.f.submit()">""", """&nbsp;&nbsp;Changer année:
<select name="sco_year" onchange="document.f.submit()">""",
] ]
for y in range(annee_courante, min(annee_courante - 6, annee_scolaire - 6), -1): for y in range(annee_courante, min(annee_courante - 6, annee_scolaire - 6), -1):
H.append("""<option value="%s" """ % y) H.append("""<option value="%s" """ % y)
@ -819,7 +846,8 @@ def ListeAbsEtud(
etudid: etudid:
with_evals: indique les evaluations aux dates d'absences with_evals: indique les evaluations aux dates d'absences
absjust_only: si vrai, renvoie table absences justifiées absjust_only: si vrai, renvoie table absences justifiées
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005" sco_year: année scolaire à utiliser.
Si non spécifier, utilie l'année en cours. e.g. "2005"
""" """
# si absjust_only, table absjust seule (export xls ou pdf) # si absjust_only, table absjust seule (export xls ou pdf)
absjust_only = scu.to_bool(absjust_only) absjust_only = scu.to_bool(absjust_only)
@ -941,10 +969,12 @@ def _tables_abs_etud(
for a in absnonjust + absjust: for a in absnonjust + absjust:
cursor.execute( cursor.execute(
"""SELECT eval.* """SELECT eval.*
FROM notes_evaluation eval, notes_moduleimpl_inscription mi, notes_moduleimpl m FROM notes_evaluation eval,
WHERE eval.jour = %(jour)s notes_moduleimpl_inscription mi,
notes_moduleimpl m
WHERE eval.jour = %(jour)s
and eval.moduleimpl_id = m.id and eval.moduleimpl_id = m.id
and mi.moduleimpl_id = m.id and mi.moduleimpl_id = m.id
and mi.etudid = %(etudid)s and mi.etudid = %(etudid)s
""", """,
{"jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid}, {"jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid},
@ -984,9 +1014,10 @@ def _tables_abs_etud(
)[0] )[0]
if format == "html": if format == "html":
ex.append( ex.append(
f"""<a title="{mod['module']['titre']}" href="{url_for('notes.moduleimpl_status', f"""<a title="{mod['module']['titre']}" href="{
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])} url_for('notes.moduleimpl_status',
">{mod["module"]["code"] or "(module sans code)"}</a>""" scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])
}">{mod["module"]["code"] or "(module sans code)"}</a>"""
) )
else: else:
ex.append(mod["module"]["code"] or "(module sans code)") ex.append(mod["module"]["code"] or "(module sans code)")
@ -1003,7 +1034,7 @@ def _tables_abs_etud(
if format == "html": if format == "html":
ex.append( ex.append(
f"""<a title="{mod['module']['titre']}" f"""<a title="{mod['module']['titre']}"
href="{url_for('notes.moduleimpl_status', href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])} scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"] or '(module sans code)'}</a>""" ">{mod["module"]["code"] or '(module sans code)'}</a>"""
) )

View File

@ -321,9 +321,10 @@ def filter_by_formsemestre(
def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
""" """
Retourne la liste des assiduite_id qui sont justifié par la justification Retourne la liste des assiduite_id qui sont justifié par la justification
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT
et que l'état du justificatif est "valide" comprise dans la plage du justificatif
renvoie des id si obj == False, sinon les Assiduités et que l'état du justificatif est "valide".
Renvoie des id si obj == False, sinon les Assiduités
""" """
if justi.etat != scu.EtatJustificatif.VALIDE: if justi.etat != scu.EtatJustificatif.VALIDE:
@ -427,7 +428,7 @@ def invalidate_assiduites_count(etudid, sem):
"""Invalidate (clear) cached counts""" """Invalidate (clear) cached counts"""
date_debut = sem["date_debut_iso"] date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"] date_fin = sem["date_fin_iso"]
for met in sco_preferences.ASSIDUITES_METRIC_LABEL.values(): for met in scu.AssiduitesMetrics.TAG:
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites" key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
sco_cache.AbsSemEtudCache.delete(key) sco_cache.AbsSemEtudCache.delete(key)
@ -444,9 +445,9 @@ def invalidate_assiduites_count_sem(sem):
def invalidate_assiduites_etud_date(etudid, date: datetime): def invalidate_assiduites_etud_date(etudid, date: datetime):
"""Doit etre appelé à chaque modification des assiduites pour cet étudiant et cette date. """Doit etre appelé à chaque modification des assiduites
pour cet étudiant et cette date.
Invalide cache absence et caches semestre Invalide cache absence et caches semestre
date: date au format ISO
""" """
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy

View File

@ -112,7 +112,7 @@ def formsemestre_bulletinetud_published_dict(
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if not etudid in nt.identdict: if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre") abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:

View File

@ -198,13 +198,6 @@ def _get_pref_default_value_from_config(name, pref_spec):
_INSTALLED_FONTS = ", ".join(sco_pdf.get_available_font_names()) _INSTALLED_FONTS = ", ".join(sco_pdf.get_available_font_names())
ASSIDUITES_METRIC_LABEL = {
# l'ordre est important, c'est celui-du menu. Le defaut en 1er donc.
"1/2 J.": "demi",
"J.": "journee",
"H.": "heure",
}
PREF_CATEGORIES = ( PREF_CATEGORIES = (
# sur page "Paramètres" # sur page "Paramètres"
("general", {"title": ""}), # voir paramètre titlr de TrivialFormulator ("general", {"title": ""}), # voir paramètre titlr de TrivialFormulator
@ -666,8 +659,8 @@ class BasePreferences(object):
{ {
"initvalue": "1/2 J.", "initvalue": "1/2 J.",
"input_type": "menu", "input_type": "menu",
"labels": list(ASSIDUITES_METRIC_LABEL.keys()), "labels": scu.AssiduitesMetrics.LONG,
"allowed_values": list(ASSIDUITES_METRIC_LABEL.keys()), "allowed_values": scu.AssiduitesMetrics.SHORT,
"title": "Métrique de l'assiduité", "title": "Métrique de l'assiduité",
"explanation": "Unité utilisée dans la fiche étudiante, les bilans et les calculs", "explanation": "Unité utilisée dans la fiche étudiante, les bilans et les calculs",
"category": "assi", "category": "assi",

View File

@ -166,6 +166,11 @@ class BiDirectionalEnum(Enum):
"""Vérifie sur un attribut existe dans l'enum""" """Vérifie sur un attribut existe dans l'enum"""
return attr.upper() in cls._member_names_ return attr.upper() in cls._member_names_
@classmethod
def all(cls, keys=True):
"""Retourne toutes les clés de l'enum"""
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
@classmethod @classmethod
def get(cls, attr: str, default: any = None): def get(cls, attr: str, default: any = None):
"""Récupère une valeur à partir de son attribut""" """Récupère une valeur à partir de son attribut"""
@ -254,15 +259,54 @@ def is_period_overlapping(
return p_deb < i_fin and p_fin > i_deb return p_deb < i_fin and p_fin > i_deb
def translate_assiduites_metric(hr_metric) -> str: class AssiduitesMetrics:
if hr_metric == "1/2 J.": """Labels associés au métrique de l'assiduité"""
return "demi"
if hr_metric == "J.": SHORT: list[str] = ["1/2 J.", "J.", "H."]
return "journee" LONG: list[str] = ["Demi-journée", "Journée", "Heure"]
if hr_metric == "N.": TAG: list[str] = ["demi", "journee", "heure"]
return "compte"
if hr_metric == "H.":
return "heure" def translate_assiduites_metric(metric, inverse=True, short=True) -> str:
"""
translate_assiduites_metric
SHORT[true] : "J." "H." "N." "1/2 J."
SHORT[false] : "Journée" "Heure" "Nombre" "Demi-Journée"
inverse[false] : "demi" -> "1/2 J."
inverse[true] : "1/2 J." -> "demi"
Args:
metric (str): la métrique à traduire
inverse (bool, optional). Defaults to True.
short (bool, optional). Defaults to True.
Returns:
str: la métrique traduite
"""
index: int = None
if not inverse:
try:
index = AssiduitesMetrics.TAG.index(metric)
return (
AssiduitesMetrics.SHORT[index]
if short
else AssiduitesMetrics.LONG[index]
)
except ValueError:
return None
try:
index = (
AssiduitesMetrics.SHORT.index(metric)
if short
else AssiduitesMetrics.LONG.index(metric)
)
return AssiduitesMetrics.TAG[index]
except ValueError:
return None
# Types de modules # Types de modules

View File

@ -125,8 +125,8 @@ class RowAssi(tb.Row):
"absent": ["Absences", 0.0, 0.0], "absent": ["Absences", 0.0, 0.0],
} }
assi_metric = sco_preferences.ASSIDUITES_METRIC_LABEL.get( assi_metric = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id) sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
) )
for etat, valeur in retour.items(): for etat, valeur in retour.items():

View File

@ -336,19 +336,21 @@
} }
const defAnnee = {{ annee }} const defAnnee = {{ annee }}
let annees = {{ annees | safe }}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
const etudid = {{ sco.etud.id }}; const etudid = {{ sco.etud.id }};
const nonwork = [{{ nonworkdays | safe }}]; const nonwork = [{{ nonworkdays | safe }}];
window.onload = () => { window.onload = () => {
const select = document.querySelector('#annee'); const select = document.querySelector('#annee');
for (let i = defAnnee + 1; i > defAnnee - 6; i--) { annees.forEach((a) => {
const opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = i + "", opt.value = a + "",
opt.textContent = i + ""; opt.textContent = `${a} - ${a + 1}`;
if (i === defAnnee) { if (a === defAnnee) {
opt.selected = true; opt.selected = true;
} }
select.appendChild(opt) select.appendChild(opt)
} })
setterAnnee(defAnnee) setterAnnee(defAnnee)
}; };

View File

@ -162,7 +162,11 @@
userIdDiv.textContent = `saisi le ${formatDateModal( userIdDiv.textContent = `saisi le ${formatDateModal(
assiduite.entry_date, assiduite.entry_date,
"à" "à"
)} \npar ${assiduite.user_id}`; )}`;
if (assiduite.user_id != null) {
userIdDiv.textContent += `\npar ${assiduite.user_id}`
}
bubble.appendChild(userIdDiv); bubble.appendChild(userIdDiv);
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`; bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;

View File

@ -327,8 +327,8 @@ def bilan_etud():
date_debut: str = f"{scu.annee_scolaire()}-09-01" date_debut: str = f"{scu.annee_scolaire()}-09-01"
date_fin: str = f"{scu.annee_scolaire()+1}-06-30" date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
assi_metric = sco_preferences.ASSIDUITES_METRIC_LABEL.get( assi_metric = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id) sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
) )
return HTMLBuilder( return HTMLBuilder(
@ -417,6 +417,16 @@ def calendrier_etud():
], ],
) )
annees: list[int] = sorted(
[ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions],
reverse=True,
)
annees_str: str = "["
for ann in annees:
annees_str += f"{ann},"
annees_str += "]"
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
@ -425,6 +435,7 @@ def calendrier_etud():
annee=scu.annee_scolaire(), annee=scu.annee_scolaire(),
nonworkdays=_non_work_days(), nonworkdays=_non_work_days(),
minitimeline=_mini_timeline(), minitimeline=_mini_timeline(),
annees=annees_str,
), ),
).build() ).build()
@ -840,8 +851,14 @@ def visu_assi_group():
return render_template( return render_template(
"assiduites/pages/visu_assi.j2", "assiduites/pages/visu_assi.j2",
assi_metric=sco_preferences.ASSIDUITES_METRIC_LABEL.get( assi_metric=scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id) scu.translate_assiduites_metric(
sco_preferences.get_preference(
"assi_metrique", dept_id=g.scodoc_dept_id
),
),
inverse=False,
short=False,
), ),
date_debut=dates["debut"], date_debut=dates["debut"],
date_fin=dates["fin"], date_fin=dates["fin"],

View File

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

View File

@ -661,7 +661,12 @@ def profile(host, port, length, profile_dir):
@click.option( @click.option(
"-n", "-n",
"--noon", "--noon",
help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`", help="Spécifie l'heure de fin du matin format `hh:mm`",
)
@click.option(
"-a",
"--afternoon",
help="Spécifie l'heure de début de l'après-midi format `hh:mm` valeur identique à --noon si non spécifié",
) )
@click.option( @click.option(
"-e", "-e",
@ -670,10 +675,14 @@ def profile(host, port, length, profile_dir):
) )
@with_appcontext @with_appcontext
def migrate_abs_to_assiduites( def migrate_abs_to_assiduites(
dept: str = None, morning: str = None, noon: str = None, evening: str = None dept: str = None,
morning: str = None,
noon: str = None,
afternoon: str = None,
evening: str = None,
): # migrate-abs-to-assiduites ): # migrate-abs-to-assiduites
"""Permet de migrer les absences vers le nouveau module d'assiduités""" """Permet de migrer les absences vers le nouveau module d'assiduités"""
tools.migrate_abs_to_assiduites(dept, morning, noon, evening) tools.migrate_abs_to_assiduites(dept, morning, noon, afternoon, evening)
# import cProfile # import cProfile
# cProfile.runctx( # cProfile.runctx(
# f"tools.migrate_abs_to_assiduites({dept})", # f"tools.migrate_abs_to_assiduites({dept})",

View File

@ -4,6 +4,22 @@
# Prend la version dans le code source local et cherche une release gitea de même tag. # Prend la version dans le code source local et cherche une release gitea de même tag.
# Lance ensuite les tests unitaires locaux. # Lance ensuite les tests unitaires locaux.
SKIP_TESTS=0
while getopts "s" opt; do
case "$opt" in
s)
SKIP_TESTS=1
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
# Le répertoire de ce script: .../scodoc/tools # Le répertoire de ce script: .../scodoc/tools
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
@ -57,13 +73,17 @@ SCODOC_USER=scodoc
[ -z "$FACTORY_DIR" ] && die "empty FACTORY_DIR" [ -z "$FACTORY_DIR" ] && die "empty FACTORY_DIR"
[ "$(id -nu)" != "$SCODOC_USER" ] && die "Erreur: le script $0 doit être lancé par l'utilisateur $SCODOC_USER" [ "$(id -nu)" != "$SCODOC_USER" ] && die "Erreur: le script $0 doit être lancé par l'utilisateur $SCODOC_USER"
# Tests unitaires lancés dans le répertoire de travail if [ "$SKIP_TESTS" = 1 ]
echo "TESTS UNITAIRES" then
(cd "$UNIT_TESTS_DIR"; pytest tests/unit) || terminate "Erreur dans tests unitaires" echo "SKIPPING UNIT TESTS !"
else
# Tests API # Tests unitaires lancés dans le répertoire de travail
(cd "$UNIT_TESTS_DIR"; tools/test_api.sh) || terminate "Erreur dans tests unitaires API" echo "TESTS UNITAIRES"
(cd "$UNIT_TESTS_DIR"; pytest tests/unit) || terminate "Erreur dans tests unitaires"
# Tests API
(cd "$UNIT_TESTS_DIR"; tools/test_api.sh) || terminate "Erreur dans tests unitaires API"
fi
# Création répertoire du paquet, et de opt # Création répertoire du paquet, et de opt
slash="$FACTORY_DIR"/"$DEST_DIR" slash="$FACTORY_DIR"/"$DEST_DIR"

View File

@ -45,7 +45,7 @@ then
PSQL=/usr/lib/postgresql/15/bin/psql PSQL=/usr/lib/postgresql/15/bin/psql
#export POSTGRES_SERVICE="postgresql@11-main.service" #export POSTGRES_SERVICE="postgresql@11-main.service"
else else
die "unsupported Debian version" die "unsupported Debian version (${debian_version}, expected 12)"
fi fi
export PSQL export PSQL

View File

@ -47,6 +47,7 @@ class _glob:
MORNING: time = None MORNING: time = None
NOON: time = None NOON: time = None
AFTERNOON: time = None
EVENING: time = None EVENING: time = None
@ -93,7 +94,7 @@ class _Merger:
time_ = _glob.NOON if end else _glob.MORNING time_ = _glob.NOON if end else _glob.MORNING
date_ = datetime.combine(couple[0], time_) date_ = datetime.combine(couple[0], time_)
else: else:
time_ = _glob.EVENING if end else _glob.NOON time_ = _glob.EVENING if end else _glob.AFTERNOON
date_ = datetime.combine(couple[0], time_) date_ = datetime.combine(couple[0], time_)
d = localize_datetime(date_) d = localize_datetime(date_)
return d return d
@ -229,6 +230,7 @@ def migrate_abs_to_assiduites(
dept: str = None, dept: str = None,
morning: str = None, morning: str = None,
noon: str = None, noon: str = None,
afternoon: str = None,
evening: str = None, evening: str = None,
debug: bool = False, debug: bool = False,
): ):
@ -266,6 +268,12 @@ def migrate_abs_to_assiduites(
noon: list[str] = str(noon).split(":") noon: list[str] = str(noon).split(":")
_glob.NOON = time(int(noon[0]), int(noon[1])) _glob.NOON = time(int(noon[0]), int(noon[1]))
if afternoon is None:
afternoon = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0))
afternoon: list[str] = str(afternoon).split(":")
_glob.AFTERNOON = time(int(afternoon[0]), int(afternoon[1]))
if evening is None: if evening is None:
evening = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0)) evening = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0))