Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into ScoDoc8

Update to rel. 1997.
This commit is contained in:
Emmanuel Viennet 2021-04-25 21:44:40 +02:00
commit e61a9752d3
46 changed files with 685 additions and 338 deletions

View File

@ -8,13 +8,14 @@ SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2021</h4> <h4>Année 2021</h4>
<ul> <ul>
<li>Évaluations de type "deuxième session"</li>
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li> <li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
<li>Diverses corrections (PV de jurys, ...)</li> <li>Diverses corrections (PV de jurys, ...)</li>
<li>Modernisation du code Python</li> <li>Modernisation du code Python</li>
</ul> </ul>
<h4>Année 2020</h4> <h4>Année 2020</h4>
<ul> <ul>
<li>Corrections d'erreurs, améliorations saise absences< et affichage bulletins</li> <li>Corrections d'erreurs, améliorations saisie absences et affichage bulletins</li>
<li>Nouveau site <a href="https://scodoc.org">scodoc.org</a> pour la documentation</li> <li>Nouveau site <a href="https://scodoc.org">scodoc.org</a> pour la documentation</li>
<li>Enregistrement de semestres extérieurs</li> <li>Enregistrement de semestres extérieurs</li>
<li>Améliorations PV de Jury</li> <li>Améliorations PV de Jury</li>

View File

@ -67,6 +67,7 @@ from sco_permissions import ScoAbsAddBillet, ScoAbsChange, ScoView
from sco_exceptions import ScoValueError, ScoInvalidDateError from sco_exceptions import ScoValueError, ScoInvalidDateError
from TrivialFormulator import TrivialFormulator, TF from TrivialFormulator import TrivialFormulator, TF
from gen_tables import GenTable from gen_tables import GenTable
import html_sco_header
import scolars import scolars
import sco_formsemestre import sco_formsemestre
import sco_moduleimpl import sco_moduleimpl
@ -78,6 +79,8 @@ import sco_compute_moy
import sco_abs import sco_abs
from sco_abs import ddmmyyyy from sco_abs import ddmmyyyy
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
def _toboolean(x): def _toboolean(x):
"convert a value to boolean (ensure backward compat with OLD intranet code)" "convert a value to boolean (ensure backward compat with OLD intranet code)"
@ -343,11 +346,17 @@ class ZAbsences(
) )
cnx.commit() cnx.commit()
security.declareProtected(ScoView, "CountAbs") def ListAbsInRange(
self, etudid, debut, fin, matin=None, moduleimpl_id=None, cursor=None
):
"""Liste des absences entre deux dates.
def CountAbs(self, etudid, debut, fin, matin=None, moduleimpl_id=None): Args:
"""CountAbs etudid
matin= 1 ou 0. debut string iso date ("2020-03-12")
end string iso date ("2020-03-12")
matin None, True, False
moduleimpl_id
""" """
if matin != None: if matin != None:
matin = _toboolean(matin) matin = _toboolean(matin)
@ -358,10 +367,11 @@ class ZAbsences(
modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s "
else: else:
modul = "" modul = ""
if not cursor:
cnx = self.GetDBConnexion() cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor)
cursor.execute( cursor.execute(
"""SELECT COUNT(*) AS NbAbs FROM ( """
SELECT DISTINCT A.JOUR, A.MATIN SELECT DISTINCT A.JOUR, A.MATIN
FROM ABSENCES A FROM ABSENCES A
WHERE A.ETUDID = %(etudid)s WHERE A.ETUDID = %(etudid)s
@ -370,13 +380,27 @@ class ZAbsences(
+ modul + modul
+ """ + """
AND A.JOUR BETWEEN %(debut)s AND %(fin)s AND A.JOUR BETWEEN %(debut)s AND %(fin)s
) AS tmp
""", """,
vars(), vars(),
) )
res = cursor.fetchone()[0] res = cursor.dictfetchall()
return res return res
security.declareProtected(ScoView, "CountAbs")
def CountAbs(self, etudid, debut, fin, matin=None, moduleimpl_id=None):
"""CountAbs
matin= 1 ou 0.
Returns:
An integer.
"""
return len(
self.ListAbsInRange(
etudid, debut, fin, matin=matin, moduleimpl_id=moduleimpl_id
)
)
security.declareProtected(ScoView, "CountAbsJust") security.declareProtected(ScoView, "CountAbsJust")
def CountAbsJust(self, etudid, debut, fin, matin=None, moduleimpl_id=None): def CountAbsJust(self, etudid, debut, fin, matin=None, moduleimpl_id=None):
@ -718,7 +742,12 @@ class ZAbsences(
) )
] ]
) )
etuds = [e for e in etuds if e["etudid"] in mod_inscrits] etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
if etuds_inscrits_module:
etuds = etuds_inscrits_module
else:
# Si aucun etudiant n'est inscrit au module choisi...
moduleimpl_id = None
nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id) nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
sem = sco_formsemestre.do_formsemestre_list( sem = sco_formsemestre.do_formsemestre_list(
self, {"formsemestre_id": formsemestre_id} self, {"formsemestre_id": formsemestre_id}
@ -745,20 +774,41 @@ class ZAbsences(
self.sco_header( self.sco_header(
page_title="Saisie hebdomadaire des absences", page_title="Saisie hebdomadaire des absences",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js", "js/abs_ajax.js"], javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
"js/etud_info.js",
"js/abs_ajax.js",
"js/groups_view.js",
],
cssstyles=CSSSTYLES,
no_side_bar=1, no_side_bar=1,
REQUEST=REQUEST, REQUEST=REQUEST,
), ),
"""<table border="0" cellspacing="16"><tr><td> """<table border="0" cellspacing="16"><tr><td>
<h2>Saisie des absences %s %s, <h2>Saisie des absences %s %s,
<span class="fontred">semaine du lundi %s</span></h2> <span class="fontred">semaine du lundi %s</span></h2>
<div>
<p><a href="index_html">Annuler</a></p> <form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
<p> <input type="hidden" name="datelundi" id="datelundi" value="%s"/>
<form action="doSignaleAbsenceGrHebdo" method="post" action="%s"> <input type="hidden" name="destination" id="destination" value="%s"/>
<input type="hidden" name="moduleimpl_id" id="moduleimpl_id_o" value="%s"/>
Groupes: %s
</form>
<form id="abs_form">
""" """
% (gr_tit, sem["titre_num"], datelundi, REQUEST.URL0), % (
gr_tit,
sem["titre_num"],
datelundi,
groups_infos.formsemestre_id,
datelundi,
destination,
moduleimpl_id or "",
sco_groups_view.menu_groups_choice(
self, groups_infos, submit_on_change=True
),
),
] ]
# #
modimpls_list = [] modimpls_list = []
@ -797,12 +847,12 @@ class ZAbsences(
sel = "selected" # aucun module specifie sel = "selected" # aucun module specifie
H.append( H.append(
""" """Module concerné:
Module concerné par ces absences (optionnel): <select id="moduleimpl_id" name="moduleimpl_id" onchange="document.location='%(url)s&amp;moduleimpl_id='+document.getElementById('moduleimpl_id').value"> <select id="moduleimpl_id" name="moduleimpl_id" onchange="change_moduleimpl('%(url)s')">
<option value="" %(sel)s>non spécifié</option> <option value="" %(sel)s>non spécifié</option>
%(menu_module)s %(menu_module)s
</select> </select>
</p>""" </div>"""
% {"menu_module": menu_module, "url": base_url, "sel": sel} % {"menu_module": menu_module, "url": base_url, "sel": sel}
) )
@ -826,7 +876,6 @@ class ZAbsences(
REQUEST=None, REQUEST=None,
): ):
"""Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier""" """Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier"""
# log('SignaleAbsenceGrSemestre: moduleimpl_id=%s destination=%s' % (moduleimpl_id, destination))
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
self, group_ids, REQUEST=REQUEST self, group_ids, REQUEST=REQUEST
) )
@ -934,7 +983,7 @@ class ZAbsences(
les <span class="fontred">%s</span></h2> les <span class="fontred">%s</span></h2>
<p> <p>
<a href="%s">%s</a> <a href="%s">%s</a>
<form action="doSignaleAbsenceGrSemestre" method="post"> <form id="abs_form" action="doSignaleAbsenceGrSemestre" method="post">
""" """
% (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg), % (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg),
] ]
@ -1006,7 +1055,7 @@ class ZAbsences(
Args: Args:
etuds: liste des étudiants etuds: liste des étudiants
dates: liste de dates iso, par exemple: [ '2020-12-24', ... ] dates: liste ordonnée de dates iso, par exemple: [ '2020-12-24', ... ]
moduleimpl_id: optionnel, module concerné. moduleimpl_id: optionnel, module concerné.
""" """
H = [ H = [
@ -1043,6 +1092,8 @@ class ZAbsences(
] ]
# Dates # Dates
odates = [datetime.date(*[int(x) for x in d.split("-")]) for d in dates] odates = [datetime.date(*[int(x) for x in d.split("-")]) for d in dates]
begin = dates[0]
end = dates[-1]
# Titres colonnes # Titres colonnes
noms_jours = [] # eg [ "Lundi", "mardi", "Samedi", ... ] noms_jours = [] # eg [ "Lundi", "mardi", "Samedi", ... ]
jn = sco_abs.day_names(self) jn = sco_abs.day_names(self)
@ -1071,6 +1122,8 @@ class ZAbsences(
'<tr><td><span class="redboldtext">Aucun étudiant inscrit !</span></td></tr>' '<tr><td><span class="redboldtext">Aucun étudiant inscrit !</span></td></tr>'
) )
i = 1 i = 1
cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor)
for etud in etuds: for etud in etuds:
i += 1 i += 1
etudid = etud["etudid"] etudid = etud["etudid"]
@ -1096,17 +1149,20 @@ class ZAbsences(
'<tr class="%s"><td><b class="etudinfo" id="%s"><a class="discretelink" href="ficheEtud?etudid=%s" target="new">%s</a></b>%s</td>' '<tr class="%s"><td><b class="etudinfo" id="%s"><a class="discretelink" href="ficheEtud?etudid=%s" target="new">%s</a></b>%s</td>'
% (tr_class, etudid, etudid, etud["nomprenom"], capstr) % (tr_class, etudid, etudid, etud["nomprenom"], capstr)
) )
for date in dates: etud_abs = self.ListAbsInRange(
etudid, begin, end, moduleimpl_id=moduleimpl_id, cursor=cursor
)
for d in odates:
date = d.strftime("%Y-%m-%d")
# matin # matin
if self.CountAbs(etudid, date, date, True, moduleimpl_id=moduleimpl_id): is_abs = {"jour": d, "matin": True} in etud_abs
if is_abs:
checked = "checked" checked = "checked"
else: else:
checked = "" checked = ""
# bulle lors du passage souris # bulle lors du passage souris
coljour = sco_abs.DAYNAMES[ coljour = sco_abs.DAYNAMES[(calendar.weekday(d.year, d.month, d.day))]
(calendar.weekday(int(date[:4]), int(date[5:7]), int(date[8:]))) datecol = coljour + " " + d.strftime("%d/%m/%Y")
]
datecol = coljour + " " + date[8:] + "/" + date[5:7] + "/" + date[:4]
bulle_am = '"' + etud["nomprenom"] + " - " + datecol + ' (matin)"' bulle_am = '"' + etud["nomprenom"] + " - " + datecol + ' (matin)"'
bulle_pm = '"' + etud["nomprenom"] + " - " + datecol + ' (ap.midi)"' bulle_pm = '"' + etud["nomprenom"] + " - " + datecol + ' (ap.midi)"'
@ -1122,9 +1178,8 @@ class ZAbsences(
) )
) )
# après-midi # après-midi
if self.CountAbs( is_abs = {"jour": d, "matin": False} in etud_abs
etudid, date, date, False, moduleimpl_id=moduleimpl_id if is_abs:
):
checked = "checked" checked = "checked"
else: else:
checked = "" checked = ""

