intégration (non testée) du code de P.-A. J.

This commit is contained in:
Emmanuel Viennet 2021-05-18 23:47:50 +02:00
parent e943e7f283
commit 06d83cc691
4 changed files with 526 additions and 18 deletions

4
ZNotes.py Normal file → Executable file
View File

@ -90,6 +90,7 @@ import sco_compute_moy
import sco_recapcomplet
import sco_liste_notes
import sco_saisie_notes
import sco_saisie_notes_moodle
import sco_placement
import sco_undo_notes
import sco_formations
@ -2460,6 +2461,9 @@ class ZNotes(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Impl
security.declareProtected(ScoEnsView, "saisie_notes_tableur")
saisie_notes_tableur = sco_saisie_notes.saisie_notes_tableur
security.declareProtected(ScoEnsView, "import_eval_notes_from_moodle")
import_eval_notes_from_moodle = sco_saisie_notes_moodle.import_from_moodle
security.declareProtected(ScoEnsView, "feuille_saisie_notes")
feuille_saisie_notes = sco_saisie_notes.feuille_saisie_notes

20
sco_preferences.py Normal file → Executable file
View File

@ -1744,6 +1744,26 @@ Année scolaire: %(anneescolaire)s
"category": "edt",
},
),
(
"moodle_server_url",
{
"title": "URL pour accéder au service web de Moodle",
"initvalue": "",
"explanation": "cette URL est du type https://nom_du_serveur/moodle/webservice/rest/server.php",
"size": 50,
"category": "portal",
},
),
(
"moodle_ws_token",
{
"title": "jeton d'identification pour le service web de Moodle",
"initvalue": "",
"explanation": "ce jeton est créé par moodle dans la gestion du plugin service web: consultez l'administrateur de Moodle",
"size": 30,
"category": "portal",
},
),
)
PREFS_NAMES = set([x[0] for x in PREFS])

View File

