feat: add csv import

This commit is contained in:
2025-09-02 06:16:14 +02:00
parent 87ff0d22c8
commit f1ae9faef8
6 changed files with 795 additions and 17 deletions

View File

@@ -1,8 +1,10 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app, abort
from werkzeug.utils import secure_filename
from models import db, ClassGroup, Student, Assessment, StudentEnrollment
from forms import ClassGroupForm
from utils import handle_db_errors, ValidationError
from repositories.class_repository import ClassRepository
from services.csv_import_service import CSVImportService
from datetime import date, datetime
bp = Blueprint('classes', __name__, url_prefix='/classes')
@@ -793,4 +795,105 @@ def cancel_departure():
db.session.rollback()
current_app.logger.error(f'Erreur annulation départ: {e}')
flash('Erreur lors de l\'annulation du départ', 'error')
return redirect(request.referrer or url_for('classes'))
return redirect(request.referrer or url_for('classes'))
@bp.route('/<int:id>/import-students-csv', methods=['POST'])
@handle_db_errors
def import_students_csv(id):
"""Importe plusieurs élèves depuis un fichier CSV dans une classe."""
class_repo = ClassRepository()
class_group = class_repo.get_or_404(id)
try:
# Vérifier qu'un fichier a été envoyé
if 'csv_file' not in request.files:
if request.is_json:
return jsonify({'success': False, 'error': 'Aucun fichier fourni'}), 400
flash('Aucun fichier sélectionné', 'error')
return redirect(request.referrer or url_for('classes.students', id=id))
csv_file = request.files['csv_file']
if csv_file.filename == '':
if request.is_json:
return jsonify({'success': False, 'error': 'Aucun fichier sélectionné'}), 400
flash('Aucun fichier sélectionné', 'error')
return redirect(request.referrer or url_for('classes.students', id=id))
# Vérifier l'extension du fichier
if not csv_file.filename.lower().endswith('.csv'):
if request.is_json:
return jsonify({'success': False, 'error': 'Le fichier doit être au format CSV'}), 400
flash('Le fichier doit être au format CSV', 'error')
return redirect(request.referrer or url_for('classes.students', id=id))
# Lire le contenu du fichier
csv_content = csv_file.read().decode('utf-8-sig') # utf-8-sig pour gérer BOM
if not csv_content.strip():
if request.is_json:
return jsonify({'success': False, 'error': 'Le fichier CSV est vide'}), 400
flash('Le fichier CSV est vide', 'error')
return redirect(request.referrer or url_for('classes.students', id=id))
# Paramètres d'import
enrollment_date_str = request.form.get('enrollment_date')
if enrollment_date_str:
enrollment_date = datetime.strptime(enrollment_date_str, '%Y-%m-%d').date()
else:
enrollment_date = date.today()
skip_duplicates = request.form.get('skip_duplicates', 'true').lower() == 'true'
# Lancer l'import via le service
csv_service = CSVImportService()
result = csv_service.import_students_to_class(
csv_content=csv_content,
class_group_id=id,
enrollment_date=enrollment_date,
skip_duplicates=skip_duplicates
)
current_app.logger.info(f'Import CSV classe {id}: {result.imported_count} importés, {result.skipped_count} ignorés, {result.error_count} erreurs')
if request.is_json:
return jsonify({
'success': result.success,
'message': result.message,
'stats': {
'total_lines': result.total_lines,
'imported_count': result.imported_count,
'skipped_count': result.skipped_count,
'error_count': result.error_count
},
'imported_students': [
{
'name': f"{s.first_name} {s.last_name}",
'line': s.line_number,
'raw_name': s.raw_name
} for s in result.imported_students
],
'skipped_students': result.skipped_students,
'errors': result.errors
})
else:
if result.success:
if result.imported_count > 0:
flash(f'{result.imported_count} élève(s) importé(s) avec succès en {class_group.name}', 'success')
if result.skipped_count > 0:
flash(f'{result.skipped_count} élève(s) ignoré(s) (doublons)', 'info')
else:
flash('Aucun élève importé', 'info')
else:
flash(f'Erreur lors de l\'import: {result.message}', 'error')
return redirect(url_for('classes.students', id=id) + '?reload=1')
except Exception as e:
current_app.logger.error(f'Erreur import CSV classe {id}: {e}')
if request.is_json:
return jsonify({'success': False, 'error': 'Erreur lors de l\'import du fichier CSV'}), 500
else:
flash('Erreur lors de l\'import du fichier CSV', 'error')
return redirect(request.referrer or url_for('classes.students', id=id))