View File

@ -394,6 +394,10 @@ REQUEST.URL0=%s<br/>
""" """
return self.ScoURL() + "/Entreprises" return self.ScoURL() + "/Entreprises"
def AbsencesURL(self):
"""URL of Absences"""
return self.ScoURL() + "/Absences"
def UsersURL(self): def UsersURL(self):
"""URL of Users """URL of Users
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users

View File

@ -251,8 +251,8 @@ class exUserFolder(Folder,BasicUserFolder,BasicGroupFolderMixin,
('Manager',)), ('Manager',)),
('View', ('manage_changePassword', ('View', ('manage_changePassword',
'manage_forgotPassword', 'docLogin','docLoginRedirect', 'manage_forgotPassword','docLoginRedirect',
'docLogout', 'logout', 'DialogHeader', 'logout', 'DialogHeader',
'DialogFooter', 'manage_signupUser', 'DialogFooter', 'manage_signupUser',
'MessageDialog', 'redirectToLogin','manage_changeProps'), 'MessageDialog', 'redirectToLogin','manage_changeProps'),
('Anonymous', 'Authenticated', 'Manager')), ('Anonymous', 'Authenticated', 'Manager')),
@ -269,7 +269,7 @@ class exUserFolder(Folder,BasicUserFolder,BasicGroupFolderMixin,
('Access contents information', ('hasProperty', 'propertyIds', ('Access contents information', ('hasProperty', 'propertyIds',
'propertyValues','propertyItems', 'propertyValues','propertyItems',
'getProperty', 'getPropertyType', 'getProperty', 'getPropertyType',
'propertyMap', 'docLogin','docLoginRedirect', 'propertyMap', 'docLoginRedirect',
'DialogHeader', 'DialogFooter', 'DialogHeader', 'DialogFooter',
'MessageDialog', 'redirectToLogin',), 'MessageDialog', 'redirectToLogin',),
('Anonymous', 'Authenticated', 'Manager')), ('Anonymous', 'Authenticated', 'Manager')),

View File

@ -9,7 +9,7 @@
# E. Viennet, Juin 2008 # E. Viennet, Juin 2008
# #
set -euo pipefail set -eo pipefail
source config.sh source config.sh
source utils.sh source utils.sh

View File

@ -60,7 +60,7 @@ then
# suppression de la base postgres # suppression de la base postgres
db_name=$(sed '/^dbname=*/!d; s///;q' < "$cfg_pathname") db_name=$(sed '/^dbname=*/!d; s///;q' < "$cfg_pathname")
if su -c "psql -lt" "$POSTGRES_SUPERUSER" | cut -d \| -f 1 | grep -wq SCORT if su -c "psql -lt" "$POSTGRES_SUPERUSER" | cut -d \| -f 1 | grep -wq "$db_name"
then then
echo "Suppression de la base postgres $db_name ..." echo "Suppression de la base postgres $db_name ..."
su -c "dropdb $db_name" "$POSTGRES_SUPERUSER" || terminate "ne peux supprimer base de donnees $db_name" su -c "dropdb $db_name" "$POSTGRES_SUPERUSER" || terminate "ne peux supprimer base de donnees $db_name"

64
config/fix_bug70_db.py Normal file
View File

@ -0,0 +1,64 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Fix bug #70
Utiliser comme:
scotests/scointeractive.sh DEPT config/fix_bug70_db.py
"""
context = context.Notes # pylint: disable=undefined-variable
REQUEST = REQUEST # pylint: disable=undefined-variable
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error
import os
import sys
import sco_utils
import notesdb
import sco_formsemestre
import sco_formsemestre_edit
import sco_moduleimpl
G = sco_fake_gen.ScoFake(context.Notes)
def fix_formsemestre_formation_bug70(formsemestre_id):
"""Le bug #70 a pu entrainer des incohérences
lors du clonage avorté de semestres.
Cette fonction réassocie le semestre à la formation
à laquelle appartiennent ses modulesimpls.
2021-04-23
"""
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
cursor = notesdb.SimpleQuery(
context,
"""SELECT m.formation_id
FROM notes_modules m, notes_moduleimpl mi
WHERE mi.module_id = m.module_id
AND mi.formsemestre_id = %(formsemestre_id)s
""",
{"formsemestre_id": formsemestre_id},
)
modimpls_formations = set([x[0] for x in cursor])
if len(modimpls_formations) > 1:
# this is should not occur
G.log(
"Warning: fix_formsemestre_formation_bug70: modules from several formations in sem %s"
% formsemestre_id
)
elif len(modimpls_formations) == 1:
modimpls_formation_id = modimpls_formations.pop()
if modimpls_formation_id != sem["formation_id"]:
# Bug #70: fix
G.log("fix_formsemestre_formation_bug70: fixing %s" % formsemestre_id)
sem["formation_id"] = modimpls_formation_id
context.do_formsemestre_edit(sem, html_quote=False)
formsemestre_ids = [
x[0]
for x in notesdb.SimpleQuery(
context, "SELECT formsemestre_id FROM notes_formsemestre", {}
)
]
for formsemestre_id in formsemestre_ids:
fix_formsemestre_formation_bug70(formsemestre_id)

View File

@ -583,27 +583,6 @@ for dept in get_depts():
], ],
) )
# add etape_apo2
check_field(
cnx,
"notes_formsemestre",
"etape_apo2",
["alter table notes_formsemestre add column etape_apo2 text"],
)
# add etape_apo3
check_field(
cnx,
"notes_formsemestre",
"etape_apo3",
["alter table notes_formsemestre add column etape_apo3 text"],
)
# add etape_apo4
check_field(
cnx,
"notes_formsemestre",
"etape_apo4",
["alter table notes_formsemestre add column etape_apo4 text"],
)
# add publish_incomplete # add publish_incomplete
check_field( check_field(
cnx, cnx,

19
config/postupgrade.py Executable file → Normal file
View File

@ -1,4 +1,5 @@
#!/opt/zope213/bin/python #!/opt/zope213/bin/python
# -*- coding: utf-8 -*-
""" """
ScoDoc post-upgrade script. ScoDoc post-upgrade script.
@ -11,15 +12,16 @@ _before_ upgrading the database.
E. Viennet, June 2008 E. Viennet, June 2008
Mar 2017: suppress upgrade of very old Apache configs Mar 2017: suppress upgrade of very old Apache configs
Aug 2020: move photos to .../var/scodoc/ Aug 2020: move photos to .../var/scodoc/
Apr 2021: bug #70
""" """
import os import os
import sys import sys
import glob import glob
import shutil import shutil
from scodocutils import log, SCODOC_DIR, SCODOC_VAR_DIR, SCODOC_LOGOS_DIR from scodocutils import log, SCODOC_DIR, SCODOC_VAR_DIR, SCODOC_LOGOS_DIR, SCO_TMPDIR
if os.getuid() != 0: if os.getuid() != 0:
log('postupgrade.py: must be run as root') log("postupgrade.py: must be run as root")
sys.exit(1) sys.exit(1)
# --- # ---
@ -54,6 +56,19 @@ for d in glob.glob( SCODOC_DIR + "/logos_*" ):
log("Moving %s to %s" % (d, SCODOC_LOGOS_DIR)) log("Moving %s to %s" % (d, SCODOC_LOGOS_DIR))
shutil.move(d, SCODOC_LOGOS_DIR) shutil.move(d, SCODOC_LOGOS_DIR)
# Fix bug #70
depts = [
os.path.splitext(os.path.basename(f))[0] for f in glob.glob(depts_dir + "/*.cfg")
]
for dept in depts:
fixed_filename = SCO_TMPDIR + "/.%s_bug70_fixed" % dept
if not os.path.exists(fixed_filename):
log("fixing #70 on %s" % dept)
os.system("../scotests/scointeractive.sh -x %s config/fix_bug70_db.py" % dept)
# n'essaie qu'une fois, même en cas d'échec
f = open(fixed_filename, "a")
f.close()
# Continue here... # Continue here...
# --- # ---

8
config/scodocutils.py Normal file → Executable file
View File

@ -9,6 +9,12 @@ import sys, os, psycopg2, glob, subprocess, traceback, time
sys.path.append("..") sys.path.append("..")
# INSTANCE_HOME est nécessaire pour sco_utils.py
# note: avec le python 2.7 de Zope2, l'import de pyscopg2 change
# INSTANCE_HOME dans l'environnement !
# Ici on le fixe à la "bonne" valeur pour ScoDoc7.
os.environ["INSTANCE_HOME"] = "/opt/scodoc"
def log(msg): def log(msg):
sys.stdout.flush() sys.stdout.flush()
@ -24,8 +30,10 @@ SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "")
if not SCODOC_VAR_DIR: if not SCODOC_VAR_DIR:
log("Error: environment variable SCODOC_VAR_DIR is not defined") log("Error: environment variable SCODOC_VAR_DIR is not defined")
sys.exit(1) sys.exit(1)
SCO_TMPDIR = os.path.join(SCODOC_VAR_DIR, "tmp")
SCODOC_LOGOS_DIR = os.environ.get("SCODOC_LOGOS_DIR", "") SCODOC_LOGOS_DIR = os.environ.get("SCODOC_LOGOS_DIR", "")
def get_dept_cnx_str(dept): def get_dept_cnx_str(dept):
"db cnx string for dept" "db cnx string for dept"
f = os.path.join(SCODOC_VAR_DIR, "config", "depts", dept + ".cfg") f = os.path.join(SCODOC_VAR_DIR, "config", "depts", dept + ".cfg")

7
config/upgrade.sh Executable file → Normal file
View File

@ -23,6 +23,7 @@ fi
# Upgrade svn working copy if possible # Upgrade svn working copy if possible
svnver=$(svn --version --quiet) svnver=$(svn --version --quiet)
# shellcheck disable=SC2072
if [[ ${svnver} > "1.7" ]] if [[ ${svnver} > "1.7" ]]
then then
(cd "$SCODOC_DIR"; find . -name .svn -type d -exec dirname {} \; | xargs svn upgrade) (cd "$SCODOC_DIR"; find . -name .svn -type d -exec dirname {} \; | xargs svn upgrade)
@ -63,7 +64,7 @@ CMD="curl --fail --connect-timeout 5 --silent http://scodoc.iutv.univ-paris13.fr
#echo $CMD #echo $CMD
SVERSION="$(${CMD})" SVERSION="$(${CMD})"
if [ $? == 0 ]; then if [ "$?" == 0 ]; then
#echo "answer=${SVERSION}" #echo "answer=${SVERSION}"
echo "${SVERSION}" > "${SCODOC_VERSION_DIR}"/scodoc.sn echo "${SVERSION}" > "${SCODOC_VERSION_DIR}"/scodoc.sn
else else
@ -132,6 +133,10 @@ then
chmod 600 "$LOCAL_CONFIG_FILENAME" chmod 600 "$LOCAL_CONFIG_FILENAME"
fi fi
# upgrade old dateutil (check version manually to speedup)
v=$(/opt/zope213/bin/python -c "import dateutil; print dateutil.__version__")
[[ "$v" < "2.8.1" ]] && /opt/zope213/bin/pip install --upgrade python-dateutil
# Ensure www-data can duplicate databases (for dumps) # Ensure www-data can duplicate databases (for dumps)
su -c $'psql -c \'alter role "www-data" with CREATEDB;\'' "$POSTGRES_SUPERUSER" su -c $'psql -c \'alter role "www-data" with CREATEDB;\'' "$POSTGRES_SUPERUSER"
#' #'

View File

@ -57,17 +57,19 @@ import sco_bulletins_xml
# Prend le premier departement comme context # Prend le premier departement comme context
def go(app, n=0): def go(app, n=0, verbose=True):
context = app.ScoDoc.objectValues("Folder")[n].Scolarite context = app.ScoDoc.objectValues("Folder")[n].Scolarite
if verbose:
print("context in dept ", context.DeptId()) print("context in dept ", context.DeptId())
return context return context
def go_dept(app, dept): def go_dept(app, dept, verbose=True):
objs = app.ScoDoc.objectValues("Folder") objs = app.ScoDoc.objectValues("Folder")
for o in objs: for o in objs:
context = o.Scolarite context = o.Scolarite
if context.DeptId() == dept: if context.DeptId() == dept:
if verbose:
print("context in dept ", context.DeptId()) print("context in dept ", context.DeptId())
return context return context
raise ValueError("dep %s not found" % dept) raise ValueError("dep %s not found" % dept)

View File

@ -123,6 +123,7 @@ class GenTable:
pdf_col_widths=None, pdf_col_widths=None,
xml_outer_tag="table", xml_outer_tag="table",
xml_row_tag="row", xml_row_tag="row",
text_with_titles=False, # CSV with header line
text_fields_separator="\t", text_fields_separator="\t",
preferences=None, preferences=None,
): ):
@ -173,6 +174,7 @@ class GenTable:
self.xml_row_tag = xml_row_tag self.xml_row_tag = xml_row_tag
# TEXT parameters # TEXT parameters
self.text_fields_separator = text_fields_separator self.text_fields_separator = text_fields_separator
self.text_with_titles = text_with_titles
# #
if preferences: if preferences:
self.preferences = preferences self.preferences = preferences
@ -265,8 +267,7 @@ class GenTable:
def get_titles_list(self): def get_titles_list(self):
"list of titles" "list of titles"
l = [] return [self.titles.get(cid, "") for cid in self.columns_ids]
return l + [self.titles.get(cid, "") for cid in self.columns_ids]
def gen(self, format="html", columns_ids=None): def gen(self, format="html", columns_ids=None):
"""Build representation of the table in the specified format. """Build representation of the table in the specified format.
@ -479,10 +480,14 @@ class GenTable:
def text(self): def text(self):
"raw text representation of the table" "raw text representation of the table"
if self.text_with_titles:
headline = [self.get_titles_list()]
else:
headline = []
return "\n".join( return "\n".join(
[ [
self.text_fields_separator.join([x for x in line]) self.text_fields_separator.join([x for x in line])
for line in self.get_data_list() for line in headline + self.get_data_list()
] ]
) )
@ -534,14 +539,6 @@ class GenTable:
) )
] ]
pdf_style_list += self.pdf_table_style pdf_style_list += self.pdf_table_style
# log('len(Pt)=%s' % len(Pt))
# log( 'line lens=%s' % [ len(x) for x in Pt ] )
# log( 'style=\n%s' % pdf_style_list)
# col_min = min([x[1][0] for x in pdf_style_list])
# col_max = max([x[2][0] for x in pdf_style_list])
# lin_min = min([x[1][1] for x in pdf_style_list])
# lin_max = max([x[2][1] for x in pdf_style_list])
# log('col_min=%s col_max=%s lin_min=%s lin_max=%s' % (col_min, col_max, lin_min, lin_max))
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list) T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
objects = [] objects = []

View File

@ -42,7 +42,13 @@ Génération de la "sidebar" (marge gauche des pages HTML)
def sidebar_common(context, REQUEST=None): def sidebar_common(context, REQUEST=None):
"partie commune a toutes les sidebar" "partie commune a toutes les sidebar"
authuser = REQUEST.AUTHENTICATED_USER authuser = REQUEST.AUTHENTICATED_USER
params = {"ScoURL": context.ScoURL(), "authuser": str(authuser)} params = {
"ScoURL": context.ScoURL(),
"UsersURL": context.UsersURL(),
"NotesURL": context.NotesURL(),
"AbsencesURL": context.AbsencesURL(),
"authuser": str(authuser),
}
H = [ H = [
'<a class="scodoc_title" href="about">ScoDoc</a>', '<a class="scodoc_title" href="about">ScoDoc</a>',
'<div id="authuser"><a id="authuserlink" href="%(ScoURL)s/Users/userinfo">%(authuser)s</a><br/><a id="deconnectlink" href="%(ScoURL)s/acl_users/logout">déconnexion</a></div>' '<div id="authuser"><a id="authuserlink" href="%(ScoURL)s/Users/userinfo">%(authuser)s</a><br/><a id="deconnectlink" href="%(ScoURL)s/acl_users/logout">déconnexion</a></div>'
@ -50,8 +56,8 @@ def sidebar_common(context, REQUEST=None):
context.sidebar_dept(REQUEST), context.sidebar_dept(REQUEST),
"""<h2 class="insidebar">Scolarit&eacute;</h2> """<h2 class="insidebar">Scolarit&eacute;</h2>
<a href="%(ScoURL)s" class="sidebar">Semestres</a> <br/> <a href="%(ScoURL)s" class="sidebar">Semestres</a> <br/>
<a href="%(ScoURL)s/Notes" class="sidebar">Programmes</a> <br/> <a href="%(NotesURL)s" class="sidebar">Programmes</a> <br/>
<a href="%(ScoURL)s/Absences" class="sidebar">Absences</a> <br/> <a href="%(AbsencesURL)s" class="sidebar">Absences</a> <br/>
""" """
% params, % params,
] ]
@ -60,14 +66,7 @@ def sidebar_common(context, REQUEST=None):
ScoUsersView, context ScoUsersView, context
): ):
H.append( H.append(
"""<a href="%(ScoURL)s/Users" class="sidebar">Utilisateurs</a> <br/>""" """<a href="%(UsersURL)s" class="sidebar">Utilisateurs</a> <br/>""" % params
% params
)
if 0: # XXX experimental
H.append(
"""<a href="%(ScoURL)s/Notes/Services" class="sidebar">Services</a> <br/>"""
% params
) )
if authuser.has_permission(ScoChangePreferences, context): if authuser.has_permission(ScoChangePreferences, context):
@ -88,8 +87,8 @@ def sidebar(context, REQUEST=None):
H.append( H.append(
"""<div class="box-chercheetud">Chercher étudiant:<br/> """<div class="box-chercheetud">Chercher étudiant:<br/>
<form id="form-chercheetud" action="%(ScoURL)s/search_etud_in_dept"> <form method="get" id="form-chercheetud" action="%(ScoURL)s/search_etud_in_dept">
<div><input type="text" size="12" id="in-expnom" name="expnom"></input></div> <div><input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false"></input></div>
</form></div> </form></div>
<div class="etud-insidebar"> <div class="etud-insidebar">
""" """

View File

@ -99,8 +99,7 @@ function get_semestre_info($sem, $dept) {
// Renvoi les informations détaillées d'un semestre // Renvoi les informations détaillées d'un semestre
// Ne nécessite pas d'authentification avec sco_user et sco_pw - Il est possible de choisir le format XML ou JSON. // Ne nécessite pas d'authentification avec sco_user et sco_pw - Il est possible de choisir le format XML ou JSON.
// formsemestre_list // formsemestre_list
// Paramètres (tous optionnels): formsesmestre_id, formation_id, etape_apo, etape_apo2 // Paramètres (tous optionnels): formsemestre_id, formation_id, etape_apo
// Coquille dans la doc : formsesmestre_id
// Résultat: liste des semestres correspondant. // Résultat: liste des semestres correspondant.
// Exemple: formsemestre_list?format=xml&etape_apo=V1RT // Exemple: formsemestre_list?format=xml&etape_apo=V1RT
global $sco_pw; global $sco_pw;

View File

@ -35,6 +35,18 @@ CREATE FUNCTION notes_newid_etud( text ) returns text as '
as result; as result;
' language SQL; ' language SQL;
-- Fonction pour anonymisation:
-- inspirée par https://www.simononsoftware.com/random-string-in-postgresql/
CREATE FUNCTION random_text_md5( integer ) returns text
LANGUAGE SQL
AS $$
select upper( substring( (SELECT string_agg(md5(random()::TEXT), '')
FROM generate_series(
1,
CEIL($1 / 32.)::integer)
), 1, $1) );
$$;
-- Preferences -- Preferences
CREATE TABLE sco_prefs ( CREATE TABLE sco_prefs (
pref_id text DEFAULT notes_newid('PREF'::text) UNIQUE NOT NULL, pref_id text DEFAULT notes_newid('PREF'::text) UNIQUE NOT NULL,

View File

@ -1132,6 +1132,9 @@ class NotesTable:
def sem_has_decisions(self): def sem_has_decisions(self):
"""True si au moins une decision de jury dans ce semestre""" """True si au moins une decision de jury dans ce semestre"""
if [x for x in self.decisions_jury_ues.values() if x]:
return True
return len([x for x in self.decisions_jury_ues.values() if x]) > 0 return len([x for x in self.decisions_jury_ues.values() if x]) > 0
def etud_has_decision(self, etudid): def etud_has_decision(self, etudid):

View File

@ -813,6 +813,7 @@ class JuryPE:
"nom": etudinfo["nom"], "nom": etudinfo["nom"],
"prenom": etudinfo["prenom"], "prenom": etudinfo["prenom"],
"civilite": etudinfo["civilite"], "civilite": etudinfo["civilite"],
"civilite_str": etudinfo["civilite_str"],
"age": str(pe_tools.calcul_age(etudinfo["date_naissance"])), "age": str(pe_tools.calcul_age(etudinfo["date_naissance"])),
"lycee": etudinfo["nomlycee"] "lycee": etudinfo["nomlycee"]
+ ( + (

View File

@ -325,7 +325,7 @@ def do_formsemestre_archive(
if data: if data:
PVArchive.store(archive_id, "Decisions_Jury.xls", data) PVArchive.store(archive_id, "Decisions_Jury.xls", data)
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
context, formsemestre_id, REQUEST, version=bulVersion context, formsemestre_id, REQUEST, version=bulVersion
) )
if data: if data:
@ -548,7 +548,7 @@ def formsemestre_delete_archive(
raise AccessDenied( raise AccessDenied(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER) "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
) )
sem = sco_formsemestre.get_formsemestre( _ = sco_formsemestre.get_formsemestre(
context, formsemestre_id context, formsemestre_id
) # check formsemestre_id ) # check formsemestre_id
archive_id = PVArchive.get_id_from_name(context, formsemestre_id, archive_name) archive_id = PVArchive.get_id_from_name(context, formsemestre_id, archive_name)

View File

@ -546,6 +546,8 @@ def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, vers
e["coef_txt"] = scu.fmt_coef(e["coefficient"]) e["coef_txt"] = scu.fmt_coef(e["coefficient"])
if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE: if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
e["coef_txt"] = "rat." e["coef_txt"] = "rat."
elif e["evaluation_type"] == scu.EVALUATION_SESSION2:
e["coef_txt"] = "sess. 2"
if e["etat"]["evalattente"]: if e["etat"]["evalattente"]:
mod_attente = True # une eval en attente dans ce module mod_attente = True # une eval en attente dans ce module
if (not is_malus) or (val != "NP"): if (not is_malus) or (val != "NP"):

View File

@ -209,6 +209,7 @@ def formsemestre_bulletinetud_published_dict(
value=scu.fmt_note(ue_status["cur_moy_ue"]), value=scu.fmt_note(ue_status["cur_moy_ue"]),
min=scu.fmt_note(ue["min"]), min=scu.fmt_note(ue["min"]),
max=scu.fmt_note(ue["max"]), max=scu.fmt_note(ue["max"]),
moy=scu.fmt_note(ue["moy"]), # CM : ajout pour faire apparaitre la moyenne des UE
), ),
rang=str(nt.ue_rangs[ue["ue_id"]][0][etudid]), rang=str(nt.ue_rangs[ue["ue_id"]][0][etudid]),
effectif=str(nt.ue_rangs[ue["ue_id"]][1]), effectif=str(nt.ue_rangs[ue["ue_id"]][1]),
@ -275,6 +276,7 @@ def formsemestre_bulletinetud_published_dict(
), ),
coefficient=e["coefficient"], coefficient=e["coefficient"],
evaluation_type=e["evaluation_type"], evaluation_type=e["evaluation_type"],
evaluation_id=e["evaluation_id"], # CM : ajout pour permettre de faire le lien sur les bulletins en ligne avec l'évaluation
description=scu.quote_xml_attr(e["description"]), description=scu.quote_xml_attr(e["description"]),
note=val, note=val,
) )

View File

@ -378,7 +378,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
P[-1]["_pdf_style"].append( P[-1]["_pdf_style"].append(
("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR) ("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR)
) )
# Espacement sous la ligne moyenne générale:
P[-1]["_pdf_style"].append(("BOTTOMPADDING", (0, 1), (-1, 1), 8))
# Moyenne générale: # Moyenne générale:
nbabs = I["nbabs"] nbabs = I["nbabs"]
nbabsjust = I["nbabsjust"] nbabsjust = I["nbabsjust"]
@ -392,8 +393,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"abs": "%s / %s" % (nbabs, nbabsjust), "abs": "%s / %s" % (nbabs, nbabsjust),
"_css_row_class": "notes_bulletin_row_gen", "_css_row_class": "notes_bulletin_row_gen",
"_titre_colspan": 2, "_titre_colspan": 2,
"_pdf_row_markup": ['font size="12"', "b"], # bold, size 12 "_pdf_row_markup": ["b"], # bold. On peut ajouter 'font size="12"'
"_pdf_style": [("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR)], "_pdf_style": [
("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR),
],
} }
P.append(t) P.append(t)

View File

@ -38,6 +38,7 @@ from sco_utils import (
NOTES_NEUTRALISE, NOTES_NEUTRALISE,
EVALUATION_NORMALE, EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE, EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
) )
from sco_exceptions import ScoException from sco_exceptions import ScoException
from notesdb import EditableTable, quote_html from notesdb import EditableTable, quote_html
@ -242,7 +243,10 @@ def do_moduleimpl_moyennes(context, nt, mod):
if e["etat"]["evalattente"]: if e["etat"]["evalattente"]:
attente = True attente = True
if e["evaluation_type"] == EVALUATION_RATTRAPAGE: if (
e["evaluation_type"] == EVALUATION_RATTRAPAGE
or e["evaluation_type"] == EVALUATION_SESSION2
):
if eval_rattr: if eval_rattr:
# !!! plusieurs rattrapages ! # !!! plusieurs rattrapages !
diag_info.update( diag_info.update(
@ -344,7 +348,7 @@ def do_moduleimpl_moyennes(context, nt, mod):
if diag_info: if diag_info:
diag_info["moduleimpl_id"] = moduleimpl_id diag_info["moduleimpl_id"] = moduleimpl_id
R[etudid] = user_moy R[etudid] = user_moy
# Note de rattrapage ? # Note de rattrapage ou deuxième session ?
if eval_rattr: if eval_rattr:
if eval_rattr["notes"].has_key(etudid): if eval_rattr["notes"].has_key(etudid):
note = eval_rattr["notes"][etudid]["value"] note = eval_rattr["notes"][etudid]["value"]
@ -353,9 +357,15 @@ def do_moduleimpl_moyennes(context, nt, mod):
R[etudid] = note R[etudid] = note
else: else:
note_sur_20 = note * 20.0 / eval_rattr["note_max"] note_sur_20 = note * 20.0 / eval_rattr["note_max"]
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
# rattrapage classique: prend la meilleure note entre moyenne
# module et note eval rattrapage
if note_sur_20 > R[etudid]: if note_sur_20 > R[etudid]:
# log('note_sur_20=%s' % note_sur_20) # log('note_sur_20=%s' % note_sur_20)
R[etudid] = note_sur_20 R[etudid] = note_sur_20
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
# rattrapage type "deuxième session": remplace la note moyenne
R[etudid] = note_sur_20
return R, valid_evals, attente, diag_info return R, valid_evals, attente, diag_info

View File

@ -129,11 +129,12 @@ def index_html(context, REQUEST=None, showcodes=0, showsemtable=0):
) )
H.append( H.append(
"""<p><form action="Notes/view_formsemestre_by_etape"> """<p><form action="%s/view_formsemestre_by_etape">
Chercher étape courante: <input name="etape_apo" type="text" size="8"></input> Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form </form
</p> </p>
""" """
% context.NotesURL()
) )
# #
authuser = REQUEST.AUTHENTICATED_USER authuser = REQUEST.AUTHENTICATED_USER
@ -155,9 +156,10 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>
"""<hr> """<hr>
<h3>Exports Apogée</h3> <h3>Exports Apogée</h3>
<ul> <ul>
<li><a class="stdlink" href="Notes/semset_page">Années scolaires / exports Apogée</a></li> <li><a class="stdlink" href="%s/semset_page">Années scolaires / exports Apogée</a></li>
</ul> </ul>
""" """
% context.NotesURL()
) )
# #
H.append( H.append(
@ -175,9 +177,9 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>
def _sem_table(context, sems): def _sem_table(context, sems):
"""Affiche liste des semestres, utilisée pour semestres en cours""" """Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = """<tr class="%(trclass)s">%(tmpcode)s tmpl = """<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td> <td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s</td><td class="datesem"><a title="%(session_id)s">-</a> %(mois_fin)s</td> <td class="datesem">%(mois_debut)s</td><td class="datesem"><a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td><a class="stdlink" href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> <td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span> <span class="respsem">(%(responsable_name)s)</span>
</td> </td>
</tr> </tr>
@ -199,6 +201,7 @@ def _sem_table(context, sems):
cur_idx = sem["semestre_id"] cur_idx = sem["semestre_id"]
else: else:
sem["trclass"] = "" sem["trclass"] = ""
sem["notes_url"] = context.NotesURL()
H.append(tmpl % sem) H.append(tmpl % sem)
H.append("</table>") H.append("</table>")
return "\n".join(H) return "\n".join(H)
@ -245,14 +248,16 @@ def _sem_table_gt(context, sems, showcodes=False):
def _style_sems(context, sems): def _style_sems(context, sems):
"""ajoute quelques attributs de présentation pour la table""" """ajoute quelques attributs de présentation pour la table"""
for sem in sems: for sem in sems:
sem["notes_url"] = context.NotesURL()
sem["_groupicon_target"] = ( sem["_groupicon_target"] = (
"Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % sem "%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
% sem
) )
sem["_formsemestre_id_class"] = "blacktt" sem["_formsemestre_id_class"] = "blacktt"
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["_dash_mois_fin_class"] = "datesem" sem["_dash_mois_fin_class"] = "datesem"
sem["titre_resp"] = ( sem["titre_resp"] = (
"""<a class="stdlink" href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> """<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>""" <span class="respsem">(%(responsable_name)s)</span>"""
% sem % sem
) )

View File

@ -108,7 +108,9 @@ def do_evaluation_delete(context, REQUEST, evaluation_id):
# news # news
mod = context.do_module_list(args={"module_id": M["module_id"]})[0] mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = (
context.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
)
sco_news.add( sco_news.add(
context, context,
REQUEST, REQUEST,
@ -133,11 +135,6 @@ def do_evaluation_etat(
à ce module ont des notes) à ce module ont des notes)
evalattente est vrai s'il ne manque que des notes en attente evalattente est vrai s'il ne manque que des notes en attente
""" """
# global _DEE_TOT
# t0=time.time()
# if evaluation_id == 'GEAEVAL82883':
# log('do_evaluation_etat: evaluation_id=%s partition_id=%s sfp=%s' % (evaluation_id, partition_id, select_first_partition))
nb_inscrits = len( nb_inscrits = len(
sco_groups.do_evaluation_listeetuds_groups( sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, getallstudents=True context, evaluation_id, getallstudents=True
@ -232,6 +229,7 @@ def do_evaluation_etat(
if ( if (
(TotalNbMissing > 0) (TotalNbMissing > 0)
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE) and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
and not is_malus and not is_malus
): ):
complete = False complete = False
@ -245,6 +243,9 @@ def do_evaluation_etat(
evalattente = True evalattente = True
else: else:
evalattente = False evalattente = False
# mais ne met pas en attente les evals immediates sans aucune notes:
if E["publish_incomplete"] != "0" and nb_notes == 0:
evalattente = False
# Calcul moyenne dans chaque groupe de TD # Calcul moyenne dans chaque groupe de TD
gr_moyennes = [] # group : {moy,median, nb_notes} gr_moyennes = [] # group : {moy,median, nb_notes}
@ -268,11 +269,6 @@ def do_evaluation_etat(
} }
) )
gr_moyennes.sort(key=operator.itemgetter("group_name")) gr_moyennes.sort(key=operator.itemgetter("group_name"))
# log('gr_moyennes=%s' % gr_moyennes)
# _DEE_TOT += (time.time() - t0)
# log('%s\t_DEE_TOT=%f' % (evaluation_id, _DEE_TOT))
# if evaluation_id == 'GEAEVAL82883':
# logCallStack()
# retourne mapping # retourne mapping
return { return {
@ -896,12 +892,20 @@ def evaluation_create_form(
notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail). les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
</p><p class="help"> </p><p class="help">
La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de
si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont façon spéciale: </p>
<ul>
<li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes du module
<em>si elles sont meilleures que celles calculées</em>.</li>
<li>les notes de "deuxième session" remplacent, lorsqu'elles sont saisies, la moyenne de l'étudiant
à ce module, même si la note de deuxième session est plus faible.</li>
</ul>
<p class="help">
Dans ces deux cas, le coefficient est ignoré, et toutes les notes n'ont
pas besoin d'être rentrées. pas besoin d'être rentrées.
</p> </p>
<p class="help"> <p class="help">
Les évaluations des modules de type "malus" sont spéciales: le coefficient n'est pas utilisé. Par ailleurs, les évaluations des modules de type "malus" sont toujours spéciales: le coefficient n'est pas utilisé.
Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne
de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée). de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
</p> </p>
@ -1024,9 +1028,17 @@ def evaluation_create_form(
{ {
"input_type": "menu", "input_type": "menu",
"title": "Modalité", "title": "Modalité",
"allowed_values": (scu.EVALUATION_NORMALE, scu.EVALUATION_RATTRAPAGE), "allowed_values": (
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"type": "int", "type": "int",
"labels": ("Normale", "Rattrapage"), "labels": (
"Normale",
"Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)",
),
}, },
), ),
] ]

View File

@ -58,7 +58,7 @@ def form_search_etud(
H.append( H.append(
"""<form action="search_etud_in_dept" method="POST"> """<form action="search_etud_in_dept" method="POST">
<b>%s</b> <b>%s</b>
<input type="text" name="expnom" width=12 value=""> <input type="text" name="expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher"> <input type="submit" value="Chercher">
<br/>(entrer une partie du nom) <br/>(entrer une partie du nom)
""" """
@ -96,71 +96,52 @@ def form_search_etud(
return "\n".join(H) return "\n".join(H)
# was chercheEtud() def search_etud_in_dept(context, expnom="", REQUEST=None):
def search_etud_in_dept( """Page recherche d'un etudiant.
context,
expnom=None, Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats, la liste des étudianst
dest_url="ficheEtud", correspondants.
parameters={}, Appelée par boite de recherche barre latérale gauche.
parameters_keys="",
add_headers=True, # complete page Args:
title=None, expnom: string, regexp sur le nom ou un code_nip ou un etudid
REQUEST=None,
):
"""Page recherche d'un etudiant
expnom est un regexp sur le nom ou un code_nip
dest_url est la page sur laquelle on sera redirigé après choix
parameters spécifie des arguments additionnels à passer à l'URL (en plus de etudid)
""" """
if type(expnom) == ListType: dest_url = "ficheEtud"
expnom = expnom[0] if len(expnom) > 1:
q = [] etuds = context.getEtudInfo(filled=1, etudid=expnom, REQUEST=REQUEST)
if parameters: if len(etuds) != 1:
for param in parameters.keys():
q.append("%s=%s" % (param, parameters[param]))
elif parameters_keys:
for key in parameters_keys.split(","):
v = REQUEST.form.get(key, False)
if v:
q.append("%s=%s" % (key, v))
query_string = "&amp;".join(q)
no_side_bar = True
H = []
if title:
H.append("<h2>%s</h2>" % title)
if scu.is_valid_code_nip(expnom): if scu.is_valid_code_nip(expnom):
etuds = search_etuds_infos(context, code_nip=expnom, REQUEST=REQUEST) etuds = search_etuds_infos(context, code_nip=expnom, REQUEST=REQUEST)
elif expnom: else:
etuds = search_etuds_infos(context, expnom=expnom, REQUEST=REQUEST) etuds = search_etuds_infos(context, expnom=expnom, REQUEST=REQUEST)
else: else:
etuds = [] etuds = [] # si expnom est trop court, n'affiche rien
if len(etuds) == 1: if len(etuds) == 1:
# va directement a la destination # va directement a la destination
return REQUEST.RESPONSE.redirect( return context.ficheEtud(etudid=etuds[0]["etudid"], REQUEST=REQUEST)
dest_url + "?etudid=%s&amp;" % etuds[0]["etudid"] + query_string
)
if len(etuds) > 0: H = [
# Choix dans la liste des résultats: context.sco_header(
H.append( page_title="Recherche d'un étudiant",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js"],
REQUEST=REQUEST,
),
"""<h2>%d résultats pour "%s": choisissez un étudiant:</h2>""" """<h2>%d résultats pour "%s": choisissez un étudiant:</h2>"""
% (len(etuds), expnom) % (len(etuds), expnom),
)
H.append(
form_search_etud( form_search_etud(
context, context,
dest_url=dest_url, dest_url=dest_url,
parameters=parameters,
parameters_keys=parameters_keys,
REQUEST=REQUEST, REQUEST=REQUEST,
title="Autre recherche", title="Autre recherche",
) ),
) ]
if len(etuds) > 0:
# Choix dans la liste des résultats:
for e in etuds: for e in etuds:
target = dest_url + "?etudid=%s&amp;" % e["etudid"] + query_string target = dest_url + "?etudid=%s&amp;" % e["etudid"]
e["_nomprenom_target"] = target e["_nomprenom_target"] = target
e["inscription_target"] = target e["inscription_target"] = target
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"]) e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
@ -185,34 +166,16 @@ def search_etud_in_dept(
form_search_etud( form_search_etud(
context, context,
dest_url=dest_url, dest_url=dest_url,
parameters=parameters,
parameters_keys=parameters_keys,
REQUEST=REQUEST, REQUEST=REQUEST,
title="Autre recherche", title="Autre recherche",
) )
) )
else: else:
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom) H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
add_headers = True
no_side_bar = False
H.append( H.append(
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant</p>""" """<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant</p>"""
) )
if add_headers: return "\n".join(H) + context.sco_footer(REQUEST)
return (
context.sco_header(
REQUEST,
page_title="Choix d'un étudiant",
init_qtip=True,
javascripts=["js/etud_info.js"],
no_side_bar=no_side_bar,
)
+ "\n".join(H)
+ context.sco_footer(REQUEST)
)
else:
return "\n".join(H)
# Was chercheEtudsInfo() # Was chercheEtudsInfo()
@ -268,14 +231,14 @@ def search_etud_by_name(context, term, REQUEST=None):
else: else:
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
context, context,
"SELECT nom, prenom FROM identite WHERE nom LIKE %(beginning)s ORDER BY nom", "SELECT etudid, nom, prenom FROM identite WHERE nom LIKE %(beginning)s ORDER BY nom",
{"beginning": term + "%"}, {"beginning": term + "%"},
) )
data = [ data = [
{ {
"label": "%s %s" % (x["nom"], scolars.format_prenom(x["prenom"])), "label": "%s %s" % (x["nom"], scolars.format_prenom(x["prenom"])),
"value": x["nom"], "value": x["etudid"],
} }
for x in r for x in r
] ]
@ -294,7 +257,7 @@ def form_search_etud_in_accessible_depts(context, REQUEST):
return "" return ""
return """<form action="table_etud_in_accessible_depts" method="POST"> return """<form action="table_etud_in_accessible_depts" method="POST">
<b>Chercher étudiant:</b> <b>Chercher étudiant:</b>
<input type="text" name="expnom" width=12 value=""> <input type="text" name="expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher"> <input type="submit" value="Chercher">
<br/>(entrer une partie du nom ou le code NIP, cherche dans tous les départements autorisés) <br/>(entrer une partie du nom ou le code NIP, cherche dans tous les départements autorisés)
""" """

View File

@ -312,8 +312,11 @@ def _write_formsemestre_aux(context, sem, fieldname, valuename):
"""fieldname: 'etapes' ou 'responsables' """fieldname: 'etapes' ou 'responsables'
valuename: 'etape_apo' ou 'responsable_id' valuename: 'etape_apo' ou 'responsable_id'
""" """
if not "etapes" in sem: if not fieldname in sem:
return return
# uniquify
values = set([str(x) for x in sem[fieldname]])
cnx = context.GetDBConnexion(autocommit=False) cnx = context.GetDBConnexion(autocommit=False)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
tablename = "notes_formsemestre_" + fieldname tablename = "notes_formsemestre_" + fieldname
@ -322,7 +325,7 @@ def _write_formsemestre_aux(context, sem, fieldname, valuename):
"DELETE from " + tablename + " where formsemestre_id = %(formsemestre_id)s", "DELETE from " + tablename + " where formsemestre_id = %(formsemestre_id)s",
{"formsemestre_id": sem["formsemestre_id"]}, {"formsemestre_id": sem["formsemestre_id"]},
) )
for item in sem[fieldname]: for item in values:
if item: if item:
cursor.execute( cursor.execute(
"INSERT INTO " "INSERT INTO "
@ -332,7 +335,7 @@ def _write_formsemestre_aux(context, sem, fieldname, valuename):
+ ") VALUES (%(formsemestre_id)s, %(" + ") VALUES (%(formsemestre_id)s, %("
+ valuename + valuename
+ ")s)", + ")s)",
{"formsemestre_id": sem["formsemestre_id"], valuename: str(item)}, {"formsemestre_id": sem["formsemestre_id"], valuename: item},
) )
except: except:
log("Warning: exception in write_formsemestre_aux !") log("Warning: exception in write_formsemestre_aux !")

View File

@ -81,7 +81,7 @@ def formsemestre_custommenu_edit(context, formsemestre_id, REQUEST=None):
"""Dialog to edit the custom menu""" """Dialog to edit the custom menu"""
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
dest_url = ( dest_url = (
context.NotesURL() + "formsemestre_status?formsemestre_id=%s" % formsemestre_id context.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
) )
H = [ H = [
context.html_sem_header(REQUEST, "Modification du menu du semestre ", sem), context.html_sem_header(REQUEST, "Modification du menu du semestre ", sem),

View File

@ -758,8 +758,12 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
"inscription module:module_id=%s,moduleimpl_id=%s: %s" "inscription module:module_id=%s,moduleimpl_id=%s: %s"
% (module_id, moduleimpl_id, etudids) % (module_id, moduleimpl_id, etudids)
) )
context.do_moduleimpl_inscrit_etuds( sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etudids, REQUEST=REQUEST context,
moduleimpl_id,
formsemestre_id,
etudids,
REQUEST=REQUEST,
) )
msg += [ msg += [
"inscription de %d étudiants au module %s" "inscription de %d étudiants au module %s"
@ -1195,7 +1199,7 @@ def _reassociate_moduleimpls(
) )
for mod in modimpls: for mod in modimpls:
mod["module_id"] = modules_old2new[mod["module_id"]] mod["module_id"] = modules_old2new[mod["module_id"]]
context.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id, cnx=cnx) sco_moduleimpl.do_moduleimpl_edit(context, mod, formsemestre_id=formsemestre_id)
# update decisions: # update decisions:
events = scolars.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id}) events = scolars.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
for e in events: for e in events:
@ -1261,7 +1265,7 @@ def formsemestre_delete(context, formsemestre_id, REQUEST=None):
elif tf[0] == -1: # cancel elif tf[0] == -1: # cancel
return REQUEST.RESPONSE.redirect( return REQUEST.RESPONSE.redirect(
context.NotesURL() context.NotesURL()
+ "formsemestre_status?formsemestre_id=" + "/formsemestre_status?formsemestre_id="
+ formsemestre_id + formsemestre_id
) )
else: else:

View File

@ -505,10 +505,7 @@ def formsemestre_page_title(context, REQUEST):
def fill_formsemestre(context, sem, REQUEST=None): def fill_formsemestre(context, sem, REQUEST=None):
"""Add some useful fields to help display formsemestres""" """Add some useful fields to help display formsemestres"""
# Notes URL notes_url = context.NotesURL()
notes_url = context.absolute_url()
if "/Notes" not in notes_url:
notes_url += "/Notes"
sem["notes_url"] = notes_url sem["notes_url"] = notes_url
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
if sem["etat"] != "1": if sem["etat"] != "1":

View File

@ -1163,8 +1163,8 @@ def formsemestre_validate_previous_ue(context, formsemestre_id, etudid, REQUEST=
return "\n".join(H) + tf[1] + X + warn + context.sco_footer(REQUEST) return "\n".join(H) + tf[1] + X + warn + context.sco_footer(REQUEST)
elif tf[0] == -1: elif tf[0] == -1:
return REQUEST.RESPONSE.redirect( return REQUEST.RESPONSE.redirect(
context.ScoURL() context.NotesURL()
+ "/Notes/formsemestre_status?formsemestre_id=" + "/formsemestre_status?formsemestre_id="
+ formsemestre_id + formsemestre_id
) )
else: else:
@ -1310,8 +1310,8 @@ def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST
_invalidate_etud_formation_caches(context, etudid, sem["formation_id"]) _invalidate_etud_formation_caches(context, etudid, sem["formation_id"])
return REQUEST.RESPONSE.redirect( return REQUEST.RESPONSE.redirect(
context.ScoURL() context.NotesURL()
+ "/Notes/formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s" + "/formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
% (etudid, formsemestre_id) % (etudid, formsemestre_id)
) )

View File

@ -578,6 +578,7 @@ def groups_table(
else: else:
filename = "etudiants_%s" % groups_infos.groups_filename filename = "etudiants_%s" % groups_infos.groups_filename
prefs = context.get_preferences(groups_infos.formsemestre_id)
tab = GenTable( tab = GenTable(
rows=groups_infos.members, rows=groups_infos.members,
columns_ids=columns_ids, columns_ids=columns_ids,
@ -591,8 +592,9 @@ def groups_table(
html_class="table_leftalign table_listegroupe", html_class="table_leftalign table_listegroupe",
xml_outer_tag="group_list", xml_outer_tag="group_list",
xml_row_tag="etud", xml_row_tag="etud",
text_fields_separator=",", # pour csvmoodle text_fields_separator=prefs["moodle_csv_separator"],
preferences=context.get_preferences(groups_infos.formsemestre_id), text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs,
) )
# #
if format == "html": if format == "html":
@ -672,7 +674,10 @@ def groups_table(
% (tab.base_url,), % (tab.base_url,),
'<li><a class="stdlink" href="%s&amp;format=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>' '<li><a class="stdlink" href="%s&amp;format=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
% (tab.base_url,), % (tab.base_url,),
'<li><a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a></li>' """<li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>"""
% groups_infos.formsemestre_id, % groups_infos.formsemestre_id,
] ]
) )
@ -784,9 +789,7 @@ def groups_table(
context.Notes, etud, groups_infos.formsemestre_id context.Notes, etud, groups_infos.formsemestre_id
) )
m["parcours"] = Se.get_parcours_descr() m["parcours"] = Se.get_parcours_descr()
m["codeparcours"], decisions_jury = sco_report.get_codeparcoursetud( m["codeparcours"], _ = sco_report.get_codeparcoursetud(context.Notes, etud)
context.Notes, etud
)
def dicttakestr(d, keys): def dicttakestr(d, keys):
r = [] r = []
@ -933,7 +936,7 @@ def form_choix_saisie_semaine(context, groups_infos, REQUEST=None):
DateJour = time.strftime("%d/%m/%Y") DateJour = time.strftime("%d/%m/%Y")
datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday()
FA = [] # formulaire avec menu saisi hebdo des absences FA = [] # formulaire avec menu saisie hebdo des absences
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">') FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi) FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id) FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id)
@ -955,12 +958,13 @@ def export_groups_as_moodle_csv(context, formsemestre_id=None, REQUEST=None):
""" """
if not formsemestre_id: if not formsemestre_id:
raise ScoValueError("missing parameter: formsemestre_id") raise ScoValueError("missing parameter: formsemestre_id")
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( _, partitions_etud_groups = sco_groups.get_formsemestre_groups(
context, formsemestre_id, with_default=True context, formsemestre_id, with_default=True
) )
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
moodle_sem_name = sem["session_id"] moodle_sem_name = sem["session_id"]
columns_ids = ("email", "semestre_groupe")
T = [] T = []
for partition_id in partitions_etud_groups: for partition_id in partitions_etud_groups:
partition = sco_groups.get_partition(context, partition_id) partition = sco_groups.get_partition(context, partition_id)
@ -975,11 +979,14 @@ def export_groups_as_moodle_csv(context, formsemestre_id=None, REQUEST=None):
elts.append(group_name) elts.append(group_name)
T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
# Make table # Make table
prefs = context.get_preferences(formsemestre_id)
tab = GenTable( tab = GenTable(
rows=T, rows=T,
columns_ids=("email", "semestre_groupe"), columns_ids=("email", "semestre_groupe"),
filename=moodle_sem_name + "-moodle", filename=moodle_sem_name + "-moodle",
text_fields_separator=",", titles={x: x for x in columns_ids},
preferences=context.get_preferences(formsemestre_id), text_fields_separator=prefs["moodle_csv_separator"],
text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs,
) )
return tab.make_page(context, format="csv", REQUEST=REQUEST) return tab.make_page(context, format="csv", REQUEST=REQUEST)

View File

@ -34,6 +34,7 @@ import sco_utils as scu
from sco_utils import ( from sco_utils import (
EVALUATION_NORMALE, EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE, EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
) )
from sco_permissions import ScoEtudInscrit, ScoAbsChange from sco_permissions import ScoEtudInscrit, ScoAbsChange
from notes_log import log from notes_log import log
@ -259,9 +260,10 @@ def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=No
ScoAbsChange, context ScoAbsChange, context
) and sco_formsemestre.sem_est_courant(context, sem): ) and sco_formsemestre.sem_est_courant(context, sem):
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday() datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
group_id = sco_groups.get_default_group(context, formsemestre_id)
H.append( H.append(
'<span class="moduleimpl_abs_link"><a class="stdlink" href="Absences/SignaleAbsenceGrHebdo?formsemestre_id=%s&moduleimpl_id=%s&datelundi=%s">Saisie Absences hebdo.</a></span>' '<span class="moduleimpl_abs_link"><a class="stdlink" href="Absences/SignaleAbsenceGrHebdo?formsemestre_id=%s&moduleimpl_id=%s&datelundi=%s&group_ids=%s">Saisie Absences hebdo.</a></span>'
% (formsemestre_id, moduleimpl_id, datelundi) % (formsemestre_id, moduleimpl_id, datelundi, group_id)
) )
H.append("</td></tr></table>") H.append("</td></tr></table>")
@ -340,8 +342,8 @@ def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=No
partition_id=partition_id, partition_id=partition_id,
select_first_partition=True, select_first_partition=True,
) )
if eval["evaluation_type"] == EVALUATION_RATTRAPAGE: if eval["evaluation_type"] in (EVALUATION_RATTRAPAGE, EVALUATION_SESSION2):
tr_class = "mievr_rattr" tr_class = "mievr mievr_rattr"
else: else:
tr_class = "mievr" tr_class = "mievr"
tr_class_1 = "mievr" tr_class_1 = "mievr"
@ -360,7 +362,13 @@ def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=No
) )
H.append("&nbsp;&nbsp;&nbsp; <em>%(description)s</em>" % eval) H.append("&nbsp;&nbsp;&nbsp; <em>%(description)s</em>" % eval)
if eval["evaluation_type"] == EVALUATION_RATTRAPAGE: if eval["evaluation_type"] == EVALUATION_RATTRAPAGE:
H.append("""<span class="mievr_rattr">rattrapage</span>""") H.append(
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>"""
)
elif eval["evaluation_type"] == EVALUATION_SESSION2:
H.append(
"""<span class="mievr_rattr" title="remplace autres notes">session 2</span>"""
)
if etat["last_modif"]: if etat["last_modif"]:
H.append( H.append(
"""<span class="mievr_lastmodif">(dernière modif le %s)</span>""" """<span class="mievr_lastmodif">(dernière modif le %s)</span>"""

View File

@ -153,6 +153,10 @@ def ficheEtud(context, etudid=None, REQUEST=None):
"fiche d'informations sur un etudiant" "fiche d'informations sur un etudiant"
authuser = REQUEST.AUTHENTICATED_USER authuser = REQUEST.AUTHENTICATED_USER
cnx = context.GetDBConnexion() cnx = context.GetDBConnexion()
if etudid and REQUEST:
# la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar()
REQUEST.form["etudid"] = etudid
args = make_etud_args(etudid=etudid, REQUEST=REQUEST) args = make_etud_args(etudid=etudid, REQUEST=REQUEST)
etuds = scolars.etudident_list(cnx, args) etuds = scolars.etudident_list(cnx, args)
if not etuds: if not etuds:

View File

@ -168,7 +168,7 @@ PREF_CATEGORIES = (
), ),
( (
"feuilles", "feuilles",
{"title": "Mise en forme des feuilles (Absences, Trombinoscopes, ...)"}, {"title": "Mise en forme des feuilles (Absences, Trombinoscopes, Moodle, ...)"},
), ),
("pe", {"title": "Avis de poursuites d'études"}), ("pe", {"title": "Avis de poursuites d'études"}),
("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}), ("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}),
@ -1637,6 +1637,28 @@ Année scolaire: %(anneescolaire)s
"only_global": True, "only_global": True,
}, },
), ),
# Exports pour Moodle:
(
"moodle_csv_with_headerline",
{
"initvalue": 0,
"title": "Inclure une ligne d'en-têtes dans les fichiers CSV pour Moodle",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"only_global": True,
"category": "feuilles",
},
),
(
"moodle_csv_separator",
{
"initvalue": ",",
"title": "séparateur de colonnes dans les fichiers CSV pour Moodle",
"size": 2,
"only_global": True,
"category": "feuilles",
},
),
# Experimental: avis poursuite d'études # Experimental: avis poursuite d'études
( (
"NomResponsablePE", "NomResponsablePE",
@ -2040,7 +2062,7 @@ function set_global_pref(el, pref_name) {
) )
dest_url = ( dest_url = (
self.context.NotesURL() self.context.NotesURL()
+ "formsemestre_status?formsemestre_id=%s" % self.formsemestre_id + "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + self.context.sco_footer(REQUEST) return "\n".join(H) + tf[1] + self.context.sco_footer(REQUEST)

View File

@ -824,10 +824,10 @@ def _pvjury_pdf_type(
# Signature du directeur # Signature du directeur
objects += sco_pdf.makeParas( objects += sco_pdf.makeParas(
"""<para spaceBefore="10mm" align="right"> """<para spaceBefore="10mm" align="right">
Le %s, %s</para>""" %s, %s</para>"""
% ( % (
context.get_preference("DirectorTitle", formsemestre_id) or "",
context.get_preference("DirectorName", formsemestre_id) or "", context.get_preference("DirectorName", formsemestre_id) or "",
context.get_preference("DirectorTitle", formsemestre_id) or "",
), ),
style, style,
) )

View File

@ -416,7 +416,7 @@ def make_formsemestre_recapcomplet(
l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric))) # moy_gen l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric))) # moy_gen
# Ajoute rangs dans groupes seulement si CSV ou XLS # Ajoute rangs dans groupes seulement si CSV ou XLS
if format[:3] == "xls" or format == "csv": if format[:3] == "xls" or format == "csv":
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( rang_gr, _, gr_name = sco_bulletins.get_etud_rangs_groups(
context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
) )
@ -758,7 +758,9 @@ def make_formsemestre_recapcomplet(
H.append("</table>") H.append("</table>")
return "\n".join(H), "", "html" return "\n".join(H), "", "html"
elif format == "csv": elif format == "csv":
CSV = scu.CSV_LINESEP.join([scu.CSV_FIELDSEP.join(str(x)) for x in F]) CSV = scu.CSV_LINESEP.join(
[scu.CSV_FIELDSEP.join([str(x) for x in l]) for l in F]
)
semname = sem["titre_num"].replace(" ", "_") semname = sem["titre_num"].replace(" ", "_")
date = time.strftime("%d-%m-%Y") date = time.strftime("%d-%m-%Y")
filename = "notes_modules-%s-%s.csv" % (semname, date) filename = "notes_modules-%s-%s.csv" % (semname, date)

View File

@ -111,6 +111,7 @@ NOTES_MENTIONS_LABS = (
EVALUATION_NORMALE = 0 EVALUATION_NORMALE = 0
EVALUATION_RATTRAPAGE = 1 EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2
def fmt_note(val, note_max=None, keep_numeric=False): def fmt_note(val, note_max=None, keep_numeric=False):

View File

@ -9,9 +9,10 @@
# le département via l'interface web (Zope) # le département via l'interface web (Zope)
usage() { usage() {
echo "Usage: $0 [-r] dept [script...]" echo "Usage: $0 [-h] [-r] [-x] dept [script...]"
echo "Lance un environnement interactif python/ScoDoc" echo "Lance un environnement interactif python/ScoDoc"
echo " -r: supprime et recrée le département (attention: efface la base !)" echo " -r: supprime et recrée le département (attention: efface la base !)"
echo " -x: exit après exécution des scripts, donc mode non interactif"
exit 1 exit 1
} }
@ -20,24 +21,38 @@ cd /opt/scodoc/Products/ScoDoc || exit 2
source config/config.sh source config/config.sh
source config/utils.sh source config/utils.sh
if [ $# -lt 1 ] RECREATE_DEPT=0
then PYTHON_INTERACTIVE="-i"
usage
fi
if [ "$1" = "-r" ] while [ -n "$1" ]; do
then PARAM="$1"
[ "${PARAM::1}" != "-" ] && break
case $PARAM in
-h | --help)
usage
exit 0
;;
-r)
RECREATE_DEPT=1
;;
-x)
PYTHON_INTERACTIVE=""
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
usage
exit 1
;;
esac
shift shift
recreate_dept=1 done
else
recreate_dept=0
fi
DEPT="$1" DEPT="$1"
shift shift
if [ "$recreate_dept" = 1 ] if [ "$RECREATE_DEPT" = 1 ]
then then
cfg_pathname="${SCODOC_VAR_DIR}/config/depts/$DEPT".cfg cfg_pathname="${SCODOC_VAR_DIR}/config/depts/$DEPT".cfg
if [ -e "$cfg_pathname" ] if [ -e "$cfg_pathname" ]
@ -48,13 +63,17 @@ then
# systemctl start scodoc # systemctl start scodoc
fi fi
cmd="from __future__ import print_function;from Zope2 import configure;configure('/opt/scodoc/etc/zope.conf');import Zope2; app=Zope2.app();from debug import *;context = go_dept(app, '""$DEPT""');" cmd="from __future__ import print_function;from Zope2 import configure;configure('/opt/scodoc/etc/zope.conf');import Zope2; app=Zope2.app();from debug import *;context = go_dept(app, '""$DEPT""', verbose=False);"
for f in "$@" for f in "$@"
do do
cmd="${cmd}exec(open(\"${f}\").read());" cmd="${cmd}exec(open(\"${f}\").read());"
done done
/opt/zope213/bin/python -i -c "$cmd" if [ -z "$PYTHON_INTERACTIVE" ]
then
/opt/zope213/bin/python -c "$cmd"
else
/opt/zope213/bin/python "$PYTHON_INTERACTIVE" -c "$cmd"
fi

View File

@ -11,12 +11,21 @@ Utiliser comme:
""" """
import random import random
# La variable context est définie par le script de lancement
# l'affecte ainsi pour évietr les warnins pylint:
context = context # pylint: disable=undefined-variable
REQUEST = REQUEST # pylint: disable=undefined-variable
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error
import sco_utils import sco_utils
import sco_abs import sco_abs
import sco_abs_views import sco_abs_views
import sco_bulletins
import sco_evaluations
import sco_codes_parcours
import sco_parcours_dut
import sco_formsemestre_validation
G = sco_fake_gen.ScoFake(context.Notes) # pylint: disable=undefined-variable G = sco_fake_gen.ScoFake(context.Notes)
G.verbose = False G.verbose = False
# --- Création d'étudiants # --- Création d'étudiants
@ -61,12 +70,65 @@ e = G.create_evaluation(
coefficient=1.0, coefficient=1.0,
) )
# --- Saisie notes # --- Saisie toutes les notes de l'évaluation
for etud in etuds: for etud in etuds:
nb_changed, nb_suppress, existing_decisions = G.create_note( nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation=e, etud=etud, note=float(random.randint(0, 20)) evaluation=e, etud=etud, note=float(random.randint(0, 20))
) )
# --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(
context.Notes, sem["formsemestre_id"], etud["etudid"], REQUEST=REQUEST
)
# Toute les notes sont saisies, donc eval complète
etat = sco_evaluations.do_evaluation_etat(context.Notes, e["evaluation_id"])
assert etat["evalcomplete"]
# Un seul module, donc moy gen == note module
assert b["ues"][0]["cur_moy_ue_txt"] == b["ues"][0]["modules"][0]["mod_moy_txt"]
# Note au module égale à celle de l'éval
assert (
b["ues"][0]["modules"][0]["mod_moy_txt"]
== b["ues"][0]["modules"][0]["evaluations"][0]["note_txt"]
)
# --- Une autre évaluation
e2 = G.create_evaluation(
moduleimpl_id=mi["moduleimpl_id"],
jour="02/01/2020",
description="evaluation test 2",
coefficient=1.0,
)
# Saisie les notes des 5 premiers étudiants:
for etud in etuds[:5]:
nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation=e2, etud=etud, note=float(random.randint(0, 20))
)
# Cette éval n'est pas complète
etat = sco_evaluations.do_evaluation_etat(context.Notes, e2["evaluation_id"])
assert etat["evalcomplete"] == False
# la première éval est toujours complète:
etat = sco_evaluations.do_evaluation_etat(context.Notes, e["evaluation_id"])
assert etat["evalcomplete"]
# Modifie l'évaluation 2 pour "prise en compte immédiate"
e2["publish_incomplete"] = "1"
context.Notes.do_evaluation_edit(REQUEST, e2)
etat = sco_evaluations.do_evaluation_etat(context.Notes, e2["evaluation_id"])
assert etat["evalcomplete"] == False
assert etat["nb_att"] == 0 # il n'y a pas de notes (explicitement) en attente
assert etat["evalattente"] # mais l'eval est en attente (prise en compte immédiate)
# Saisie des notes qui manquent:
for etud in etuds[5:]:
nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation=e2, etud=etud, note=float(random.randint(0, 20))
)
etat = sco_evaluations.do_evaluation_etat(context.Notes, e2["evaluation_id"])
assert etat["evalcomplete"]
assert etat["nb_att"] == 0
assert not etat["evalattente"] # toutes les notes sont présentes
# --- Saisie absences # --- Saisie absences
etudid = etuds[0]["etudid"] etudid = etuds[0]["etudid"]
@ -91,3 +153,27 @@ _ = sco_abs_views.doJustifAbsence(
a = sco_abs.getAbsSemEtud(context.Absences, sem, etudid) a = sco_abs.getAbsSemEtud(context.Absences, sem, etudid)
assert a.CountAbs() == 3 assert a.CountAbs() == 3
assert a.CountAbsJust() == 1 assert a.CountAbsJust() == 1
# --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance
# on n'a pas encore saisi de décisions
assert not sco_parcours_dut.formsemestre_has_decisions(context, sem["formsemestre_id"])
# Saisie d'un décision AJ, non assidu
etudid = etuds[-1]["etudid"]
sco_parcours_dut.formsemestre_validate_ues(
context.Notes,
sem["formsemestre_id"],
etudid,
sco_codes_parcours.AJ,
False,
REQUEST=REQUEST,
)
assert sco_parcours_dut.formsemestre_has_decisions(
context.Notes, sem["formsemestre_id"]
)
# Suppression de la décision
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
context.Notes, sem["formsemestre_id"], etudid
)
assert not sco_parcours_dut.formsemestre_has_decisions(
context.Notes, sem["formsemestre_id"]
)

