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 = """
<h4>Année 2021</h4>
<ul>
<li>Évaluations de type "deuxième session"</li>
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
<li>Diverses corrections (PV de jurys, ...)</li>
<li>Modernisation du code Python</li>
</ul>
<h4>Année 2020</h4>
<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>Enregistrement de semestres extérieurs</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 TrivialFormulator import TrivialFormulator, TF
from gen_tables import GenTable
import html_sco_header
import scolars
import sco_formsemestre
import sco_moduleimpl
@ -78,6 +79,8 @@ import sco_compute_moy
import sco_abs
from sco_abs import ddmmyyyy
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
def _toboolean(x):
"convert a value to boolean (ensure backward compat with OLD intranet code)"
@ -343,11 +346,17 @@ class ZAbsences(
)
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):
"""CountAbs
matin= 1 ou 0.
Args:
etudid
debut string iso date ("2020-03-12")
end string iso date ("2020-03-12")
matin None, True, False
moduleimpl_id
"""
if matin != None:
matin = _toboolean(matin)
@ -358,10 +367,11 @@ class ZAbsences(
modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s "
else:
modul = ""
if not cursor:
cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor)
cursor.execute(
"""SELECT COUNT(*) AS NbAbs FROM (
"""
SELECT DISTINCT A.JOUR, A.MATIN
FROM ABSENCES A
WHERE A.ETUDID = %(etudid)s
@ -370,13 +380,27 @@ class ZAbsences(
+ modul
+ """
AND A.JOUR BETWEEN %(debut)s AND %(fin)s
) AS tmp
""",
vars(),
)
res = cursor.fetchone()[0]
res = cursor.dictfetchall()
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")
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)
sem = sco_formsemestre.do_formsemestre_list(
self, {"formsemestre_id": formsemestre_id}
@ -745,20 +774,41 @@ class ZAbsences(
self.sco_header(
page_title="Saisie hebdomadaire des absences",
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,
REQUEST=REQUEST,
),
"""<table border="0" cellspacing="16"><tr><td>
<h2>Saisie des absences %s %s,
<span class="fontred">semaine du lundi %s</span></h2>
<p><a href="index_html">Annuler</a></p>
<p>
<form action="doSignaleAbsenceGrHebdo" method="post" action="%s">
<div>
<form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
<input type="hidden" name="datelundi" id="datelundi" value="%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 = []
@ -797,12 +847,12 @@ class ZAbsences(
sel = "selected" # aucun module specifie
H.append(
"""
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">
"""Module concerné:
<select id="moduleimpl_id" name="moduleimpl_id" onchange="change_moduleimpl('%(url)s')">
<option value="" %(sel)s>non spécifié</option>
%(menu_module)s
</select>
</p>"""
</div>"""
% {"menu_module": menu_module, "url": base_url, "sel": sel}
)
@ -826,7 +876,6 @@ class ZAbsences(
REQUEST=None,
):
"""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(
self, group_ids, REQUEST=REQUEST
)
@ -934,7 +983,7 @@ class ZAbsences(
les <span class="fontred">%s</span></h2>
<p>
<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),
]
@ -1006,7 +1055,7 @@ class ZAbsences(
Args:
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é.
"""
H = [
@ -1043,6 +1092,8 @@ class ZAbsences(
]
# Dates
odates = [datetime.date(*[int(x) for x in d.split("-")]) for d in dates]
begin = dates[0]
end = dates[-1]
# Titres colonnes
noms_jours = [] # eg [ "Lundi", "mardi", "Samedi", ... ]
jn = sco_abs.day_names(self)
@ -1071,6 +1122,8 @@ class ZAbsences(
'<tr><td><span class="redboldtext">Aucun étudiant inscrit !</span></td></tr>'
)
i = 1
cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor)
for etud in etuds:
i += 1
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, 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
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"
else:
checked = ""
# bulle lors du passage souris
coljour = sco_abs.DAYNAMES[
(calendar.weekday(int(date[:4]), int(date[5:7]), int(date[8:])))
]
datecol = coljour + " " + date[8:] + "/" + date[5:7] + "/" + date[:4]
coljour = sco_abs.DAYNAMES[(calendar.weekday(d.year, d.month, d.day))]
datecol = coljour + " " + d.strftime("%d/%m/%Y")
bulle_am = '"' + etud["nomprenom"] + " - " + datecol + ' (matin)"'
bulle_pm = '"' + etud["nomprenom"] + " - " + datecol + ' (ap.midi)"'
@ -1122,9 +1178,8 @@ class ZAbsences(
)
)
# après-midi
if self.CountAbs(
etudid, date, date, False, moduleimpl_id=moduleimpl_id
):
is_abs = {"jour": d, "matin": False} in etud_abs
if is_abs:
checked = "checked"
else:
checked = ""

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ then
# suppression de la base postgres
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
echo "Suppression de la base postgres $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
check_field(
cnx,

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

@ -1,4 +1,5 @@
#!/opt/zope213/bin/python
# -*- coding: utf-8 -*-
"""
ScoDoc post-upgrade script.
@ -11,15 +12,16 @@ _before_ upgrading the database.
E. Viennet, June 2008
Mar 2017: suppress upgrade of very old Apache configs
Aug 2020: move photos to .../var/scodoc/
Apr 2021: bug #70
"""
import os
import sys
import glob
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:
log('postupgrade.py: must be run as root')
log("postupgrade.py: must be run as root")
sys.exit(1)
# ---
@ -54,6 +56,19 @@ for d in glob.glob( SCODOC_DIR + "/logos_*" ):
log("Moving %s to %s" % (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...
# ---

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("..")
# 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):
sys.stdout.flush()
@ -24,8 +30,10 @@ SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "")
if not SCODOC_VAR_DIR:
log("Error: environment variable SCODOC_VAR_DIR is not defined")
sys.exit(1)
SCO_TMPDIR = os.path.join(SCODOC_VAR_DIR, "tmp")
SCODOC_LOGOS_DIR = os.environ.get("SCODOC_LOGOS_DIR", "")
def get_dept_cnx_str(dept):
"db cnx string for dept"
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
svnver=$(svn --version --quiet)
# shellcheck disable=SC2072
if [[ ${svnver} > "1.7" ]]
then
(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
SVERSION="$(${CMD})"
if [ $? == 0 ]; then
if [ "$?" == 0 ]; then
#echo "answer=${SVERSION}"
echo "${SVERSION}" > "${SCODOC_VERSION_DIR}"/scodoc.sn
else
@ -132,6 +133,10 @@ then
chmod 600 "$LOCAL_CONFIG_FILENAME"
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)
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
def go(app, n=0):
def go(app, n=0, verbose=True):
context = app.ScoDoc.objectValues("Folder")[n].Scolarite
if verbose:
print("context in dept ", context.DeptId())
return context
def go_dept(app, dept):
def go_dept(app, dept, verbose=True):
objs = app.ScoDoc.objectValues("Folder")
for o in objs:
context = o.Scolarite
if context.DeptId() == dept:
if verbose:
print("context in dept ", context.DeptId())
return context
raise ValueError("dep %s not found" % dept)

View File

@ -123,6 +123,7 @@ class GenTable:
pdf_col_widths=None,
xml_outer_tag="table",
xml_row_tag="row",
text_with_titles=False, # CSV with header line
text_fields_separator="\t",
preferences=None,
):
@ -173,6 +174,7 @@ class GenTable:
self.xml_row_tag = xml_row_tag
# TEXT parameters
self.text_fields_separator = text_fields_separator
self.text_with_titles = text_with_titles
#
if preferences:
self.preferences = preferences
@ -265,8 +267,7 @@ class GenTable:
def get_titles_list(self):
"list of titles"
l = []
return l + [self.titles.get(cid, "") for cid in self.columns_ids]
return [self.titles.get(cid, "") for cid in self.columns_ids]
def gen(self, format="html", columns_ids=None):
"""Build representation of the table in the specified format.
@ -479,10 +480,14 @@ class GenTable:
def text(self):
"raw text representation of the table"
if self.text_with_titles:
headline = [self.get_titles_list()]
else:
headline = []
return "\n".join(
[
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
# 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)
objects = []

View File

@ -42,7 +42,13 @@ Génération de la "sidebar" (marge gauche des pages HTML)
def sidebar_common(context, REQUEST=None):
"partie commune a toutes les sidebar"
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 = [
'<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>'
@ -50,8 +56,8 @@ def sidebar_common(context, REQUEST=None):
context.sidebar_dept(REQUEST),
"""<h2 class="insidebar">Scolarit&eacute;</h2>
<a href="%(ScoURL)s" class="sidebar">Semestres</a> <br/>
<a href="%(ScoURL)s/Notes" class="sidebar">Programmes</a> <br/>
<a href="%(ScoURL)s/Absences" class="sidebar">Absences</a> <br/>
<a href="%(NotesURL)s" class="sidebar">Programmes</a> <br/>
<a href="%(AbsencesURL)s" class="sidebar">Absences</a> <br/>
"""
% params,
]
@ -60,14 +66,7 @@ def sidebar_common(context, REQUEST=None):
ScoUsersView, context
):
H.append(
"""<a href="%(ScoURL)s/Users" class="sidebar">Utilisateurs</a> <br/>"""
% params
)
if 0: # XXX experimental
H.append(
"""<a href="%(ScoURL)s/Notes/Services" class="sidebar">Services</a> <br/>"""
% params
"""<a href="%(UsersURL)s" class="sidebar">Utilisateurs</a> <br/>""" % params
)
if authuser.has_permission(ScoChangePreferences, context):
@ -88,8 +87,8 @@ def sidebar(context, REQUEST=None):
H.append(
"""<div class="box-chercheetud">Chercher étudiant:<br/>
<form id="form-chercheetud" action="%(ScoURL)s/search_etud_in_dept">
<div><input type="text" size="12" id="in-expnom" name="expnom"></input></div>
<form method="get" id="form-chercheetud" action="%(ScoURL)s/search_etud_in_dept">
<div><input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false"></input></div>
</form></div>
<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
// Ne nécessite pas d'authentification avec sco_user et sco_pw - Il est possible de choisir le format XML ou JSON.
// formsemestre_list
// Paramètres (tous optionnels): formsesmestre_id, formation_id, etape_apo, etape_apo2
// Coquille dans la doc : formsesmestre_id
// Paramètres (tous optionnels): formsemestre_id, formation_id, etape_apo
// Résultat: liste des semestres correspondant.
// Exemple: formsemestre_list?format=xml&etape_apo=V1RT
global $sco_pw;

View File

@ -35,6 +35,18 @@ CREATE FUNCTION notes_newid_etud( text ) returns text as '
as result;
' 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
CREATE TABLE sco_prefs (
pref_id text DEFAULT notes_newid('PREF'::text) UNIQUE NOT NULL,

View File

@ -1132,6 +1132,9 @@ class NotesTable:
def sem_has_decisions(self):
"""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
def etud_has_decision(self, etudid):

View File

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

View File

@ -325,7 +325,7 @@ def do_formsemestre_archive(
if data:
PVArchive.store(archive_id, "Decisions_Jury.xls", data)
# 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
)
if data:
@ -548,7 +548,7 @@ def formsemestre_delete_archive(
raise AccessDenied(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
sem = sco_formsemestre.get_formsemestre(
_ = sco_formsemestre.get_formsemestre(
context, formsemestre_id
) # check formsemestre_id
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"])
if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
e["coef_txt"] = "rat."
elif e["evaluation_type"] == scu.EVALUATION_SESSION2:
e["coef_txt"] = "sess. 2"
if e["etat"]["evalattente"]:
mod_attente = True # une eval en attente dans ce module
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"]),
min=scu.fmt_note(ue["min"]),
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]),
effectif=str(nt.ue_rangs[ue["ue_id"]][1]),
@ -275,6 +276,7 @@ def formsemestre_bulletinetud_published_dict(
),
coefficient=e["coefficient"],
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"]),
note=val,
)

View File

@ -378,7 +378,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
P[-1]["_pdf_style"].append(
("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:
nbabs = I["nbabs"]
nbabsjust = I["nbabsjust"]
@ -392,8 +393,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"abs": "%s / %s" % (nbabs, nbabsjust),
"_css_row_class": "notes_bulletin_row_gen",
"_titre_colspan": 2,
"_pdf_row_markup": ['font size="12"', "b"], # bold, size 12
"_pdf_style": [("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR)],
"_pdf_row_markup": ["b"], # bold. On peut ajouter 'font size="12"'
"_pdf_style": [
("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR),
],
}
P.append(t)

View File

@ -38,6 +38,7 @@ from sco_utils import (
NOTES_NEUTRALISE,
EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
)
from sco_exceptions import ScoException
from notesdb import EditableTable, quote_html
@ -242,7 +243,10 @@ def do_moduleimpl_moyennes(context, nt, mod):
if e["etat"]["evalattente"]:
attente = True
if e["evaluation_type"] == EVALUATION_RATTRAPAGE:
if (
e["evaluation_type"] == EVALUATION_RATTRAPAGE
or e["evaluation_type"] == EVALUATION_SESSION2
):
if eval_rattr:
# !!! plusieurs rattrapages !
diag_info.update(
@ -344,7 +348,7 @@ def do_moduleimpl_moyennes(context, nt, mod):
if diag_info:
diag_info["moduleimpl_id"] = moduleimpl_id
R[etudid] = user_moy
# Note de rattrapage ?
# Note de rattrapage ou deuxième session ?
if eval_rattr:
if eval_rattr["notes"].has_key(etudid):
note = eval_rattr["notes"][etudid]["value"]
@ -353,9 +357,15 @@ def do_moduleimpl_moyennes(context, nt, mod):
R[etudid] = note
else:
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]:
# log('note_sur_20=%s' % 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

View File

@ -129,11 +129,12 @@ def index_html(context, REQUEST=None, showcodes=0, showsemtable=0):
)
H.append(
"""<p><form action="Notes/view_formsemestre_by_etape">
Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>
"""<p><form action="%s/view_formsemestre_by_etape">
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form
</p>
"""
% context.NotesURL()
)
#
authuser = REQUEST.AUTHENTICATED_USER
@ -155,9 +156,10 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>
"""<hr>
<h3>Exports Apogée</h3>
<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>
"""
% context.NotesURL()
)
#
H.append(
@ -175,9 +177,9 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>
def _sem_table(context, sems):
"""Affiche liste des semestres, utilisée pour semestres en cours"""
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><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>
</td>
</tr>
@ -199,6 +201,7 @@ def _sem_table(context, sems):
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
sem["notes_url"] = context.NotesURL()
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
@ -245,14 +248,16 @@ def _sem_table_gt(context, sems, showcodes=False):
def _style_sems(context, sems):
"""ajoute quelques attributs de présentation pour la table"""
for sem in sems:
sem["notes_url"] = context.NotesURL()
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["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["_dash_mois_fin_class"] = "datesem"
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>"""
% sem
)

View File

@ -108,7 +108,9 @@ def do_evaluation_delete(context, REQUEST, evaluation_id):
# news
mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
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(
context,
REQUEST,
@ -133,11 +135,6 @@ def do_evaluation_etat(
à ce module ont des notes)
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(
sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, getallstudents=True
@ -232,6 +229,7 @@ def do_evaluation_etat(
if (
(TotalNbMissing > 0)
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
and not is_malus
):
complete = False
@ -245,6 +243,9 @@ def do_evaluation_etat(
evalattente = True
else:
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
gr_moyennes = [] # group : {moy,median, nb_notes}
@ -268,11 +269,6 @@ def do_evaluation_etat(
}
)
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
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
les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
</p><p class="help">
La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules
si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont
Les modalités "rattrapage" et "deuxième session" définissent des évaluations prises en compte de
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.
</p>
<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
de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
</p>
@ -1024,9 +1028,17 @@ def evaluation_create_form(
{
"input_type": "menu",
"title": "Modalité",
"allowed_values": (scu.EVALUATION_NORMALE, scu.EVALUATION_RATTRAPAGE),
"allowed_values": (
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"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(
"""<form action="search_etud_in_dept" method="POST">
<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">
<br/>(entrer une partie du nom)
"""
@ -96,71 +96,52 @@ def form_search_etud(
return "\n".join(H)
# was chercheEtud()
def search_etud_in_dept(
context,
expnom=None,
dest_url="ficheEtud",
parameters={},
parameters_keys="",
add_headers=True, # complete page
title=None,
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)
def search_etud_in_dept(context, expnom="", REQUEST=None):
"""Page recherche d'un etudiant.
Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats, la liste des étudianst
correspondants.
Appelée par boite de recherche barre latérale gauche.
Args:
expnom: string, regexp sur le nom ou un code_nip ou un etudid
"""
if type(expnom) == ListType:
expnom = expnom[0]
q = []
if parameters:
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)
dest_url = "ficheEtud"
if len(expnom) > 1:
etuds = context.getEtudInfo(filled=1, etudid=expnom, REQUEST=REQUEST)
if len(etuds) != 1:
if scu.is_valid_code_nip(expnom):
etuds = search_etuds_infos(context, code_nip=expnom, REQUEST=REQUEST)
elif expnom:
else:
etuds = search_etuds_infos(context, expnom=expnom, REQUEST=REQUEST)
else:
etuds = []
etuds = [] # si expnom est trop court, n'affiche rien
if len(etuds) == 1:
# va directement a la destination
return REQUEST.RESPONSE.redirect(
dest_url + "?etudid=%s&amp;" % etuds[0]["etudid"] + query_string
)
return context.ficheEtud(etudid=etuds[0]["etudid"], REQUEST=REQUEST)
if len(etuds) > 0:
# Choix dans la liste des résultats:
H.append(
H = [
context.sco_header(
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>"""
% (len(etuds), expnom)
)
H.append(
% (len(etuds), expnom),
form_search_etud(
context,
dest_url=dest_url,
parameters=parameters,
parameters_keys=parameters_keys,
REQUEST=REQUEST,
title="Autre recherche",
)
)
),
]
if len(etuds) > 0:
# Choix dans la liste des résultats:
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["inscription_target"] = target
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
@ -185,34 +166,16 @@ def search_etud_in_dept(
form_search_etud(
context,
dest_url=dest_url,
parameters=parameters,
parameters_keys=parameters_keys,
REQUEST=REQUEST,
title="Autre recherche",
)
)
else:
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
add_headers = True
no_side_bar = False
H.append(
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant</p>"""
)
if add_headers:
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)
return "\n".join(H) + context.sco_footer(REQUEST)
# Was chercheEtudsInfo()
@ -268,14 +231,14 @@ def search_etud_by_name(context, term, REQUEST=None):
else:
r = ndb.SimpleDictFetch(
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 + "%"},
)
data = [
{
"label": "%s %s" % (x["nom"], scolars.format_prenom(x["prenom"])),
"value": x["nom"],
"value": x["etudid"],
}
for x in r
]
@ -294,7 +257,7 @@ def form_search_etud_in_accessible_depts(context, REQUEST):
return ""
return """<form action="table_etud_in_accessible_depts" method="POST">
<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">
<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'
valuename: 'etape_apo' ou 'responsable_id'
"""
if not "etapes" in sem:
if not fieldname in sem:
return
# uniquify
values = set([str(x) for x in sem[fieldname]])
cnx = context.GetDBConnexion(autocommit=False)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
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",
{"formsemestre_id": sem["formsemestre_id"]},
)
for item in sem[fieldname]:
for item in values:
if item:
cursor.execute(
"INSERT INTO "
@ -332,7 +335,7 @@ def _write_formsemestre_aux(context, sem, fieldname, valuename):
+ ") VALUES (%(formsemestre_id)s, %("
+ valuename
+ ")s)",
{"formsemestre_id": sem["formsemestre_id"], valuename: str(item)},
{"formsemestre_id": sem["formsemestre_id"], valuename: item},
)
except:
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"""
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
dest_url = (
context.NotesURL() + "formsemestre_status?formsemestre_id=%s" % formsemestre_id
context.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
H = [
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"
% (module_id, moduleimpl_id, etudids)
)
context.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etudids, REQUEST=REQUEST
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
context,
moduleimpl_id,
formsemestre_id,
etudids,
REQUEST=REQUEST,
)
msg += [
"inscription de %d étudiants au module %s"
@ -1195,7 +1199,7 @@ def _reassociate_moduleimpls(
)
for mod in modimpls:
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:
events = scolars.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
for e in events:
@ -1261,7 +1265,7 @@ def formsemestre_delete(context, formsemestre_id, REQUEST=None):
elif tf[0] == -1: # cancel
return REQUEST.RESPONSE.redirect(
context.NotesURL()
+ "formsemestre_status?formsemestre_id="
+ "/formsemestre_status?formsemestre_id="
+ formsemestre_id
)
else:

View File

@ -505,10 +505,7 @@ def formsemestre_page_title(context, REQUEST):
def fill_formsemestre(context, sem, REQUEST=None):
"""Add some useful fields to help display formsemestres"""
# Notes URL
notes_url = context.absolute_url()
if "/Notes" not in notes_url:
notes_url += "/Notes"
notes_url = context.NotesURL()
sem["notes_url"] = notes_url
formsemestre_id = sem["formsemestre_id"]
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)
elif tf[0] == -1:
return REQUEST.RESPONSE.redirect(
context.ScoURL()
+ "/Notes/formsemestre_status?formsemestre_id="
context.NotesURL()
+ "/formsemestre_status?formsemestre_id="
+ formsemestre_id
)
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"])
return REQUEST.RESPONSE.redirect(
context.ScoURL()
+ "/Notes/formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
context.NotesURL()
+ "/formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
% (etudid, formsemestre_id)
)

View File

@ -578,6 +578,7 @@ def groups_table(
else:
filename = "etudiants_%s" % groups_infos.groups_filename
prefs = context.get_preferences(groups_infos.formsemestre_id)
tab = GenTable(
rows=groups_infos.members,
columns_ids=columns_ids,
@ -591,8 +592,9 @@ def groups_table(
html_class="table_leftalign table_listegroupe",
xml_outer_tag="group_list",
xml_row_tag="etud",
text_fields_separator=",", # pour csvmoodle
preferences=context.get_preferences(groups_infos.formsemestre_id),
text_fields_separator=prefs["moodle_csv_separator"],
text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs,
)
#
if format == "html":
@ -672,7 +674,10 @@ def groups_table(
% (tab.base_url,),
'<li><a class="stdlink" href="%s&amp;format=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
% (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,
]
)
@ -784,9 +789,7 @@ def groups_table(
context.Notes, etud, groups_infos.formsemestre_id
)
m["parcours"] = Se.get_parcours_descr()
m["codeparcours"], decisions_jury = sco_report.get_codeparcoursetud(
context.Notes, etud
)
m["codeparcours"], _ = sco_report.get_codeparcoursetud(context.Notes, etud)
def dicttakestr(d, keys):
r = []
@ -933,7 +936,7 @@ def form_choix_saisie_semaine(context, groups_infos, REQUEST=None):
DateJour = time.strftime("%d/%m/%Y")
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('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
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:
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
)
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
moodle_sem_name = sem["session_id"]
columns_ids = ("email", "semestre_groupe")
T = []
for partition_id in partitions_etud_groups:
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)
T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
# Make table
prefs = context.get_preferences(formsemestre_id)
tab = GenTable(
rows=T,
columns_ids=("email", "semestre_groupe"),
filename=moodle_sem_name + "-moodle",
text_fields_separator=",",
preferences=context.get_preferences(formsemestre_id),
titles={x: x for x in columns_ids},
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)

View File

@ -34,6 +34,7 @@ import sco_utils as scu
from sco_utils import (
EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
)
from sco_permissions import ScoEtudInscrit, ScoAbsChange
from notes_log import log
@ -259,9 +260,10 @@ def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=No
ScoAbsChange, context
) and sco_formsemestre.sem_est_courant(context, sem):
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
group_id = sco_groups.get_default_group(context, formsemestre_id)
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>'
% (formsemestre_id, moduleimpl_id, datelundi)
'<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, group_id)
)
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,
select_first_partition=True,
)
if eval["evaluation_type"] == EVALUATION_RATTRAPAGE:
tr_class = "mievr_rattr"
if eval["evaluation_type"] in (EVALUATION_RATTRAPAGE, EVALUATION_SESSION2):
tr_class = "mievr mievr_rattr"
else:
tr_class = "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)
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"]:
H.append(
"""<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"
authuser = REQUEST.AUTHENTICATED_USER
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)
etuds = scolars.etudident_list(cnx, args)
if not etuds:

View File

@ -168,7 +168,7 @@ PREF_CATEGORIES = (
),
(
"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"}),
("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}),
@ -1637,6 +1637,28 @@ Année scolaire: %(anneescolaire)s
"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
(
"NomResponsablePE",
@ -2040,7 +2062,7 @@ function set_global_pref(el, pref_name) {
)
dest_url = (
self.context.NotesURL()
+ "formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + self.context.sco_footer(REQUEST)

View File

@ -824,10 +824,10 @@ def _pvjury_pdf_type(
# Signature du directeur
objects += sco_pdf.makeParas(
"""<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("DirectorTitle", formsemestre_id) or "",
),
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
# Ajoute rangs dans groupes seulement si CSV ou XLS
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
)
@ -758,7 +758,9 @@ def make_formsemestre_recapcomplet(
H.append("</table>")
return "\n".join(H), "", "html"
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(" ", "_")
date = time.strftime("%d-%m-%Y")
filename = "notes_modules-%s-%s.csv" % (semname, date)

View File

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

View File

@ -9,9 +9,10 @@
# le département via l'interface web (Zope)
usage() {
echo "Usage: $0 [-r] dept [script...]"
echo "Usage: $0 [-h] [-r] [-x] dept [script...]"
echo "Lance un environnement interactif python/ScoDoc"
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
}
@ -20,24 +21,38 @@ cd /opt/scodoc/Products/ScoDoc || exit 2
source config/config.sh
source config/utils.sh
if [ $# -lt 1 ]
then
usage
fi
RECREATE_DEPT=0
PYTHON_INTERACTIVE="-i"
if [ "$1" = "-r" ]
then
while [ -n "$1" ]; do
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
recreate_dept=1
else
recreate_dept=0
fi
done
DEPT="$1"
shift
if [ "$recreate_dept" = 1 ]
if [ "$RECREATE_DEPT" = 1 ]
then
cfg_pathname="${SCODOC_VAR_DIR}/config/depts/$DEPT".cfg
if [ -e "$cfg_pathname" ]
@ -48,13 +63,17 @@ then
# systemctl start scodoc
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 "$@"
do
cmd="${cmd}exec(open(\"${f}\").read());"
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
# 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 sco_utils
import sco_abs
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
# --- Création d'étudiants
@ -61,12 +70,65 @@ e = G.create_evaluation(
coefficient=1.0,
)
# --- Saisie notes
# --- Saisie toutes les notes de l'évaluation
for etud in etuds:
nb_changed, nb_suppress, existing_decisions = G.create_note(
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
etudid = etuds[0]["etudid"]
@ -91,3 +153,27 @@ _ = sco_abs_views.doJustifAbsence(
a = sco_abs.getAbsSemEtud(context.Absences, sem, etudid)
assert a.CountAbs() == 3
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:
"""
import scotests.sco_fake_gen as sco_fake_gen # pylint: disable=import-error
# 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
import scotests.sco_fake_gen as sco_fake_gen
import sco_utils as scu
import sco_moduleimpl

View File

@ -10,13 +10,15 @@ Utiliser comme:
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 sco_utils
import sco_codes_parcours
import sco_modalites
G = sco_fake_gen.ScoFake(context.Notes) # pylint: disable=undefined-variable
G = sco_fake_gen.ScoFake(context.Notes)
G.verbose = False
# --- Création d'étudiants

View File

@ -12,16 +12,24 @@ Utiliser comme:
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 sco_utils
import sco_bulletins
G = sco_fake_gen.ScoFake(context.Notes) # pylint: disable=undefined-variable
G = sco_fake_gen.ScoFake(context.Notes)
G.verbose = False
nb_etuds = 10
# --- 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
form, ue_list, mod_list = G.setup_formation(
nb_semestre=1, titre="Essai 1", acronyme="ESS01"
@ -59,3 +67,35 @@ print(bul["moy_gen"])
assert bul["moy_gen"] == "NA"
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;
}
span.mievr_rattr {
display: inline-block;
font-weight: bold;
color: blue;
font-size: 80%;
color: white;
background-color: orangered;
margin-left: 2em;
margin-top: 1px;
margin-bottom: 2px;;
border: 1px solid red;
padding: 1px 3px 1px 3px;
}

View File

@ -30,10 +30,9 @@ function ajaxFunction(mod, etudid, dat){
}
ajaxRequest.open("POST", "doSignaleAbsenceGrSemestre", true);
ajaxRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
oForm = document.forms[0];
oSelectOne = oForm.elements["moduleimpl_id"];
index = oSelectOne.selectedIndex;
modul_id = oSelectOne.options[index].value;
var oSelectOne = $("#abs_form")[0].elements["moduleimpl_id"];
var index = oSelectOne.selectedIndex;
var modul_id = oSelectOne.options[index].value;
if (mod == 'add') {
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
source: "search_etud_by_name",
select: function (event, ui) {
$("#in-expnom").val(ui.item.value);
$("#form-chercheetud").submit();
}
});