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(),
- """
- Opération effectuée
- """
- % page_title,
- ]
- if ignored_zipfiles:
- H.append("Fichiers ignorés dans le zip:
")
- for name in ignored_zipfiles:
- H.append("- %s
" % name)
- H.append("
")
- if unmatched_files:
- H.append(
- "Fichiers indiqués dans feuille mais non trouvés dans le zip:
"
- )
- for name in unmatched_files:
- H.append("- %s
" % name)
- H.append("
")
- if stored:
- H.append("Fichiers chargés:
")
- for (etud, name) in stored:
- H.append("- %s: %s
" % (etud["nomprenom"], name))
- H.append("
")
-
- 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 %}
+
+
+Opération 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 %}
+
+
+
+{% 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
- {% if prefs["DeptIntranetURL"] %}
-
- {% endif %}
-
+
+{% if prefs["DeptIntranetURL"] %}
+
+{% endif %}
+
-{#
+{#
# Entreprises pas encore supporté en ScoDoc8
-#
+#
#}
\ 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(
''
)
-
- 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('") # 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(
"""
@@ -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},