View File

@ -11,8 +11,10 @@ Utiliser comme:
""" """
# La variable context est définie par le script de lancement
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error # l'affecte ainsi pour évietr les warnins pylint:
context = context # pylint: disable=undefined-variable
import scotests.sco_fake_gen as sco_fake_gen
import sco_utils as scu import sco_utils as scu
import sco_moduleimpl import sco_moduleimpl

View File

@ -10,13 +10,15 @@ Utiliser comme:
scotests/scointeractive.sh -r TEST00 scotests/test_capitalisation.py scotests/scointeractive.sh -r TEST00 scotests/test_capitalisation.py
""" """
# La variable context est définie par le script de lancement
# l'affecte ainsi pour éviter les warnings pylint:
context = context # pylint: disable=undefined-variable
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error
import sco_utils import sco_utils
import sco_codes_parcours import sco_codes_parcours
import sco_modalites import sco_modalites
G = sco_fake_gen.ScoFake(context.Notes) # pylint: disable=undefined-variable G = sco_fake_gen.ScoFake(context.Notes)
G.verbose = False G.verbose = False
# --- Création d'étudiants # --- Création d'étudiants

View File

@ -12,16 +12,24 @@ Utiliser comme:
scotests/scointeractive.sh -r TEST00 scotests/test_demissions.py scotests/scointeractive.sh -r TEST00 scotests/test_demissions.py
""" """
import datetime
import re
import json
# La variable context est définie par le script de lancement
# l'affecte ainsi pour évietr les warnins pylint:
context = context # pylint: disable=undefined-variable
REQUEST = REQUEST # pylint: disable=undefined-variable
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error
import sco_utils import sco_utils
import sco_bulletins import sco_bulletins
G = sco_fake_gen.ScoFake(context.Notes) # pylint: disable=undefined-variable G = sco_fake_gen.ScoFake(context.Notes)
G.verbose = False G.verbose = False
nb_etuds = 10
# --- Création d'étudiants # --- Création d'étudiants
etuds = [G.create_etud(code_nip=None) for _ in range(2)] etuds = [G.create_etud(code_nip=None) for _ in range(nb_etuds)]
# --- Mise en place formation # --- Mise en place formation
form, ue_list, mod_list = G.setup_formation( form, ue_list, mod_list = G.setup_formation(
nb_semestre=1, titre="Essai 1", acronyme="ESS01" nb_semestre=1, titre="Essai 1", acronyme="ESS01"
@ -59,3 +67,35 @@ print(bul["moy_gen"])
assert bul["moy_gen"] == "NA" assert bul["moy_gen"] == "NA"
assert bul["ins"][0]["etat"] == "D" assert bul["ins"][0]["etat"] == "D"
# ------------ Billets d'absences
etud = etuds[1] # non demissionnaire
d = sem["date_debut_iso"]
d_beg = datetime.datetime(*[int(x) for x in d.split("-")])
d_end = d_beg + datetime.timedelta(2)
description = "billet test 0"
x = context.Absences.AddBilletAbsence(
d_beg.isoformat(),
d_end.isoformat(),
description=description,
etudid=etud["etudid"],
REQUEST=REQUEST,
)
#
billet_id = re.search(r"billet_id value=\"([A-Z0-9]+)\"", x).group(1)
context.Absences.deleteBilletAbsence(billet_id, REQUEST=REQUEST, dialog_confirmed=True)
j = context.Absences.listeBilletsEtud(
etudid=etud["etudid"], REQUEST=REQUEST, format="json"
)
assert len(json.loads(j)) == 0
x = context.Absences.AddBilletAbsence(
d_beg.isoformat(),
d_end.isoformat(),
description=description,
etudid=etud["etudid"],
REQUEST=REQUEST,
)
j = context.Absences.listeBilletsEtud(
etudid=etud["etudid"], REQUEST=REQUEST, format="json"
)
assert json.loads(j)[0]["description"] == description

View File

@ -1261,9 +1261,14 @@ tr.mievr_rattr {
background-color:#dddddd; background-color:#dddddd;
} }
span.mievr_rattr { span.mievr_rattr {
display: inline-block;
font-weight: bold; font-weight: bold;
color: blue; font-size: 80%;
color: white;
background-color: orangered;
margin-left: 2em; margin-left: 2em;
margin-top: 1px;
margin-bottom: 2px;;
border: 1px solid red; border: 1px solid red;
padding: 1px 3px 1px 3px; padding: 1px 3px 1px 3px;
} }

View File

@ -30,10 +30,9 @@ function ajaxFunction(mod, etudid, dat){
} }
ajaxRequest.open("POST", "doSignaleAbsenceGrSemestre", true); ajaxRequest.open("POST", "doSignaleAbsenceGrSemestre", true);
ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
oForm = document.forms[0]; var oSelectOne = $("#abs_form")[0].elements["moduleimpl_id"];
oSelectOne = oForm.elements["moduleimpl_id"]; var index = oSelectOne.selectedIndex;
index = oSelectOne.selectedIndex; var modul_id = oSelectOne.options[index].value;
modul_id = oSelectOne.options[index].value;
if (mod == 'add') { if (mod == 'add') {
ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id + "&abslist:list=" + etudid + ":" + dat); ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id + "&abslist:list=" + etudid + ":" + dat);
} }
@ -42,3 +41,7 @@ function ajaxFunction(mod, etudid, dat){
} }
} }
// -----
function change_moduleimpl(url) {
document.location = url + '&amp;moduleimpl_id=' + document.getElementById('moduleimpl_id').value;
}

View File

@ -10,6 +10,7 @@ $(function() {
position: { collision: 'flip' }, // automatic menu position up/down position: { collision: 'flip' }, // automatic menu position up/down
source: "search_etud_by_name", source: "search_etud_by_name",
select: function (event, ui) { select: function (event, ui) {
$("#in-expnom").val(ui.item.value);
$("#form-chercheetud").submit(); $("#form-chercheetud").submit();
} }
}); });