diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 88beeb762..b49ab5dae 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -185,6 +185,9 @@ class GenTable(object): else: self.preferences = DEFAULT_TABLE_PREFERENCES() + def __repr__(self): + return f"" + def get_nb_cols(self): return len(self.columns_ids) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index b9cb6f2c1..019d7833d 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -130,7 +130,7 @@ def formation_export(formation_id, export_ids=False, export_tags=True, format=No if mod["ects"] is None: del mod["ects"] - return scu.sendResult(F, name="formation", format=format, force_outer_xml_tag=False) + return scu.sendResult(F, name="formation", format=format, force_outer_xml_tag=False, attached=True) def formation_import_xml(doc: str, import_tags=True): diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 364ecce62..034a2f41b 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -81,6 +81,7 @@ def list_authorized_etuds_by_sem(sem, delai=274): "title": src["titreannee"], "title_target": "formsemestre_status?formsemestre_id=%s" % src["formsemestre_id"], + "filename": "etud_autorises", }, } # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. @@ -99,6 +100,7 @@ def list_authorized_etuds_by_sem(sem, delai=274): % sem["formsemestre_id"], "comment": " actuellement inscrits dans ce semestre", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", + "filename": "etud_inscrits", }, } @@ -506,7 +508,12 @@ def etuds_select_boxes(
""" ] # " - + # Élimine les boites vides: + auth_etuds_by_cat = { + k: auth_etuds_by_cat[k] + for k in auth_etuds_by_cat + if auth_etuds_by_cat[k]["etuds"] + } for src_cat in auth_etuds_by_cat.keys(): infos = auth_etuds_by_cat[src_cat]["infos"] infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite @@ -549,10 +556,8 @@ def etuds_select_boxes( if with_checkbox or sel_inscrits: H.append(")") if base_url and etuds: - H.append( - '%s ' - % (base_url, src_cat, scu.ICON_XLS) - ) + url = scu.build_url_query(base_url, export_cat_xls=src_cat) + H.append(f'{scu.ICON_XLS} ') H.append("
") for etud in etuds: if etud.get("inscrit", False): @@ -632,4 +637,4 @@ def etuds_select_box_xls(src_cat): caption="%(title)s. %(help)s" % src_cat["infos"], preferences=sco_preferences.SemPreferences(), ) - return tab.excel() + return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"]) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index b788702f2..9521b53cb 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -231,7 +231,7 @@ def do_formsemestre_recapcomplet( return data elif format == "csv": return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE) - elif format[:3] == "xls" or format[:4] == "xlsx": + elif format.startswith("xls") or format.startswith("xlsx"): return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE) elif format == "json": js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index f3d928cb8..2ac4ade42 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -32,7 +32,7 @@ import time import pprint from operator import itemgetter -from flask import g, url_for, send_file +from flask import g, url_for from flask_login import current_user import app.scodoc.sco_utils as scu @@ -112,6 +112,7 @@ def formsemestre_synchro_etuds( base_url = url_for( "notes.formsemestre_synchro_etuds", scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, anneeapogee=anneeapogee or None, # si None, le param n'est pas dans l'URL ) @@ -146,11 +147,11 @@ def formsemestre_synchro_etuds( base_url=base_url, read_only=read_only, ) - return send_file( + return scu.send_file( xls, - mimetype=scu.XLS_MIMETYPE, - download_name=scu.sanitize_filename(filename + scu.XLSX_SUFFIX), - as_attachment=True, + mime=scu.XLS_MIMETYPE, + filename=filename, + suffix=scu.XLSX_SUFFIX, ) H = [header] @@ -317,7 +318,8 @@ def build_page( """ % sem, """ - Année Apogée: """ % (sem["formsemestre_id"]), "\n".join(options), """ @@ -430,17 +432,20 @@ def list_synch(sem, anneeapogee=None): return etuds # - r = { - "etuds_ok": { - "etuds": set_to_sorted_list(etuds_ok, is_inscrit=True), + boites = { + "etuds_a_importer": { + "etuds": set_to_sorted_list(a_importer, is_inscrit=True, etud_apo=True), "infos": { - "id": "etuds_ok", - "title": "Etudiants dans Apogée et déjà inscrits", - "help": "Ces etudiants sont inscrits dans le semestre ScoDoc et sont présents dans Apogée: tout est donc correct. Décocher les étudiants que vous souhaitez désinscrire.", + "id": "etuds_a_importer", + "title": "Etudiants dans Apogée à importer", + "help": """Ces étudiants sont inscrits dans cette étape Apogée mais ne sont pas connus par ScoDoc: + cocher les noms à importer et inscrire puis appuyer sur le bouton "Appliquer".""", "title_target": "", "with_checkbox": True, - "etud_key": EKEY_SCO, + "etud_key": EKEY_APO, # clé à stocker dans le formulaire html + "filename": "etuds_a_importer", }, + "nomprenoms": etudsapo_ident, }, "etuds_noninscrits": { "etuds": set_to_sorted_list(etuds_noninscrits, is_inscrit=True), @@ -453,20 +458,9 @@ def list_synch(sem, anneeapogee=None): "title_target": "", "with_checkbox": True, "etud_key": EKEY_SCO, + "filename": "etuds_non_inscrits", }, }, - "etuds_a_importer": { - "etuds": set_to_sorted_list(a_importer, is_inscrit=True, etud_apo=True), - "infos": { - "id": "etuds_a_importer", - "title": "Etudiants dans Apogée à importer", - "help": """Ces étudiants sont inscrits dans cette étape Apogée mais ne sont pas connus par ScoDoc: cocher les noms à importer et inscrire puis appuyer sur le bouton "Appliquer".""", - "title_target": "", - "with_checkbox": True, - "etud_key": EKEY_APO, # clé à stocker dans le formulaire html - }, - "nomprenoms": etudsapo_ident, - }, "etuds_nonapogee": { "etuds": set_to_sorted_list(etuds_nonapogee, is_inscrit=True), "infos": { @@ -478,6 +472,7 @@ def list_synch(sem, anneeapogee=None): "title_target": "", "with_checkbox": True, "etud_key": EKEY_SCO, + "filename": "etuds_non_apogee", }, }, "inscrits_without_key": { @@ -489,11 +484,25 @@ def list_synch(sem, anneeapogee=None): "title_target": "", "with_checkbox": True, "checkbox_name": "inscrits_without_key", + "filename": "inscrits_without_key", + }, + }, + "etuds_ok": { + "etuds": set_to_sorted_list(etuds_ok, is_inscrit=True), + "infos": { + "id": "etuds_ok", + "title": "Etudiants dans Apogée et déjà inscrits", + "help": """Ces etudiants sont inscrits dans le semestre ScoDoc et sont présents dans Apogée: + tout est donc correct. Décocher les étudiants que vous souhaitez désinscrire.""", + "title_target": "", + "with_checkbox": True, + "etud_key": EKEY_SCO, + "filename": "etuds_inscrits_ok_apo", }, }, } return ( - r, + boites, a_importer, etuds_noninscrits, inscrits_set, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 63e638787..4862714c4 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -39,15 +39,11 @@ import os import pydot import re import requests -import six -import six.moves._thread -import sys +import _thread import time -import traceback -import types import unicodedata import urllib -from xml.etree import ElementTree +from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode from flask import g, current_app @@ -218,7 +214,7 @@ def group_by_key(d, key): # ----- Global lock for critical sections (except notes_tables caches) -GSL = six.moves._thread.allocate_lock() # Global ScoDoc Lock +GSL = _thread.allocate_lock() # Global ScoDoc Lock SCODOC_DIR = Config.SCODOC_DIR @@ -308,6 +304,7 @@ JSON_SUFFIX = ".json" PDF_MIMETYPE = "application/pdf" PDF_SUFFIX = ".pdf" XLS_MIMETYPE = "application/vnd.ms-excel" +XLS_SUFFIX = ".xls" XLSX_MIMETYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" XLSX_SUFFIX = ".xlsx" XML_MIMETYPE = "text/xml" @@ -411,6 +408,18 @@ def unescape_html(s): return s +def build_url_query(url: str, **params) -> str: + """Add parameters to existing url, as a query string""" + url_parse = urlparse(url) + query = url_parse.query + url_dict = dict(parse_qsl(query)) + url_dict.update(params) + url_new_query = urlencode(url_dict) + url_parse = url_parse._replace(query=url_new_query) + new_url = urlunparse(url_parse) + return new_url + + # test if obj is iterable (but not a string) isiterable = lambda obj: getattr(obj, "__iter__", False) @@ -553,28 +562,28 @@ class ScoDocJSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -def sendJSON(data): +def sendJSON(data, attached=False): js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) - return send_file(js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=False) + return send_file(js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached) -def sendXML(data, tagname=None, force_outer_xml_tag=True): +def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False): 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) - return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=False) + 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): +def sendResult(data, name=None, format=None, force_outer_xml_tag=True, attached=False): if (format is None) or (format == "html"): return data elif format == "xml": # name is outer tagname - return sendXML(data, tagname=name, force_outer_xml_tag=force_outer_xml_tag) + return sendXML(data, tagname=name, force_outer_xml_tag=force_outer_xml_tag, attached=attached) elif format == "json": - return sendJSON(data) + return sendJSON(data, attached=attached) else: raise ValueError("invalid format: %s" % format) diff --git a/app/views/absences.py b/app/views/absences.py index d8c6810ef..720d3ea4d 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1497,4 +1497,4 @@ def XMLgetAbsEtud(beg_date="", end_date=""): ) log("XMLgetAbsEtud (%gs)" % (time.time() - t0)) data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) - scu.send_file(data, mime=scu.XML_MIMETYPE, attached=False) + return scu.send_file(data, mime=scu.XML_MIMETYPE, attached=False) diff --git a/tools/restore_scodoc9_data.sh b/tools/restore_scodoc9_data.sh index 5bcc3566a..12e09b191 100644 --- a/tools/restore_scodoc9_data.sh +++ b/tools/restore_scodoc9_data.sh @@ -76,11 +76,15 @@ then echo "Suppression de la base $DBNAME..." su -c "dropdb $DBNAME" "$SCODOC_USER" || die "Erreur destruction db" fi -su -c "createdb $DBNAME" "$SCODOC_USER" || die "Erreur destruction db" +su -c "createdb $DBNAME" "$SCODOC_USER" || die "Erreur création db" echo "Chargement de la base SQL..." su -c "pg_restore -d $DBNAME ${SCODOC_VAR_DIR}/SCODOC.dump" "$SCODOC_USER" || die "Erreur chargement de la base SQL" +# -- Apply migrations if needed (only on "production" databse, = SCODOC sauf config particulière) +export FLASK_ENV="production" +su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER" + # -- Start ScoDoc systemctl start scodoc9