From 1153bc9a7c41664c3b5376adea0c1affd67d6daf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Apr 2022 13:01:47 +0200 Subject: [PATCH] Trombino: export en doc. Closes #344 --- app/scodoc/sco_trombino.py | 44 ++++++++++++-------- app/scodoc/sco_trombino_doc.py | 76 ++++++++++++++++++++++++++++++++++ app/scodoc/sco_utils.py | 19 +++++++++ 3 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 app/scodoc/sco_trombino_doc.py diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index e9102fc67..9534cfb29 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -55,19 +55,18 @@ from app.scodoc.sco_pdf import SU from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_import_etuds +from app.scodoc import sco_etud from app.scodoc import sco_excel -from app.scodoc import sco_formsemestre -from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_pdf from app.scodoc import sco_photos from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences -from app.scodoc import sco_etud +from app.scodoc import sco_trombino_doc def trombino( - group_ids=[], # liste des groupes à afficher + group_ids=(), # liste des groupes à afficher formsemestre_id=None, # utilisé si pas de groupes selectionné etat=None, format="html", @@ -93,6 +92,8 @@ def trombino( return _trombino_pdf(groups_infos) elif format == "pdflist": return _listeappel_photos_pdf(groups_infos) + elif format == "doc": + return sco_trombino_doc.trombino_doc(groups_infos) else: raise Exception("invalid format") # return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer() @@ -176,8 +177,13 @@ def trombino_html(groups_infos): H.append("") H.append( - '
Version PDF
' - % groups_infos.groups_query_args + f"""
+ Version PDF +    + Version doc +
""" ) return "\n".join(H) @@ -234,7 +240,7 @@ def _trombino_zip(groups_infos): Z.writestr(filename, img) Z.close() size = data.tell() - log("trombino_zip: %d bytes" % size) + log(f"trombino_zip: {size} bytes") data.seek(0) return send_file( data, @@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos): # --------------------- Upload des photos de tout un groupe -def photos_generate_excel_sample(group_ids=[]): +def photos_generate_excel_sample(group_ids=()): """Feuille excel pour import fichiers photos""" fmt = sco_import_etuds.sco_import_format() data = sco_import_etuds.sco_import_generate_excel_sample( @@ -492,31 +498,33 @@ def photos_generate_excel_sample(group_ids=[]): # return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX) -def photos_import_files_form(group_ids=[]): +def photos_import_files_form(group_ids=()): """Formulaire pour importation photos""" if not group_ids: raise ScoValueError("paramètre manquant !") groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) - back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args + back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos" H = [ html_sco_header.sco_header(page_title="Import des photos des étudiants"), - """

Téléchargement des photos des étudiants

-

Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").

-

Cette page permet de charger en une seule fois les photos de plusieurs étudiants.
- Il faut d'abord remplir une feuille excel donnant les noms + f"""

Téléchargement des photos des étudiants

+

Vous pouvez aussi charger les photos individuellement via la fiche + de chaque étudiant (menu "Etudiant" / "Changer la photo"). +

+

Cette page permet de charger en une seule fois les photos + de plusieurs étudiants.
+ Il faut d'abord remplir une feuille excel donnant les noms des fichiers images (une image par étudiant).

-

Ensuite, réunir vos images dans un fichier zip, puis télécharger +

