forked from ScoDoc/ScoDoc
368 lines
13 KiB
Python
368 lines
13 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
#
|
|
##############################################################################
|
|
|
|
"""ScoDoc : gestion des fichiers archivés associés aux étudiants
|
|
Il s'agit de fichiers quelconques, généralement utilisés pour conserver
|
|
les dossiers d'admission et autres pièces utiles.
|
|
"""
|
|
import flask
|
|
from flask import flash, render_template, url_for
|
|
from flask import g, request
|
|
from flask_login import current_user
|
|
|
|
from app.models import Identite
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc import sco_import_etuds
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_trombino
|
|
from app.scodoc import sco_archives
|
|
from app.scodoc.sco_permissions import Permission
|
|
from app.scodoc.sco_exceptions import AccessDenied
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
|
|
|
|
class EtudsArchiver(sco_archives.BaseArchiver):
|
|
def __init__(self):
|
|
sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
|
|
|
|
|
|
# Global au processus, attention !
|
|
ETUDS_ARCHIVER = EtudsArchiver()
|
|
|
|
|
|
def can_edit_etud_archive(user):
|
|
"""True si l'utilisateur peut modifier les archives etudiantes"""
|
|
if user.passwd_must_be_changed:
|
|
return False
|
|
return user.has_permission(Permission.EtudAddAnnotations)
|
|
|
|
|
|
def etud_list_archives_html(etud: Identite):
|
|
"""HTML snippet listing archives."""
|
|
can_edit = can_edit_etud_archive(current_user)
|
|
etud_archive_id = etud.id
|
|
L = []
|
|
for archive_id in ETUDS_ARCHIVER.list_obj_archives(
|
|
etud_archive_id, dept_id=etud.dept_id
|
|
):
|
|
a = {
|
|
"archive_id": archive_id,
|
|
"description": ETUDS_ARCHIVER.get_archive_description(
|
|
archive_id, dept_id=etud.dept_id
|
|
),
|
|
"date": ETUDS_ARCHIVER.get_archive_date(archive_id),
|
|
"content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id),
|
|
}
|
|
L.append(a)
|
|
delete_icon = scu.icontag(
|
|
"delete_small_img", title="Supprimer fichier", alt="supprimer"
|
|
)
|
|
delete_disabled_icon = scu.icontag(
|
|
"delete_small_dis_img", title="Suppression non autorisée"
|
|
)
|
|
H = ['<div class="etudarchive"><ul>']
|
|
for a in L:
|
|
archive_name = ETUDS_ARCHIVER.get_archive_name(a["archive_id"])
|
|
H.append(
|
|
"""<li><span class ="etudarchive_descr" title="%s">%s</span>"""
|
|
% (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"])
|
|
)
|
|
for filename in a["content"]:
|
|
H.append(
|
|
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
|
|
% (etud.id, archive_name, filename, filename)
|
|
)
|
|
if not a["content"]:
|
|
H.append("<em>aucun fichier !</em>")
|
|
if can_edit:
|
|
H.append(
|
|
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
|
|
% (etud.id, archive_name, delete_icon)
|
|
)
|
|
else:
|
|
H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
|
|
H.append("</li>")
|
|
if can_edit:
|
|
H.append(
|
|
'<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>'
|
|
% etud.id
|
|
)
|
|
H.append("</ul></div>")
|
|
return "".join(H)
|
|
|
|
|
|
def add_archives_info_to_etud_list(etuds):
|
|
"""Add key 'etudarchive' describing archive of etuds
|
|
(used to list all archives of a group)
|
|
"""
|
|
for etud in etuds:
|
|
l = []
|
|
etud_archive_id = etud["etudid"]
|
|
# Here, ETUDS_ARCHIVER will use g.dept_id
|
|
for archive_id in ETUDS_ARCHIVER.list_obj_archives(etud_archive_id):
|
|
l.append(
|
|
"%s (%s)"
|
|
% (
|
|
ETUDS_ARCHIVER.get_archive_description(archive_id),
|
|
ETUDS_ARCHIVER.list_archive(archive_id)[0],
|
|
)
|
|
)
|
|
etud["etudarchive"] = ", ".join(l)
|
|
|
|
|
|
def etud_upload_file_form(etudid):
|
|
"""Page with a form to choose and upload a file, with a description."""
|
|
# check permission
|
|
if not can_edit_etud_archive(current_user):
|
|
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
|
etud = Identite.get_etud(etudid)
|
|
|
|
H = [
|
|
f"""<h2>Chargement d'un document associé à {etud.nomprenom}</h2>
|
|
|
|
<p>Le fichier ne doit pas dépasser {
|
|
scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)}Mo.</p>
|
|
""",
|
|
]
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
(
|
|
("etudid", {"default": etudid, "input_type": "hidden"}),
|
|
("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
|
|
(
|
|
"description",
|
|
{
|
|
"input_type": "textarea",
|
|
"rows": 4,
|
|
"cols": 77,
|
|
"title": "Description",
|
|
},
|
|
),
|
|
),
|
|
submitlabel="Valider",
|
|
cancelbutton="Annuler",
|
|
)
|
|
if tf[0] == 0:
|
|
return render_template(
|
|
"sco_page.j2",
|
|
title=f"Chargement d'un document associé à {etud.nomprenom}",
|
|
content="\n".join(H) + tf[1],
|
|
)
|
|
if tf[0] == -1:
|
|
return flask.redirect(etud.url_fiche())
|
|
data = tf[2]["datafile"].read()
|
|
descr = tf[2]["description"]
|
|
filename = tf[2]["datafile"].filename
|
|
_store_etud_file_to_new_archive(etudid, data, filename, description=descr)
|
|
return flask.redirect(etud.url_fiche())
|
|
|
|
|
|
def _store_etud_file_to_new_archive(
|
|
etud_archive_id, data, filename, description=""
|
|
) -> tuple[bool, str]:
|
|
"""Store data to new archive."""
|
|
filesize = len(data)
|
|
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
|
|
return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
|
|
archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description)
|
|
ETUDS_ARCHIVER.store(archive_id, filename, data)
|
|
return True, "ok"
|
|
|
|
|
|
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
|
"""Delete an archive"""
|
|
# check permission
|
|
if not can_edit_etud_archive(current_user):
|
|
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
|
etud = Identite.get_etud(etudid)
|
|
etud_archive_id = etudid
|
|
archive_id = ETUDS_ARCHIVER.get_id_from_name(
|
|
etud_archive_id, archive_name, dept_id=etud.dept_id
|
|
)
|
|
if not dialog_confirmed:
|
|
return scu.confirm_dialog(
|
|
f"""<h2>Confirmer la suppression des fichiers ?</h2>
|
|
<p>Fichier associé le {
|
|
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
|
|
} à l'étudiant {etud.nomprenom}
|
|
</p>
|
|
<p>La suppression sera définitive.</p>
|
|
""",
|
|
dest_url="",
|
|
cancel_url=url_for(
|
|
"scolar.fiche_etud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
etudid=etudid,
|
|
),
|
|
parameters={"etudid": etudid, "archive_name": archive_name},
|
|
)
|
|
|
|
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud.dept_id)
|
|
flash("Archive supprimée")
|
|
return flask.redirect(etud.url_fiche())
|
|
|
|
|
|
def etud_get_archived_file(etudid, archive_name, filename):
|
|
"""Send file to client."""
|
|
etud = Identite.get_etud(etudid)
|
|
etud_archive_id = etud.id
|
|
return ETUDS_ARCHIVER.get_archived_file(
|
|
etud_archive_id, archive_name, filename, dept_id=etud.dept_id
|
|
)
|
|
|
|
|
|
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
|
|
def etudarchive_generate_excel_sample(group_id=None):
|
|
"""Feuille excel pour import fichiers etudiants (utilisé pour admissions)"""
|
|
fmt = sco_import_etuds.sco_import_format()
|
|
data = sco_import_etuds.sco_import_generate_excel_sample(
|
|
fmt,
|
|
group_ids=[group_id],
|
|
only_tables=["identite"],
|
|
exclude_cols=[
|
|
"date_naissance",
|
|
"lieu_naissance",
|
|
"nationalite",
|
|
"statut",
|
|
"photo_filename",
|
|
],
|
|
extra_cols=["fichier_a_charger"],
|
|
)
|
|
return scu.send_file(
|
|
data,
|
|
"ImportFichiersEtudiants",
|
|
suffix=scu.XLSX_SUFFIX,
|
|
mime=scu.XLSX_MIMETYPE,
|
|
)
|
|
|
|
|
|
def etudarchive_import_files_form(group_id):
|
|
"""Formulaire pour importation fichiers d'un groupe"""
|
|
H = [
|
|
"""<h2 class="formsemestre">Téléchargement de fichier associés aux étudiants</h2>
|
|
<p>Les fichiers associés (dossiers d'admission, certificats, ...), de
|
|
types quelconques (pdf, doc, images) sont accessibles aux utilisateurs via
|
|
la fiche individuelle de l'étudiant.
|
|
</p>
|
|
<p class="warning">Ne pas confondre avec les photos des étudiants, qui se
|
|
chargent via l'onglet "Photos".</p>
|
|
<p><b>Vous pouvez aussi charger à tout moment de nouveaux fichiers, ou en
|
|
supprimer, via la fiche de chaque étudiant.</b>
|
|
</p>
|
|
<p class="help">Cette page permet de charger en une seule fois les fichiers
|
|
de plusieurs étudiants.<br>
|
|
Il faut d'abord remplir une feuille excel donnant les noms
|
|
des fichiers (un fichier par étudiant).
|
|
</p>
|
|
<p class="help">Ensuite, réunir vos fichiers dans un fichier zip, puis
|
|
télécharger simultanément le fichier excel et le fichier zip.
|
|
</p>
|
|
<ol>
|
|
<li><a class="stdlink" href="etudarchive_generate_excel_sample?group_id=%s">
|
|
Obtenir la feuille excel à remplir</a>
|
|
</li>
|
|
<li style="padding-top: 2em;">
|
|
"""
|
|
% group_id,
|
|
]
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
(
|
|
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
|
|
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
|
|
(
|
|
"description",
|
|
{
|
|
"input_type": "textarea",
|
|
"rows": 4,
|
|
"cols": 77,
|
|
"title": "Description",
|
|
},
|
|
),
|
|
("group_id", {"input_type": "hidden"}),
|
|
),
|
|
)
|
|
|
|
if tf[0] == 0:
|
|
return render_template(
|
|
"sco_page.j2",
|
|
title="Import de fichiers associés aux étudiants",
|
|
content="\n".join(H) + tf[1] + "</li></ol>",
|
|
)
|
|
# 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",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=group["formsemestre_id"],
|
|
)
|
|
)
|
|
else:
|
|
return etudarchive_import_files(
|
|
formsemestre_id=group["formsemestre_id"],
|
|
xlsfile=tf[2]["xlsfile"],
|
|
zipfile=tf[2]["zipfile"],
|
|
description=tf[2]["description"],
|
|
)
|
|
|
|
|
|
def etudarchive_import_files(
|
|
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
|
|
):
|
|
"Importe des fichiers"
|
|
|
|
def callback(etud: Identite, data, filename):
|
|
return _store_etud_file_to_new_archive(etud.id, data, filename, description)
|
|
|
|
# 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.j2",
|
|
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_feuilles",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
),
|
|
)
|