Export/Import d'étudiant via fichiers xlsx.

Point délicats:
* Le message d'erreur pour une case vide était une exception python.
diagnostic: la création de l'étudiant dans la BDD se faisait avant le controle de la civilité et plantait quand None
correctif: ajout d'une methode _check_civilite (a cote des méthodes de contrôle d unicité de nip et d ine (sco_etud.py)
* Le format de date a changé entre pyExcelerator et openpyxl (réécriture de sco_excel.xldate_as_datetime)
le format xlxs d import précise qu'une date peut être spécifié soit en ISO soit sous forme d'un nombre.
c est testé avec des écriture de fichier xlsx depuis Excel 2019 et LibreOffice 7 (mais sans maitrise sur la forme de date utilisée)
par contre plantage si tentative de lire un fichier ods (fonction excel_bytes_to_list a fixer)
* Le renvoi vers la page de formation_id se faisait mal
correction: calcul de l'url (sco_import_etuds.py:245) et (scolar.py:1710 celle-ci peut être pas necessaire)
This commit is contained in:
Jean-Marie Place 2021-08-14 10:12:40 +02:00
parent 7372a953fa
commit 432831140c
4 changed files with 59 additions and 43 deletions

View File

@ -366,6 +366,11 @@ def _check_duplicate_code(cnx, args, code_name, context, edit=True, REQUEST=None
raise ScoGenError(err_page) raise ScoGenError(err_page)
def _check_civilite(args):
civilite = args.get("civilite", "X") or "X"
args["civilite"] = input_civilite(civilite) # TODO: A faire valider
def identite_edit(cnx, args, context=None, REQUEST=None): def identite_edit(cnx, args, context=None, REQUEST=None):
"""Modifie l'identite d'un étudiant. """Modifie l'identite d'un étudiant.
Si context et notification et difference, envoie message notification. Si context et notification et difference, envoie message notification.
@ -403,6 +408,7 @@ def identite_create(cnx, args, context=None, REQUEST=None):
"check unique etudid, then create" "check unique etudid, then create"
_check_duplicate_code(cnx, args, "code_nip", context, edit=False, REQUEST=REQUEST) _check_duplicate_code(cnx, args, "code_nip", context, edit=False, REQUEST=REQUEST)
_check_duplicate_code(cnx, args, "code_ine", context, edit=False, REQUEST=REQUEST) _check_duplicate_code(cnx, args, "code_ine", context, edit=False, REQUEST=REQUEST)
_check_civilite(args)
if "etudid" in args: if "etudid" in args:
etudid = args["etudid"] etudid = args["etudid"]
@ -755,7 +761,6 @@ _etud_annotationsEditor = ndb.EditableTable(
output_formators={"comment": safehtml.html_to_safe_html, "date": ndb.DateISOtoDMY}, output_formators={"comment": safehtml.html_to_safe_html, "date": ndb.DateISOtoDMY},
) )
etud_annotations_create = _etud_annotationsEditor.create etud_annotations_create = _etud_annotationsEditor.create
etud_annotations_delete = _etud_annotationsEditor.delete etud_annotations_delete = _etud_annotationsEditor.delete
etud_annotations_list = _etud_annotationsEditor.list etud_annotations_list = _etud_annotationsEditor.list

View File

@ -34,6 +34,7 @@ import time
from enum import Enum from enum import Enum
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import openpyxl.utils.datetime
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
@ -86,40 +87,42 @@ def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE):
# a datetime.time object will be returned. # a datetime.time object will be returned.
# <br>Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number" # <br>Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number"
# is zero. # is zero.
#
_XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01 # _XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01
#
def xldate_as_datetime(xldate, datemode=0): def xldate_as_datetime(xldate, datemode=0):
if datemode not in (0, 1): return openpyxl.utils.datetime.from_ISO8601(xldate)
raise ValueError("invalid mode %s" % datemode) # if datemode not in (0, 1):
if xldate == 0.00: # raise ValueError("invalid mode %s" % datemode)
return datetime.time(0, 0, 0) # if xldate == 0.00:
if xldate < 0.00: # return datetime.time(0, 0, 0)
raise ValueError("invalid date code %s" % xldate) # if xldate < 0.00:
xldays = int(xldate) # raise ValueError("invalid date code %s" % xldate)
frac = xldate - xldays # xldays = int(xldate)
seconds = int(round(frac * 86400.0)) # frac = xldate - xldays
assert 0 <= seconds <= 86400 # seconds = int(round(frac * 86400.0))
if seconds == 86400: # assert 0 <= seconds <= 86400
seconds = 0 # if seconds == 86400:
xldays += 1 # seconds = 0
if xldays >= _XLDAYS_TOO_LARGE[datemode]: # xldays += 1
raise ValueError("date too large %s" % xldate) # if xldays >= _XLDAYS_TOO_LARGE[datemode]:
# raise ValueError("date too large %s" % xldate)
if xldays == 0: #
# second = seconds % 60; minutes = seconds // 60 # if xldays == 0:
minutes, second = divmod(seconds, 60) # # second = seconds % 60; minutes = seconds // 60
# minute = minutes % 60; hour = minutes // 60 # minutes, second = divmod(seconds, 60)
hour, minute = divmod(minutes, 60) # # minute = minutes % 60; hour = minutes // 60
return datetime.time(hour, minute, second) # hour, minute = divmod(minutes, 60)
# return datetime.time(hour, minute, second)
if xldays < 61 and datemode == 0: #
raise ValueError("ambiguous date %s" % xldate) # if xldays < 61 and datemode == 0:
# raise ValueError("ambiguous date %s" % xldate)
return datetime.datetime.fromordinal( #
xldays + 693594 + 1462 * datemode # return datetime.datetime.fromordinal(
) + datetime.timedelta(seconds=seconds) # xldays + 693594 + 1462 * datemode
# ) + datetime.timedelta(seconds=seconds)
class ScoExcelBook: class ScoExcelBook:

View File

@ -32,10 +32,14 @@ import collections
import os import os
import re import re
import time import time
from datetime import date
import flask
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.notes_log import log from app.scodoc.notes_log import log
from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_formsemestre_inscriptions import ( from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules, do_formsemestre_inscription_with_modules,
) )
@ -164,7 +168,7 @@ def sco_import_generate_excel_sample(
If group_ids, liste les etudiants de ces groupes If group_ids, liste les etudiants de ces groupes
""" """
style = sco_excel.excel_make_style(bold=True) style = sco_excel.excel_make_style(bold=True)
style_required = sco_excel.excel_make_style(bold=True, color="red") style_required = sco_excel.excel_make_style(bold=True, color=COLORS.RED)
titles = [] titles = []
titlesStyles = [] titlesStyles = []
for l in fmt: for l in fmt:
@ -214,7 +218,7 @@ def sco_import_generate_excel_sample(
else: else:
lines = [[]] # empty content, titles only lines = [[]] # empty content, titles only
return sco_excel.excel_simple_table( return sco_excel.excel_simple_table(
titles=titles, titlesStyles=titlesStyles, sheet_name="Etudiants", lines=lines titles=titles, titles_styles=titlesStyles, sheet_name="Etudiants", lines=lines
) )
@ -238,7 +242,7 @@ def students_import_excel(
) )
if REQUEST: if REQUEST:
if formsemestre_id: if formsemestre_id:
dest = "formsemestre_status?formsemestre_id=%s" % formsemestre_id dest = "Notes/formsemestre_status?formsemestre_id=%s" % formsemestre_id
else: else:
dest = scu.NotesURL() dest = scu.NotesURL()
H = [html_sco_header.sco_header(page_title="Import etudiants")] H = [html_sco_header.sco_header(page_title="Import etudiants")]
@ -271,12 +275,11 @@ def scolars_import_excel_file(
exceldata = datafile.read() exceldata = datafile.read()
if not exceldata: if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide") raise ScoValueError("Ficher excel vide ou invalide")
diag, data = sco_excel.Excel_to_list(exceldata) diag, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug if not data: # probably a bug
raise ScoException("scolars_import_excel_file: empty file !") raise ScoException("scolars_import_excel_file: empty file !")
formsemestre_to_invalidate = set() formsemestre_to_invalidate = set()
# 1- --- check title line # 1- --- check title line
titles = {} titles = {}
fmt = sco_import_format() fmt = sco_import_format()
@ -378,8 +381,8 @@ def scolars_import_excel_file(
# Excel date conversion: # Excel date conversion:
if scu.strlower(titleslist[i]) == "date_naissance": if scu.strlower(titleslist[i]) == "date_naissance":
if val: if val:
if re.match(r"^[0-9]*\.?[0-9]*$", str(val)): # if re.match(r"^[0-9]*\.?[0-9]*$", str(val)):
val = sco_excel.xldate_as_datetime(float(val)) val = sco_excel.xldate_as_datetime(val)
# INE # INE
if ( if (
scu.strlower(titleslist[i]) == "code_ine" scu.strlower(titleslist[i]) == "code_ine"
@ -625,7 +628,7 @@ def scolars_import_admission(
etuds_by_nomprenom[np] = m etuds_by_nomprenom[np] = m
exceldata = datafile.read() exceldata = datafile.read()
diag2, data = sco_excel.Excel_to_list(exceldata, convert_to_string=False) diag2, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: if not data:
raise ScoException("scolars_import_admission: empty file !") raise ScoException("scolars_import_admission: empty file !")
diag += diag2 diag += diag2

View File

@ -1699,7 +1699,7 @@ def check_group_apogee(
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
@bp.route("/form_students_import_excel") @bp.route("/form_students_import_excel", methods=["GET", "POST"])
@permission_required(Permission.ScoEtudInscrit) @permission_required(Permission.ScoEtudInscrit)
@scodoc7func(context) @scodoc7func(context)
def form_students_import_excel(context, REQUEST, formsemestre_id=None): def form_students_import_excel(context, REQUEST, formsemestre_id=None):
@ -1707,7 +1707,12 @@ def form_students_import_excel(context, REQUEST, formsemestre_id=None):
if formsemestre_id: if formsemestre_id:
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
dest_url = ( dest_url = (
scu.ScoURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id # scu.ScoURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id # TODO: Remplacer par for_url ?
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
else: else:
sem = None sem = None