Ensuite, réunir vos images dans un fichier zip, puis télécharger simultanément le fichier excel et le fichier zip.

    -
  1. +
  2. Obtenir la feuille excel à remplir
  3. - """ - % groups_infos.groups_query_args, + """, ] F = html_sco_header.sco_footer() vals = scu.get_request_args() diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py new file mode 100644 index 000000000..038832e5a --- /dev/null +++ b/app/scodoc/sco_trombino_doc.py @@ -0,0 +1,76 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Génération d'un trombinoscope en doc +""" + +import docx +from docx.shared import Mm +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_ALIGN_VERTICAL + +from app.scodoc import sco_etud +from app.scodoc import sco_photos +import app.scodoc.sco_utils as scu +import sco_version + + +def trombino_doc(groups_infos): + "Send photos as docx document" + filename = f"trombino_{groups_infos.groups_filename}.docx" + sem = groups_infos.formsemestre # suppose 1 seul semestre + PHOTO_WIDTH = Mm(25) + N_PER_ROW = 5 # XXX should be in ScoDoc preferences + + document = docx.Document() + document.add_heading( + f"Trombinoscope {sem['titreannee']} {groups_infos.groups_titles}", 1 + ) + section = document.sections[0] + footer = section.footer + footer.paragraphs[ + 0 + ].text = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}" + + nb_images = len(groups_infos.members) + table = document.add_table(rows=2 * (nb_images // N_PER_ROW + 1), cols=N_PER_ROW) + table.allow_autofit = False + + for i, t in enumerate(groups_infos.members): + li = i // N_PER_ROW + co = i % N_PER_ROW + img_path = ( + sco_photos.photo_pathname(t["photo_filename"], size="small") + or sco_photos.UNKNOWN_IMAGE_PATH + ) + cell = table.rows[2 * li].cells[co] + cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP + cell_p, cell_f, cell_r = _paragraph_format_run(cell) + cell_r.add_picture(img_path, width=PHOTO_WIDTH) + + # le nom de l'étudiant: cellules de lignes impaires + cell = table.rows[2 * li + 1].cells[co] + cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP + cell_p, cell_f, cell_r = _paragraph_format_run(cell) + cell_r.add_text(sco_etud.format_nomprenom(t)) + cell_f.space_after = Mm(8) + + return scu.send_docx(document, filename) + + +def _paragraph_format_run(cell): + "parag. dans cellule tableau" + # inspired by https://stackoverflow.com/questions/64218305/problem-with-python-docx-putting-pictures-in-a-table + paragraph = cell.paragraphs[0] + fmt = paragraph.paragraph_format + run = paragraph.add_run() + + fmt.space_before = Mm(0) + fmt.space_after = Mm(0) + fmt.line_spacing = 1.0 + fmt.alignment = WD_ALIGN_PARAGRAPH.CENTER + + return paragraph, fmt, run diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 0a4da781a..8cba3a59c 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -33,6 +33,7 @@ import bisect import copy import datetime from enum import IntEnum +import io import json from hashlib import md5 import numbers @@ -49,6 +50,7 @@ from PIL import Image as PILImage import pydot import requests +import flask from flask import g, request from flask import flash, url_for, make_response, jsonify @@ -379,6 +381,10 @@ CSV_FIELDSEP = ";" CSV_LINESEP = "\n" CSV_MIMETYPE = "text/comma-separated-values" CSV_SUFFIX = ".csv" +DOCX_MIMETYPE = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" +) +DOCX_SUFFIX = ".docx" JSON_MIMETYPE = "application/json" JSON_SUFFIX = ".json" PDF_MIMETYPE = "application/pdf" @@ -398,6 +404,7 @@ def get_mime_suffix(format_code: str) -> tuple[str, str]: """ d = { "csv": (CSV_MIMETYPE, CSV_SUFFIX), + "docx": (DOCX_MIMETYPE, DOCX_SUFFIX), "xls": (XLSX_MIMETYPE, XLSX_SUFFIX), "xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX), "pdf": (PDF_MIMETYPE, PDF_SUFFIX), @@ -740,6 +747,18 @@ def send_file(data, filename="", suffix="", mime=None, attached=None): return response +def send_docx(document, filename): + "Send a python-docx document" + buffer = io.BytesIO() # in-memory document, no disk file + document.save(buffer) + buffer.seek(0) + return flask.send_file( + buffer, + attachment_filename=sanitize_filename(filename), + mimetype=DOCX_MIMETYPE, + ) + + def get_request_args(): """returns a dict with request (POST or GET) arguments converted to suit legacy Zope style (scodoc7) functions.