Trombino: export en doc. Closes #344

This commit is contained in:
Emmanuel Viennet 2022-04-08 13:01:47 +02:00
parent 6ac096d29d
commit 1153bc9a7c
3 changed files with 121 additions and 18 deletions

View File

@ -55,19 +55,18 @@ from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_etud
from app.scodoc import sco_excel 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_groups_view
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_trombino_doc
def trombino( def trombino(
group_ids=[], # liste des groupes à afficher group_ids=(), # liste des groupes à afficher
formsemestre_id=None, # utilisé si pas de groupes selectionné formsemestre_id=None, # utilisé si pas de groupes selectionné
etat=None, etat=None,
format="html", format="html",
@ -93,6 +92,8 @@ def trombino(
return _trombino_pdf(groups_infos) return _trombino_pdf(groups_infos)
elif format == "pdflist": elif format == "pdflist":
return _listeappel_photos_pdf(groups_infos) return _listeappel_photos_pdf(groups_infos)
elif format == "doc":
return sco_trombino_doc.trombino_doc(groups_infos)
else: else:
raise Exception("invalid format") raise Exception("invalid format")
# return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer() # return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer()
@ -176,8 +177,13 @@ def trombino_html(groups_infos):
H.append("</div>") H.append("</div>")
H.append( H.append(
'<div style="margin-bottom:15px;"><a class="stdlink" href="trombino?format=pdf&%s">Version PDF</a></div>' f"""<div style="margin-bottom:15px;">
% groups_infos.groups_query_args <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
&nbsp;&nbsp;
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
</div>"""
) )
return "\n".join(H) return "\n".join(H)
@ -234,7 +240,7 @@ def _trombino_zip(groups_infos):
Z.writestr(filename, img) Z.writestr(filename, img)
Z.close() Z.close()
size = data.tell() size = data.tell()
log("trombino_zip: %d bytes" % size) log(f"trombino_zip: {size} bytes")
data.seek(0) data.seek(0)
return send_file( return send_file(
data, data,
@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos):
# --------------------- Upload des photos de tout un groupe # --------------------- 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""" """Feuille excel pour import fichiers photos"""
fmt = sco_import_etuds.sco_import_format() fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample( data = sco_import_etuds.sco_import_generate_excel_sample(
@ -492,18 +498,21 @@ def photos_generate_excel_sample(group_ids=[]):
# return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX) # 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""" """Formulaire pour importation photos"""
if not group_ids: if not group_ids:
raise ScoValueError("paramètre manquant !") raise ScoValueError("paramètre manquant !")
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) 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 = [ H = [
html_sco_header.sco_header(page_title="Import des photos des étudiants"), html_sco_header.sco_header(page_title="Import des photos des étudiants"),
"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2> f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").</b></p> <p><b>Vous pouvez aussi charger les photos individuellement via la fiche
<p class="help">Cette page permet de charger en une seule fois les photos de plusieurs étudiants.<br/> de chaque étudiant (menu "Etudiant" / "Changer la photo").</b>
</p>
<p class="help">Cette page permet de charger en une seule fois les photos
de plusieurs étudiants.<br/>
Il faut d'abord remplir une feuille excel donnant les noms Il faut d'abord remplir une feuille excel donnant les noms
des fichiers images (une image par étudiant). des fichiers images (une image par étudiant).
</p> </p>
@ -511,12 +520,11 @@ def photos_import_files_form(group_ids=[]):
simultanément le fichier excel et le fichier zip. simultanément le fichier excel et le fichier zip.
</p> </p>
<ol> <ol>
<li><a class="stdlink" href="photos_generate_excel_sample?%s"> <li><a class="stdlink" href="photos_generate_excel_sample?{groups_infos.groups_query_args}">
Obtenir la feuille excel à remplir</a> Obtenir la feuille excel à remplir</a>
</li> </li>
<li style="padding-top: 2em;"> <li style="padding-top: 2em;">
""" """,
% groups_infos.groups_query_args,
] ]
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
vals = scu.get_request_args() vals = scu.get_request_args()

View File

@ -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

View File

@ -33,6 +33,7 @@ import bisect
import copy import copy
import datetime import datetime
from enum import IntEnum from enum import IntEnum
import io
import json import json
from hashlib import md5 from hashlib import md5
import numbers import numbers
@ -49,6 +50,7 @@ from PIL import Image as PILImage
import pydot import pydot
import requests import requests
import flask
from flask import g, request from flask import g, request
from flask import flash, url_for, make_response, jsonify from flask import flash, url_for, make_response, jsonify
@ -379,6 +381,10 @@ CSV_FIELDSEP = ";"
CSV_LINESEP = "\n" CSV_LINESEP = "\n"
CSV_MIMETYPE = "text/comma-separated-values" CSV_MIMETYPE = "text/comma-separated-values"
CSV_SUFFIX = ".csv" CSV_SUFFIX = ".csv"
DOCX_MIMETYPE = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
DOCX_SUFFIX = ".docx"
JSON_MIMETYPE = "application/json" JSON_MIMETYPE = "application/json"
JSON_SUFFIX = ".json" JSON_SUFFIX = ".json"
PDF_MIMETYPE = "application/pdf" PDF_MIMETYPE = "application/pdf"
@ -398,6 +404,7 @@ def get_mime_suffix(format_code: str) -> tuple[str, str]:
""" """
d = { d = {
"csv": (CSV_MIMETYPE, CSV_SUFFIX), "csv": (CSV_MIMETYPE, CSV_SUFFIX),
"docx": (DOCX_MIMETYPE, DOCX_SUFFIX),
"xls": (XLSX_MIMETYPE, XLSX_SUFFIX), "xls": (XLSX_MIMETYPE, XLSX_SUFFIX),
"xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX), "xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX),
"pdf": (PDF_MIMETYPE, PDF_SUFFIX), "pdf": (PDF_MIMETYPE, PDF_SUFFIX),
@ -740,6 +747,18 @@ def send_file(data, filename="", suffix="", mime=None, attached=None):
return response 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(): def get_request_args():
"""returns a dict with request (POST or GET) arguments """returns a dict with request (POST or GET) arguments
converted to suit legacy Zope style (scodoc7) functions. converted to suit legacy Zope style (scodoc7) functions.