From e56a97eaf6a06c87060a6021f9940d3e51c83c9b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 19 Oct 2021 15:57:28 +0200 Subject: [PATCH 01/12] Aligne max upload de Flask et nginx (16Mo) --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index f9f359b54..672832db4 100755 --- a/config.py +++ b/config.py @@ -33,7 +33,7 @@ class Config: # evite confusion avec le log nginx scodoc_error.log: SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log") # - MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # Flask uploads + MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Flask uploads (16Mo, en ligne avec nginx) # STATIC_URL_PATH = "/ScoDoc/static" # static_folder = "stat" From 66dbec86bf065650ced12293192c54d7d4505618 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 16:47:41 +0200 Subject: [PATCH 02/12] Add cli: photos-import-files --- app/scodoc/htmlutils.py | 65 ++++++++++++- app/scodoc/sco_archives_etud.py | 45 ++++++--- app/scodoc/sco_trombino.py | 95 ++++++++----------- app/templates/scolar/photos_import_files.html | 39 ++++++++ app/templates/scolar/photos_import_files.txt | 23 +++++ scodoc.py | 45 +++++++++ 6 files changed, 243 insertions(+), 69 deletions(-) create mode 100644 app/templates/scolar/photos_import_files.html create mode 100755 app/templates/scolar/photos_import_files.txt diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index 68e835c3a..c06e86e91 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -27,9 +27,12 @@ """Various HTML generation functions """ +from html.parser import HTMLParser +from html.entities import name2codepoint +import re + from flask import g, url_for -import app.scodoc.sco_utils as scu from . import listhistogram @@ -130,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False): if alone: H.append("") return "".join(H) + + +""" +HTML <-> text conversions. +http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python +""" + + +class _HTMLToText(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self._buf = [] + self.hide_output = False + + def handle_starttag(self, tag, attrs): + if tag in ("p", "br") and not self.hide_output: + self._buf.append("\n") + elif tag in ("script", "style"): + self.hide_output = True + + def handle_startendtag(self, tag, attrs): + if tag == "br": + self._buf.append("\n") + + def handle_endtag(self, tag): + if tag == "p": + self._buf.append("\n") + elif tag in ("script", "style"): + self.hide_output = False + + def handle_data(self, text): + if text and not self.hide_output: + self._buf.append(re.sub(r"\s+", " ", text)) + + def handle_entityref(self, name): + if name in name2codepoint and not self.hide_output: + c = chr(name2codepoint[name]) + self._buf.append(c) + + def handle_charref(self, name): + if not self.hide_output: + n = int(name[1:], 16) if name.startswith("x") else int(name) + self._buf.append(chr(n)) + + def get_text(self): + return re.sub(r" +", " ", "".join(self._buf)) + + +def html_to_text(html): + """ + Given a piece of HTML, return the plain text it contains. + This handles entities and char refs, but not javascript and stylesheets. + """ + parser = _HTMLToText() + try: + parser.feed(html) + parser.close() + except: # HTMLParseError: No good replacement? + pass + return parser.get_text() diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 1805c085f..3a47995a8 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -30,7 +30,8 @@ les dossiers d'admission et autres pièces utiles. """ import flask -from flask import url_for, g, request +from flask import url_for, render_template +from flask import g, request from flask_login import current_user import app.scodoc.sco_utils as scu @@ -328,9 +329,9 @@ def etudarchive_import_files_form(group_id): if tf[0] == 0: return "\n".join(H) + tf[1] + "" + F - elif tf[0] == -1: - # retrouve le semestre à partir du groupe: - group = sco_groups.get_group(group_id) + # retrouve le semestre à partir du groupe: + group = sco_groups.get_group(group_id) + if tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_status", @@ -340,21 +341,41 @@ def etudarchive_import_files_form(group_id): ) else: return etudarchive_import_files( - group_id=tf[2]["group_id"], + formsemestre_id=group["formsemestre_id"], xlsfile=tf[2]["xlsfile"], zipfile=tf[2]["zipfile"], description=tf[2]["description"], ) -def etudarchive_import_files(group_id=None, xlsfile=None, zipfile=None, description=""): +def etudarchive_import_files( + formsemestre_id=None, xlsfile=None, zipfile=None, description="" +): + "Importe des fichiers" + def callback(etud, data, filename): _store_etud_file_to_new_archive(etud["etudid"], data, filename, description) - filename_title = "fichier_a_charger" - page_title = "Téléchargement de fichiers associés aux étudiants" - # Utilise la fontion au depart developpee pour les photos - r = sco_trombino.zip_excel_import_files( - xlsfile, zipfile, callback, filename_title, page_title + # Utilise la fontion developpée au depart pour les photos + ( + ignored_zipfiles, + unmatched_files, + stored_etud_filename, + ) = sco_trombino.zip_excel_import_files( + xlsfile=xlsfile, + zipfile=zipfile, + callback=callback, + filename_title="fichier_a_charger", + ) + return render_template( + "scolar/photos_import_files.html", + page_title="Téléchargement de fichiers associés aux étudiants", + ignored_zipfiles=ignored_zipfiles, + unmatched_files=unmatched_files, + stored_etud_filename=stored_etud_filename, + next_page=url_for( + "scolar.groups_view", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ), ) - return r + html_sco_header.sco_footer() diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 7b38f6a2d..9441438fa 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -30,6 +30,7 @@ import io from zipfile import ZipFile, BadZipfile +from flask.templating import render_template import reportlab from reportlab.lib.units import cm, mm from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY @@ -531,25 +532,33 @@ def photos_import_files_form(group_ids=[]): elif tf[0] == -1: return flask.redirect(back_url) else: - return photos_import_files( - group_ids=tf[2]["group_ids"], + + def callback(etud, data, filename): + sco_photos.store_photo(etud, data) + + ( + ignored_zipfiles, + unmatched_files, + stored_etud_filename, + ) = zip_excel_import_files( xlsfile=tf[2]["xlsfile"], zipfile=tf[2]["zipfile"], + callback=callback, + filename_title="fichier_photo", + ) + return render_template( + "scolar/photos_import_files.html", + page_title="Téléchargement des photos des étudiants", + ignored_zipfiles=ignored_zipfiles, + unmatched_files=unmatched_files, + stored_etud_filename=stored_etud_filename, + next_page=url_for( + "scolar.groups_view", + scodoc_dept=g.scodoc_dept, + formsemestre_id=groups_infos.formsemestre_id, + curtab="tab-photos", + ), ) - - -def photos_import_files(group_ids=[], xlsfile=None, zipfile=None): - """Importation des photos""" - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) - back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args - filename_title = "fichier_photo" - page_title = "Téléchargement des photos des étudiants" - - def callback(etud, data, filename): - sco_photos.store_photo(etud, data) - - zip_excel_import_files(xlsfile, zipfile, callback, filename_title, page_title) - return flask.redirect(back_url + "&head_message=photos%20 importees") def zip_excel_import_files( @@ -557,19 +566,19 @@ def zip_excel_import_files( zipfile=None, callback=None, filename_title="", # doit obligatoirement etre specifié - page_title="", ): """Importation de fichiers à partir d'un excel et d'un zip La fonction callback() - est appelé pour chaque fichier trouvé. + est appelée pour chaque fichier trouvé. + Fonction utilisée pour les photos et les fichiers étudiants (archives). """ # 1- build mapping etudid -> filename exceldata = xlsfile.read() if not exceldata: raise ScoValueError("Fichier excel vide ou invalide") _, data = sco_excel.excel_bytes_to_list(exceldata) - if not data: # probably a bug + if not data: raise ScoValueError("Fichier excel vide !") # on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo') titles = data[0] @@ -591,30 +600,30 @@ def zip_excel_import_files( fn = fn.split("/")[-1] # use only last component, not directories return fn - Filename2Etud = {} # filename : etudid + filename_to_etud = {} # filename : etudid for l in data[1:]: filename = l[filename_idx].strip() if filename: - Filename2Etud[normfilename(filename)] = l[etudid_idx] + filename_to_etud[normfilename(filename)] = l[etudid_idx] # 2- Ouvre le zip et try: z = ZipFile(zipfile) except BadZipfile: - raise ScoValueError("Fichier ZIP incorrect !") + raise ScoValueError("Fichier ZIP incorrect !") from BadZipfile ignored_zipfiles = [] - stored = [] # [ (etud, filename) ] + stored_etud_filename = [] # [ (etud, filename) ] for name in z.namelist(): if len(name) > 4 and name[-1] != "/" and "." in name: data = z.read(name) # match zip filename with name given in excel normname = normfilename(name) - if normname in Filename2Etud: - etudid = Filename2Etud[normname] + if normname in filename_to_etud: + etudid = filename_to_etud[normname] # ok, store photo try: etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - del Filename2Etud[normname] + del filename_to_etud[normname] except: raise ScoValueError("ID étudiant invalide: %s" % etudid) @@ -624,7 +633,7 @@ def zip_excel_import_files( normfilename(name, lowercase=False), ) - stored.append((etud, name)) + stored_etud_filename.append((etud, name)) else: log("zip: zip name %s not in excel !" % name) ignored_zipfiles.append(name) @@ -632,35 +641,9 @@ def zip_excel_import_files( if name[-1] != "/": ignored_zipfiles.append(name) log("zip: ignoring %s" % name) - if Filename2Etud: + if filename_to_etud: # lignes excel non traitées - unmatched_files = list(Filename2Etud.keys()) + unmatched_files = list(filename_to_etud.keys()) else: unmatched_files = [] - # 3- Result page - H = [ - _trombino_html_header(), - """

