diff --git a/ZNotes.py b/ZNotes.py old mode 100644 new mode 100755 index c1d30eba0..ff667fd30 --- a/ZNotes.py +++ b/ZNotes.py @@ -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 diff --git a/sco_preferences.py b/sco_preferences.py old mode 100644 new mode 100755 index 888696938..94cfd9cd3 --- a/sco_preferences.py +++ b/sco_preferences.py @@ -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]) diff --git a/sco_saisie_notes.py b/sco_saisie_notes.py index 5309ee9cd..7b5b6cfd6 100644 --- a/sco_saisie_notes.py +++ b/sco_saisie_notes.py @@ -921,27 +921,37 @@ def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None): H.append("""
""") H.append(sco_groups_view.form_groups_choice(context, groups_infos)) H.append(' | ')
+
+ # 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,
)
diff --git a/sco_saisie_notes_moodle.py b/sco_saisie_notes_moodle.py
new file mode 100644
index 000000000..563256506
--- /dev/null
+++ b/sco_saisie_notes_moodle.py
@@ -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("–")
+ 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)
+ + "Modification des notes impossible pour %s" % authusername + + """(vérifiez que le semestre n'est pas verrouillé et que vous + avez l'autorisation d'effectuer cette opération) + + """ + % 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 + ), + """Import des notes depuis Moodle""", + ] + H.append( + """
+
+
+ """
+ % (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(
+ """
+ " %s " n'est pas un nom abrégé de cours connu sur ce Moodle + """ + % course_short_name + ) + else: + list_evaluations, bareme = get_evaluation_list( + moodle_serveur, moodle_token, course_short_name + ) + + msg = "Remarque : Si l'étudiant n'a pas de note sur Moodle la note dans cette évaluation sera supprimée " + if len(list_evaluations) > 5: + msg += "ATTENTION : Le chargement des notes peut prendre beaucoup de temps " + H.append( + """
+ liste des évaluations du cours %s
+
+ %s
+
+ """
+ % (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(
+ """Notes chargées. + + Revenir au tableau de bord du module + + Charger d'autres notes dans cette évaluation + """ + % E + ) + else: + H.append( + """Notes non chargées ! """ + + updiag[1] + ) + H.append( + """ + """ + % E + ) + # + H.append("""Autres opérations
%d notes changées (%d sans notes, %d absents, %d note supprimées) " + % (nb_changed, len(withoutnotes), len(absents), nb_suppress) + ) + if existing_decisions: + msg += """Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification ! """ + # msg += '' + str(notes) # debug + return 1, msg + + except InvalidNoteValue: + if diag: + msg = ( + '
(pas de notes modifiées) " |