@ -921,27 +921,37 @@ def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
H.append("""<div id="group-tabs"><table><tr><td>""")
H.append(sco_groups_view.form_groups_choice(context, groups_infos))
H.append('</td><td style="padding-left: 35px;">')
# Pour savoir si l'interface Moodle est configurée:
moodle_token = context.get_preference("moodle_ws_token", formsemestre_id)
moodle_serveur = context.get_preference("moodle_server_url", formsemestre_id)
menu_items = [
{
"title": "Saisie par fichier tableur",
"id": "menu_saisie_tableur",
"url": "/saisie_notes_tableur?evaluation_id=%s&%s"
% (E["evaluation_id"], groups_infos.groups_query_args),
},
{
"id": "import_moodle",
"title": "Importer les notes depuis Moodle",
"url": "/import_from_moodle?evaluation_id=%s" % (E["evaluation_id"],),
"enabled": moodle_serveur and moodle_token,
},
{
"title": "Voir toutes les notes du module",
"url": "/evaluation_listenotes?moduleimpl_id=%s" % E["moduleimpl_id"],
},
{
"title": "Effacer toutes les notes de cette évaluation",
"url": "/evaluation_suppress_alln?evaluation_id=%s" % (E["evaluation_id"],),
},
]
H.append(
htmlutils.make_menu(
"Autres opérations",
[
{
"title": "Saisie par fichier tableur",
"id": "menu_saisie_tableur",
"url": "/saisie_notes_tableur?evaluation_id=%s&%s"
% (E["evaluation_id"], groups_infos.groups_query_args),
},
{
"title": "Voir toutes les notes du module",
"url": "/evaluation_listenotes?moduleimpl_id=%s"
% E["moduleimpl_id"],
},
{
"title": "Effacer toutes les notes de cette évaluation",
"url": "/evaluation_suppress_alln?evaluation_id=%s"
% (E["evaluation_id"],),
},
],
menu_items,
base_url=context.absolute_url(),
alone=True,
)

474
sco_saisie_notes_moodle.py Normal file
View File

@ -0,0 +1,474 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Importation de notes depuis Moodle
Contrib. Pierre-Alain Jacquot, mai 2021
"""
# QUESTION: un (long) commentaire expliquant le principe de base de ce module
import requests
import re
def cleanhtml(raw_html):
cleanr = re.compile("<.*?>")
cleantext = re.sub(cleanr, " ", raw_html)
cleantext = cleantext.strip()
cleantext = cleantext.encode("utf-8")
return cleantext
#
def get_moodle_course_id(moodle_serveur, moodle_token, courses_short_name):
param_cours = {
"wstoken": moodle_token,
"moodlewsrestformat": "json",
"wsfunction": "core_course_get_courses_by_field",
"field": "shortname",
"value": courses_short_name,
}
try:
r = requests.post(url=moodle_serveur, data=param_cours).json()
except ValueError:
raise ValueError("Erreur de connexion vérifiez l'URL de Moodle")
if "exception" in r:
raise ValueError(
"Connexion au service web de Moodle impossible %s : Vérifiez votre paramétrage"
% r["message"]
)
if len(r["courses"]) == 0:
courseid = 0
else:
courseid = r["courses"][0]["id"]
return courseid
def has_student_role(user):
"""
Retourne vrai si l'utilisateur a le role 5 : «etudiant» ou «student» dans le cours
i.e. il a des notes
"""
# QUESTION: ce nombre "5" est une constante universelle dans Moodle ?
est_etudiant = False
for role in user["roles"]:
# print "role : "+str(role['roleid'] )
if role["roleid"] == 5:
# print "est_etudiant "+str(role['roleid'] )
est_etudiant = 1
return est_etudiant
def get_etudiants_from_course(moodle_serveur, moodle_token, courses_short_name):
"""
Extrait la liste des étudiants des utilisateurs inscrit dans le cours.
Cette liste contient les informations suivante :
- id moodle
- email
- idnumber (numéro d'identification) s'il existe : celui ci peut servir a stocker le EID ou le nip
"""
courseid = get_moodle_course_id(moodle_serveur, moodle_token, courses_short_name)
param_cours = {
"wstoken": moodle_token,
"moodlewsrestformat": "json",
"wsfunction": "core_enrol_get_enrolled_users",
"options[0][name]": "onlyactive",
"options[0][value]": "1",
"options[1][name]": "userfields",
"options[1][value]": "id,email,idnumber,roles",
"courseid": courseid,
}
r = requests.post(url=moodle_serveur, data=param_cours).json()
etudiants = [user for user in r if has_student_role(user)]
# le role n'est plus une information pertinente : suppression
for etudiant in etudiants:
del etudiant["roles"]
etudiant["email"] = etudiant["email"].encode("ascii").lower()
return etudiants
def get_evaluation_list(moodle_serveur, moodle_token, courses_short_name):
"""
Récupère la liste des evaluations du cours Moodle
On recherche les notes d'un seul etudiant pour gagner du temps.
"""
# QUESTION: documenter les valeurs résultats
etudiants = get_etudiants_from_course(
moodle_serveur, moodle_token, courses_short_name
)
a_userid = etudiants[0]["id"]
courseid = get_moodle_course_id(moodle_serveur, moodle_token, courses_short_name)
param_notes = {
"wstoken": moodle_token,
"moodlewsrestformat": "json",
"wsfunction": "gradereport_user_get_grades_table",
"courseid": courseid,
"userid": a_userid,
}
r = requests.post(url=moodle_serveur, data=param_notes)
notes = r.json()
bareme = {}
liste_evals = []
for etu_notes in notes["tables"][0]["tabledata"]:
if "grade" in etu_notes:
nom_eval = cleanhtml(etu_notes["itemname"]["content"])
liste_evals.append(nom_eval)
bareme_min, bareme_max = etu_notes["range"]["content"].split("&ndash;")
bareme[nom_eval] = {
"min": float(bareme_min.replace(",", ".")),
"max": float(bareme_max.replace(",", ".")),
}
return liste_evals, bareme
def get_grades_from_moodle_course(moodle_serveur, moodle_token, courses_short_name):
"""
Récupère toutes les notes du cours et les remet en forme
dans un dictionnaire indexé par le userid de moodle
{userid: { nom_eval:note, ...}}
"""
courseid = get_moodle_course_id(moodle_serveur, moodle_token, courses_short_name)
param_notes = {
"wstoken": moodle_token,
"moodlewsrestformat": "json",
"wsfunction": "gradereport_user_get_grades_table",
"courseid": courseid,
}
r = requests.post(url=moodle_serveur, data=param_notes)
notes = r.json()
notes_evals = {}
for etudiant in notes["tables"]:
# remise en forme des notes dans un dictionnaire indexe par le nom de la note
tab_notes = {}
for etu_notes in etudiant["tabledata"]:
if "grade" in etu_notes:
if etu_notes["grade"]["content"] == "-":
etu_notes["grade"]["content"] = "SUPR"
tab_notes[cleanhtml(etu_notes["itemname"]["content"])] = etu_notes[
"grade"
]["content"]
notes_evals[etudiant["userid"]] = tab_notes
return notes_evals
# QUESTION: j'ai l'impression qu'il y a trop de code en commun entre cette fonction et
# sco_saisie_notes._form_saisie_notes
# QUESTION: manque vérification de la présence de décisions de jury ?? (qui devrait bloquer l'import amha)
def import_eval_notes_from_moodle(context, evaluation_id, group_ids=[], REQUEST=None):
"""Récuperation des notes sur moodle"""
moodle_token = context.get_preference("moodle_ws_token", formsemestre_id)
moodle_serveur = context.get_preference("moodle_server_url", formsemestre_id)
# Désactive si l'interface n'est pas configurée:
if not moodle_serveur or not moodle_token:
return "Interface Moodle non paramétrée !"
authuser = REQUEST.AUTHENTICATED_USER
evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
E = evals[0]
M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
# M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
formsemestre_id = M["formsemestre_id"]
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
return (
context.sco_header(REQUEST)
+ "<h2>Modification des notes impossible pour %s</h2>" % authusername
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
"""
% E["moduleimpl_id"]
+ context.sco_footer(REQUEST)
)
if E["description"]:
page_title = 'Saisie des notes de "%s"' % E["description"]
else:
page_title = "Saisie des notes"
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
context,
group_ids=group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
etat=None,
REQUEST=REQUEST,
)
H = [
context.sco_header(
REQUEST,
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
context, evaluation_id=evaluation_id, REQUEST=REQUEST
),
"""<span class="eval_title">Import des notes depuis Moodle</span>""",
]
H.append(
"""<div class="saisienote_etape1">
<form action="import_from_moodle" method="post">
Nom abrégé du cours sur Moodle <input type="text" size="20" name="course_short_name"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="%s"/></form>
</div>
"""
% (evaluation_id)
)
if "course_short_name" in REQUEST.form:
course_short_name = REQUEST.form["course_short_name"]
courseid = get_moodle_course_id(moodle_serveur, moodle_token, course_short_name)
if courseid == 0:
H.append(
"""
<p class="warning">" %s " n'est pas un nom abrégé de cours connu sur ce Moodle</p>
"""
% course_short_name
)
else:
list_evaluations, bareme = get_evaluation_list(
moodle_serveur, moodle_token, course_short_name
)
msg = "<p> <b>Remarque</b> : Si l'étudiant n'a pas de note sur Moodle la note dans cette évaluation sera supprimée</p>"
if len(list_evaluations) > 5:
msg += "<p>ATTENTION : Le chargement des notes peut prendre beaucoup de temps </p>"
H.append(
"""<div class="saisienote_etape2">
<span class="eval_title"> liste des évaluations du cours %s </span>
<form action="import_from_moodle" method="post">
"""
% course_short_name
)
pbplage = False
for ev in range(0, len(list_evaluations)):
# verification du bareme
marque = ""
if (
bareme[list_evaluations[ev]]["min"] != scu.NOTES_MIN
or bareme[list_evaluations[ev]]["max"] != E["note_max"]
):
marque = (
"""<span class="redboldtext" >note entre %.2f et %.2f</span>"""
% (
bareme[list_evaluations[ev]]["min"],
bareme[list_evaluations[ev]]["max"],
)
)
pbplage = True
H.append(
"""
<input type="radio" name="num_eval" value=%d > %s %s<br>
"""
% (ev, list_evaluations[ev], marque)
)
if pbplage:
msg += (
'<p><span class="redboldtext">ATTENTION </span>: certaines évaluations ne sont pas dans la plage %.2f - %.2f il faudrait modifier cette cette évaluation pour pouvoir les importer !</p> '
% (scu.NOTES_MIN, E["note_max"])
)
H.append(
"""
<input type="submit" value="OK"/>
<input type="hidden" name="course_short_name" value="%s"/>
<input type="hidden" name="evaluation_id" value="%s"/></form>
%s
</div>
"""
% (course_short_name, evaluation_id, msg)
)
if "num_eval" in REQUEST.form:
nom_eval = list_evaluations[int(REQUEST.form["num_eval"])]
etudiant_info = get_etudiants_from_course(
moodle_serveur, moodle_token, course_short_name
)
moodle_notes = get_grades_from_moodle_course(
moodle_serveur, moodle_token, course_short_name
)
email_id = {}
nip_id = {}
for etu in groups_infos.members:
email = str(etu["email"]).lower()
email_id[email] = etu["etudid"]
nip_id[etu["code_nip"]] = etu["etudid"]
nouvelles_notes = []
for etu in etudiant_info:
# La présence d'un code nip est prioritaire sur l'adresse mail
if "idnumber" in etu:
nouvelles_notes.append(
(nip_id[etu["idnumber"]], moodle_notes[etu["id"]][nom_eval])
)
elif etu["email"] in email_id:
email = str(etu["email"]).lower()
nouvelles_notes.append(
(email_id[email], moodle_notes[etu["id"]][nom_eval])
)
updiag = do_moodle_import(
context,
REQUEST,
nouvelles_notes,
"Moodle/%s/%s" % (course_short_name, nom_eval),
)
# updiag=[0,"en test: merci de patienter"]
if updiag[0]:
H.append(updiag[1])
H.append(
"""<p>Notes chargées.&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">
Revenir au tableau de bord du module</a>
&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
</p>"""
% E
)
else:
H.append(
"""<p class="redboldtext">Notes non chargées !</p>"""
+ updiag[1]
)
H.append(
"""
<p><a class="stdlink" href="saisie_notes_tableur?evaluation_id=%(evaluation_id)s">
Reprendre</a>
</p>"""
% E
)
#
H.append("""<h3>Autres opérations</h3><ul>""")
if can_edit_notes(
context, REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
):
H.append(
"""
<li>
<form action="do_evaluation_set_missing" method="get">
Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="%s"/>
<em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
</form>
</li>
<li><a class="stdlink" href="evaluation_suppress_alln?evaluation_id=%s">Effacer toutes les notes de cette évaluation
</a> (ceci permet ensuite de supprimer l'évaluation si besoin)
</li>"""
% (evaluation_id, evaluation_id)
) #'
H.append(
"""<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a></li>
<li><a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Revenir au formulaire de saisie</a></li>
</ul>"""
% E
)
H.append(context.sco_footer(REQUEST))
return "\n".join(H)
# QUESTION: Beaucoup de code dupliqué de sco-saisie_notes => maintenance trop difficile à terme
# => refactoring nécessaire
def do_moodle_import(context, REQUEST, notes, comment):
"""import moodle"""
authuser = REQUEST.AUTHENTICATED_USER
evaluation_id = REQUEST.form["evaluation_id"]
# comment = "Importée de moodle"#REQUEST.form['comment']
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
# M = context.do_moduleimpl_withmodule_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
# Check access
# (admin, respformation, and responsable_id)
# if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
# XXX imaginer un redirect + msg erreur
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
#
diag = []
try:
# -- check values
L, invalids, withoutnotes, absents, tosuppress = _check_notes(
notes, E, M["module"]
)
if len(invalids):
diag.append(
"Erreur: Moodle fournit %d notes invalides vérifiez que la note maximale est bien la même sur scodoc et sur Moodle</p>"
% len(invalids)
)
if len(invalids) < 25:
etudsnames = [
context.getEtudInfo(etudid=etudid, filled=True)[0]["nomprenom"]
for etudid in invalids
]
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue()
else:
nb_changed, nb_suppress, existing_decisions = _notes_add(
context, authuser, evaluation_id, L, comment
)
# news
cnx = context.GetDBConnexion()
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
# M = context.do_moduleimpl_list( args={ 'moduleimpl_id':E['moduleimpl_id'] } )[0]
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
sco_news.add(
context,
REQUEST,
typ=NEWS_NOTE,
object=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
msg = (
"<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
% (nb_changed, len(withoutnotes), len(absents), nb_suppress)
)
if existing_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
# msg += '<p>' + str(notes) # debug
return 1, msg
except InvalidNoteValue:
if diag:
msg = (
'<ul class="tf-msg"><li class="tf_msg">'
+ '</li><li class="tf_msg">'.join(diag)
+ "</li></ul>"
)
else:
msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
return 0, msg + "<p>(pas de notes modifiées)</p>"