%s

-

Opération effectuée

- """ - % page_title, - ] - if ignored_zipfiles: - H.append("

Fichiers ignorés dans le zip:

") - if unmatched_files: - H.append( - "

Fichiers indiqués dans feuille mais non trouvés dans le zip:

") - if stored: - H.append("

Fichiers chargés:

") - - return "\n".join(H) + return ignored_zipfiles, unmatched_files, stored_etud_filename diff --git a/app/templates/scolar/photos_import_files.html b/app/templates/scolar/photos_import_files.html new file mode 100644 index 000000000..f4bae574a --- /dev/null +++ b/app/templates/scolar/photos_import_files.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% block app_content %} + +

{{ page_title }}

+

Opération effectuée

+ +{% if ignored_zipfiles %} +

Fichiers ignorés dans le zip:

+ +{% endif %} + +{% if unmatched_files %} +

Fichiers indiqués dans la feuille mais non trouvés dans le zip:

+ +{% endif %} + +{% if stored_etud_filename %} +

Fichiers chargés:

+ +{% endif %} + +
+

Continuer +

+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/scolar/photos_import_files.txt b/app/templates/scolar/photos_import_files.txt new file mode 100755 index 000000000..d9aab53ee --- /dev/null +++ b/app/templates/scolar/photos_import_files.txt @@ -0,0 +1,23 @@ + +Importation des photo effectuée + +{% if ignored_zipfiles %} +# Fichiers ignorés dans le zip: + {% for name in ignored_zipfiles %} + - {{name}} + {% endfor %} +{% endif %} + +{% if unmatched_files %} +# Fichiers indiqués dans la feuille mais non trouvés dans le zip: + {% for name in unmatched_files %} + - {{name}} + {% endfor %} +{% endif %} + +{% if stored_etud_filename %} +# Fichiers chargés: + {% for (etud, name) in stored_etud_filename %} + - {{etud["nomprenom"]}}: {{name}} + {% endfor %} +{% endif %} diff --git a/scodoc.py b/scodoc.py index 818867989..3785dc6ea 100755 --- a/scodoc.py +++ b/scodoc.py @@ -13,6 +13,7 @@ import sys import click import flask from flask.cli import with_appcontext +from flask.templating import render_template from app import create_app, cli, db from app import initialize_scodoc_database @@ -323,6 +324,50 @@ def migrate_scodoc7_dept_archive(dept: str): # migrate-scodoc7-dept-archive tools.migrate_scodoc7_dept_archive(dept) +@app.cli.command() +@click.argument("formsemestre_id", type=click.INT) +@click.argument("xlsfile", type=click.File("rb")) +@click.argument("zipfile", type=click.File("rb")) +def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str): + import app as mapp + from app.scodoc import sco_trombino, sco_photos + from app.scodoc import notesdb as ndb + from flask_login import login_user + from app.auth.models import get_super_admin + + sem = mapp.models.formsemestre.FormSemestre.query.get(formsemestre_id) + if not sem: + sys.stderr.write("photos-import-files: numéro de semestre invalide\n") + return 2 + + with app.test_request_context(): + mapp.set_sco_dept(sem.departement.acronym) + admin_user = get_super_admin() + login_user(admin_user) + + def callback(etud, data, filename): + sco_photos.store_photo(etud, data) + + ( + ignored_zipfiles, + unmatched_files, + stored_etud_filename, + ) = sco_trombino.zip_excel_import_files( + xlsfile=xlsfile, + zipfile=zipfile, + callback=callback, + filename_title="fichier_photo", + ) + print( + render_template( + "scolar/photos_import_files.txt", + ignored_zipfiles=ignored_zipfiles, + unmatched_files=unmatched_files, + stored_etud_filename=stored_etud_filename, + ) + ) + + @app.cli.command() @with_appcontext def clear_cache(): # clear-cache From 0c913dacdcbdba84396a2cb9f1ee096fb9fbb46c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 17:41:38 +0200 Subject: [PATCH 03/12] Fix: ordre des partitions --- app/scodoc/sco_groups.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 7a31b995b..120db6a43 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -806,8 +806,21 @@ def partition_create( ) cnx = ndb.GetDBConnexion() + if numero is None: + numero = ( + ndb.SimpleQuery( + "SELECT MAX(id) FROM partition WHERE formsemestre_id=%(formsemestre_id)s", + {"formsemestre_id": formsemestre_id}, + ).fetchone()[0] + or 0 + ) partition_id = partitionEditor.create( - cnx, {"formsemestre_id": formsemestre_id, "partition_name": partition_name} + cnx, + { + "formsemestre_id": formsemestre_id, + "partition_name": partition_name, + "numero": numero, + }, ) log("createPartition: created partition_id=%s" % partition_id) # @@ -1041,7 +1054,7 @@ def partition_move(partition_id, after=0, redirect=1): others = get_partitions_list(formsemestre_id) if len(others) > 1: pidx = [p["partition_id"] for p in others].index(partition_id) - log("partition_move: after=%s pidx=%s" % (after, pidx)) + # log("partition_move: after=%s pidx=%s" % (after, pidx)) neigh = None # partition to swap with if after == 0 and pidx > 0: neigh = others[pidx - 1] @@ -1049,8 +1062,20 @@ def partition_move(partition_id, after=0, redirect=1): neigh = others[pidx + 1] if neigh: # # swap numero between partition and its neighbor - log("moving partition %s" % partition_id) + # log("moving partition %s" % partition_id) cnx = ndb.GetDBConnexion() + # Si aucun numéro n'a été affecté, le met au minimum + min_numero = ( + ndb.SimpleQuery( + "SELECT MIN(numero) FROM partition WHERE formsemestre_id=%(formsemestre_id)s", + {"formsemestre_id": formsemestre_id}, + ).fetchone()[0] + or 0 + ) + if neigh["numero"] is None: + neigh["numero"] = min_numero - 1 + if partition["numero"] is None: + partition["numero"] = min_numero - 1 - after partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"] partitionEditor.edit(cnx, partition) partitionEditor.edit(cnx, neigh) From f73e720de16c5e381b16706c2bf9291c04aafd40 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 19:11:26 +0200 Subject: [PATCH 04/12] =?UTF-8?q?Fix:=20suppression=20de=20notes=20par=20u?= =?UTF-8?q?n=20enseignant=20non=20privil=C3=A9gi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_saisie_notes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 356ff339d..81fb5bef8 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -388,7 +388,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): ): # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi NotesDB = sco_evaluations.do_evaluation_get_all_notes( - evaluation_id, by_uid=current_user.user_name + evaluation_id, by_uid=current_user.id ) else: raise AccessDenied("Modification des notes impossible pour %s" % current_user) @@ -399,7 +399,10 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): nb_changed, nb_suppress, existing_decisions = _notes_add( current_user, evaluation_id, notes, do_it=False ) - msg = "

Confirmer la suppression des %d notes ?

" % nb_suppress + msg = ( + "

Confirmer la suppression des %d notes ? (peut affecter plusieurs groupes)

" + % nb_suppress + ) if existing_decisions: msg += """

Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !

""" return scu.confirm_dialog( From 92de66f734105946ea96d52ac791434d285f1059 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 21:58:01 +0200 Subject: [PATCH 05/12] Modif liens sidebar. Closes #53 --- app/scodoc/html_sidebar.py | 2 +- app/templates/sidebar_dept.html | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 658bbbea7..ba57c67aa 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission def sidebar_common(): "partie commune à toutes les sidebar" H = [ - f"""ScoDoc 9 + f"""ScoDoc 9
Dépt. {{ prefs["DeptName"] }} -Accueil
- {% if prefs["DeptIntranetURL"] %} - - {{ prefs["DeptIntranetTitle"] }} - {% endif %} -
+Accueil
+{% if prefs["DeptIntranetURL"] %} + + {{ prefs["DeptIntranetTitle"] }} +{% endif %} +
-{# +{# # Entreprises pas encore supporté en ScoDoc8 -#
Entreprises
+#
Entreprises
#} \ No newline at end of file From 280f6cf1c11eb6839300a7cb97ba72ab2daec758 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 22:34:06 +0200 Subject: [PATCH 06/12] Fix etud_info xml quote --- app/scodoc/sco_utils.py | 14 +++++++++++--- app/scodoc/sco_xml.py | 2 +- app/views/scolar.py | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index e5dec56b8..d3acdb3d0 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -572,17 +572,24 @@ def sendJSON(data, attached=False): ) -def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False): +def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True): if type(data) != list: data = [data] # always list-of-dicts if force_outer_xml_tag: data = [{tagname: data}] tagname += "_list" - doc = sco_xml.simple_dictlist2xml(data, tagname=tagname) + doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote) return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached) -def sendResult(data, name=None, format=None, force_outer_xml_tag=True, attached=False): +def sendResult( + data, + name=None, + format=None, + force_outer_xml_tag=True, + attached=False, + quote_xml=True, +): if (format is None) or (format == "html"): return data elif format == "xml": # name is outer tagname @@ -591,6 +598,7 @@ def sendResult(data, name=None, format=None, force_outer_xml_tag=True, attached= tagname=name, force_outer_xml_tag=force_outer_xml_tag, attached=attached, + quote=quote_xml, ) elif format == "json": return sendJSON(data, attached=attached) diff --git a/app/scodoc/sco_xml.py b/app/scodoc/sco_xml.py index 633d27142..ea3c3ee93 100644 --- a/app/scodoc/sco_xml.py +++ b/app/scodoc/sco_xml.py @@ -134,4 +134,4 @@ def xml_to_dicts(element): for child in element.childNodes: if child.nodeType == ELEMENT_NODE: childs.append(xml_to_dicts(child)) - return (element.nodeName, d, childs) \ No newline at end of file + return (element.nodeName, d, childs) diff --git a/app/views/scolar.py b/app/views/scolar.py index 0e06d9246..74e80a1a8 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -363,6 +363,8 @@ def search_etud_by_name(): @scodoc7func def etud_info(etudid=None, format="xml"): "Donne les informations sur un etudiant" + if not format in ("xml", "json"): + raise ScoValueError("format demandé non supporté par cette fonction.") t0 = time.time() args = sco_etud.make_etud_args(etudid=etudid) cnx = ndb.GetDBConnexion() @@ -449,7 +451,9 @@ def etud_info(etudid=None, format="xml"): ) log("etud_info (%gs)" % (time.time() - t0)) - return scu.sendResult(d, name="etudiant", format=format, force_outer_xml_tag=False) + return scu.sendResult( + d, name="etudiant", format=format, force_outer_xml_tag=False, quote_xml=False + ) # -------------------------- FICHE ETUDIANT -------------------------- From 0f67ee33aeb425efe8962e03369f1f2322c7eb9e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 20 Oct 2021 23:18:00 +0200 Subject: [PATCH 07/12] Fix etud_info xml/json quote --- app/views/scolar.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/views/scolar.py b/app/views/scolar.py index 74e80a1a8..38f63f063 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -415,12 +415,10 @@ def etud_info(etudid=None, format="xml"): "codelycee", "date_naissance_iso", ): - d[a] = scu.quote_xml_attr(etud[a]) - d["civilite"] = scu.quote_xml_attr( - etud["civilite_str"] - ) # exception: ne sort pas la civilite brute + d[a] = etud[a] # ne pas quoter car ElementTree.tostring quote déjà + d["civilite"] = etud["civilite_str"] # exception: ne sort pas la civilite brute d["sexe"] = d["civilite"] # backward compat pour anciens clients - d["photo_url"] = scu.quote_xml_attr(sco_photos.etud_photo_url(etud)) + d["photo_url"] = sco_photos.etud_photo_url(etud) sem = etud["cursem"] if sem: @@ -431,10 +429,8 @@ def etud_info(etudid=None, format="xml"): "formsemestre_id": sem["formsemestre_id"], "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), - "etat": scu.quote_xml_attr(sem["ins"]["etat"]), - "groupes": scu.quote_xml_attr( - etud["groupes"] - ), # slt pour semestre courant + "etat": sem["ins"]["etat"], + "groupes": etud["groupes"], # slt pour semestre courant } ] else: @@ -446,7 +442,7 @@ def etud_info(etudid=None, format="xml"): "formsemestre_id": sem["formsemestre_id"], "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), - "etat": scu.quote_xml_attr(sem["ins"]["etat"]), + "etat": sem["ins"]["etat"], } ) From c49aecaa2fd0fa2783474a5f878022f97d79fdd6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 21 Oct 2021 06:32:03 +0200 Subject: [PATCH 08/12] Fix regression on ue_list --- app/scodoc/sco_edit_module.py | 10 ++++++--- app/scodoc/sco_edit_ue.py | 42 +++++++++++++++++------------------ app/views/notes.py | 12 +++++++++- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index a23ddf8c8..301ef585a 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -588,9 +588,9 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True): """Création d'un module de "malus" dans chaque UE d'une formation""" from app.scodoc import sco_edit_ue - ue_list = sco_edit_ue.ue_list(args={"formation_id": formation_id}) + ues = sco_edit_ue.ue_list(args={"formation_id": formation_id}) - for ue in ue_list: + for ue in ues: # Un seul module de malus par UE: nb_mod_malus = len( [ @@ -603,7 +603,11 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True): ue_add_malus_module(ue["ue_id"], titre=titre) if redirect: - return flask.redirect("ue_list?formation_id=" + str(formation_id)) + return flask.redirect( + url_for( + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id + ) + ) def ue_add_malus_module(ue_id, titre=None, code=None): diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index cc7d7557d..2e99407de 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -374,12 +374,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None): ) -def _add_ue_semestre_id(ue_list): +def _add_ue_semestre_id(ues): """ajoute semestre_id dans les ue, en regardant le premier module de chacune. Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), qui les place à la fin de la liste. """ - for ue in ue_list: + for ue in ues: Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) if Modlist: ue["semestre_id"] = Modlist[0]["semestre_id"] @@ -391,27 +391,27 @@ def next_ue_numero(formation_id, semestre_id=None): """Numero d'une nouvelle UE dans cette formation. Si le semestre est specifie, cherche les UE ayant des modules de ce semestre """ - ue_list = ue_list(args={"formation_id": formation_id}) - if not ue_list: + ues = ue_list(args={"formation_id": formation_id}) + if not ues: return 0 if semestre_id is None: - return ue_list[-1]["numero"] + 1000 + return ues[-1]["numero"] + 1000 else: # Avec semestre: (prend le semestre du 1er module de l'UE) - _add_ue_semestre_id(ue_list) - ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id] + _add_ue_semestre_id(ues) + ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id] if ue_list_semestre: return ue_list_semestre[-1]["numero"] + 10 else: - return ue_list[-1]["numero"] + 1000 + return ues[-1]["numero"] + 1000 def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): """Delete an UE""" - ue = ue_list(args={"ue_id": ue_id}) - if not ue: + ues = ue_list(args={"ue_id": ue_id}) + if not ues: raise ScoValueError("UE inexistante !") - ue = ue[0] + ue = ues[0] if not dialog_confirmed: return scu.confirm_dialog( @@ -438,11 +438,11 @@ def ue_table(formation_id=None, msg=""): # was ue_list parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) locked = sco_formations.formation_has_locked_sems(formation_id) - ue_list = ue_list(args={"formation_id": formation_id}) + ues = ue_list(args={"formation_id": formation_id}) # tri par semestre et numero: - _add_ue_semestre_id(ue_list) - ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"])) - has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list) + _add_ue_semestre_id(ues) + ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) + has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues) perm_change = current_user.has_permission(Permission.ScoChangeFormation) # editable = (not locked) and perm_change @@ -559,7 +559,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); cur_ue_semestre_id = None iue = 0 - for UE in ue_list: + for UE in ues: if UE["ects"]: UE["ects_str"] = ", %g ECTS" % UE["ects"] else: @@ -593,7 +593,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) else: H.append(arrow_none) - if iue < len(ue_list) - 1 and editable: + if iue < len(ues) - 1 and editable: H.append( '%s' % (UE["ue_id"], arrow_down) @@ -964,9 +964,9 @@ def formation_table_recap(formation_id, format="html"): raise ScoValueError("invalid formation_id") F = F[0] T = [] - ue_list = ue_list(args={"formation_id": formation_id}) - for UE in ue_list: - Matlist = sco_edit_matiere.matiere_list(args={"ue_id": UE["ue_id"]}) + ues = ue_list(args={"formation_id": formation_id}) + for ue in ues: + Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]}) for Mat in Matlist: Modlist = sco_edit_module.module_list( args={"matiere_id": Mat["matiere_id"]} @@ -978,7 +978,7 @@ def formation_table_recap(formation_id, format="html"): # T.append( { - "UE_acro": UE["acronyme"], + "UE_acro": ue["acronyme"], "Mat_tit": Mat["titre"], "Mod_tit": Mod["abbrev"] or Mod["titre"], "Mod_code": Mod["code"], diff --git a/app/views/notes.py b/app/views/notes.py index b3ba8c599..bd69ffbe3 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -334,7 +334,17 @@ sco_publish( Permission.ScoChangeFormation, methods=["GET", "POST"], ) -sco_publish("/ue_list", sco_edit_ue.ue_table, Permission.ScoView) + + +@bp.route("/ue_table") +@bp.route("/ue_list") # backward compat +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def ue_table(formation_id=None, msg=""): + return sco_edit_ue.ue_table(formation_id=formation_id, msg=msg) + + sco_publish("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView) sco_publish( "/edit_ue_set_code_apogee", From 2fe9e5ec3945eae808c2fa28ca8e9591ae83e8de Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 22 Oct 2021 23:09:15 +0200 Subject: [PATCH 09/12] =?UTF-8?q?S=C3=A9pare=20les=20UE=20externes=20dans?= =?UTF-8?q?=20la=20pae=20=C3=A9dition=20programme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/notesdb.py | 16 + app/scodoc/sco_edit_matiere.py | 25 +- app/scodoc/sco_edit_module.py | 24 +- app/scodoc/sco_edit_ue.py | 565 ++++++++++++++-------- app/scodoc/sco_etape_apogee_view.py | 2 +- app/scodoc/sco_etud.py | 9 +- app/scodoc/sco_formations.py | 6 +- app/scodoc/sco_formsemestre_edit.py | 2 +- app/scodoc/sco_formsemestre_exterieurs.py | 27 +- app/scodoc/sco_moduleimpl.py | 13 + app/scodoc/sco_recapcomplet.py | 1 - app/scodoc/sco_xml.py | 18 +- app/static/css/scodoc.css | 13 + app/views/notes.py | 24 +- sco_version.py | 2 +- tests/unit/sco_fake_gen.py | 6 +- tests/unit/test_formations.py | 4 + 17 files changed, 492 insertions(+), 265 deletions(-) diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 6fa29fb98..b03b5427d 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -597,6 +597,22 @@ def float_null_is_null(x): return float(x) +BOOL_STR = { + "": False, + "false": False, + "0": False, + "1": True, + "true": "true", +} + + +def bool_or_str(x): + """a boolean, may also be encoded as a string "0", "False", "1", "True" """ + if isinstance(x, str): + return BOOL_STR[x.lower()] + return x + + # post filtering # def UniqListofDicts(L, key): diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index 1cbd199d3..f07b9b588 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -190,7 +190,7 @@ def do_matiere_delete(oid): def matiere_delete(matiere_id=None): - """Delete an UE""" + """Delete matière""" from app.scodoc import sco_edit_ue M = matiere_list(args={"matiere_id": matiere_id})[0] @@ -200,7 +200,11 @@ def matiere_delete(matiere_id=None): "

Suppression de la matière %(titre)s" % M, " dans l'UE (%(acronyme)s))

" % UE, ] - dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(UE["formation_id"]) + dest_url = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(UE["formation_id"]), + ) tf = TrivialFormulator( request.base_url, scu.get_request_args(), @@ -227,13 +231,13 @@ def matiere_edit(matiere_id=None): if not F: raise ScoValueError("Matière inexistante !") F = F[0] - U = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]}) - if not F: + ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]}) + if not ues: raise ScoValueError("UE inexistante !") - U = U[0] - Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[0] + ue = ues[0] + Fo = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0] - ues = sco_edit_ue.ue_list(args={"formation_id": U["formation_id"]}) + ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]}) ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues] ue_ids = [u["ue_id"] for u in ues] H = [ @@ -278,8 +282,11 @@ associé. submitlabel="Modifier les valeurs", ) - dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(U["formation_id"]) - + dest_url = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(ue["formation_id"]), + ) if tf[0] == 0: return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer() elif tf[0] == -1: diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 301ef585a..54d7fc846 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -285,21 +285,25 @@ def module_delete(module_id=None): """Delete a module""" if not module_id: raise ScoValueError("invalid module !") - Mods = module_list(args={"module_id": module_id}) - if not Mods: + modules = module_list(args={"module_id": module_id}) + if not modules: raise ScoValueError("Module inexistant !") - Mod = Mods[0] + mod = modules[0] H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), - """

Suppression du module %(titre)s (%(code)s)

""" % Mod, + """

Suppression du module %(titre)s (%(code)s)

""" % mod, ] - dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"]) + dest_url = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(mod["formation_id"]), + ) tf = TrivialFormulator( request.base_url, scu.get_request_args(), (("module_id", {"input_type": "hidden"}),), - initvalues=Mod, + initvalues=mod, submitlabel="Confirmer la suppression", cancelbutton="Annuler", ) @@ -367,9 +371,11 @@ def module_edit(module_id=None): Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) - - dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"]) - + dest_url = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(Mod["formation_id"]), + ) H = [ html_sco_header.sco_header( page_title="Modification du module %(titre)s" % Mod, diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 2e99407de..66d8777d5 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -75,7 +75,7 @@ _ueEditor = ndb.EditableTable( sortkey="numero", input_formators={ "type": ndb.int_null_is_zero, - "is_external": bool, + "is_external": ndb.bool_or_str, }, output_formators={ "numero": ndb.int_null_is_zero, @@ -139,7 +139,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): % (len(validations), ue["acronyme"], ue["titre"]), dest_url="", target_variable="delete_validations", - cancel_url="ue_list?formation_id=%s" % ue["formation_id"], + cancel_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(ue["formation_id"]), + ), parameters={"ue_id": ue_id, "dialog_confirmed": 1}, ) if delete_validations: @@ -294,6 +298,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None): "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", }, ), + ( + "is_external", + { + "input_type": "boolcheckbox", + "title": "UE externe", + "explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement", + }, + ), ] if parcours.UE_IS_MODULE: # demande le semestre pour creer le module immediatement: @@ -418,7 +430,11 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): "

Suppression de l'UE %(titre)s (%(acronyme)s))

" % ue, dest_url="", parameters={"ue_id": ue_id}, - cancel_url="ue_list?formation_id=%s" % ue["formation_id"], + cancel_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(ue["formation_id"]), + ), ) return do_ue_delete(ue_id, delete_validations=delete_validations) @@ -438,21 +454,24 @@ def ue_table(formation_id=None, msg=""): # was ue_list parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) locked = sco_formations.formation_has_locked_sems(formation_id) - ues = ue_list(args={"formation_id": formation_id}) + ues = ue_list(args={"formation_id": formation_id, "is_external": False}) + ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True}) # tri par semestre et numero: _add_ue_semestre_id(ues) + _add_ue_semestre_id(ues_externes) ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) + ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"])) has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues) - perm_change = current_user.has_permission(Permission.ScoChangeFormation) - # editable = (not locked) and perm_change + has_perm_change = current_user.has_permission(Permission.ScoChangeFormation) + # editable = (not locked) and has_perm_change # On autorise maintanant la modification des formations qui ont des semestres verrouillés, # sauf si cela affect les notes passées (verrouillées): # - pas de modif des modules utilisés dans des semestres verrouillés # - pas de changement des codes d'UE utilisés dans des semestres verrouillés - editable = perm_change + editable = has_perm_change tag_editable = ( - current_user.has_permission(Permission.ScoEditFormationTags) or perm_change + current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change ) if locked: lockicon = scu.icontag("lock32_img", title="verrouillé") @@ -556,213 +575,20 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); H.append( '
montrer les tags
' ) - - cur_ue_semestre_id = None - iue = 0 - for UE in ues: - if UE["ects"]: - UE["ects_str"] = ", %g ECTS" % UE["ects"] - else: - UE["ects_str"] = "" - if editable: - klass = "span_apo_edit" - else: - klass = "" - UE["code_apogee_str"] = ( - """, Apo: """ - % (klass, UE["ue_id"], scu.APO_MISSING_CODE_STR) - + (UE["code_apogee"] or "") - + "" + H.append( + _ue_table_ues( + parcours, + ues, + editable, + tag_editable, + has_perm_change, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, ) - - if cur_ue_semestre_id != UE["semestre_id"]: - cur_ue_semestre_id = UE["semestre_id"] - if iue > 0: - H.append("") - if UE["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: - lab = "Pas d'indication de semestre:" - else: - lab = "Semestre %s:" % UE["semestre_id"] - H.append('
%s
' % lab) - H.append('
    ') - H.append('
  • ') - if iue != 0 and editable: - H.append( - '%s' - % (UE["ue_id"], arrow_up) - ) - else: - H.append(arrow_none) - if iue < len(ues) - 1 and editable: - H.append( - '%s' - % (UE["ue_id"], arrow_down) - ) - else: - H.append(arrow_none) - iue += 1 - UE["acro_titre"] = str(UE["acronyme"]) - if UE["titre"] != UE["acronyme"]: - UE["acro_titre"] += " " + str(UE["titre"]) - H.append( - """%(acro_titre)s (code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s) - - """ - % UE - ) - - if UE["type"] != sco_codes_parcours.UE_STANDARD: - H.append( - '%s' - % sco_codes_parcours.UE_TYPE_NAME[UE["type"]] - ) - ue_editable = editable and not ue_is_locked(UE["ue_id"]) - if ue_editable: - H.append( - 'modifier' % UE - ) - else: - H.append('[verrouillé]') - if not parcours.UE_IS_MODULE: - H.append('
      ') - Matlist = sco_edit_matiere.matiere_list(args={"ue_id": UE["ue_id"]}) - for Mat in Matlist: - if not parcours.UE_IS_MODULE: - H.append('
    • ') - if editable and not sco_edit_matiere.matiere_is_locked( - Mat["matiere_id"] - ): - H.append( - f""" - """ - ) - H.append("%(titre)s" % Mat) - if editable and not sco_edit_matiere.matiere_is_locked( - Mat["matiere_id"] - ): - H.append("") - - H.append('
        ') - Modlist = sco_edit_module.module_list( - args={"matiere_id": Mat["matiere_id"]} - ) - im = 0 - for Mod in Modlist: - Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls( - Mod["module_id"] - ) - klass = "notes_module_list" - if Mod["module_type"] == scu.MODULE_MALUS: - klass += " module_malus" - H.append('
      • ' % klass) - - H.append('') - if im != 0 and editable: - H.append( - '%s' - % (Mod["module_id"], arrow_up) - ) - else: - H.append(arrow_none) - if im < len(Modlist) - 1 and editable: - H.append( - '%s' - % (Mod["module_id"], arrow_down) - ) - else: - H.append(arrow_none) - im += 1 - if Mod["nb_moduleimpls"] == 0 and editable: - H.append( - '%s' - % (Mod["module_id"], delete_icon) - ) - else: - H.append(delete_disabled_icon) - H.append("") - - mod_editable = editable # and not sco_edit_module.module_is_locked( Mod['module_id']) - if mod_editable: - H.append( - '' - % Mod - ) - H.append( - '%s' - % scu.join_words(Mod["code"], Mod["titre"]) - ) - if mod_editable: - H.append("") - heurescoef = ( - "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" - % Mod - ) - if mod_editable: - klass = "span_apo_edit" - else: - klass = "" - heurescoef += ( - ', Apo: ' - % (klass, Mod["module_id"], scu.APO_MISSING_CODE_STR) - + (Mod["code_apogee"] or "") - + "" - ) - if tag_editable: - tag_cls = "module_tag_editor" - else: - tag_cls = "module_tag_editor_ro" - tag_mk = """
        """ - tag_edit = tag_mk.format( - Mod["module_id"], - tag_cls, - ",".join(sco_tag_module.module_tag_list(Mod["module_id"])), - ) - H.append( - " %s %s" % (parcours.SESSION_NAME, Mod["semestre_id"]) - + " (%s)" % heurescoef - + tag_edit - ) - H.append("
      • ") - if not Modlist: - H.append("
      • Aucun module dans cette matière !") - if editable: - H.append( - f"""supprimer cette matière - """ - ) - H.append("
      • ") - if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0): - H.append( - f"""
      • créer un module
      • - """ - ) - H.append("
      ") - H.append("
    • ") - if not Matlist: - H.append("
    • Aucune matière dans cette UE ! ") - if editable: - H.append( - """supprimer l'UE""" - % UE - ) - H.append("
    • ") - if editable and not parcours.UE_IS_MODULE: - H.append( - '
    • créer une matière
    • ' - % UE - ) - if not parcours.UE_IS_MODULE: - H.append("
    ") - H.append("
") + ) if editable: H.append( '
  • Ajouter une UE
  • ' @@ -774,6 +600,27 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) H.append("
") # formation_ue_list + if ues_externes: + H.append('
') + H.append( + '
UE externes déclarées (pour information):
' + ) + H.append( + _ue_table_ues( + parcours, + ues_externes, + editable, + tag_editable, + has_perm_change, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, + ) + ) + H.append("
") # formation_ue_list + H.append("

    ") if editable: H.append( @@ -795,7 +642,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);

    """ % F ) - if perm_change: + if has_perm_change: H.append( """

    Semestres ou sessions de cette formation

    @@ -836,6 +683,294 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); return "".join(H) +def _ue_table_ues( + parcours, + ues, + editable, + tag_editable, + has_perm_change, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, +): + """Édition de programme: liste des UEs (avec leurs matières et modules).""" + H = [] + cur_ue_semestre_id = None + iue = 0 + for ue in ues: + if ue["ects"]: + ue["ects_str"] = ", %g ECTS" % ue["ects"] + else: + ue["ects_str"] = "" + if editable: + klass = "span_apo_edit" + else: + klass = "" + ue["code_apogee_str"] = ( + """, Apo: """ + % (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR) + + (ue["code_apogee"] or "") + + "" + ) + + if cur_ue_semestre_id != ue["semestre_id"]: + cur_ue_semestre_id = ue["semestre_id"] + if iue > 0: + H.append("
") + if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: + lab = "Pas d'indication de semestre:" + else: + lab = "Semestre %s:" % ue["semestre_id"] + H.append('
%s
' % lab) + H.append('
    ') + H.append('
  • ') + if iue != 0 and editable: + H.append( + '%s' + % (ue["ue_id"], arrow_up) + ) + else: + H.append(arrow_none) + if iue < len(ues) - 1 and editable: + H.append( + '%s' + % (ue["ue_id"], arrow_down) + ) + else: + H.append(arrow_none) + iue += 1 + ue["acro_titre"] = str(ue["acronyme"]) + if ue["titre"] != ue["acronyme"]: + ue["acro_titre"] += " " + str(ue["titre"]) + H.append( + """%(acro_titre)s (code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s) + + """ + % ue + ) + if ue["type"] != sco_codes_parcours.UE_STANDARD: + H.append( + '%s' + % sco_codes_parcours.UE_TYPE_NAME[ue["type"]] + ) + if ue["is_external"]: + # Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE + # qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml) + # Dans ce cas, propose de changer le type (même si verrouillée) + if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1: + H.append('') + if has_perm_change: + H.append( + f"""transformer en UE ordinaire """ + ) + H.append("") + ue_editable = editable and not ue_is_locked(ue["ue_id"]) + if ue_editable: + H.append( + 'modifier' % ue + ) + else: + H.append('[verrouillé]') + H.append( + _ue_table_matieres( + parcours, + ue, + editable, + tag_editable, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, + ) + ) + return "\n".join(H) + + +def _ue_table_matieres( + parcours, + ue, + editable, + tag_editable, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, +): + """Édition de programme: liste des matières (et leurs modules) d'une UE.""" + H = [] + if not parcours.UE_IS_MODULE: + H.append('
      ') + matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]}) + for mat in matieres: + if not parcours.UE_IS_MODULE: + H.append('
    • ') + if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]): + H.append( + f""" + """ + ) + H.append("%(titre)s" % mat) + if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]): + H.append("") + + modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]}) + H.append( + _ue_table_modules( + parcours, + mat, + modules, + editable, + tag_editable, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, + ) + ) + if not matieres: + H.append("
    • Aucune matière dans cette UE ! ") + if editable: + H.append( + """supprimer l'UE""" + % ue + ) + H.append("
    • ") + if editable and not parcours.UE_IS_MODULE: + H.append( + '
    • créer une matière
    • ' + % ue + ) + if not parcours.UE_IS_MODULE: + H.append("
    ") + return "\n".join(H) + + +def _ue_table_modules( + parcours, + mat, + modules, + editable, + tag_editable, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, +): + """Édition de programme: liste des modules d'une matière d'une UE""" + H = ['
      '] + im = 0 + for mod in modules: + mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls( + mod["module_id"] + ) + klass = "notes_module_list" + if mod["module_type"] == scu.MODULE_MALUS: + klass += " module_malus" + H.append('
    • ' % klass) + + H.append('') + if im != 0 and editable: + H.append( + '%s' + % (mod["module_id"], arrow_up) + ) + else: + H.append(arrow_none) + if im < len(modules) - 1 and editable: + H.append( + '%s' + % (mod["module_id"], arrow_down) + ) + else: + H.append(arrow_none) + im += 1 + if mod["nb_moduleimpls"] == 0 and editable: + H.append( + '%s' + % (mod["module_id"], delete_icon) + ) + else: + H.append(delete_disabled_icon) + H.append("") + + mod_editable = ( + editable # and not sco_edit_module.module_is_locked( Mod['module_id']) + ) + if mod_editable: + H.append( + '' + % mod + ) + H.append( + '%s' + % scu.join_words(mod["code"], mod["titre"]) + ) + if mod_editable: + H.append("") + heurescoef = ( + "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod + ) + if mod_editable: + klass = "span_apo_edit" + else: + klass = "" + heurescoef += ( + ', Apo: ' + % (klass, mod["module_id"], scu.APO_MISSING_CODE_STR) + + (mod["code_apogee"] or "") + + "" + ) + if tag_editable: + tag_cls = "module_tag_editor" + else: + tag_cls = "module_tag_editor_ro" + tag_mk = """
      """ + tag_edit = tag_mk.format( + mod["module_id"], + tag_cls, + ",".join(sco_tag_module.module_tag_list(mod["module_id"])), + ) + H.append( + " %s %s" % (parcours.SESSION_NAME, mod["semestre_id"]) + + " (%s)" % heurescoef + + tag_edit + ) + H.append("
    • ") + if not modules: + H.append("
    • Aucun module dans cette matière ! ") + if editable: + H.append( + f"""la supprimer + """ + ) + H.append("
    • ") + if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0): + H.append( + f"""
    • créer un module
    • + """ + ) + H.append("
    ") + H.append("
  • ") + return "\n".join(H) + + def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None): """HTML list of UE sharing this code Either ue_code or ue_id may be specified. diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index de1e7ec94..5f2c1ed82 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -356,7 +356,7 @@ def apo_semset_maq_status( H.append( ", ".join( [ - '%(acronyme)s v%(version)s' + '%(acronyme)s v%(version)s' % f for f in formations ] diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index a2f2aebc9..5fc1242f2 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -152,7 +152,7 @@ def format_nom(s, uppercase=True): def input_civilite(s): """Converts external representation of civilite to internal: 'M', 'F', or 'X' (and nothing else). - Raises valueError if conversion fails. + Raises ScoValueError if conversion fails. """ s = s.upper().strip() if s in ("M", "M.", "MR", "H"): @@ -161,12 +161,13 @@ def input_civilite(s): return "F" elif s == "X" or not s: return "X" - raise ValueError("valeur invalide pour la civilité: %s" % s) + raise ScoValueError("valeur invalide pour la civilité: %s" % s) def format_civilite(civilite): """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, - personne ne souhaitant pas d'affichage) + personne ne souhaitant pas d'affichage). + Raises ScoValueError if conversion fails. """ try: return { @@ -175,7 +176,7 @@ def format_civilite(civilite): "X": "", }[civilite] except KeyError: - raise ValueError("valeur invalide pour la civilité: %s" % civilite) + raise ScoValueError("valeur invalide pour la civilité: %s" % civilite) def format_lycee(nomlycee): diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index a320afe57..5e4f0ab69 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -254,7 +254,11 @@ def formation_list_table(formation_id=None, args={}): ).NAME except: f["parcours_name"] = "" - f["_titre_target"] = "ue_list?formation_id=%(formation_id)s" % f + f["_titre_target"] = url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=str(f["formation_id"]), + ) f["_titre_link_class"] = "stdlink" f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-") # Ajoute les semestres associés à chaque formation: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 416e531e0..bd36427d3 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -675,7 +675,7 @@ def do_formsemestre_createwithmodules(edit=False): if tf[0] == 0 or msg: return ( - '

    Formation %(titre)s (%(acronyme)s), version %(version)s, code %(formation_code)s

    ' + '

    Formation %(titre)s (%(acronyme)s), version %(version)s, code %(formation_code)s

    ' % F + msg + str(tf[1]) diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 412e06d4e..a4c782d49 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -221,12 +221,11 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid): """ sem = sco_formsemestre.get_formsemestre(formsemestre_id) etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - ue_list = _list_ue_with_coef_and_validations(sem, etudid) - descr = _ue_form_description(ue_list, scu.get_request_args()) + ues = _list_ue_with_coef_and_validations(sem, etudid) + descr = _ue_form_description(ues, scu.get_request_args()) if request.method == "GET": initvalues = { - "note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") - for ue in ue_list + "note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues } else: initvalues = {} @@ -247,15 +246,13 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid): return "\n".join(H) else: # soumission # simule erreur - ok, message = _check_values(ue_list, tf[2]) + ok, message = _check_values(ues, tf[2]) if not ok: H = _make_page(etud, sem, tf, message=message) return "\n".join(H) else: # Submit - _record_ue_validations_and_coefs( - formsemestre_id, etudid, ue_list, tf[2] - ) + _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2]) return flask.redirect( "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % (formsemestre_id, etudid) @@ -303,7 +300,7 @@ _UE_VALID_CODES = { } -def _ue_form_description(ue_list, values): +def _ue_form_description(ues, values): """Description du formulaire de saisie des UE / validations Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient. """ @@ -320,7 +317,7 @@ def _ue_form_description(ue_list, values): ("formsemestre_id", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}), ] - for ue in ue_list: + for ue in ues: # Menu pour code validation UE: # Ne propose que ADM, CMP et "Non inscrit" select_name = "valid_" + str(ue["ue_id"]) @@ -439,8 +436,8 @@ def _list_ue_with_coef_and_validations(sem, etudid): """ cnx = ndb.GetDBConnexion() formsemestre_id = sem["formsemestre_id"] - ue_list = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]}) - for ue in ue_list: + ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]}) + for ue in ues: # add coefficient uecoef = sco_formsemestre.formsemestre_uecoef_list( cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]} @@ -462,11 +459,11 @@ def _list_ue_with_coef_and_validations(sem, etudid): ue["validation"] = validation[0] else: ue["validation"] = {} - return ue_list + return ues -def _record_ue_validations_and_coefs(formsemestre_id, etudid, ue_list, values): - for ue in ue_list: +def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values): + for ue in ues: code = values.get("valid_" + str(ue["ue_id"]), False) if code == "None": code = None diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index 739061b12..2ffed0f48 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -178,6 +178,19 @@ def moduleimpl_withmodule_list( return modimpls +def moduleimpls_in_external_ue(ue_id): + """List of modimpls in this ue""" + cursor = ndb.SimpleQuery( + """SELECT DISTINCT mi.* + FROM notes_ue u, notes_moduleimpl mi, notes_modules m + WHERE u.is_external is true + AND mi.module_id = m.id AND m.ue_id = %(ue_id)s + """, + {"ue_id": ue_id}, + ) + return cursor.dictfetchall() + + def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None): "list moduleimpl_inscriptions" args = locals() diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 06f7b95eb..0ac27c8b3 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -348,7 +348,6 @@ def make_formsemestre_recapcomplet( if not hidemodules: h.append("") pass - if not hidemodules and not ue["is_external"]: for modimpl in modimpls: if modimpl["module"]["ue_id"] == ue["ue_id"]: diff --git a/app/scodoc/sco_xml.py b/app/scodoc/sco_xml.py index ea3c3ee93..222ef84b1 100644 --- a/app/scodoc/sco_xml.py +++ b/app/scodoc/sco_xml.py @@ -82,25 +82,35 @@ def simple_dictlist2xml(dictlist, tagname=None, quote=False, pretty=True): return ans +def _repr_as_xml(v): + if isinstance(v, bool): + return str(int(v)) # booleans as "0" / "1" + return str(v) + + def _dictlist2xml(dictlist, root=None, tagname=None, quote=False): - scalar_types = (bytes, str, int, float) + scalar_types = (bytes, str, int, float, bool) for d in dictlist: elem = ElementTree.Element(tagname) root.append(elem) if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI): - elem.set("code", str(d)) + elem.set("code", _repr_as_xml(d)) else: if quote: d_scalar = dict( [ - (k, quote_xml_attr(v)) + (k, quote_xml_attr(_repr_as_xml(v))) for (k, v) in d.items() if isinstance(v, scalar_types) ] ) else: d_scalar = dict( - [(k, str(v)) for (k, v) in d.items() if isinstance(v, scalar_types)] + [ + (k, _repr_as_xml(v)) + for (k, v) in d.items() + if isinstance(v, scalar_types) + ] ) for k in d_scalar: elem.set(k, d_scalar[k]) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 59a96712b..9bb52b1a6 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1511,6 +1511,19 @@ span.ue_type { margin-right: 1.5em; } +div.formation_ue_list_externes { + background-color: #98cc98; +} +div.formation_ue_list_externes ul.notes_ue_list, div.formation_ue_list_externes li.notes_ue_list { + background-color: #98cc98; +} +span.ue_is_external span { + color: orange; +} +span.ue_is_external a { + font-weight: normal; +} + li.notes_matiere_list { margin-top: 2px; } diff --git a/app/views/notes.py b/app/views/notes.py index bd69ffbe3..90d70c466 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -41,9 +41,12 @@ import flask from flask import url_for from flask import current_app, g, request from flask_login import current_user +from werkzeug.utils import redirect from config import Config +from app import db +from app import models from app.auth.models import User from app.decorators import ( @@ -336,8 +339,8 @@ sco_publish( ) -@bp.route("/ue_table") @bp.route("/ue_list") # backward compat +@bp.route("/ue_table") @scodoc @permission_required(Permission.ScoView) @scodoc7func @@ -345,6 +348,25 @@ def ue_table(formation_id=None, msg=""): return sco_edit_ue.ue_table(formation_id=formation_id, msg=msg) +@bp.route("/ue_set_internal", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoChangeFormation) +@scodoc7func +def ue_set_internal(ue_id): + """""" + ue = models.formations.NotesUE.query.get(ue_id) + if not ue: + raise ScoValueError("invalid ue_id") + ue.is_external = False + db.session.add(ue) + db.session.commit() + return redirect( + url_for( + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id + ) + ) + + sco_publish("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView) sco_publish( "/edit_ue_set_code_apogee", diff --git a/sco_version.py b/sco_version.py index 4bb48370f..b464e1d19 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.54" +SCOVERSION = "9.0.55" SCONAME = "ScoDoc" diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index 8b98e35df..f99365226 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -324,7 +324,7 @@ class ScoFake(object): formation (dict), liste d'ue (dicts), liste de modules. """ f = self.create_formation(acronyme=acronyme, titre=titre) - ue_list = [] + ues = [] mod_list = [] for semestre_id in range(1, nb_semestre + 1): for n in range(1, nb_ue_per_semestre + 1): @@ -333,7 +333,7 @@ class ScoFake(object): acronyme="TSU%s%s" % (semestre_id, n), titre="ue test %s%s" % (semestre_id, n), ) - ue_list.append(ue) + ues.append(ue) mat = self.create_matiere(ue_id=ue["ue_id"], titre="matière test") for _ in range(nb_module_per_ue): mod = self.create_module( @@ -346,7 +346,7 @@ class ScoFake(object): formation_id=f["formation_id"], # faiblesse de l'API ) mod_list.append(mod) - return f, ue_list, mod_list + return f, ues, mod_list def setup_formsemestre( self, diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py index 3d3cdbc50..613d35739 100644 --- a/tests/unit/test_formations.py +++ b/tests/unit/test_formations.py @@ -339,6 +339,10 @@ def test_import_formation(test_client): f = sco_formations.formation_import_xml(doc) assert len(f) == 3 # 3-uple formation_id = f[0] + # --- Vérification des UE + ues = sco_edit_ue.ue_list({"formation_id": formation_id}) + assert len(ues) == 10 + assert all(not ue["is_external"] for ue in ues) # aucune UE externe dans le XML # --- Mise en place de 4 semestres sems = [ G.create_formsemestre( From e8ce1e303ea99ebce2244bd3370bf4cd2f811341 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 24 Oct 2021 11:43:53 +0200 Subject: [PATCH 10/12] formation_export: n'exporte plus les UE externes --- app/scodoc/sco_formations.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 5e4f0ab69..8aa7c72c2 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -93,12 +93,21 @@ def formation_has_locked_sems(formation_id): return sems -def formation_export(formation_id, export_ids=False, export_tags=True, format=None): +def formation_export( + formation_id, + export_ids=False, + export_tags=True, + export_external_ues=False, + format=None, +): """Get a formation, with UE, matieres, modules in desired format """ F = formation_list(args={"formation_id": formation_id})[0] - ues = sco_edit_ue.ue_list({"formation_id": formation_id}) + selector = {"formation_id": formation_id} + if not export_external_ues: + selector["is_external"] = False + ues = sco_edit_ue.ue_list(selector) F["ue"] = ues for ue in ues: ue_id = ue["ue_id"] From ad0cd6236cb6ae69cd90c219af99096eecc902b8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 24 Oct 2021 12:01:42 +0200 Subject: [PATCH 11/12] AddBilletAbsence: autorise POST pour anciens clients PHP --- app/views/absences.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/absences.py b/app/views/absences.py index 24d972eeb..21ed46f60 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1046,9 +1046,9 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) -@bp.route("/AddBilletAbsence") +@bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat @scodoc -@permission_required(Permission.ScoAbsAddBillet) +@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) @scodoc7func def AddBilletAbsence( begin, @@ -1060,7 +1060,7 @@ def AddBilletAbsence( justified=True, xml_reply=True, ): - """Memorise un "billet" + """Mémorise un "billet" begin et end sont au format ISO (eg "1999-01-08 04:05:06") """ t0 = time.time() @@ -1251,9 +1251,9 @@ def XMLgetBilletsEtud(etudid=False): return r -@bp.route("/listeBillets", methods=["GET", "POST"]) # pour compat anciens clients PHP +@bp.route("/listeBillets", methods=["GET"]) @scodoc -@permission_required_compat_scodoc7(Permission.ScoView) +@permission_required(Permission.ScoView) @scodoc7func def listeBillets(): """Page liste des billets non traités et formulaire recherche d'un billet""" From 66d443944ab2dc7f4d8ffbf8f86cc88f13305ffb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 24 Oct 2021 18:28:01 +0200 Subject: [PATCH 12/12] Fix: partition_rename error message --- app/scodoc/sco_groups.py | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 120db6a43..14410977a 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -89,8 +89,8 @@ def get_group(group_id): """Returns group object, with partition""" r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* - FROM group_descr gd, partition p - WHERE gd.id=%(group_id)s + FROM group_descr gd, partition p + WHERE gd.id=%(group_id)s AND p.id = gd.partition_id """, {"group_id": group_id}, @@ -112,8 +112,8 @@ def group_delete(group, force=False): def get_partition(partition_id): r = ndb.SimpleDictFetch( - """SELECT p.id AS partition_id, p.* - FROM partition p + """SELECT p.id AS partition_id, p.* + FROM partition p WHERE p.id = %(partition_id)s """, {"partition_id": partition_id}, @@ -126,7 +126,7 @@ def get_partition(partition_id): def get_partitions_list(formsemestre_id, with_default=True): """Liste des partitions pour ce semestre (list of dicts)""" partitions = ndb.SimpleDictFetch( - """SELECT p.id AS partition_id, p.* + """SELECT p.id AS partition_id, p.* FROM partition p WHERE formsemestre_id=%(formsemestre_id)s ORDER BY numero""", @@ -143,7 +143,7 @@ def get_default_partition(formsemestre_id): """Get partition for 'all' students (this one always exists, with NULL name)""" r = ndb.SimpleDictFetch( """SELECT p.id AS partition_id, p.* FROM partition p - WHERE formsemestre_id=%(formsemestre_id)s + WHERE formsemestre_id=%(formsemestre_id)s AND partition_name is NULL """, {"formsemestre_id": formsemestre_id}, @@ -170,10 +170,10 @@ def get_partition_groups(partition): """List of groups in this partition (list of dicts). Some groups may be empty.""" return ndb.SimpleDictFetch( - """SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.* - FROM group_descr gd, partition p - WHERE gd.partition_id=%(partition_id)s - AND gd.partition_id=p.id + """SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.* + FROM group_descr gd, partition p + WHERE gd.partition_id=%(partition_id)s + AND gd.partition_id=p.id ORDER BY group_name """, partition, @@ -184,9 +184,9 @@ def get_default_group(formsemestre_id, fix_if_missing=False): """Returns group_id for default ('tous') group""" r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id - FROM group_descr gd, partition p - WHERE p.formsemestre_id=%(formsemestre_id)s - AND p.partition_name is NULL + FROM group_descr gd, partition p + WHERE p.formsemestre_id=%(formsemestre_id)s + AND p.partition_name is NULL AND p.id = gd.partition_id """, {"formsemestre_id": formsemestre_id}, @@ -218,8 +218,8 @@ def get_sem_groups(formsemestre_id): """Returns groups for this sem (in all partitions).""" return ndb.SimpleDictFetch( """SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.* - FROM group_descr gd, partition p - WHERE p.formsemestre_id=%(formsemestre_id)s + FROM group_descr gd, partition p + WHERE p.formsemestre_id=%(formsemestre_id)s AND p.id = gd.partition_id """, {"formsemestre_id": formsemestre_id}, @@ -340,7 +340,7 @@ def get_etud_groups(etudid, sem, exclude_default=False): """Infos sur groupes de l'etudiant dans ce semestre [ group + partition_name ] """ - req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.* + req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.* FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.id @@ -377,10 +377,10 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): { etudid : { partition_id : group_name }} (attr=group_name or group_id) """ infos = ndb.SimpleDictFetch( - """SELECT i.id AS etudid, p.id AS partition_id, - gd.group_name, gd.id AS group_id - FROM notes_formsemestre_inscription i, partition p, - group_descr gd, group_membership gm + """SELECT i.id AS etudid, p.id AS partition_id, + gd.group_name, gd.id AS group_id + FROM notes_formsemestre_inscription i, partition p, + group_descr gd, group_membership gm WHERE i.formsemestre_id=%(formsemestre_id)s and i.formsemestre_id = p.formsemestre_id and p.id = gd.partition_id @@ -413,7 +413,7 @@ def etud_add_group_infos(etud, sem, sep=" "): FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.id and g.partition_id = p.id - and p.formsemestre_id = %(formsemestre_id)s + and p.formsemestre_id = %(formsemestre_id)s ORDER BY p.numero """, {"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]}, @@ -1141,13 +1141,13 @@ def partition_set_name(partition_id, partition_name, redirect=1): # check unicity r = ndb.SimpleDictFetch( - """SELECT p.* FROM partition p - WHERE p.partition_name = %(partition_name)s + """SELECT p.* FROM partition p + WHERE p.partition_name = %(partition_name)s AND formsemestre_id = %(formsemestre_id)s """, {"partition_name": partition_name, "formsemestre_id": formsemestre_id}, ) - if len(r) > 1 or (len(r) == 1 and r[0]["partition_id"] != partition_id): + if len(r) > 1 or (len(r) == 1 and r[0]["id"] != partition_id): raise ScoValueError( "Partition %s déjà existante dans ce semestre !" % partition_name ) @@ -1494,9 +1494,9 @@ def listgroups(group_ids): groups = [] for group_id in group_ids: cursor.execute( - """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* - FROM group_descr gd, partition p - WHERE p.id = gd.partition_id + """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* + FROM group_descr gd, partition p + WHERE p.id = gd.partition_id AND gd.id = %(group_id)s """, {"group_id": group_id},